From 57fd3ea5d8fcb0bbd5c58e127986f05183c9dc18 Mon Sep 17 00:00:00 2001 From: Jay-Alexander Elliot <10952625+jaelliot@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:43:30 -0600 Subject: [PATCH] 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. --- docs/cli.md | 17 +- docs/managed-code-analysis.md | 45 +- .../analysis-efficiency/managed-cli-costs.py | 84 ++ .../managed-cli-local-control.py | 44 + .../analysis-efficiency/managed-cli-oracle.py | 18 + .../managed-cli-runtime.mjs | 87 ++ .../managed-cli-shared-read.json | 72 + .../analysis-efficiency/managed-cli-task.mjs | 266 ++++ .../analysis-efficiency/managed-cli.json | 1332 +++++++++++++++++ .../analysis-efficiency/managed-cli.md | 219 +++ src/application/AnalysisProvider.ts | 3 + src/application/DirectAnalysis.ts | 113 +- src/application/EvidenceBundleCommands.ts | 171 ++- src/application/EvidenceBundleFiles.ts | 42 +- src/application/EvidenceInputResolver.test.ts | 4 +- src/application/EvidenceInputResolver.ts | 2 +- .../JsonAuthenticationHeadroom.test.ts | 69 + src/application/JsonAuthenticationHeadroom.ts | 78 +- src/application/JsonFiles.ts | 12 +- .../analysisView/AnalysisViewService.ts | 75 +- .../analysisView/evidenceFileSource.ts | 17 +- .../javascript/SourceToBundleReference.ts | 17 +- .../native/NativeMatchingBundleSources.ts | 2 + src/cli/analysisViewCommands.ts | 97 +- src/cli/finishAnalysisCommand.ts | 9 +- src/cli/managedCommands.ts | 33 +- src/composition/directAnalysis.ts | 7 +- src/domain/analysisErrorPresentation.ts | 4 +- src/domain/evidence.ts | 17 +- src/domain/evidenceBundle.ts | 3 +- src/domain/immutableJson.ts | 68 + src/domain/jsonSerialization.test.ts | 26 + src/domain/jsonSerialization.ts | 51 + src/dotnet/ManagedPe.fixture.ts | 7 +- src/process/queuedInterrupts.ts | 7 + src/server/registerBundleComparisonTool.ts | 2 + src/server/registerSessionRecordTools.ts | 1 + tests/boundary/cli/analysisViewCli.test.ts | 167 ++- .../cli/evidenceBundleCommands.test.ts | 34 +- tests/boundary/cli/jsonOutput.test.ts | 2 +- .../cli/managedMemberSummaryCli.test.ts | 515 +++++++ .../filesystem/evidenceBundleFiles.test.ts | 152 +- .../composition/directAnalysisCleanup.test.ts | 89 +- .../directAnalysisFactories.test.ts | 51 + .../evidenceCompletionSignalPreload.mjs | 51 + .../managedPublicationSignalPreload.mjs | 33 + .../commandFinalizationCancellation.test.ts | 162 +- .../managedBundleReadGuards.test.ts | 490 ++++++ 48 files changed, 4671 insertions(+), 196 deletions(-) create mode 100644 experiments/analysis-efficiency/managed-cli-costs.py create mode 100644 experiments/analysis-efficiency/managed-cli-local-control.py create mode 100644 experiments/analysis-efficiency/managed-cli-oracle.py create mode 100644 experiments/analysis-efficiency/managed-cli-runtime.mjs create mode 100644 experiments/analysis-efficiency/managed-cli-shared-read.json create mode 100644 experiments/analysis-efficiency/managed-cli-task.mjs create mode 100644 experiments/analysis-efficiency/managed-cli.json create mode 100644 experiments/analysis-efficiency/managed-cli.md create mode 100644 src/process/queuedInterrupts.ts create mode 100644 tests/boundary/cli/managedMemberSummaryCli.test.ts create mode 100644 tests/fixtures/evidenceCompletionSignalPreload.mjs create mode 100644 tests/fixtures/managedPublicationSignalPreload.mjs create mode 100644 tests/process-global/managedBundleReadGuards.test.ts diff --git a/docs/cli.md b/docs/cli.md index e291589bb1..f9062dc9ff 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -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 diff --git a/docs/managed-code-analysis.md b/docs/managed-code-analysis.md index 759b6f29f7..17b37d8fb4 100644 --- a/docs/managed-code-analysis.md +++ b/docs/managed-code-analysis.md @@ -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 diff --git a/experiments/analysis-efficiency/managed-cli-costs.py b/experiments/analysis-efficiency/managed-cli-costs.py new file mode 100644 index 0000000000..6527219373 --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-costs.py @@ -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)) diff --git a/experiments/analysis-efficiency/managed-cli-local-control.py b/experiments/analysis-efficiency/managed-cli-local-control.py new file mode 100644 index 0000000000..b92f5704c2 --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-local-control.py @@ -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)) diff --git a/experiments/analysis-efficiency/managed-cli-oracle.py b/experiments/analysis-efficiency/managed-cli-oracle.py new file mode 100644 index 0000000000..6220d003ba --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-oracle.py @@ -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() diff --git a/experiments/analysis-efficiency/managed-cli-runtime.mjs b/experiments/analysis-efficiency/managed-cli-runtime.mjs new file mode 100644 index 0000000000..f7655e5dd2 --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-runtime.mjs @@ -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), + ); +} diff --git a/experiments/analysis-efficiency/managed-cli-shared-read.json b/experiments/analysis-efficiency/managed-cli-shared-read.json new file mode 100644 index 0000000000..37b52edd59 --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-shared-read.json @@ -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 +} diff --git a/experiments/analysis-efficiency/managed-cli-task.mjs b/experiments/analysis-efficiency/managed-cli-task.mjs new file mode 100644 index 0000000000..1aa80cc326 --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli-task.mjs @@ -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, + })), + }), +); diff --git a/experiments/analysis-efficiency/managed-cli.json b/experiments/analysis-efficiency/managed-cli.json new file mode 100644 index 0000000000..1d98d21eae --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli.json @@ -0,0 +1,1332 @@ +{ + "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": { + "url": "https://api.nuget.org/v3-flatcontainer/newtonsoft.json/13.0.3/newtonsoft.json.13.0.3.nupkg", + "package_bytes": 2441966, + "package_sha256": "872fc189e638ab1056555b03aaa38f68bcb54286e221aa646eb1129babf63c77", + "selected_member": "lib/net6.0/Newtonsoft.Json.dll", + "assembly_bytes": 712464, + "assembly_sha256": "22c649f75fce5be7c7ccda8880473b634ef69ecf33f5d1ab8ad892caf47d5a07", + "target_execution": false + }, + "measurement_inputs": { + "baseline_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "feature_parent_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "merged_original_revision": "7b3ed4d025edd24d062cde0327a53bd311e174fc", + "feature_tracked_patch_sha256": "d950d199b3bed648d8752b67b2ff32725569528ca3d7102a65cf258722d1d98c", + "untracked_source_sha256": { + "src/process/queuedInterrupts.ts": "71ed47dc4a215a82f0f2c7b6bb3a32f22f1d5c2a8b7644cb6c9999539a16950d" + }, + "node": "v24.18.0", + "npm": "11.16.0", + "oracle_packages": { + "dnfile": "0.18.0", + "pefile": "2024.8.26" + }, + "no_overlapping_build_or_test": true, + "production_sources_sha256": { + "src/application/AnalysisProvider.ts": "97a8b6a01b1cbe50f8f0755a807f7ba93697b82705551bd613089a6f95f51f67", + "src/application/DirectAnalysis.ts": "00dd3d85729785953a81b08fc2591c905d4ea9b0a150c1b421f5e5061661d62b", + "src/application/EvidenceBundleCommands.ts": "571c69cde0b6e313a0eb3d0ed181aa07f33b0e4f943509fd8c28ca14f587f61f", + "src/application/EvidenceBundleFiles.ts": "1dfd37b3bed5306a21f25ca334caa53172c8ef2e754a4961ccdfc3bb98e1bf8c", + "src/application/EvidenceInputResolver.test.ts": "96c1bf405fd7ae492da75959bb9285ec91c2f6abc2f161316057e1fd8efee5aa", + "src/application/EvidenceInputResolver.ts": "7b676ec2f70ad55b97c9a635dce51468101eca964e7a1205f9ce9633ec946f89", + "src/application/JsonAuthenticationHeadroom.test.ts": "510e22d9fcbe976bfe6ffdfa57bd9fd5469e568519ec5474dcbb5957eb14a375", + "src/application/JsonAuthenticationHeadroom.ts": "a6f46bf1906d2b614992f97c47cc0647d827b883a09b4d7fee54ce00081e01da", + "src/application/JsonFiles.ts": "9071b2a303c76507f78f2ac902b7e5d041d74b8b73d66039cb8f7e2ec3496147", + "src/application/analysisView/AnalysisViewService.ts": "f7e4ce77705229039c5d81d2273bd6e08df11719436c24bb3c5c1d0452650ba9", + "src/application/analysisView/evidenceFileSource.ts": "241a262f793bb1101cee7f623c2b297728231a938f9163ddc71453d1d36a45f4", + "src/application/javascript/SourceToBundleReference.ts": "579917fe41ef3e447fc28915ced0c2c5809af70ec2763cc980f58930832ce4b6", + "src/application/native/NativeMatchingBundleSources.ts": "ecf3c5e2d0c6872177d55b9746b692e0d585176027b336b589501809d5819b21", + "src/cli/analysisViewCommands.ts": "0e228c83cf73d264535393cde851892ed11a302be0a77ee05a0d07e620226633", + "src/cli/finishAnalysisCommand.ts": "7eca30a3b5ea20fce9caeaedae791a0406364476aaa06e3919535bc000fda61f", + "src/cli/managedCommands.ts": "4bca27702452f426efeeb7a034fa78613c2db458eac7b3f1c0afca3b144d4700", + "src/composition/directAnalysis.ts": "d6d8002818fdb2d05d33d79563dd31dbe27e2d330a080ba9a9c64a99e9732006", + "src/domain/analysisErrorPresentation.ts": "293685c9dd89c107aabe63fb2fa7242c2fd0f42101683722badcdf8b2eb7e24c", + "src/domain/evidence.ts": "eb1895f08ef034b0b644b2f0d18aa12a4790928ccd888c2301d967521d454b11", + "src/domain/evidenceBundle.ts": "5f997e98512d4ddd11abd8d3902daed8ebd0821d04d48629863c3551b6b514d8", + "src/domain/immutableJson.ts": "c1d977408b013514c55fd309533814caa18e82b647adedb08e2f1cba2401ca82", + "src/domain/jsonSerialization.test.ts": "34f2acee9b65efb9fa59a888cbc7ea957dc915e7195e1459bc98afbfeaa10a9b", + "src/domain/jsonSerialization.ts": "f47ed8571093675a2a60686bab3acb9c0e1d44cb7bfc956db38b89a4e9633ee9", + "src/dotnet/ManagedPe.fixture.ts": "710af895b190f3b5dbdb17d922c87d3ee699b99c4c5fb46d1f3601fad97bb1a5", + "src/process/queuedInterrupts.ts": "71ed47dc4a215a82f0f2c7b6bb3a32f22f1d5c2a8b7644cb6c9999539a16950d", + "src/server/registerBundleComparisonTool.ts": "b95a0a48ef1f9ea77ce589b873d62fcae0273f869320138ab3f3fb8c43f79fc6", + "src/server/registerSessionRecordTools.ts": "d836c79459f241345d66b46c6402e5893a12c7ff5cb2ca2cc3bbe1da9976bb14" + }, + "harness_sha256": { + "managed-cli-task.mjs": "74d3b9b180a4ca73f856496c8e0c82d2f429cb946d2171b9ad4bc129cc758068", + "managed-cli-costs.py": "ff1deb2238e684fa347da9c811b552da29651b6a1254d33fc76758c7a91f6b60", + "managed-cli-local-control.py": "68f9bfd316f4183afbe6f6a8dcfb3fb0ec086cdb1f7a1731e97fe31e4e523979", + "managed-cli-oracle.py": "c917c1bc3f86f628d394581b227b1c7975ec7d7d5d9c2d806b5b908a4bbf576d", + "managed-cli-runtime.mjs": "56405411a3d30ca2aa175678b6d18d1d6c7f0e44bbf9f30fc958fee75d9edb48" + }, + "expected_runtime_bindings": { + "complete": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "selected": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + } + } + }, + "summary": { + "cold": { + "complete": { + "wall_seconds": { + "median": 9.969, + "min": 5.267, + "max": 11.408 + }, + "cpu_seconds": { + "median": 9.742865, + "min": 4.99231, + "max": 11.174552 + }, + "peak_process_family_bytes_including_page_cache": { + "median": 412385280, + "min": 410767360, + "max": 412815360 + }, + "whole_task_tokens_estimated": [451373, 451373, 451373] + }, + "selected": { + "wall_seconds": { + "median": 14.801, + "min": 12.929, + "max": 23.934 + }, + "cpu_seconds": { + "median": 14.555183, + "min": 12.66105, + "max": 23.604561 + }, + "peak_process_family_bytes_including_page_cache": { + "median": 419532800, + "min": 416526336, + "max": 422551552 + }, + "whole_task_tokens_estimated": [13078, 13078, 13078] + } + }, + "warm": { + "complete": { + "wall_seconds": { + "median": 10.339, + "min": 9.863, + "max": 11.283 + }, + "cpu_seconds": { + "median": 10.097275, + "min": 9.65094, + "max": 11.039148 + }, + "peak_process_family_bytes_including_page_cache": { + "median": 421326848, + "min": 417812480, + "max": 421904384 + }, + "whole_task_tokens_estimated": [451373, 451373, 451373] + }, + "selected": { + "wall_seconds": { + "median": 24.141, + "min": 23.425, + "max": 24.698 + }, + "cpu_seconds": { + "median": 23.84209, + "min": 23.121681, + "max": 24.436836 + }, + "peak_process_family_bytes_including_page_cache": { + "median": 426942464, + "min": 422301696, + "max": 428773376 + }, + "whole_task_tokens_estimated": [13078, 13078, 13078] + } + } + }, + "runs": [ + { + "case": "pair-cold-1-complete", + "mode": "complete", + "cache": "cold", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 92.96445600000001, + "after": 96.76712599999973 + }, + "wall_seconds": 5.267, + "cpu_seconds": 4.99231, + "peak_process_family_bytes_including_page_cache": 412815360, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 1961.4121309999998, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 2694.6139500000004, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-cold-1-selected", + "mode": "selected", + "cache": "cold", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 88.39460599999998, + "after": 126.48440400000072 + }, + "wall_seconds": 12.929, + "cpu_seconds": 12.66105, + "peak_process_family_bytes_including_page_cache": 419532800, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 1941.8404130000001, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 1953.026523, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 2909.7679009999997, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 2682.350791, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 2869.4516239999994, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-cold-2-complete", + "mode": "complete", + "cache": "cold", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 165.90159300000002, + "after": 145.30863500000123 + }, + "wall_seconds": 9.969, + "cpu_seconds": 9.742865, + "peak_process_family_bytes_including_page_cache": 410767360, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4194.595781, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 4995.024759999999, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-cold-2-selected", + "mode": "selected", + "cache": "cold", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 121.436015, + "after": 140.179814000001 + }, + "wall_seconds": 14.801, + "cpu_seconds": 14.555183, + "peak_process_family_bytes_including_page_cache": 416526336, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 2517.517109, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 1917.0473659999998, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 2768.9943000000003, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 2439.9305540000005, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 4504.241926999999, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-cold-3-complete", + "mode": "complete", + "cache": "cold", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 205.53081100000003, + "after": 201.48722199999975 + }, + "wall_seconds": 11.408, + "cpu_seconds": 11.174552, + "peak_process_family_bytes_including_page_cache": 412385280, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4881.421936, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 5597.8320969999995, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-cold-3-selected", + "mode": "selected", + "cache": "cold", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 188.422892, + "after": 141.58796399999846 + }, + "wall_seconds": 23.934, + "cpu_seconds": 23.604561, + "peak_process_family_bytes_including_page_cache": 422551552, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4050.1051940000007, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 3982.378068, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 5668.475990999999, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 4786.150098, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-cold-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 4575.609088000001, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-1-complete", + "mode": "complete", + "cache": "warm", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 175.11634400000003, + "after": 152.94952399999966 + }, + "wall_seconds": 9.863, + "cpu_seconds": 9.65094, + "peak_process_family_bytes_including_page_cache": 421326848, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 3560.906756, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 5544.523894000001, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-1-selected", + "mode": "selected", + "cache": "warm", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 139.815575, + "after": 183.66561400000137 + }, + "wall_seconds": 24.141, + "cpu_seconds": 23.84209, + "peak_process_family_bytes_including_page_cache": 428773376, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4048.32056, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 3638.761117, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 5129.003754000001, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 4614.337113000001, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-1-selected/parent.json", + "--json" + ], + "elapsed_ms": 5891.990218999999, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-2-complete", + "mode": "complete", + "cache": "warm", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 213.612103, + "after": 150.689765000001 + }, + "wall_seconds": 10.339, + "cpu_seconds": 10.097275, + "peak_process_family_bytes_including_page_cache": 417812480, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 3991.182484, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 5493.264734000001, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-2-selected", + "mode": "selected", + "cache": "warm", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 190.953993, + "after": 140.88060499999847 + }, + "wall_seconds": 24.698, + "cpu_seconds": 24.436836, + "peak_process_family_bytes_including_page_cache": 426942464, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4438.342933, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 4773.725002, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 5718.39093, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 4286.861861000001, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-2-selected/parent.json", + "--json" + ], + "elapsed_ms": 4702.040359000002, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-3-complete", + "mode": "complete", + "cache": "warm", + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 211.89498299999997, + "after": 138.75285499999882 + }, + "wall_seconds": 11.283, + "cpu_seconds": 11.039148, + "peak_process_family_bytes_including_page_cache": 421904384, + "tokens_estimated": { + "question_and_recipe": 58, + "arguments": 50, + "help_and_replies": 451265, + "whole_task": 451373 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 4497.207909000001, + "stdout_bytes": 1134, + "stdout_file": "help-members.stdout" + }, + { + "label": "complete", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--json" + ], + "elapsed_ms": 5940.504772, + "stdout_bytes": 1374919, + "stdout_file": "complete.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + }, + { + "case": "pair-warm-3-selected", + "mode": "selected", + "cache": "warm", + "runtime": "/home/yann/yann/rea-perf-managed-cli-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-perf-managed-cli-20261021", + "node": "v24.18.0", + "git_revision": "4fab2dfda16236dc407aa179920780d6a970dda3", + "source_diff_sha256": "b4199304e4163e67f08a0b77337aa45bc73a7790646ca01e6fcc56e41eaa7f2a", + "untracked_source_sha256": "55691f6b55a13e9abbe9dbe2b5c75ac27021b370ed012fceafa91d4a8b60e25f", + "compiled_cli_sha256": "2e86e22eecc4a5012963311e2b14e3101e53750459092e45d07b8030bfb687b4", + "compiled_cli_files": 144, + "compiled_cli_bytes": 10305138 + }, + "runtime_binding_audit_ms": { + "before": 177.43348500000002, + "after": 171.273545 + }, + "wall_seconds": 23.425, + "cpu_seconds": 23.121681, + "peak_process_family_bytes_including_page_cache": 422301696, + "tokens_estimated": { + "question_and_recipe": 93, + "arguments": 330, + "help_and_replies": 12655, + "whole_task": 13078 + }, + "calls": [ + { + "label": "help-members", + "arguments": ["rea", "inspect-managed-members", "--help"], + "elapsed_ms": 3439.6059069999997, + "stdout_bytes": 1487, + "stdout_file": "help-members.stdout" + }, + { + "label": "help-view", + "arguments": ["rea", "inspect-analysis-view", "--help"], + "elapsed_ms": 3851.0081439999994, + "stdout_bytes": 1219, + "stdout_file": "help-view.stdout" + }, + { + "label": "summary", + "arguments": [ + "rea", + "inspect-managed-members", + "/tmp/rea-perf-managed-cli-20261021/Newtonsoft.Json.dll", + "--type", + "Newtonsoft.Json.JsonConvert", + "--detail", + "summary", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 5618.057167999999, + "stdout_bytes": 6940, + "stdout_file": "summary.stdout" + }, + { + "label": "methods", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"methods\",\"offset\":0,\"limit\":8}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 4301.484570999999, + "stdout_bytes": 20274, + "stdout_file": "methods.stdout" + }, + { + "label": "member_refs", + "arguments": [ + "rea", + "inspect-analysis-view", + "{\"source\":{\"kind\":\"retained-evidence\",\"evidence_id\":\"ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af\"},\"view\":{\"kind\":\"managed-members\",\"collection\":\"member_refs\",\"offset\":0,\"limit\":16}}", + "--evidence-bundle", + "/tmp/rea-perf-managed-cli-20261021/pair-warm-3-selected/parent.json", + "--json" + ], + "elapsed_ms": 5387.867847999998, + "stdout_bytes": 13803, + "stdout_file": "member_refs.stdout" + } + ], + "parent_evidence_id": "ev_fd7bfb9b0a4c0b856f03511a9af2809b22cdb4be0d03acd0f1e72931199577af", + "parent_semantic_sha256": "bdb3ecd208b39fac03dd1dadf6b65ee0f35f69bc7e10f9ebca0d0cd91a024c87", + "answer_sha256": "5185257209a53a252eee3eede05abfbbec9c3f5ee1b313c8ffa0df89a43c7397", + "oracle_verified": { + "all_selected_method_metadata": 72, + "returned_methods": 8, + "returned_member_refs": 16 + }, + "cleanup_verified": true, + "resource_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + } + } + ], + "existing_native_control": { + "runtime": "/home/yann/yann/rea-5148-main-control-20261021", + "runtime_binding": { + "binding_version": 1, + "root": "/home/yann/yann/rea-5148-main-control-20261021", + "node": "v24.18.0", + "git_revision": "3dbe314ada9f6dcc994ca4655dc0faf082503847", + "source_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "untracked_source_sha256": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945", + "compiled_cli_sha256": "cd1691399852b3191acdb1a22ac937d11f9b3226d1928eff41cf36905d676008", + "compiled_cli_files": 140, + "compiled_cli_bytes": 10151181 + }, + "runtime_binding_audit_ms": { + "before": 293.54318098921794, + "after": 227.8408429992851 + }, + "repetitions": 1, + "tokens_estimated_script_plus_help_plus_answer": 10555, + "token_scope": "Projection source, original CLI help and returned answer; excludes shared question and launcher argv. No actual model usage measured.", + "wall_seconds": 11.249, + "cpu_seconds": 10.999851, + "peak_process_family_bytes_including_page_cache": 393580544, + "answer_matches": true, + "cleanup_verified": true + } +} diff --git a/experiments/analysis-efficiency/managed-cli.md b/experiments/analysis-efficiency/managed-cli.md new file mode 100644 index 0000000000..5a54fe68bc --- /dev/null +++ b/experiments/analysis-efficiency/managed-cli.md @@ -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. diff --git a/src/application/AnalysisProvider.ts b/src/application/AnalysisProvider.ts index 70d61ddd4b..0001a72d22 100644 --- a/src/application/AnalysisProvider.ts +++ b/src/application/AnalysisProvider.ts @@ -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. */ diff --git a/src/application/DirectAnalysis.ts b/src/application/DirectAnalysis.ts index d6255030ba..bc01644eda 100644 --- a/src/application/DirectAnalysis.ts +++ b/src/application/DirectAnalysis.ts @@ -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>, - logger?: Logger, - signal?: AbortSignal, - ] + ...[path, tool, arguments_, logger = silentLogger, signal, delivery]: + | readonly [ + path: string, + tool: NativeToolName | ArtifactAnalysisOperation | ManagedToolName, + arguments_: Readonly>, + logger?: Logger | undefined, + signal?: AbortSignal | undefined, + ] + | readonly [ + path: string, + tool: "inspect_managed_members", + arguments_: Readonly>, + logger: Logger | undefined, + signal: AbortSignal | undefined, + delivery: { + readonly detail?: "complete" | "summary"; + readonly evidenceBundlePath?: string; + }, + ] ): Promise => { - 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, path: string, tool: ManagedToolName, arguments_: Readonly>, signal?: AbortSignal, -): Promise => { +): Promise> => { 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 ( diff --git a/src/application/EvidenceBundleCommands.ts b/src/application/EvidenceBundleCommands.ts index 036f1e21d7..a737613bf1 100644 --- a/src/application/EvidenceBundleCommands.ts +++ b/src/application/EvidenceBundleCommands.ts @@ -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> => { - 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> => { // 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> => { + 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> => { + 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> => { 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>, + written: Result, ): Result => written.ok ? ok({ diff --git a/src/application/EvidenceBundleFiles.ts b/src/application/EvidenceBundleFiles.ts index df421ecb97..1662951765 100644 --- a/src/application/EvidenceBundleFiles.ts +++ b/src/application/EvidenceBundleFiles.ts @@ -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> => - 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> => - 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> => { 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 }, ); }; diff --git a/src/application/EvidenceInputResolver.test.ts b/src/application/EvidenceInputResolver.test.ts index 9dbb69e756..1a884775c7 100644 --- a/src/application/EvidenceInputResolver.test.ts +++ b/src/application/EvidenceInputResolver.test.ts @@ -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", ); diff --git a/src/application/EvidenceInputResolver.ts b/src/application/EvidenceInputResolver.ts index 6f382f9f99..22a2717ba9 100644 --- a/src/application/EvidenceInputResolver.ts +++ b/src/application/EvidenceInputResolver.ts @@ -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, ), ); diff --git a/src/application/JsonAuthenticationHeadroom.test.ts b/src/application/JsonAuthenticationHeadroom.test.ts index a78897061e..00d9e73822 100644 --- a/src/application/JsonAuthenticationHeadroom.test.ts +++ b/src/application/JsonAuthenticationHeadroom.test.ts @@ -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, + }, + }, + }); +}); diff --git a/src/application/JsonAuthenticationHeadroom.ts b/src/application/JsonAuthenticationHeadroom.ts index 36c161978e..0f21d47ab4 100644 --- a/src/application/JsonAuthenticationHeadroom.ts +++ b/src/application/JsonAuthenticationHeadroom.ts @@ -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[] = [[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)[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 => { +): Result => { 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, }, diff --git a/src/application/JsonFiles.ts b/src/application/JsonFiles.ts index fa3729e07a..ac3f607a23 100644 --- a/src/application/JsonFiles.ts +++ b/src/application/JsonFiles.ts @@ -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 ( > >, signal?: AbortSignal, + operation = "read_evidence_file", ): Promise< Result< Value, @@ -153,7 +157,7 @@ const readJsonFileInput = async ( 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 ( ); if (isJsonReadCancellation(cause, signal)) return err( - new AnalysisCancelledError("read_evidence_file", { + new AnalysisCancelledError(operation, { cause: signal?.reason, }), ); diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index 2e5cb772d7..659d98dc24 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -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 }, + ); }; diff --git a/src/application/analysisView/evidenceFileSource.ts b/src/application/analysisView/evidenceFileSource.ts index 303e6726cb..753a3b35ef 100644 --- a/src/application/analysisView/evidenceFileSource.ts +++ b/src/application/analysisView/evidenceFileSource.ts @@ -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, { diff --git a/src/application/javascript/SourceToBundleReference.ts b/src/application/javascript/SourceToBundleReference.ts index c6596a7837..203320e252 100644 --- a/src/application/javascript/SourceToBundleReference.ts +++ b/src/application/javascript/SourceToBundleReference.ts @@ -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( diff --git a/src/application/native/NativeMatchingBundleSources.ts b/src/application/native/NativeMatchingBundleSources.ts index 378a823cff..0ac14d9f03 100644 --- a/src/application/native/NativeMatchingBundleSources.ts +++ b/src/application/native/NativeMatchingBundleSources.ts @@ -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]) diff --git a/src/cli/analysisViewCommands.ts b/src/cli/analysisViewCommands.ts index dfaa5b9113..74aa05e892 100644 --- a/src/cli/analysisViewCommands.ts +++ b/src/cli/analysisViewCommands.ts @@ -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; + }), + ), }); }; diff --git a/src/cli/finishAnalysisCommand.ts b/src/cli/finishAnalysisCommand.ts index 7e4823dfca..2b91779de2 100644 --- a/src/cli/finishAnalysisCommand.ts +++ b/src/cli/finishAnalysisCommand.ts @@ -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, { diff --git a/src/cli/managedCommands.ts b/src/cli/managedCommands.ts index 7825497203..29519e5299 100644 --- a/src/cli/managedCommands.ts +++ b/src/cli/managedCommands.ts @@ -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 }), + }, + ), ), ), }); diff --git a/src/composition/directAnalysis.ts b/src/composition/directAnalysis.ts index 2512e2e43c..42d078ed17 100644 --- a/src/composition/directAnalysis.ts +++ b/src/composition/directAnalysis.ts @@ -7,10 +7,13 @@ type DirectAnalysisStatusRuntime = typeof import("../application/DirectAnalysisStatus.js"); type BoundOperation = 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. */ diff --git a/src/domain/analysisErrorPresentation.ts b/src/domain/analysisErrorPresentation.ts index 5db4783b91..173b7189bd 100644 --- a/src/domain/analysisErrorPresentation.ts +++ b/src/domain/analysisErrorPresentation.ts @@ -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."; diff --git a/src/domain/evidence.ts b/src/domain/evidence.ts index ad78f74f63..cfde6d7935 100644 --- a/src/domain/evidence.ts +++ b/src/domain/evidence.ts @@ -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. */ diff --git a/src/domain/evidenceBundle.ts b/src/domain/evidenceBundle.ts index 4ffd140128..1d5d01f848 100644 --- a/src/domain/evidenceBundle.ts +++ b/src/domain/evidenceBundle.ts @@ -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); diff --git a/src/domain/immutableJson.ts b/src/domain/immutableJson.ts index 9565fd075c..81f5ca1d67 100644 --- a/src/domain/immutableJson.ts +++ b/src/domain/immutableJson.ts @@ -139,6 +139,74 @@ export const freezeJsonSnapshot = (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 = ( + value: T, +): T => { + const frames: { value: object; children: Iterator }[] = []; + const ancestors = new Set(); + 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 { + 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. diff --git a/src/domain/jsonSerialization.test.ts b/src/domain/jsonSerialization.test.ts index 560b89d290..1187f51a0d 100644 --- a/src/domain/jsonSerialization.test.ts +++ b/src/domain/jsonSerialization.test.ts @@ -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), + ); + }); +}); diff --git a/src/domain/jsonSerialization.ts b/src/domain/jsonSerialization.ts index 65330950e7..a965f609e8 100644 --- a/src/domain/jsonSerialization.ts +++ b/src/domain/jsonSerialization.ts @@ -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 { yield* encodeValue(value, pretty, false); @@ -8,6 +28,37 @@ export function* canonicalJsonParts(value: unknown): Generator { 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): Generator { let buffer = ""; diff --git a/src/dotnet/ManagedPe.fixture.ts b/src/dotnet/ManagedPe.fixture.ts index 7487544015..64a212708d 100644 --- a/src/dotnet/ManagedPe.fixture.ts +++ b/src/dotnet/ManagedPe.fixture.ts @@ -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 diff --git a/src/process/queuedInterrupts.ts b/src/process/queuedInterrupts.ts new file mode 100644 index 0000000000..9c1803000e --- /dev/null +++ b/src/process/queuedInterrupts.ts @@ -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 => { + await setImmediate(); + await setImmediate(); +}; diff --git a/src/server/registerBundleComparisonTool.ts b/src/server/registerBundleComparisonTool.ts index 3dbc92298e..d74913e42b 100644 --- a/src/server/registerBundleComparisonTool.ts +++ b/src/server/registerBundleComparisonTool.ts @@ -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); diff --git a/src/server/registerSessionRecordTools.ts b/src/server/registerSessionRecordTools.ts index 68daab23bc..2b92d67e96 100644 --- a/src/server/registerSessionRecordTools.ts +++ b/src/server/registerSessionRecordTools.ts @@ -111,6 +111,7 @@ const registerImportEvidenceTool = ({ path, signal, resources, + importContract.name, ); if (!loaded.ok) return server.delivery.toCallToolResult(loaded, importContract); diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts index ca10bd9dcf..a8863754fc 100644 --- a/tests/boundary/cli/analysisViewCli.test.ts +++ b/tests/boundary/cli/analysisViewCli.test.ts @@ -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); + }, +); diff --git a/tests/boundary/cli/evidenceBundleCommands.test.ts b/tests/boundary/cli/evidenceBundleCommands.test.ts index abb56f7e80..a0e890fbfa 100644 --- a/tests/boundary/cli/evidenceBundleCommands.test.ts +++ b/tests/boundary/cli/evidenceBundleCommands.test.ts @@ -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 }) => { diff --git a/tests/boundary/cli/jsonOutput.test.ts b/tests/boundary/cli/jsonOutput.test.ts index 53ae17fc24..b0525f16a4 100644 --- a/tests/boundary/cli/jsonOutput.test.ts +++ b/tests/boundary/cli/jsonOutput.test.ts @@ -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; diff --git a/tests/boundary/cli/managedMemberSummaryCli.test.ts b/tests/boundary/cli/managedMemberSummaryCli.test.ts new file mode 100644 index 0000000000..dcacd6f4a4 --- /dev/null +++ b/tests/boundary/cli/managedMemberSummaryCli.test.ts @@ -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", + ]); + }, +); diff --git a/tests/boundary/filesystem/evidenceBundleFiles.test.ts b/tests/boundary/filesystem/evidenceBundleFiles.test.ts index d94b949c86..9dcee56214 100644 --- a/tests/boundary/filesystem/evidenceBundleFiles.test.ts +++ b/tests/boundary/filesystem/evidenceBundleFiles.test.ts @@ -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, 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>; + 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) => { diff --git a/tests/composition/directAnalysisCleanup.test.ts b/tests/composition/directAnalysisCleanup.test.ts index 54a917b9f4..36f1bf1104 100644 --- a/tests/composition/directAnalysisCleanup.test.ts +++ b/tests/composition/directAnalysisCleanup.test.ts @@ -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(); diff --git a/tests/composition/directAnalysisFactories.test.ts b/tests/composition/directAnalysisFactories.test.ts index 40aa995b75..4dbba76519 100644 --- a/tests/composition/directAnalysisFactories.test.ts +++ b/tests/composition/directAnalysisFactories.test.ts @@ -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(); diff --git a/tests/fixtures/evidenceCompletionSignalPreload.mjs b/tests/fixtures/evidenceCompletionSignalPreload.mjs new file mode 100644 index 0000000000..ebca72e95b --- /dev/null +++ b/tests/fixtures/evidenceCompletionSignalPreload.mjs @@ -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; +}; diff --git a/tests/fixtures/managedPublicationSignalPreload.mjs b/tests/fixtures/managedPublicationSignalPreload.mjs new file mode 100644 index 0000000000..3f9314e7e1 --- /dev/null +++ b/tests/fixtures/managedPublicationSignalPreload.mjs @@ -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(); diff --git a/tests/process-global/commandFinalizationCancellation.test.ts b/tests/process-global/commandFinalizationCancellation.test.ts index 3839c57334..f1587ee6d7 100644 --- a/tests/process-global/commandFinalizationCancellation.test.ts +++ b/tests/process-global/commandFinalizationCancellation.test.ts @@ -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 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 | 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 | 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; } }, ); diff --git a/tests/process-global/managedBundleReadGuards.test.ts b/tests/process-global/managedBundleReadGuards.test.ts new file mode 100644 index 0000000000..0f14247f4d --- /dev/null +++ b/tests/process-global/managedBundleReadGuards.test.ts @@ -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("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, + 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); +});