feat(cli): summarize managed members with durable Evidence paging (#5167)

Fixes #5148.

Add opt-in durable managed member summaries and authenticated paging while preserving complete default output, original Evidence, cleanup ownership and queued cancellation.

Verified with bounded real Newtonsoft/Redux/Rust tasks, independent oracles, 556 focused tests and the expanded domain gate; paired token estimates and latency tradeoffs are recorded in experiments/analysis-efficiency/managed-cli.md.
This commit is contained in:
Jay-Alexander Elliot authored and GitHub committed 2026-10-22 00:43:30 +00:00
1 parent d8948efb81
commit 57fd3ea5d8
48 files changed
+4671 -196

No files matched your search

+16 -1
View File
@@ -63,6 +63,16 @@ or repeat it to select several types.
Selection limits decoding to the selected types; member references remain an
assembly-wide inventory, so the output can still be large.
Select `--detail summary --evidence-bundle /absolute/path/members.json` to
receive identity, selection, coverage, limitations and collection counts while
saving the complete original Evidence in a new bundle. Summary requires that
explicit destination; existing files are preserved. Later
`inspect-analysis-view` calls can page the exact parent from the saved bundle
without reopening the assembly. See
[CLI summary and saved member rows](managed-code-analysis.md#cli-summary-and-saved-member-rows)
for the complete command sequence. Omitted or `--detail complete` retains the
full inline response.
Choose the workflow from the artifact's contents rather than its `.exe` or
`.dll` extension. The managed parser requires PE/CLI metadata; it does not
unpack .NET single-file hosts or decode IL2CPP. For the image, type and method
@@ -279,7 +289,12 @@ Exports preserve an existing destination unless `--overwrite` is explicit.
For application traces and version comparisons, see
[JavaScript application workflows](javascript-application-workflows.md).
MCP [retained Evidence references](mcp-contracts.md#retained-application-evidence-inputs)
belong to one connection. Separate CLI calls consume full saved records.
belong to one connection. Separate CLI calls can consume full saved records.
`inspect-analysis-view REQUEST.json --evidence-bundle BUNDLE.json` resolves
the request's `retained-evidence` source ID from that authenticated bundle,
including bundles exported from an MCP session. The same view JSON works for
every supported parent kind; no target is reopened and no record is selected
by file order. Omit `--evidence-bundle` when the request supplies inline Evidence.
## Import historical source
+44 -1
View File
@@ -85,7 +85,7 @@ rejects selected summary before analysis; complete delivery remains available.
Complete `inspect_managed_members` delivery always includes the assembly-wide
`member_refs` and `exported_types`, which `type_names` does not narrow, so even
one selected or unmatched type can return megabytes. Select
one selected or unmatched type can return megabytes. In MCP, select
`{"path":"/absolute/path/Assembly.dll","type_names":["Namespace.Type"],"detail":"summary"}`
to keep the complete source in the session and receive identity, metadata
status, the selection with its unmatched names, coverage/issues, the separate
@@ -115,6 +115,49 @@ literal pool's own coverage. Older Evidence that never examined exported types
or user strings reports a null count and refuses those pages rather than
returning an empty one.
#### CLI summary and saved member rows
The CLI accepts the same `complete` and `summary` detail choices. Omitted or
`--detail complete` returns complete Evidence. To receive a summary, choose a
new Evidence bundle destination so its original complete parent remains
available after the command exits:
```bash
rea inspect-managed-members /absolute/path/Assembly.dll --type Namespace.Type \
--detail summary --evidence-bundle /absolute/path/evidence/members.json --json
```
`--evidence-bundle` writes the complete original Evidence in REA's canonical
bundle format. Its parent directory must exist, and it never replaces an
existing path. Relative paths resolve against the command's working directory.
The option also works with complete delivery when a saved bundle is useful.
Summary without a destination is rejected before analysis. A publication failure
returns a typed error with the completed Evidence under
`details.partial_observation`; it does not report successful summary delivery.
Cancellation before publication prevents the new output. Once atomic
publication has started, it can complete despite cancellation; a cancelled
delivery then retains the committed path and byte count under
`details.diagnostics.evidence_bundle`.
Copy `normalized_result.parent_evidence_id` from the summary into the
`source.evidence_id` field of the `managed-members` request above, then save that
small request as `members-page.json` and run:
```bash
rea inspect-analysis-view /absolute/path/evidence/members-page.json \
--evidence-bundle /absolute/path/evidence/members.json --json
```
The view command authenticates the selected bundle and resolves that exact
parent ID. It does not reopen the assembly, so the original target may have
been moved or removed. Subsequent calls can select another collection or use
the returned `coverage.next_offset` to continue the same collection. An inline
source already includes its parent and cannot also select `--evidence-bundle`.
Summary changes delivery, while type selection still controls decoding and the
same inspection resource limits apply. Each separate CLI page reads and
authenticates the saved bundle; the full source is retained on disk.
## Shipped scope
The canonical parser admits PE/CLI bytes and their metadata/CIL without loading
@@ -0,0 +1,84 @@
"""Summarize completed CLI trials; token counts are estimates, not model usage."""
import argparse
import hashlib
import json
import statistics
from pathlib import Path
import tiktoken
parser = argparse.ArgumentParser()
parser.add_argument('root', type=Path)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
root = args.root
inputs = json.loads((root / 'measurement-inputs.json').read_text())
expected = inputs['expected_runtime_bindings']
assert expected['complete']['git_revision'] == inputs['baseline_revision']
assert expected['selected']['git_revision'] == inputs['feature_parent_revision']
assert expected['complete']['root'] != expected['selected']['root']
assert expected['complete']['compiled_cli_sha256'] != expected['selected']['compiled_cli_sha256']
assert expected['complete']['source_diff_sha256'] == hashlib.sha256(b'').hexdigest()
assert expected['complete']['untracked_source_sha256'] == hashlib.sha256(b'[]').hexdigest()
enc = tiktoken.get_encoding('cl100k_base')
count = lambda text: len(enc.encode(text, disallowed_special=()))
compact = lambda obj: json.dumps(obj, ensure_ascii=False, separators=(',', ':'))
runs = []
for case in sorted(root.glob('pair-*')):
if not case.is_dir():
continue
report = json.loads((case / 'report.json').read_text())
assert report['status'] == 'passed' and report['cleanup_verified']
assert report['runtime_binding_verified']
assert report['runtime_binding'] == expected[report['mode']], case.name
assert str(Path(report['runtime']).resolve()) == report['runtime_binding']['root']
receipt = next(json.loads(line) for line in reversed(case.with_name(case.name + '.log').read_text().splitlines()) if line.startswith('{"resource_scope":'))
obs = receipt['payload_resource_observation']
assert receipt['abort'] is None and receipt['payload_resource_observation_verified']
assert obs['payload_exit_code'] == 0 and obs['payload_cleanup_verified']
assert obs['resource_events']['memory.events']['oom_kill'] == 0
tokens = {'question_and_recipe': count(report['question'] + '\n' + report['recipe']),
'arguments': sum(count(compact(call['arguments'])) for call in report['calls']),
'help_and_replies': sum(count((case / call['stdout_file']).read_text()) for call in report['calls'])}
tokens['whole_task'] = sum(tokens.values())
runs.append({'case': case.name, 'mode': report['mode'], 'cache': report['cache'], 'runtime': report['runtime'], 'runtime_binding': report['runtime_binding'], 'runtime_binding_audit_ms': report['runtime_binding_audit_ms'],
'wall_seconds': receipt['elapsed_seconds'], 'cpu_seconds': obs['cpu_stat']['usage_usec'] / 1e6,
'peak_process_family_bytes_including_page_cache': obs['memory_peak_bytes'],
'tokens_estimated': tokens, 'calls': report['calls'],
'parent_evidence_id': report['parent_evidence_id'], 'parent_semantic_sha256': report['parent_semantic_sha256'],
'answer_sha256': report['answer_sha256'], 'oracle_verified': report['oracle_verified'],
'cleanup_verified': True, 'resource_events': obs['resource_events']})
assert len(runs) == 12
for key in ['answer_sha256', 'parent_semantic_sha256', 'parent_evidence_id']:
assert len({row[key] for row in runs}) == 1, key
summary = {}
for cache in ['cold', 'warm']:
summary[cache] = {}
for mode in ['complete', 'selected']:
rows = [r for r in runs if r['mode'] == mode and r['cache'] == cache]
assert len(rows) == 3
summary[cache][mode] = {key: {'median': statistics.median(r[key] for r in rows), 'min': min(r[key] for r in rows), 'max': max(r[key] for r in rows)} for key in ['wall_seconds', 'cpu_seconds', 'peak_process_family_bytes_including_page_cache']}
summary[cache][mode]['whole_task_tokens_estimated'] = [r['tokens_estimated']['whole_task'] for r in rows]
result = {'status': 'passed', 'tokenizer': 'tiktoken==0.12.0/cl100k_base; estimates, not model usage',
'context_scope': 'Question, supplied command recipe, actual selected-command help, all logical CLI argv and all stdout. No model invoked; reasoning, framing, irrelevant tool discovery and actual native-agent usage unmeasured.',
'cache_scope': 'cold disables Node compile cache; warm primes the identical workflow once per runtime/mode before timing. Fresh processes and isolated HOME each trial; OS page cache uncontrolled.',
'resource_scope': 'Whole task driver plus all CLI children; includes pre/post source-and-compiled-runtime binding verification, bundle publication/authentication, help, extra calls and local answer/oracle checking. Kernel cgroup peak includes file cache; not sampled RSS. Download/build/oracle preparation and priming excluded.',
'acquisition': json.loads((root / 'acquisition.json').read_text()), 'measurement_inputs': inputs,
'summary': summary, 'runs': runs}
native = root / 'native-control'
if native.exists():
binding = json.loads((native / 'runtime-binding.json').read_text())
assert binding['runtime_binding_verified'] and binding['runtime_binding'] == expected['complete']
assert str(Path(binding['runtime']).resolve()) == binding['runtime_binding']['root']
receipt = next(json.loads(line) for line in reversed((root / 'native-control.log').read_text().splitlines()) if line.startswith('{"resource_scope":'))
obs = receipt['payload_resource_observation']
assert receipt['abort'] is None and receipt['payload_resource_observation_verified'] and obs['payload_exit_code'] == 0 and obs['payload_cleanup_verified']
baseline_case = root / next(run['case'] for run in runs if run['mode'] == 'complete')
assert json.loads((native / 'answer.json').read_text()) == json.loads((baseline_case / 'answer.json').read_text())
control_source = Path(__file__).with_name('managed-cli-local-control.py').read_text()
result['existing_native_control'] = {'runtime': binding['runtime'], 'runtime_binding': binding['runtime_binding'], 'runtime_binding_audit_ms': binding['runtime_binding_audit_ms'], 'repetitions': 1,
'tokens_estimated_script_plus_help_plus_answer': count(control_source) + count((native / 'help.txt').read_text()) + count((native / 'answer.json').read_text()),
'token_scope': 'Projection source, original CLI help and returned answer; excludes shared question and launcher argv. No actual model usage measured.',
'wall_seconds': receipt['elapsed_seconds'], 'cpu_seconds': obs['cpu_stat']['usage_usec'] / 1e6,
'peak_process_family_bytes_including_page_cache': obs['memory_peak_bytes'], 'answer_matches': True, 'cleanup_verified': True}
args.output.write_text(json.dumps(result, indent=2) + '\n')
print(json.dumps(summary, indent=2))
@@ -0,0 +1,44 @@
"""Existing native code-composition control: save complete stdout, return selected facts."""
import argparse
import json
import subprocess
import time
from pathlib import Path
parser = argparse.ArgumentParser()
parser.add_argument('runtime', type=Path)
parser.add_argument('target', type=Path)
parser.add_argument('output', type=Path)
parser.add_argument('--measurement-inputs', type=Path, required=True)
args = parser.parse_args()
args.output.mkdir()
node = 'node'
inputs = json.loads(args.measurement_inputs.read_text())
expected = inputs['expected_runtime_bindings']['complete']
assert expected['git_revision'] == inputs['baseline_revision']
helper = Path(__file__).with_name('managed-cli-runtime.mjs')
def capture_binding():
return json.loads(subprocess.check_output([node, str(helper), str(args.runtime)], text=True))
binding_started = time.monotonic()
binding = capture_binding()
binding_audit_ms = {'before': (time.monotonic() - binding_started) * 1000}
assert binding == expected
entry = str(args.runtime / 'scripts/rea.mjs')
help_result = subprocess.run([node, entry, 'inspect-managed-members', '--help'], check=True, capture_output=True, text=True)
(args.output / 'help.txt').write_text(help_result.stdout)
with (args.output / 'parent.json').open('w') as file:
subprocess.run([node, entry, 'inspect-managed-members', str(args.target), '--type', 'Newtonsoft.Json.JsonConvert', '--json'], check=True, stdout=file)
parent = json.loads((args.output / 'parent.json').read_text())
data = parent['normalized_result']
answer = {'parent_evidence_id': parent['evidence_id'], 'artifact_sha256': data['artifact']['sha256'],
'selection': data['selection'], 'source_coverage': data['coverage'],
'total_methods': len(data['methods']), 'total_member_refs': len(data['member_refs']),
'methods': data['methods'][:8], 'member_refs': data['member_refs'][:16],
'limitation': 'MemberRef rows are assembly-wide; methods are selected-type rows. Page exhaustion is not an absence claim.'}
(args.output / 'answer.json').write_text(json.dumps(answer))
binding_finished = time.monotonic()
assert capture_binding() == binding
binding_audit_ms['after'] = (time.monotonic() - binding_finished) * 1000
(args.output / 'runtime-binding.json').write_text(json.dumps({'runtime': str(args.runtime), 'runtime_binding': binding, 'runtime_binding_verified': True, 'runtime_binding_audit_ms': binding_audit_ms}))
print(json.dumps(answer))
@@ -0,0 +1,18 @@
import json,hashlib,argparse
from pathlib import Path
import dnfile
parser=argparse.ArgumentParser();parser.add_argument('root',type=Path);root=parser.parse_args().root;path=root/'Newtonsoft.Json.dll'
pe=dnfile.dnPE(str(path));assert pe.net is not None
counts={name:len(getattr(pe.net.mdtables,name).rows) if getattr(pe.net.mdtables,name,None) else 0 for name in ['TypeDef','MethodDef','MemberRef','Field','Property','Event','Param','StandAloneSig','TypeSpec','ExportedType']}
selected=[]
for index,row in enumerate(pe.net.mdtables.TypeDef.rows,1):
name=(str(row.TypeNamespace)+'.' if str(row.TypeNamespace) else '')+str(row.TypeName)
if name!='Newtonsoft.Json.JsonConvert':continue
selected.append({'token':f'0x{0x02000000|index:08x}','name':name,'methods':[{'token':f'0x{0x06000000|m.row_index:08x}','name':str(m.row.Name),'rva':m.row.Rva,'signature_hex':m.row.Signature.value.hex()} for m in row.MethodList]})
assert len(selected)==1
refs=[{'token':f'0x{0x0a000000|i:08x}','name':str(row.Name),'signature_hex':row.Signature.value.hex()} for i,row in enumerate(pe.net.mdtables.MemberRef.rows[:16],1)]
assembly=pe.net.mdtables.Assembly.rows[0]
oracle={'decoder':'dnfile==0.18.0 (independent Python PE/CLI parser)','assembly_sha256':hashlib.sha256(path.read_bytes()).hexdigest(),'assembly_name':str(assembly.Name),'assembly_version':'.'.join(str(getattr(assembly,k)) for k in ['MajorVersion','MinorVersion','BuildNumber','RevisionNumber']),'row_counts':counts,'selected_types':selected,'member_refs_first_16':refs,'target_execution':False}
(root/'oracle.json').write_text(json.dumps(oracle,indent=2)+'\n')
print(json.dumps({'assembly':oracle['assembly_name'],'version':oracle['assembly_version'],'row_counts':counts,'selected_type':selected[0]['name'],'selected_methods':len(selected[0]['methods']),'target_execution':False}))
pe.close()
@@ -0,0 +1,87 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { readFile, readdir, realpath } from "node:fs/promises";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const sha = (value) => createHash("sha256").update(value).digest("hex");
/** Bind a trial to its source checkout and the compiled CLI actually launched. */
export const captureRuntimeBinding = async (runtime) => {
const root = await realpath(runtime);
const git = (args) =>
execFileSync("git", args, { cwd: root, maxBuffer: 16 * 1024 * 1024 });
const gitRevision = git(["rev-parse", "HEAD"]).toString().trim();
const sourcePaths = ["src", "scripts", "package.json", "package-lock.json"];
const sourceDiff = git([
"diff",
"--no-ext-diff",
"--no-textconv",
"--binary",
"HEAD",
"--",
...sourcePaths,
]);
const untracked = git([
"ls-files",
"--others",
"--exclude-standard",
"-z",
"--",
...sourcePaths,
])
.toString()
.split("\0")
.filter(Boolean)
.sort();
const untrackedEntries = [];
for (const name of untracked)
untrackedEntries.push([name, sha(await readFile(join(root, name)))]);
const files = ["scripts/rea.mjs", "package.json", "package-lock.json"];
const visit = async (directory) => {
const entries = await readdir(directory, { withFileTypes: true });
for (const entry of entries.sort((a, b) =>
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
)) {
const path = join(directory, entry.name);
assert.ok(!entry.isSymbolicLink(), `Unbound compiled CLI link: ${path}`);
if (entry.isDirectory()) await visit(path);
else if (entry.isFile() && /\.(?:[cm]?js|json)$/.test(entry.name))
files.push(relative(root, path));
}
};
await visit(join(root, "dist/cli-runtime"));
assert.ok(
files.includes(join("dist", "cli-runtime", "cli.js")),
"Missing compiled CLI entry",
);
const hash = createHash("sha256");
let bytes = 0;
for (const name of files.sort()) {
const value = await readFile(join(root, name));
bytes += value.length;
hash.update(JSON.stringify([name, value.length, sha(value)]) + "\n");
}
return {
binding_version: 1,
root,
node: process.version,
git_revision: gitRevision,
source_diff_sha256: sha(sourceDiff),
untracked_source_sha256: sha(JSON.stringify(untrackedEntries)),
compiled_cli_sha256: hash.digest("hex"),
compiled_cli_files: files.length,
compiled_cli_bytes: bytes,
};
};
export const assertRuntimeBinding = (actual, expected, mode) => {
assert.ok(expected, `Missing expected runtime binding for ${mode}`);
assert.deepEqual(actual, expected, `Runtime binding differs for ${mode}`);
};
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
console.log(
JSON.stringify(await captureRuntimeBinding(process.argv[2]), null, 2),
);
}
@@ -0,0 +1,72 @@
{
"status": "passed",
"scope": "Single completed SDK regression after shared JSON reader/admission changes; not a new paired source-to-bundle performance comparison. Existing semantic-node cap one answers static correspondence only; no equal semantic-graph coverage claim. Corpus/reference preparation performed earlier and excluded.",
"artifact": {
"name": "Redux Toolkit 2.2.7",
"tarball_sha256": "f2517c466b50424b661a27e48039f15db7783e5822f46b5c7d39f20104ac67a9"
},
"runtime_parent_revision": "4fab2dfda16236dc407aa179920780d6a970dda3",
"production_manifest": "managed-cli.json#measurement_inputs.production_sources_sha256",
"input_schema_profile": "default compact",
"output_schema_profile": "full",
"tokens_estimated": {
"input_catalog_once": 49054,
"arguments": 307,
"text_replies": 7017,
"whole_task": 56378
},
"token_scope": "tiktoken==0.12.0/cl100k_base; one name/description/input-schema catalog plus actual arguments and MCP text replies; excludes output schemas, framing, model prompts/reasoning and actual model usage. Not actual native-agent token usage.",
"actual_model_usage": null,
"wall_seconds": 21.38,
"cpu_seconds": 21.022683,
"peak_process_family_bytes_including_page_cache": 974041088,
"calls": [
{
"name": "analyze_javascript_application",
"elapsed_ms": 9837.265845000002,
"is_error": false,
"reply_bytes": 25941
},
{
"name": "compare_source_to_bundle",
"elapsed_ms": 256.3669919999993,
"is_error": false,
"reply_bytes": 13199
},
{
"name": "inspect_analysis_view",
"elapsed_ms": 3012.057904000001,
"is_error": false,
"reply_bytes": 19393
},
{
"name": "export_evidence_bundle",
"elapsed_ms": 148.9011160000009,
"is_error": false,
"reply_bytes": 364
}
],
"independent_proof": {
"status": "passed",
"method": "Independent Python JSON/SHA-256/POSIX path and basename joins; no REA parser or matcher.",
"source_files": 180,
"embedded_nodes": 99,
"candidate_relations": 210,
"strong_location_rows": 33,
"limitation": "Static multiple-location projections; no semantic-equivalence or removal claim."
},
"resource_events": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
},
"cleanup_verified": true
}
@@ -0,0 +1,266 @@
import {
captureRuntimeBinding,
assertRuntimeBinding,
} from "./managed-cli-runtime.mjs";
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { createHash } from "node:crypto";
import { readFile, writeFile, mkdir } from "node:fs/promises";
import { join, resolve } from "node:path";
const execute = promisify(execFile);
const [runtime, root, output, mode, cache = "cold"] = process.argv.slice(2);
for (const value of [runtime, root, output])
assert.equal(resolve(value), value);
assert.ok(["complete", "selected"].includes(mode));
assert.ok(["cold", "warm"].includes(cache));
await mkdir(output);
await mkdir(join(output, "tmp"));
const oracle = JSON.parse(await readFile(join(root, "oracle.json"), "utf8"));
const target = join(root, "Newtonsoft.Json.dll");
const hash = (value) => createHash("sha256").update(value).digest("hex");
assert.equal(hash(await readFile(target)), oracle.assembly_sha256);
const stable = (value) =>
Array.isArray(value)
? value.map(stable)
: value !== null && typeof value === "object"
? Object.fromEntries(
Object.keys(value)
.sort()
.map((key) => [key, stable(value[key])]),
)
: value;
const report = {
status: "incomplete",
runtime,
mode,
cache,
node: process.version,
question:
"Report identity/selection/source coverage for JsonConvert, its total method count and the first eight complete method records, plus the first sixteen assembly-wide MemberRef records and continuation coverage.",
recipe:
mode === "selected"
? "Use --detail summary --evidence-bundle FILE; pass the exact parent_evidence_id to inspect-analysis-view --evidence-bundle FILE with source kind retained-evidence and view {kind:managed-members,collection:methods|member_refs,offset:0,limit:8|16}."
: "Use inspect-managed-members --type Newtonsoft.Json.JsonConvert; the completed full Evidence supplies the requested rows and remains accessible.",
calls: [],
cleanup_verified: false,
};
const began = performance.now();
const environment = {
PATH: process.env.PATH,
HOME: output,
TMPDIR: join(output, "tmp"),
LANG: "C.UTF-8",
REA_LOG_LEVEL: "silent",
UV_THREADPOOL_SIZE: "1",
NODE_OPTIONS: "--max-old-space-size=2048",
...(cache === "cold"
? { NODE_DISABLE_COMPILE_CACHE: "1" }
: { NODE_COMPILE_CACHE: join(root, `compile-cache-${mode}`) }),
};
const call = async (label, args, json = true) => {
const started = performance.now();
const result = await execute(
process.execPath,
[join(runtime, "scripts/rea.mjs"), ...args],
{
cwd: output,
env: environment,
maxBuffer: 16 * 1024 * 1024,
timeout: 120000,
},
);
await writeFile(join(output, label + ".stdout"), result.stdout);
await writeFile(join(output, label + ".stderr"), result.stderr);
report.calls.push({
label,
arguments: ["rea", ...args],
elapsed_ms: performance.now() - started,
stdout_bytes: Buffer.byteLength(result.stdout),
stdout_file: label + ".stdout",
});
return json ? JSON.parse(result.stdout) : result.stdout;
};
let parent, summary, methods, refs;
try {
const inputs = JSON.parse(
await readFile(join(root, "measurement-inputs.json"), "utf8"),
);
const expected = inputs.expected_runtime_bindings?.[mode];
assert.ok(expected, `Missing expected runtime for ${mode}`);
assert.equal(
expected.git_revision,
mode === "complete"
? inputs.baseline_revision
: inputs.feature_parent_revision,
);
assert.notEqual(
inputs.expected_runtime_bindings.complete.root,
inputs.expected_runtime_bindings.selected.root,
);
assert.notEqual(
inputs.expected_runtime_bindings.complete.compiled_cli_sha256,
inputs.expected_runtime_bindings.selected.compiled_cli_sha256,
);
const bindingStarted = performance.now();
report.runtime_binding = await captureRuntimeBinding(runtime);
report.runtime_binding_audit_ms = {
before: performance.now() - bindingStarted,
};
assertRuntimeBinding(report.runtime_binding, expected, mode);
await call("help-members", ["inspect-managed-members", "--help"], false);
if (mode === "complete") {
parent = await call("complete", [
"inspect-managed-members",
target,
"--type",
"Newtonsoft.Json.JsonConvert",
"--json",
]);
methods = parent.normalized_result.methods.slice(0, 8);
refs = parent.normalized_result.member_refs.slice(0, 16);
} else {
await call("help-view", ["inspect-analysis-view", "--help"], false);
const bundlePath = join(output, "parent.json");
summary = await call("summary", [
"inspect-managed-members",
target,
"--type",
"Newtonsoft.Json.JsonConvert",
"--detail",
"summary",
"--evidence-bundle",
bundlePath,
"--json",
]);
const id = summary.normalized_result.parent_evidence_id;
assert.ok(id);
const bundle = JSON.parse(await readFile(bundlePath, "utf8"));
assert.equal(bundle.records.length, 1);
parent = bundle.records[0];
assert.equal(parent.evidence_id, id);
for (const [collection, limit] of [
["methods", 8],
["member_refs", 16],
]) {
const value = await call(collection, [
"inspect-analysis-view",
JSON.stringify({
source: { kind: "retained-evidence", evidence_id: id },
view: { kind: "managed-members", collection, offset: 0, limit },
}),
"--evidence-bundle",
bundlePath,
"--json",
]);
const page = value.normalized_result;
assert.equal(page.parent_evidence_id, id);
assert.deepEqual(page.source_coverage, parent.normalized_result.coverage);
assert.deepEqual(
page.page.rows,
parent.normalized_result[collection].slice(0, limit),
);
assert.equal(
page.coverage.total,
parent.normalized_result[collection].length,
);
assert.equal(page.coverage.next_offset, limit);
assert.equal(page.coverage.exhausted, false);
if (collection === "methods") methods = page.page.rows;
else refs = page.page.rows;
}
}
const data = parent.normalized_result;
assert.equal(data.artifact.sha256, oracle.assembly_sha256);
assert.equal(data.methods.length, 72);
assert.equal(data.member_refs.length, 2149);
assert.equal(data.types.length, 1);
assert.equal(data.types[0].token, oracle.selected_types[0].token);
for (let i = 0; i < data.methods.length; i++) {
const row = data.methods[i],
wanted = oracle.selected_types[0].methods[i];
assert.equal(row.token, wanted.token);
assert.equal(row.name, wanted.name);
assert.equal(row.rva, wanted.rva);
assert.equal(
row.signature.raw_sha256,
hash(Buffer.from(wanted.signature_hex, "hex")),
);
}
for (let i = 0; i < refs.length; i++) {
const row = refs[i],
wanted = oracle.member_refs_first_16[i];
assert.equal(row.token, wanted.token);
assert.equal(row.name, wanted.name);
assert.equal(
row.signature.raw_sha256,
hash(Buffer.from(wanted.signature_hex, "hex")),
);
}
assert.equal(methods.length, 8);
assert.equal(refs.length, 16);
if (summary) {
assert.equal(summary.normalized_result.summary.reported_counts.methods, 72);
assert.equal(
summary.normalized_result.summary.reported_counts.member_refs,
2149,
);
assert.deepEqual(summary.normalized_result.summary.coverage, data.coverage);
assert.deepEqual(
summary.normalized_result.summary.selection,
data.selection,
);
}
const answer = {
parent_evidence_id: parent.evidence_id,
artifact_sha256: oracle.assembly_sha256,
selection: data.selection,
source_coverage: data.coverage,
total_methods: data.methods.length,
total_member_refs: data.member_refs.length,
methods,
member_refs: refs,
limitation:
"MemberRef rows are assembly-wide; methods are selected-type rows. Page exhaustion is not an absence claim.",
};
report.parent_evidence_id = parent.evidence_id;
report.parent_semantic_sha256 = hash(JSON.stringify(stable(parent)));
report.answer_sha256 = hash(JSON.stringify(stable(answer)));
report.oracle_verified = {
all_selected_method_metadata: 72,
returned_methods: 8,
returned_member_refs: 16,
};
const bindingFinished = performance.now();
assertRuntimeBinding(
await captureRuntimeBinding(runtime),
report.runtime_binding,
mode,
);
report.runtime_binding_audit_ms.after = performance.now() - bindingFinished;
report.runtime_binding_verified = true;
report.status = "passed";
await writeFile(join(output, "parent-evidence.json"), JSON.stringify(parent));
await writeFile(join(output, "answer.json"), JSON.stringify(answer));
} finally {
report.elapsed_ms = performance.now() - began;
report.cleanup_verified = true;
await writeFile(
join(output, "report.json"),
JSON.stringify(report, null, 2) + "\n",
);
}
console.log(
JSON.stringify({
status: report.status,
mode,
cache,
parent: report.parent_evidence_id,
answer: report.answer_sha256,
calls: report.calls.map((c) => ({
label: c.label,
elapsed_ms: c.elapsed_ms,
bytes: c.stdout_bytes,
})),
}),
);
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,219 @@
# Portable managed CLI member views
Issue #5148 / PR #5167. Baseline `main@3dbe314ada9f6dcc994ca4655dc0faf082503847`;
feature integrates the original `7b3ed4d025edd24d062cde0327a53bd311e174fc`.
The measured production-file SHA-256 manifest is in [managed-cli.json](managed-cli.json).
These are working-tree measurements. Each trial verifies both source and the
complete compiled CLI graph before and after execution against its explicitly
selected build. The two roles have distinct checkout roots and compiled digests.
Identity-audit time is included in whole-task costs and recorded separately.
The audit reads compiled files before timing individual CLI calls, warming those
filesystem pages; cold means disabled Node compile cache, not disk-cold startup.
## Question and independent checks
Report identity, selected type, source coverage, the total method count, the first
eight complete method records, and the first sixteen assembly-wide MemberRef
records, including continuation coverage. Keep complete original Evidence available.
The question deliberately does not ask for every method or reference.
Public artifact: [Newtonsoft.Json 13.0.3](https://www.nuget.org/packages/Newtonsoft.Json/13.0.3),
from the NuGet flat-container archive. The 2,441,966-byte archive SHA-256 is
`872fc189e638ab1056555b03aaa38f68bcb54286e221aa646eb1129babf63c77`.
Selected member `lib/net6.0/Newtonsoft.Json.dll`, 712,464 bytes, SHA-256
`22c649f75fce5be7c7ccda8880473b634ef69ecf33f5d1ab8ad892caf47d5a07`.
Package version 13.0.3 is distinct from metadata assembly version 13.0.0.0.
No library code is executed; REA parses PE/CLI statically.
An independent Python oracle uses `dnfile==0.18.0` and `pefile==2024.8.26`.
It finds 494 TypeDefs, 4,208 MethodDefs and 2,149 MemberRefs. The selected
`Newtonsoft.Json.JsonConvert` has 72 methods. Every trial checks all 72 method
tokens, names, RVAs and original signature hashes, plus the sixteen returned
MemberRef tokens, names and signature hashes, against this oracle. This does
not independently establish complete CIL semantics or opcode correctness.
The full parent Evidence and requested answers are identical across all twelve
trials; returned rows equal the exact parent slices. Selection and source
coverage remain distinct from page exhaustion. MemberRefs remain assembly-wide.
## Completed-task cost
Linux x64, Node 24.18.0/npm 11.16.0. Three alternating pairs per cache condition,
serial, one CPU; 4 GiB hard / 3 GiB early-stop family guard, no swap, 192-task
ceiling. Every task includes actual selected-command help, all CLI calls,
provider parsing, bundle publication/authentication, local answer extraction
and oracle comparisons, plus both runtime-identity audits. Build, download, oracle preparation and warm priming
are excluded. OS page cache is uncontrolled. No build/test overlapped timing.
Complete mode starts two CLI processes (help plus analysis). Selected mode starts
five (two help commands, summary/publication, methods page, MemberRef page).
Cold disables the Node compile cache; warm primes the exact workflow once per
runtime/mode, then still starts fresh processes with isolated HOME each trial.
All trials complete with verified process cleanup and no OOM/resource-stop event.
| Whole task, median | Complete | Summary + two pages |
| ----------------------------------------------- | -------: | ------------------: |
| Estimated context tokens, both cache conditions | 451,373 | 13,078 |
| Cold wall seconds | 9.969 | 14.801 |
| Cold family CPU seconds | 9.743 | 14.555 |
| Cold kernel family peak MiB | 393.281 | 400.098 |
| Warm wall seconds | 10.339 | 24.141 |
| Warm family CPU seconds | 10.097 | 23.842 |
| Warm kernel family peak MiB | 401.809 | 407.164 |
Context estimates include the question, supplied command recipe, all logical
argv, actual help and stdout. Tokenizer `tiktoken==0.12.0`, `cl100k_base`.
The reduction is 97.1% for this fixed task. There is no model invocation;
actual billed/native-agent usage, reasoning and transport framing are unmeasured.
The memory counter is the kernel cgroup process-family peak including file
cache, not sampled RSS. Ranges and individual receipts are in the JSON.
This is an opt-in context/portability improvement with extra latency and CPU,
not a speed or memory optimization. Complete provider parsing and full authentication of file-supplied bundles
remain. Fresh managed Evidence is sealed at its producing factory and reused
without another payload clone, including low-headroom error projection. Complete output remains the default and is preferable
for small outputs or questions requiring every row. No cache or parser cap
was introduced.
## Existing native code composition
The unchanged baseline can already redirect full stdout to a file and select
rows in code. [The measured control](managed-cli-local-control.py) returns the
identical answer and retains the complete original Evidence. One cold run:
11.249 s wall, 11.000 s family CPU,
375.3 MiB kernel peak. Projection source,
original help and returned answer together estimate 10,555 tokens; the shared
question and launcher argv are excluded. This single-run control does not
establish stable performance or actual native-agent usage. It does demonstrate
that raw CLI reply savings are not universal savings over code composition.
The additive interface supplies authenticated reusable projections through the
same application/domain owner as MCP, without caller-maintained JSON slicing.
## Correctness and resource boundaries
The public CLI regression pages all eight managed collections after process
exit, target deletion and a working-directory change. Complete parent identity,
original row order, signatures, source/page coverage and unknowns survive.
Existing outputs are not replaced; failed publication retains completed facts.
The Evidence factory parses mutable caller data into independent trees, computes
identity, then seals its fresh owned snapshot with active ancestry/iterators.
It does not retain a set/array of every completed object. Existing sealed roots
can be shared; mutable input aliases are cloned independently. General graph
sealing keeps its separate cycle, sharing, hidden-field and atomic-failure rules.
Managed and derived-view factories seal their own completed Evidence, so CLI
error projection can borrow it without reauthentication or another payload copy.
The view owner retains strict JSON normalization for optional-field contracts;
the CLI no longer copies its already validated completed projection again.
Shared heap admission traverses by depth rather than queueing every report row
or allocating an entries array. It reserves parsed working copies and actual
escaping expansion before cloning/hashing, reporting UTF-16 storage explicitly.
Canonical bundle equality streams existing ordinary JSON encoder parts, preserving
native key order, omission, negative zero and Unicode semantics without two
whole-document strings. All production bundle readers explicitly pass their
operation: CLI import/export/compare/inspection, retained views, MCP import/
comparison and both native-matching read passes. Errors retain the original
cause, cleanup primary and lease through retry.
One process primitive dispatches queued callbacks and then their OS signal
notifications. The managed producer checks it before accepting complete Evidence.
Publication/read stages use it before their final checks. Inline, file and bundle
CLI views share the existing command finalizer and actual resource scope; the
finalizer checks interruption after close while preserving any primary/cleanup
failure. Existing inline error labels remain intact. Synchronous analysis is
cooperatively cancellable between stages, not preemptible.
Four compiled publication cases (SIGINT/SIGTERM × event/actual POSIX signal)
fail without their checkpoint and pass restored. Eight additional compiled
completion cases cover default complete delivery and inline projection; all fail
with only their new completion checkpoints removed and pass restored. Fixture
hooks match the selected target path or exact parent ID. Two real Newtonsoft
publication runs and four real default/inline completion runs retain the identical
parent or independently checked selected summary, exit 130/143 and preserve
committed receipts where applicable. No target library code is executed.
All 556 focused tests across 36 files pass, including direct
finalizer/cleanup ownership, WASM/Android cancellation and native matching.
All six `npm run check` tasks plus architecture guards, and all six
`npm run docs:check` tasks plus five README tests pass. The expanded domain
project passes 5,617 tests in 287 files with
3 skipped cases; this overlaps the focused gate. Its first
batched shard hit two existing 30-second timeouts in unchanged large synthetic
tests. Both passed isolated and in the fresh shard rerun under the same timeout;
the cause of the batch variation is unverified. No production/timeout change
was made for those cases. An earlier unsplit expansion was manually stopped
before its heap ceiling; that incomplete attempt is not counted as a pass.
Eight producing/operation negative controls also catch graph-sized sealing/
admission scratch and wrong operation attribution, including native reload.
Native built-in instrumentation is confined to the isolated process-global lane.
Initial test-only spy/probe/schema-import issues were corrected before these
passing gates. No actual heap exhaustion is induced. Simulated providers do not
qualify foreign engines; Windows OS signals remain unverified locally.
## Shared-reader real-artifact regression
The existing Redux Toolkit 2.2.7 source/bundle task also completes through the
updated reader with default full output schemas and compact input schemas.
An independent Python SHA-256/path oracle verifies 180 historical files,
99 embedded source nodes and 210 candidate relations (33 strong-location rows,
147 unknown). Original signals and partial source coverage remain visible.
This uses the existing explicit semantic-node cap of one for static correspondence;
it does not establish equivalent semantic-graph coverage or behavioral equivalence.
One completed task: 21.380 s wall,
21.023 s family CPU,
928.9 MiB kernel family peak,
56,378 estimated context tokens (one input catalog,
all arguments and MCP text replies; excludes output schemas, framing and model
reasoning). Corpus/reference preparation was performed earlier and excluded.
This is a regression verification with resource observation, not a new paired
performance comparison. Actual model usage remains unmeasured. Details:
[managed-cli-shared-read.json](managed-cli-shared-read.json).
## Reproduction
Extract the selected DLL to a private lab as `Newtonsoft.Json.dll`. Create an
`acquisition.json` containing the URL, hashes, sizes and selected ZIP member
shown above; never commit the DLL. Prepare the independent oracle:
```bash
uv run --no-project --with dnfile==0.18.0 --with pefile==2024.8.26 python \
experiments/analysis-efficiency/managed-cli-oracle.py /absolute/lab
```
Build baseline and feature separately with the pinned toolchain. Run each under
the shared resource guard, alternating complete/selected, selected/complete,
complete/selected for cold and warm. Use a new output directory for every run:
```bash
python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \
node experiments/analysis-efficiency/managed-cli-task.mjs \
/absolute/runtime /absolute/lab /absolute/lab/pair-cold-1-selected selected cold
```
Save each guard output to the adjacent `pair-*.log`. Warm mode uses the lab's
mode-specific compile-cache directory; prime each identical workflow once first.
Supply a `measurement-inputs.json` with exact revisions and production hashes,
plus `expected_runtime_bindings.complete` and `.selected`. Capture each with
`node experiments/analysis-efficiency/managed-cli-runtime.mjs /absolute/runtime`
after building. Bind complete to the clean baseline revision, selected to the
feature parent revision plus tracked/untracked source digests, with different
roots and compiled-graph digests. The collector rejects a wrong build before any
CLI call, rechecks it afterward, and aggregation independently enforces identity.
A real wrong-runtime attempt is rejected before analysis. A copied trial with a
tampered revision fails aggregation; removing only that consumer guard accepts it.
The twelve-trial aggregation reproduces without private pilots. The native
control also requires `--measurement-inputs /absolute/lab/measurement-inputs.json`
and performs the same before/after identity checks.
Then aggregate with:
```bash
uv run --no-project --with tiktoken==0.12.0 python \
experiments/analysis-efficiency/managed-cli-costs.py /absolute/lab \
--output /absolute/results.json
```
The original issue reporter's executable, foreign operating systems and actual
model sessions remain unverified. Setup pilots and earlier unbound cohorts, including one failed setup run
while compiled artifacts were absent after a typecheck failure, are excluded.
+3
View File
@@ -1,3 +1,4 @@
import type { EvidenceCreationOptions } from "../domain/evidence.js";
import type { ProviderInspectionScope } from "../providers/ProviderInspectionScope.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
@@ -146,6 +147,7 @@ export const createAnalysisExecutionEvidence = (
input: unknown,
execution: AnalysisExecution,
confidence: "observed" | "derived",
options?: EvidenceCreationOptions,
): Evidence =>
createEvidence(
execution.subject === undefined
@@ -167,6 +169,7 @@ export const createAnalysisExecutionEvidence = (
locations: execution.locations,
confidence,
},
options,
);
/** Provider-neutral application capability for one analysis operation. */
+90 -23
View File
@@ -1,4 +1,5 @@
import type { ExecutableFormatHint } from "../domain/dosCom.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import type { BinaryArchitecture } from "../domain/binaryTargetTypes.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
@@ -59,7 +60,11 @@ import {
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { resolveXrefsAddress } from "./XrefsAddressResolution.js";
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import { deliverAnalysisEvidence } from "./EvidenceBundleCommands.js";
import type { BinaryTargetError } from "../domain/configurationErrors.js";
import {
directAnalysisCleanupFailure,
@@ -146,24 +151,81 @@ export const runDirectAnalysis = async (
/** Execute one provider-native semantic operation with atomic provenance. */
export const runProviderAnalysis = async (
dependencies: DirectAnalysisDependencies,
...[path, tool, arguments_, logger = silentLogger, signal]: readonly [
path: string,
tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName,
arguments_: Readonly<Record<string, JsonValue>>,
logger?: Logger,
signal?: AbortSignal,
]
...[path, tool, arguments_, logger = silentLogger, signal, delivery]:
| readonly [
path: string,
tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName,
arguments_: Readonly<Record<string, JsonValue>>,
logger?: Logger | undefined,
signal?: AbortSignal | undefined,
]
| readonly [
path: string,
tool: "inspect_managed_members",
arguments_: Readonly<Record<string, JsonValue>>,
logger: Logger | undefined,
signal: AbortSignal | undefined,
delivery: {
readonly detail?: "complete" | "summary";
readonly evidenceBundlePath?: string;
},
]
): Promise<JsonValue> => {
if (isManagedToolName(tool))
return runManagedProviderAnalysis(
dependencies,
path,
tool,
arguments_,
signal,
if (delivery !== undefined && tool !== "inspect_managed_members")
return cliError(
new AnalysisInputError(tool, undefined, [
{
path: ["delivery"],
reason: "invalid_value",
message:
"Evidence delivery options are only supported by inspect_managed_members",
},
]),
);
return withProcessCancellation(signal, (operationSignal) =>
runAnalysis(dependencies, path, tool, arguments_, {
if (
delivery?.detail === "summary" &&
delivery.evidenceBundlePath === undefined
)
return cliError(
new AnalysisInputError(tool, undefined, [
{
path: ["evidence_bundle"],
reason: "missing_argument",
message:
"summary detail requires --evidence-bundle so complete Evidence remains available to later CLI commands",
},
]),
);
return withProcessCancellation(signal, async (operationSignal) => {
if (isManagedToolName(tool)) {
const original = await runManagedProviderAnalysis(
dependencies,
path,
tool,
arguments_,
operationSignal,
);
await dispatchQueuedInterrupts();
if (!original.ok) return cliError(original.error);
if (operationSignal.aborted)
return cliError(
new AnalysisCancelledError(tool, {
cause: operationSignal.reason,
partialObservation: original.value,
}),
);
const result =
delivery?.evidenceBundlePath === undefined
? original
: await deliverAnalysisEvidence(
original.value,
delivery.evidenceBundlePath,
delivery.detail,
operationSignal,
);
return result.ok ? result.value : cliError(result.error);
}
return runAnalysis(dependencies, path, tool, arguments_, {
logger,
snapshotPath: undefined,
signal: operationSignal,
@@ -172,8 +234,8 @@ export const runProviderAnalysis = async (
: NATIVE_INSPECTION_OPERATIONS.has(tool)
? { createSession: dependencies.createNativeBinarySession }
: {}),
}),
);
});
});
};
/** Execute managed metadata inspection in an isolated managed-only session. */
@@ -199,13 +261,14 @@ export const runManagedProviderExecution = async (
);
});
/** Preserve typed managed Evidence before selecting its CLI delivery. */
const runManagedProviderAnalysis = async (
dependencies: DirectAnalysisDependencies,
dependencies: Pick<DirectAnalysisDependencies, "createManagedBinarySession">,
path: string,
tool: ManagedToolName,
arguments_: Readonly<Record<string, JsonValue>>,
signal?: AbortSignal,
): Promise<JsonValue> => {
): Promise<Result<Evidence, AnalysisError>> => {
const execution = await runManagedProviderExecution(
dependencies,
path,
@@ -213,9 +276,13 @@ const runManagedProviderAnalysis = async (
signal,
arguments_,
);
if (!execution.ok) return cliError(execution.error);
if (!execution.ok) return execution;
const value = execution.value;
return createAnalysisExecutionEvidence(tool, arguments_, value, "observed");
return ok(
createAnalysisExecutionEvidence(tool, arguments_, value, "observed", {
immutable: true,
}),
);
};
const runAnalysis = async (
+159 -12
View File
@@ -1,9 +1,23 @@
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import {
EvidenceFileError,
EvidenceIntegrityError,
type PublishedEvidenceFile,
} from "../domain/evidenceErrors.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { EvidenceBundle } from "../domain/evidenceBundle.js";
import {
AnalysisCancelledError,
AnalysisInputError,
} from "../domain/analysisErrorCore.js";
import type { Evidence } from "../domain/evidence.js";
import {
createImmutableEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import type { InspectAnalysisViewInput } from "../domain/analysisView/analysisView.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -14,12 +28,18 @@ import {
readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "./EvidenceBundleFiles.js";
import { inspectAnalysisViewValidated } from "./analysisView/AnalysisViewService.js";
/** Validate a standalone bundle and report its complete record/revision counts. */
export const importEvidenceBundleCommand = async (
path: string,
): Promise<Result<JsonValue, AnalysisError>> => {
const loaded = await readEvidenceBundle(path);
const loaded = await readEvidenceBundle(
path,
undefined,
undefined,
"import_evidence_bundle",
);
if (!loaded.ok) return loaded;
return ok({
imported: loaded.value.records.length,
@@ -35,16 +55,136 @@ export const exportEvidenceBundleCommand = async (
overwrite: boolean,
): Promise<Result<JsonValue, AnalysisError>> => {
// The sealed snapshot lets the writer reuse this authentication.
const loaded = await readImmutableEvidenceBundle(sourcePath);
const loaded = await readImmutableEvidenceBundle(
sourcePath,
undefined,
undefined,
"export_evidence_bundle",
);
if (!loaded.ok) return loaded;
return projectWrite(
loaded.value,
await persistEvidenceBundleCommand(loaded.value, outputPath, overwrite),
);
};
/** Publish complete parent Evidence before delivering it or its linked summary. */
export const deliverAnalysisEvidence = async (
evidence: Evidence,
outputPath: string,
detail: "complete" | "summary" = "complete",
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => {
const headroom = requireAuthenticationHeadroom(evidence, {
operation: evidence.operation,
path: absoluteCallerPath(outputPath),
description:
"Insufficient heap headroom to authenticate the complete parent Evidence",
});
if (!headroom.ok)
return err(headroom.error.retainPartialObservation(evidence));
const bundle = createImmutableEvidenceBundle([evidence]);
const delivered =
detail === "summary"
? inspectAnalysisViewValidated(
{
source: {
kind: "retained-evidence",
evidence_id: evidence.evidence_id,
},
view: { kind: "summary" },
},
(id) => bundle.records.find((record) => record.evidence_id === id),
)
: ok(evidence);
if (!delivered.ok) return delivered;
const written = await persistEvidenceBundleCommand(
bundle,
outputPath,
false,
signal,
evidence.operation,
);
// Deliver interrupts queued during final publication cleanup before the
// command's signal owner accepts completion and removes its listeners.
await dispatchQueuedInterrupts();
if (!written.ok) return err(written.error.retainPartialObservation(evidence));
if (signal?.aborted)
return err(
new AnalysisCancelledError(evidence.operation, {
cause: signal.reason,
partialObservation: delivered.value,
diagnostics: { evidence_bundle: { ...written.value } },
}),
);
return delivered;
};
/** Resolve the caller's exact retained parent from an authenticated portable bundle. */
export const inspectAnalysisViewFromBundle = async (
input: InspectAnalysisViewInput,
bundlePath: string,
resources: ArtifactResourceScope,
signal?: AbortSignal,
): Promise<Result<Evidence, AnalysisError>> => {
if (input.source.kind !== "retained-evidence")
return err(
new AnalysisInputError("inspect_analysis_view", undefined, [
{
path: ["source", "kind"],
reason: "invalid_value",
message:
"--evidence-bundle requires a retained-evidence source with the exact parent evidence_id; inline Evidence already supplies its source",
},
]),
);
const loaded = await readImmutableEvidenceBundle(
bundlePath,
signal,
resources,
"inspect_analysis_view",
);
// Authentication and projection are synchronous; dispatch queued interrupts before accepting either result.
await dispatchQueuedInterrupts();
if (!loaded.ok) return loaded;
if (signal?.aborted)
return err(
new AnalysisCancelledError("inspect_analysis_view", {
cause: signal.reason,
}),
);
const projected = inspectAnalysisViewValidated(input, (id) =>
loaded.value.records.find((record) => record.evidence_id === id),
);
await dispatchQueuedInterrupts();
if (projected.ok && signal?.aborted)
return err(
new AnalysisCancelledError("inspect_analysis_view", {
cause: signal.reason,
partialObservation: projected.value,
}),
);
return projected;
};
/** Publish a completed bundle and retry its exact staged-output cleanup owner. */
const persistEvidenceBundleCommand = async (
bundle: EvidenceBundle,
outputPath: string,
overwrite: boolean,
signal?: AbortSignal,
operation?: string,
): Promise<Result<PublishedEvidenceFile, AnalysisError>> => {
const resources = new ArtifactResourceScope();
const written = await writeEvidenceBundle(
loaded.value,
bundle,
outputPath,
overwrite,
resources,
signal,
operation,
);
if (written.ok) return projectWrite(loaded.value, written);
if (written.ok) return written;
// Confirmed release resolves the bound error to its primary outcome; a
// failed retry leaves that error incomplete with its exact retained owner.
@@ -54,10 +194,7 @@ export const exportEvidenceBundleCommand = async (
written.error instanceof EvidenceFileError &&
written.error.published
) {
return projectWrite(loaded.value, {
ok: true,
value: written.error.published,
});
return ok(written.error.published);
}
return written;
};
@@ -70,8 +207,18 @@ export const compareEvidenceBundlesCommand = async (input: {
// Sealed snapshots let comparison and its bundle digests reuse this
// authentication instead of re-validating every record twice more.
const [left, right] = await Promise.all([
readImmutableEvidenceBundle(input.leftPath),
readImmutableEvidenceBundle(input.rightPath),
readImmutableEvidenceBundle(
input.leftPath,
undefined,
undefined,
"compare_bundles",
),
readImmutableEvidenceBundle(
input.rightPath,
undefined,
undefined,
"compare_bundles",
),
]);
if (!left.ok) return left;
if (!right.ok) return right;
@@ -88,7 +235,7 @@ export const compareEvidenceBundlesCommand = async (input: {
const projectWrite = (
bundle: EvidenceBundle,
written: Awaited<ReturnType<typeof writeEvidenceBundle>>,
written: Result<PublishedEvidenceFile, AnalysisError>,
): Result<JsonValue, AnalysisError> =>
written.ok
? ok({
+24 -18
View File
@@ -1,3 +1,4 @@
import { requireAuthenticationHeadroom } from "./JsonAuthenticationHeadroom.js";
import { absoluteCallerPath } from "../filesystem/callerPath.js";
import {
@@ -39,45 +40,47 @@ export const readEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseEvidenceBundle, signal, resources);
readBundle(path, parseEvidenceBundle, signal, resources, operation);
/** Read an authenticated immutable snapshot for a subsequent session import. */
export const readImmutableEvidenceBundle = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> =>
readBundle(path, parseImmutableEvidenceBundle, signal, resources);
readBundle(path, parseImmutableEvidenceBundle, signal, resources, operation);
const readBundle = async (
path: string,
parseBundle: (input: unknown) => EvidenceBundle,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "import_evidence_bundle",
): Promise<Result<EvidenceBundle, EvidenceReadFailure>> => {
if (signal?.aborted)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
const loaded = await readJsonFile(path, signal, resources);
if (!loaded.ok) {
if (loaded.error instanceof AnalysisCancelledError)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
cause: loaded.error.cause,
}),
);
return loaded;
}
const loaded = await readJsonFile(path, signal, resources, operation);
if (!loaded.ok) return loaded;
if (signal?.aborted)
return err(
new AnalysisCancelledError("import_evidence_bundle", {
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
const headroom = requireAuthenticationHeadroom(loaded.value, {
operation,
path: absoluteCallerPath(path),
description:
"Insufficient heap headroom to authenticate the Evidence bundle",
});
if (!headroom.ok) return headroom;
let bundle: EvidenceBundle;
try {
bundle = parseBundle(loaded.value);
@@ -121,6 +124,7 @@ export const writeEvidenceBundle = async (
overwrite: boolean,
resources: ArtifactResourceScope,
signal?: AbortSignal,
operation = "export_evidence_bundle",
): Promise<
Result<
{ readonly path: string; readonly bytes: number },
@@ -128,7 +132,11 @@ export const writeEvidenceBundle = async (
>
> => {
if (signal?.aborted === true)
return err(new AnalysisCancelledError("export_evidence_bundle"));
return err(
new AnalysisCancelledError(operation, {
cause: signal.reason,
}),
);
let checked: EvidenceBundle;
try {
checked = parseEvidenceBundle(bundle);
@@ -144,8 +152,6 @@ export const writeEvidenceBundle = async (
path,
overwrite,
resources,
signal === undefined
? undefined
: { signal, operation: "export_evidence_bundle" },
signal === undefined ? undefined : { signal, operation },
);
};
@@ -22,7 +22,9 @@ describe("exact Evidence input resolution", () => {
actual: null,
},
});
expect(projected.message).toContain("not retained in this session");
expect(projected.message).toContain(
"not available from the selected source",
);
expect(projected.remediation.action).toContain(
"complete inline Evidence",
);
+1 -1
View File
@@ -18,7 +18,7 @@ export const resolveEvidenceInput = (
new EvidenceReferenceError(
input.evidence_id,
"missing",
"Evidence retained in this session",
"Evidence available to the selected source",
null,
),
);
@@ -4,6 +4,50 @@ import {
requireAuthenticationHeadroom,
} from "./JsonAuthenticationHeadroom.js";
describe("JSON authentication headroom", () => {
it("does not queue every row while deciding whether a wide report can fit", () => {
let pending = 0;
let peak = 0;
const rows: { admission_probe: boolean }[] = [];
for (let index = 0; index < 512; index++) {
const row = {
get admission_probe() {
pending--;
return true;
},
};
// Observe reading a row separately from examining its scalar. A width
// queue reads every row first; an iterator finishes each row before advancing.
Object.defineProperty(rows, index, {
enumerable: true,
get: () => {
peak = Math.max(peak, ++pending);
if (pending > 32)
throw new Error(
"Admission queued report rows before refusing their allocation",
);
return row;
},
});
}
const value = { rows };
const result = requireAuthenticationHeadroom(value, {
operation: "inspect_managed_members",
path: "/tmp/parent.json",
description: "Cannot authenticate",
availableBytes: 0,
});
expect(pending).toBe(0);
expect(peak).toBe(1);
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
reportedLimits: {
serialized_json_utf16_bytes: JSON.stringify(value).length * 2,
},
},
});
});
it("estimates parsed JSON at no less than its UTF-16 text", () => {
const text = "x".repeat(1000);
expect(estimateJsonValueBytes(text)).toBeGreaterThanOrEqual(2000);
@@ -42,3 +86,28 @@ describe("JSON authentication headroom", () => {
).toEqual({ ok: true, value: undefined });
});
});
it.each([
["control characters", { text: "\u0000\u0001\b\t\n\f\r".repeat(2000) }],
[
"escaped keys",
{ ['\u0000\\"'.repeat(2000)]: [true, false, null, -0, 1e30] },
],
["surrogate boundaries", { text: "😀\ud800x\udfff".repeat(2000) }],
])("reserves actual escaped JSON storage for %s", (_label, value) => {
const result = requireAuthenticationHeadroom(value, {
operation: "inspect_analysis_view",
path: "/tmp/bundle.json",
description: "Cannot authenticate bundle",
availableBytes: estimateJsonValueBytes(value) * 2 + 1,
});
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
reportedLimits: {
serialized_json_utf16_bytes: JSON.stringify(value).length * 2,
},
},
});
});
+58 -20
View File
@@ -1,32 +1,66 @@
import { jsonStringCodeUnits } from "../domain/jsonSerialization.js";
import { getHeapStatistics } from "node:v8";
import { AnalysisResourceConstraintError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../domain/result.js";
/** Authentication clones parsed JSON and hashes its canonical commitment. */
const AUTHENTICATION_COPIES = 2;
/** Conservative bytes a parsed JSON value occupies: UTF-16 text plus container and slot overhead. */
export const estimateJsonValueBytes = (value: unknown): number => {
let bytes = 0;
const stack: unknown[] = [value];
/** Parsed working copies and conservative UTF-16 storage for escaped JSON. */
const estimateJsonStorage = (
value: unknown,
): { valueBytes: number; serializedUtf16Bytes: number } => {
let valueBytes = 0;
let serializedCharacters = 0;
const stack: Iterator<unknown>[] = [[value][Symbol.iterator]()];
while (stack.length > 0) {
const item = stack.pop();
if (typeof item === "string") bytes += 64 + item.length * 2;
else if (Array.isArray(item)) {
bytes += 128 + item.length * 16;
for (const entry of item) stack.push(entry);
const next = stack[stack.length - 1]!.next();
if (next.done) {
stack.pop();
continue;
}
const item = next.value;
if (typeof item === "string") {
valueBytes += 64 + item.length * 2;
serializedCharacters += jsonStringCodeUnits(item);
} else if (Array.isArray(item)) {
valueBytes += 128 + item.length * 16;
serializedCharacters += 2 + Math.max(0, item.length - 1);
stack.push(item.values());
} else if (typeof item === "object" && item !== null) {
bytes += 128;
for (const [key, entry] of Object.entries(item)) {
bytes += 64 + key.length * 2;
stack.push(entry);
}
} else bytes += 16;
valueBytes += 128;
serializedCharacters += 2;
stack.push(
(function* () {
let count = 0;
for (const key in item) {
if (!Object.hasOwn(item, key)) continue;
valueBytes += 64 + key.length * 2;
serializedCharacters +=
jsonStringCodeUnits(key) + 1 + (count++ === 0 ? 0 : 1);
yield (item as Record<string, unknown>)[key];
}
})(),
);
} else {
valueBytes += 16;
serializedCharacters +=
typeof item === "number"
? String(item).length
: item === true
? 4
: item === false
? 5
: 4;
}
}
return bytes;
return { valueBytes, serializedUtf16Bytes: serializedCharacters * 2 };
};
/** Conservative parsed JSON bytes, independent of its escaped representation. */
export const estimateJsonValueBytes = (value: unknown): number =>
estimateJsonStorage(value).valueBytes;
/** Refuse before authentication when live heap headroom cannot hold its validation and commitment working copies. */
export const requireAuthenticationHeadroom = (
value: unknown,
@@ -36,11 +70,14 @@ export const requireAuthenticationHeadroom = (
readonly description: string;
readonly availableBytes?: number;
},
): Result<void, AnalysisError> => {
): Result<void, AnalysisResourceConstraintError> => {
const availableBytes =
options.availableBytes ?? getHeapStatistics().total_available_size;
const valueBytes = estimateJsonValueBytes(value);
const requiredBytes = valueBytes * AUTHENTICATION_COPIES;
const { valueBytes, serializedUtf16Bytes } = estimateJsonStorage(value);
// Canonical hashing and manifest normalization can still encode scalars or
// metadata; account for their escaped representations before allocating them.
const requiredBytes =
(valueBytes + serializedUtf16Bytes) * AUTHENTICATION_COPIES;
if (requiredBytes <= availableBytes) return ok(undefined);
return err(
new AnalysisResourceConstraintError(
@@ -50,6 +87,7 @@ export const requireAuthenticationHeadroom = (
{
input_path: options.path,
projected_value_bytes: valueBytes,
serialized_json_utf16_bytes: serializedUtf16Bytes,
authentication_bytes: requiredBytes,
available_heap_bytes: availableBytes,
},
+8 -4
View File
@@ -52,13 +52,14 @@ export const readJsonFile = async (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "read_evidence_file",
): Promise<
Result<
unknown,
EvidenceFileError | AnalysisResourceConstraintError | AnalysisCancelledError
>
> => {
const read = await readJsonFileWithSource(path, signal, resources);
const read = await readJsonFileWithSource(path, signal, resources, operation);
return read.ok ? ok(read.value.data) : read;
};
@@ -67,6 +68,7 @@ export const readJsonFileWithSource = (
path: string,
signal?: AbortSignal,
resources?: ArtifactResourceScope,
operation = "read_evidence_file",
): Promise<
Result<
{ readonly data: unknown; readonly path: string },
@@ -78,13 +80,14 @@ export const readJsonFileWithSource = (
async (selected) => {
const read = await readJsonInputFile(
selected,
"read_evidence_file",
operation,
signal,
resources,
);
return read.ok ? ok({ data: read.value, path: selected }) : read;
},
signal,
operation,
);
/** Read selected JSON members through the same regular-file error boundary. */
@@ -124,6 +127,7 @@ const readJsonFileInput = async <Value>(
>
>,
signal?: AbortSignal,
operation = "read_evidence_file",
): Promise<
Result<
Value,
@@ -153,7 +157,7 @@ const readJsonFileInput = async <Value>(
if (cause instanceof AnalysisResourceConstraintError) return err(cause);
if (cause instanceof JsonInputCancellationFailure)
return err(
new AnalysisCancelledError("read_evidence_file", {
new AnalysisCancelledError(operation, {
cause: cause.reason,
}),
);
@@ -166,7 +170,7 @@ const readJsonFileInput = async <Value>(
);
if (isJsonReadCancellation(cause, signal))
return err(
new AnalysisCancelledError("read_evidence_file", {
new AnalysisCancelledError(operation, {
cause: signal?.reason,
}),
);
@@ -1,5 +1,6 @@
import { setImmediate } from "node:timers/promises";
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { z } from "zod";
import { ANALYSIS_VIEW_PROVIDER } from "../InvestigationProviders.js";
@@ -25,7 +26,6 @@ import {
type AnalysisViewRequest,
type InspectAnalysisViewInput,
} from "../../domain/analysisView/analysisView.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import {
immutableJsonSnapshotMember,
isImmutableJsonSnapshot,
@@ -332,39 +332,44 @@ const createAnalysisViewEvidence = (
? {}
: { architecture: parent.subject.architecture }),
};
return createEvidence(target, ANALYSIS_VIEW_PROVIDER, {
predicateType: "rea.analysis-view",
operation: INSPECT_ANALYSIS_VIEW_OPERATION,
parameters: {
parent_evidence_id: parent.evidence_id,
view: jsonValueSchema.parse(input.view),
return createEvidence(
target,
ANALYSIS_VIEW_PROVIDER,
{
predicateType: "rea.analysis-view",
operation: INSPECT_ANALYSIS_VIEW_OPERATION,
parameters: {
parent_evidence_id: parent.evidence_id,
view: jsonValueSchema.parse(input.view),
},
result: jsonValueSchema.parse(result),
rawResult: null,
confidence:
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
// entity types and identity in an extracted state always are.
result.kind === "environment-state" ||
((result.kind === "api-surface" ||
result.kind === "environment-manifest") &&
result.templated_groups > 0)
? "inferred"
: "derived",
authority: parent.authority,
limitations: result.limitations,
...(result.kind === "native" && parent.relationship_coverage !== null
? { relationshipCoverage: parent.relationship_coverage }
: {}),
locations: [
...(result.artifact != null && result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
result.procedure_address !== null &&
result.procedure_address.length > 0
? [{ kind: "address" as const, address: result.procedure_address }]
: []),
],
evidenceLinks: [parent.evidence_id],
},
result: jsonValueSchema.parse(result),
rawResult: null,
confidence:
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
// entity types and identity in an extracted state always are.
result.kind === "environment-state" ||
((result.kind === "api-surface" ||
result.kind === "environment-manifest") &&
result.templated_groups > 0)
? "inferred"
: "derived",
authority: parent.authority,
limitations: result.limitations,
...(result.kind === "native" && parent.relationship_coverage !== null
? { relationshipCoverage: parent.relationship_coverage }
: {}),
locations: [
...(result.artifact != null && result.artifact.path.length > 0
? [{ kind: "artifact-path" as const, path: result.artifact.path }]
: []),
...(result.kind === "native" &&
result.procedure_address !== null &&
result.procedure_address.length > 0
? [{ kind: "address" as const, address: result.procedure_address }]
: []),
],
evidenceLinks: [parent.evidence_id],
});
{ immutable: true },
);
};
@@ -32,16 +32,13 @@ export const readEvidenceFileSource = async (
cause: signal.reason,
}),
);
const loaded = await readJsonFile(path, signal, resources);
if (!loaded.ok) {
if (loaded.error instanceof AnalysisCancelledError)
return err(
new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, {
cause: loaded.error.cause,
}),
);
return loaded;
}
const loaded = await readJsonFile(
path,
signal,
resources,
INSPECT_ANALYSIS_VIEW_OPERATION,
);
if (!loaded.ok) return loaded;
if (signal?.aborted)
return err(
new AnalysisCancelledError(INSPECT_ANALYSIS_VIEW_OPERATION, {
@@ -37,16 +37,13 @@ export const resolveSourceToBundleReference = async (
}),
);
if (!("kind" in reference)) return ok({ reference });
const read = await readJsonFileWithSource(reference.path, signal, resources);
if (!read.ok) {
return read.error instanceof AnalysisCancelledError
? err(
new AnalysisCancelledError("compare_source_to_bundle", {
cause: read.error.cause,
}),
)
: read;
}
const read = await readJsonFileWithSource(
reference.path,
signal,
resources,
"compare_source_to_bundle",
);
if (!read.ok) return read;
await setImmediate();
if (cancelled())
return err(
@@ -83,6 +83,7 @@ export const loadNativeMatchingBundleSources = async ({
paths[file]!,
admission.signal,
resources,
admission.operation,
);
if (!loaded.ok) throw loaded.error;
snapshots.push(await recordSnapshot(loaded.value, admission));
@@ -141,6 +142,7 @@ export const loadNativeMatchingBundleSources = async ({
paths[file]!,
admission.signal,
resources,
admission.operation,
);
if (!loaded.ok) throw loaded.error;
if ((await recordSnapshot(loaded.value, admission)) !== snapshots[file])
+68 -29
View File
@@ -1,6 +1,9 @@
import { z } from "incur";
import { inspectAnalysisViewValidatedWithFiles } from "../application/analysisView/AnalysisViewService.js";
import { inspectAnalysisViewFromBundle } from "../application/EvidenceBundleCommands.js";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { ANALYSIS_VIEW_PROVIDER } from "../application/InvestigationProviders.js";
import { readEvidenceBundle } from "../application/EvidenceBundleFiles.js";
import { inspectEvidenceBundle } from "../application/investigation/InspectEvidenceBundle.js";
import {
@@ -12,9 +15,11 @@ import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js";
import { logCliCommand } from "../cliLogging.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisView.js";
/** Project selected views of completed analysis Evidence through the CLI. */
@@ -54,7 +59,12 @@ export const registerAnalysisViewCommands = (
),
),
};
const loaded = await readEvidenceBundle(parsed.data.path);
const loaded = await readEvidenceBundle(
parsed.data.path,
undefined,
undefined,
"get_evidence_bundle",
);
if (!loaded.ok)
return {
error: "Evidence bundle inspection failed",
@@ -75,34 +85,63 @@ export const registerAnalysisViewCommands = (
args: z.object({
inputJson: z.string().describe("Inline workflow JSON or JSON file path"),
}),
run: ({ args }) =>
logCliCommand(logger, CLI_COMMANDS.inspectAnalysisView, async () => {
const input = await parseCliJsonInput(
args.inputJson,
CLI_COMMANDS.inspectAnalysisView,
);
if (!input.ok) return input.error;
// An evidence-file path is resolved against the operator's working directory.
const value = resolveCliJsonPaths(input.value, [["source", "path"]]);
const parsed = inspectAnalysisViewInputSchema.safeParse(value);
if (!parsed.success)
return {
error: "Application workflow failed",
...projectAnalysisError(
analysisInputErrorFromIssues(
CLI_COMMANDS.inspectAnalysisView,
parsed.error.issues,
value,
),
),
};
const result = await inspectAnalysisViewValidatedWithFiles(parsed.data);
return result.ok
? jsonValueSchema.parse(result.value)
: {
options: z.object({
evidenceBundle: localPathStringSchema
.optional()
.describe(
"Read the exact retained-evidence source ID from this local Evidence bundle without reopening the target; omit for inline Evidence",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectAnalysisView, async () => {
const input = await parseCliJsonInput(
args.inputJson,
CLI_COMMANDS.inspectAnalysisView,
signal,
);
if (!input.ok) return input.error;
// An evidence-file path is resolved against the operator's working directory.
const value = resolveCliJsonPaths(input.value, [["source", "path"]]);
const parsed = inspectAnalysisViewInputSchema.safeParse(value);
if (!parsed.success)
return {
error: "Application workflow failed",
...projectAnalysisError(result.error),
...projectAnalysisError(
analysisInputErrorFromIssues(
CLI_COMMANDS.inspectAnalysisView,
parsed.error.issues,
value,
),
),
};
}),
const resources = new ArtifactResourceScope();
const bundlePath = options.evidenceBundle;
const result = await finishAnalysisCommand(
() =>
bundlePath === undefined
? inspectAnalysisViewValidatedWithFiles(
parsed.data,
undefined,
{ signal, resources },
)
: inspectAnalysisViewFromBundle(
parsed.data,
bundlePath,
resources,
signal,
),
resources,
ANALYSIS_VIEW_PROVIDER.id,
"inspect_analysis_view",
signal,
);
// Preserve the inline command's existing error label without copying
// the authenticated completed Evidence or its partial observation.
return bundlePath === undefined && "code" in result
? { error: "Application workflow failed", ...result }
: result;
}),
),
});
};
+6 -3
View File
@@ -9,6 +9,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import type { Result } from "../domain/result.js";
import { dispatchQueuedInterrupts } from "../process/queuedInterrupts.js";
/** Finish a one-shot CLI operation while retaining primary results through close. */
export const finishAnalysisCommand = async <
@@ -59,16 +60,18 @@ export const finishAnalysisCommand = async <
);
}
if (result.ok) cleanup.retainPartialObservation(result.value);
return projectAnalysisError(
result.ok
result = {
ok: false,
error: result.ok
? cleanup
: analysisErrorWithCleanupFailure(
result.error,
cleanup,
canonicalOperation,
),
);
};
}
await dispatchQueuedInterrupts();
if (result.ok && signal.aborted)
return projectAnalysisError(
new AnalysisCancelledError(canonicalOperation, {
+27 -6
View File
@@ -17,6 +17,8 @@ import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { registerProjectManagedApplicationGraph } from "./managedProjectGraphCommand.js";
import { managedMembersInputSchema } from "../contracts/managed/managedToolContracts.js";
import { localPathStringSchema } from "../domain/localPath.js";
import { withCommandCancellation } from "./commandCancellation.js";
export const registerManagedCommands = (
cli: CliInstance,
@@ -64,14 +66,33 @@ const registerInspectManagedMembers = (
type: managedMembersInputSchema.shape.type_names.describe(
"Inspect only members declared by this exact type full name, and the string literals they load, such as System.Threading.Monitor; a nested type is named by its bare name, which selects every nested type of that name (repeatable)",
),
detail: managedMembersInputSchema.shape.detail.describe(
"complete returns full Evidence (default); summary returns identity, selection, coverage, limitations and collection counts and requires --evidence-bundle",
),
evidenceBundle: localPathStringSchema
.optional()
.describe(
"Write complete parent Evidence to a new local bundle for later inspect-analysis-view calls; never replaces an existing path",
),
}),
run: ({ args, options }) =>
logCliCommand(logger, "inspect-managed-members", () =>
runProviderAnalysis(
args.path,
"inspect_managed_members",
options.type === undefined ? {} : { type_names: options.type },
logger,
withCommandCancellation((signal) =>
logCliCommand(logger, "inspect-managed-members", () =>
runProviderAnalysis(
args.path,
"inspect_managed_members",
options.type === undefined ? {} : { type_names: options.type },
logger,
signal,
{
...(options.detail === undefined
? {}
: { detail: options.detail }),
...(options.evidenceBundle === undefined
? {}
: { evidenceBundlePath: options.evidenceBundle }),
},
),
),
),
});
+5 -2
View File
@@ -7,10 +7,13 @@ type DirectAnalysisStatusRuntime =
typeof import("../application/DirectAnalysisStatus.js");
type BoundOperation<Operation> = Operation extends (
dependencies: infer _Dependencies,
...args: infer Arguments
) => infer Result
? (...args: Arguments) => Result
? (
...args: Arguments extends [unknown, ...infer BoundArguments]
? BoundArguments
: never
) => Result
: never;
/** Bind one-shot analysis and status commands to one caller-selected environment. */
+2 -2
View File
@@ -118,7 +118,7 @@ export const analysisErrorRemediationAction = (
? "Correct the reported address, document, or arguments and retry."
: "Review the Hopper diagnostic details and correct the request or target; run `rea doctor` if the provider itself is failing. Repeating the unchanged request is not expected to succeed.";
if (error instanceof EvidenceReferenceError)
return "Use an exact Evidence reference retained by this session, supply the complete inline Evidence, re-run its producer, or import its Evidence bundle. close_binary clears retained records.";
return "Use an exact Evidence reference available from the selected source, supply the complete inline Evidence, re-run its producer, or select/import its Evidence bundle. close_binary clears a server session's retained records.";
if (
error instanceof BinaryTargetError &&
error.constraint === "directory_requires_file"
@@ -286,7 +286,7 @@ export const analysisErrorUserMessage = (error: AnalysisError): string => {
if (error instanceof ArtifactOperationError) return artifactMessage(error);
if (error instanceof EvidenceReferenceError)
return error.reason === "missing"
? `Evidence ${error.evidenceId} is not retained in this session. Supply complete inline Evidence, re-run its producer, or import its bundle before using this reference.`
? `Evidence ${error.evidenceId} is not available from the selected source. Supply complete inline Evidence, re-run its producer, or select/import its bundle before using this reference.`
: `Evidence ${error.evidenceId} does not match the requested reference (${error.reason}). Check the expected and actual identity in the diagnostic details.`;
if (error instanceof EvidenceIntegrityError)
return "Evidence is invalid or has changed. Recreate or re-import it, then try again.";
+16 -1
View File
@@ -24,6 +24,7 @@ import {
} from "./jsonValue.js";
import {
freezeJsonSnapshot,
freezeOwnedJsonTreeSnapshot,
canBorrowParsedJsonSnapshot,
isImmutableJsonSnapshot,
} from "./immutableJson.js";
@@ -346,11 +347,17 @@ const rememberImmutableEvidence = (evidence: Evidence): Evidence => {
return evidence;
};
/** Ownership policy for the freshly parsed Evidence snapshot. */
export interface EvidenceCreationOptions {
readonly immutable?: boolean;
}
/** Build deterministic Evidence from an immutable artifact subject. */
export const createEvidence = (
target: EvidenceSubjectTarget | BinaryTarget | undefined,
provider: EvidenceProvider,
observation: EvidenceObservation,
options?: EvidenceCreationOptions,
): Evidence => {
const { normalized, sharedResult } = normalizeEvidenceObservation(
target,
@@ -361,7 +368,15 @@ export const createEvidence = (
...normalized,
evidence_id: computeEvidenceId(normalized),
};
return sharedResult ? rememberImmutableEvidence(evidence) : evidence;
// Seal this already parsed, exclusively owned snapshot without authenticating
// or copying its payload again, including later partial-error delivery.
if (sharedResult || options?.immutable === true) {
// The normalizer's JSON parser clones mutable payloads into trees and only
// borrows previously sealed roots. No graph-sized sealing scratch is needed.
freezeOwnedJsonTreeSnapshot(evidence);
immutableEvidenceSnapshots.set(evidence, evidence);
}
return evidence;
};
/** Create Evidence from owned immutable JSON while exposing cooperative computation steps. */
+2 -1
View File
@@ -1,3 +1,4 @@
import { equalJsonText } from "./jsonSerialization.js";
import { z } from "zod";
import canonicalize from "canonicalize";
@@ -322,7 +323,7 @@ const authenticateBundle = (
}),
parsed.unknowns,
);
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
if (!equalJsonText(parsed, canonical))
throw new NonCanonicalEvidenceBundleError();
if (seal) {
freezeJsonSnapshot(canonical);
+68
View File
@@ -139,6 +139,74 @@ export const freezeJsonSnapshot = <T extends JsonValue>(value: T): T => {
return value;
};
/**
* Seal a freshly parsed, exclusively owned JSON tree. Its parser has removed
* aliases, accessors and hidden properties; existing sealed subtrees may be shared.
* Keep only the active ancestry rather than retaining every completed object.
* Unlike general graph sealing, failure can leave this provisional tree frozen.
*/
export const freezeOwnedJsonTreeSnapshot = <T extends JsonValue>(
value: T,
): T => {
const frames: { value: object; children: Iterator<unknown> }[] = [];
const ancestors = new Set<object>();
const enter = (item: unknown): void => {
if (isImmutableJsonSnapshot(item)) return;
if (typeof item !== "object" || item === null)
throw new TypeError("Immutable JSON snapshots require JSON values");
if (isProxy(item))
throw new TypeError("Immutable JSON snapshots cannot contain proxies");
if (ancestors.has(item))
throw new TypeError(
"Immutable JSON snapshots cannot contain circular references",
);
const prototype: unknown = Object.getPrototypeOf(item);
if (
!Array.isArray(item) &&
prototype !== Object.prototype &&
prototype !== null
)
throw new TypeError("Immutable JSON snapshots require ordinary objects");
ancestors.add(item);
frames.push({ value: item, children: ownedTreeChildren(item) });
};
enter(value);
while (frames.length > 0) {
const frame = frames[frames.length - 1]!;
const next = frame.children.next();
if (!next.done) enter(next.value);
else {
Object.freeze(frame.value);
ancestors.delete(frame.value);
frames.pop();
}
}
if (typeof value === "object" && value !== null)
immutableSnapshots.add(value);
return value;
};
function* ownedTreeChildren(value: object): Generator<unknown> {
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index++) {
const descriptor = Object.getOwnPropertyDescriptor(value, index);
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
throw new TypeError("Owned JSON trees require dense data arrays");
yield descriptor.value;
}
} else {
for (const key in value) {
if (!Object.hasOwn(value, key)) continue;
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (descriptor === undefined || !Object.hasOwn(descriptor, "value"))
throw new TypeError(
"Immutable JSON snapshots cannot contain accessors",
);
yield descriptor.value;
}
}
}
/**
* Seal exclusively owned JSON in bounded steps, authenticating only completion.
* Abandonment or failure can leave the provisional input partly frozen.
+26
View File
@@ -5,6 +5,7 @@ import {
bufferedJsonParts,
canonicalJsonParts,
jsonParts,
equalJsonText,
} from "./jsonSerialization.js";
/** Compact and pretty streamed JSON must equal native JSON. */
@@ -104,3 +105,28 @@ describe("iterative JSON encoding", () => {
expect(smallParts.length).toBeLessThan(20);
});
});
describe("JSON text equality without whole-document strings", () => {
it.each([
[null, null],
[-0, 0],
[
{ first: 1, omitted: undefined, second: 2 },
{ first: 1, second: 2 },
],
[
{ first: 1, second: 2 },
{ second: 2, first: 1 },
],
[
{ ["\u0000".repeat(9000)]: "\u0001😀\ud800".repeat(20000) },
{ ["\u0000".repeat(9000)]: "\u0001😀\ud800".repeat(20000) },
],
["prefix".repeat(20000) + "a", "prefix".repeat(20000) + "b"],
["prefix".repeat(20000), "prefix".repeat(20000) + "a"],
])("preserves native JSON comparison semantics", (left, right) => {
expect(equalJsonText(left, right)).toBe(
JSON.stringify(left) === JSON.stringify(right),
);
});
});
+51
View File
@@ -1,3 +1,23 @@
/** Exact UTF-16 code units of a JSON string, including escaping and quotes. */
export const jsonStringCodeUnits = (value: string): number => {
let length = value.length + 2;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code === 34 || code === 92) length += 1;
else if (code < 32)
length +=
code === 8 || code === 9 || code === 10 || code === 12 || code === 13
? 1
: 5;
else if (code >= 0xd800 && code <= 0xdfff) {
const next = value.charCodeAt(index + 1);
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) index += 1;
else length += 5;
}
}
return length;
};
/** Encode ordinary JSON as bounded string parts without a document-sized allocation. */
export function* jsonParts(value: unknown, pretty = false): Generator<string> {
yield* encodeValue(value, pretty, false);
@@ -8,6 +28,37 @@ export function* canonicalJsonParts(value: unknown): Generator<string> {
yield* encodeValue(value, false, true);
}
/** Compare ordinary JSON text in bounded parts, preserving native key order. */
export const equalJsonText = (left: unknown, right: unknown): boolean => {
const a = jsonParts(left);
const b = jsonParts(right);
let x = a.next();
let y = b.next();
let i = 0;
let j = 0;
try {
while (!x.done && !y.done) {
const length = Math.min(x.value.length - i, y.value.length - j);
if (x.value.slice(i, i + length) !== y.value.slice(j, j + length))
return false;
i += length;
j += length;
if (i === x.value.length) {
x = a.next();
i = 0;
}
if (j === y.value.length) {
y = b.next();
j = 0;
}
}
return x.done === true && y.done === true;
} finally {
a.return(undefined);
b.return(undefined);
}
};
/** Buffer JSON parts while preserving complete UTF-16 surrogate pairs. */
export function* bufferedJsonParts(parts: Iterable<string>): Generator<string> {
let buffer = "";
+4 -3
View File
@@ -29,6 +29,7 @@ interface ManagedPeFixtureOptions {
readonly moduleRowCount?: number;
readonly assemblyRowCount?: number;
readonly memberRefParentRaw?: number;
readonly memberRefRowCount?: number;
readonly memberRefName?: string;
readonly memberRefSignature?: Buffer;
/** Raw MethodDefOrRef coded values, one MethodSpec row each. */
@@ -771,7 +772,7 @@ const fixtureIndexWidths = ({
[4, 1],
[6, options.methodRowCount ?? 1],
[8, 0],
[10, 1],
[10, options.memberRefRowCount ?? 1],
[
12,
options.targetFramework === null
@@ -885,7 +886,7 @@ const fixtureMetadataRows = ({
],
[
10,
[
repeatedRows(options.memberRefRowCount ?? 1, () =>
memberRefRow({
name: indexes.constructorName,
signature: indexes.constructorSignature,
@@ -896,7 +897,7 @@ const fixtureMetadataRows = ({
? {}
: { parentRaw: options.memberRefParentRaw }),
}),
],
),
],
[12, customAttributeRows({ options, indexes, heapSizes }, widths)],
...(indexes.pinvokeModuleName === null
+7
View File
@@ -0,0 +1,7 @@
import { setImmediate } from "node:timers/promises";
/** Dispatch queued callbacks, then the OS signal notifications they may enqueue. */
export const dispatchQueuedInterrupts = async (): Promise<void> => {
await setImmediate();
await setImmediate();
};
@@ -37,11 +37,13 @@ export const registerBundleComparisonTool = (
input.left_bundle_path,
context.mcpReq.signal,
resources,
contract.name,
),
readImmutableEvidenceBundle(
input.right_bundle_path,
context.mcpReq.signal,
resources,
contract.name,
),
]);
if (!left.ok) return server.delivery.toCallToolResult(left, contract);
+1
View File
@@ -111,6 +111,7 @@ const registerImportEvidenceTool = ({
path,
signal,
resources,
importContract.name,
);
if (!loaded.ok)
return server.delivery.toCallToolResult(loaded, importContract);
+165 -2
View File
@@ -4,8 +4,13 @@ import {
} from "../../fixtures/historicalHar.js";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import { createHash } from "node:crypto";
import { mkdir, unlink, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import {
analysisCliErrorEnvelopeSchema,
analysisErrorProjectionSchema,
} from "../../../src/contracts/errorSchemas.js";
import { expect } from "vitest";
import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js";
@@ -29,6 +34,101 @@ import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { inspectEvidenceBundle } from "../../../src/application/investigation/InspectEvidenceBundle.js";
cliTest(
"authenticates an exact portable parent for non-managed views and rejects ambiguous or altered sources",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-analysis-bundle-cli-");
const path = join(root, "bundle.json");
const parent = analysisViewLayoutEvidence();
const view = {
kind: "item",
collection: "sections",
selector: { name: ".data" },
};
const request = {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view,
};
const bundle = createEvidenceBundle([
analysisViewJavaScriptEvidence(),
parent,
]);
const original = JSON.stringify(bundle);
await writeFile(path, original);
const result = await cli.run({
arguments: [
"inspect-analysis-view",
JSON.stringify(request),
"--evidence-bundle",
path,
"--json",
],
});
expect(result.exitCode).toBe(0);
const expected = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view,
});
if (!expected.ok) throw expected.error;
const evidence = parseEvidence(result.json);
expect(evidence.evidence_id).toBe(expected.value.evidence_id);
expect(evidence.normalized_result).toEqual(
expected.value.normalized_result,
);
expect(evidence.evidence_links).toEqual([parent.evidence_id]);
expect(await readFile(path, "utf8")).toBe(original);
for (const [source, expectedCode] of [
[{ kind: "inline", evidence: parent }, "invalid_request"],
[
{ kind: "retained-evidence", evidence_id: `ev_${"f".repeat(64)}` },
"evidence_integrity_mismatch",
],
] as const) {
const rejected = await cli.run({
arguments: [
"inspect-analysis-view",
JSON.stringify({ source, view }),
"--evidence-bundle",
path,
"--json",
],
});
expect(rejected.exitCode).toBe(1);
expect(rejected.json).toMatchObject({ code: expectedCode });
expect(rejected.json).not.toHaveProperty("evidence_id");
}
for (const [contents, expectedCode] of [
[JSON.stringify(parent), "invalid_request"],
["{invalid JSON", "evidence_integrity_mismatch"],
[
JSON.stringify({
...bundle,
records: bundle.records.map((record) => ({
...record,
normalized_result: "altered",
})),
}),
"evidence_integrity_mismatch",
],
] as const) {
await writeFile(path, contents);
const rejected = await cli.run({
arguments: [
"inspect-analysis-view",
JSON.stringify(request),
"--evidence-bundle",
path,
"--json",
],
});
expect(rejected.exitCode).toBe(1);
expect(rejected.json).toMatchObject({ code: expectedCode });
expect(rejected.json).not.toHaveProperty("evidence_id");
}
},
);
cliTest(
"preserves native relationship coverage independently of exhausted CLI pages",
async ({ cli }) => {
@@ -759,3 +859,66 @@ cliTest(
expect(JSON.stringify(result.json)).not.toContain("probe folder");
},
);
cliTest.for(
(process.platform === "win32"
? (["event"] as const)
: (["event", "os"] as const)
).flatMap((delivery) => [
{ signal: "SIGINT" as const, status: 130, delivery },
{ signal: "SIGTERM" as const, status: 143, delivery },
]),
)(
"retains inline projection when $signal ($delivery) is queued during final sealing",
async ({ signal, status, delivery }, { cli }) => {
const root = await createTestTempDirectory("rea-inline-view-signal-");
const parent = analysisViewLayoutEvidence(),
marker = join(root, "signal.json");
const preload = resolve(
"tests/fixtures/evidenceCompletionSignalPreload.mjs",
);
const reply = await cli.run({
arguments: [
"inspect-analysis-view",
JSON.stringify({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
}),
"--json",
],
environment: {
HOME: root,
REA_LOG_LEVEL: "silent",
REA_TEST_EVIDENCE_COMPLETION: JSON.stringify({
operation: "inspect_analysis_view",
parent_evidence_id: parent.evidence_id,
marker,
signal,
delivery,
}),
NODE_OPTIONS: `--max-old-space-size=2048 --import=${pathToFileURL(preload).href}`,
},
});
expect(reply.exitCode).toBe(status);
const { error: label, ...projection } =
analysisCliErrorEnvelopeSchema.parse(reply.json);
expect(label).toBe("Application workflow failed");
const error = analysisErrorProjectionSchema.parse(projection);
expect(error.code).toBe("cancelled");
expect(error.details?.operation).toBe("inspect_analysis_view");
const partial = parseEvidence(error.details?.partial_observation);
expect(partial.operation).toBe("inspect_analysis_view");
expect(partial.evidence_links).toEqual([parent.evidence_id]);
expect(partial.normalized_result).toMatchObject({
parent_evidence_id: parent.evidence_id,
kind: "summary",
});
const proof = JSON.parse(await readFile(marker, "utf8"));
expect(proof).toMatchObject({
evidence_id: partial.evidence_id,
signal,
delivery,
});
expect(proof.listeners).toBeGreaterThan(0);
},
);
@@ -1,4 +1,4 @@
import { writeFile } from "node:fs/promises";
import { readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
@@ -52,6 +52,38 @@ const revisionBundle = () => {
return createEvidenceBundle(records, [initial, revised]);
};
cliTest.for([
"export_evidence_bundle",
"compare_bundles",
"get_evidence_bundle",
])("attributes an early heap refusal to $0", async (operation, { cli }) => {
const root = await createTestTempDirectory("rea-bundle-operation-");
const path = join(root, "parent.json");
const output = join(root, "output.json");
await writeFile(path, JSON.stringify(revisionBundle()));
const preload =
"import v8 from 'node:v8';import {syncBuiltinESMExports} from 'node:module';const read=v8.getHeapStatistics;v8.getHeapStatistics=()=>({...read(),total_available_size:0});syncBuiltinESMExports();";
const arguments_ =
operation === "export_evidence_bundle"
? ["evidence-export", path, output]
: operation === "compare_bundles"
? ["compare", path, path]
: ["inspect-evidence-bundle", JSON.stringify({ path })];
const result = await cli.run({
arguments: [...arguments_, "--json"],
environment: {
REA_LOG_LEVEL: "silent",
NODE_OPTIONS: `--max-old-space-size=2048 --import=data:text/javascript;base64,${Buffer.from(preload).toString("base64")}`,
},
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "resource_constraint",
details: { operation },
});
expect(await readdir(root)).toEqual(["parent.json"]);
});
cliTest(
"reports standalone import counts for every unknown revision",
async ({ cli }) => {
+1 -1
View File
@@ -533,7 +533,7 @@ describe("CLI result output surface", () => {
},
});
const cli = Cli.create("rea-output-test", { sync: false });
registerManagedCommands(cli, silentLogger, async (path, operation) => {
registerManagedCommands(cli, silentLogger, async (...[path, operation]) => {
expect(path).toBe("fixture.dll");
expect(operation).toBe("inspect_managed_members");
return value;
@@ -0,0 +1,515 @@
import { pathToFileURL } from "node:url";
import {
mkdir,
readFile,
readdir,
realpath,
unlink,
writeFile,
} from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { inspectAnalysisView } from "../../../src/application/analysisView/AnalysisViewService.js";
import {
analysisCliErrorEnvelopeSchema,
analysisErrorProjectionSchema,
} from "../../../src/contracts/errorSchemas.js";
import {
analysisViewResultSchema,
managedMemberSummaryViewResultSchema,
} from "../../../src/domain/analysisView/analysisView.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js";
import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const memberFixture = (): Buffer =>
buildManagedPeFixture({
methodRowCount: 2,
memberRefRowCount: 3,
// ldarg.0; ldfld field; call member ref; ldstr "ok"; pop; ret
ilBody: Buffer.from([
(18 << 2) | 2,
0x02,
0x7b,
1,
0,
0,
0x04,
0x28,
1,
0,
0,
0x0a,
0x72,
1,
0,
0,
0x70,
0x26,
0x2a,
]),
userStringHeap: Buffer.from([0, 5, 0x6f, 0, 0x6b, 0, 0]),
exportedTypes: [
{
namespace: "System",
name: "Object",
flags: 0x0020_0001,
implementationRaw: (1 << 2) | 1,
},
],
});
cliTest(
"advertises durable summary and portable paging options in CLI help",
async ({ cli }) => {
for (const command of [
"inspect-managed-members",
"inspect-analysis-view",
]) {
const result = await cli.run({ arguments: [command, "--help"] });
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("--evidence-bundle");
if (command === "inspect-managed-members") {
expect(result.stdout).toContain("--detail");
expect(result.stdout).toContain("summary");
expect(result.stdout).toContain("complete");
}
}
},
);
cliTest(
"retains complete member Evidence for exact CLI pages after process exit and target removal",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-cli-");
const path = join(root, "fixture.dll");
const bundlePath = join(root, "members.json");
await writeFile(path, memberFixture());
const typeNames = ["Fixture.Program", "Missing.Type"];
const arguments_ = [
"inspect-managed-members",
path,
...typeNames.flatMap((type) => ["--type", type]),
"--json",
];
const omitted = await cli.run({ arguments: arguments_ });
expect(omitted.exitCode).toBe(0);
const complete = parseEvidence(omitted.json);
const completeBundlePath = join(root, "complete.json");
const explicit = await cli.run({
arguments: [
...arguments_,
"--detail",
"complete",
"--evidence-bundle",
completeBundlePath,
],
});
expect(explicit.exitCode).toBe(0);
expect(parseEvidence(explicit.json).evidence_id).toBe(complete.evidence_id);
expect(
parseEvidenceBundle(
JSON.parse(await readFile(completeBundlePath, "utf8")),
).records[0]?.evidence_id,
).toBe(complete.evidence_id);
const reply = await cli.run({
cwd: root,
arguments: [
...arguments_,
"--detail",
"summary",
"--evidence-bundle",
"members.json",
],
});
expect(reply.exitCode).toBe(0);
const receipt = parseEvidence(reply.json);
const summary = managedMemberSummaryViewResultSchema.parse(
receipt.normalized_result,
);
const originalBytes = await readFile(bundlePath, "utf8");
const bundle = parseEvidenceBundle(JSON.parse(originalBytes));
expect(bundle.records).toHaveLength(1);
const parent = bundle.records[0]!;
expect(parent.evidence_id).toBe(complete.evidence_id);
expect(parent.normalized_result).toEqual(complete.normalized_result);
expect(parent.raw_result).toEqual(complete.raw_result);
expect(parent.subject).toEqual(complete.subject);
expect(parent.locations).toEqual(complete.locations);
expect(parent.parameters).toEqual({ type_names: typeNames });
expect(receipt.evidence_links).toEqual([parent.evidence_id]);
const portable = inspectAnalysisView({
source: { kind: "inline", evidence: parent },
view: { kind: "summary" },
});
if (!portable.ok) throw portable.error;
expect(receipt.evidence_id).toBe(portable.value.evidence_id);
expect(receipt.normalized_result).toEqual(portable.value.normalized_result);
const report = managedMemberInspectionSchema.parse(
parent.normalized_result,
);
expect(summary.summary.selection).toEqual({
type_names: typeNames,
unmatched_type_names: ["Missing.Type"],
});
expect(summary.summary.coverage).toEqual(report.coverage);
expect(summary.summary.identity_scope).toEqual(report.identity_scope);
expect(summary.summary).not.toHaveProperty("member_refs");
expect(summary.summary).not.toHaveProperty("methods");
await unlink(path);
const otherDirectory = join(root, "later-process");
await mkdir(otherDirectory);
for (const collection of [
"types",
"exported_types",
"fields",
"methods",
"member_refs",
"call_edges",
"field_accesses",
"user_strings",
] as const) {
const rows =
collection === "user_strings"
? report.user_strings?.literals
: report[collection];
if (rows == null) throw new Error(`${collection} was not examined`);
expect(rows.length, collection).toBeGreaterThan(0);
expect(summary.summary.reported_counts[collection]).toBe(rows.length);
const paged: unknown[] = [];
for (let offset: number | null = 0; offset !== null;) {
const response = await cli.run({
cwd: otherDirectory,
arguments: [
"inspect-analysis-view",
JSON.stringify({
source: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
view: { kind: "managed-members", collection, offset, limit: 1 },
}),
"--evidence-bundle",
bundlePath,
"--json",
],
});
expect(response.exitCode).toBe(0);
const evidence = parseEvidence(response.json);
expect(evidence.evidence_links).toEqual([parent.evidence_id]);
expect(evidence.subject).toEqual(parent.subject);
const page = analysisViewResultSchema.parse(evidence.normalized_result);
if (page.kind !== "managed-members") throw new Error("Wrong page kind");
expect(page.page.collection).toBe(collection);
expect(page.selection).toEqual(report.selection);
expect(page.source_coverage).toEqual(report.coverage);
expect(page.source_metadata_status).toBe(report.metadata.status);
expect(page.coverage.total).toBe(rows.length);
paged.push(...page.page.rows);
offset = page.coverage.next_offset;
}
expect(paged).toEqual(rows);
}
expect(await readFile(bundlePath, "utf8")).toBe(originalBytes);
},
180_000,
);
cliTest(
"refuses summary without a durable destination before opening the target",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-missing-");
const result = await cli.run({
arguments: [
"inspect-managed-members",
join(root, "absent.dll"),
"--detail",
"summary",
"--json",
],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: {
issues: [
expect.objectContaining({
path: ["evidence_bundle"],
message: expect.stringContaining("summary"),
}),
],
},
});
expect(result.json).not.toHaveProperty("evidence_id");
},
);
cliTest(
"does not replace caller files or report successful summary when bundle publication fails",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-write-");
const path = join(root, "fixture.dll");
const bytes = buildManagedPeFixture();
await writeFile(path, bytes);
const complete = await cli.run({
arguments: ["inspect-managed-members", path, "--json"],
});
expect(complete.exitCode).toBe(0);
const parent = parseEvidence(complete.json);
for (const destination of [path, join(root, "absent", "bundle.json")]) {
const result = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(result.exitCode).toBe(1);
expect(result.json).toMatchObject({
code: "invalid_request",
details: {
path: destination,
reason: destination === path ? "exists" : "missing",
},
});
const {
error: _label,
input_path: _inputPath,
input_reason: _inputReason,
...projection
} = analysisCliErrorEnvelopeSchema.parse(result.json);
const failure = analysisErrorProjectionSchema.parse(projection);
if (failure.code !== "invalid_request")
throw new Error(`Unexpected publication failure: ${failure.code}`);
const retained = parseEvidence(failure.details?.partial_observation);
expect(retained.evidence_id).toBe(parent.evidence_id);
expect(retained.normalized_result).toEqual(parent.normalized_result);
expect(result.json).not.toHaveProperty("evidence_id");
expect(result.json).not.toHaveProperty("normalized_result");
}
expect(await readFile(path)).toEqual(bytes);
expect(await readdir(root)).toEqual(["fixture.dll"]);
},
);
cliTest(
"preserves provider refusal without publishing an Evidence bundle",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-provider-");
const path = join(root, "absent.dll");
const destination = join(root, "bundle.json");
const complete = await cli.run({
arguments: ["inspect-managed-members", path, "--json"],
});
const summary = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(complete.exitCode).toBe(1);
expect(summary.exitCode).toBe(1);
expect(summary.json).toEqual(complete.json);
await expect(readFile(destination)).rejects.toMatchObject({
code: "ENOENT",
});
},
);
cliTest(
"keeps unavailable managed member coverage explicit in saved summary Evidence",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-member-summary-unknown-");
const path = join(root, "malformed.dll");
const destination = join(root, "bundle.json");
await writeFile(path, buildManagedPeFixture({ virtualSectionSize: 0 }));
const result = await cli.run({
arguments: [
"inspect-managed-members",
path,
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
});
expect(result.exitCode).toBe(0);
const summary = managedMemberSummaryViewResultSchema.parse(
parseEvidence(result.json).normalized_result,
);
expect(summary.summary).toMatchObject({
metadata: { status: "malformed" },
coverage: { state: "unavailable" },
reported_counts: { methods: 0 },
});
expect(summary.unknowns.length).toBeGreaterThan(0);
const bundle = parseEvidenceBundle(
JSON.parse(await readFile(destination, "utf8")),
);
const report = managedMemberInspectionSchema.parse(
bundle.records[0]!.normalized_result,
);
expect(summary.summary.coverage).toEqual(report.coverage);
expect(summary.limitations).toEqual(report.limitations);
},
);
cliTest.for(
(process.platform === "win32"
? (["event"] as const)
: (["event", "os"] as const)
).flatMap((delivery) => [
{ signal: "SIGINT" as const, status: 130, delivery },
{ signal: "SIGTERM" as const, status: 143, delivery },
]),
)(
"keeps a committed receipt when $signal ($delivery) is queued during final cleanup",
async ({ signal, status, delivery }, { cli }) => {
const root = await createTestTempDirectory("rea-managed-queued-signal-");
const target = join(root, "fixture.dll");
const destination = join(root, "parent.json");
const marker = join(root, "signal.json");
const preload = resolve(
"tests/fixtures/managedPublicationSignalPreload.mjs",
);
const prefix = join(await realpath(root), ".rea-write-");
await writeFile(target, memberFixture());
const reply = await cli.run({
cwd: root,
arguments: [
"inspect-managed-members",
target,
"--type",
"Fixture.Program",
"--detail",
"summary",
"--evidence-bundle",
destination,
"--json",
],
environment: {
HOME: root,
REA_LOG_LEVEL: "silent",
REA_TEST_MANAGED_PUBLICATION: JSON.stringify({
prefix,
marker,
signal,
delivery,
}),
NODE_OPTIONS: `--max-old-space-size=2048 --import=${pathToFileURL(preload).href}`,
},
});
expect(reply.exitCode).toBe(status);
const { error: _label, ...projection } =
analysisCliErrorEnvelopeSchema.parse(reply.json);
const error = analysisErrorProjectionSchema.parse(projection);
expect(error.code).toBe("cancelled");
expect(error.details?.operation).toBe("inspect_managed_members");
const bytes = await readFile(destination);
const parent = parseEvidenceBundle(JSON.parse(bytes.toString()))
.records[0]!;
expect(error.details).toMatchObject({
diagnostics: {
evidence_bundle: { path: destination, bytes: bytes.length },
},
});
const partial = parseEvidence(error.details?.partial_observation);
expect(partial.evidence_links).toEqual([parent.evidence_id]);
expect(partial.normalized_result).toMatchObject({
kind: "summary",
parent_evidence_id: parent.evidence_id,
});
expect(JSON.parse(await readFile(marker, "utf8"))).toMatchObject({
signal,
listeners: expect.any(Number),
});
expect(
JSON.parse(await readFile(marker, "utf8")).listeners,
).toBeGreaterThan(0);
expect(
(await readdir(root)).some((name) => name.startsWith(".rea-write-")),
).toBe(false);
},
);
cliTest.for(
(process.platform === "win32"
? (["event"] as const)
: (["event", "os"] as const)
).flatMap((delivery) => [
{ signal: "SIGINT" as const, status: 130, delivery },
{ signal: "SIGTERM" as const, status: 143, delivery },
]),
)(
"retains complete default Evidence when $signal ($delivery) is queued during sealing",
async ({ signal, status, delivery }, { cli }) => {
const root = await createTestTempDirectory("rea-managed-complete-signal-");
const target = join(root, "fixture.dll"),
marker = join(root, "signal.json");
await writeFile(target, memberFixture());
const preload = resolve(
"tests/fixtures/evidenceCompletionSignalPreload.mjs",
);
const reply = await cli.run({
arguments: [
"inspect-managed-members",
target,
"--type",
"Fixture.Program",
"--json",
],
environment: {
HOME: root,
REA_LOG_LEVEL: "silent",
REA_TEST_EVIDENCE_COMPLETION: JSON.stringify({
operation: "inspect_managed_members",
target_path: target,
marker,
signal,
delivery,
}),
NODE_OPTIONS: `--max-old-space-size=2048 --import=${pathToFileURL(preload).href}`,
},
});
expect(reply.exitCode).toBe(status);
const { error: _label, ...projection } =
analysisCliErrorEnvelopeSchema.parse(reply.json);
const error = analysisErrorProjectionSchema.parse(projection);
expect(error.code).toBe("cancelled");
expect(error.details?.operation).toBe("inspect_managed_members");
const partial = parseEvidence(error.details?.partial_observation);
expect(partial.operation).toBe("inspect_managed_members");
expect(partial.subject?.local_path).toBe(target);
expect(
managedMemberInspectionSchema.parse(partial.normalized_result).methods,
).toHaveLength(2);
const proof = JSON.parse(await readFile(marker, "utf8"));
expect(proof).toMatchObject({
evidence_id: partial.evidence_id,
signal,
delivery,
});
expect(proof.listeners).toBeGreaterThan(0);
expect((await readdir(root)).sort()).toEqual([
"fixture.dll",
"signal.json",
]);
},
);
@@ -18,20 +18,26 @@ import {
readImmutableEvidenceBundle,
writeEvidenceBundle,
} from "../../../src/application/EvidenceBundleFiles.js";
import { compareEvidenceBundlesCommand } from "../../../src/application/EvidenceBundleCommands.js";
import {
compareEvidenceBundlesCommand,
deliverAnalysisEvidence,
} from "../../../src/application/EvidenceBundleCommands.js";
import { EvidenceLedger } from "../../../src/application/investigation/EvidenceLedger.js";
import { RegularFileCleanupFailure } from "../../../src/application/RegularFileRead.js";
import { OwnedFileHandle } from "../../../src/filesystem/OwnedFileHandle.js";
import { OwnedStagedOutput } from "../../../src/filesystem/OwnedStagedOutput.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { createEvidence } from "../../../src/domain/evidence.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { writeTextParts } from "../../../src/application/JsonFiles.js";
import { failFileReadAfterAbort } from "../../support/failFileReadAfterAbort.js";
import { holdFirstFileRead } from "../../support/holdFirstFileRead.js";
import { JsonInputPhysicalReadFailure } from "../../../src/application/JsonInputFile.js";
import {
createEvidenceBundle,
parseEvidenceBundle,
serializeEvidenceBundle,
} from "../../../src/domain/evidenceBundle.js";
import { analysisViewLayoutEvidence } from "../../fixtures/analysisView.js";
const bundle = (result = true) =>
createEvidenceBundle([
@@ -58,6 +64,7 @@ it.each([
error: {
_tag: "AnalysisCancelledError",
operation: "import_evidence_bundle",
cause: controller.signal.reason,
},
});
if (result.ok) throw new Error("Expected cancelled bundle read");
@@ -194,6 +201,32 @@ it("preserves the failed-close owner when import is also cancelled", async () =>
});
describe("evidence bundle export cancellation", () => {
it("retains the completed parent and exact reason when delivery is cancelled before publication", async () => {
const root = await createTestTempDirectory("rea-delivery-cancel-");
const parent = analysisViewLayoutEvidence();
const controller = new AbortController();
const reason = new Error("cancel before bundle publication");
controller.abort(reason);
const result = await deliverAnalysisEvidence(
parent,
join(root, "bundle.json"),
"summary",
controller.signal,
);
if (result.ok) throw new Error("Cancelled delivery reported success");
expect(result.error).toMatchObject({
_tag: "AnalysisCancelledError",
operation: parent.operation,
});
expect(projectAnalysisError(result.error)).toMatchObject({
code: "cancelled",
details: { operation: parent.operation },
});
expect(result.error.cause).toBe(reason);
expect(result.error.partialObservation).toBe(parent);
expect(await readdir(root)).toEqual([]);
});
it("does not inspect the path or advance a source cancelled before writing", async () => {
const root = await createTestTempDirectory("rea-export-pre-cancel-");
const controller = new AbortController();
@@ -213,6 +246,7 @@ describe("evidence bundle export cancellation", () => {
error: {
_tag: "AnalysisCancelledError",
operation: "export_evidence_bundle",
cause: controller.signal.reason,
},
});
expect(advanced).toBe(false);
@@ -224,7 +258,13 @@ describe("evidence bundle export cancellation", () => {
false,
controller.signal,
),
).toMatchObject({ ok: false, error: { _tag: "AnalysisCancelledError" } });
).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
cause: controller.signal.reason,
},
});
expect(await readdir(root)).toEqual([]);
});
@@ -296,6 +336,68 @@ describe("evidence bundle export cancellation", () => {
});
});
it.each([1, 2])(
"reports only unresolved cancellation cleanup after %s failed close attempts",
async (failedAttempts) => {
const root = await createTestTempDirectory("rea-delivery-cleanup-retry-");
const parent = analysisViewLayoutEvidence();
const controller = new AbortController();
const reason = new Error("cancel before bundle publication");
const cleanupCause = new Error("fixture staged-output close failure");
const ready = OwnedStagedOutput.prototype.assertReadyForPublication;
const close = OwnedStagedOutput.prototype.close;
let owner: OwnedStagedOutput | undefined;
let attempts = 0;
const publication = vi
.spyOn(OwnedStagedOutput.prototype, "assertReadyForPublication")
.mockImplementationOnce(async function (this: OwnedStagedOutput, staged) {
await ready.call(this, staged);
controller.abort(reason);
});
const cleanup = vi
.spyOn(OwnedStagedOutput.prototype, "close")
.mockImplementation(async function (this: OwnedStagedOutput) {
owner = this;
if (++attempts <= failedAttempts) throw cleanupCause;
await close.call(this);
});
try {
const result = await deliverAnalysisEvidence(
parent,
join(root, "bundle.json"),
"summary",
controller.signal,
);
if (result.ok) throw new Error("Cancelled delivery reported success");
expect(attempts).toBe(2);
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.partialObservation).toBe(parent);
expect(result.error.cause).toBe(reason);
expect(result.error.cleanupPrimary).toMatchObject({
operation: parent.operation,
});
expect(result.error.cleanupPrimary?.cause).toBe(reason);
expect(result.error.cleanup?.reason).toBe(cleanupCause.message);
const incomplete = failedAttempts === 2;
expect(result.error.cleanupIncomplete).toBe(incomplete);
expect(result.error.cleanupResources).toEqual(
incomplete ? [owner?.directory] : [],
);
expect(projectAnalysisError(result.error)).toMatchObject({
code: incomplete ? "cleanup_incomplete" : "cancelled",
details: { operation: parent.operation },
});
expect((await readdir(root)).map((entry) => join(root, entry))).toEqual(
incomplete ? [owner?.directory] : [],
);
} finally {
publication.mockRestore();
cleanup.mockRestore();
await owner?.close();
}
},
);
const writeBundle = (
value: ReturnType<typeof bundle>,
path: string,
@@ -320,6 +422,50 @@ const writeParts = (
);
describe("evidence bundle publication", () => {
it("retains the committed bundle receipt when cancellation arrives during final cleanup", async () => {
const root = await createTestTempDirectory(
"rea-delivery-published-cancel-",
);
const path = join(root, "bundle.json");
const parent = analysisViewLayoutEvidence();
const controller = new AbortController();
const reason = new Error("cancel after actual staged-output cleanup");
const close = OwnedStagedOutput.prototype.close;
const seam = vi
.spyOn(OwnedStagedOutput.prototype, "close")
.mockImplementationOnce(async function (this: OwnedStagedOutput) {
await close.call(this);
controller.abort(reason);
});
let result: Awaited<ReturnType<typeof deliverAnalysisEvidence>>;
try {
result = await deliverAnalysisEvidence(
parent,
path,
"summary",
controller.signal,
);
} finally {
seam.mockRestore();
}
if (result.ok) throw new Error("Cancelled delivery reported success");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBe(reason);
const bytes = await readFile(path);
expect(result.error.diagnostics?.evidence_bundle).toEqual({
path,
bytes: bytes.length,
});
expect(
parseEvidence(result.error.partialObservation).evidence_links,
).toEqual([parent.evidence_id]);
expect(
parseEvidenceBundle(JSON.parse(bytes.toString("utf8"))).records[0]
?.evidence_id,
).toBe(parent.evidence_id);
expect(await readdir(root)).toEqual(["bundle.json"]);
});
it.each([false, true])(
"never publishes a partial streamed file (overwrite: %s)",
async (overwrite) => {
@@ -1,3 +1,5 @@
import { readdir } from "node:fs/promises";
import { dirname, join } from "node:path";
import { describe, expect, it } from "vitest";
import {
runDirectAnalysis,
@@ -8,7 +10,11 @@ import type { DirectAnalysisDependencies } from "../../src/application/DirectAna
import { parseConfig } from "../../src/config/parseConfig.js";
import { analysisCliErrorEnvelopeSchema } from "../../src/contracts/errorSchemas.js";
import type { AnalysisError } from "../../src/domain/analysisErrorBase.js";
import { AnalysisTimeoutError } from "../../src/domain/analysisErrorCore.js";
import {
AnalysisCancelledError,
AnalysisInputError,
AnalysisTimeoutError,
} from "../../src/domain/analysisErrorCore.js";
import {
projectAnalysisError,
projectAnalysisErrorWithoutCapturedOutput,
@@ -44,6 +50,7 @@ const factories = (
readonly thrownExecution?: Error;
readonly cleanupError?: AnalysisError;
readonly thrownCleanup?: Error;
readonly onClose?: () => void;
} = {},
) => {
const session = createTestBinarySession(() => ({
@@ -56,6 +63,7 @@ const factories = (
},
close: async () => {
if (options.thrownCleanup !== undefined) throw options.thrownCleanup;
options.onClose?.();
return options.cleanupError === undefined
? ok(null)
: err(options.cleanupError);
@@ -222,6 +230,85 @@ describe("one-shot analysis cleanup outcomes", () => {
});
});
it.each(["complete", "summary"] as const)(
"retains completed Evidence when cancellation arrives during managed close before %s delivery",
async (detail) => {
const [path] = await createBinarySessionTargets();
const files = await readdir(dirname(path));
const original = parseEvidence(
await runProviderAnalysis(
factories().dependencies,
path,
"inspect_managed_members",
{},
),
);
const controller = new AbortController();
const reason = new Error("cancel during managed close");
const { dependencies, session } = factories({
onClose: () => controller.abort(reason),
});
const result =
detail === "summary"
? await runProviderAnalysis(
dependencies,
path,
"inspect_managed_members",
{},
undefined,
controller.signal,
{
detail,
evidenceBundlePath: join(dirname(path), "bundle.json"),
},
)
: await runProviderAnalysis(
dependencies,
path,
"inspect_managed_members",
{},
undefined,
controller.signal,
);
expect(result).toEqual({
error: "Analysis failed",
...projectAnalysisError(
new AnalysisCancelledError("inspect_managed_members", {
cause: reason,
partialObservation: original,
}),
),
});
expect(session.activeTarget()).toBeUndefined();
expect(await readdir(dirname(path))).toEqual(files);
},
);
it("preserves a managed provider failure when cancellation arrives during close", async () => {
const [path] = await createBinarySessionTargets();
const controller = new AbortController();
const failure = new AnalysisInputError("inspect_managed_members", undefined, [
{ path: ["path"], reason: "invalid_format" },
]);
const { dependencies, session } = factories({
executionError: failure,
onClose: () => controller.abort(new Error("later cancellation")),
});
const result = await runProviderAnalysis(
dependencies,
path,
"inspect_managed_members",
{},
undefined,
controller.signal,
);
expect(result).toEqual({
error: "Analysis failed",
...projectAnalysisError(failure),
});
expect(session.activeTarget()).toBeUndefined();
});
describe("managed one-shot cleanup outcomes", () => {
it("returns a managed cleanup error retaining the complete successful execution", async () => {
const [path] = await createBinarySessionTargets();
@@ -56,6 +56,57 @@ const recordingFactories = (
return { dependencies, sessions, clientsClosed: () => clientsClosed };
};
it("requires a durable summary destination before acquiring a managed session", async () => {
const factories = recordingFactories();
const result = await runProviderAnalysis(
factories.dependencies,
"/fixture/missing.dll",
"inspect_managed_members",
{},
undefined,
undefined,
{ detail: "summary" },
);
expect(result).toMatchObject({
code: "invalid_request",
details: {
operation: "inspect_managed_members",
issues: [{ path: ["evidence_bundle"], reason: "missing_argument" }],
},
});
expect(factories.sessions).toEqual([]);
});
it.each([
"inspect_macho",
"inventory_artifact",
"inspect_managed_artifact",
] as const)(
"rejects delivery options for %s before selecting a provider",
async (tool) => {
const factories = recordingFactories();
const invocation = [
factories.dependencies,
"/fixture/missing",
tool,
{},
undefined,
undefined,
{ detail: "summary", evidenceBundlePath: "/fixture/bundle.json" },
] as const;
// @ts-expect-error JavaScript callers can bypass managed-members-only delivery.
const result = await runProviderAnalysis(...invocation);
expect(result).toMatchObject({
code: "invalid_request",
details: {
operation: tool,
issues: [{ path: ["delivery"], reason: "invalid_value" }],
},
});
expect(factories.sessions).toEqual([]);
},
);
describe("one-shot analysis factory boundary", () => {
it("preserves thrown failures while releasing clients and cancellation listeners", async () => {
const [path] = await createBinarySessionTargets();
+51
View File
@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import { writeFileSync } from "node:fs";
const input = JSON.parse(process.env.REA_TEST_EVIDENCE_COMPLETION ?? "null");
assert.ok(input && typeof input.marker === "string");
assert.ok(
["inspect_managed_members", "inspect_analysis_view"].includes(
input.operation,
),
);
assert.ok(["SIGINT", "SIGTERM"].includes(input.signal));
assert.ok(["event", "os"].includes(input.delivery));
assert.ok(
input.operation === "inspect_managed_members"
? typeof input.target_path === "string"
: typeof input.parent_evidence_id === "string",
);
const freeze = Object.freeze;
let queued = false;
Object.freeze = (value) => {
const frozen = freeze(value);
if (
!queued &&
value &&
typeof value === "object" &&
value.operation === input.operation &&
typeof value.evidence_id === "string" &&
/^ev_[a-f0-9]{64}$/.test(value.evidence_id) &&
(input.operation === "inspect_managed_members"
? value.subject?.local_path === input.target_path
: Array.isArray(value.evidence_links) &&
value.evidence_links.includes(input.parent_evidence_id))
) {
queued = true;
setImmediate(() => {
writeFileSync(
input.marker,
JSON.stringify({
operation: input.operation,
evidence_id: value.evidence_id,
signal: input.signal,
delivery: input.delivery,
listeners: process.listenerCount(input.signal),
}),
);
if (input.delivery === "os") process.kill(process.pid, input.signal);
else process.emit(input.signal);
});
}
return frozen;
};
+33
View File
@@ -0,0 +1,33 @@
import assert from "node:assert/strict";
import { createRequire, syncBuiltinESMExports } from "node:module";
import { writeFileSync } from "node:fs";
const input = JSON.parse(process.env.REA_TEST_MANAGED_PUBLICATION ?? "null");
assert.ok(
input && typeof input.prefix === "string" && typeof input.marker === "string",
);
assert.ok(["SIGINT", "SIGTERM"].includes(input.signal));
assert.ok(["event", "os"].includes(input.delivery));
const require = createRequire(import.meta.url);
const fs = require("node:fs/promises");
const remove = fs.rmdir;
let queued = false;
fs.rmdir = async (...arguments_) => {
const result = await remove(...arguments_);
if (!queued && String(arguments_[0]).startsWith(input.prefix)) {
queued = true;
setImmediate(() => {
writeFileSync(
input.marker,
JSON.stringify({
signal: input.signal,
delivery: input.delivery,
listeners: process.listenerCount(input.signal),
}),
);
if (input.delivery === "os") process.kill(process.pid, input.signal);
else process.emit(input.signal);
});
}
return result;
};
syncBuiltinESMExports();
@@ -1,13 +1,22 @@
import { readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Cli } from "incur";
import { afterEach, expect, it, vi } from "vitest";
import * as analysisViewService from "../../src/application/analysisView/AnalysisViewService.js";
import { WasmArtifactService } from "../../src/application/wasm/WasmArtifactService.js";
import { registerAnalysisViewCommands } from "../../src/cli/analysisViewCommands.js";
import { registerWasmCommands } from "../../src/cli/wasmCommands.js";
import { wasmArtifactModule } from "../../src/composition/wasm.js";
import { AnalysisOutputError } from "../../src/domain/analysisErrorCore.js";
import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js";
import { ProviderCleanupError } from "../../src/domain/providerCleanupError.js";
import { err, ok } from "../../src/domain/result.js";
import { OwnedFileHandle } from "../../src/filesystem/OwnedFileHandle.js";
import { silentLogger } from "../../src/logger.js";
import { analysisViewLayoutEvidence } from "../fixtures/analysisView.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import {
wasmArtifactExecution,
wasmArtifactFixture,
@@ -15,7 +24,12 @@ import {
const host = vi.hoisted(() => ({
listeners: new Map<string, () => void>(),
exitCode: undefined as number | undefined,
get exitCode() {
return process.exitCode;
},
set exitCode(value: typeof process.exitCode) {
process.exitCode = value;
},
on(signal: string, listener: () => void) {
this.listeners.set(signal, listener);
},
@@ -40,7 +54,6 @@ vi.mock("../../src/cli/commandCancellation.js", async (importOriginal) => {
afterEach(() => {
vi.restoreAllMocks();
host.listeners.clear();
host.exitCode = undefined;
});
it.each([
@@ -86,7 +99,9 @@ it.each([
registerWasmCommands(cli, silentLogger, {});
let output = "";
const previousExitCode = process.exitCode;
let exitCode;
try {
process.exitCode = undefined;
await cli.serve(
["inspect-wasm-artifact", value.artifact.path, "--json"],
{
@@ -96,6 +111,7 @@ it.each([
exit: () => {},
},
);
exitCode = process.exitCode;
} finally {
process.exitCode = previousExitCode;
}
@@ -104,13 +120,13 @@ it.each([
expect(host.listeners.size).toBe(0);
if (scenario === "control") {
expect(result.normalized_result).toEqual(value);
expect(host.exitCode).toBeUndefined();
expect(exitCode).toBeUndefined();
} else if (scenario === "primary-failure") {
expect(result).toMatchObject({
code: "unreadable_output",
details: { reason: "primary reply failure" },
});
expect(host.exitCode).toBe(130);
expect(exitCode).toBe(130);
} else {
expect(result).toMatchObject({
code: scenario === "interrupt" ? "cancelled" : "cleanup_incomplete",
@@ -124,7 +140,143 @@ it.each([
: { resources: ["exact-owned-root"] }),
},
});
if (scenario !== "cleanup-failure") expect(host.exitCode).toBe(130);
if (scenario !== "cleanup-failure") expect(exitCode).toBe(130);
}
},
);
it.each([
["bundle", "control", "close"],
["bundle", "SIGINT", "close"],
["bundle", "SIGTERM", "close"],
["evidence-file", "control", "close"],
["evidence-file", "SIGINT", "close"],
["evidence-file", "SIGTERM", "close"],
["request-file", "control", "close"],
["request-file", "SIGINT", "close"],
["request-file", "SIGTERM", "close"],
["bundle", "SIGINT", "authentication"],
["bundle", "SIGTERM", "authentication"],
["bundle", "SIGINT", "projection"],
["bundle", "SIGTERM", "projection"],
["bundle", "SIGINT", "authentication-failure"],
["bundle", "SIGTERM", "projection-failure"],
] as const)(
"preserves the analysis-view result and exit status for %s: %s during %s",
async (source, signal, phase) => {
const root = await createTestTempDirectory("rea-analysis-view-signal-");
const parent = analysisViewLayoutEvidence();
const filename = `${source}.json`;
const path = join(root, filename);
const request = {
source:
source === "bundle"
? {
kind: "retained-evidence",
evidence_id:
phase === "projection-failure"
? `ev_${"f".repeat(64)}`
: parent.evidence_id,
}
: source === "evidence-file"
? { kind: "evidence-file", path }
: { kind: "inline", evidence: parent },
view: { kind: "summary" },
};
await writeFile(
path,
JSON.stringify(
source === "bundle"
? {
...createEvidenceBundle([parent]),
...(phase === "authentication-failure"
? { records: [{ ...parent, normalized_result: "altered" }] }
: {}),
}
: source === "evidence-file"
? parent
: request,
),
);
let interruptHandled = false;
let pendingInterrupt: Promise<void> | undefined;
const interrupt = () => {
const listener = host.listeners.get(signal);
interruptHandled = listener !== undefined;
listener?.();
};
const queueInterrupt = () => {
pendingInterrupt = new Promise((resolve) => {
setImmediate(() => {
interrupt();
resolve();
});
});
};
const originalProject = analysisViewService.inspectAnalysisViewValidated;
let projected: ReturnType<typeof originalProject> | undefined;
const project = vi
.spyOn(analysisViewService, "inspectAnalysisViewValidated")
.mockImplementation((...args) => {
projected = originalProject(...args);
if (phase.startsWith("projection")) queueInterrupt();
return projected;
});
const originalClose = OwnedFileHandle.prototype.close;
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
await originalClose.call(this);
expect(this.handle.fd).toBe(-1);
if (signal !== "control" && phase === "close") interrupt();
if (phase.startsWith("authentication")) queueInterrupt();
});
const cli = Cli.create("analysis-view-finalization-test", { sync: false });
registerAnalysisViewCommands(cli, silentLogger);
let output = "";
const previousExitCode = process.exitCode;
try {
process.exitCode = undefined;
await cli.serve(
[
"inspect-analysis-view",
source === "request-file" ? path : JSON.stringify(request),
...(source === "bundle" ? ["--evidence-bundle", path] : []),
"--json",
],
{
stdout: (part) => {
output += part;
},
exit: () => {},
},
);
expect(close).toHaveBeenCalledOnce();
expect(host.listeners.size).toBe(0);
expect(await readdir(root)).toEqual([filename]);
const result = JSON.parse(output);
expect(result).toMatchObject(
signal === "control"
? { operation: "inspect_analysis_view" }
: {
code: phase.endsWith("failure")
? "evidence_integrity_mismatch"
: "cancelled",
},
);
expect(process.exitCode ?? 0).toBe(
signal === "SIGINT" ? 130 : signal === "SIGTERM" ? 143 : 0,
);
if (signal !== "control") expect(interruptHandled).toBe(true);
if (phase.startsWith("authentication"))
expect(project).not.toHaveBeenCalled();
if (phase === "projection") {
if (!projected?.ok) throw new Error("Expected a completed page");
expect(result.details.partial_observation).toEqual(projected.value);
}
} finally {
await pendingInterrupt;
process.exitCode = previousExitCode;
}
},
);
@@ -0,0 +1,490 @@
import { getHeapStatistics } from "node:v8";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { createServer } from "../../src/server/createServer.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { loadNativeMatchingBundleSources } from "../../src/application/native/NativeMatchingBundleSources.js";
import { NativeMatchingAdmission } from "../../src/application/native/NativeMatchingAdmission.js";
import { matchNativeFunctionsInputSchema } from "../../src/contracts/native/functionMatchingContracts.js";
import { createEvidence } from "../../src/domain/evidence.js";
import { runProviderAnalysis } from "../../src/application/DirectAnalysis.js";
import type { DirectAnalysisDependencies } from "../../src/application/DirectAnalysisDependencies.js";
import { parseConfig } from "../../src/config/parseConfig.js";
import {
isImmutableEvidence,
parseEvidence,
type Evidence,
} from "../../src/domain/evidence.js";
import { ok } from "../../src/domain/result.js";
import { observed } from "../fixtures/analysisExecution.js";
import {
createBinarySessionTargets,
createTestBinarySession,
} from "../fixtures/binarySession.js";
import { readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import {
createImmutableEvidenceBundle,
parseImmutableEvidenceBundle,
} from "../../src/domain/evidenceBundle.js";
import { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js";
import {
deliverAnalysisEvidence,
inspectAnalysisViewFromBundle,
} from "../../src/application/EvidenceBundleCommands.js";
import { readImmutableEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js";
import { requireAuthenticationHeadroom } from "../../src/application/JsonAuthenticationHeadroom.js";
import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE } from "../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
import { holdFirstFileRead } from "../support/holdFirstFileRead.js";
vi.mock("../../src/application/JsonAuthenticationHeadroom.js", { spy: true });
vi.mock("node:v8", { spy: true });
vi.mock("../../src/domain/evidenceBundle.js", { spy: true });
afterEach(() => vi.restoreAllMocks());
it.each(["compare_bundles", "match_native_functions"])(
"attributes bundle authentication refusal to the actual %s boundary",
async (operation) => {
const { path, parent } = await savedParent();
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
vi.mocked(requireAuthenticationHeadroom).mockImplementation(
(value, options) =>
actual.requireAuthenticationHeadroom(value, {
...options,
availableBytes: 0,
}),
);
const resources = new ArtifactResourceScope();
try {
if (operation === "match_native_functions") {
const source = {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
};
await expect(
loadNativeMatchingBundleSources({
input: matchNativeFunctionsInputSchema.parse({
left: source,
right: source,
}),
paths: [path],
resources,
admission: new NativeMatchingAdmission(operation),
}),
).rejects.toMatchObject({
_tag: "AnalysisResourceConstraintError",
operation,
});
} else {
const session = createTestBinarySession(() => {
throw new Error("Comparison must not open a target");
});
const server = createServer({ kind: "session", session });
const client = new Client({
name: "bundle-operation-refusal",
version: "1",
});
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const result = await client.callTool({
name: operation,
arguments: { left_bundle_path: path, right_bundle_path: path },
});
expect(result.isError).toBe(true);
expect(parseMcpToolError(result)).toMatchObject({
error: { code: "resource_constraint", details: { operation } },
});
expect(session.exportEvidenceBundle().records).toEqual([]);
} finally {
await client.close();
await server.close();
await session.close();
}
}
} finally {
vi.mocked(requireAuthenticationHeadroom).mockImplementation(
actual.requireAuthenticationHeadroom,
);
await resources.close();
}
expect(resources.hasPending).toBe(false);
},
);
const savedParent = async () => {
const root = await createTestTempDirectory("rea-managed-bundle-guard-");
const path = join(root, "parent.json");
const parent = SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application;
await writeFile(
path,
JSON.stringify(createImmutableEvidenceBundle([parent])),
);
return { path, parent };
};
it("keeps the owning operation when native matching reloads a needed earlier bundle", async () => {
const root = await createTestTempDirectory("rea-native-bundle-reload-");
const path = join(root, "parent.json"),
empty = join(root, "empty.json");
const parent = createEvidence(
undefined,
{ id: "fixture", name: "Fixture", version: "1" },
{ operation: "list_procedures", parameters: {}, result: {} },
);
await writeFile(
path,
JSON.stringify(createImmutableEvidenceBundle([parent])),
);
await writeFile(empty, JSON.stringify(createImmutableEvidenceBundle([])));
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
let reads = 0;
vi.mocked(requireAuthenticationHeadroom).mockImplementation(
(value, options) =>
actual.requireAuthenticationHeadroom(
value,
++reads === 3 ? { ...options, availableBytes: 0 } : options,
),
);
const resources = new ArtifactResourceScope();
const source = { kind: "retained-evidence", evidence_id: parent.evidence_id };
try {
await expect(
loadNativeMatchingBundleSources({
input: matchNativeFunctionsInputSchema.parse({
left: source,
right: source,
}),
paths: [path, empty],
resources,
admission: new NativeMatchingAdmission("match_native_functions"),
}),
).rejects.toMatchObject({
_tag: "AnalysisResourceConstraintError",
operation: "match_native_functions",
});
expect(reads).toBe(3);
} finally {
vi.mocked(requireAuthenticationHeadroom).mockImplementation(
actual.requireAuthenticationHeadroom,
);
await resources.close();
}
expect(resources.hasPending).toBe(false);
});
it("admits bundle authentication before cloning and hashing retained records", async () => {
const { path } = await savedParent();
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
vi.mocked(requireAuthenticationHeadroom).mockImplementationOnce(
(value, options) =>
actual.requireAuthenticationHeadroom(value, {
...options,
availableBytes: 0,
}),
);
vi.mocked(parseImmutableEvidenceBundle).mockClear();
const resources = new ArtifactResourceScope();
try {
expect(
await readImmutableEvidenceBundle(path, undefined, resources),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError", resource: "memory" },
});
expect(parseImmutableEvidenceBundle).not.toHaveBeenCalled();
} finally {
await resources.close();
}
expect(resources.hasPending).toBe(false);
});
it("preserves the view operation and caller reason when bundle I/O is cancelled", async () => {
const { path, parent } = await savedParent();
const held = await holdFirstFileRead(path);
const controller = new AbortController();
const reason = new Error("cancel managed bundle page");
const resources = new ArtifactResourceScope();
try {
const pending = inspectAnalysisViewFromBundle(
{
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "summary" },
},
path,
resources,
controller.signal,
);
await held.firstRead;
controller.abort(reason);
held.release();
const result = await pending;
expect(result).toMatchObject({
ok: false,
error: {
_tag: "AnalysisCancelledError",
operation: "inspect_analysis_view",
},
});
if (result.ok) throw new Error("Expected cancelled bundle read");
expect(result.error.cause).toBe(reason);
} finally {
held.release();
await resources.close();
}
expect(resources.hasPending).toBe(false);
});
it("retains completed parent facts when bundle publication cannot admit authentication", async () => {
const root = await createTestTempDirectory("rea-managed-delivery-guard-");
const evidence = SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application;
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
vi.mocked(requireAuthenticationHeadroom).mockImplementationOnce(
(value, options) =>
actual.requireAuthenticationHeadroom(value, {
...options,
availableBytes: 0,
}),
);
vi.mocked(createImmutableEvidenceBundle).mockClear();
const result = await deliverAnalysisEvidence(
evidence,
join(root, "parent.json"),
"summary",
);
expect(result).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError", resource: "memory" },
});
if (result.ok) throw new Error("Expected refused bundle authentication");
expect(result.error.partialObservation).toBe(evidence);
expect(createImmutableEvidenceBundle).not.toHaveBeenCalled();
expect(await readdir(root)).toEqual([]);
});
it("projects a rejected managed parent by reference without cloning under low headroom", async () => {
const root = await createTestTempDirectory(
"rea-managed-low-heap-projection-",
);
const [target] = await createBinarySessionTargets();
const source = {
rows: Array.from({ length: 512 }, (_, token) => ({
token,
name: "member",
})),
};
let closes = 0;
const session = () =>
createTestBinarySession(() => ({
execute: async () => observed(source),
close: async () => {
closes += 1;
return ok(null);
},
}));
const dependencies: DirectAnalysisDependencies = {
readConfiguration: () => parseConfig({}),
createBinarySession: session,
createNativeDispatchBinarySession: session,
createArtifactBinarySession: session,
createNativeBinarySession: session,
createManagedBinarySession: session,
};
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
let parent: Evidence | undefined;
vi.mocked(requireAuthenticationHeadroom).mockImplementationOnce(
(value, options) => {
parent = parseEvidence(value);
expect(isImmutableEvidence(parent)).toBe(true);
expect(parent).toBe(value);
return actual.requireAuthenticationHeadroom(value, {
...options,
availableBytes: 0,
});
},
);
const result = await runProviderAnalysis(
dependencies,
target,
"inspect_managed_members",
{},
undefined,
undefined,
{
detail: "summary",
evidenceBundlePath: join(root, "parent.json"),
},
);
expect(result).toMatchObject({
code: "resource_constraint",
details: { operation: "inspect_managed_members" },
});
if (typeof result !== "object" || result === null || Array.isArray(result))
throw new Error("Expected CLI error");
const details = result.details;
if (typeof details !== "object" || details === null || Array.isArray(details))
throw new Error("Expected CLI error details");
expect(details.partial_observation).toBe(parent);
expect(parent?.normalized_result).toEqual(source);
expect(Object.isFrozen(source.rows[0])).toBe(false);
expect(closes).toBe(1);
expect(await readdir(root)).toEqual([]);
});
it.each(["JSON assembly", "bundle authentication"])(
"attributes low-headroom %s to the invoking view",
async (stage) => {
const { path, parent } = await savedParent();
const actualV8 = await vi.importActual<typeof import("node:v8")>("node:v8");
if (stage === "JSON assembly") {
vi.mocked(getHeapStatistics).mockReturnValue({
...getHeapStatistics(),
total_available_size: 0,
});
} else {
const actual = await vi.importActual<
typeof import("../../src/application/JsonAuthenticationHeadroom.js")
>("../../src/application/JsonAuthenticationHeadroom.js");
vi.mocked(requireAuthenticationHeadroom).mockImplementationOnce(
(value, options) =>
actual.requireAuthenticationHeadroom(value, {
...options,
availableBytes: 0,
}),
);
}
const resources = new ArtifactResourceScope();
try {
expect(
await inspectAnalysisViewFromBundle(
{
source: {
kind: "retained-evidence",
evidence_id: parent.evidence_id,
},
view: { kind: "summary" },
},
path,
resources,
),
).toMatchObject({
ok: false,
error: {
_tag: "AnalysisResourceConstraintError",
operation: "inspect_analysis_view",
},
});
} finally {
vi.mocked(getHeapStatistics).mockImplementation(
actualV8.getHeapStatistics,
);
await resources.close();
}
expect(resources.hasPending).toBe(false);
},
);
it("authenticates escaped payloads without serializing either complete bundle", async () => {
const root = await createTestTempDirectory("rea-escaped-bundle-");
const path = join(root, "parent.json");
const escaped = '\u0000\u0001\\"😀\ud800'.repeat(20000);
const example = SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application;
const parent = createEvidence(undefined, example.provider, {
predicateType: example.predicate_type,
operation: example.operation,
parameters: example.parameters,
result: example.normalized_result,
rawResult: { escaped },
});
await writeFile(
path,
JSON.stringify(createImmutableEvidenceBundle([parent])),
);
const stringify = JSON.stringify;
const fullDocuments: unknown[] = [];
vi.spyOn(JSON, "stringify").mockImplementation((value: unknown) => {
if (
typeof value === "object" &&
value !== null &&
Object.hasOwn(value, "records")
) {
fullDocuments.push(value);
throw new Error(
"Document-sized JSON allocation forbidden during authentication",
);
}
return stringify(value);
});
const resources = new ArtifactResourceScope();
try {
const loaded = await readImmutableEvidenceBundle(
path,
undefined,
resources,
);
if (!loaded.ok) throw loaded.error;
expect(loaded.value.records[0]?.evidence_id).toBe(parent.evidence_id);
expect(loaded.value.records[0]?.raw_result).toEqual({ escaped });
expect(fullDocuments).toEqual([]);
} finally {
await resources.close();
}
expect(resources.hasPending).toBe(false);
});
it("seals a fresh object-heavy Evidence tree without retaining a set of completed rows", () => {
const provider = { id: "fixture", name: "Fixture", version: "1" };
const row = { seal_probe: true, fields: [1, 2] };
const result = { rows: Array.from({ length: 512 }, () => row) };
const observation = {
operation: "inspect_managed_members",
parameters: {},
result,
};
const complete = createEvidence(undefined, provider, observation);
const add = Set.prototype.add;
const guarded = vi.spyOn(Set.prototype, "add").mockImplementation(function (
this: Set<unknown>,
value: unknown,
) {
if (
typeof value === "object" &&
value !== null &&
Object.hasOwn(value, "seal_probe") &&
this.size > 32
)
throw new Error(
"Sealing retained completed rows beyond its ancestry budget",
);
return add.call(this, value);
});
let sealed;
try {
sealed = createEvidence(undefined, provider, observation, {
immutable: true,
});
} finally {
guarded.mockRestore();
}
expect(sealed).toEqual(complete);
expect(isImmutableEvidence(sealed)).toBe(true);
const data = sealed.normalized_result as typeof result;
expect(data.rows[0]).not.toBe(row);
expect(data.rows[0]).not.toBe(data.rows[1]);
expect(Object.isFrozen(data.rows[0]!.fields)).toBe(true);
expect(Object.isFrozen(row)).toBe(false);
expect(parseEvidence(sealed)).toBe(sealed);
});