diff --git a/.agents/skills/reverse-engineer-anything/SKILL.md b/.agents/skills/reverse-engineer-anything/SKILL.md index 4877b2a7ba..19c280eb6a 100644 --- a/.agents/skills/reverse-engineer-anything/SKILL.md +++ b/.agents/skills/reverse-engineer-anything/SKILL.md @@ -2,7 +2,7 @@ name: reverse-engineer-anything description: Reverse engineer native, managed, Electron/JavaScript, packaged, firmware, and browser targets with REA, and build agent environments (AgentEnv bundles) of web applications from a live instance or a capture. Use shipped-artifact or requested runtime evidence to explain features, compare versions, decompile code, or guide a reconstruction. Skip REA for ordinary source-repository architecture analysis. metadata: - version: "58" + version: "59" --- # REA @@ -401,9 +401,18 @@ call `analyze_javascript_application` or `inspect_binary_layout` with a flag: its default reply is the summary, and the complete dossier stays retained under `parent_evidence_id` for facet paging. Pass that ID as a `retained-evidence` reference to `compare_functions`, or inside -`match_native_functions` `left_observations`/`right_observations`; request -`"detail": "complete"` only when the dossier must travel inline, as -`build_call_path` requires. +`match_native_functions` `left_observations`/`right_observations` or +`build_call_path.functions`; request `"detail": "complete"` only when the +dossier must travel inline. +Use the same exact-reference form in other advertised logical Evidence inputs, +including optional sources, arrays and nested sources across domains. Each +source can independently be inline or retained; preserve its source identity, +coverage, partial facts and unknowns. A summary or inspection page has its own +Evidence ID: use `normalized_result.parent_evidence_id` when the consumer needs +the complete source. For portable CLI follow-ups, commands advertising +`--evidence-bundle` resolve references from that selected exported bundle. +See [Evidence composition](references/evidence-workflows.md) for scope and +recovery; file selectors are supported only where explicitly advertised. Page module identities and select the returned `node_id` before requesting an item; a single file path can represent several graph roles. Do not repeat an identical analysis or rejected call without a changed input or resolved blocker. diff --git a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md index fcdff795ce..32a5962d40 100644 --- a/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md +++ b/.agents/skills/reverse-engineer-anything/references/evidence-workflows.md @@ -11,6 +11,44 @@ returned inline. When complete `inspect_binary_layout` or `inspect_analysis_view` for a summary, one section/symbol/module, or a stable page instead of repeating the producer. +For any logical Evidence source that advertises retained references, pass +`{kind: "retained-evidence", evidence_id}` instead of copying the complete +record back into a call. The same contract covers individual, optional, array +and nested sources across native, process, JavaScript, managed, mobile and WASM +workflows. Sources can independently mix inline records and exact references. +Resolution preserves the source's identity, provenance, coverage, partial facts +and unknowns; it neither reruns analysis nor makes incomplete facts complete. +Keep domain role and identity requirements: a native dossier cannot substitute +for a managed member observation, for example. + +When starting from a summary or inspection page, use its +`normalized_result.parent_evidence_id` to reference the complete source. +The view's own `evidence_id` names +only that view. `compare_artifacts` explicitly supports artifact summary inputs +and resolves their parents; do not assume every consumer does. When a workflow +follows source links, keep those linked records available too. + +MCP references belong to the current connection's ledger. If the ID is known, +pass it directly; no discovery call or provider setup is needed. If it is missing, +import the exported bundle or supply complete inline Evidence. Do not retry an +unchanged reference or rerun a producer merely to move an existing record. +Export before closing the session when the source must survive it. For CLI JSON +workflows that advertise `--evidence-bundle`, the same reference JSON resolves +against one explicitly selected authenticated bundle: + +```bash +rea trace-application-feature request.json --evidence-bundle /absolute/path/evidence.json --json +``` + +This includes JavaScript traces/comparisons and runtime reconciliation, managed +reconstruction/native verification/graph projection, Android/Apple/Harmony graph +projection, WASM interface comparison and retained analysis views. The managed +member comparison CLI still takes two assembly paths; process comparison takes +two capture Evidence JSON paths. Check command help for the installed version. +An input bundle grants no implicit file lookup: `evidence-file` and historical +graph file selectors are accepted only by contracts advertising them. See the +[CLI reuse guide](https://github.com/morluto/rea/blob/main/docs/cli.md#import-export-and-compare-evidence). + Completed `inspect_artifact` inventory also supports a summary and `view: {kind: "inventory", collection: "occurrences", logical_paths: ["package.json", "main.js"]}`. Omit `logical_paths` to page all available occurrences; `[]` selects none. @@ -56,11 +94,12 @@ or absence: the complete dossier. - `compare_bundles` compares canonical bundle membership and unknown history. - `find_changed_behavior` combines existing comparisons; static differences - remain candidates, not causal proof. + remain candidates, not causal proof. Its `comparisons` accept exact references. - `build_call_path` needs explicit function dossiers from one artifact/provider; - an incomplete frontier makes absence unknown. + pass complete parent IDs in `functions`. An incomplete frontier makes absence + unknown. - `correlate_static_and_runtime` uses explicit mappings; matching patterns are - hypotheses, not causality. + hypotheses, not causality. Both comparison arrays accept exact references. - `verify_reconstruction` evaluates a finite typed specification. A pass covers only declared comparable claims, not global equivalence. diff --git a/.agents/skills/reverse-engineer-anything/references/javascript-applications.md b/.agents/skills/reverse-engineer-anything/references/javascript-applications.md index 191060b90c..fa47760ff4 100644 --- a/.agents/skills/reverse-engineer-anything/references/javascript-applications.md +++ b/.agents/skills/reverse-engineer-anything/references/javascript-applications.md @@ -236,8 +236,13 @@ This notation is a template: replace it with the actual returned ID. Versions before 4.1.0 accept only full inline Evidence. Use the exact ID returned by the producer on the same MCP connection. Resolution does not run -analysis or select a provider; findings remain inline. Native observation -arrays still take full Evidence. `close_binary` clears retained references; +analysis or select a provider; findings remain inline. For summary or inspection +replies, select the complete `normalized_result.parent_evidence_id`. Native +observation arrays and runtime reconciliation's `static_layers[].analysis` and +`runtime_observations[]` also accept exact references where advertised. Each +source can independently be inline or retained, preserving identity, coverage +and unknowns. CLI workflows advertising `--evidence-bundle` resolve these inputs +from the explicitly selected bundle. `close_binary` clears retained references; export a bundle before closing, or supply portable inline Evidence in another connection. A missing reference includes its ID and recovery guidance. diff --git a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md index a1bba7b213..aaf3158b3b 100644 --- a/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md +++ b/.agents/skills/reverse-engineer-anything/references/native-and-artifacts.md @@ -671,7 +671,9 @@ For exact core-module import/export differences, use `compare_wasm_interfaces` with two complete inspection Evidence records (or current-session retained references) and explicit scope. Preserve unknown types/duplicate names; static interface equality is not runtime compatibility. CLI accepts complete inline -Evidence in `rea compare-wasm-interfaces input.json --json`. +Evidence in `rea compare-wasm-interfaces input.json --json`, or exact references +with `--evidence-bundle /absolute/path/evidence.json`. Use the complete parent +ID when the inspection returned a summary. ## Godot PCK diff --git a/docs/cli.md b/docs/cli.md index 251b7cd102..2bf994224c 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -673,12 +673,45 @@ with `details.reason: path-limit` and `boundary: filesystem-path`. 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 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. +belong to one connection. Separate CLI calls can consume full saved records or +resolve exact retained IDs from an explicitly selected local bundle: + +```bash +rea trace-application-feature request.json --evidence-bundle /absolute/path/evidence.json --json +``` + +Here `request.json` uses the same logical Evidence input as MCP, for example +`"application": {"kind":"retained-evidence","evidence_id":"ev_<64 lowercase hex characters>"}`. +Inline records and references may be mixed in the request's declared source +fields, including arrays and nested inputs. The bundle is read and authenticated +once for that command; no target is reopened, no producer is rerun, and no +record is selected by file order. Source identity, provenance, coverage and +partial or unknown facts agree with the complete inline form. When starting +from summary or inspection Evidence, select its +`normalized_result.parent_evidence_id` for a workflow requiring complete source +facts. Omit `--evidence-bundle` when all sources are inline. + +The JSON workflow commands supporting this option are: + +- `trace-application-feature`, `trace-javascript-semantics`, + `compare-application-versions`, `compare-javascript-export-shapes`, + `compare-source-to-bundle`, and `reconcile-javascript-runtime`. +- `import-managed-reconstruction`, `verify-managed-native-boundaries`, and + `project-managed-application-graph`. +- `project-android-application-graph`, `project-apple-application-graph`, and + `project-harmony-application-graph`. +- `compare-wasm-interfaces` and `inspect-analysis-view`. + +For `inspect-analysis-view`, the request's source reference selects a parent +from the bundle for every supported view kind. Bundles exported from MCP work +for these commands. Its inline source form cannot also select a bundle. +A missing ID is refused with `details.reason: "missing"`; +supplying a bundle does not make incompatible Evidence valid for a workflow. +`compare-managed-members` keeps its two assembly paths, and +`compare-process-captures` keeps its two capture Evidence JSON paths. The bundle +option above selects an input; producer options that write a new bundle, such +as `inspect-managed-members --evidence-bundle`, retain their existing meaning. +File selectors remain available only on contracts that explicitly support them. Compare two artifact inventories with `compare-artifacts`. It takes the same `left` and `right` input as MCP `compare_artifacts`: complete Evidence, or an @@ -691,7 +724,7 @@ rea inspect-artifact new.zip --json > new.json rea compare-artifacts '{"left":{"kind":"evidence-file","path":"old.json"},"right":{"kind":"evidence-file","path":"new.json"}}' --json ``` -A one-shot command keeps no session Evidence, so it refuses a +A one-shot `compare-artifacts` command keeps no session Evidence, so it refuses a `retained-evidence` source with `details.reason: "missing"`. Select the record from a saved bundle instead. For an artifact summary selected from a bundle, include its complete parent diff --git a/docs/javascript-application-workflows.md b/docs/javascript-application-workflows.md index 2f89295c45..c3a975f842 100644 --- a/docs/javascript-application-workflows.md +++ b/docs/javascript-application-workflows.md @@ -622,8 +622,10 @@ or process workflows. ## CLI and verification All six CLI commands accept inline JSON or a path to a JSON file. The CLI -returns an Evidence record directly. Put the full records in a later CLI input; -a separate CLI process has no retained MCP connection state. +returns an Evidence record directly. Later workflow inputs can contain full +records, or exact retained references when `--evidence-bundle PATH` selects an +authenticated exported bundle. A separate CLI process has no retained MCP +connection state. See [CLI Evidence reuse](cli.md#import-export-and-compare-evidence). File inputs must be regular files; symlinks to regular files are accepted. Directories, named pipes and device files produce an input error before JSON @@ -646,8 +648,12 @@ rea trace-application-feature ./trace-input.json --json MCP analysis returns the complete Evidence record, with the operation result in `normalized_result` and its identity in `evidence_id`. If the connected server advertises retained references, reuse its exact returned ID as `{"kind":"retained-evidence","evidence_id":"RETURNED_ID"}` -in `application`, or `left`/`right` for comparisons. `RETURNED_ID` is a template, -not a literal valid ID. Native Evidence arrays still use complete records. +in `application`, or `left`/`right` for comparisons. For summary or inspection +replies, use `normalized_result.parent_evidence_id` to select the complete source. +`RETURNED_ID` is a template, not a literal valid ID. Native-observation arrays +and `reconcile_javascript_runtime`'s nested static layers and runtime-observation +array also accept references where advertised, independently mixed with inline +records. Exact identity, coverage and partial or unknown facts are preserved. References are scoped to one connection; `close_binary` clears them. Export a bundle before closing and import it on another connection, or supply the full inline Evidence there. Versions before 4.1.0 accept full inline Evidence only; installing diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 4dbc5735e9..f030bd13f9 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -53,8 +53,9 @@ reject the summary record. Give `compare_functions` the retained dossier as `{"kind": "retained-evidence", "evidence_id": ""}`; give `match_native_functions` the same reference inside `left_observations` or `right_observations`, since its `left` and `right` take `list_procedures` -Evidence. Request `detail: "complete"` for `build_call_path`, which accepts -only inline complete dossiers. +Evidence. `build_call_path.functions` also accepts retained references to the +complete dossiers. Request `detail: "complete"` only when the record must travel +inline. Saved records from the pre-6.6 identity projection that omitted `relationship_coverage: null` from its digest remain incompatible, even when @@ -623,10 +624,24 @@ counts, and whether the decoded graph is incomplete. Each page repeats budget (XLD.app: 6.7 MB), so select `detail: "summary"` on the first call. Delivery detail never reaches the decoder or changes the source Evidence ID. -`trace_application_feature`, `trace_javascript_semantics`, -`compare_application_versions`, `compare_source_to_bundle`, and -`compare_javascript_export_shapes` accept complete inline application Evidence -or an exact reference to a record already retained by the current connection: +Logical Evidence source fields share one input contract across domains: +complete inline Evidence or an exact reference to a record already retained by +the current connection. This applies to individual sources, optional sources, +arrays and nested sources declared by the tool's input schema: + +| Workflows | Retained-reference inputs | +| ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | +| `compare_functions`, `compare_process_captures`, `compare_wasm_interfaces` | `left`, `right` | +| `match_native_functions` | `left`, `right`, `left_observations[]`, `right_observations[]` | +| `trace_application_feature`, `trace_javascript_semantics`, `compare_source_to_bundle` | `application`; feature tracing also accepts `native_observations[]` | +| `compare_application_versions`, `compare_javascript_export_shapes` | `left`, `right`; version comparison also accepts both native-observation arrays | +| `reconcile_javascript_runtime` | `static_layers[].analysis`, `runtime_observations[]` | +| `compare_managed_members`, `verify_managed_native_boundaries`, `import_managed_reconstruction` | Comparison `left`/`right`; verification `managed_boundaries`/`native_observations[]`; reconstruction `static_members` | +| `project_managed_application_graph` | `managed_artifact`, `managed_members`, `managed_native_boundaries` | +| `project_android_application_graph`, `project_apple_application_graph`, `project_harmony_application_graph` | `inventory_evidence[]` | +| `build_call_path`, `find_changed_behavior`, `correlate_static_and_runtime` | `functions[]`, `comparisons[]`, or `static_comparisons[]`/`runtime_comparisons[]`, respectively | + +For example: ```json { @@ -642,11 +657,16 @@ or an exact reference to a record already retained by the current connection: ``` Use the `evidence_id` returned by `analyze_javascript_application` (or another -compatible application-graph producer). Comparisons accept this form in `left` -and `right`; each side can independently be inline or retained. Native -observation arrays continue to take complete inline Evidence. Results and their -Evidence remain complete inline, and both input forms pass the same semantic, -identity, authority, and provenance checks without running the producer again. +compatible application-graph producer). When the producer returned a summary +or inspection page, use its `normalized_result.parent_evidence_id` to select +the complete source, rather than the view's own `evidence_id`. Each source can +independently be inline or retained, including within one array. Both forms use +the same application workflow and owner validation, preserve exact source +identity, provenance, authority, coverage and partial or unknown facts, and do +not run the producer again. A reference cannot supply facts absent from that +record. Workflows that follow source links also require those linked records +in the ledger. File references are accepted only where the tool explicitly +advertises a file selector; a retained ID does not authorize a file read. References belong to the current connection's Evidence ledger. Opening another target or restarting the current target preserves retained records, including @@ -659,10 +679,13 @@ Supply complete inline Evidence, repeat its producer, or import an exported Evidence bundle before referencing that imported record. Export a bundle before closing if the investigation needs it later. -CLI application workflows continue to read portable inline Evidence from files -and run the same analysis workflows; a standalone CLI invocation cannot resolve -another MCP connection's retained records. No additional lookup call, provider -selection, or approval step is required for a same-session follow-up. +CLI workflows that advertise `--evidence-bundle` can resolve the same reference +JSON from one explicitly selected local bundle, including an MCP export. They +also accept portable inline Evidence and use the same application workflows. +A standalone CLI invocation cannot resolve another MCP connection's ledger +without that exported bundle. See [CLI Evidence reuse](cli.md#import-export-and-compare-evidence) +for the supported commands. No additional lookup call, provider selection, or +approval step is required for a same-session follow-up. CLI `inspect-artifact` continues to return complete portable Evidence. Save that JSON to a file and use `inspect-analysis-view` with inline source Evidence for later inventory summaries or pages; a one-shot CLI session cannot retain a diff --git a/docs/tool-design.md b/docs/tool-design.md index 660544827b..2523c9156a 100644 --- a/docs/tool-design.md +++ b/docs/tool-design.md @@ -134,6 +134,51 @@ before proposing another issue, and avoid treating lack of API parity as a bug. ceiling and its recovery as described in [admission limits and recovery](design/limits-and-recovery.md). +### Compose declared Evidence inputs + +Declare each logical Evidence source with the shared `evidenceInputSchema`: +complete inline Evidence or an exact +`{kind: "retained-evidence", evidence_id}` reference. Use that declaration in +single, optional, array and nested source fields across domains. Keep complete +record schemas for stored Evidence, bundle records and pure semantic inputs; +request alternatives do not change those representations. Ordinary IDs in +parameters or result data are not implicitly source selectors. + +Derive borrowing request validation and source resolution from the canonical +request schema with `getEvidenceInputContract` and `resolveEvidenceRequest`. +The shared application entrypoint selects references for CLI and MCP before the +format owner admits, authenticates and interprets their records. Preserve +admission before copying large source payloads. Do not maintain a second list +of source paths or resolve references only in an adapter. A resolved reference +has the same identity, caller-selected observations, provenance, coverage and +partial or unknown facts as its complete inline record; it never reruns a +producer or upgrades incomplete evidence. +Keep Evidence-bearing request unions outside skipping `default` wrappers; +use `prefault` when a default must pass through branch selection. Catalog +conformance rejects unsupported source containers rather than silently losing +their references. + +Return authenticated source records alongside derived workflow Evidence when +the adapter must retain them. Keep links to those exact sources and require the +source closure when the workflow follows their links. Missing references and +incompatible workflow roles are explicit failures; do not infer a substitute +from ledger order, display names or the active target. A summary or inspection +page has its own identity: consumers needing complete source facts use its +`normalized_result.parent_evidence_id`, unless their contract explicitly accepts +that view. + +New provider-owned source observations require a retention owner. Existing +caller-owned inputs can also compose on a fixed port without a recorder; +when a recorder is available, preserve its acceptance or rejection and retain +completed sources through cancellation. + +MCP supplies the current ledger lookup. CLI commands that advertise +`--evidence-bundle` use one explicitly selected, authenticated bundle for the +command's lifetime. Keep cleanup with that owner. A retained reference does not +grant filesystem access: file selectors remain separate, explicit contracts +where supported. See [retained Evidence inputs](mcp-contracts.md#retained-application-evidence-inputs) +and [CLI reuse](cli.md#import-export-and-compare-evidence). + ### Interpret boundary facts once The format or protocol reader owns interpretation. Pass its parsed facts and diff --git a/docs/verification/evidence-input-composition-2026-10-31.json b/docs/verification/evidence-input-composition-2026-10-31.json new file mode 100644 index 0000000000..debe9aa135 --- /dev/null +++ b/docs/verification/evidence-input-composition-2026-10-31.json @@ -0,0 +1,3592 @@ +{ + "schema_version": 1, + "record_date": "2026-10-31", + "scope": "Factual record of 14 actual local Codex CLI attempts: six initial primary tasks, one interrupted supplemental task, and seven final-runtime tasks. Original source records remain unchanged. This receipt excludes raw transcripts, authentication, account configuration, ambient environment and private workspace paths. Additional verification records 13 scripted public-transport attempts across nine real-artifact lanes and focused source/CLI/MCP boundary checks; these have no model-token measurement.", + "model_identity": { + "requested_alias": "gpt-6.1-terra", + "reasoning": "high", + "client": "codex-cli 0.165.0", + "resolved_server_model": "not inferred or claimed" + }, + "runtime": { + "source_base_commit": "23de10bb2849cdce8ddbe70504aa7cc57de75a98", + "initial-dirty-runtime": { + "runtime_files_sha256": "b25a88f176cb8e874f5b9c5a4f89d2e9f3357c51cc6ee0c4db282a75ed81f378", + "working_diff_sha256": "99f9331581c177e399bb2d9caae66eeb656375d58475d24757cd68558a126097", + "source_state": "base commit plus uncommitted working tree" + }, + "final-dirty-runtime": { + "runtime_files_sha256": "26caffaa63a30858d6f8c22fb299b1cf2a310292e785851bc0847ba75c5b2c0a", + "working_diff_sha256": "d1a6404523285eb123e673e76cddcd3d6b38063210862503a500435e30fe6611", + "source_state": "same base commit plus later uncommitted working tree" + } + }, + "resource_policy": { + "logical_CPU_quota_percent": 100, + "hard_memory_bytes": 4294967296, + "early_stop_bytes": 3221225472, + "swap_bytes": 0, + "tasks_max": 192, + "model_task_timeout_seconds": 850 + }, + "measurement_limits": [ + "Tasks use fresh isolated REA/Codex processes/profiles; provider toolchains were preinstalled, runtime built, OS page cache uncontrolled. No controlled cold/warm or before/after benchmark.", + "Actual token totals include schemas, accumulated context, extra calls and retries. Cached input is a subset, not additional input. Byte sizes do not establish token savings.", + "Scoped diagnostic/catalog-selection trials change task context/tool exposure; comparisons to full catalog are observational.", + "Original Obsidian partial and failed supplemental are retained; final selected smallerJS case does not fix or replace Obsidian limitations.", + "Local CPU/process-family memory includes client,MCP and child providers; remote model compute and monitoring outside payload scope are not measured.", + "Selected independent managed metadata reader emitted six warnings; no wholeCLI validity or provider-complete correctness claim.", + "Signed archive naming/path hints do not establish cryptographic authenticity. Static model trials did not execute targets; the controlled jq process captures were executed separately, as recorded in their provenance.", + "Kernel local resource counters are observed at payload exit with observer alive, not final whole-scope counters. All per-run requested model/reasoning/client settings equal model_identity; cached_input_tokens is a subset of input_tokens." + ], + "artifacts": { + "jq": { + "file_name": "jq", + "bytes": 30800, + "sha256": "90b1eef4498f31836342898fc6e97cfeb8a197c4366773a0fb120ae02a45ea52", + "provenance": { + "kind": "existing-system-installed-file", + "observed_path": "/usr/bin/jq", + "publisher_authenticity": "not established by file identity" + }, + "independent_facts": { + "format": "ELF64", + "machine": 62, + "entry": "0x3e40" + }, + "oracles": ["base-artifacts", "jq-disassembly"] + }, + "obsidian-asar": { + "file_name": "obsidian-1.14.4.asar", + "bytes": 27477966, + "sha256": "146ef8470d8cbb4a6db209bdc19bf2bad13350444186c0fd93c31087b142b92f", + "provenance": { + "kind": "official-public-release-gzip-streamed-to-ASAR", + "url": "https://github.com/obsidianmd/obsidian-releases/releases/download/v1.14.4/obsidian-1.14.4.asar.gz", + "transport_bytes": 9200670, + "transport_sha256": "d1ed428c363968774f0f3906e67d8a53a058ece0b6cdd924b07865977b2ada21", + "version": "1.14.4" + }, + "independent_facts": { + "asar_entries": 396, + "package_name": "obsidian-dev", + "package_version": "1.14.4" + }, + "oracles": ["base-artifacts", "obsidian-literals"] + }, + "nuget-cli": { + "file_name": "NuGet.CommandLine.XPlat.dll", + "bytes": 756736, + "sha256": "ddea7807716a77a166cc321ddfad566e80569d9919dd8ecdf0ce50e4e9059642", + "provenance": { + "kind": "existing-local-dotnet-SDK-copy", + "SDK_version": "8.0.427", + "assembly_version": "6.11.2.1", + "publisher_authenticity": "not independently established" + }, + "independent_facts": { + "mvid": "30253a31-d9fc-46b8-89c2-c793e1d5689e", + "type_defs": 295, + "method_defs": 1507 + }, + "oracles": ["base-artifacts", "managed-getter"] + }, + "css-wasm": { + "file_name": "tree-sitter-css.wasm", + "bytes": 128668, + "sha256": "8a23977fe271357cce6f254ef88c9bebf3854602d8046605aef6a45c02135c59", + "provenance": { + "kind": "existing-installed-copy-matching-pinned-repository-corpus-member", + "project": "VS Code Tree-sitter published WASM package", + "version": "0.3.1", + "upstream_url": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", + "transport_sha256": "985684b2cfbe5e8f90499a9f36e37519887c7d4e2697d0457d0f1e923ce5184c", + "transport_scope": "Published corpus provenance; transport archive was not reacquired for this validation." + }, + "independent_facts": { + "imports": 7, + "exports": 3, + "tree_sitter_css": { + "function_index": 9, + "parameters": [], + "results": ["i32"] + } + }, + "oracles": ["base-artifacts", "wasm-interface"] + }, + "rectangle-zip": { + "file_name": "Rectangle2.0.4.zip", + "bytes": 5080855, + "sha256": "0b9b8b11edeb3237e3224c619a6f6bfb5e5996a477048be6ff4b98b0346aedc9", + "provenance": { + "kind": "official-public-release", + "url": "https://github.com/rxhanson/Rectangle/releases/download/v2.0.4/Rectangle2.0.4.zip", + "version": "2.0.4" + }, + "independent_facts": { + "zip_entries": 274, + "symlinks": 6, + "app_root": "Rectangle.app" + }, + "oracles": ["base-artifacts", "rectangle-members"] + }, + "agent-browser-entry": { + "file_name": "bin/agent-browser.js", + "bytes": 3184, + "sha256": "8e382f4a5ba22f45e1e0339abfe5a55ed95a19540b16a69ee3faf31c8dc8216a", + "provenance": { + "kind": "existing-installed-npm-package-entrypoint", + "package": "agent-browser", + "version": "0.27.0", + "repository_from_package_metadata": "https://github.com/vercel-labs/agent-browser", + "publisher_authenticity": "not independently established" + }, + "independent_facts": { + "import_sources": ["child_process", "fs", "path", "url", "os"], + "import_lines": [11, 12, 13, 14, 15], + "source_functions": ["isMusl", "getBinaryName", "main"] + }, + "identity_scope": "Harness pins individual entrypoint bytes; REA application subject identifies the containing directory, separately.", + "oracles": ["agent-browser-entry", "agent-browser-locations"] + }, + "newpipe-apk": { + "file_name": "NewPipe_v0.29.1.apk", + "bytes": 11507122, + "sha256": "18447bfb1e06d113edc88df93f471827280de06f6f3d4dc42f56f28b9c1bab79", + "provenance": { + "kind": "official-public-release-matching-repository-corpus-pin", + "url": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.29.1/NewPipe_v0.29.1.apk", + "version": "0.29.1" + }, + "independent_facts": { + "zip_entries": 1375, + "dex_members": 3, + "native_ABI_members": 4 + }, + "oracles": ["mobile-bytes"] + }, + "shark-zip": { + "file_name": "Shark-default-v1.0.8-signed.zip", + "bytes": 1854336, + "sha256": "086861edbee759f9f2b48ee4e0bdce02a61cda6df39b465564df39973da4a18b", + "provenance": { + "kind": "official-public-release-asset-digest", + "url": "https://github.com/xiaobingtech/Shark/releases/download/v1.0.8/Shark-default-v1.0.8-signed.zip", + "version": "v1.0.8", + "release_api": "https://api.github.com/repos/xiaobingtech/Shark/releases/tags/v1.0.8" + }, + "oracles": ["shark-app-member"] + }, + "shark-app": { + "file_name": "Shark-default-v1.0.8-signed.app", + "bytes": 1867667, + "sha256": "b2344fb11bb31477c5c44734c2d26fd29adcab40719712664e48f973ae0909d3", + "provenance": { + "kind": "exact-regular-ZIP-member", + "parent_artifact_id": "shark-zip", + "member_path": "Shark-default-v1.0.8-signed.app" + }, + "independent_facts": { + "members": ["pac.json", "pack.info", "entry-default.hap"] + }, + "oracles": ["shark-app-member", "mobile-bytes"] + }, + "shark-hap": { + "file_name": "Shark-v1.0.8-entry-default.hap", + "bytes": 3061314, + "sha256": "177659902ed83329a5fbda867c7985660aa2d33f71cbc8ac8baf53b7da4caac8", + "provenance": { + "kind": "exact-regular-APP-member", + "parent_artifact_id": "shark-app", + "member_path": "entry-default.hap" + }, + "independent_facts": { + "zip_entries": 55, + "selected_members": { + "module.json": 1979, + "resources.index": 3827, + "ets/modules.abc": 775040 + } + }, + "oracles": ["shark-app-member", "mobile-bytes"] + }, + "process-capture-bundle": { + "file_name": "captures.bundle.json", + "bytes": 15199, + "sha256": "55da0f130dc2265aae54261d49949471e5c929163c2b7314b42e95df3325b739", + "provenance": { + "kind": "two-authentic-controlled-local-jq-captures", + "executable_artifact_id": "jq", + "scenario": "jq -cn with explicit before/after revision filters and equal comparison contracts; capture producers ran separately from static model trial." + }, + "capture_evidence_ids": [ + "ev_892da4f0557c355c1127634b9c69780d2e9f7dc64ddc75e39093dba382e4b525", + "ev_33bb6e0546183b842d986bbf422e3d1f7281030aee543689d25e2efe2baebd28" + ], + "independent_facts": { + "before": { + "domain": "rea-process", + "revision": "before" + }, + "after": { + "domain": "rea-process", + "revision": "after" + }, + "root_exit_codes": [0, 0], + "truncated": [false, false], + "settlement_states": ["unverifiable", "quiesced"] + }, + "oracles": ["process-capture", "process-output", "process-inline"] + } + }, + "oracles": { + "base-artifacts": { + "receipt_file": "independent-oracles.json", + "sha256": "9a073a390f6b34efff311d9485669394f867eebbe69d446c135c1ed0935aaf8c", + "scope": "Independent Python ELF/ASAR/ZIP/WASM bytes and dnfile selected managed metadata." + }, + "jq-disassembly": { + "receipt_file": "jq-objdump.txt", + "sha256": "97abd64e549b9145bed646da9527961ad49f52706d04cac08f80f095a0eeb42c", + "scope": "Independent objdump static call instructions; provider addresses use image-base shift0x100000." + }, + "managed-getter": { + "receipt_file": "managed-selected-member-oracle.json", + "sha256": "489bb5734e15f3fb5918156c18ca00f5704a67b13d00fbaeb0d3639941f16d81", + "scope": "Independent owner/token/signature and raw selected getter CIL bytes; generic MemberRef remains unresolved." + }, + "wasm-interface": { + "receipt_file": "wasm-signature-oracle.json", + "sha256": "ebfb954f1f606173fe0539f8781b3d8ad8724f1e137bbe222dd048b0386503c7", + "scope": "Independent WASMv1 section/type/function/export parsing, not complete validation." + }, + "rectangle-members": { + "receipt_file": "artifact-selected-members-oracle.json", + "sha256": "aa670d3f5218fad2e1d8b3d27cfc3e3ccdf7e487130655d9ee18eca188d5c40e", + "scope": "ZIP membership, six Unix symlinks and universal Mach-O magic." + }, + "obsidian-literals": { + "receipt_file": "javascript-selected-literal-oracle.json", + "sha256": "6b89e8e3345b306d3fa730cb0c06c431952a146b22bdc80361ed7d28321669e0", + "scope": "Exact app.js bytes/literal locations and proximity, not behavior." + }, + "agent-browser-entry": { + "receipt_file": "agent-browser-shipped-artifact.json", + "sha256": "fa15475292bdeae665824387eeee39edb21c89f8fea9acd1cc98c52488cc5081", + "scope": "Actual installed npm metadata and shipped wrapper file identity/source declarations." + }, + "agent-browser-locations": { + "receipt_file": "agent-browser-source-locations.json", + "sha256": "86de5f9df5e02e0552172f69798a6539ea60df8683877c932879d905b4103d12", + "scope": "Exact static import/function/call syntax source positions." + }, + "mobile-bytes": { + "receipt_file": "mobile-independent-oracles.json", + "sha256": "3b2384558020bff1ec6d91b97be7e7048f80f9a034e21741606a7cd2dcde3498", + "scope": "APK/HAP ZIP members/hashes, ELF headers and independently parsed Harmony JSON declarations." + }, + "shark-app-member": { + "receipt_file": "harmony-shark-app-identity.json", + "sha256": "2ff4e7e82373735d44f8e00da790e512e096a85864c3066a9d4ca20077aa4d07", + "scope": "Official ZIP -> APP -> HAP exact member byte identities; no signature verification." + }, + "process-capture": { + "receipt_file": "process/validation-summary.json", + "sha256": "37da2e897f19e980815bf31b7fef60236a3c7de707776b2ccf5c86c7b7e8e09a", + "scope": "Actual controlled jq capture receipts, owned cleanup, rendered terminal JSON and comparison scope." + }, + "process-output": { + "receipt_file": "process/independent-oracle.json", + "sha256": "c48c2083a9b23bb80a91ac74859668a24f815b543df6a26176ed7daeee16574f", + "scope": "Controlled terminal JSON revision before/after, observed independently of REA comparison." + }, + "process-inline": { + "receipt_file": "process/expected.json", + "sha256": "72e430aaf4e6505039e414bc3deda56a568cd60bb686780220c92acc0a2a1278", + "scope": "Existing actual inline CLI comparison result and capture IDs, matched against MCP result." + }, + "wasm-export": { + "receipt_file": "final-runtime-cohort/wasm-export-identity.json", + "sha256": "0937303fff3223a8a79aee5453240157bff4413ca16809ca83dc8a5a2454dc89", + "scope": "Actual model-exported bundle bytes, four records, exact source/comparison IDs; CLI follow-up not executed by this agent." + } + }, + "runs": [ + { + "id": "initial/artifact-rectangle-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": "rectangle-zip", + "prompt_sha256": "1136286c9772e6a4a3679255c2bb994153035112382558ef343b72609d6ac65e", + "usage": { + "scope": "completed-turn", + "input_tokens": 346798, + "cached_input_tokens": 295680, + "cache_write_input_tokens": 0, + "output_tokens": 1540, + "reasoning_output_tokens": 46 + }, + "calls": { + "completed": 6, + "failed": 0, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "inspect_analysis_view": 1, + "project_apple_application_graph": 1, + "compare_artifacts": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 40.218, + "complete_resource_scope": 40.818 + }, + "local_resources": { + "cpu_usec": 8063731, + "peak_process_family_memory_bytes": 681668608, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Six symlink availabilities remain unknown; self-comparison overallunknown,269unchanged+6unknown. Path roles do not establish parsed plist/runtime behavior." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/javascript-agent-browser-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-failed-supplemental", + "artifact_id": "agent-browser-entry", + "prompt_sha256": "bd40c5aced0ad3784732707612868fd70fcccffc1def5c09381f710ada5a09c6", + "usage": { + "scope": "observed-cumulative-lower-bound-before-interruption", + "input_tokens": 1234923, + "cached_input_tokens": 1182336, + "cache_write_input_tokens": 0, + "output_tokens": 3274, + "reasoning_output_tokens": 716, + "total_tokens": 1238197, + "observed_at": "2026-10-31T11:14:00.265Z", + "final_total": "unavailable" + }, + "calls": { + "completed": 31, + "failed": 10, + "by_tool": { + "inspect_desktop_application": 22, + "search_guidance": 2, + "open_binary": 2, + "read_bytes": 1, + "read_guidance_section": 1, + "analyze_javascript_application": 1, + "close_binary": 2 + } + }, + "elapsed_seconds": { + "task": 86.873, + "complete_resource_scope": 87.528 + }, + "local_resources": { + "cpu_usec": 23684944, + "peak_process_family_memory_bytes": 937619456, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "terminated_for_model_task_scope_deviation", + "known_limitations": [ + "Terminated after model wandered beyond supplied target and used wrong analysis input; no final answer/composition claim.", + "Tool availability proven by typed response from application-analysis tool; this was target/task-following failure.", + "Graceful client exit0 does not establish completed task. Final completed-turn usage absent; retained last cumulative usage is a lower bound only." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/javascript-obsidian-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": "obsidian-asar", + "prompt_sha256": "b6fa427f876d1270000f48853a4736fac76044d5fe576ee0aaf1a01faab7767c", + "usage": { + "scope": "completed-turn", + "input_tokens": 679803, + "cached_input_tokens": 644480, + "cache_write_input_tokens": 0, + "output_tokens": 2233, + "reasoning_output_tokens": 308 + }, + "calls": { + "completed": 7, + "failed": 4, + "by_tool": { + "analyze_javascript_application": 1, + "inspect_analysis_view": 4, + "trace_application_feature": 1, + "trace_javascript_semantics": 1 + } + }, + "elapsed_seconds": { + "task": 347.242, + "complete_resource_scope": 347.812 + }, + "local_resources": { + "cpu_usec": 313685188, + "peak_process_family_memory_bytes": 1871228928, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "partial_task_existing_producer_timeout_and_output_limit", + "known_limitations": [ + "Initial source hit existing300000ms deadline; retained partial source reused without producer replay.", + "Feature computation retained but >=10534041-byte response exceeded10MiB transport budget; feature summary unsupported.", + "One model view-argument error. Requested bounded feature task remained incomplete;31of32sources unexamined." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/managed-nuget-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": "nuget-cli", + "prompt_sha256": "04580fa379f975f315b2b35b370824c9c668f36c913aa976fafe50cbc5a19879", + "usage": { + "scope": "completed-turn", + "input_tokens": 312762, + "cached_input_tokens": 258560, + "cache_write_input_tokens": 0, + "output_tokens": 1977, + "reasoning_output_tokens": 184 + }, + "calls": { + "completed": 8, + "failed": 1, + "by_tool": { + "inspect_managed_artifact": 1, + "inspect_managed_members": 2, + "inspect_analysis_view": 3, + "project_managed_application_graph": 1, + "compare_managed_members": 1 + } + }, + "elapsed_seconds": { + "task": 47.209, + "complete_resource_scope": 47.757 + }, + "local_resources": { + "cpu_usec": 9963962, + "peak_process_family_memory_bytes": 884748288, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "One expected typed refusal: managed member comparison requires distinct Evidence. Selected metadata and getter CIL matched independent reader; six dnfile compressed-integer warnings preclude whole-file validity claim." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/native-jq-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": "jq", + "prompt_sha256": "771bbef6a8e4b544707ed533a4d80f16fd45fb5f133efe061f347ab3adfc7025", + "usage": { + "scope": "completed-turn", + "input_tokens": 619582, + "cached_input_tokens": 558208, + "cache_write_input_tokens": 0, + "output_tokens": 1707, + "reasoning_output_tokens": 119 + }, + "calls": { + "completed": 12, + "failed": 2, + "by_tool": { + "open_binary": 1, + "list_procedures": 2, + "procedure_callees": 1, + "analyze_function": 2, + "build_call_path": 2, + "compare_functions": 2, + "search_guidance": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 74.555, + "complete_resource_scope": 75.181 + }, + "local_resources": { + "cpu_usec": 34261922, + "peak_process_family_memory_bytes": 1463037952, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Two model argument failures omitted required reference kind; guidance corrected them. Each dossier producer called once; static direct call corroborated, runtime unknown." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/reference-negative-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": null, + "prompt_sha256": "11930f48db70d30ca33623a4c583aa9158501f08a6b82043ef439e8316d9d6b3", + "usage": { + "scope": "completed-turn", + "input_tokens": 36805, + "cached_input_tokens": 23680, + "cache_write_input_tokens": 0, + "output_tokens": 284, + "reasoning_output_tokens": 0 + }, + "calls": { + "completed": 1, + "failed": 1, + "by_tool": { + "build_call_path": 1 + } + }, + "elapsed_seconds": { + "task": 18.152, + "complete_resource_scope": 18.708 + }, + "local_resources": { + "cpu_usec": 5248159, + "peak_process_family_memory_bytes": 518017024, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": null, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_expected_missing_reference_failure", + "known_limitations": [ + "One intentional unknown-ID failure; exact missing retained-closure diagnostic/recovery preserved. No target opened or retry." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "initial/wasm-css-01", + "runtime_id": "initial-dirty-runtime", + "cohort": "complete-catalog-primary", + "artifact_id": "css-wasm", + "prompt_sha256": "62d72a4715be9912e0463304d22046d101ae1508f2713d392e08268c6beb5777", + "usage": { + "scope": "completed-turn", + "input_tokens": 226862, + "cached_input_tokens": 186240, + "cache_write_input_tokens": 0, + "output_tokens": 934, + "reasoning_output_tokens": 70 + }, + "calls": { + "completed": 4, + "failed": 0, + "by_tool": { + "inspect_wasm_artifact": 1, + "inspect_analysis_view": 1, + "compare_wasm_interfaces": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 33.172, + "complete_resource_scope": 33.678 + }, + "local_resources": { + "cpu_usec": 9233193, + "peak_process_family_memory_bytes": 723169280, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Static interface self-comparison only; no runtime/ABI/linking compatibility claim." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "final/android-newpipe-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "newpipe-apk", + "prompt_sha256": "b9530066a166ba81d4a4a012f48918cbbc5579c84d2ff0bd998520b3e770fe65", + "usage": { + "scope": "completed-turn", + "input_tokens": 179635, + "cached_input_tokens": 150528, + "cache_write_input_tokens": 0, + "output_tokens": 1750, + "reasoning_output_tokens": 46 + }, + "calls": { + "completed": 5, + "failed": 0, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "project_android_application_graph": 1, + "inspect_analysis_view": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 40.835, + "complete_resource_scope": 41.352 + }, + "local_resources": { + "cpu_usec": 10080132, + "peak_process_family_memory_bytes": 704409600, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Path-based runtime/JNI hypotheses only; central-directory inventory excludes non-entry signing bytes, DEX/manifest/signature semantics unverified." + ], + "tool_profile_id": "selected-1" + }, + { + "id": "final/harmony-shark-app-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "shark-app", + "prompt_sha256": "cc2bf2d25f7cb4497411c2ab656be84470847ba828a851debccb307ea95ade77", + "usage": { + "scope": "completed-turn", + "input_tokens": 221571, + "cached_input_tokens": 197376, + "cache_write_input_tokens": 0, + "output_tokens": 1339, + "reasoning_output_tokens": 105 + }, + "calls": { + "completed": 6, + "failed": 0, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "inspect_analysis_view": 2, + "project_harmony_application_graph": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 32.437, + "complete_resource_scope": 33.184 + }, + "local_resources": { + "cpu_usec": 6377128, + "peak_process_family_memory_bytes": 560021504, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "APP inventory is shallow: innerHAP model/manifest/Ark behavior unknown. Signed naming and empty signing list do not verify signature status." + ], + "tool_profile_id": "selected-2" + }, + { + "id": "final/harmony-shark-hap-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "shark-hap", + "prompt_sha256": "d7f22a3764fa5673f7d7f3235093f41d43a4028b8cf7eaa2342b9a2527c4a200", + "usage": { + "scope": "completed-turn", + "input_tokens": 184452, + "cached_input_tokens": 152576, + "cache_write_input_tokens": 0, + "output_tokens": 1212, + "reasoning_output_tokens": 43 + }, + "calls": { + "completed": 5, + "failed": 0, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "inspect_analysis_view": 1, + "project_harmony_application_graph": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 31.108, + "complete_resource_scope": 31.783 + }, + "local_resources": { + "cpu_usec": 6012587, + "peak_process_family_memory_bytes": 580714496, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Stage/Ark/resource roles are path hints; no signature validation, JSON semantic interpretation by projection, bytecode decoding or runtime execution." + ], + "tool_profile_id": "selected-2" + }, + { + "id": "final/javascript-agent-browser-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "agent-browser-entry", + "prompt_sha256": "db542ef5ed9c66b8b6421dc719bfd7352849798e6526476360e3f425661513bf", + "usage": { + "scope": "completed-turn", + "input_tokens": 619589, + "cached_input_tokens": 563712, + "cache_write_input_tokens": 0, + "output_tokens": 2519, + "reasoning_output_tokens": 180 + }, + "calls": { + "completed": 12, + "failed": 1, + "by_tool": { + "analyze_javascript_application": 1, + "inspect_analysis_view": 5, + "trace_application_feature": 2, + "search_guidance": 1, + "read_guidance_section": 1, + "trace_javascript_semantics": 2 + } + }, + "elapsed_seconds": { + "task": 54.521, + "complete_resource_scope": 55.108 + }, + "local_resources": { + "cpu_usec": 9472096, + "peak_process_family_memory_bytes": 903847936, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "One corrected model view-syntax error;12calls include2feature and2semantic requests, producer once.", + "Imported targets external/unresolved, ownership trace has no edges,5unselectedsources unknown. Directory subject digest distinct from independently pinned file digest." + ], + "tool_profile_id": "selected-3" + }, + { + "id": "final/native-jq-01", + "runtime_id": "final-dirty-runtime", + "cohort": "same-prompt-uncontrolled-final-runtime-repeat", + "artifact_id": "jq", + "prompt_sha256": "771bbef6a8e4b544707ed533a4d80f16fd45fb5f133efe061f347ab3adfc7025", + "usage": { + "scope": "completed-turn", + "input_tokens": 599933, + "cached_input_tokens": 535424, + "cache_write_input_tokens": 0, + "output_tokens": 1894, + "reasoning_output_tokens": 56 + }, + "calls": { + "completed": 11, + "failed": 0, + "by_tool": { + "open_binary": 1, + "list_procedures": 1, + "procedure_callees": 1, + "analyze_function": 2, + "search_guidance": 1, + "read_guidance_section": 2, + "build_call_path": 1, + "compare_functions": 1, + "close_binary": 1 + } + }, + "elapsed_seconds": { + "task": 80.931, + "complete_resource_scope": 81.487 + }, + "local_resources": { + "cpu_usec": 38326466, + "peak_process_family_memory_bytes": 1269936128, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts", + "known_limitations": [ + "Uncontrolled same-prompt/full-catalog repetition selected a different callee;3guidance calls,0invalid references. No causal retry/token/time improvement claim." + ], + "tool_profile_id": "complete-catalog" + }, + { + "id": "final/process-static-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "process-capture-bundle", + "prompt_sha256": "5c9669834c9db02974e20940f23f3fed99c3253943844fc259e21ffa41c9ef3c", + "usage": { + "scope": "completed-turn", + "input_tokens": 89101, + "cached_input_tokens": 73984, + "cache_write_input_tokens": 0, + "output_tokens": 725, + "reasoning_output_tokens": 0 + }, + "calls": { + "completed": 3, + "failed": 0, + "by_tool": { + "import_evidence_bundle": 1, + "compare_process_captures": 1, + "find_changed_behavior": 1 + } + }, + "elapsed_seconds": { + "task": 26.911, + "complete_resource_scope": 27.578 + }, + "local_resources": { + "cpu_usec": 5575060, + "peak_process_family_memory_bytes": 531283968, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": null, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_authentic_capture_reference_chain", + "known_limitations": [ + "Exact static import->refpair comparison->reflist aggregate chain; no model capture/target execution.", + "MCP comparison deep-equals existing real inline CLI oracle; terminal/overallchanged, aggregateobserved_changed.", + "Exitdimension includes settlement differences; both root numeric exitcodes0. Process/filesystem/network/environment/imagebinding unknowns preserved." + ], + "tool_profile_id": "selected-4", + "input_bundle_bytes_unchanged": true + }, + { + "id": "final/wasm-css-export-final-01", + "runtime_id": "final-dirty-runtime", + "cohort": "diagnostic/catalog-selection", + "artifact_id": "css-wasm", + "prompt_sha256": "b490a05c18a3e9a674a649042fbb9489970c07d8bc12ba07dc966b90d3c9f1ac", + "usage": { + "scope": "completed-turn", + "input_tokens": 161702, + "cached_input_tokens": 141056, + "cache_write_input_tokens": 0, + "output_tokens": 1033, + "reasoning_output_tokens": 27 + }, + "calls": { + "completed": 4, + "failed": 0, + "by_tool": { + "inspect_wasm_artifact": 1, + "inspect_analysis_view": 1, + "compare_wasm_interfaces": 1, + "export_evidence_bundle": 1 + } + }, + "elapsed_seconds": { + "task": 32.32, + "complete_resource_scope": 32.888 + }, + "local_resources": { + "cpu_usec": 9036127, + "peak_process_family_memory_bytes": 700076032, + "observation_verified": true, + "oom": 0, + "oom_kill": 0 + }, + "cleanup": { + "payload_family_verified": true, + "copied_auth_profile_removed": true + }, + "target_bytes_unchanged": true, + "client_exit_code": 0, + "task_timeout": false, + "resource_abort": null, + "outcome": "passed_within_reviewed_task_facts_and_real_export", + "known_limitations": [ + "Actual bundle export checked, four records; bundleunknown count0 does not mean all target behavior known. Portable CLI equality follow-up excluded from this receipt." + ], + "tool_profile_id": "selected-5" + } + ], + "actual_wasm_export": { + "file_name": "wasm-css-evidence.json", + "bytes": 13925066, + "sha256": "acb0cb6c3e0d520f83144e106e171c3ecebba70c5c0498bbb0113185a3e4bf95", + "records": 4, + "source_artifact_id": "css-wasm", + "full_parent_evidence_id": "ev_27ff1c46b138fd9809ab403aa2db919b440b0d822006cdc18a44e651f2508f43", + "comparison_evidence_id": "ev_b32346965c153a067abff9698aeba1ccbace34c78ac2071ac18311250b68c6da", + "scope": "exports", + "portable_CLI_followup": "Not executed by this receipt author; separate driver evidence required." + }, + "audit": { + "attempts": 14, + "all_copied_auth_profiles_removed": true, + "all_payload_cleanup_verified": true, + "all_resource_observations_verified": true, + "all_oom_events_zero": true + }, + "tool_profiles": { + "complete-catalog": { + "kind": "complete-catalog", + "enabled_tools": null, + "exact_name_guidance": false + }, + "selected-1": { + "kind": "selected-relevant-tools", + "enabled_tools": [ + "open_binary", + "inspect_artifact", + "inspect_analysis_view", + "close_binary", + "search_guidance", + "read_guidance_section", + "project_android_application_graph" + ], + "exact_name_guidance": true + }, + "selected-2": { + "kind": "selected-relevant-tools", + "enabled_tools": [ + "open_binary", + "inspect_artifact", + "inspect_analysis_view", + "close_binary", + "search_guidance", + "read_guidance_section", + "project_harmony_application_graph" + ], + "exact_name_guidance": true + }, + "selected-3": { + "kind": "selected-relevant-tools", + "enabled_tools": [ + "analyze_javascript_application", + "inspect_analysis_view", + "trace_application_feature", + "trace_javascript_semantics", + "search_guidance", + "read_guidance_section" + ], + "exact_name_guidance": true + }, + "selected-4": { + "kind": "selected-relevant-tools", + "enabled_tools": [ + "import_evidence_bundle", + "compare_process_captures", + "find_changed_behavior", + "search_guidance", + "read_guidance_section" + ], + "exact_name_guidance": true + }, + "selected-5": { + "kind": "selected-relevant-tools", + "enabled_tools": [ + "inspect_wasm_artifact", + "inspect_analysis_view", + "compare_wasm_interfaces", + "export_evidence_bundle", + "search_guidance", + "read_guidance_section" + ], + "exact_name_guidance": true + } + }, + "review_method": "Agent transcript review against the listed independent byte/metadata oracles, limited to stated task facts; not human acceptance or complete-provider correctness. Oracle receipts are retained externally, identified here by relative file and SHA; raw source/transcripts are not embedded.", + "model_receipt_source_sha256": "7643761cbd91d1a75180ac7bc4ae49560abc176eea8e302c6915e014980623f0", + "portable_cli_comparison": { + "scope": "Candidate runtime: equivalent complete inline vs selected-bundle retained-reference self-comparisons of one authentic real WASM inspection, scope exports.", + "selected_bundle_sha256": "acb0cb6c3e0d520f83144e106e171c3ecebba70c5c0498bbb0113185a3e4bf95", + "selected_bundle_bytes": 13925066, + "selected_parent_evidence_id": "ev_27ff1c46b138fd9809ab403aa2db919b440b0d822006cdc18a44e651f2508f43", + "expected_mcp_comparison_evidence_id": "ev_b32346965c153a067abff9698aeba1ccbace34c78ac2071ac18311250b68c6da", + "arm_order": ["inline", "reference"], + "cache_state": "Each arm starts a fresh public CLI process. No system/filesystem cache flush; no cold-cache claim. Order is explicit and later arms may benefit from warm host caches.", + "common_costs": "Source inspection and MCP export happened before this paired experiment and are not included in per-arm metrics. Their separate measurements must be reported when making whole-task claims. Preparation/verification/tokenization each have separate bounded receipts; no outer driver process-family metric is claimed.", + "commands": { + "inline": "node scripts/rea.mjs compare-wasm-interfaces INLINE_REQUEST.json --json", + "reference": "node scripts/rea.mjs compare-wasm-interfaces REFERENCE_REQUEST.json --json --evidence-bundle EXPORTED_BUNDLE.json" + }, + "arms": [ + { + "arm": "inline", + "scope_elapsed_seconds": 2.832, + "cpu_usage_usec": 2612440, + "peak_process_family_memory_bytes": 370515968, + "metric_scope": "Counters at payload exit while observer remains alive; not final whole-scope counters.", + "cleanup_verified": true, + "exit_code": 0, + "request_json_bytes": 27823017, + "request_json_sha256": "b5b341ff02f4044ab69396bd3008b8184faff89108867ebc21f09589a3e59525", + "output_json_bytes": 3732, + "output_json_sha256": "9d2dd8cdc7f72d863b30662b05fc73960d20c49cb1472f67f77c3173063e1f97", + "selected_bundle_unchanged": true, + "canonical_source_record_unchanged": true + }, + { + "arm": "reference", + "scope_elapsed_seconds": 2.632, + "cpu_usage_usec": 2385397, + "peak_process_family_memory_bytes": 310009856, + "metric_scope": "Counters at payload exit while observer remains alive; not final whole-scope counters.", + "cleanup_verified": true, + "exit_code": 0, + "request_json_bytes": 261, + "request_json_sha256": "7e5056031244e1c7dec5f417a06634e348d989139a134e6911acd1b5b8bd65f7", + "output_json_bytes": 3732, + "output_json_sha256": "9d2dd8cdc7f72d863b30662b05fc73960d20c49cb1472f67f77c3173063e1f97", + "selected_bundle_unchanged": true, + "canonical_source_record_unchanged": true + } + ], + "preparation": { + "scope_elapsed_seconds": 1.119, + "cpu_usage_usec": 875255, + "peak_process_family_memory_bytes": 316796928, + "metric_scope": "Counters at payload exit while observer remains alive; not final whole-scope counters.", + "cleanup_verified": true + }, + "verification": { + "scope_elapsed_seconds": 0.518, + "cpu_usage_usec": 282755, + "peak_process_family_memory_bytes": 42881024, + "metric_scope": "Counters at payload exit while observer remains alive; not final whole-scope counters.", + "cleanup_verified": true + }, + "all_output_bytes_and_authenticated_evidence_equal": true, + "source_bundle_and_runtime_unchanged": true, + "serialized_input_token_estimates": "Not measured; the optional tokenizer was unavailable. Bytes are not model token usage." + }, + "review_followup": { + "scope": "Source review and regression checks after the recorded application model cohorts; no new real-provider measurement is inferred.", + "findings": [ + { + "origin": "local Codex CLI gpt-6.1-terra/high source-only review", + "root_cause": "Process CLI read later caller-selected files before authenticating earlier capture Evidence.", + "fix": "Shared authenticated-source readers and parsed-fact comparator; restore sequential source admission.", + "regressions": "Two public CLI invalid-source/missing-later-file cases." + }, + { + "origin": "Greptile PR review", + "root_cause": "Caller-owned inline WASM comparison sources incorrectly required a fixed-server recorder.", + "fix": "Explicit caller ownership in shared retention policy; provider default remains required and writer failures/cancellation remain preserved.", + "regressions": "Public fixed-server MCP comparison plus shared retention ownership/error/cancellation checks." + } + ], + "focused_checks": { + "review_related": 103, + "schema_cleanup_and_cli": 10 + }, + "actual_capture_recomparison_evidence_id": "ev_d9360c023f36af83abe76b9ee84625588d25db7453d89d449a22b8f357220969", + "actual_capture_recomparison_id_unchanged": true, + "source_review": { + "requested_model": "gpt-6.1-terra", + "reasoning": "high", + "task_elapsed_seconds": 57.86048012799802, + "family_cpu_seconds": 4.720926, + "peak_family_bytes": 427364352, + "auth_profile_removed": true, + "payload_cleanup_verified": true, + "model_tokens": "Not reported by the CLI review mode; zero-valued usage counters are not treated as measured zero cost." + } + }, + "public_transport_matrix": { + "schema_version": 1, + "date": "2026-10-31", + "scope": "Actual deterministic public stdioMCP/CLI transport matrix, no models or simulated/providerfixture Evidence.13attempts across9lanes; all failed harness/capacity attempts retained.", + "client": { + "node": "24.22.1", + "MCP_SDK_client": "2.3.1", + "model_invocations": 0, + "token_measurement": "not performed; no byte-to-token claim" + }, + "runtime": { + "built_source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "source_state": "Actual perattempt HEAD/dirty recorded. Later source-only CLI diagnostic naming/test expectation fixes were not rebuilt during this matrix and are verified separately by parent focused7file/161test gate." + }, + "bounds": { + "CPU_quota_percent": 100, + "hard_memory_bytes": 4294967296, + "early_stop_bytes": 3221225472, + "swap_bytes": 0, + "tasks": 192, + "case_seconds": 300, + "MCP_call_seconds": 120, + "owned_CLI_seconds": 120, + "default_MCP_input_elements": 1024, + "default_SDK_stdio_buffer_bytes": 10485760, + "allowances_raised": false + }, + "methods": [ + "Genuine summary producers ran once per declared source selection, exported bundle and closed original connection. Harness retries reused the same export, no producer replay.", + "Fresh target-free servers imported authentic bundles; binary_session.open stayedfalse. Missing beforeimport/unknownID/malformedkind/wrongrole and managedsameID negatives were tested; negative calls did not mutate registry.", + "Large MCPinline inputs either hit explicit1024-element refusal before schema/workflow, or the distinct10MiB SDKframe boundary. Supported CLI forms and MCPrefs still return complete equivalent Evidence.", + "Public CLI syntax was preserved: native noCLI; managed membercomparison PE-pathproducer only; process2complete capture paths; artifact comparison explicit evidence-file selectors.", + "Successful full structural Evidence equality preserves identity,links,coverage,partial/unknown andlimitations; reference-only native/member workflows are not claimed to have unsupported CLIparity." + ], + "measurement_limits": [ + "Perlane totals sum ALL attempt scopeelapsed/CPU and take max kernelpeak, including failed harness attempts. Raw perattempt figures remain below.", + "Kernel CPU/memory are payload-exit counters with observeralive, not sampled peaks; remote model compute absent. Orchestration/editing and earlier sourceproducer costs are not included.", + "Fresh processes, preinstalled tools, uncontrolled OS/filesystem caches; no before/after/cold/warm benchmark or token-saving claim.", + "WASM/process reused earlier authentic producer data. Common upstream sourceinspection/capture costs are in prior verification receipts, not new analysis.", + "Optional static/runtime correlation is an explicit hypothesis only, actualresultunknown/unresolved1; no causal inference." + ], + "artifacts": { + "agent-browser-entry": { + "file_name": "bin/agent-browser.js", + "bytes": 3184, + "sha256": "8e382f4a5ba22f45e1e0339abfe5a55ed95a19540b16a69ee3faf31c8dc8216a", + "provenance": { + "kind": "existing-installed-npm-package-entrypoint", + "package": "agent-browser", + "version": "0.27.0", + "repository_from_package_metadata": "https://github.com/vercel-labs/agent-browser", + "publisher_authenticity": "not independently established" + }, + "independent_oracles": [ + "agent-browser-entry", + "agent-browser-locations" + ] + }, + "css-wasm": { + "file_name": "tree-sitter-css.wasm", + "bytes": 128668, + "sha256": "8a23977fe271357cce6f254ef88c9bebf3854602d8046605aef6a45c02135c59", + "provenance": { + "kind": "existing-installed-copy-matching-pinned-repository-corpus-member", + "project": "VS Code Tree-sitter published WASM package", + "version": "0.3.1", + "upstream_url": "https://registry.npmjs.org/@vscode/tree-sitter-wasm/-/tree-sitter-wasm-0.3.1.tgz", + "transport_sha256": "985684b2cfbe5e8f90499a9f36e37519887c7d4e2697d0457d0f1e923ce5184c", + "transport_scope": "Published corpus provenance; transport archive was not reacquired for this validation." + }, + "independent_oracles": ["base-artifacts", "wasm-interface"] + }, + "jq": { + "file_name": "jq", + "bytes": 30800, + "sha256": "90b1eef4498f31836342898fc6e97cfeb8a197c4366773a0fb120ae02a45ea52", + "provenance": { + "kind": "existing-system-installed-file", + "publisher_authenticity": "not established by file identity" + }, + "independent_oracles": ["base-artifacts", "jq-disassembly"] + }, + "newpipe-apk": { + "file_name": "NewPipe_v0.29.1.apk", + "bytes": 11507122, + "sha256": "18447bfb1e06d113edc88df93f471827280de06f6f3d4dc42f56f28b9c1bab79", + "provenance": { + "kind": "official-public-release-matching-repository-corpus-pin", + "url": "https://github.com/TeamNewPipe/NewPipe/releases/download/v0.29.1/NewPipe_v0.29.1.apk", + "version": "0.29.1" + }, + "independent_oracles": ["mobile-bytes"] + }, + "nuget-cli": { + "file_name": "NuGet.CommandLine.XPlat.dll", + "bytes": 756736, + "sha256": "ddea7807716a77a166cc321ddfad566e80569d9919dd8ecdf0ce50e4e9059642", + "provenance": { + "kind": "existing-local-dotnet-SDK-copy", + "SDK_version": "8.0.427", + "assembly_version": "6.11.2.1", + "publisher_authenticity": "not independently established" + }, + "independent_oracles": ["base-artifacts", "managed-getter"] + }, + "process-capture-bundle": { + "file_name": "captures.bundle.json", + "bytes": 15199, + "sha256": "55da0f130dc2265aae54261d49949471e5c929163c2b7314b42e95df3325b739", + "provenance": { + "kind": "two-authentic-controlled-local-jq-captures", + "executable_artifact_id": "jq", + "scenario": "jq -cn with explicit before/after revision filters and equal comparison contracts; capture producers ran separately from static model trial." + }, + "independent_oracles": [ + "process-capture", + "process-output", + "process-inline" + ] + }, + "rectangle-zip": { + "file_name": "Rectangle2.0.4.zip", + "bytes": 5080855, + "sha256": "0b9b8b11edeb3237e3224c619a6f6bfb5e5996a477048be6ff4b98b0346aedc9", + "provenance": { + "kind": "official-public-release", + "url": "https://github.com/rxhanson/Rectangle/releases/download/v2.0.4/Rectangle2.0.4.zip", + "version": "2.0.4" + }, + "independent_oracles": ["base-artifacts", "rectangle-members"] + }, + "shark-app": { + "file_name": "Shark-default-v1.0.8-signed.app", + "bytes": 1867667, + "sha256": "b2344fb11bb31477c5c44734c2d26fd29adcab40719712664e48f973ae0909d3", + "provenance": { + "kind": "exact-regular-ZIP-member", + "parent_artifact_id": "shark-zip", + "member_path": "Shark-default-v1.0.8-signed.app" + }, + "independent_oracles": ["shark-app-member", "mobile-bytes"] + }, + "shark-hap": { + "file_name": "Shark-v1.0.8-entry-default.hap", + "bytes": 3061314, + "sha256": "177659902ed83329a5fbda867c7985660aa2d33f71cbc8ac8baf53b7da4caac8", + "provenance": { + "kind": "exact-regular-APP-member", + "parent_artifact_id": "shark-app", + "member_path": "entry-default.hap" + }, + "independent_oracles": ["shark-app-member", "mobile-bytes"] + } + }, + "lanes": [ + { + "lane": "native", + "attempts": 1, + "all_attempt_scope_seconds_sum": 35.915, + "all_attempt_CPU_usec_sum": 34674174, + "max_kernel_peak_bytes": 973070336, + "MCP_completed_calls": 31, + "CLI_arms_output_files": 0, + "successful_attempt": "native-01", + "notes": "Fresh imported inventory+dossier refs support path/comparison/matching inclnested observation arrays. Inline forms refused1024; these operations have no CLI counterparts. Staticfacts only, unresolved/truncated data preserved." + }, + { + "lane": "javascript", + "attempts": 3, + "all_attempt_scope_seconds_sum": 34.324, + "all_attempt_CPU_usec_sum": 33094918, + "max_kernel_peak_bytes": 690913280, + "MCP_completed_calls": 35, + "CLI_arms_output_files": 8, + "successful_attempt": "javascript-03", + "notes": "Feature+semantic MCPrefs == CLIinline == CLIbundle refs; both large MCPinline forms refused default1024. One genuine producer across3attempts. Partial/unselected-source facts preserved." + }, + { + "lane": "managed", + "attempts": 2, + "all_attempt_scope_seconds_sum": 37.341, + "all_attempt_CPU_usec_sum": 36221694, + "max_kernel_peak_bytes": 728391680, + "MCP_completed_calls": 41, + "CLI_arms_output_files": 9, + "successful_attempt": "managed-02", + "notes": "Graph3-way parity; member comparison refs use genuine distinct Program/anonymous selections. Same-ID refusal checked. Selected-Evidence CLI member comparison unavailable: existing command produces from2PEpaths." + }, + { + "lane": "android", + "attempts": 1, + "all_attempt_scope_seconds_sum": 20.208, + "all_attempt_CPU_usec_sum": 19569397, + "max_kernel_peak_bytes": 730226688, + "MCP_completed_calls": 17, + "CLI_arms_output_files": 4, + "successful_attempt": "android-01", + "notes": "Graph3-way parity with real APK; manifest/3DEX/4ABI path facts and JNIinferences preserved. FullinlineMCP refused1024." + }, + { + "lane": "harmony-app", + "attempts": 1, + "all_attempt_scope_seconds_sum": 16.714, + "all_attempt_CPU_usec_sum": 15950327, + "max_kernel_peak_bytes": 519761920, + "MCP_completed_calls": 17, + "CLI_arms_output_files": 4, + "successful_attempt": "harmony-app-01", + "notes": "Small realAPP full MCPinline also admitted:4-way equality. Inventory remains shallow, innerHAP/signature semantics unknown." + }, + { + "lane": "harmony-hap", + "attempts": 1, + "all_attempt_scope_seconds_sum": 16.294, + "all_attempt_CPU_usec_sum": 15886932, + "max_kernel_peak_bytes": 533172224, + "MCP_completed_calls": 17, + "CLI_arms_output_files": 4, + "successful_attempt": "harmony-hap-01", + "notes": "Graph3-way parity; Stage/Ark/resource roles remain path hints, no signature/bytecode/runtime claim. FullinlineMCP refused1024." + }, + { + "lane": "apple", + "attempts": 1, + "all_attempt_scope_seconds_sum": 25.453, + "all_attempt_CPU_usec_sum": 24824482, + "max_kernel_peak_bytes": 674127872, + "MCP_completed_calls": 21, + "CLI_arms_output_files": 8, + "successful_attempt": "apple-01", + "notes": "Applegraph3-way parity plus compare_artifacts MCPrefs==CLIinline==CLIexplicit evidence-file selectors. Symlink availabilityunknown preserved; artifact CLI has no generic bundle flag." + }, + { + "lane": "wasm", + "attempts": 2, + "all_attempt_scope_seconds_sum": 22.648, + "all_attempt_CPU_usec_sum": 22027649, + "max_kernel_peak_bytes": 830128128, + "MCP_completed_calls": 21, + "CLI_arms_output_files": 4, + "successful_attempt": "wasm-02", + "notes": "Existing authentic14MBbundle, no WABT replay. Fresh MCPrefs == full CLIinline == CLIbundle refs. MCPinline unavailable at default10MiB frame limit; actual failed attempt retained separately from1024admission." + }, + { + "lane": "process", + "attempts": 1, + "all_attempt_scope_seconds_sum": 9.056, + "all_attempt_CPU_usec_sum": 8586552, + "max_kernel_peak_bytes": 495534080, + "MCP_completed_calls": 20, + "CLI_arms_output_files": 2, + "successful_attempt": "process-01", + "notes": "Existing authenticcaptures, no capture replay. MCPref/inline == CLIcomplete-capture-file pair; aggregate ref/inline equal. Imported genuine nativecomparison and explicit correlation ref/inline equal, unknown/unresolved1, no causalclaim. No selected-bundle CLI process option." + } + ], + "attempts": [ + { + "attempt": "android-01", + "lane": "android", + "artifact_id": "newpipe-apk", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "0ae5954705a398dee5fdd893cc3664b61c298a64895e135df32610ac00c6f40c", + "scope_elapsed_seconds": 20.208, + "CPU_usec": 19569397, + "peak_process_family_bytes": 730226688, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 17, + "typed_error_results": 5, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 4, + "project_android_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 4, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "graph-cli-inline", + "command": "project-android-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "371324919957bab8d89fc1a41db087ecf682bf24f83e6a1fc79d2b6378ad2e24", + "input_bytes": 1519195, + "output_sha256": "797dd46faa2674b92b15b672866fc2eafc7fef0ffb81a30450577eb6fb0d29a8", + "output_bytes": 7740, + "exit": 0, + "elapsed_ms": 2966.651, + "receipt_sha256": "6cb19918388b026ad560f9d5bd7314fdf068756a1024505554cfb08d8fe433a5" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-android-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "219d31de32c202d720bed52c486079cb2a866b855a97314578eaa434c702245d", + "input_bytes": 138, + "output_sha256": "7252ca8c73c1d467d47327875852d2eb3bc5814d33a0cfdef4aefc0d48304702", + "output_bytes": 856, + "exit": 1, + "elapsed_ms": 2260.39, + "receipt_sha256": "7973d2928df41a31cedf08da4bb41516bbcf599186cc149fc7a4f557f4d19d37" + }, + { + "arm": "graph-cli-reference", + "command": "project-android-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "219d31de32c202d720bed52c486079cb2a866b855a97314578eaa434c702245d", + "input_bytes": 138, + "output_sha256": "797dd46faa2674b92b15b672866fc2eafc7fef0ffb81a30450577eb6fb0d29a8", + "output_bytes": 7740, + "exit": 0, + "elapsed_ms": 3163.593, + "receipt_sha256": "4ec5a1c4f18ac7789c8deb3879e8d1fc272fd6c8d7ae5393496bda19948c26a8" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-android-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "48637c9186665e28ecb9a1a03c1ae715ce0fb4901f6ee59f014a75be74dea61d", + "input_bytes": 138, + "output_sha256": "e2018837de3d3ae9baf4f3baba5790d3b24a375ce87849f600d2eb97da006183", + "output_bytes": 764, + "exit": 1, + "elapsed_ms": 2912.329, + "receipt_sha256": "ceba628a80785f0dcef360883fc35c98617e53a1c2e1c708ee95aa9e988361c3" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_7913ac380e2fd5067b8577354c9d09716d1437fd2f8dee0d344a83af2378104a", + "ev_d961e65ef320db021fa5a51e963edd8adf1160879a6326026d840c0a321dedf6" + ], + "bundle": { + "sha256": "fa499d0464a40ddc2f5159c893d68b8f50aa8e6d2b0080de22f2f3e5fc326825", + "bytes": 3046685, + "records": 3, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "graph", + "evidence_id": "ev_d9b65ab886e8c5358447b1e4649adfe0cdff7407ce20b99796d87a60761692c7", + "normalized_result_sha256": "80ed46f7a668d0f6f332ba27871538b389618a779f6d5bb5c3cd6889b8743643", + "actual_reply_sha256": "d3f5c85ec30c7f293a3107781cd1c882b1e3f491c6038ba9a4f1aef1a59ef156", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "complete-within-inventory", + "inventory_complete": true + }, + "summary": null, + "limitations_count": 7 + } + ], + "raw_outer_command_receipt_sha256": "ef1985c7d7c365de39fc290bedb0fec25ba572742294e1c24f2d904339f5f9c2", + "raw_resource_receipt_sha256": "ae762dd83ec01092dea2ef54d0b512b1bf3961f9fee1d53e55b4b64adf000570" + }, + { + "attempt": "apple-01", + "lane": "apple", + "artifact_id": "rectangle-zip", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "1d7e3c0241e81c6050e857bebea68a7e3012bced5a5d386cac390ae7eb2ff6da", + "scope_elapsed_seconds": 25.453, + "CPU_usec": 24824482, + "peak_process_family_bytes": 674127872, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 21, + "typed_error_results": 8, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 4, + "project_apple_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "compare_artifacts": 4 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 8, + "completed_command_receipts": 8, + "arms": [ + { + "arm": "artifacts-cli-file-missing-id", + "command": "compare-artifacts", + "syntax": "COMMAND INPUT_JSON --json; selected form uses {kind:evidence-file,path,evidence_id} on each side", + "input_sha256": "82c589f539b80a23f613562c6ddb07c11de891413c58641e49b31eb7163acb88", + "input_bytes": 517, + "output_sha256": "74eb37060ea4c89c6fbdc7eb8e730e3a3eb54f9164671505170e428664de7a7f", + "output_bytes": 1576, + "exit": 1, + "elapsed_ms": 2553.992, + "receipt_sha256": "ba46b8cc456cd2096b5966ba1d842449331bb4dc0f006f694e12c12deaf49b4b" + }, + { + "arm": "artifacts-cli-inline", + "command": "compare-artifacts", + "syntax": "COMMAND INPUT_JSON --json; selected form uses {kind:evidence-file,path,evidence_id} on each side", + "input_sha256": "30fa6ae443a63d0d222fb32ae34d257c16c336a3516c5ffc1673d3d211c3228c", + "input_bytes": 635331, + "output_sha256": "162b71db7b8266fc5c41fa207802b66d8bed5236c121bd484bbb617cf74ff9fb", + "output_bytes": 5448, + "exit": 0, + "elapsed_ms": 2579.646, + "receipt_sha256": "4595c882e3af131cca842a667ca71beb262b3394c5b69f6b50aa036d985efbe2" + }, + { + "arm": "artifacts-cli-missing-context", + "command": "compare-artifacts", + "syntax": "COMMAND INPUT_JSON --json; selected form uses {kind:evidence-file,path,evidence_id} on each side", + "input_sha256": "c4189ae706fac5876211cabfdb3a605b11e862cce0161c0d3385770ac5abfcfb", + "input_bytes": 243, + "output_sha256": "e8d9785d063b46a4338d978df8937bfb637c5225ae160d60dcdacb714313303f", + "output_bytes": 802, + "exit": 1, + "elapsed_ms": 1939.933, + "receipt_sha256": "f139e86091940e004a3d77cdc8ad96769f115a626cd1252f05eadb453d76c0af" + }, + { + "arm": "artifacts-cli-selected-file", + "command": "compare-artifacts", + "syntax": "COMMAND INPUT_JSON --json; selected form uses {kind:evidence-file,path,evidence_id} on each side", + "input_sha256": "f6bd17fb6614caf82d8935e997822e3177c60230952a364eecab72d35b581919", + "input_bytes": 517, + "output_sha256": "162b71db7b8266fc5c41fa207802b66d8bed5236c121bd484bbb617cf74ff9fb", + "output_bytes": 5448, + "exit": 0, + "elapsed_ms": 2749.454, + "receipt_sha256": "32a089b1f6466882b6d3c6d7f510739ef29006242427b8e35121205c4417c4c3" + }, + { + "arm": "graph-cli-inline", + "command": "project-apple-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "f4c9a9c51e2556cbc002b26e098cb9e381ad739a948a7c60e49cb2319a6cb60e", + "input_bytes": 317682, + "output_sha256": "4068af82691fc0e82f7ec32587c6124fa836c51f965073c80526c5a303b00b28", + "output_bytes": 28531, + "exit": 0, + "elapsed_ms": 2094.129, + "receipt_sha256": "4385c3863060cf8451811f33a9515d2774ea75a5a73da96cbe22038dd62051f8" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-apple-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "3fbfe6fd7d25d09e0befef6fb4e3d065f088c97b06e59b18928f4801a72c1256", + "input_bytes": 138, + "output_sha256": "b975e44c6e3f559b961d17059b5d7520f25c37dda62ea173e40da79a6f666c68", + "output_bytes": 856, + "exit": 1, + "elapsed_ms": 2223.693, + "receipt_sha256": "1ffa07fda2b850f333ce5d17e54be121fd735d51eaf87958abb169381e26f795" + }, + { + "arm": "graph-cli-reference", + "command": "project-apple-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "3fbfe6fd7d25d09e0befef6fb4e3d065f088c97b06e59b18928f4801a72c1256", + "input_bytes": 138, + "output_sha256": "4068af82691fc0e82f7ec32587c6124fa836c51f965073c80526c5a303b00b28", + "output_bytes": 28531, + "exit": 0, + "elapsed_ms": 2011.641, + "receipt_sha256": "cdec128f4d45f7544aee9b2f553932fa1bd95d9e300c73e9311c673664a78b9a" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-apple-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "d8afc8807230e4e91224f73c1626536b6fe6e4c6cb0c6843afa1b529aaed030d", + "input_bytes": 138, + "output_sha256": "96a649806c258037c1fd0712ba14d891eb3f4cf050dfb59f5b8b2aaf56951cab", + "output_bytes": 758, + "exit": 1, + "elapsed_ms": 2337.506, + "receipt_sha256": "f3192ede7516bb5417837e0f82f4db51481ba2781de0d68c2fed6f8698b0ae12" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_23ed0f86edeb2ae1c17345fbd28ccfafff290e944147c26ddf984410337918d0", + "ev_a1fc683933f6f7f17d33883645fb37ea6127b63eb2fcbc8a117711608dd82ad3" + ], + "bundle": { + "sha256": "8daa6286b28aadf5ba0d41038046805e5c69874eaa0bc5daf0e265360fcae79d", + "bytes": 642903, + "records": 3, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "graph", + "evidence_id": "ev_8c514288778171c79c59f2c999967619147e139c57ec71cd1ad195a651baa453", + "normalized_result_sha256": "39b597b93793e062094873a1370e107504265e733333902e4cd6185ee5de6c0a", + "actual_reply_sha256": "ee444460fc0d3df47f0b6b8948211acb1e3cc28934268777f0284bfb289cdd36", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "complete-within-inventory", + "inventory_complete": true + }, + "summary": null, + "limitations_count": 8 + }, + { + "name": "artifacts", + "evidence_id": "ev_e2ee78bfbcc74d5e234b8a65c1bb4580f62721f4630d8607349fcb36a4c4121d", + "normalized_result_sha256": "7e5eb19870f3fd51444274b407792456a98ebb7d6b4ca8affcf1215a21358068", + "actual_reply_sha256": "4ff066a1d56c8d3e0213366a3ef97d01aaa19ca8d1b36c7afbf5c1e0952c1db5", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_explicit_selected_file_equality": true + }, + "CLI_unavailable": null, + "status": "unknown", + "coverage": null, + "summary": null, + "limitations_count": 3 + } + ], + "raw_outer_command_receipt_sha256": "89681727d8b8584686b67b8d16af4159c3ac7e74ddcbabfb29cfda367897bcce", + "raw_resource_receipt_sha256": "463fb2d69281af932397cd2ff66938f416fc06dadcc5c070ea96d4862306b3c5" + }, + { + "attempt": "harmony-app-01", + "lane": "harmony-app", + "artifact_id": "shark-app", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "0ae5954705a398dee5fdd893cc3664b61c298a64895e135df32610ac00c6f40c", + "scope_elapsed_seconds": 16.714, + "CPU_usec": 15950327, + "peak_process_family_bytes": 519761920, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 17, + "typed_error_results": 4, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 4, + "project_harmony_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 4, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "graph-cli-inline", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "c42116e37822d50dd7be47a7bc0ee226d3358f26ed6b35a325ee245b1df4aba0", + "input_bytes": 5950, + "output_sha256": "0001af5a4a3c4d1c67aaf8cd20630ee9a13c87ea7f12137ea5406d797887bf88", + "output_bytes": 5158, + "exit": 0, + "elapsed_ms": 2994.299, + "receipt_sha256": "37ce21310221a64f779b421d87232fdd1f9b65c56ced22806ecb374362398b8d" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "e8bbcf4b076e26ed426cbb71ec1d0a6252913e563925b25124d71d318e61b208", + "input_bytes": 138, + "output_sha256": "b5ad4202c355090f013b7297320e2ade5a0508388e5f7aab442f59259b1093c4", + "output_bytes": 856, + "exit": 1, + "elapsed_ms": 2121.806, + "receipt_sha256": "12a5141ed001e37100551c8431c65031c0b8d6cbdf7130824592b002456a47b9" + }, + { + "arm": "graph-cli-reference", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "e8bbcf4b076e26ed426cbb71ec1d0a6252913e563925b25124d71d318e61b208", + "input_bytes": 138, + "output_sha256": "0001af5a4a3c4d1c67aaf8cd20630ee9a13c87ea7f12137ea5406d797887bf88", + "output_bytes": 5158, + "exit": 0, + "elapsed_ms": 2218.17, + "receipt_sha256": "1abeb505eb9802f39d9aca4d74d2dbe2767903f74949a781e7c73383535d96ca" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "ca6864f5bd7b60ef2de9d83dc3c3a0229b889866867a4b8fcf05a41d0233f40b", + "input_bytes": 138, + "output_sha256": "658dd5c9b3fe5c758c91180c36a2a98b9acec2885882be3e7c0025edfdf607b2", + "output_bytes": 764, + "exit": 1, + "elapsed_ms": 2274.238, + "receipt_sha256": "f3e1591a047cb3d028f1973acf4313e04e760eb6934b3b3b30a54382d2b4b99a" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_06fa019a6fca96d6e4e3195df0520562e249aa283902dd10a52fb58c7f3dde2a", + "ev_66602655d6e7fa7076146997ef1bee7fb0023b306275e37f2bb5e4f6da613499" + ], + "bundle": { + "sha256": "3a526ab0113f80ada26ff5a0631fc95dac91485946f20584fc5a76f5ee8eb880", + "bytes": 19523, + "records": 3, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "graph", + "evidence_id": "ev_391e76c5a80b32b0920479f0749eaef42878da22b02d44cde98f30f392d28c13", + "normalized_result_sha256": "0d6c7bab4fbafcbe8a1a0c9de509e3a5954fd9bb06d858db252fa2a66e7a34c0", + "actual_reply_sha256": "8aeae332ea8b88cb4886aa2436b56ce21ce05f810e8da68dd432fd5deae7c19a", + "MCP_inline": "complete-MCP-inline-parity", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "complete-within-inventory", + "inventory_complete": true + }, + "summary": null, + "limitations_count": 9 + } + ], + "raw_outer_command_receipt_sha256": "2ca28f534f32438171df20358ebc797d01f1dd6d4871f876db805f5973dc6c3e", + "raw_resource_receipt_sha256": "e0a8890df2a9ddb546ba51000371a0f36345f57440ec1bff01cb97fbe65b4d38" + }, + { + "attempt": "harmony-hap-01", + "lane": "harmony-hap", + "artifact_id": "shark-hap", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "1d7e3c0241e81c6050e857bebea68a7e3012bced5a5d386cac390ae7eb2ff6da", + "scope_elapsed_seconds": 16.294, + "CPU_usec": 15886932, + "peak_process_family_bytes": 533172224, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 17, + "typed_error_results": 5, + "by_tool": { + "open_binary": 1, + "inspect_artifact": 1, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 4, + "project_harmony_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 4, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "graph-cli-inline", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "dd5bc96a694adc714bc7ed6bae6d62bdb90b764c547393fb2a1432289ed7026f", + "input_bytes": 64475, + "output_sha256": "e76a813817fbf7248228dcac7a107d2b5e1e1896d41b292b074bf49e21a6b913", + "output_bytes": 16063, + "exit": 0, + "elapsed_ms": 2579.432, + "receipt_sha256": "567fed1e53e80853673ee1694c49132c94c13b24cae46f645776af89318c58d6" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "e2519bec92adfbb92ade41ad60f1b1efdea0d6c03e45f57eab3e94b27741b956", + "input_bytes": 138, + "output_sha256": "c1b96a2a087c2ca13d705caae9c4266d87fe1f4cfdf6e661dc03cca36438596d", + "output_bytes": 856, + "exit": 1, + "elapsed_ms": 2175.388, + "receipt_sha256": "df8feb31876030d29cc6240ac360027b1526b7692010893ae9331a6e9c236320" + }, + { + "arm": "graph-cli-reference", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "e2519bec92adfbb92ade41ad60f1b1efdea0d6c03e45f57eab3e94b27741b956", + "input_bytes": 138, + "output_sha256": "e76a813817fbf7248228dcac7a107d2b5e1e1896d41b292b074bf49e21a6b913", + "output_bytes": 16063, + "exit": 0, + "elapsed_ms": 2587.581, + "receipt_sha256": "05f9ed3602c69ef545c9353eeff652486a651f75bff890e8ec58c72071419561" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-harmony-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "09df54af157e1ccfb1f9a2beaa5c755221dac267a9e0bb0b877b3ecbff2792b3", + "input_bytes": 138, + "output_sha256": "658dd5c9b3fe5c758c91180c36a2a98b9acec2885882be3e7c0025edfdf607b2", + "output_bytes": 764, + "exit": 1, + "elapsed_ms": 2528.308, + "receipt_sha256": "4409c3f3f6d7b0df5ab542221193c2b0d426ea364a4d0eb0a1432e7641bd8d18" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_344e1e7628c1ff2121b1fdebdbfc14e63c12b7f8c61042ae617a83c5354c38ee", + "ev_8e81e991aee54cdbe3396d4174d0119e94b79240b4a55e2e70b2f94fdb2d9d35" + ], + "bundle": { + "sha256": "3431c0a347f331c3effacf7984ecb890f3fef46d8b00b03b50980023b065cd4a", + "bytes": 136549, + "records": 3, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "graph", + "evidence_id": "ev_88c25f50ab0735f809e7447aa729e9ce2be64b84048b56d24c680becf2cb28e2", + "normalized_result_sha256": "f4518c9d3284031e4adea3d4138789d5d78d0af2b1d10dab1ee0de2b70d15a02", + "actual_reply_sha256": "f779587fc7ca72340a3db3a131d307f3c371e836a64bbe3348193f66d24cf3c1", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "complete-within-inventory", + "inventory_complete": true + }, + "summary": null, + "limitations_count": 7 + } + ], + "raw_outer_command_receipt_sha256": "1d498421a67197c2f5fa91db994ed4559ef1e9f71df003677cfd63cef13a9a8e", + "raw_resource_receipt_sha256": "8b2a6419012df37de3a172a47d2c544c0f70dcd2d57b81c899642807f36e9501" + }, + { + "attempt": "javascript-01", + "lane": "javascript", + "artifact_id": "agent-browser-entry", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "0d1d616bc8581822c84468c0855b940675ff037b162a4219198ce339f846870f", + "scope_elapsed_seconds": 6.65, + "CPU_usec": 6194774, + "peak_process_family_bytes": 466661376, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 1, + "MCP_calls": { + "completed": 4, + "typed_error_results": 0, + "by_tool": { + "analyze_javascript_application": 1, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 1 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 0, + "completed_command_receipts": 0, + "arms": [], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_73f59e34d58539cffa3fef9a28fe444b734fa50549dc5d410cad7d7163daa521" + ], + "bundle": { + "sha256": "66232b6eb01b3dbf435949a1cd94cb2532b7f989b91cdf952a105b3cee231bda", + "bytes": 616150, + "records": 2, + "unchanged": null + }, + "outcome": "harness-source-identity-assumption", + "failure_note": "Selector assumed canonical-path; actual typed source-module observation uses artifact-local-key. Genuine producer/export retained and reused.", + "producer_closed_before_consumer": null, + "zero_consumer_producer_calls": true, + "results": [], + "raw_outer_command_receipt_sha256": "d8853634589c8be65bc597fe562fe5a9d2d891b9542641d5fd0820d64dee70ed", + "raw_resource_receipt_sha256": "5285092a2ff4c43558974f35eb7fac1daf67c605560353bb727f827b25d2bd82" + }, + { + "attempt": "javascript-02", + "lane": "javascript", + "artifact_id": "agent-browser-entry", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "7d104443d70fe3c020977502489ef3447df3d89412482f0aba8b30400ece78a9", + "scope_elapsed_seconds": 3.941, + "CPU_usec": 3705443, + "peak_process_family_bytes": 373411840, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 1, + "MCP_calls": { + "completed": 14, + "typed_error_results": 8, + "by_tool": { + "binary_session": 2, + "trace_application_feature": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "trace_javascript_semantics": 3 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 0, + "completed_command_receipts": 0, + "arms": [], + "unrecorded_commands": [] + }, + "source_evidence_ids": [], + "bundle": { + "sha256": "66232b6eb01b3dbf435949a1cd94cb2532b7f989b91cdf952a105b3cee231bda", + "bytes": 616150, + "records": 2, + "unchanged": null + }, + "outcome": "harness-error-projection-assumption", + "failure_note": "Default1024 refusal worked; harness expected max_input_elements at wrong nesting. Same original export reused.", + "producer_closed_before_consumer": null, + "zero_consumer_producer_calls": true, + "results": [], + "raw_outer_command_receipt_sha256": "229c1a76f7957b6785989ac1237ec68fb266048542143c37c8d977e1949ee2b7", + "raw_resource_receipt_sha256": "acdc47ad619d6d8e6a68ac7bbc9995b16c0b64006d2d664861fb9fcf198fad2a" + }, + { + "attempt": "javascript-03", + "lane": "javascript", + "artifact_id": "agent-browser-entry", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "81858a348907cdaceb250228dc0a2423d05c0d51ecee824241cd1ad2c80b0f64", + "scope_elapsed_seconds": 23.733, + "CPU_usec": 23194701, + "peak_process_family_bytes": 690913280, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 17, + "typed_error_results": 9, + "by_tool": { + "binary_session": 3, + "trace_application_feature": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "trace_javascript_semantics": 5 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 8, + "completed_command_receipts": 8, + "arms": [ + { + "arm": "feature-cli-inline", + "command": "trace-application-feature", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "84b1cb83b75423f104c69f76d3e4eb061e7f9d703b7edc072126144314316fdf", + "input_bytes": 604529, + "output_sha256": "7372742b055a89224be01fb5cc9a663811726d5f74d87bfd85211319081ae4bf", + "output_bytes": 64076, + "exit": 0, + "elapsed_ms": 2702.281, + "receipt_sha256": "115c2cbbfc02d4bbe445abf6389c05495a40d4ea1b8b72031db9193bf966c261" + }, + { + "arm": "feature-cli-missing-context", + "command": "trace-application-feature", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "f534edd7d43a5504dffc2796d6074c2d492c261c7d4d66bcc58686d15e114230", + "input_bytes": 262, + "output_sha256": "77e86f8d90340f9fc4ad8e9774e9a846af591e97ca48a4367754d83effe3dde9", + "output_bytes": 847, + "exit": 1, + "elapsed_ms": 2261.674, + "receipt_sha256": "b3326d0ef51ead158592e361049ae73659a3f8aabb44e3e7a80c1c974662df15" + }, + { + "arm": "feature-cli-reference", + "command": "trace-application-feature", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "f534edd7d43a5504dffc2796d6074c2d492c261c7d4d66bcc58686d15e114230", + "input_bytes": 262, + "output_sha256": "7372742b055a89224be01fb5cc9a663811726d5f74d87bfd85211319081ae4bf", + "output_bytes": 64076, + "exit": 0, + "elapsed_ms": 2243.416, + "receipt_sha256": "0141b214be616f9a7a388508ecd4331468a8c1f9ca55e8ceb0e04a5844bdc115" + }, + { + "arm": "feature-cli-wrong-source-role", + "command": "trace-application-feature", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "0d3f17857e407528caab3e9790b1028688ea106e7e704e604da76ea6723b0390", + "input_bytes": 262, + "output_sha256": "5c498487a3b20ae997feab560e646bc434cd2f999e1a5e77e16a00eb0d0d64c1", + "output_bytes": 748, + "exit": 1, + "elapsed_ms": 2692.942, + "receipt_sha256": "b290decf1e064e979e9cec97aa42fe6c069524534f6f4e534c3276e9483bebd9" + }, + { + "arm": "semantic-cli-inline", + "command": "trace-javascript-semantics", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "7c922341dda3289d225e52d914a0e45f53160f41d69bcbc389741fa16ebb497a", + "input_bytes": 604549, + "output_sha256": "c3db176dfa0277de95ba756b1198bae9c7693c982776770b1528637bf5dadb61", + "output_bytes": 6091, + "exit": 0, + "elapsed_ms": 2765.772, + "receipt_sha256": "a511a6ecf36b95b6218ecf4ea5897ba828d37a95fffe4fbcfd73a62e3be70b93" + }, + { + "arm": "semantic-cli-missing-context", + "command": "trace-javascript-semantics", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "770d316254a469240f48f037bee16666c3f702177b447eaf0ad3718827b18f43", + "input_bytes": 282, + "output_sha256": "77e86f8d90340f9fc4ad8e9774e9a846af591e97ca48a4367754d83effe3dde9", + "output_bytes": 847, + "exit": 1, + "elapsed_ms": 2123.255, + "receipt_sha256": "cd8dac85432d56245aa615d50c0354da7646a8ceb071b5a83fbb2d6845b9b93b" + }, + { + "arm": "semantic-cli-reference", + "command": "trace-javascript-semantics", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "770d316254a469240f48f037bee16666c3f702177b447eaf0ad3718827b18f43", + "input_bytes": 282, + "output_sha256": "c3db176dfa0277de95ba756b1198bae9c7693c982776770b1528637bf5dadb61", + "output_bytes": 6091, + "exit": 0, + "elapsed_ms": 2386.479, + "receipt_sha256": "3db2e9112e77f646e95ff34e8d69e48f9e9f600ba69ee6720f165cf38188b3fd" + }, + { + "arm": "semantic-cli-wrong-source-role", + "command": "trace-javascript-semantics", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "5f147be4934dc2a0c740322a18fd114aea8acb53534c5aca2e1adbdc6140cb8e", + "input_bytes": 282, + "output_sha256": "bfc86b7bff3835601f3b242b2253c8dda6ee2250bb9e8356dab50563bedca0d6", + "output_bytes": 751, + "exit": 1, + "elapsed_ms": 2354.597, + "receipt_sha256": "f2a104e2b3c456e57fbeb4893a24b714ba38c6f31e500ec1191596ff5cd5c2a8" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_73f59e34d58539cffa3fef9a28fe444b734fa50549dc5d410cad7d7163daa521" + ], + "bundle": { + "sha256": "66232b6eb01b3dbf435949a1cd94cb2532b7f989b91cdf952a105b3cee231bda", + "bytes": 616150, + "records": 2, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "feature", + "evidence_id": "ev_22ba6c0041089032045b3c16732fd1c08750dad1675fdb5a8be69cb3e741ce09", + "normalized_result_sha256": "c9f8d15cbbe36237cab96b00a9fb1cafeb24b211636eb41efc5dc477a401a234", + "actual_reply_sha256": "cb873c1c266b51dc70056b075bb81e68fc488e9d688b1895ad8f3b642a655fb6", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "partial", + "source_graph_status": "partial", + "total_seed_matches": 1 + }, + "summary": null, + "limitations_count": 5 + }, + { + "name": "semantic", + "evidence_id": "ev_53d5b19ce0cbdacf37910631e16a848549866cbb4de46b0fcb3b4785c86337ab", + "normalized_result_sha256": "b250fcc8a6de2f9037ccfe094cb1b4de498b34d311e6a6a73f9797d38693f83a", + "actual_reply_sha256": "9ab5687f1155f6e463d58240175e122a954907effe81a2032ca1d57a00b51e14", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": "found", + "coverage": { + "status": "partial" + }, + "summary": null, + "limitations_count": 13 + } + ], + "raw_outer_command_receipt_sha256": "34b0d6de4a46f1e8826f3aee704a2cb5d92e7136d4b63e0c5e0be1659ab34f0f", + "raw_resource_receipt_sha256": "f5ac33652f000cacc8545f89b8dd83858339091229639cd28fb5e14f1fff3ae2" + }, + { + "attempt": "managed-01", + "lane": "managed", + "artifact_id": "nuget-cli", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "81858a348907cdaceb250228dc0a2423d05c0d51ecee824241cd1ad2c80b0f64", + "scope_elapsed_seconds": 21.327, + "CPU_usec": 20802841, + "peak_process_family_bytes": 716390400, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 1, + "MCP_calls": { + "completed": 23, + "typed_error_results": 10, + "by_tool": { + "inspect_managed_artifact": 1, + "inspect_managed_members": 2, + "export_evidence_bundle": 1, + "close_binary": 1, + "binary_session": 3, + "project_managed_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "compare_managed_members": 6 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 5, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "graph-cli-inline", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "3cfd14f8e321f4117a324bcc2beaafccc8b0234463b0913eb9c26424f5c38101", + "input_bytes": 1247672, + "output_sha256": "c412c979610e6f5df439a76a309ac6d6b51d2014b98580a2c124ff6012a921c5", + "output_bytes": 44485, + "exit": 0, + "elapsed_ms": 3004.226, + "receipt_sha256": "1bac1fd3401ee87b509a5fc790b052be945b991034ac9271d59e6455538ee98b" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "c0fc740ebec48f5fb4eaff702b9206c0c9f3e7ebf75e504732e9911df1fe96f3", + "input_bytes": 265, + "output_sha256": "add714c3bb79cbd6dff943bf806a89bacd07d613abf91d8096b0ba2986395fe9", + "output_bytes": 814, + "exit": 1, + "elapsed_ms": 2110.97, + "receipt_sha256": "d81cfa839c9bcb5497dec3af26b3d6ac327784a56ffedbe4de37a7dcb5d93f34" + }, + { + "arm": "graph-cli-reference", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "c0fc740ebec48f5fb4eaff702b9206c0c9f3e7ebf75e504732e9911df1fe96f3", + "input_bytes": 265, + "output_sha256": "c412c979610e6f5df439a76a309ac6d6b51d2014b98580a2c124ff6012a921c5", + "output_bytes": 44485, + "exit": 0, + "elapsed_ms": 3416.484, + "receipt_sha256": "dbfeac00e7de8583d14ff494733defc5f1eb3fe39ffd96c31dbafddcc02163ee" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "a0873801f349c88cb940e0537bb2281f23c2ea7fee78f7f6feba2e695e28b2ab", + "input_bytes": 265, + "output_sha256": "dd55289cf9b2be1b64c4d6d502af7f6cdf932fac41590ba1894e9f090cc1e043", + "output_bytes": 770, + "exit": 1, + "elapsed_ms": 2475.263, + "receipt_sha256": "d9f7b180021bb1bf2043c2691105036e98be3d5ae437703063dfa642dbe49388" + } + ], + "unrecorded_commands": [ + { + "arm": "members-cli-inline.output.json", + "output_sha256": "b361938a31f4e436768b9707d5aa1fdbac16946432b9ed89a9c986434d0f7417", + "observed_error_code": "VALIDATION_ERROR", + "exit": "not retained in command receipt; harness aborted before writing it" + } + ] + }, + "source_evidence_ids": [ + "ev_66da1e751fda71ecd0a6cb30ccd6c831e3953553c89740c8c170bff8bc8cc389", + "ev_a0618cd67a3dff3b2d05153f2781ed91d01e551d08aa7e40b590ac97ee6b9ef8", + "ev_c2ccb747eee32a8fee31c4a48d8e994b12b613e4603b6e0a130799fa782f7777" + ], + "bundle": { + "sha256": "11ac63842299c0ac641a320ec9b2a5161b454a8a322d57724fd5f2ffb2fddfa4", + "bytes": 2114329, + "records": 6, + "unchanged": null + }, + "outcome": "harness-unsupported-CLI-grammar", + "failure_note": "Member comparison CLI accepts two PE paths, not selected Evidence JSON. Existing graph CLI parity passed; same original export reused.", + "producer_closed_before_consumer": null, + "zero_consumer_producer_calls": true, + "results": [], + "raw_outer_command_receipt_sha256": "f6a1d1dc5939a7208ef793e82c1fec0eea60ac626fc76ad1acd6f5ff9c92e03f", + "raw_resource_receipt_sha256": "2816f07e1bc0268f6e2a9a8c82ea594311bd2ad90595affed9a62758ef3b4fa7" + }, + { + "attempt": "managed-02", + "lane": "managed", + "artifact_id": "nuget-cli", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "4ba5d793e40cbf539abf794c2b44b6d7edb5a36e998123fc11922d1f00c567af", + "scope_elapsed_seconds": 16.014, + "CPU_usec": 15418853, + "peak_process_family_bytes": 728391680, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 18, + "typed_error_results": 10, + "by_tool": { + "binary_session": 3, + "project_managed_application_graph": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "compare_managed_members": 6 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 4, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "graph-cli-inline", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "3cfd14f8e321f4117a324bcc2beaafccc8b0234463b0913eb9c26424f5c38101", + "input_bytes": 1247672, + "output_sha256": "c412c979610e6f5df439a76a309ac6d6b51d2014b98580a2c124ff6012a921c5", + "output_bytes": 44485, + "exit": 0, + "elapsed_ms": 3038.9, + "receipt_sha256": "999912534ebec93fcc649ed51963e4e97c4703568f857a8d58491362c2fefd11" + }, + { + "arm": "graph-cli-missing-context", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "c0fc740ebec48f5fb4eaff702b9206c0c9f3e7ebf75e504732e9911df1fe96f3", + "input_bytes": 265, + "output_sha256": "add714c3bb79cbd6dff943bf806a89bacd07d613abf91d8096b0ba2986395fe9", + "output_bytes": 814, + "exit": 1, + "elapsed_ms": 2462.415, + "receipt_sha256": "de50b5eae428084bea4b28d9c4674d910a83065d177a9cbe5e6ef6a54fc1f47a" + }, + { + "arm": "graph-cli-reference", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "c0fc740ebec48f5fb4eaff702b9206c0c9f3e7ebf75e504732e9911df1fe96f3", + "input_bytes": 265, + "output_sha256": "c412c979610e6f5df439a76a309ac6d6b51d2014b98580a2c124ff6012a921c5", + "output_bytes": 44485, + "exit": 0, + "elapsed_ms": 3175.737, + "receipt_sha256": "88e87f6a4f7aee99c499fbc267d8c47ff29be183cea36a579dbd45223cb5a689" + }, + { + "arm": "graph-cli-wrong-source-role", + "command": "project-managed-application-graph", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "a0873801f349c88cb940e0537bb2281f23c2ea7fee78f7f6feba2e695e28b2ab", + "input_bytes": 265, + "output_sha256": "dd55289cf9b2be1b64c4d6d502af7f6cdf932fac41590ba1894e9f090cc1e043", + "output_bytes": 770, + "exit": 1, + "elapsed_ms": 2749.849, + "receipt_sha256": "20c92b5f898a5fcaea79c68d183c43bebd28aa57dc80ce621d10ee3b6a28469f" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_66da1e751fda71ecd0a6cb30ccd6c831e3953553c89740c8c170bff8bc8cc389", + "ev_a0618cd67a3dff3b2d05153f2781ed91d01e551d08aa7e40b590ac97ee6b9ef8", + "ev_c2ccb747eee32a8fee31c4a48d8e994b12b613e4603b6e0a130799fa782f7777" + ], + "bundle": { + "sha256": "11ac63842299c0ac641a320ec9b2a5161b454a8a322d57724fd5f2ffb2fddfa4", + "bytes": 2114329, + "records": 6, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "graph", + "evidence_id": "ev_408bb551fed7ac6e63940ed2e2e83a8404c8f75af327a181098cac2efb9b1107", + "normalized_result_sha256": "c2d36028b7564c91e29e5631a7d675e62ff4788bdc909023145b6a74417ae488", + "actual_reply_sha256": "765e882262da30103ac9027e5ab3b87f1cfb092d7016296fae843d0708aa445a", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": { + "status": "partial" + }, + "summary": null, + "limitations_count": 5 + }, + { + "name": "members", + "evidence_id": "ev_26bff8c0c7a2cc5a13209c40ef0d640305b6e221c05d2b41b6bf0d8d0b1224da", + "normalized_result_sha256": "1e31e162983f06b9e715e4fa58a414d996e5b01fd1b4e170417578fc630c1022", + "actual_reply_sha256": "ea84b71f2b646ac38d2c5b86f68af358b236b7adcc370bdc39a309f95dc51f89", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": null, + "CLI_unavailable": "Existing CLI accepts two artifact paths and runs producer inspections; selected Evidence/--evidence-bundle inputs are not supported for this command.", + "status": null, + "coverage": { + "status": "partial", + "left_status": "complete", + "right_status": "complete" + }, + "summary": null, + "limitations_count": 8 + } + ], + "raw_outer_command_receipt_sha256": "5bc74b251e4be374101ae9807191e6ae270ca0884efb5c84909067e40b492653", + "raw_resource_receipt_sha256": "0460877a645a54f8bdffb15a5c5ef3b4b57cf8089efd5a353e65f85784287264" + }, + { + "attempt": "native-01", + "lane": "native", + "artifact_id": "jq", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "81858a348907cdaceb250228dc0a2423d05c0d51ecee824241cd1ad2c80b0f64", + "scope_elapsed_seconds": 35.915, + "CPU_usec": 34674174, + "peak_process_family_bytes": 973070336, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 31, + "typed_error_results": 13, + "by_tool": { + "open_binary": 1, + "list_procedures": 1, + "procedure_callees": 1, + "analyze_function": 2, + "export_evidence_bundle": 2, + "close_binary": 1, + "binary_session": 4, + "build_call_path": 6, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2, + "compare_functions": 5, + "match_native_functions": 5 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 0, + "completed_command_receipts": 0, + "arms": [], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_681e6e6b603413f6dc0eeff8ca31acb3875d1d72d97b5e2de837460c2c03b0f9", + "ev_6a73ba03158b02af19dc76fe1865647b1dbeeaff7e7c4eed20a5f8fc58caff59", + "ev_df9c037f6b8a7c14d87b43162eac21246fa6a47f739361f653f459ec094d2df6" + ], + "bundle": { + "sha256": "0a3adad6ab39bfe29320e1f070a8e598382d2f17c4d6c19079bd6ff16dfe0937", + "bytes": 3097994, + "records": 8, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "path", + "evidence_id": "ev_b3a67463b568bd4dd5f338a995014b937dc3572a8e8557f72433a86dd98f44be", + "normalized_result_sha256": "32aeea7af5a251e6140ab169ebf860dec796c92b5cb369dde7388d06674a236b", + "actual_reply_sha256": "314398a30dc2fc69a5b0c48ed02e1c3196f61c58947349dbbe20412292f2925b", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": null, + "CLI_unavailable": null, + "status": "found", + "coverage": null, + "summary": null, + "limitations_count": 77 + }, + { + "name": "functions", + "evidence_id": "ev_a287514fc988745a27e6b1a52bfe3833bafdaebc956aace351b420ef36982259", + "normalized_result_sha256": "435b6811e6cebcdc7fbf759063c80f2a1af1230cb3e02d18129d594f91cf04e3", + "actual_reply_sha256": "a038a9f6ab1a5dd595298652ecccaed563b9ba42d7f7e5501a6613d5f0d9138c", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": null, + "CLI_unavailable": null, + "status": "changed", + "coverage": null, + "summary": null, + "limitations_count": 28 + }, + { + "name": "matching", + "evidence_id": "ev_3aea60bbe5bcea71c2454fa1f560bc7a3a3df9534125471322ae5c6302cf6737", + "normalized_result_sha256": "73ec328c668981d0fbbfd14bccd0f6e1d2f11bd8eda308358334dccd51771af4", + "actual_reply_sha256": "7ba60ae9e42775d116d57f92e4a47bd2e88ce34e4b96c86eb62e9173a9fa58c8", + "MCP_inline": "default1024-element-refusal", + "CLI_parity": null, + "CLI_unavailable": null, + "status": null, + "coverage": null, + "summary": null, + "limitations_count": 14 + } + ], + "raw_outer_command_receipt_sha256": "e70f8eed2d35ba08a86be636fbbd0b3b5870ec063947919be20745a9800577f5", + "raw_resource_receipt_sha256": "be59c7f6ce5af700d05dde44d2aafa5d1303a41d876e49cbe901a49898d5fd39" + }, + { + "attempt": "process-01", + "lane": "process", + "artifact_id": "process-capture-bundle", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "1d7e3c0241e81c6050e857bebea68a7e3012bced5a5d386cac390ae7eb2ff6da", + "scope_elapsed_seconds": 9.056, + "CPU_usec": 8586552, + "peak_process_family_bytes": 495534080, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 20, + "typed_error_results": 7, + "by_tool": { + "binary_session": 3, + "compare_process_captures": 6, + "import_evidence_bundle": 2, + "get_evidence_bundle": 2, + "find_changed_behavior": 4, + "correlate_static_and_runtime": 3 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 2, + "completed_command_receipts": 2, + "arms": [ + { + "arm": "captures-cli-inline", + "command": "compare-process-captures", + "syntax": "COMMAND LEFT_COMPLETE_EVIDENCE_JSON RIGHT_COMPLETE_EVIDENCE_JSON --json", + "input_sha256": "ac7fbce0dd24ce96fbb1cfbf7e8d07c826dca08c318dc0429822dab920814060", + "input_bytes": 14406, + "output_sha256": "12572c553d2a473500fddc3caa01d7d5bbbaee11ab1431aef37a8a86321d0802", + "output_bytes": 5250, + "exit": 0, + "elapsed_ms": 2334.864, + "receipt_sha256": "8b34f92bde135cbcfc6cfaded0319594dc0dcbdd36b57e7daf813139583bfa0a" + }, + { + "arm": "captures-cli-wrong-source-role", + "command": "compare-process-captures", + "syntax": "COMMAND LEFT_COMPLETE_EVIDENCE_JSON RIGHT_COMPLETE_EVIDENCE_JSON --json", + "input_sha256": "20d12d3fc7e1bc9680c7ed5159eacb322a277a91482023a0153acbb3ffbcd436", + "input_bytes": 12460, + "output_sha256": "355800523fc017a499fdf61fa4a9d83d3a4f69efd8d2e19edbd27f68f94ceb02", + "output_bytes": 776, + "exit": 1, + "elapsed_ms": 2289.876, + "receipt_sha256": "1bb4901bb336ab1fcda0f482f3796c6f04905f4682cd5cba63f0cf6e58f3cc8d" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_33bb6e0546183b842d986bbf422e3d1f7281030aee543689d25e2efe2baebd28", + "ev_681e6e6b603413f6dc0eeff8ca31acb3875d1d72d97b5e2de837460c2c03b0f9", + "ev_892da4f0557c355c1127634b9c69780d2e9f7dc64ddc75e39093dba382e4b525", + "ev_a287514fc988745a27e6b1a52bfe3833bafdaebc956aace351b420ef36982259", + "ev_d9360c023f36af83abe76b9ee84625588d25db7453d89d449a22b8f357220969", + "ev_df9c037f6b8a7c14d87b43162eac21246fa6a47f739361f653f459ec094d2df6" + ], + "bundle": { + "sha256": "55da0f130dc2265aae54261d49949471e5c929163c2b7314b42e95df3325b739", + "bytes": 15199, + "records": 2, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "behavior", + "evidence_id": "ev_5396afe64e4b21c11f64b730c6d605b42cffd2e072d0c272ed0db559456a84f2", + "normalized_result_sha256": null, + "actual_reply_sha256": "866c6ffad7c3fec8db4d363196a36b19f83a7ee919398d04689e58ce0dde7300", + "MCP_inline": "complete-MCP-inline-parity", + "CLI_parity": null, + "CLI_unavailable": null, + "status": "observed_changed", + "coverage": null, + "summary": null, + "limitations_count": 10 + }, + { + "name": "optional-correlation", + "evidence_id": "ev_68dab303764a8a1351578295bb5c94b720405b66ec815581958bb2153fb8a650", + "normalized_result_sha256": null, + "actual_reply_sha256": "179930cb771c35c4360109c14d966211fdc4dbdf65e18e7489c472429e46c8d7", + "MCP_inline": "complete-MCP-inline-parity", + "CLI_parity": null, + "CLI_unavailable": null, + "status": "unknown", + "coverage": null, + "summary": { + "hypotheses": 0, + "contradictions": 0, + "unresolved": 1 + }, + "limitations_count": 3 + }, + { + "name": "captures", + "evidence_id": "ev_d9360c023f36af83abe76b9ee84625588d25db7453d89d449a22b8f357220969", + "normalized_result_sha256": "14faf03ada1d5ea827af5b797d6b7d7b94ff3fdd5e38c0c949e9f8d2269e00b0", + "actual_reply_sha256": "fc8387c0c42082380389452c1fa71e2221f8e9552750537cb461261aa8ca1d38", + "MCP_inline": "complete-MCP-inline-parity", + "CLI_parity": { + "complete_MCP_reference_CLI_Evidence_files_equality": true + }, + "CLI_unavailable": "CLI accepts complete Evidence file pairs; no selected-bundle reference option exists.", + "status": "changed", + "coverage": null, + "summary": null, + "limitations_count": 21 + } + ], + "raw_outer_command_receipt_sha256": "244eac3c5b6ed22064528de401ad7545a53220026a87a452061041227342cec7", + "raw_resource_receipt_sha256": "14966dbd9dc6d42589de68565d9a569028af3f06772397e180543b3f301e77f6" + }, + { + "attempt": "wasm-01", + "lane": "wasm", + "artifact_id": "css-wasm", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "1d7e3c0241e81c6050e857bebea68a7e3012bced5a5d386cac390ae7eb2ff6da", + "scope_elapsed_seconds": 6.353, + "CPU_usec": 6166135, + "peak_process_family_bytes": 741670912, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 1, + "MCP_calls": { + "completed": 10, + "typed_error_results": 4, + "by_tool": { + "binary_session": 2, + "compare_wasm_interfaces": 5, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2 + }, + "transport_interrupted_attempts": 1 + }, + "CLI_arms": { + "output_files": 0, + "completed_command_receipts": 0, + "arms": [], + "unrecorded_commands": [] + }, + "source_evidence_ids": [], + "bundle": { + "sha256": "acb0cb6c3e0d520f83144e106e171c3ecebba70c5c0498bbb0113185a3e4bf95", + "bytes": 13925066, + "records": 4, + "unchanged": null + }, + "outcome": "observed-default-SDK-frame-capacity", + "failure_note": "Actual27823016-byte inline arguments exceeded default10485760-byte SDKstdio buffer; connection closed before1024-element admission. Ref comparison had succeeded. Same authentic bundle reused; no limits raised.", + "producer_closed_before_consumer": null, + "zero_consumer_producer_calls": true, + "results": [], + "raw_outer_command_receipt_sha256": "d2f9329e9a4cc58322255702b212f00a7d6d6ad71d8d82477f30e368eb687bd4", + "raw_resource_receipt_sha256": "f6525fe80be106b72ea9f8898c09ced3e7a9e7fca8181e6aa8e993d7e375609e" + }, + { + "attempt": "wasm-02", + "lane": "wasm", + "artifact_id": "css-wasm", + "source_head": "7e10c1cc9d2ff8371e5cf9ba3f77c834f644e759", + "source_dirty": true, + "built_runtime_sha256": "63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021", + "harness_sha256": "5df9e8da8ca635fc8f3938573f5148f6b87f45f5c14490218bd5a65b0b083983", + "scope_elapsed_seconds": 16.295, + "CPU_usec": 15861514, + "peak_process_family_bytes": 830128128, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "OOM": 0, + "OOM_kill": 0, + "resource_abort": null, + "payload_exit_code": 0, + "MCP_calls": { + "completed": 11, + "typed_error_results": 4, + "by_tool": { + "binary_session": 3, + "compare_wasm_interfaces": 5, + "import_evidence_bundle": 1, + "get_evidence_bundle": 2 + }, + "transport_interrupted_attempts": 0 + }, + "CLI_arms": { + "output_files": 4, + "completed_command_receipts": 4, + "arms": [ + { + "arm": "exports-cli-inline", + "command": "compare-wasm-interfaces", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "2b4dd215ca79a454cf78ab6fb35ac05cdbdbb5e02b46198bbf4970916b4f227e", + "input_bytes": 27823017, + "output_sha256": "9d2dd8cdc7f72d863b30662b05fc73960d20c49cb1472f67f77c3173063e1f97", + "output_bytes": 3732, + "exit": 0, + "elapsed_ms": 3706.572, + "receipt_sha256": "c84df677833aa2de16609d0a83c36d26ab6f13bc8eec6e9f8babaf99dfd117cc" + }, + { + "arm": "exports-cli-missing-context", + "command": "compare-wasm-interfaces", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "7e5056031244e1c7dec5f417a06634e348d989139a134e6911acd1b5b8bd65f7", + "input_bytes": 261, + "output_sha256": "986c9d251501a7b72905d1dd14a2210f82c27a7305132d708b0e4010be9ca806", + "output_bytes": 802, + "exit": 1, + "elapsed_ms": 1846.982, + "receipt_sha256": "6929ca32cf1802a9d6e19c2ca073753e144d0ab21d1ac4400656b36eb8f65165" + }, + { + "arm": "exports-cli-reference", + "command": "compare-wasm-interfaces", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "7e5056031244e1c7dec5f417a06634e348d989139a134e6911acd1b5b8bd65f7", + "input_bytes": 261, + "output_sha256": "9d2dd8cdc7f72d863b30662b05fc73960d20c49cb1472f67f77c3173063e1f97", + "output_bytes": 3732, + "exit": 0, + "elapsed_ms": 3027.952, + "receipt_sha256": "06c38f565b9de677b98ca1e4c2f8e0a27138f006c2540471983d6134b907c0d6" + }, + { + "arm": "exports-cli-wrong-source-role", + "command": "compare-wasm-interfaces", + "syntax": "COMMAND INPUT_JSON --json [--evidence-bundle BUNDLE]", + "input_sha256": "739fa460ed693c898031481b06ca68830b6b68487a35c95dd0361808177f5861", + "input_bytes": 261, + "output_sha256": "4a82841234259d8b034de25facaccf01ee3314be4cbf50c1520f93cfbcc83000", + "output_bytes": 621, + "exit": 1, + "elapsed_ms": 2621.199, + "receipt_sha256": "35efd0fcd7a55670845001941b38fb67b3c43c867c15ea053a3f11c2fcb555c2" + } + ], + "unrecorded_commands": [] + }, + "source_evidence_ids": [ + "ev_27ff1c46b138fd9809ab403aa2db919b440b0d822006cdc18a44e651f2508f43" + ], + "bundle": { + "sha256": "acb0cb6c3e0d520f83144e106e171c3ecebba70c5c0498bbb0113185a3e4bf95", + "bytes": 13925066, + "records": 4, + "unchanged": true + }, + "outcome": "passed-supported-contracts", + "failure_note": null, + "producer_closed_before_consumer": true, + "zero_consumer_producer_calls": true, + "results": [ + { + "name": "exports", + "evidence_id": "ev_b32346965c153a067abff9698aeba1ccbace34c78ac2071ac18311250b68c6da", + "normalized_result_sha256": "3ac2132f1693e92833495e49ec793a40e131718c8eee9db80842a24e0c9641fd", + "actual_reply_sha256": "f5394ca19e4615babb43a7091ba53932fc5a88dfea7b6b015c4861d5caa2f435", + "MCP_inline": "unavailable-default10MiB-stdio-frame-capacity", + "CLI_parity": { + "complete_MCP_reference_CLI_inline_CLI_reference_equality": true + }, + "CLI_unavailable": null, + "status": null, + "coverage": "complete-within-inputs", + "summary": null, + "limitations_count": 3 + } + ], + "raw_outer_command_receipt_sha256": "d5804ca30f1d867bdcf360b9e16b4e7a0f75f75c00f8de998de16dbfdf6cbd09", + "raw_resource_receipt_sha256": "9a3f71b04e4e49d60f2d0ebbd8027a3378b45ab7c12f800a91796b5edff1c4db" + } + ], + "known_transport_gap": { + "attempt": "wasm-01", + "actual_inline_arguments_UTF8_bytes": 27823016, + "default_stdio_buffer_bytes": 10485760, + "observed": "Connection closed before1024element admission; no typed application-capacity result.", + "resumed_attempt": "wasm-02", + "positive_required_parity": "Fresh imported MCPrefs == complete CLIinline == CLIselected-bundle refs", + "source_inspection_replayed": false + }, + "audit": { + "attempts": 13, + "successful_domain_lanes": 9, + "failed_harness_attempts": 3, + "preserved_transport_capacity_attempts": 1, + "all_payload_cleanup_verified": true, + "all_resource_observations_verified": true, + "all_OOM_events_zero": true, + "all_consumer_producer_replay_zero": true, + "single_built_runtime": true + }, + "source_receipt_sha256": "2ec40f17a2b49b619528e4d3fe008a1e7a5653ae2a27240a5eec5414ab25c39b" + }, + "additional_boundary_checks": { + "runs": [ + { + "check": "focused-source-CLI-MCP", + "source_state": "7e10c1cc9 plus CLI diagnostic-name fix and boundary tests", + "files": 7, + "tests": 161, + "elapsed_seconds": 154.106, + "CPU_usec": 153592029, + "peak_process_family_bytes": 1884516352, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "log_sha256": "9fd5f8b9823651a71b8546d8ee62a4054fe54b715fed1adcad4f5e07bfafe8f0" + }, + { + "check": "typecheck-touched-lint-module-boundaries", + "source_state": "7e10c1cc9 plus CLI diagnostic-name fix and boundary tests", + "files": null, + "tests": null, + "elapsed_seconds": 20.937, + "CPU_usec": 20691983, + "peak_process_family_bytes": 1142951936, + "kernel_payload_exit_observation_verified": true, + "cleanup_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "log_sha256": "041ace1484c9790277e1cc8b8021eedaeea126868efc5b42042d60b525aad3e5" + } + ], + "fixed_server_fixture_scope": "17 checks cover six JavaScript and three mobile consumers, direct inline inspect_analysis_view, missing references, role errors, summary retention and unchanged 1024-element admission; authored fixtures do not establish real providers.", + "CLI_regression": "Shared JSON parsing preserves public hyphenated command names in malformed-inline/file diagnostics; canonical application operation still owns dispatch.", + "canonical_source_errors": "Native function and process workflows diagnose malformed complete source schemas/identities with EvidenceIntegrityError after resource admission; other workflows preserve their format-owner projections. Request-envelope/reference-grammar failures remain invalid_request. Native directions cover both schema profiles. The process-depth regression preserves the selected side, parameters path and depth ceiling in public diagnostics, verifies a subsequent normal comparison and forbids provider startup." + }, + "reconciliation_review_probe": { + "source_head": "70db1e8164f31b3e3125e1f688f11187233517dd", + "mode": "authored-fixture public MCP review probe, no real provider claim", + "profiles": ["full", "compact"], + "positive_controls": 2, + "malformed_MCP_rejections": 24, + "complete_schema_rejections": 12, + "identities_schema_rejections": 12, + "provider_calls": 0, + "all_connections_closed": true, + "observations": [ + { + "profile": "full", + "carrier_index": 0, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 0, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "full", + "carrier_index": 1, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 1, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "full", + "carrier_index": 2, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 2, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "full", + "carrier_index": 3, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 3, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "full", + "carrier_index": 4, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 4, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "full", + "carrier_index": 5, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "full", + "carrier_index": 5, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 0, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 0, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 1, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 1, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 2, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 2, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 3, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 3, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 4, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 4, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + }, + { + "profile": "compact", + "carrier_index": 5, + "position": "static", + "code": "invalid_request", + "source_path": ["static_layers", 0, "analysis"] + }, + { + "profile": "compact", + "carrier_index": 5, + "position": "runtime", + "code": "invalid_request", + "source_path": ["runtime_observations", 0] + } + ], + "elapsed_seconds": 3.334, + "CPU_usec": 3155635, + "peak_process_family_bytes": 287404032, + "payload_cleanup_verified": true, + "kernel_payload_exit_observation_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "probe_source_sha256": "c62ad57d5f1634dd322dbda4926f239c18c57c177777ca653fdbe8df9f69e14d", + "log_sha256": "83faa53d876db6f5ec9e83462c2d021645304b5051f0008d147dd3093ba0d1fe", + "review_disposition": "Reported null dereference not reachable on recorded source head. Public borrowed request has no identity refinement; owner authentication precedes identity-only validation. Complete schemas reject invalid children before root refinement." + }, + "source_diagnostic_followup": { + "source_head": "63cf473051ebad83ce36bd7000abd1b88ac8450a", + "changes": "Boundary regression/documentation only; runtime unchanged. Existing process-depth assertion had expected SDK envelope diagnostics instead of the canonical Evidence owner.", + "files": 6, + "tests": 104, + "elapsed_seconds": 64.384, + "CPU_usec": 64066692, + "peak_process_family_bytes": 1476808704, + "cleanup_verified": true, + "kernel_payload_exit_observation_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "log_sha256": "ece39c677ea57af842b8cb8f5b86dad9263b5ea675617cc714d5fbc640d6ea4d", + "CI_failure_receipt_sha256": "18d0cfd236b7b50be74f5adb48cfb2ac01a8f125ad80340e122b59b14f960821", + "covered_boundaries": [ + "session lifecycle and depth rejection", + "process comparison diagnostics", + "native comparison diagnostics", + "source/provider identity", + "investigation composition", + "full/compact native directions" + ] + }, + "closure_acceptance_followup": { + "source_head": "a92c947305cf172a415a00ab902e6b0c58885e86", + "changes": "Acceptance regression/documentation only; runtime unchanged. Inline comparisons are supplied source records, so missing closure identifies a linked dependency; retained comparison selectors must resolve the comparison itself first.", + "files": 5, + "tests": 17, + "elapsed_seconds": 123.152, + "CPU_usec": 121276327, + "peak_process_family_bytes": 1466314752, + "cleanup_verified": true, + "kernel_payload_exit_observation_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "log_sha256": "f18ff526800c02d0dac778ed9e820f055e9a3eac7f5efd79642e469434bf47cb", + "CI_failure_receipt_sha256": "c0512165f3455cb76a7a351361c6d8678867f8a6cf26063225c1e7eeb4cb410f", + "coverage": [ + "inline and retained missing-closure identities", + "full originating-bundle recovery and complete output equality", + "default1024 retained aggregate capacity", + "artifact traversal/comparison coverage", + "derived replay preserves analyst revisions", + "advertised local tool contracts and malformed process choices" + ] + }, + "source_path_admission_followup": { + "source_head": "e2ec93bea1056ea07caba132b7ee2e80c4844e5e", + "source_state": "working tree includes shared owner-failure input path attribution and boundary regressions", + "semantics": "Preserve the original typed owner failure and all diagnostics; attach its declared source path only when the owner did not supply one. Do not reparse source values or overwrite owner context.", + "files": 9, + "tests": 75, + "elapsed_seconds": 98.459, + "CPU_usec": 98133579, + "peak_process_family_bytes": 1605656576, + "cleanup_verified": true, + "kernel_payload_exit_observation_verified": true, + "memory_events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "log_sha256": "2a5747e2a4330c2e2ff4e1524f887317994d4374f9bc362c74963d7aab25c9e0", + "HTTP_default_boundary": [ + { + "source_records": 1019, + "request_bytes": 3123, + "response_bytes": 703, + "code": "evidence_integrity_mismatch", + "diagnostics": { + "input_path": ["functions", 0] + } + }, + { + "source_records": 1020, + "request_bytes": 3126, + "response_bytes": 824, + "code": "resource_constraint", + "max_input_elements": 1024 + } + ], + "ADB_CLI_file": { + "tests": 41, + "passed": 41, + "17_extra_case_elapsed_ms": 1597, + "test_deadline_changed": false + }, + "CI_failure_receipt_sha256": "b661561b19afbdd3c07f67bf7765aec8f3a3ac52aee425e9190e7c6c3c3694b1" + }, + "adb_timeout_probe": { + "metadata": { + "head": "e2ec93bea1056ea07caba132b7ee2e80c4844e5e", + "dirty": true, + "runtime_files_sha256": "18a7b20e0548ecfb155b49b289af0a5458a20a406ad54bf6ce47de5cb75947e1", + "runtime_files_count": 2656, + "node_version": "v24.22.1", + "source_changes_during_probe": "None by probe operator", + "helper_sha256": "1aa0d21ddb8c05520a8abb432e54821f145772ec75bc7503a34ad0c41484464c", + "script_sha256": "0da8d19ab6946102036a76abaab9bb907f637a0a4c80374b8bed72703b243e98", + "probe_sha256": "3636eaefc7b6506404cb4288c6e60131aa61ed19bcc0909594aa4edf10a2b0a9", + "claim_scope": "Bounded exact invalid CLI validation case; no baseline runtime measurement or OS cold-cache claim" + }, + "attempts": [ + { + "lane": "cold", + "wrapper_exit_code": 0, + "scope_elapsed_seconds": 2.223, + "cpu_usec": 2050776, + "peak_memory_bytes": 208924672, + "cleanup_verified": true, + "abort": null, + "payload_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "result": { + "lane": "cold", + "cache_scope": "Node compile cache only; OS page cache uncontrolled", + "elapsed_ms": 1970.016705, + "exit_code": 1, + "signal": null, + "killed": false, + "structured_error": { + "code": "VALIDATION_ERROR", + "message": "REA could not read the command arguments. Check the named argument and its expected type, then try again.", + "fieldErrors": [ + { + "code": "too_big", + "missing": false, + "path": "extra", + "expected": "", + "received": "", + "message": "Too big: expected array to have <=16 items" + } + ] + }, + "stderr_bytes": 0, + "output_truncated": false, + "helper_timeout_ms": 20000, + "valid": true, + "compile_cache_entries_before": 0, + "compile_cache_entries_after": 1, + "configured_adb_exists": false, + "target_execution": "The 17-element option is rejected before command run/provider admission; no ADB executable is available at the explicitly selected path" + }, + "resource_metric_scope": "Kernel cgroup counters at payload exit, including probe helper and observer; CLI elapsed excludes scope setup" + }, + { + "lane": "warm", + "wrapper_exit_code": 0, + "scope_elapsed_seconds": 1.42, + "cpu_usec": 1250331, + "peak_memory_bytes": 202555392, + "cleanup_verified": true, + "abort": null, + "payload_events": { + "memory.events": { + "low": 0, + "high": 0, + "max": 0, + "oom": 0, + "oom_kill": 0, + "oom_group_kill": 0 + }, + "pids.events": { + "max": 0 + } + }, + "result": { + "lane": "warm", + "cache_scope": "Node compile cache only; OS page cache uncontrolled", + "elapsed_ms": 1170.06075, + "exit_code": 1, + "signal": null, + "killed": false, + "structured_error": { + "code": "VALIDATION_ERROR", + "message": "REA could not read the command arguments. Check the named argument and its expected type, then try again.", + "fieldErrors": [ + { + "code": "too_big", + "missing": false, + "path": "extra", + "expected": "", + "received": "", + "message": "Too big: expected array to have <=16 items" + } + ] + }, + "stderr_bytes": 0, + "output_truncated": false, + "helper_timeout_ms": 20000, + "valid": true, + "compile_cache_entries_before": 1, + "compile_cache_entries_after": 1, + "configured_adb_exists": false, + "target_execution": "The 17-element option is rejected before command run/provider admission; no ADB executable is available at the explicitly selected path" + }, + "resource_metric_scope": "Kernel cgroup counters at payload exit, including probe helper and observer; CLI elapsed excludes scope setup" + } + ], + "prelaunch_attempts": [ + { + "stage": "harness Node resolution before payload launch", + "error": "/usr/local/bin/node is absent; pinned PATH fallback was not used", + "cli_payloads_started": 0, + "resolution": "Resolve node from the already-authorized pinned PATH and assert v24.22.1" + } + ], + "limitations": [ + "Two serial local probes; CI host load and its coverage/worker environment are not reproduced.", + "Cold/warm describes only explicitly selected Node compile-cache directory. OS page cache is uncontrolled.", + "No equivalent baseline runtime was built or measured; passing probes do not establish baseline flakiness.", + "The production 20-second helper deadline, 1MiB output budget, 16-element option maximum, and guarded 1CPU/4GiB hard/3GiB early-stop limits were preserved." + ], + "source_findings": { + "ci_failure": "The helper timed out after 20000ms and terminated the child with SIGTERM; the expected validation assertion was never evaluated.", + "unchanged_paths": "CLI entry/bootstrap, ADB domain/command/provider, argument/error presenters, ExecFileOutput helper and adbCli test are unchanged against base 23de10bb2.", + "shared_startup": "CLI eagerly imports all command registrars, including changed Evidence consumers; no deterministic startup regression is ruled out by source inspection alone.", + "interpretation": "Exact local validation works without provider admission. Transient CI load/startup/exit delay is plausible; baseline flakiness remains unproven." + }, + "source_receipt_sha256": "a4a4d70886f800b49f4035127a8c8f1cc01e789a2bfb4feb93a37287056126cf" + }, + "final_native_replay": { + "metadata": { + "runtime_head": "e2ec93bea1056ea07caba132b7ee2e80c4844e5e", + "source_dirty": true, + "source_status_sha256": "b14ac2eb972870d1d63d0a210d69eb1768200ed1aeaae3373e4ed7e7307c9c11", + "runtime_files_sha256": "1bcdcd29df3b90f2cfa61aad29d647dbfb22176d0bca880c7a537507fdeab271", + "runtime_files_count": 2656, + "node_version": "v24.22.1", + "harness_sha256": "b27ebed606c3a5449d9f21d050b63b77a0d6769e5ddbd24ab0aec4aae301236a", + "prior_harness_sha256": "5df9e8da8ca635fc8f3938573f5148f6b87f45f5c14490218bd5a65b0b083983", + "model_invocations": 0 + }, + "runtime_unchanged_during_replay": true, + "wrapper_exit_code": 0, + "scope_elapsed_seconds": 5.543, + "cpu_seconds": 5.263196, + "kernel_peak_memory_bytes": 447791104, + "kernel_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, + "abort": null, + "resource_scope": "Kernel cgroup counters at payload exit, including replay helper and observer", + "result": { + "status": "passed", + "scope": "Final rebuilt runtime, fresh target-free public stdio MCP replay of immutable genuine jq/Ghidra Evidence", + "elapsed_ms": 4832.407, + "source_bundle": { + "label": "transport-roundtrip/native-01/source.bundle.json", + "bytes": 3097994, + "sha256": "0a3adad6ab39bfe29320e1f070a8e598382d2f17c4d6c19079bd6ff16dfe0937", + "records": 8, + "unchanged": true + }, + "source_evidence_ids": [ + "ev_6a73ba03158b02af19dc76fe1865647b1dbeeaff7e7c4eed20a5f8fc58caff59", + "ev_681e6e6b603413f6dc0eeff8ca31acb3875d1d72d97b5e2de837460c2c03b0f9", + "ev_df9c037f6b8a7c14d87b43162eac21246fa6a47f739361f653f459ec094d2df6" + ], + "results": [ + { + "tool": "build_call_path", + "evidence_id": "ev_b3a67463b568bd4dd5f338a995014b937dc3572a8e8557f72433a86dd98f44be", + "complete_Evidence_equal": true, + "identity_equal": true, + "provenance_coverage_partial_unknowns_equal": true, + "complete_Evidence_JSON_sha256": "d594b20cf2f0113ae56328154421012630d17700b858dc1c728153c3d7f42ce2", + "normalized_result_sha256": "32aeea7af5a251e6140ab169ebf860dec796c92b5cb369dde7388d06674a236b", + "original_golden_file_sha256": "314398a30dc2fc69a5b0c48ed02e1c3196f61c58947349dbbe20412292f2925b", + "request_sha256": "9336645bf08d22b43fccfa4b5dbc31d489c2d859e94d978054fe586f121b6745", + "raw_public_reply_value_equal": true, + "raw_public_reply_value_sorted_JSON_sha256": "4c50d38a4dd3b2a0c8297be9ceab0daa78a9552e2f223033e1cee4b3e0acf1f6" + }, + { + "tool": "compare_functions", + "evidence_id": "ev_a287514fc988745a27e6b1a52bfe3833bafdaebc956aace351b420ef36982259", + "complete_Evidence_equal": true, + "identity_equal": true, + "provenance_coverage_partial_unknowns_equal": true, + "complete_Evidence_JSON_sha256": "e8e7a231c330cf8303b16b0fce652c7ec70314981ec1f16c080ef19a7511c86d", + "normalized_result_sha256": "435b6811e6cebcdc7fbf759063c80f2a1af1230cb3e02d18129d594f91cf04e3", + "original_golden_file_sha256": "a038a9f6ab1a5dd595298652ecccaed563b9ba42d7f7e5501a6613d5f0d9138c", + "request_sha256": "df40a1886c6a02ce09c1f2112bedd0afc4eb4387f2e031acc3d7c4784ab35d2e", + "raw_public_reply_value_equal": true, + "raw_public_reply_value_sorted_JSON_sha256": "4302c2da170600d290e32532628fba366ac1125a1bab8495d5d907d456c064d3" + }, + { + "tool": "match_native_functions", + "evidence_id": "ev_3aea60bbe5bcea71c2454fa1f560bc7a3a3df9534125471322ae5c6302cf6737", + "complete_Evidence_equal": true, + "identity_equal": true, + "provenance_coverage_partial_unknowns_equal": true, + "complete_Evidence_JSON_sha256": "dda700d743438c9ffbee33c4129c728de6249d85f6440a5e411fe21c7bdc922e", + "normalized_result_sha256": "73ec328c668981d0fbbfd14bccd0f6e1d2f11bd8eda308358334dccd51771af4", + "original_golden_file_sha256": "7ba60ae9e42775d116d57f92e4a47bd2e88ce34e4b96c86eb62e9173a9fa58c8", + "request_sha256": "2bc4b0444f12b039a42a4770442614647fe3cbd3d34251f29e30db59db3db039", + "raw_public_reply_value_equal": true, + "raw_public_reply_value_sorted_JSON_sha256": "21173d4fb03df46e6ffa981739c57a0de30f0366221c53c411cb0384d07d0ed4" + } + ], + "calls": 7, + "producer_calls": 0, + "target_open_calls": 0, + "ghidra_starts_requested": 0, + "source_reanalysis": false, + "complete_Evidence_equal_to_prior_runtime": true, + "default_MCP_input_elements": 1024, + "limits_raised": false, + "model_invocations": 0, + "token_measurement": "Not measured; scripted replay has no model", + "cleanup": { + "client_closed": true, + "transport_closed": true, + "private_root_removed": true + }, + "raw_public_reply_values_equal_before_parsing": true + }, + "limitations": [ + "Replay uses genuine prior native Evidence; prior provider-analysis costs are reported in original transport matrix, not repeated here.", + "One final rebuilt-runtime replay, no before/after performance comparison or cold-cache claim.", + "No models or token measurement; no native CLI counterpart exists for these three operations." + ], + "source_receipt_sha256": "63eb768f4e5d4b4fed04ee03eb2bf374b75bc391559df335110a557c8f4d6444" + } +} diff --git a/docs/verification/evidence-input-composition-2026-10-31.md b/docs/verification/evidence-input-composition-2026-10-31.md new file mode 100644 index 0000000000..3626703fb1 --- /dev/null +++ b/docs/verification/evidence-input-composition-2026-10-31.md @@ -0,0 +1,263 @@ +# Evidence input composition verification + +The shared declaration and request resolver cover 20 individual-Evidence +composition tools. Artifact file sources, analysis views and environment export +keep their explicit source owners. Catalog conformance checks those boundaries +and rejects undeclared record inputs or unsupported source containers. + +This record accompanies [#8293](https://github.com/morluto/rea/issues/8293) and +the real Ghidra input-capacity regression +[#8281](https://github.com/morluto/rea/issues/8281). +The [machine-readable receipt](evidence-input-composition-2026-10-31.json) +retains all fourteen model attempts, artifact identities, oracle provenance, +tool profiles, actual usage, resource measurements and limitations. It also +records the thirteen subsequent scripted public-transport attempts below. + +## Runtime and measurement scope + +Tests used Node 24.22.1 and local Codex CLI 0.165.0, explicitly requesting +`gpt-6.1-terra` with reasoning `high`. This identifies the requested alias; +the resolved server model is not inferred. Both dirty candidate builds were +based on `23de10bb2849cdce8ddbe70504aa7cc57de75a98`. Their complete built-file +fingerprints are recorded in the receipt. The later build includes the shared +reference-format hint and authenticated source/type-boundary corrections. +Subsequent cleanup reused the existing pure semantic-query input schema and +moved SDK tests into their boundary project. Full and compact catalog bytes +remained identical to the later model build; 22 affected checks were rerun. + +Heavy tasks ran serially in owned Linux scopes: one CPU, 4 GiB hard memory, +3 GiB early stop, no swap and at most 192 tasks. CPU and peak memory are kernel +process-family counters at payload exit while the observer remains alive; +they are not final whole-scope counters. Cleanup was verified for every model +attempt and CLI phase. No OOM event occurred. + +Each model task starts a fresh MCP process and isolated client profile. Native +tasks start a fresh Ghidra database, then reuse its two retained dossiers. +System/filesystem caches were not cleared; cold filesystem costs are unmeasured. +Initial primary tasks used the complete catalog. Later scoped tasks are labeled +`diagnostic/catalog-selection`; their different tools, guidance and selected +facts prevent a controlled speed or token comparison. + +## Real application results + +| Domain | Real artifact | Reviewed outcome | +| ---------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Native | Installed jq ELF, real Ghidra 12.1.4 | Each selected function analyzed once; complete parent references supported a direct one-hop path and function comparison. Independent `objdump` corroborated the selected call. Nonexhaustive frontier and behavioral uncertainty remained visible. | +| Managed | Shipped NuGet.CommandLine.XPlat.dll, assembly 6.11.2.1 | Artifact/member parent references projected a managed graph. The existing distinct-Evidence rule refused self-comparison. Digest, MVID, owner/token and selected CIL facts matched an independent metadata reader; its parser warnings limit that oracle's scope. | +| JavaScript | Obsidian 1.14.4 ASAR | Partial: the producer hit its existing 300-second per-source timeout. The retained parent still supported a semantic trace. A bidirectional feature trace exceeded 10 MiB and had no supported retained summary. Tracked separately in [#8296](https://github.com/morluto/rea/issues/8296). | +| JavaScript | Shipped agent-browser 0.27.0 CLI entrypoint | The final scoped task analyzed the exact selected source once, then reused its parent for feature and semantic traces. Independent source facts confirmed builtin imports/call locations. Unselected sources remained unknown; directory and file digests were distinguished. | +| WASM | Shipped VS Code Tree-sitter CSS module | One real WABT inspection supplied retained references for export comparison and a portable bundle. An independent raw WASM reader confirmed seven imports, three exports and `tree_sitter_css: () -> i32`. Static interface equality did not imply runtime compatibility. | +| Artifact / Apple | Rectangle 2.0.4 release ZIP | One retained inventory supported Apple projection and artifact comparison. Independent ZIP and Mach-O observations corroborated paths/content. Symlink metadata gaps remained unknown. | +| Android | NewPipe 0.29.1 release APK | One retained inventory supported Android projection. Independent ZIP observations confirmed manifest, three DEX files and four ABI library members/digests. JNI path-pair hypotheses stayed inferred. | +| Harmony | Shark 1.0.8 release APP and its real inner HAP | Both retained inventories supported Harmony projection. Separate ZIP/JSON checks preserved outer ZIP, APP and HAP identities, Stage/Ark/resource path observations and shallow APP coverage. Signature naming was not treated as cryptographic verification. | +| Process | Two controlled real jq CLI captures | Authentic captures were imported and compared by references, then their retained comparison was aggregated. MCP output deep-equaled the existing inline CLI result. Terminal change, settlement differences, filesystem/process unknowns and source closure were preserved. | + +The first supplemental agent-browser model task wandered into unrelated +inventory tools and failed to perform the requested producer analysis. It was +stopped and retained as an interrupted failure, including cumulative usage as +a lower bound. The successful later scoped task does not erase that attempt. +The missing-reference task produced its expected typed failure without opening +a target, repeating a request or inventing Evidence. + +## Actual model task costs + +Input counts include schemas, accumulated context, extra calls and retries. +Cached input is a subset of total input. Reasoning is included in output; +its separate subset is retained in JSON. These are completed task costs, not +serialized-byte estimates. The initial partial/interrupted attempts and their +costs also remain in the receipt. + +| Later task | Calls / failed | Input tokens | Cached input | Output tokens | Task elapsed (s) | Family CPU (s) | Peak bytes | +| ------------------------------ | -------------: | -----------: | -----------: | ------------: | ---------------: | -------------: | ------------: | +| jq, complete catalog | 11 / 0 | 599,933 | 535,424 | 1,894 | 80.931 | 38.326466 | 1,269,936,128 | +| agent-browser, scoped | 12 / 1 | 619,589 | 563,712 | 2,519 | 54.521 | 9.472096 | 903,847,936 | +| NewPipe, scoped | 5 / 0 | 179,635 | 150,528 | 1,750 | 40.835 | 10.080132 | 704,409,600 | +| Shark APP, scoped | 6 / 0 | 221,571 | 197,376 | 1,339 | 32.437 | 6.377128 | 560,021,504 | +| Shark HAP, scoped | 5 / 0 | 184,452 | 152,576 | 1,212 | 31.108 | 6.012587 | 580,714,496 | +| WASM plus export, scoped | 4 / 0 | 161,702 | 141,056 | 1,033 | 32.320 | 9.036127 | 700,076,032 | +| Imported process chain, scoped | 3 / 0 | 89,101 | 73,984 | 725 | 26.911 | 5.575060 | 531,283,968 | + +The later jq task made no malformed reference calls, while the initial task +omitted the required `kind` twice. It selected a different callee and made three +guidance calls; its longer elapsed time and different facts preclude attributing +a general improvement to the new hint. + +## Equivalent portable CLI comparison + +The actual WASM model task exported a 13,925,066-byte authenticated bundle. +Both fresh CLI processes selected its same complete inspection parent and +compared exports against that parent. Their 3,732-byte outputs were byte-identical +and deep-equal to the model's MCP comparison: +`ev_b32346965c153a067abff9698aeba1ccbace34c78ac2071ac18311250b68c6da`. +Bundle, selected source and runtime hashes stayed unchanged. Neither CLI arm +started a decoder or reanalyzed the module. + +| Input | Request bytes | Elapsed including scope (s) | Family CPU (s) | Peak bytes | +| ---------------------------------- | ------------: | --------------------------: | -------------: | ----------: | +| Complete inline pair | 27,823,017 | 2.832 | 2.612440 | 370,515,968 | +| Retained pair plus selected bundle | 261 | 2.632 | 2.385397 | 310,009,856 | + +The reference arm still reads/authenticates the existing bundle. This single +ordered pair ran inline first with shared uncontrolled host caches. It establishes +equivalent completed results and records observed costs; it does not establish +a general speedup. Common upstream inspection/export costs appear in the model +table. Preparation additionally cost 1.119 s / 0.875255 CPU s / 316,796,928 peak +bytes; verification cost 0.518 s / 0.282755 CPU s / 42,881,024 peak bytes. +The optional tokenizer was unavailable, so request-token savings are unmeasured. +Request bytes are not presented as model tokens. + +## Fresh-connection public transport matrix + +The real transport matrix completed all nine supported domain lanes on built revision `7e10c1cc9` (`63746cf7b340358ac5ce609761ea3198ae3a62e24df52f62ed5be16893940021`). It used public stdio MCP and CLI, real artifacts and authentic exported Evidence; no model or fixture provider ran. + +| Lane | Attempts | Scope seconds, all attempts | CPU seconds, all attempts | Max kernel peak MiB | CLI calls, including negatives | +| ----------- | -------: | --------------------------: | ------------------------: | ------------------: | -----------------------------: | +| native | 1 | 35.915 | 34.674174 | 927.99 | 0 | +| javascript | 3 | 34.324 | 33.094918 | 658.91 | 8 | +| managed | 2 | 37.341 | 36.221694 | 694.65 | 9 | +| android | 1 | 20.208 | 19.569397 | 696.40 | 4 | +| harmony-app | 1 | 16.714 | 15.950327 | 495.68 | 4 | +| harmony-hap | 1 | 16.294 | 15.886932 | 508.47 | 4 | +| apple | 1 | 25.453 | 24.824482 | 642.90 | 8 | +| wasm | 2 | 22.648 | 22.027649 | 791.67 | 4 | +| process | 1 | 9.056 | 8.586552 | 472.58 | 2 | + +Producer/export costs from the failed JS/managed harness attempts are included. Three harness assumptions and one actual SDK transport-capacity failure are retained individually. Every payload cleanup and kernel observation verified; no OOM or increased allowance. + +Fresh imported references match complete CLI inline and selected-bundle outputs where those command contracts support them. Small APP/process/correlation inputs also match admitted MCP inline. Larger MCP inline forms hit the unchanged 1024-element guard. Native and selected managed-member comparisons have no equivalent Evidence-input CLI form; process accepts two complete capture files, and artifact comparison uses explicit evidence-file selectors. + +The 27,823,016-byte WASM inline arguments exceeded the default 10,485,760-byte stdio frame buffer and closed the connection before application admission. The resumed attempt reused the same authentic bundle: fresh MCP references and both supported complete CLI forms agree. No WABT replay or limit increase. + +Process capture comparison and behavior aggregation preserve changed/unknown facts. Correlation with genuine native comparison Evidence remains unknown with one unresolved branch under an explicit noncausal hypothesis. + +These are scripted transport-task costs with uncontrolled caches, not a before/after benchmark or model-token measurement. Earlier WASM inspection/process capture costs are linked in the prior verification report. Later source-only diagnostic fixes have separate focused validation. + +The accompanying JSON retains all thirteen attempts, exact kernel counters, +source/result identities and hashes, CLI arm receipts, failures and limits. +These scripted costs do not add to the model-token table. + +## Review follow-up + +An additional local terra/high source-only review found that the refactored +process CLI read later selected files before authenticating the first capture. +The shared owner now exposes authenticated source facts and their comparison; +CLI authenticates each source before reading the next, while MCP uses the same +readers and comparator. Two public CLI regressions cover invalid left/right +records with missing later files. The original real captures were compared +again without target execution and retained the exact comparison Evidence ID. + +Greptile found a fixed-server WASM regression: caller-supplied complete inputs +incorrectly required a recorder. Shared retention now explicitly recognizes +caller-owned inputs. Provider-produced sources still require a recorder; +available recorders, their rejections and cancellation sources remain honored. +A fixture-backed public fixed-server MCP regression and ownership/cancellation +checks cover that distinction. These follow-up regressions are not additional +real-provider measurements. The related nine-file run passed 103 checks; +subsequent schema cleanup and CLI checks passed ten more selected checks. +Type, lint, Knip and architecture/module-boundary checks were repeated. + +The fourteen application-analysis attempts above are separate from source-only +review; that CLI review did not report usable model-token counts. Its cleanup +was verified, with 57.860 s task elapsed, 4.720926 s family CPU and 427,364,352 +peak bytes. Existing captured model measurements remain tied to their recorded +runtime fingerprints, preceding these review corrections. + +The additional seven-file boundary run passed 161 checks after the CLI's JSON +parser was corrected to keep public command names in malformed-input diagnostics. +It includes 17 fixed-server cases for six JavaScript and three mobile consumers, +direct inline analysis views, missing references, wrong source roles, summary +retention and default input admission. These use complete authored fixtures; +they do not substitute for the real-artifact matrix. The run included a runtime +build and cost 154.106 s / 153.592029 CPU s / 1,884,516,352 peak bytes. Subsequent +type, touched-file lint and module checks passed, with no OOM or cleanup gap. + +Native function and process workflows send malformed complete source records to +the canonical Evidence owner after resource admission. Schema or identity failures return +`evidence_integrity_mismatch` in both MCP schema profiles, including the original +`relationship_coverage` diagnostics. Other workflows preserve their format-owner +error projections. The directional-coverage regression still +checks every malformed case and the advertised full schema. Invalid request +envelopes and reference grammar remain request errors; no duplicate source parse +was introduced solely to reproduce the former SDK error classification. + +A later review reported a possible null-source dereference in reconciliation. +On `70db1e816`, the public request has no identity refinement; source authentication +runs before identity-only validation. A public fixed-server MCP probe passed two +valid controls and rejected 24 malformed source cases across full/compact profiles +with structured `invalid_request` and accurate paths. Both complete schemas also +rejected each of twelve cases without throwing. No provider ran. Cleanup was +verified: 3.334 s elapsed, 3.155635 CPU s and 287,404,032 peak bytes, with no OOM. +This authored-fixture probe disproves the reported path on that revision; +it does not establish real passive-runtime behavior. No production change was +needed for that finding. The accompanying JSON records its observations. + +CI later exposed the corresponding process-depth assertion against the old SDK +error envelope. Its regression now checks the canonical integrity error while +preserving the public left-source, `parameters` and exact nesting-limit diagnostic. +A normal comparison must still succeed afterward and provider startup is forbidden. +The iterative JSON depth guard still rejects before recursive cloning or hashing; +no second source parser was added. Runtime code was unchanged. Six related MCP +files passed 104 checks, including source identity, native/process diagnostics, +investigation composition and session lifecycle: 64.384 s elapsed, 64.066692 CPU s +and 1,476,808,704 peak bytes. Cleanup was verified with no OOM. + +A retained-closure acceptance assertion also assumed that a complete inline +comparison must already exist in the ledger. The shared workflow intentionally +uses that supplied record: a missing dependency is reported for inline input, +while an unresolved selector reports the comparison itself. Both still require +the full referenced closure. Importing the originating bundle restores both +forms to complete, equal Evidence. Five relevant acceptance files passed 17 +checks, covering default-budget aggregation, artifact coverage, replay and public +local contracts: 123.152 s elapsed, 121.276327 CPU s and 1,466,314,752 peak bytes. +Cleanup was verified with no OOM; runtime code was unchanged. + +The HTTP admission-edge regression now checks the same source-owner semantics. +An admitted array of 1,019 malformed carriers fails at its first source, while +1,020 carriers exceed the unchanged 1,024-element envelope allowance. Responses +were 703 and 824 bytes, respectively; both stayed below 4 KiB and the server kept +serving. The shared resolver additionally preserves `input_path` on owner errors +when absent. Original error identity, owner diagnostics and existing paths remain +intact, with no second source parse. The rebuilt nine-file gate passed 75 checks, +including all 41 ADB CLI cases, resolver, native references and HTTP/stdio admission: +98.459 s elapsed, 98.133579 CPU s and 1,605,656,576 peak bytes; cleanup verified, +no OOM. + +CI also terminated one unchanged ADB CLI child at its helper's existing 20-second +deadline. Two isolated exact 17-extra CLI probes returned the expected argument +validation failure in 1.970 s with a fresh Node compile cache and 1.170 s reusing +it. Neither ran ADB or reached a device; the 20-second deadline was unchanged. +Both local scopes cleaned up with no OOM. The rebuilt full ADB CLI file passed, +including that case in 1.597 s. The CI delay was not reproduced; these observations +do not establish baseline flakiness or a controlled before/after startup result. + +After the diagnostic change, the untouched genuine jq/Ghidra bundle was imported +again into a fresh target-free stdio server. Call path, function comparison and +native matching returned complete public values equal to their original matrix +goldens, including IDs, provenance, coverage and unknowns. Seven calls included +no producer, target opening or Ghidra execution. The built runtime fingerprint +was `1bcdcd29df3b90f2cfa61aad29d647dbfb22176d0bca880c7a537507fdeab271`; +source bundle and runtime stayed unchanged. The replay cost 5.543 s / 5.263196 CPU s +/ 447,791,104 peak bytes, with verified cleanup and no OOM. This is an additional +scripted replay, separate from the thirteen-attempt matrix and fourteen model +attempts. Its full receipt and the timeout probes are included in JSON. + +## Regression and coverage limits + +The focused regression run passed 508 checks covering source integrity, exact +IDs, mixed/optional/nested selections, default constraints, converted full and +compact SDK schemas, source closure, caller choices, CLI bundle immutability, +capacity admission and late cancellation/source retention. SDK boundary tests +are kept in the MCP boundary project; pure declaration checks stay in contracts. + +Full-library lint exceeded the local early-stop limit even with one worker. +The same selected 6,105 files were checked in sequential batches of 400; +peak family memory was 741,650,432 bytes. Two new test grouping errors were +corrected and those files rerun. Failed lint/driver attempts remain in local +receipts. Process-driver path/ANSI oracle corrections reused the original +authentic captures. + +No real Hopper, IDA, Windows capture, Android device, Harmony runtime, passive +browser reconciliation or managed/native runtime equivalence is established +by these tests. There is no controlled main-versus-candidate model cohort, +no cold filesystem measurement and no claim that every provider facet was +independently verified. diff --git a/docs/wasm-artifacts.md b/docs/wasm-artifacts.md index cc6ef5a292..571214dd7f 100644 --- a/docs/wasm-artifacts.md +++ b/docs/wasm-artifacts.md @@ -189,11 +189,13 @@ typed interfaces. `compare_wasm_interfaces` accepts two complete `inspect_wasm_artifact` Evidence records, or exact retained references in the current MCP session, and required -`scope: "imports" | "exports" | "both"`. It reads no files and needs no active -WABT toolchain. CLI uses portable inline Evidence in one JSON document: +`scope: "imports" | "exports" | "both"`. It uses producer-owned typed facts +without rereading the artifact and needs no active WABT toolchain. CLI accepts +portable inline Evidence in one JSON document, or +resolves the same retained-reference inputs from an explicit saved bundle: ```bash -rea compare-wasm-interfaces comparison-input.json --json +rea compare-wasm-interfaces comparison-input.json --evidence-bundle /absolute/path/evidence.json --json ``` ```json @@ -204,8 +206,10 @@ rea compare-wasm-interfaces comparison-input.json --json } ``` -The references above are MCP-only; put complete Evidence objects in the CLI -input. Resolve summary delivery to its retained complete parent before comparing. +Omit `--evidence-bundle` when the CLI input contains complete inline records. +Resolve summary delivery to its complete `normalized_result.parent_evidence_id` +before comparing. References preserve that exact source identity and coverage; +the bundle supplies no facts absent from the selected record. Results retain both input Evidence IDs, artifact paths/digests/lengths and every selected occurrence. Unique names are added, removed, changed or unchanged; duplicate names and unsupported signatures are unknown. Incomplete or historical diff --git a/scripts/verify/javascript/runtime-observation.mjs b/scripts/verify/javascript/runtime-observation.mjs index 5dcaf2ca71..8587e5341e 100644 --- a/scripts/verify/javascript/runtime-observation.mjs +++ b/scripts/verify/javascript/runtime-observation.mjs @@ -60,13 +60,13 @@ const reconciled = reconcileJavaScriptRuntimeEvidence({ runtime_observations: runtimeObservations, }); if (!reconciled.ok) throw reconciled.error; -const result = reconciled.value.normalized_result; +const result = reconciled.value.evidence.normalized_result; process.stdout.write( `${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), reconciliation_id: result.reconciliation_id, - evidence_id: reconciled.value.evidence_id, + evidence_id: reconciled.value.evidence.evidence_id, graph_id: result.graph.graph_id, layers: result.static_layers.map((layer) => ({ role: layer.role, diff --git a/src/application/EvidenceInputResolver.ts b/src/application/EvidenceInputResolver.ts index 53325b73e3..327463a04f 100644 --- a/src/application/EvidenceInputResolver.ts +++ b/src/application/EvidenceInputResolver.ts @@ -3,9 +3,20 @@ import type { EvidenceSourceInput, } from "../domain/evidenceReference.js"; import type { Evidence } from "../domain/evidence.js"; +import type { z } from "zod"; +import { + getEvidenceInputContract, + type EvidenceInputPath, + type EvidenceRequestFrameInput, + type ResolvedEvidenceRequestInput, +} from "../domain/evidenceInputContract.js"; +import { retainedEvidenceReferenceSchema } from "../domain/evidenceReference.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { EvidenceReferenceError } from "../domain/evidenceErrors.js"; import { err, ok, type Result } from "../domain/result.js"; +import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import { parseAnalysisInput } from "./AnalysisInput.js"; import { readEvidenceFileSource, type EvidenceFileReadOptions, @@ -14,6 +25,139 @@ import { /** Read-only lookup supplied by the current investigation's Evidence ledger. */ export type EvidenceLookup = (evidenceId: string) => Evidence | undefined; +export interface EvidenceRequestResolutionOptions { + readonly signal?: AbortSignal; + readonly referenceScope?: "retained-closure"; + /** Optional format-owner admission, after lookup and before source use. */ + readonly readEvidence?: ( + input: unknown, + path: EvidenceInputPath, + ) => Result; +} + +interface EvidenceRequestResolution { + readonly input: Input; + readonly sources: readonly unknown[]; +} + +/** An admitting owner may rely on complete Evidence at each declared source. */ +export function resolveEvidenceRequest( + operation: string, + rawInput: unknown, + schema: Schema, + lookup: EvidenceLookup | undefined, + options: EvidenceRequestResolutionOptions & { + readonly readEvidence: NonNullable< + EvidenceRequestResolutionOptions["readEvidence"] + >; + }, +): Result< + EvidenceRequestResolution>>, + AnalysisError +>; +/** Source carriers remain unknown until their format owner admits them. */ +export function resolveEvidenceRequest( + operation: string, + rawInput: unknown, + schema: Schema, + lookup?: EvidenceLookup, + options?: EvidenceRequestResolutionOptions, +): Result< + EvidenceRequestResolution>>, + AnalysisError +>; +/** Resolve only schema-declared Evidence fields, sharing one lookup per exact ID. */ +export function resolveEvidenceRequest( + operation: string, + rawInput: unknown, + schema: z.ZodType, + lookup?: EvidenceLookup, + options: EvidenceRequestResolutionOptions = {}, +): Result, AnalysisError> { + const contract = getEvidenceInputContract(schema); + const parsed = parseAnalysisInput( + operation, + rawInput, + contract.frameSchema, + options.signal, + ); + if (!parsed.ok) return parsed; + const records = new Map(); + const read: EvidenceLookup = (id) => { + if (!records.has(id)) records.set(id, lookup?.(id)); + return records.get(id); + }; + const sources = new Set(); + const result = contract.resolve( + parsed.value, + (input, path) => { + if (options.signal?.aborted) + return err( + new AnalysisCancelledError(operation, { + cause: options.signal.reason, + }), + ); + let selected = input; + if (typeof input === "object" && input !== null && "kind" in input) { + const reference = retainedEvidenceReferenceSchema.safeParse(input); + if (!reference.success) + return err( + analysisInputErrorFromIssues( + operation, + reference.error.issues.map((issue) => ({ + ...issue, + path: [...path, ...issue.path], + })), + rawInput, + { cause: reference.error }, + ), + ); + const found = resolveEvidenceInput( + reference.data, + read, + options.referenceScope, + ); + if (!found.ok) + return err( + new EvidenceReferenceError( + found.error.evidenceId, + found.error.reason, + { + cause: found.error, + expected: found.error.expected, + actual: found.error.actual, + referenceScope: found.error.referenceScope, + diagnostics: { input_path: [...path] }, + }, + ), + ); + selected = found.value; + } + if (options.readEvidence !== undefined) { + const admitted = options.readEvidence(selected, path); + if (!admitted.ok) { + if (admitted.error.diagnostics?.input_path === undefined) + admitted.error.retainDiagnostics({ input_path: [...path] }); + return admitted; + } + selected = admitted.value; + } + sources.add(selected); + return ok(selected); + }, + ); + if (result.ok && options.signal?.aborted) + return err( + new AnalysisCancelledError(operation, { cause: options.signal.reason }), + ); + return result.ok + ? ok({ + input: result.value, + sources: [...sources], + }) + : result; +} + /** Normalize each selected record using its file context and the current ledger. */ export interface EvidenceSourceResolutionOptions extends EvidenceFileReadOptions { readonly operation: string; @@ -50,42 +194,7 @@ export const resolveEvidenceInput = ( return ok(record); }; -/** Resolve an application's Evidence before its existing semantic validation. */ -export const resolveApplicationEvidenceRequest = < - T extends { readonly application: EvidenceInput }, ->( - input: T, - lookup: EvidenceLookup | undefined, -): Result< - Omit & { readonly application: Evidence }, - EvidenceReferenceError -> => { - const application = resolveEvidenceInput(input.application, lookup); - if (!application.ok) return application; - return ok({ ...input, application: application.value }); -}; - -/** Resolve explicitly paired Evidence without guessing another session's record. */ -export const resolvePairedEvidenceRequest = < - T extends { readonly left: EvidenceInput; readonly right: EvidenceInput }, ->( - input: T, - lookup: EvidenceLookup | undefined, -): Result< - Omit & { - readonly left: Evidence; - readonly right: Evidence; - }, - EvidenceReferenceError -> => { - const left = resolveEvidenceInput(input.left, lookup); - if (!left.ok) return left; - const right = resolveEvidenceInput(input.right, lookup); - if (!right.ok) return right; - return ok({ ...input, left: left.value, right: right.value }); -}; - -/** Resolve paired sources like resolvePairedEvidenceRequest; Evidence files use the shared file reader. */ +/** Resolve explicit paired sources; Evidence files use the shared file reader. */ export const resolvePairedEvidenceSources = async < T extends { readonly left: EvidenceSourceInput; diff --git a/src/application/EvidenceRequestResolution.test.ts b/src/application/EvidenceRequestResolution.test.ts new file mode 100644 index 0000000000..b6e04b8789 --- /dev/null +++ b/src/application/EvidenceRequestResolution.test.ts @@ -0,0 +1,375 @@ +import { describe, expect, expectTypeOf, it } from "vitest"; +import { z } from "zod"; + +import { createEvidence, type Evidence } from "../domain/evidence.js"; +import { + borrowEvidenceArray, + borrowEvidenceInput, + evidenceInputSchema, +} from "../domain/evidenceReference.js"; +import { getEvidenceInputContract } from "../domain/evidenceInputContract.js"; +import { readImmutableEvidence } from "../domain/evidenceValidation.js"; +import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { + EvidenceIntegrityError, + EvidenceReferenceError, +} from "../domain/evidenceErrors.js"; +import { err } from "../domain/result.js"; +import { + resolveEvidenceRequest, + type EvidenceRequestResolutionOptions, +} from "./EvidenceInputResolver.js"; + +const evidence = createEvidence( + undefined, + { + id: "test-evidence-input", + name: "Evidence input regression", + version: "1", + }, + { + operation: "test_observation", + parameters: {}, + result: { value: 42 }, + }, +); +const reference = { + kind: "retained-evidence" as const, + evidence_id: evidence.evidence_id, +}; + +describe("schema-owned Evidence request resolution", () => { + it("resolves declared single, nested and array sources once per exact ID", () => { + const schema = z.strictObject({ + source: evidenceInputSchema.describe("Owner-described source"), + layers: z.array(z.strictObject({ analysis: evidenceInputSchema })), + optional: evidenceInputSchema.optional(), + ordinary: z.unknown(), + }); + let lookups = 0; + const ordinary = { kind: "retained-evidence", evidence_id: "display-data" }; + const result = resolveEvidenceRequest( + "test", + { + source: reference, + layers: [{ analysis: reference }, { analysis: evidence }], + ordinary, + }, + schema, + () => { + lookups += 1; + return evidence; + }, + ); + if (!result.ok) throw result.error; + expectTypeOf(result.value.input.source).toBeUnknown(); + expectTypeOf(result.value.input.layers).toEqualTypeOf< + { analysis: unknown }[] + >(); + expect(lookups).toBe(1); + expect(result.value.input.source).toBe(evidence); + expect(result.value.input.layers.map(({ analysis }) => analysis)).toEqual([ + evidence, + evidence, + ]); + expect(result.value.input).not.toHaveProperty("optional"); + expect(result.value.input.ordinary).toBe(ordinary); + expect(result.value.sources).toEqual([evidence]); + }); + + it("borrows inline values until the format owner authenticates them", () => { + const schema = z.strictObject({ source: evidenceInputSchema }); + const opaque = Object.defineProperty({}, "normalized_result", { + get: () => { + throw new Error("premature payload access"); + }, + }); + const result = resolveEvidenceRequest("test", { source: opaque }, schema); + if (!result.ok) throw result.error; + expectTypeOf(result.value.input.source).toBeUnknown(); + expect(result.value.input.source).toBe(opaque); + expect(result.value.sources).toEqual([opaque]); + const admitted = resolveEvidenceRequest( + "test", + { source: evidence }, + schema, + undefined, + { readEvidence: readImmutableEvidence }, + ); + if (!admitted.ok) throw admitted.error; + expectTypeOf(admitted.value.input.source).toEqualTypeOf(); + expect(admitted.value.input.source).toEqual(evidence); + expect(admitted.value.sources).toEqual([admitted.value.input.source]); + const optionalAdmission: EvidenceRequestResolutionOptions = {}; + const borrowed = resolveEvidenceRequest( + "test", + { source: null }, + schema, + undefined, + optionalAdmission, + ); + if (!borrowed.ok) throw borrowed.error; + expectTypeOf(borrowed.value.input.source).toBeUnknown(); + expect(borrowed.value.input.source).toBeNull(); + const rejected = resolveEvidenceRequest( + "test", + { source: null }, + schema, + undefined, + { readEvidence: readImmutableEvidence }, + ); + expect(rejected.ok).toBe(false); + if (!rejected.ok) + expect(rejected.error.diagnostics).toEqual({ input_path: ["source"] }); + }); + + it.each([{ ownerPath: undefined }, { ownerPath: ["payload", 2] }])( + "preserves source-owner failures and existing paths: %j", + ({ ownerPath }) => { + const failure = new EvidenceIntegrityError("Invalid source", { + diagnostics: { + owner: "fixture", + ...(ownerPath === undefined ? {} : { input_path: ownerPath }), + }, + }); + let reads = 0; + const result = resolveEvidenceRequest( + "test", + { layers: [{ source: {} }] }, + z.strictObject({ + layers: z.array(z.strictObject({ source: evidenceInputSchema })), + }), + undefined, + { + readEvidence: () => { + reads += 1; + return err(failure); + }, + }, + ); + if (result.ok) throw new Error("Invalid source was accepted"); + expect(result.error).toBe(failure); + expect(reads).toBe(1); + expect(failure.diagnostics).toEqual({ + owner: "fixture", + input_path: ownerPath ?? ["layers", 0, "source"], + }); + }, + ); + + it("preserves optional/default/nullable source selection and envelope constraints", () => { + const schema = z.strictObject({ + sources: z.array(evidenceInputSchema).min(1).default([reference]), + absent: evidenceInputSchema.exactOptional(), + nullable: evidenceInputSchema.nullable(), + limit: z.int().positive(), + }); + const result = resolveEvidenceRequest( + "test", + { + nullable: null, + limit: 1, + }, + schema, + () => evidence, + ); + if (!result.ok) throw result.error; + expectTypeOf(result.value.input.limit).toEqualTypeOf(); + expect(result.value.input.sources).toEqual([evidence]); + expect(result.value.input.nullable).toBeNull(); + expect(result.value.input).not.toHaveProperty("absent"); + const rejected = resolveEvidenceRequest( + "test", + { + sources: [], + nullable: null, + limit: 0, + }, + schema, + ); + if (rejected.ok) throw new Error("invalid envelope was admitted"); + expect(rejected.error).toBeInstanceOf(AnalysisInputError); + }); +}); + +describe("Evidence request branches and source diagnostics", () => { + it("preserves selected union defaults and overwrites without running them twice", () => { + let refinements = 0; + const schema = z + .union([ + z.strictObject({ + kind: z.literal("selected"), + source: evidenceInputSchema, + label: z.string().overwrite((value) => value + "!"), + }), + z.strictObject({ kind: z.literal("plain"), value: z.string() }), + ]) + .superRefine(() => { + refinements += 1; + }); + const result = resolveEvidenceRequest( + "test", + { + kind: "selected", + source: reference, + label: "once", + }, + schema, + () => evidence, + ); + if (!result.ok) throw result.error; + expect(result.value.input).toEqual({ + kind: "selected", + source: evidence, + label: "once!", + }); + expect(refinements).toBe(1); + const plain = resolveEvidenceRequest( + "test", + { + kind: "plain", + value: "literal", + }, + schema, + ); + if (!plain.ok) throw plain.error; + expect(plain.value.sources).toEqual([]); + }); + + it("preserves nested optional malformed carriers for typed owner rejection", () => { + const schema = z.strictObject({ + sources: z.array( + z.strictObject({ source: evidenceInputSchema.optional() }), + ), + }); + const result = resolveEvidenceRequest( + "test", + { + sources: [{ source: null }], + }, + schema, + undefined, + { readEvidence: readImmutableEvidence }, + ); + expect(result.ok).toBe(false); + }); + + it("reports malformed reference paths without treating arbitrary objects as refs", () => { + const schema = z.strictObject({ + layers: z.array( + z.strictObject({ + source: evidenceInputSchema, + }), + ), + }); + const result = resolveEvidenceRequest( + "test", + { + layers: [{ source: { kind: "retained-evidence" } }], + }, + schema, + ); + if (result.ok) throw new Error("missing ID was accepted"); + expect(result.error).toBeInstanceOf(AnalysisInputError); + expect(result.error).toMatchObject({ + issues: [ + expect.objectContaining({ + path: ["layers", 0, "source", "evidence_id"], + }), + ], + }); + }); + + it("retains the exact missing/incorrect identity and its source path", () => { + const schema = z.strictObject({ source: evidenceInputSchema }); + const missing = resolveEvidenceRequest( + "test", + { source: reference }, + schema, + undefined, + { referenceScope: "retained-closure" }, + ); + if (missing.ok) throw new Error("missing reference was accepted"); + expect(missing.error).toBeInstanceOf(EvidenceReferenceError); + expect(missing.error).toMatchObject({ + reason: "missing", + referenceScope: "retained-closure", + diagnostics: { input_path: ["source"] }, + }); + const different = { ...reference, evidence_id: `ev_${"f".repeat(64)}` }; + const mismatched = resolveEvidenceRequest( + "test", + { source: different }, + schema, + () => evidence, + ); + if (mismatched.ok) throw new Error("mismatched record was accepted"); + expect(mismatched.error).toMatchObject({ reason: "identity_mismatch" }); + }); +}); + +describe("Evidence request borrowing and cancellation", () => { + it("keeps borrowed SDK selections untouched before service capacity admission", () => { + const schema = z.strictObject({ + source: borrowEvidenceInput(), + observations: borrowEvidenceArray().default([]), + }); + const observations = [reference, evidence]; + const parsed = getEvidenceInputContract(schema).frameSchema.parse({ + source: evidence, + observations, + }); + expect(parsed.source).toBe(evidence); + expect(parsed.observations).toBe(observations); + const result = resolveEvidenceRequest( + "test", + parsed, + schema, + () => evidence, + ); + if (!result.ok) throw result.error; + expect(result.value.input.observations).toEqual([evidence, evidence]); + expect(observations[0]).toBe(reference); + }); + + it("honors cancellation before parsing or resolving selected sources", () => { + const controller = new AbortController(); + const reason = new Error("cancelled by caller"); + controller.abort(reason); + let lookups = 0; + const result = resolveEvidenceRequest( + "test", + { source: reference }, + z.strictObject({ source: evidenceInputSchema }), + () => { + lookups += 1; + return evidence; + }, + { signal: controller.signal }, + ); + if (result.ok) throw new Error("cancelled request was admitted"); + expect(result.error.cause).toBe(reason); + expect(lookups).toBe(0); + }); + + it("observes cancellation raised by the final source owner", () => { + const controller = new AbortController(); + const reason = new Error("cancelled during source admission"); + const result = resolveEvidenceRequest( + "test", + { source: reference }, + z.strictObject({ source: evidenceInputSchema }), + () => evidence, + { + signal: controller.signal, + readEvidence: (input) => { + const read = readImmutableEvidence(input); + controller.abort(reason); + return read; + }, + }, + ); + if (result.ok) throw new Error("late source cancellation was lost"); + expect(result.error.cause).toBe(reason); + }); +}); diff --git a/src/application/InventoryProjectionEvidence.ts b/src/application/InventoryProjectionEvidence.ts index 9d25988723..2d69bd9463 100644 --- a/src/application/InventoryProjectionEvidence.ts +++ b/src/application/InventoryProjectionEvidence.ts @@ -1,3 +1,8 @@ +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "./EvidenceInputResolver.js"; +import type { ExecutionOptions } from "./AnalysisProvider.js"; import type { AnalysisInputError } from "../domain/analysisErrorCore.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { @@ -8,7 +13,10 @@ import { } from "../domain/evidence.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { ParsedInventorySet } from "../domain/artifactInventoryEvidence.js"; -import { readApplicationInventoryProjection } from "../domain/mobileApplicationGraphProjection.js"; +import { + applicationInventoryProjectionInputSchema, + readApplicationInventoryProjection, +} from "../domain/mobileApplicationGraphProjection.js"; import { ok, type Result } from "../domain/result.js"; interface InventoryProjectionResult { @@ -26,6 +34,8 @@ interface InventoryProjectionOptions< Output extends InventoryProjectionResult & JsonValue, > { readonly rawInput: unknown; + readonly lookup?: EvidenceLookup; + readonly execution?: Pick; readonly project: ( input: ParsedInventorySet, ) => Result; @@ -41,8 +51,16 @@ export const projectInventoryEvidence = < >( options: InventoryProjectionOptions, ): Result => { - const source = readApplicationInventoryProjection( + const request = resolveEvidenceRequest( + options.operation, options.rawInput, + applicationInventoryProjectionInputSchema, + options.lookup, + options.execution, + ); + if (!request.ok) return request; + const source = readApplicationInventoryProjection( + request.value.input, options.operation, ); if (!source.ok) return source; diff --git a/src/application/android/AndroidApplicationService.ts b/src/application/android/AndroidApplicationService.ts index b1bf68d23c..05a7744828 100644 --- a/src/application/android/AndroidApplicationService.ts +++ b/src/application/android/AndroidApplicationService.ts @@ -1,3 +1,5 @@ +import type { EvidenceLookup } from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { projectAndroidInventory } from "../../domain/android/androidApplication.js"; import type { Result } from "../../domain/result.js"; @@ -12,9 +14,13 @@ const OPERATION = "project_android_application_graph" as const; /** Project authenticated APK inventory Evidence into Android application Evidence. */ export const projectAndroidApplicationEvidence = ( rawInput: unknown, + lookup?: EvidenceLookup, + execution?: Pick, ): Result => { return projectInventoryEvidence({ rawInput, + ...(lookup === undefined ? {} : { lookup }), + ...(execution === undefined ? {} : { execution }), project: projectAndroidInventory, operation: OPERATION, predicateType: "rea.android-application-graph", diff --git a/src/application/apple/AppleApplicationService.ts b/src/application/apple/AppleApplicationService.ts index f15bb90af3..bee7e256cb 100644 --- a/src/application/apple/AppleApplicationService.ts +++ b/src/application/apple/AppleApplicationService.ts @@ -1,3 +1,5 @@ +import type { EvidenceLookup } from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { projectAppleInventory } from "../../domain/apple/appleApplication.js"; import type { Result } from "../../domain/result.js"; @@ -12,9 +14,13 @@ const OPERATION = "project_apple_application_graph" as const; /** Project authenticated IPA or macOS app inventory Evidence into Apple application evidence. */ export const projectAppleApplicationEvidence = ( rawInput: unknown, + lookup?: EvidenceLookup, + execution?: Pick, ): Result => { return projectInventoryEvidence({ rawInput, + ...(lookup === undefined ? {} : { lookup }), + ...(execution === undefined ? {} : { execution }), project: projectAppleInventory, operation: OPERATION, predicateType: "rea.apple-application-graph", diff --git a/src/application/artifacts/ArtifactComparisonEvidence.ts b/src/application/artifacts/ArtifactComparisonEvidence.ts index dda3bca039..94d2b7f7c4 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.ts @@ -7,7 +7,8 @@ import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewService.js"; import type { EvidenceFileReadOptions } from "../analysisView/evidenceFileSource.js"; -import type { ArtifactComparisonInput } from "../../contracts/sessionToolSchemas.js"; +import { artifactComparisonInputSchema } from "../../contracts/artifactComparisonContracts.js"; +import { parseAnalysisInput } from "../AnalysisInput.js"; import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; import { compareArtifacts, @@ -46,11 +47,18 @@ export interface ArtifactComparisonOutcome { * then resolve a summary to its exact retained parent inventory. */ export const resolveArtifactComparisonSources = async ( - input: ArtifactComparisonInput, + input: unknown, evidenceById: EvidenceLookup | undefined, options: EvidenceFileReadOptions = {}, ): Promise> => { - return resolvePairedEvidenceSources(input, evidenceById, { + const request = parseAnalysisInput( + OPERATION, + input, + artifactComparisonInputSchema, + options.signal, + ); + if (!request.ok) return request; + return resolvePairedEvidenceSources(request.value, evidenceById, { ...options, operation: OPERATION, resolveEvidence: (candidate, related, side) => { diff --git a/src/application/harmony/HarmonyApplicationService.ts b/src/application/harmony/HarmonyApplicationService.ts index 6a18cb9005..7ee6c946b5 100644 --- a/src/application/harmony/HarmonyApplicationService.ts +++ b/src/application/harmony/HarmonyApplicationService.ts @@ -1,3 +1,5 @@ +import type { EvidenceLookup } from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { projectHarmonyInventory } from "../../domain/harmony/harmonyApplication.js"; import type { Result } from "../../domain/result.js"; @@ -12,9 +14,13 @@ const OPERATION = "project_harmony_application_graph" as const; /** Project authenticated HarmonyOS package inventory Evidence into application evidence. */ export const projectHarmonyApplicationEvidence = ( rawInput: unknown, + lookup?: EvidenceLookup, + execution?: Pick, ): Result => { return projectInventoryEvidence({ rawInput, + ...(lookup === undefined ? {} : { lookup }), + ...(execution === undefined ? {} : { execution }), project: projectHarmonyInventory, operation: OPERATION, predicateType: "rea.harmony-application-graph", diff --git a/src/application/investigation/ChangedBehaviorEvidence.ts b/src/application/investigation/ChangedBehaviorEvidence.ts deleted file mode 100644 index fa301068ac..0000000000 --- a/src/application/investigation/ChangedBehaviorEvidence.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { readChangedBehaviorSelection } from "../../domain/changedBehavior.js"; -import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; -import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { err, ok, type Result } from "../../domain/result.js"; -import { - resolveEvidenceInput, - type EvidenceLookup, -} from "../EvidenceInputResolver.js"; - -/** Resolve exact selected identities; the domain still owns full-record authentication. */ -export const resolveChangedBehaviorEvidence = ( - input: unknown, - lookup: EvidenceLookup | undefined, -): Result<{ readonly comparisons: readonly unknown[] }, AnalysisError> => { - const selection = readChangedBehaviorSelection(input); - if (!selection.ok) return selection; - const comparisons: unknown[] = []; - for (const [index, item] of selection.value.entries()) { - if (typeof item !== "object" || item === null || !("kind" in item)) { - comparisons.push(item); - continue; - } - const reference = retainedEvidenceReferenceSchema.safeParse(item); - if (!reference.success) - return err( - analysisInputErrorFromIssues( - "find_changed_behavior", - reference.error.issues.map((issue) => ({ - ...issue, - path: ["comparisons", index, ...issue.path], - })), - input, - { cause: reference.error }, - ), - ); - const resolved = resolveEvidenceInput( - reference.data, - lookup, - "retained-closure", - ); - if (!resolved.ok) return resolved; - comparisons.push(resolved.value); - } - return ok({ comparisons }); -}; diff --git a/src/application/investigation/EvidenceWorkflowExecution.ts b/src/application/investigation/EvidenceWorkflowExecution.ts new file mode 100644 index 0000000000..506837033c --- /dev/null +++ b/src/application/investigation/EvidenceWorkflowExecution.ts @@ -0,0 +1,7 @@ +import type { Evidence } from "../../domain/evidence.js"; + +/** Completed workflow observation and the authenticated sources it consumed. */ +export interface EvidenceWorkflowExecution { + readonly evidence: Evidence; + readonly sources: readonly Evidence[]; +} diff --git a/src/application/investigation/FunctionComparisonEvidence.ts b/src/application/investigation/FunctionComparisonEvidence.ts new file mode 100644 index 0000000000..6d93033172 --- /dev/null +++ b/src/application/investigation/FunctionComparisonEvidence.ts @@ -0,0 +1,149 @@ +import { functionComparisonInputSchema } from "../../domain/functionComparisonSchemas.js"; +import { FUNCTION_COMPARISON_PROVIDER } from "../../domain/comparisonEvidenceIdentity.js"; +import { compareFunctions } from "../../domain/functionComparison.js"; +import { parseFunctionEvidence } from "../../domain/functionDossierEvidence.js"; +import { createEvidence } from "../../domain/evidence.js"; +import { uniqueSorted } from "../../domain/canonicalOrdering.js"; +import type { RecordUnknownInput } from "../../domain/residualUnknown.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { EvidenceWorkflowExecution } from "./EvidenceWorkflowExecution.js"; + +export interface FunctionComparisonExecution extends EvidenceWorkflowExecution { + readonly unknown: RecordUnknownInput | undefined; +} + +/** Resolve selected records once, then compare their producer-owned typed facts. */ +export const compareFunctionEvidence = ( + input: unknown, + lookup?: EvidenceLookup, + signal?: AbortSignal, +): Result => { + const selected = resolveEvidenceRequest( + "compare_functions", + input, + functionComparisonInputSchema, + lookup, + signal === undefined ? {} : { signal }, + ); + if (!selected.ok) return selected; + const left = parseFunctionEvidence(selected.value.input.left, { + operation: "compare_functions", + path: ["left"], + }); + if (!left.ok) return left; + const right = parseFunctionEvidence(selected.value.input.right, { + operation: "compare_functions", + path: ["right"], + }); + if (!right.ok) return right; + try { + const comparison = compareFunctions(left.value, right.value); + const leftEvidence = left.value.evidence; + const rightEvidence = right.value.evidence; + const leftIds = [leftEvidence.evidence_id]; + const rightIds = [rightEvidence.evidence_id]; + const evidence = createEvidence(undefined, FUNCTION_COMPARISON_PROVIDER, { + predicateType: "rea.function-comparison", + operation: "compare_functions", + parameters: { + left_evidence_id: leftEvidence.evidence_id, + right_evidence_id: rightEvidence.evidence_id, + }, + result: comparison, + confidence: "derived", + authority: "analyst-inference", + limitations: comparison.limitations, + evidenceLinks: [...leftIds, ...rightIds], + }); + return ok({ + evidence, + sources: [leftEvidence, rightEvidence], + unknown: comparisonUnknownInput( + comparison, + leftIds, + rightIds, + evidence.evidence_id, + ), + }); + } catch (cause: unknown) { + if (cause instanceof AnalysisError) return err(cause); + throw cause; + } +}; + +const comparisonUnknownInput = ( + comparison: ReturnType, + leftIds: readonly string[], + rightIds: readonly string[], + comparisonEvidenceId: string, +): RecordUnknownInput | undefined => + functionUnknownInput({ + status: comparison.status, + dimensions: comparison.dimensions + .filter(({ status }) => + comparison.status === "unknown" + ? status === "unknown" + : status === "changed", + ) + .map(({ dimension }) => dimension), + leftIds, + rightIds, + comparisonEvidenceId, + }); + +const functionUnknownInput = ({ + status, + dimensions, + leftIds, + rightIds, + comparisonEvidenceId, +}: { + status: ReturnType["status"]; + dimensions: readonly ReturnType< + typeof compareFunctions + >["dimensions"][number]["dimension"][]; + leftIds: readonly string[]; + rightIds: readonly string[]; + comparisonEvidenceId: string; +}): RecordUnknownInput | undefined => { + if (status === "unchanged") return undefined; + const supportingIds = [ + ...new Set([...leftIds, ...rightIds, comparisonEvidenceId]), + ]; + const contradictingIds = rightIds.filter((id) => !leftIds.includes(id)); + const contradictory = status === "changed"; + return { + question: `Function comparison is ${status} (comparison ${comparisonEvidenceId})`, + severity: status === "changed" ? "medium" : "high", + domain: "function-comparison", + comparison_conclusion_scope: { + comparison_evidence_id: comparisonEvidenceId, + dimensions: uniqueSorted([ + ...(status === "unknown" || dimensions.length === 0 + ? ["overall" as const] + : []), + ...dimensions, + ]), + }, + supporting_evidence_ids: contradictory + ? [...leftIds, comparisonEvidenceId] + : supportingIds, + contradicting_evidence_ids: contradictory ? contradictingIds : [], + required_authority: "shipped-artifact", + required_confidence: "observed", + required_environment: null, + recommended_probes: [ + { + operation: "analyze_function", + rationale: + "Capture complete dossiers for both functions under the same target context.", + }, + ], + relationships: [], + }; +}; diff --git a/src/application/investigation/InvestigationEvidence.ts b/src/application/investigation/InvestigationEvidence.ts index e8960462c3..2bdf36fec6 100644 --- a/src/application/investigation/InvestigationEvidence.ts +++ b/src/application/investigation/InvestigationEvidence.ts @@ -2,14 +2,22 @@ import { resolveEvidenceInput } from "../EvidenceInputResolver.js"; import type { EvidenceReader } from "./InvestigationRecordPort.js"; import { uniqueSorted } from "../../domain/canonicalOrdering.js"; import type { EvidenceIntegrityError } from "../../domain/evidenceErrors.js"; +import type { Evidence } from "../../domain/evidence.js"; import { ok, type Result } from "../../domain/result.js"; /** Follow exact session references without exporting unrelated records or Unknowns. */ export const resolveInvestigationEvidenceClosure = ( reader: Pick, seedIds: readonly string[], + sources: readonly Evidence[] = [], ): Result => { - const lookup = reader.evidenceForAnalysis.bind(reader); + // Supplied authenticated records are part of this investigation too. A + // portable inline source need not already have been inserted in the ledger. + const supplied = new Map( + sources.map((source) => [source.evidence_id, source]), + ); + const lookup = (id: string) => + supplied.get(id) ?? reader.evidenceForAnalysis(id); const visited = new Set(); const pending = [...seedIds]; while (pending.length > 0) { diff --git a/src/application/investigation/InvestigationWorkflows.ts b/src/application/investigation/InvestigationWorkflows.ts new file mode 100644 index 0000000000..ed125863d4 --- /dev/null +++ b/src/application/investigation/InvestigationWorkflows.ts @@ -0,0 +1,252 @@ +import { + callPathRequestSchema, + staticRuntimeCorrelationRequestSchema, +} from "../../contracts/investigationInputContracts.js"; +import { + changedBehaviorInputSchema, + findChangedBehavior, +} from "../../domain/changedBehavior.js"; +import { buildCallPath } from "../../domain/callPath.js"; +import { correlateStaticAndRuntime } from "../../domain/staticRuntimeCorrelation.js"; +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { readImmutableEvidence } from "../../domain/evidenceValidation.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { + CALL_PATH_PROVIDER, + CHANGED_BEHAVIOR_PROVIDER, + STATIC_RUNTIME_PROVIDER, +} from "../../domain/investigationProviders.js"; +import type { RecordUnknownInput } from "../../domain/residualUnknown.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { resolveEvidenceRequest } from "../EvidenceInputResolver.js"; +import type { EvidenceReader } from "./InvestigationRecordPort.js"; +import type { EvidenceWorkflowExecution } from "./EvidenceWorkflowExecution.js"; +import { resolveInvestigationEvidenceClosure } from "./InvestigationEvidence.js"; +import { workflowUnknownInput } from "./WorkflowUnknown.js"; + +export interface InvestigationWorkflowExecution extends EvidenceWorkflowExecution { + readonly unknown: RecordUnknownInput | undefined; +} + +type Reader = Pick; +type UnknownDescription = { + readonly question: string; + readonly domain: string; + readonly requiredAuthority: "shipped-artifact" | "controlled-replay" | null; + readonly requiredConfidence: "observed" | "derived"; + readonly probes: readonly { + readonly operation: string; + readonly rationale: string; + }[]; +}; + +const finish = ( + evidence: Evidence, + sources: readonly Evidence[], + unresolved: boolean, + description: UnknownDescription, +): Result => { + if (!unresolved) return ok({ evidence, sources, unknown: undefined }); + const unknown = workflowUnknownInput(evidence.evidence_id, { + question: description.question, + severity: "high", + domain: description.domain, + required_authority: description.requiredAuthority, + required_confidence: description.requiredConfidence, + required_environment: null, + recommended_probes: [...description.probes], + relationships: [], + }); + return unknown.ok + ? ok({ evidence, sources, unknown: unknown.value }) + : unknown; +}; + +/** Compose exact existing dossiers without starting a provider or resending payloads. */ +export const buildCallPathEvidence = ( + input: unknown, + reader: Reader, + signal?: AbortSignal, +): Result => { + const selected = resolveEvidenceRequest( + "build_call_path", + input, + callPathRequestSchema, + reader.evidenceForAnalysis.bind(reader), + { + ...(signal === undefined ? {} : { signal }), + referenceScope: "retained-closure", + readEvidence: readImmutableEvidence, + }, + ); + if (!selected.ok) return selected; + try { + const result = buildCallPath(selected.value.input); + const sources = selected.value.input.functions; + const closure = resolveInvestigationEvidenceClosure( + reader, + result.evidence_links, + sources, + ); + if (!closure.ok) return closure; + const evidence = createEvidence(undefined, CALL_PATH_PROVIDER, { + predicateType: "rea.call-path", + operation: "build_call_path", + parameters: { + start: selected.value.input.start.address, + goal: selected.value.input.goal.address, + }, + result, + confidence: "derived", + authority: "analyst-inference", + limitations: result.limitations, + evidenceLinks: closure.value, + }); + return finish(evidence, sources, result.status === "unknown", { + question: + "Can the requested call path be established from complete analysis?", + domain: "call-path", + requiredAuthority: "shipped-artifact", + requiredConfidence: "derived", + probes: [ + { + operation: "analyze_function", + rationale: + "Collect complete callee dossiers for the reported frontier addresses.", + }, + ], + }); + } catch (cause: unknown) { + if (cause instanceof AnalysisError) return err(cause); + throw cause; + } +}; + +/** Aggregate caller-selected comparisons and preserve their exact source closure. */ +export const findChangedBehaviorEvidence = ( + input: unknown, + reader: Reader, + signal?: AbortSignal, +): Result => { + const selected = resolveEvidenceRequest( + "find_changed_behavior", + input, + changedBehaviorInputSchema, + reader.evidenceForAnalysis.bind(reader), + { + ...(signal === undefined ? {} : { signal }), + referenceScope: "retained-closure", + readEvidence: readImmutableEvidence, + }, + ); + if (!selected.ok) return selected; + try { + const { result, comparisonEvidenceIds } = findChangedBehavior( + selected.value.input, + ); + const sources = selected.value.input.comparisons; + const closure = resolveInvestigationEvidenceClosure( + reader, + result.evidence_links, + sources, + ); + if (!closure.ok) return closure; + const evidence = createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, { + predicateType: "rea.changed-behavior", + operation: "find_changed_behavior", + parameters: { comparison_evidence_ids: [...comparisonEvidenceIds] }, + result, + confidence: "derived", + authority: "analyst-inference", + limitations: result.limitations, + evidenceLinks: closure.value, + }); + return finish( + evidence, + sources, + result.behavior_status === "unknown" || + result.behavior_status === "truncated", + { + question: + "Did both versions behave the same under a complete controlled replay?", + domain: "changed-behavior", + requiredAuthority: "controlled-replay", + requiredConfidence: "observed", + probes: [ + { + operation: "capture_process_scenario", + rationale: + "Capture both versions under the same bounded scenario and environment.", + }, + ], + }, + ); + } catch (cause: unknown) { + if (cause instanceof AnalysisError) return err(cause); + throw cause; + } +}; + +/** Keep correlation hypotheses explicit while resolving record transport centrally. */ +export const correlateStaticAndRuntimeEvidence = ( + input: unknown, + reader: Reader, + signal?: AbortSignal, +): Result => { + const selected = resolveEvidenceRequest( + "correlate_static_and_runtime", + input, + staticRuntimeCorrelationRequestSchema, + reader.evidenceForAnalysis.bind(reader), + { + ...(signal === undefined ? {} : { signal }), + referenceScope: "retained-closure", + readEvidence: readImmutableEvidence, + }, + ); + if (!selected.ok) return selected; + try { + const result = correlateStaticAndRuntime(selected.value.input); + const sources = [ + ...selected.value.input.static_comparisons, + ...selected.value.input.runtime_comparisons, + ]; + const closure = resolveInvestigationEvidenceClosure( + reader, + result.evidence_links, + sources, + ); + if (!closure.ok) return closure; + const evidence = createEvidence(undefined, STATIC_RUNTIME_PROVIDER, { + predicateType: "rea.static-runtime-correlation", + operation: "correlate_static_and_runtime", + parameters: { mapping_count: selected.value.input.mappings.length }, + result, + confidence: "inferred", + authority: "analyst-inference", + limitations: result.limitations, + evidenceLinks: closure.value, + }); + return finish( + evidence, + sources, + result.status === "unknown" || result.status === "truncated", + { + question: "Does runtime behavior match the available static analysis?", + domain: "static-runtime-correlation", + requiredAuthority: null, + requiredConfidence: "derived", + probes: [ + { + operation: "capture_process_scenario", + rationale: + "Repeat runtime observations and complete the mapped static comparison Evidence.", + }, + ], + }, + ); + } catch (cause: unknown) { + if (cause instanceof AnalysisError) return err(cause); + throw cause; + } +}; diff --git a/src/application/investigation/recordEvidenceSources.test.ts b/src/application/investigation/recordEvidenceSources.test.ts index 5c5bb3252f..63f15f3700 100644 --- a/src/application/investigation/recordEvidenceSources.test.ts +++ b/src/application/investigation/recordEvidenceSources.test.ts @@ -8,14 +8,20 @@ import { withEvidenceSources, } from "./recordEvidenceSources.js"; -it.each([false, true])( - "reports source recording failure without replacing earlier cancellation (cancelled=%s)", - (cancelled) => { - const source = createEvidence( - undefined, - { id: "fixture", name: "Fixture", version: "1" }, - { operation: "health", parameters: {}, result: "historical" }, - ); +const source = createEvidence( + undefined, + { id: "fixture", name: "Fixture", version: "1" }, + { operation: "health", parameters: {}, result: "historical" }, +); + +it.each([ + { cancelled: false, sourceOwner: undefined }, + { cancelled: true, sourceOwner: undefined }, + { cancelled: false, sourceOwner: "caller" as const }, + { cancelled: true, sourceOwner: "caller" as const }, +])( + "reports source recording failure without replacing earlier cancellation (cancelled=$cancelled, owner=$sourceOwner)", + ({ cancelled, sourceOwner }) => { const primary = new AnalysisCancelledError("analyze_function", { cause: "caller", }); @@ -24,9 +30,73 @@ it.each([false, true])( "analyze_function", () => err(failure), withEvidenceSources(cancelled ? err(primary) : ok("complete"), [source]), + sourceOwner === undefined ? undefined : { sourceOwner }, ); expect(result.ok).toBe(false); if (result.ok) throw new Error("Expected recording failure"); expect(result.error).toBe(cancelled ? primary : failure); + expect(primary.secondaryFailures).toEqual(cancelled ? [failure] : []); }, ); + +it.each([false, true])( + "requires retention for provider sources without a writer (cancelled=%s)", + (cancelled) => { + const primary = new AnalysisCancelledError("analyze_function", { + cause: "caller", + }); + const result = retainAnalysisResultSources( + "analyze_function", + undefined, + withEvidenceSources(cancelled ? err(primary) : ok("complete"), [source]), + ); + if (result.ok) + throw new Error("Provider sources lost their retention owner"); + if (cancelled) { + expect(result.error).toBe(primary); + expect(primary.secondaryFailures).toMatchObject([ + { _tag: "AnalysisCapabilityUnavailableError" }, + ]); + } else + expect(result.error).toMatchObject({ + _tag: "AnalysisCapabilityUnavailableError", + }); + }, +); + +it.each([false, true])( + "preserves caller sources without a writer, including late cancellation (cancelled=%s)", + (cancelled) => { + const primary = new AnalysisCancelledError("compare_wasm_interfaces", { + cause: "caller", + }); + const outcome = withEvidenceSources( + cancelled ? err(primary) : ok("complete"), + [source], + ); + const result = retainAnalysisResultSources( + "compare_wasm_interfaces", + undefined, + outcome, + { sourceOwner: "caller" }, + ); + expect(result).toBe(outcome); + expect(outcome.sourceEvidence).toEqual([source]); + expect(primary.secondaryFailures).toEqual([]); + }, +); + +it("still records caller sources when an explicit writer exists", () => { + const recorded: string[] = []; + const result = retainAnalysisResultSources( + "compare_wasm_interfaces", + (evidence) => { + recorded.push(evidence.evidence_id); + return ok("added"); + }, + withEvidenceSources(ok("complete"), [source]), + { sourceOwner: "caller" }, + ); + expect(result).toMatchObject({ ok: true, value: "complete" }); + expect(recorded).toEqual([source.evidence_id]); +}); diff --git a/src/application/investigation/recordEvidenceSources.ts b/src/application/investigation/recordEvidenceSources.ts index 0b25b761b5..b6ced118ac 100644 --- a/src/application/investigation/recordEvidenceSources.ts +++ b/src/application/investigation/recordEvidenceSources.ts @@ -18,15 +18,18 @@ export const withEvidenceSources = < ): Outcome & { readonly sourceEvidence?: readonly Evidence[] } => sources.length === 0 ? result : { ...result, sourceEvidence: sources }; -/** Never deliver links to records a plain-port adapter cannot retain. */ +/** Retain producer sources; caller-owned records remain available without a writer. */ export const retainAnalysisResultSources = ( operation: string, recordEvidence: EvidenceWriter["recordEvidence"] | undefined, result: AnalysisResultWithSources, + options: { readonly sourceOwner?: "caller" } = {}, ): Result => { const sources = result.sourceEvidence ?? []; const retained = - sources.length > 0 && recordEvidence === undefined + sources.length > 0 && + recordEvidence === undefined && + options.sourceOwner !== "caller" ? err( new AnalysisCapabilityUnavailableError( "rea", diff --git a/src/application/javascript/JavaScriptApplicationWorkflowService.ts b/src/application/javascript/JavaScriptApplicationWorkflowService.ts index 908137a855..a0c4d96a98 100644 --- a/src/application/javascript/JavaScriptApplicationWorkflowService.ts +++ b/src/application/javascript/JavaScriptApplicationWorkflowService.ts @@ -1,39 +1,39 @@ +import type { ExecutionOptions } from "../AnalysisProvider.js"; import { - resolveEvidenceInput, + resolveEvidenceRequest, type EvidenceLookup, } from "../EvidenceInputResolver.js"; -import { retainedEvidenceReferenceSchema } from "../../domain/evidenceReference.js"; -import { parseAnalysisInput } from "../AnalysisInput.js"; +import { + compareApplicationVersionsRequestSchema, + compareJavaScriptExportShapesRequestSchema, + compareSourceToBundleCliInputSchema, + compareSourceToBundleRequestSchema, + traceApplicationFeatureRequestSchema, +} from "../../contracts/javascript/applicationWorkflowInputContracts.js"; +import type { EvidenceWorkflowExecution } from "../investigation/EvidenceWorkflowExecution.js"; +import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { ResolvedSourceToBundleReference } from "./SourceToBundleReference.js"; -import { z } from "zod"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import type { Evidence } from "../../domain/evidence.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { compareJavaScriptApplicationVersions } from "../../domain/javascript/javascriptApplicationVersionComparison.js"; import { - compareApplicationVersionsInputSchema, applicationVersionIdentitiesSchema, type ApplicationVersionComparisonResult, } from "../../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { compareJavaScriptExportShapes } from "../../domain/javascript/javascriptExportShapeComparison.js"; import { - compareJavaScriptExportShapesInputSchema, javaScriptExportShapeIdentitiesSchema, type JavaScriptExportShapeComparisonResult, } from "../../domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { traceApplicationFeature } from "../../domain/javascript/javascriptFeatureTrace.js"; import { - traceApplicationFeatureInputSchema, applicationTraceIdentitiesSchema, type ApplicationFeatureTraceResult, } from "../../domain/javascript/javascriptFeatureTraceSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js"; -import { - compareSourceToBundleInputSchema, - type SourceToBundleComparisonResult, -} from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; +import type { SourceToBundleComparisonResult } from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; import { applicationGraphEvidenceInputError, parseApplicationGraphEvidence, @@ -48,68 +48,32 @@ import { } from "./JavaScriptApplicationWorkflowEvidence.js"; /** Completed workflow facts and authenticated sources retained for adapter delivery. */ -export interface ApplicationWorkflowExecution { - readonly evidence: Evidence; +export interface ApplicationWorkflowExecution< + Projection, +> extends EvidenceWorkflowExecution { readonly result: Projection; - readonly sources: readonly Evidence[]; } -// Validate the request frame without cloning source payloads before their owner reads them. -const featureTraceFrameSchema = z.strictObject({ - ...traceApplicationFeatureInputSchema.shape, - application: z.unknown().nonoptional(), - native_observations: z.array(z.unknown()).default([]), -}); -const versionComparisonFrameSchema = z.strictObject({ - ...compareApplicationVersionsInputSchema.shape, - left: z.unknown().nonoptional(), - right: z.unknown().nonoptional(), - left_native_observations: z.array(z.unknown()).default([]), - right_native_observations: z.array(z.unknown()).default([]), -}); -const exportShapeFrameSchema = z.strictObject({ - ...compareJavaScriptExportShapesInputSchema.shape, - left: z.unknown().nonoptional(), - right: z.unknown().nonoptional(), -}); - -const resolveWorkflowEvidence = ( - raw: unknown, - lookup: EvidenceLookup | undefined, -): Result => { - const reference = retainedEvidenceReferenceSchema.safeParse(raw); - return reference.success - ? resolveEvidenceInput(reference.data, lookup) - : ok(raw); -}; - -/** Resolve both selections before authenticating either graph, retaining diagnostic inputs. */ -const resolveApplicationGraphPair = ( +/** Authenticate the resolved graph pair at the owning domain boundary. */ +const parseApplicationGraphPair = ( operation: string, input: { readonly left: unknown; readonly right: unknown }, - lookup: EvidenceLookup | undefined, ): Result< { readonly left: ApplicationGraphEvidenceSource; readonly right: ApplicationGraphEvidenceSource; - readonly selected: { readonly left: unknown; readonly right: unknown }; }, AnalysisError > => { - const selectedLeft = resolveWorkflowEvidence(input.left, lookup); - if (!selectedLeft.ok) return selectedLeft; - const selectedRight = resolveWorkflowEvidence(input.right, lookup); - if (!selectedRight.ok) return selectedRight; - const left = parseApplicationGraphEvidence(selectedLeft.value, ["left"]); + const left = parseApplicationGraphEvidence(input.left, ["left"]); if (!left.ok) return err(applicationGraphEvidenceInputError(operation, left.error)); - const right = parseApplicationGraphEvidence(selectedRight.value, ["right"]); + const right = parseApplicationGraphEvidence(input.right, ["right"]); if (!right.ok) return err(applicationGraphEvidenceInputError(operation, right.error)); return ok({ left: left.value, right: right.value, - selected: { left: selectedLeft.value, right: selectedRight.value }, }); }; @@ -117,20 +81,21 @@ const resolveApplicationGraphPair = ( export const traceApplicationFeatureEvidence = ( rawInput: unknown, lookup?: EvidenceLookup, + options: Pick = {}, ): Result< ApplicationWorkflowExecution, AnalysisError > => { const operation = "trace_application_feature"; - const request = parseAnalysisInput( + const request = resolveEvidenceRequest( operation, rawInput, - featureTraceFrameSchema, + traceApplicationFeatureRequestSchema, + lookup, + options, ); if (!request.ok) return request; - const selected = resolveWorkflowEvidence(request.value.application, lookup); - if (!selected.ok) return selected; - const input = { ...request.value, application: selected.value }; + const input = request.value.input; const sourceResult = parseApplicationGraphEvidence(input.application, [ "application", ]); @@ -189,21 +154,24 @@ export const traceApplicationFeatureEvidence = ( export const compareApplicationVersionsEvidence = ( rawInput: unknown, lookup?: EvidenceLookup, + options: Pick = {}, ): Result< ApplicationWorkflowExecution, AnalysisError > => { const operation = "compare_application_versions"; - const request = parseAnalysisInput( + const request = resolveEvidenceRequest( operation, rawInput, - versionComparisonFrameSchema, + compareApplicationVersionsRequestSchema, + lookup, + options, ); if (!request.ok) return request; - const pair = resolveApplicationGraphPair(operation, request.value, lookup); + const input = request.value.input; + const pair = parseApplicationGraphPair(operation, input); if (!pair.ok) return pair; const { left, right } = pair.value; - const input = { ...request.value, ...pair.value.selected }; const leftNativeResult = parseNativeApplicationEvidence( input.left_native_observations, ["left_native_observations"], @@ -262,10 +230,47 @@ export const compareApplicationVersionsEvidence = ( }); }; -/** Compare source and bundle inputs already parsed by a trusted adapter. */ -export const compareSourceToBundleEvidence = ( - input: ResolvedSourceToBundleReference & - Pick, "application">, +/** Select the application record and historical graph through their shared owners. */ +export const executeSourceToBundleComparison = async ( + rawInput: unknown, + schema: + | typeof compareSourceToBundleCliInputSchema + | typeof compareSourceToBundleRequestSchema, + lookup: EvidenceLookup | undefined, + resources: ArtifactResourceScope, + options: Pick = {}, +): Promise< + Result< + ApplicationWorkflowExecution, + AnalysisError + > +> => { + const selected = resolveEvidenceRequest( + "compare_source_to_bundle", + rawInput, + schema, + lookup, + options, + ); + if (!selected.ok) return selected; + const { resolveSourceToBundleReference } = + await import("./SourceToBundleReference.js"); + const reference = await resolveSourceToBundleReference( + selected.value.input.reference, + options.signal, + resources, + ); + return reference.ok + ? compareSourceToBundleEvidence({ + ...reference.value, + application: selected.value.input.application, + }) + : reference; +}; + +/** Compare an owner-parsed historical graph with an unadmitted application source. */ +const compareSourceToBundleEvidence = ( + input: ResolvedSourceToBundleReference & { readonly application: unknown }, ): Result< ApplicationWorkflowExecution, AnalysisError @@ -307,21 +312,24 @@ export const compareSourceToBundleEvidence = ( export const compareJavaScriptExportShapesEvidence = ( rawInput: unknown, lookup?: EvidenceLookup, + options: Pick = {}, ): Result< ApplicationWorkflowExecution, AnalysisError > => { const operation = "compare_javascript_export_shapes"; - const request = parseAnalysisInput( + const request = resolveEvidenceRequest( operation, rawInput, - exportShapeFrameSchema, + compareJavaScriptExportShapesRequestSchema, + lookup, + options, ); if (!request.ok) return request; - const pair = resolveApplicationGraphPair(operation, request.value, lookup); + const input = request.value.input; + const pair = parseApplicationGraphPair(operation, input); if (!pair.ok) return pair; const { left, right } = pair.value; - const input = { ...request.value, ...pair.value.selected }; const identities = javaScriptExportShapeIdentitiesSchema.safeParse({ left: left.evidence, right: right.evidence, diff --git a/src/application/javascript/JavaScriptEvidenceInputs.test.ts b/src/application/javascript/JavaScriptEvidenceInputs.test.ts new file mode 100644 index 0000000000..2bd6d77197 --- /dev/null +++ b/src/application/javascript/JavaScriptEvidenceInputs.test.ts @@ -0,0 +1,195 @@ +import { expect, it, vi } from "vitest"; + +import { createEvidence, type Evidence } from "../../domain/evidence.js"; +import { + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + JAVASCRIPT_FEATURE_TRACE_EXAMPLE, +} from "../../contracts/javascript/javascriptApplicationWorkflowExamples.js"; +import { JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE } from "../../contracts/javascript/javascriptExportShapeComparisonExample.js"; +import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { + compareApplicationVersionsEvidence, + compareJavaScriptExportShapesEvidence, + traceApplicationFeatureEvidence, +} from "./JavaScriptApplicationWorkflowService.js"; +import { reconcileJavaScriptRuntimeEvidence } from "./JavaScriptRuntimeReconciliationService.js"; +import { traceJavaScriptSemanticsEvidence } from "./JavaScriptSemanticTraceService.js"; + +const reference = ({ evidence_id }: Evidence) => ({ + kind: "retained-evidence" as const, + evidence_id, +}); + +const native = createEvidence( + { path: "/example/addon.node", sha256: "e".repeat(64), format: "elf" }, + { id: "fixture", name: "Native fixture", version: "1" }, + { operation: "procedure_info", parameters: {}, result: {} }, +); + +it("resolves feature native arrays without changing source identity or unknowns", () => { + const input = { + ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + native_observations: [native], + }; + const inline = traceApplicationFeatureEvidence(input); + const records = new Map( + [input.application, native].map((record) => [record.evidence_id, record]), + ); + const lookup = vi.fn((id: string) => records.get(id)); + const retained = traceApplicationFeatureEvidence( + { + ...input, + application: reference(input.application), + native_observations: [reference(native)], + }, + lookup, + ); + if (!inline.ok) throw inline.error; + if (!retained.ok) throw retained.error; + expect(retained.value.evidence.evidence_id).toBe( + inline.value.evidence.evidence_id, + ); + expect(retained.value.result).toEqual(inline.value.result); + expect(retained.value.sources.map(({ evidence_id }) => evidence_id)).toEqual([ + input.application.evidence_id, + native.evidence_id, + ]); + expect(lookup).toHaveBeenCalledTimes(2); +}); + +it("resolves per-side native observations with one lookup per repeated exact ID", () => { + const input = { + ...JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + left_native_observations: [native], + right_native_observations: [native], + }; + const inline = compareApplicationVersionsEvidence(input); + const records = new Map( + [input.left, input.right, native].map((record) => [ + record.evidence_id, + record, + ]), + ); + const lookup = vi.fn((id: string) => records.get(id)); + const retained = compareApplicationVersionsEvidence( + { + left: reference(input.left), + right: reference(input.right), + left_native_observations: [reference(native)], + right_native_observations: [reference(native)], + }, + lookup, + ); + if (!inline.ok) throw inline.error; + if (!retained.ok) throw retained.error; + expect(retained.value.evidence.evidence_id).toBe( + inline.value.evidence.evidence_id, + ); + expect(retained.value.result).toEqual(inline.value.result); + expect(lookup).toHaveBeenCalledTimes(3); +}); + +it("uses retained graphs for exact export-shape comparison", () => { + const input = JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE; + const inline = compareJavaScriptExportShapesEvidence(input); + const records = new Map( + [input.left, input.right].map((record) => [record.evidence_id, record]), + ); + const retained = compareJavaScriptExportShapesEvidence( + { + ...input, + left: reference(input.left), + right: reference(input.right), + }, + (id) => records.get(id), + ); + if (!inline.ok) throw inline.error; + if (!retained.ok) throw retained.error; + expect(retained.value.evidence.evidence_id).toBe( + inline.value.evidence.evidence_id, + ); +}); + +it("resolves nested static layers and runtime arrays before source-owned reconciliation", () => { + const input = JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE; + const records = new Map( + [ + ...input.static_layers.map(({ analysis }) => analysis), + ...input.runtime_observations, + ].map((record) => [record.evidence_id, record]), + ); + const inline = reconcileJavaScriptRuntimeEvidence(input); + const retained = reconcileJavaScriptRuntimeEvidence( + { + ...input, + static_layers: input.static_layers.map((layer) => ({ + ...layer, + analysis: reference(layer.analysis), + })), + runtime_observations: input.runtime_observations.map(reference), + }, + (id) => records.get(id), + ); + if (!inline.ok) throw inline.error; + if (!retained.ok) throw retained.error; + expect(retained.value.evidence.evidence_id).toBe( + inline.value.evidence.evidence_id, + ); + expect( + retained.value.sources.map(({ evidence_id }) => evidence_id).sort(), + ).toEqual([...records.keys()].sort()); +}); + +it("preserves semantic-trace unknown coverage through retained sources", () => { + const application = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application; + const input = { + application, + query: { + seed: { kind: "property", name: "unobserved" }, + direction: "ownership", + }, + }; + const inline = traceJavaScriptSemanticsEvidence(input); + const retained = traceJavaScriptSemanticsEvidence( + { ...input, application: reference(application) }, + (id) => (id === application.evidence_id ? application : undefined), + ); + if (!inline.ok) throw inline.error; + if (!retained.ok) throw retained.error; + expect(retained.value.result).toEqual(inline.value.result); + expect(retained.value.sources).toEqual([application]); +}); + +it("reports missing nested references before reconciliation without guessing a source", () => { + const layer = JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers[0]; + if (layer === undefined) throw new Error("Expected an application layer"); + const result = reconcileJavaScriptRuntimeEvidence({ + ...JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, + static_layers: [{ ...layer, analysis: reference(layer.analysis) }], + }); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "EvidenceIntegrityError" }, + }); +}); + +it("honors cancellation before reading any retained graph", () => { + const controller = new AbortController(); + const reason = { request: "cancel before source resolution" }; + controller.abort(reason); + const application = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application; + const lookup = vi.fn(() => application); + const result = traceApplicationFeatureEvidence( + { + ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + application: reference(application), + }, + lookup, + { signal: controller.signal }, + ); + expect(result).toMatchObject({ + ok: false, + error: { _tag: "AnalysisCancelledError", cause: reason }, + }); + expect(lookup).not.toHaveBeenCalled(); +}); diff --git a/src/application/javascript/JavaScriptRuntimeReconciliationService.ts b/src/application/javascript/JavaScriptRuntimeReconciliationService.ts index 923d8b9b92..320c91171f 100644 --- a/src/application/javascript/JavaScriptRuntimeReconciliationService.ts +++ b/src/application/javascript/JavaScriptRuntimeReconciliationService.ts @@ -1,14 +1,21 @@ +import type { ExecutionOptions } from "../AnalysisProvider.js"; import { resolve } from "node:path"; -import { z } from "zod"; +import type { z } from "zod"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { EvidenceWorkflowExecution } from "../investigation/EvidenceWorkflowExecution.js"; +import type { EvidenceRequestFrameInput } from "../../domain/evidenceInputContract.js"; +import { reconcileJavaScriptRuntimeRequestSchema } from "../../contracts/javascript/applicationWorkflowInputContracts.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; -import type { Evidence } from "../../domain/evidence.js"; import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { readRuntimeReconciliationInput, reconcileJavaScriptRuntime, } from "../../domain/javascript/javascriptRuntimeReconciliation.js"; -import { reconcileJavaScriptRuntimeInputSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { runtimeReconciliationIdentitiesSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; import { createJavaScriptRuntimeReconciliationEvidence } from "./JavaScriptRuntimeReconciliationEvidence.js"; @@ -17,32 +24,56 @@ const OPERATION = "reconcile_javascript_runtime" as const; /** Derive a combined JAG from verified static and passive-runtime Evidence. */ export const reconcileJavaScriptRuntimeEvidence = ( rawInput: unknown, -): Result => { - const parsed = reconcileJavaScriptRuntimeInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput), - ); - return reconcileJavaScriptRuntimeEvidenceValidated(parsed.data); -}; - -/** Reconcile input already parsed by a trusted adapter boundary. */ -export const reconcileJavaScriptRuntimeEvidenceValidated = ( - parsedInput: z.output, -): Result => { + lookup?: EvidenceLookup, + options: Pick = {}, +): Result => { + const selected = resolveEvidenceRequest( + OPERATION, + rawInput, + reconcileJavaScriptRuntimeRequestSchema, + lookup, + options, + ); + if (!selected.ok) return selected; const parsed = readRuntimeReconciliationInput( - normalizeRuntimeMappingRoots(parsedInput), + normalizeRuntimeMappingRoots(selected.value.input), ); if (!parsed.ok) return parsed; + const identities = runtimeReconciliationIdentitiesSchema.safeParse({ + static_layers: parsed.value.layers.map(({ role, evidence }) => ({ + role, + analysis: evidence, + })), + runtime_observations: parsed.value.captures.map(({ evidence }) => evidence), + }); + if (!identities.success) + return err( + analysisInputErrorFromIssues( + OPERATION, + identities.error.issues, + rawInput, + ), + ); const result = reconcileJavaScriptRuntime(parsed.value); - return ok( - createJavaScriptRuntimeReconciliationEvidence(parsed.value.input, result), - ); + return ok({ + evidence: createJavaScriptRuntimeReconciliationEvidence( + parsed.value.input, + result, + ), + sources: [ + ...parsed.value.layers.map(({ evidence }) => evidence), + ...parsed.value.captures.map(({ evidence }) => evidence), + ], + }); }; +type RuntimeReconciliationFrame = EvidenceRequestFrameInput< + z.output +>; + const normalizeRuntimeMappingRoots = ( - input: z.output, -): z.output => ({ + input: RuntimeReconciliationFrame, +): RuntimeReconciliationFrame => ({ ...input, static_layers: input.static_layers.map((layer) => ({ ...layer, diff --git a/src/application/javascript/JavaScriptSemanticTraceService.ts b/src/application/javascript/JavaScriptSemanticTraceService.ts index 1b5f6f698e..0f0be04ca6 100644 --- a/src/application/javascript/JavaScriptSemanticTraceService.ts +++ b/src/application/javascript/JavaScriptSemanticTraceService.ts @@ -1,26 +1,41 @@ -import { z } from "zod"; - +import type { ExecutionOptions } from "../AnalysisProvider.js"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import { traceJavaScriptSemanticsRequestSchema } from "../../contracts/javascript/applicationWorkflowInputContracts.js"; import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { applicationGraphEvidenceInputError, parseApplicationGraphEvidence, } from "./JavaScriptApplicationEvidenceGraph.js"; -import type { Evidence } from "../../domain/evidence.js"; import { queryJavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticQuery.js"; -import { - traceJavaScriptSemanticsInputSchema, - type JavaScriptSemanticTraceResult, -} from "../../domain/javascript/javascriptSemanticTraceSchemas.js"; +import type { JavaScriptSemanticTraceResult } from "../../domain/javascript/javascriptSemanticTraceSchemas.js"; import { err, ok, type Result } from "../../domain/result.js"; import { createJavaScriptSemanticTraceEvidence } from "./JavaScriptApplicationWorkflowEvidence.js"; +import type { ApplicationWorkflowExecution } from "./JavaScriptApplicationWorkflowService.js"; const OPERATION = "trace_javascript_semantics" as const; -/** Trace semantic relations from input already parsed by a trusted adapter. */ -export const traceJavaScriptSemanticsEvidenceValidated = ( - input: z.output, -): Result => { +/** Resolve and authenticate one application source before tracing semantic relations. */ +export const traceJavaScriptSemanticsEvidence = ( + rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, +): Result< + ApplicationWorkflowExecution, + AnalysisError +> => { + const request = resolveEvidenceRequest( + OPERATION, + rawInput, + traceJavaScriptSemanticsRequestSchema, + lookup, + options, + ); + if (!request.ok) return request; + const input = request.value.input; const sourceResult = parseApplicationGraphEvidence(input.application, [ "application", ]); @@ -36,7 +51,7 @@ export const traceJavaScriptSemanticsEvidenceValidated = ( path: ["application"], reason: "invalid_value", expected: - "inline Evidence from analyze_javascript_application with a semantic graph", + "Evidence from analyze_javascript_application with a semantic graph", }, ]), ); @@ -47,16 +62,15 @@ export const traceJavaScriptSemanticsEvidenceValidated = ( evidence_links: [source.evidence.evidence_id], }; const { allowed_relations, ...queryParameters } = input.query; - return ok( - createJavaScriptSemanticTraceEvidence( - { - application_evidence_id: source.evidence.evidence_id, - query: - allowed_relations === undefined - ? queryParameters - : { ...queryParameters, allowed_relations }, - }, - result, - ), + const evidence = createJavaScriptSemanticTraceEvidence( + { + application_evidence_id: source.evidence.evidence_id, + query: + allowed_relations === undefined + ? queryParameters + : { ...queryParameters, allowed_relations }, + }, + result, ); + return ok({ evidence, result, sources: [source.evidence] }); }; diff --git a/src/application/managed/ManagedApplicationGraphService.ts b/src/application/managed/ManagedApplicationGraphService.ts index f92d08fe5f..5f401a45b0 100644 --- a/src/application/managed/ManagedApplicationGraphService.ts +++ b/src/application/managed/ManagedApplicationGraphService.ts @@ -1,3 +1,8 @@ +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { createEvidence, @@ -5,6 +10,7 @@ import { type EvidenceSubjectTarget, } from "../../domain/evidence.js"; import { + managedApplicationGraphReferenceInputSchema, projectManagedGraph, readManagedGraphProjection, type ManagedApplicationGraphResult, @@ -19,12 +25,30 @@ const OPERATION = "project_managed_application_graph" as const; /** Project authenticated managed Evidence into a provider-neutral graph. */ export const projectManagedApplicationGraphEvidence = ( rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, ): Result => { - const request = readManagedGraphProjection(rawInput); + const request = resolveManagedGraphEvidenceInput(rawInput, lookup, options); if (!request.ok) return request; return ok(projectManagedGraphEvidence(request.value)); }; +/** Resolve selected records before the managed owner authenticates and joins them. */ +export const resolveManagedGraphEvidenceInput = ( + rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, +): Result => { + const request = resolveEvidenceRequest( + OPERATION, + rawInput, + managedApplicationGraphReferenceInputSchema, + lookup, + options, + ); + return request.ok ? readManagedGraphProjection(request.value.input) : request; +}; + /** Project the workflow owner's authenticated, identity-checked source facts. */ export const projectManagedGraphEvidence = ( request: ManagedGraphProjection, diff --git a/src/application/managed/ManagedMemberComparisonService.ts b/src/application/managed/ManagedMemberComparisonService.ts index 2d1184932a..a324518702 100644 --- a/src/application/managed/ManagedMemberComparisonService.ts +++ b/src/application/managed/ManagedMemberComparisonService.ts @@ -1,8 +1,12 @@ -import { parseAnalysisInput } from "../AnalysisInput.js"; -import { z } from "zod"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import { compareManagedMembers, + compareManagedMembersRequestSchema, type ManagedMemberComparisonResult, } from "../../domain/managed/managedMemberComparison.js"; import { readManagedMemberEvidence } from "../../domain/managed/managedMemberComparisonMatch.js"; @@ -31,11 +35,6 @@ import { MANAGED_WORKFLOW_PROVIDER, } from "../../domain/investigationProviders.js"; -const managedMemberComparisonRequestSchema = z.strictObject({ - left: z.unknown(), - right: z.unknown(), -}); - export interface ManagedMemberComparisonExecution { readonly evidence: Evidence; readonly summary: ManagedMemberComparisonResult["summary"]; @@ -45,17 +44,21 @@ export interface ManagedMemberComparisonExecution { /** Authenticate both source records, then compare their parsed observations. */ export const executeManagedMemberComparison = ( input: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, ): Result => { const operation = "compare_managed_members"; - const request = parseAnalysisInput( + const request = resolveEvidenceRequest( operation, input, - managedMemberComparisonRequestSchema, + compareManagedMembersRequestSchema, + lookup, + options, ); if (!request.ok) return request; - const left = readManagedMemberEvidence(request.value.left, "left"); + const left = readManagedMemberEvidence(request.value.input.left, "left"); if (!left.ok) return left; - const right = readManagedMemberEvidence(request.value.right, "right"); + const right = readManagedMemberEvidence(request.value.input.right, "right"); if (!right.ok) return right; if (left.value.evidence.evidence_id === right.value.evidence.evidence_id) return err( diff --git a/src/application/managed/ManagedNativeVerificationService.ts b/src/application/managed/ManagedNativeVerificationService.ts index 87f99abcdc..6842714e1a 100644 --- a/src/application/managed/ManagedNativeVerificationService.ts +++ b/src/application/managed/ManagedNativeVerificationService.ts @@ -1,3 +1,8 @@ +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { AnalysisError } from "../../domain/analysisErrorBase.js"; import { createEvidence, type Evidence } from "../../domain/evidence.js"; import { ok, type Result } from "../../domain/result.js"; @@ -9,7 +14,10 @@ import { readManagedNativeVerificationSource, type ManagedNativeVerificationSource, } from "../../domain/managed/managedNativeVerificationSource.js"; -import type { ManagedNativeVerificationResult } from "../../domain/managed/managedNativeVerificationSchemas.js"; +import { + managedNativeVerificationRequestSchema, + type ManagedNativeVerificationResult, +} from "../../domain/managed/managedNativeVerificationSchemas.js"; import { MANAGED_WORKFLOW_PROVIDER } from "../../domain/investigationProviders.js"; const OPERATION = "verify_managed_native_boundaries" as const; @@ -24,8 +32,18 @@ export interface ManagedNativeVerificationExecution { /** Resolve untrusted source Evidence, then execute over the trusted parsed source. */ export const executeManagedNativeBoundaries = ( rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, ): Result => { - const source = readManagedNativeVerificationSource(rawInput); + const request = resolveEvidenceRequest( + OPERATION, + rawInput, + managedNativeVerificationRequestSchema, + lookup, + options, + ); + if (!request.ok) return request; + const source = readManagedNativeVerificationSource(request.value.input); if (!source.ok) return source; const result = verifyManagedNativeVerificationSource(source.value); const evidence = createManagedNativeVerificationEvidence( diff --git a/src/application/managed/ManagedReconstructionService.ts b/src/application/managed/ManagedReconstructionService.ts index 27c2e6092a..f2578876ea 100644 --- a/src/application/managed/ManagedReconstructionService.ts +++ b/src/application/managed/ManagedReconstructionService.ts @@ -10,21 +10,29 @@ import { } from "../../domain/managed/managedReconstruction.js"; import { ok, type Result } from "../../domain/result.js"; import { MANAGED_WORKFLOW_PROVIDER } from "../../domain/investigationProviders.js"; -import { parseAnalysisInput } from "../AnalysisInput.js"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../AnalysisProvider.js"; const OPERATION = "import_managed_reconstruction" as const; /** Validate the request envelope and authenticate its managed Evidence source once. */ export const resolveManagedReconstructionEvidenceInput = ( rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, ): Result => { - const parsed = parseAnalysisInput( + const parsed = resolveEvidenceRequest( OPERATION, rawInput, managedReconstructionImportRequestSchema, + lookup, + options, ); if (!parsed.ok) return parsed; - return resolveManagedReconstructionImport(parsed.value); + return resolveManagedReconstructionImport(parsed.value.input); }; /** Execute from the source facts returned by the resolver. */ @@ -38,8 +46,14 @@ export const importManagedReconstructionEvidenceResolved = ( /** Validate the raw request envelope, then authenticate and import its source. */ export const importManagedReconstructionEvidence = ( rawInput: unknown, + lookup?: EvidenceLookup, + options: Pick = {}, ): Result => { - const resolved = resolveManagedReconstructionEvidenceInput(rawInput); + const resolved = resolveManagedReconstructionEvidenceInput( + rawInput, + lookup, + options, + ); if (!resolved.ok) return resolved; return importManagedReconstructionEvidenceResolved(resolved.value); }; diff --git a/src/application/process/ProcessCli.ts b/src/application/process/ProcessCli.ts index fea70d23bf..737c4c0f2d 100644 --- a/src/application/process/ProcessCli.ts +++ b/src/application/process/ProcessCli.ts @@ -9,21 +9,24 @@ import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import type { AnalysisErrorProjection } from "../../domain/analysisErrorProjection.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { describeValidationFailure } from "../../domain/validationFailure.js"; +import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; import { analysisInputErrorFromIssues, projectInputIssues, } from "../../domain/inputIssueProjection.js"; import { processTraceSpecificationSchema } from "../../domain/process/processTraceSpecification.js"; import { processScenarioSchema } from "../../domain/process/processScenario.js"; -import { compareProcessCaptureEvidence } from "../../domain/process/processComparisonEvidence.js"; +import { + compareParsedProcessEvidence, + readProcessComparisonSource, +} from "./ProcessComparisonService.js"; import { createProgressReporter } from "../ProgressReporter.js"; import { captureProcessScenario } from "../../process/capture/ProcessHarness.js"; import { ProcessCaptureResourceScope } from "../../process/capture/ProcessCaptureLifecycle.js"; import { ProcessCaptureError } from "../../process/capture/ProcessCaptureError.js"; -import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; import { createProcessCaptureEvidence, - parseProcessCaptureEvidence, + type ProcessCaptureEvidenceFailure, } from "./ProcessEvidence.js"; import { readJsonFile } from "../JsonFiles.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -99,14 +102,30 @@ export const compareProcessEvidenceFiles = async ( signal?: AbortSignal, ) => withProcessInputResources("compare_process_captures", async (resources) => { - const left = parseCaptureEvidence( - await readJson(leftPath, "compare_process_captures", signal, resources), + const leftInput = await readJson( + leftPath, + "compare_process_captures", + signal, + resources, + ); + const left = readProcessComparisonSource( "left", + leftInput, + projectCaptureFileFailure, ); - const right = parseCaptureEvidence( - await readJson(rightPath, "compare_process_captures", signal, resources), + if (!left.ok) throw left.error; + const rightInput = await readJson( + rightPath, + "compare_process_captures", + signal, + resources, + ); + const right = readProcessComparisonSource( "right", + rightInput, + projectCaptureFileFailure, ); + if (!right.ok) throw right.error; const traceSpecification = traceSpecPath === undefined ? undefined @@ -118,34 +137,38 @@ export const compareProcessEvidenceFiles = async ( resources, ), ); - return compareProcessCaptureEvidence( - left, - right, + const result = compareParsedProcessEvidence( + left.value, + right.value, traceSpecification === undefined ? {} : { traceSpecification }, - ).evidence; + ); + if (!result.ok) throw result.error; + return result.value.evidence; }); -const parseCaptureEvidence = (input: unknown, side: "left" | "right") => { - const parsed = parseProcessCaptureEvidence(input); - if (parsed.ok) return parsed.value; - const failure = parsed.error; +/** Preserve CLI file diagnostics while sharing all source interpretation. */ +const projectCaptureFileFailure = ( + failure: ProcessCaptureEvidenceFailure, + side: "left" | "right", + input: unknown, +): AnalysisError => { switch (failure.kind) { case "invalid-evidence": - throw analysisInputErrorFromIssues( + return analysisInputErrorFromIssues( "compare_process_captures", failure.cause.issues, input, { cause: failure.cause }, ); case "wrong-workflow": - throw processInputFailure( + return processInputFailure( "compare_process_captures", "Capture evidence is not from the current process-capture workflow. Create new capture evidence, then try again.", [side], "invalid_value", ); case "unsupported-provider": - throw new AnalysisInputError("compare_process_captures", undefined, [ + return new AnalysisInputError("compare_process_captures", undefined, [ { path: [side, "provider"], reason: "invalid_value", @@ -153,7 +176,7 @@ const parseCaptureEvidence = (input: unknown, side: "left" | "right") => { }, ]); case "invalid-capture": - throw invalidCaptureEvidence(failure.cause, side); + return invalidCaptureEvidence(failure.cause, side); } }; diff --git a/src/application/process/ProcessComparisonService.ts b/src/application/process/ProcessComparisonService.ts new file mode 100644 index 0000000000..f2fc3a13ba --- /dev/null +++ b/src/application/process/ProcessComparisonService.ts @@ -0,0 +1,188 @@ +import { processComparisonInputSchema } from "../../contracts/processComparisonContracts.js"; +import { AnalysisError } from "../../domain/analysisErrorBase.js"; +import { AnalysisInputError } from "../../domain/analysisErrorCore.js"; +import { describeValidationFailure } from "../../domain/validationFailure.js"; +import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js"; +import { + PROCESS_COMPARISON_DIMENSIONS, + type ProcessCaptureComparison, +} from "../../domain/process/processComparison.js"; +import { + compareProcessCaptureEvidence, + type ParsedProcessCaptureEvidence, +} from "../../domain/process/processComparisonEvidence.js"; +import { PROCESS_PROVIDER } from "../../domain/process/processEvidenceProvider.js"; +import type { RecordUnknownInput } from "../../domain/residualUnknown.js"; +import { err, ok, type Result } from "../../domain/result.js"; +import { + parseProcessCaptureEvidence, + type ProcessCaptureEvidenceFailure, +} from "./ProcessEvidence.js"; +import { + resolveEvidenceRequest, + type EvidenceLookup, +} from "../EvidenceInputResolver.js"; +import type { EvidenceWorkflowExecution } from "../investigation/EvidenceWorkflowExecution.js"; + +interface ProcessComparisonExecution extends EvidenceWorkflowExecution { + readonly unknown: RecordUnknownInput | undefined; +} + +/** Shared exact-source selection and process-capture interpretation for CLI/MCP. */ +export const compareProcessEvidence = ( + input: unknown, + lookup?: EvidenceLookup, + options: { + readonly signal?: AbortSignal; + readonly now?: () => number; + } = {}, +): Result => { + const selected = resolveEvidenceRequest( + "compare_process_captures", + input, + processComparisonInputSchema, + lookup, + options, + ); + if (!selected.ok) return selected; + const request = selected.value.input; + const left = readProcessComparisonSource("left", request.left); + if (!left.ok) return left; + const right = readProcessComparisonSource("right", request.right); + if (!right.ok) return right; + return compareParsedProcessEvidence(left.value, right.value, { + ...(request.max_capture_age_ms === undefined + ? {} + : { maxCaptureAgeMs: request.max_capture_age_ms }), + ...(request.trace_spec === undefined + ? {} + : { traceSpecification: request.trace_spec }), + ...(options.now === undefined ? {} : { now: options.now }), + }); +}; + +/** Compare authenticated facts without reinterpreting or copying source carriers. */ +export const compareParsedProcessEvidence = ( + left: ParsedProcessCaptureEvidence, + right: ParsedProcessCaptureEvidence, + options: Parameters[2] = {}, +): Result => { + try { + const { comparison, evidence } = compareProcessCaptureEvidence( + left, + right, + options, + ); + return ok({ + evidence, + sources: [left.record, right.record], + unknown: comparisonUnknownInput( + { + left_evidence_id: left.record.evidence_id, + right_evidence_id: right.record.evidence_id, + comparison_evidence_id: evidence.evidence_id, + }, + comparison, + ), + }); + } catch (cause: unknown) { + if (cause instanceof AnalysisError) return err(cause); + throw cause; + } +}; + +/** Authenticate each selected capture before a caller admits its next source. */ +export const readProcessComparisonSource = ( + side: "left" | "right", + value: unknown, + projectFailure?: ( + failure: ProcessCaptureEvidenceFailure, + side: "left" | "right", + input: unknown, + ) => AnalysisError, +): Result => { + const operation = "compare_process_captures"; + const parsed = parseProcessCaptureEvidence(value); + if (parsed.ok) return parsed; + const failure = parsed.error; + if (projectFailure !== undefined) + return err(projectFailure(failure, side, value)); + switch (failure.kind) { + case "invalid-evidence": + return err( + new EvidenceIntegrityError(`${side} Evidence failed validation`, { + cause: failure.cause, + userMessage: `The ${side} Evidence failed validation (${describeValidationFailure(failure.cause)}). Supply unmodified capture Evidence from capture_process_scenario.`, + }), + ); + case "wrong-workflow": + return err( + new AnalysisInputError(operation, undefined, [ + { + path: [side], + reason: "invalid_value", + message: `Expected process capture Evidence (operation ${failure.expected.operation}, predicate ${failure.expected.predicate}); the ${side} Evidence has operation ${failure.actual.operation} and predicate ${failure.actual.predicate}.`, + }, + ]), + ); + case "unsupported-provider": + return err( + new AnalysisInputError(operation, undefined, [ + { + path: [side, "provider"], + reason: "invalid_value", + message: `The ${side} Evidence provider ${failure.provider.id} v${failure.provider.version} (${failure.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, + }, + ]), + ); + case "invalid-capture": + return err( + new EvidenceIntegrityError( + `${side} process capture failed validation`, + { + cause: failure.cause, + userMessage: `The ${side} Evidence has an invalid process capture result (${describeValidationFailure(failure.cause)}). Recreate the capture with capture_process_scenario.`, + }, + ), + ); + } +}; + +const comparisonUnknownInput = ( + parsed: { + readonly left_evidence_id: string; + readonly right_evidence_id: string; + readonly comparison_evidence_id: string; + }, + comparison: ProcessCaptureComparison, +): RecordUnknownInput | undefined => { + if (comparison.status === "unchanged") return undefined; + const differingScopes = PROCESS_COMPARISON_DIMENSIONS.filter( + (dimension) => comparison[dimension] !== "unchanged", + ).join(", "); + const contradictory = + comparison.status === "changed" && + parsed.left_evidence_id !== parsed.right_evidence_id; + // Unknown identity uses the question, not its supporting records. Bind the + // question to this comparison so distinct capture pairs and policies coexist. + return { + question: `${contradictory ? "Process captures disagree" : `Process comparison is ${comparison.status}`} across: ${differingScopes} (comparison ${parsed.comparison_evidence_id})`, + severity: "high", + domain: "process-comparison", + supporting_evidence_ids: contradictory + ? [parsed.left_evidence_id] + : [...new Set([parsed.left_evidence_id, parsed.right_evidence_id])], + contradicting_evidence_ids: contradictory ? [parsed.right_evidence_id] : [], + required_authority: "controlled-replay", + required_confidence: "observed", + required_environment: null, + recommended_probes: [ + { + operation: "capture_process_scenario", + rationale: + "Repeat both scenarios under the same controlled environment.", + }, + ], + relationships: [], + }; +}; diff --git a/src/application/wasm/WasmInterfaceComparisonService.test.ts b/src/application/wasm/WasmInterfaceComparisonService.test.ts index 41917bcfc0..14cd133185 100644 --- a/src/application/wasm/WasmInterfaceComparisonService.test.ts +++ b/src/application/wasm/WasmInterfaceComparisonService.test.ts @@ -4,6 +4,8 @@ import { createEvidence, parseEvidence } from "../../domain/evidence.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { wasmInterfaceComparisonSchema } from "../../contracts/wasm/wasmComparisonContracts.js"; import { compareWasmInterfaces } from "./WasmInterfaceComparisonService.js"; +import { retainAnalysisResultSources } from "../investigation/recordEvidenceSources.js"; +import { ok } from "../../domain/result.js"; import { WASM_INTERFACE_REPRESENTATION_BYTES, wasmInterfaceEntryRepresentationBytes, @@ -57,7 +59,9 @@ const compare = async ( ) => { const result = await compareWasmInterfaces({ left, right, scope: "both" }); if (!result.ok) throw result.error; - return wasmInterfaceComparisonSchema.parse(result.value.normalized_result); + return wasmInterfaceComparisonSchema.parse( + result.value.evidence.normalized_result, + ); }; it("reports all five comparison states, including duplicate ambiguity and explicit selected scope", async () => { @@ -93,7 +97,7 @@ it("reports all five comparison states, including duplicate ambiguity and explic scope: "imports", }); if (!selected.ok) throw selected.error; - expect(selected.value.normalized_result).toMatchObject({ + expect(selected.value.evidence.normalized_result).toMatchObject({ changes: [], coverage: "complete-within-inputs", }); @@ -180,7 +184,7 @@ it("resolves exact retained references and permits cancellation between comparis id === left.evidence_id ? left : right, ); if (!result.ok) throw result.error; - expect(result.value.evidence_links).toEqual([ + expect(result.value.evidence.evidence_links).toEqual([ left.evidence_id, right.evidence_id, ]); @@ -315,6 +319,18 @@ it("retains the complete comparison when cancelled at the final handoff", async if (result.ok) throw new Error("Expected cancellation during final handoff"); expect(result.error).toMatchObject({ _tag: "AnalysisCancelledError" }); expect(result.error.cause).toBe(reason); + expect(result.sourceEvidence).toEqual([left, right]); + const retainedSources: string[] = []; + const retained = retainAnalysisResultSources( + "compare_wasm_interfaces", + (source) => { + retainedSources.push(source.evidence_id); + return ok("added"); + }, + result, + ); + expect(retained.ok).toBe(false); + expect(retainedSources).toEqual([left.evidence_id, right.evidence_id]); const partial = parseEvidence(result.error.partialObservation); expect(partial).toMatchObject({ operation: "compare_wasm_interfaces", diff --git a/src/application/wasm/WasmInterfaceComparisonService.ts b/src/application/wasm/WasmInterfaceComparisonService.ts index f7b51d4fd0..a71b6e5491 100644 --- a/src/application/wasm/WasmInterfaceComparisonService.ts +++ b/src/application/wasm/WasmInterfaceComparisonService.ts @@ -22,16 +22,20 @@ import { } from "../../domain/analysisErrorCore.js"; import { fixedAllowanceOptions } from "../../domain/fixedAllowanceRemediation.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; -import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js"; import { err, ok, type Result } from "../../domain/result.js"; import { - resolvePairedEvidenceRequest, + resolveEvidenceRequest, type EvidenceLookup, } from "../EvidenceInputResolver.js"; import type { ExecutionOptions } from "../AnalysisProvider.js"; import type { z } from "zod"; import { safeParseEvidence } from "../../domain/evidenceValidation.js"; import { completeProviderEvidence } from "../ProviderOperationExecution.js"; +import type { EvidenceWorkflowExecution } from "../investigation/EvidenceWorkflowExecution.js"; +import { + withEvidenceSources, + type AnalysisResultWithSources, +} from "../investigation/recordEvidenceSources.js"; const OPERATION = "compare_wasm_interfaces"; const parseInspection = ( @@ -81,17 +85,18 @@ export const compareWasmInterfaces = async ( rawInput: unknown, options?: ExecutionOptions, lookup?: EvidenceLookup, -): Promise> => { +): Promise> => { if (options?.signal?.aborted) return err( new AnalysisCancelledError(OPERATION, { cause: options.signal.reason }), ); - const parsed = compareWasmInterfacesInputSchema.safeParse(rawInput); - if (!parsed.success) - return err( - analysisInputErrorFromIssues(OPERATION, parsed.error.issues, rawInput), - ); - const resolved = resolvePairedEvidenceRequest(parsed.data, lookup); + const resolved = resolveEvidenceRequest( + OPERATION, + rawInput, + compareWasmInterfacesInputSchema, + lookup, + options, + ); if (!resolved.ok) return resolved; const assertActive = () => { if (options?.signal?.aborted) @@ -102,13 +107,13 @@ export const compareWasmInterfaces = async ( try { await setImmediate(); assertActive(); - const parsedLeft = parseInspection(resolved.value.left); + const parsedLeft = parseInspection(resolved.value.input.left); if (!parsedLeft.ok) return parsedLeft; - const parsedRight = parseInspection(resolved.value.right); + const parsedRight = parseInspection(resolved.value.input.right); if (!parsedRight.ok) return parsedRight; const left = parsedLeft.value; const right = parsedRight.value; - const scope = parsed.data.scope; + const scope = resolved.value.input.scope; const selected = (entry: WasmInterfaceEntry): boolean => scope === "both" || entry.direction === (scope === "imports" ? "import" : "export"); @@ -270,7 +275,12 @@ export const compareWasmInterfaces = async ( evidenceLinks: [left.evidence.evidence_id, right.evidence.evidence_id], }); await setImmediate(); - return completeProviderEvidence(evidence, options?.signal); + const completed = completeProviderEvidence(evidence, options?.signal); + const sources = [left.evidence, right.evidence]; + return withEvidenceSources( + completed.ok ? ok({ evidence: completed.value, sources }) : completed, + sources, + ); } catch (cause: unknown) { if (cause instanceof AnalysisError) return err(cause); throw cause; diff --git a/src/cli/applicationCommands.ts b/src/cli/applicationCommands.ts index a52bf7af06..e7a64f5048 100644 --- a/src/cli/applicationCommands.ts +++ b/src/cli/applicationCommands.ts @@ -7,9 +7,16 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; import type { JsonValue } from "../domain/jsonValue.js"; import type { Logger } from "pino"; -import { traceJavaScriptSemanticsInputSchema } from "../domain/javascript/javascriptSemanticTraceSchemas.js"; import { registerSourceToBundleCommand } from "./sourceToBundleCommand.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import type { Evidence } from "../domain/evidence.js"; +import type { Result } from "../domain/result.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; +import { withCommandCancellation } from "./commandCancellation.js"; type CliInstance = ReturnType; @@ -18,103 +25,114 @@ export const registerApplicationCommands = ( cli: ReturnType, logger: Logger, ): void => { - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.traceApplicationFeature, + operation: "trace_application_feature", description: "Trace a typed seed through authenticated application Evidence JSON", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { traceApplicationFeatureEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - const result = traceApplicationFeatureEvidence(input); + const result = traceApplicationFeatureEvidence(input, lookup, { signal }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.traceJavaScriptSemantics, + operation: "trace_javascript_semantics", description: "Trace bounded static semantic relations through authenticated application Evidence", - inputSchema: traceJavaScriptSemanticsInputSchema, - workflow: async (input) => { - const { traceJavaScriptSemanticsEvidenceValidated } = + workflow: async (input, lookup, signal) => { + const { traceJavaScriptSemanticsEvidence } = await import("../application/javascript/JavaScriptSemanticTraceService.js"); - return traceJavaScriptSemanticsEvidenceValidated(input); + const result = traceJavaScriptSemanticsEvidence(input, lookup, { + signal, + }); + return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.compareApplicationVersions, + operation: "compare_application_versions", description: "Compare two authenticated JavaScript Application Graph versions", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { compareApplicationVersionsEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - const result = compareApplicationVersionsEvidence(input); + const result = compareApplicationVersionsEvidence(input, lookup, { + signal, + }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); registerSourceToBundleCommand(cli, logger); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.compareJavaScriptExportShapes, + operation: "compare_javascript_export_shapes", description: "Compare exact static JavaScript export return shapes without execution", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { compareJavaScriptExportShapesEvidence } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - const result = compareJavaScriptExportShapesEvidence(input); + const result = compareJavaScriptExportShapesEvidence(input, lookup, { + signal, + }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); registerObligationLedgerCommand(cli, logger); registerCoverageCommand(cli, logger); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.projectAndroidApplicationGraph, + operation: "project_android_application_graph", description: "Project authenticated APK inventory Evidence into an Android application graph", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { projectAndroidApplicationEvidence } = await import("../application/android/AndroidApplicationService.js"); - const result = projectAndroidApplicationEvidence(input); + const result = projectAndroidApplicationEvidence(input, lookup, { + signal, + }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.projectAppleApplicationGraph, + operation: "project_apple_application_graph", description: "Project authenticated IPA or macOS app inventory Evidence into an Apple application graph", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { projectAppleApplicationEvidence } = await import("../application/apple/AppleApplicationService.js"); - const result = projectAppleApplicationEvidence(input); + const result = projectAppleApplicationEvidence(input, lookup, { signal }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); - registerJsonCommand({ + registerEvidenceJsonCommand({ cli, logger, name: CLI_COMMANDS.projectHarmonyApplicationGraph, + operation: "project_harmony_application_graph", description: "Project authenticated HarmonyOS HAP, HSP, or App Pack inventory Evidence into an application graph", - inputSchema: z.unknown(), - workflow: async (input) => { + workflow: async (input, lookup, signal) => { const { projectHarmonyApplicationEvidence } = await import("../application/harmony/HarmonyApplicationService.js"); - const result = projectHarmonyApplicationEvidence(input); + const result = projectHarmonyApplicationEvidence(input, lookup, { + signal, + }); return result.ok ? { ok: true, value: result.value.evidence } : result; }, }); @@ -205,3 +223,51 @@ const registerJsonCommand = ({ }), }); }; + +/** Evidence consumers share one explicit portable source and cancellation lifetime. */ +const registerEvidenceJsonCommand = ({ + cli, + logger, + name, + operation, + description, + workflow, +}: { + readonly cli: CliInstance; + readonly logger: Logger; + readonly name: string; + readonly operation: string; + readonly description: string; + readonly workflow: ( + input: unknown, + lookup: EvidenceLookup | undefined, + signal: AbortSignal, + ) => Promise>; +}): void => { + cli.command(name, { + description, + args: z.object({ + inputJson: z.string().describe("Inline workflow JSON or JSON file path"), + }), + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, name, async () => { + const input = await parseCliJsonInput(args.inputJson, name, signal); + if (!input.ok) return input.error; + const result = await runEvidenceInputCommand( + operation, + options.evidenceBundle, + signal, + (lookup) => workflow(input.value, lookup, signal), + ); + return result.ok + ? result.value + : { + error: "Application workflow failed", + ...result.error, + }; + }), + ), + }); +}; diff --git a/src/cli/artifactComparisonCommand.ts b/src/cli/artifactComparisonCommand.ts index ce2e44357e..804a338525 100644 --- a/src/cli/artifactComparisonCommand.ts +++ b/src/cli/artifactComparisonCommand.ts @@ -9,10 +9,7 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { parseCliJsonInput, resolveCliJsonPaths } from "../cliJsonInput.js"; import { logCliCommand } from "../cliLogging.js"; -import { artifactComparisonInputSchema } from "../contracts/sessionToolSchemas.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { ARTIFACT_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js"; -import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import type { CliInstance } from "./types.js"; @@ -45,23 +42,11 @@ export const registerArtifactComparisonCommand = ( ["left", "path"], ["right", "path"], ]); - const parsed = artifactComparisonInputSchema.safeParse(value); - if (!parsed.success) - return { - error: "Application workflow failed", - ...projectAnalysisError( - analysisInputErrorFromIssues( - OPERATION, - parsed.error.issues, - value, - ), - ), - }; const resources = new ArtifactResourceScope(); const result = await finishAnalysisCommand( async () => { const sources = await resolveArtifactComparisonSources( - parsed.data, + value, undefined, { signal, resources }, ); diff --git a/src/cli/electronCommands.ts b/src/cli/electronCommands.ts index c066ccea5e..dd2604239a 100644 --- a/src/cli/electronCommands.ts +++ b/src/cli/electronCommands.ts @@ -32,6 +32,10 @@ import { withCommandCancellation } from "./commandCancellation.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import type { ElectronActiveObservationPort } from "../application/javascript/ElectronActiveObservationPort.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; /** Register CLI equivalents of the Electron MCP tools. */ export const registerElectronCommands = ( @@ -182,23 +186,41 @@ const registerJavaScriptRuntimeReconciliationCommand = ( "Inline JSON or a JSON file path matching reconcile_javascript_runtime", ), }), - run: ({ args }) => - logCliCommand( - logger, - CLI_COMMANDS.reconcileJavaScriptRuntime, - async () => { - const input = await parseCliJsonInput( - args.inputJson, - "reconcile_javascript_runtime", - ); - if (!input.ok) return input.error; - const { reconcileJavaScriptRuntimeEvidence } = - await import("../application/javascript/JavaScriptRuntimeReconciliationService.js"); - const result = reconcileJavaScriptRuntimeEvidence(input.value); - return result.ok - ? result.value - : cliError(result.error, "Application workflow failed"); - }, + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand( + logger, + CLI_COMMANDS.reconcileJavaScriptRuntime, + async () => { + const input = await parseCliJsonInput( + args.inputJson, + "reconcile_javascript_runtime", + signal, + ); + if (!input.ok) return input.error; + const { reconcileJavaScriptRuntimeEvidence } = + await import("../application/javascript/JavaScriptRuntimeReconciliationService.js"); + const result = await runEvidenceInputCommand( + "reconcile_javascript_runtime", + options.evidenceBundle, + signal, + async (lookup) => { + const reconciled = reconcileJavaScriptRuntimeEvidence( + input.value, + lookup, + { signal }, + ); + return reconciled.ok + ? { ok: true, value: reconciled.value.evidence } + : reconciled; + }, + ); + return result.ok + ? result.value + : { error: "Application workflow failed", ...result.error }; + }, + ), ), }); }; diff --git a/src/cli/evidenceInputCommand.ts b/src/cli/evidenceInputCommand.ts new file mode 100644 index 0000000000..9416b61c8b --- /dev/null +++ b/src/cli/evidenceInputCommand.ts @@ -0,0 +1,53 @@ +import { localPathStringSchema } from "../domain/localPath.js"; +import type { Evidence } from "../domain/evidence.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { Result } from "../domain/result.js"; +import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; +import { absoluteCallerPath } from "../filesystem/callerPath.js"; +import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; +import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; + +/** Explicit portable lookup context for CLI workflows accepting retained IDs. */ +export const evidenceBundleOption = localPathStringSchema + .optional() + .describe( + "Resolve exact retained Evidence IDs from this local Evidence bundle without reopening an analysis target", + ); + +/** Own one selected bundle read through workflow execution and final cleanup. */ +export const runEvidenceInputCommand = ( + operation: string, + bundlePath: string | undefined, + signal: AbortSignal, + execute: ( + lookup: EvidenceLookup | undefined, + resources: ArtifactResourceScope, + ) => Promise>, +) => { + const resources = new ArtifactResourceScope(); + return finishAnalysisCommand( + async () => { + let lookup: EvidenceLookup | undefined; + if (bundlePath !== undefined) { + const { readImmutableEvidenceBundle } = + await import("../application/EvidenceBundleFiles.js"); + const bundle = await readImmutableEvidenceBundle( + absoluteCallerPath(bundlePath), + signal, + resources, + operation, + ); + if (!bundle.ok) return bundle; + const records = new Map( + bundle.value.records.map((record) => [record.evidence_id, record]), + ); + lookup = (id) => records.get(id); + } + return execute(lookup, resources); + }, + resources, + "rea", + operation, + signal, + ); +}; diff --git a/src/cli/managedCommands.ts b/src/cli/managedCommands.ts index daa09c2b27..3c588d609b 100644 --- a/src/cli/managedCommands.ts +++ b/src/cli/managedCommands.ts @@ -8,7 +8,11 @@ import type { DirectAnalysis } from "../composition/directAnalysis.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { parseCliJsonInput } from "../cliJsonInput.js"; import { MANAGED_STATIC_PROVIDER } from "../domain/investigationProviders.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { ok } from "../domain/result.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "pino"; @@ -188,16 +192,29 @@ const registerImportManagedReconstruction = ( .string() .describe("Inline managed reconstruction JSON or JSON file path"), }), - run: ({ args }) => - logCliCommand(logger, "import-managed-reconstruction", async () => { - const input = await parseCliJsonInput( - args.inputJson, - "import-managed-reconstruction", - ); - if (!input.ok) return input.error; - const result = importManagedReconstructionEvidence(input.value); - return result.ok ? result.value : projectAnalysisError(result.error); - }), + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, "import-managed-reconstruction", async () => { + const input = await parseCliJsonInput( + args.inputJson, + "import-managed-reconstruction", + signal, + ); + if (!input.ok) return input.error; + return runEvidenceInputCommand( + "import_managed_reconstruction", + options.evidenceBundle, + signal, + (lookup) => + Promise.resolve( + importManagedReconstructionEvidence(input.value, lookup, { + signal, + }), + ), + ).then(projectCliAnalysisResult); + }), + ), }); }; @@ -213,17 +230,32 @@ const registerVerifyManagedNativeBoundaries = ( .string() .describe("Inline managed/native verification JSON or JSON file path"), }), - run: ({ args }) => - logCliCommand(logger, "verify-managed-native-boundaries", async () => { - const input = await parseCliJsonInput( - args.inputJson, - "verify-managed-native-boundaries", - ); - if (!input.ok) return input.error; - const result = executeManagedNativeBoundaries(input.value); - return result.ok - ? result.value.evidence - : projectAnalysisError(result.error); - }), + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, "verify-managed-native-boundaries", async () => { + const input = await parseCliJsonInput( + args.inputJson, + "verify-managed-native-boundaries", + signal, + ); + if (!input.ok) return input.error; + return runEvidenceInputCommand( + "verify_managed_native_boundaries", + options.evidenceBundle, + signal, + (lookup) => { + const result = executeManagedNativeBoundaries( + input.value, + lookup, + { signal }, + ); + return Promise.resolve( + result.ok ? ok(result.value.evidence) : result, + ); + }, + ).then(projectCliAnalysisResult); + }), + ), }); }; diff --git a/src/cli/managedProjectGraphCommand.ts b/src/cli/managedProjectGraphCommand.ts index 1c69d8cfd6..6865b9c3dd 100644 --- a/src/cli/managedProjectGraphCommand.ts +++ b/src/cli/managedProjectGraphCommand.ts @@ -4,7 +4,12 @@ import { projectManagedApplicationGraphEvidence } from "../application/managed/M import { CLI_COMMANDS } from "../cliCommandNames.js"; import { parseCliJsonInput } from "../cliJsonInput.js"; import { logCliCommand } from "../cliLogging.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; +import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js"; +import { withCommandCancellation } from "./commandCancellation.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; @@ -21,15 +26,28 @@ export const registerProjectManagedApplicationGraph = ( .string() .describe("Inline managed application graph JSON or JSON file path"), }), - run: ({ args }) => - logCliCommand(logger, "project-managed-application-graph", async () => { - const input = await parseCliJsonInput( - args.inputJson, - "project-managed-application-graph", - ); - if (!input.ok) return input.error; - const result = projectManagedApplicationGraphEvidence(input.value); - return result.ok ? result.value : projectAnalysisError(result.error); - }), + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => + withCommandCancellation((signal) => + logCliCommand(logger, "project-managed-application-graph", async () => { + const input = await parseCliJsonInput( + args.inputJson, + "project-managed-application-graph", + signal, + ); + if (!input.ok) return input.error; + return runEvidenceInputCommand( + "project_managed_application_graph", + options.evidenceBundle, + signal, + (lookup) => + Promise.resolve( + projectManagedApplicationGraphEvidence(input.value, lookup, { + signal, + }), + ), + ).then(projectCliAnalysisResult); + }), + ), }); }; diff --git a/src/cli/sourceToBundleCommand.ts b/src/cli/sourceToBundleCommand.ts index 92f61e8647..2d4d01a787 100644 --- a/src/cli/sourceToBundleCommand.ts +++ b/src/cli/sourceToBundleCommand.ts @@ -1,4 +1,3 @@ -import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js"; import { z } from "incur"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; @@ -6,11 +5,11 @@ import { CLI_COMMANDS } from "../cliCommandNames.js"; import { parseCliJsonInput } from "../cliJsonInput.js"; import { logCliCommand } from "../cliLogging.js"; import { compareSourceToBundleCliInputSchema } from "../contracts/javascript/applicationWorkflowInputContracts.js"; -import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; -import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { withCommandCancellation } from "./commandCancellation.js"; -import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; /** CLI file transport delegates graph authentication and comparison to the same owners as MCP. */ export const registerSourceToBundleCommand = ( @@ -24,50 +23,30 @@ export const registerSourceToBundleCommand = ( args: z.object({ inputJson: z.string().describe("Inline workflow JSON or JSON file path"), }), - run: ({ args }) => + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => withCommandCancellation((signal) => logCliCommand(logger, name, async () => { const input = await parseCliJsonInput(args.inputJson, name, signal); if (!input.ok) return input.error; - const parsed = compareSourceToBundleCliInputSchema.safeParse( - input.value, - ); - if (!parsed.success) - return { - error: "Application workflow failed", - ...projectAnalysisError( - analysisInputErrorFromIssues( - name, - parsed.error.issues, - input.value, - ), - ), - }; - const resources = new ArtifactResourceScope(); - const result = await finishAnalysisCommand( - async () => { - const { resolveSourceToBundleReference } = - await import("../application/javascript/SourceToBundleReference.js"); - const reference = await resolveSourceToBundleReference( - parsed.data.reference, - signal, - resources, - ); - if (!reference.ok) return reference; - const { compareSourceToBundleEvidence } = + const result = await runEvidenceInputCommand( + "compare_source_to_bundle", + options.evidenceBundle, + signal, + async (lookup, resources) => { + const { executeSourceToBundleComparison } = await import("../application/javascript/JavaScriptApplicationWorkflowService.js"); - const compared = compareSourceToBundleEvidence({ - ...reference.value, - application: parsed.data.application, - }); + const compared = await executeSourceToBundleComparison( + input.value, + compareSourceToBundleCliInputSchema, + lookup, + resources, + { signal }, + ); return compared.ok ? { ok: true, value: compared.value.evidence } : compared; }, - resources, - "javascript-application-workflow", - "compare_source_to_bundle", - signal, ); return result.ok ? result.value diff --git a/src/cli/wasmCommands.ts b/src/cli/wasmCommands.ts index 278671da07..69a8b2e001 100644 --- a/src/cli/wasmCommands.ts +++ b/src/cli/wasmCommands.ts @@ -6,12 +6,15 @@ import { CLI_COMMANDS } from "../cliCommandNames.js"; import { z } from "incur"; import { wasmArtifactModule } from "../composition/wasm.js"; import { logCliCommand } from "../cliLogging.js"; -import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishCliEvidenceCommand } from "./cliEvidenceCallerPath.js"; +import { + evidenceBundleOption, + runEvidenceInputCommand, +} from "./evidenceInputCommand.js"; import { inspectWasmArtifactInputSchema, wasmArtifactPathsSchema, @@ -30,10 +33,11 @@ export const registerWasmCommands = ( input: z .string() .describe( - "Inline JSON or input file containing left/right complete Evidence and required scope", + "Inline JSON or input file containing left/right Evidence or retained references and required scope", ), }), - run: ({ args }) => + options: z.object({ evidenceBundle: evidenceBundleOption }), + run: ({ args, options }) => withCommandCancellation(async (signal) => logCliCommand(logger, CLI_COMMANDS.compareWasmInterfaces, async () => { const input = await parseCliJsonInput( @@ -42,8 +46,21 @@ export const registerWasmCommands = ( signal, ); if (!input.ok) return input.error; - const result = await compareWasmInterfaces(input.value, { signal }); - return result.ok ? result.value : projectAnalysisError(result.error); + return runEvidenceInputCommand( + "compare_wasm_interfaces", + options.evidenceBundle, + signal, + async (lookup) => { + const result = await compareWasmInterfaces( + input.value, + { signal }, + lookup, + ); + return result.ok + ? { ok: true, value: result.value.evidence } + : result; + }, + ).then(projectCliAnalysisResult); }), ), }); diff --git a/src/contracts/artifactComparisonContracts.ts b/src/contracts/artifactComparisonContracts.ts new file mode 100644 index 0000000000..d48f9fbf3b --- /dev/null +++ b/src/contracts/artifactComparisonContracts.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; +import { evidenceSourceInputSchema } from "../domain/evidenceReference.js"; + +/** Artifact sources retain their explicit local-file selection authority. */ +export const artifactComparisonInputSchema = z.strictObject({ + left: evidenceSourceInputSchema, + right: evidenceSourceInputSchema, +}); diff --git a/src/contracts/evidenceInputConformance.test.ts b/src/contracts/evidenceInputConformance.test.ts new file mode 100644 index 0000000000..0950af031e --- /dev/null +++ b/src/contracts/evidenceInputConformance.test.ts @@ -0,0 +1,253 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; + +import { analysisViewSourceSchema } from "../domain/analysisView/analysisViewRequest.js"; +import { evidenceSchema } from "../domain/evidence.js"; +import { evidenceBundleSchema } from "../domain/evidenceBundle.js"; +import { + getEvidenceInputContract, + isDeclaredEvidenceInput, +} from "../domain/evidenceInputContract.js"; +import { + evidenceFileReferenceSchema, + evidenceInputSchema, + evidenceSourceInputSchema, + retainedEvidenceReferenceSchema, +} from "../domain/evidenceReference.js"; +import { exportEnvironmentBundleInputSchema } from "../domain/environmentBundleExport.js"; +import { TOOL_CONTRACTS } from "./toolContracts.js"; + +type Owner = "record" | "reference" | "source-or-bundle"; +const owners = z.registry<{ readonly owner: Owner }>(); +owners.add(evidenceSchema, { owner: "record" }); +owners.add(retainedEvidenceReferenceSchema, { owner: "reference" }); +for (const schema of [ + evidenceBundleSchema, + evidenceSourceInputSchema, + evidenceFileReferenceSchema, + analysisViewSourceSchema, + exportEnvironmentBundleInputSchema.shape.source, +]) + owners.add(schema, { owner: "source-or-bundle" }); + +type SchemaChild = readonly [ + location: string, + schema: z.ZodType, + uncompiledEdge?: string, +]; + +/** Walk schema declarations only, including containers unsupported by the compiler. */ +const children = (schema: z.ZodType): readonly SchemaChild[] => { + if (schema instanceof z.ZodObject) + return [ + ...Object.entries(schema.shape).map(([key, child]): SchemaChild => [ + key, + child as z.ZodType, + ]), + ...(schema.def.catchall === undefined + ? [] + : [ + [ + "catchall", + schema.def.catchall as z.ZodType, + "object catchall", + ] as const, + ]), + ]; + if (schema instanceof z.ZodArray) + return [["[]", schema.element as z.ZodType]]; + if (schema instanceof z.ZodUnion || schema instanceof z.ZodXor) + return schema.options.map((option, index) => [ + `union[${index}]`, + option as z.ZodType, + ]); + if (schema instanceof z.ZodIntersection) + return [ + ["intersection.left", schema.def.left as z.ZodType], + ["intersection.right", schema.def.right as z.ZodType], + ]; + if (schema instanceof z.ZodTuple) + return [ + ...schema.def.items.map((item, index): SchemaChild => [ + `tuple[${index}]`, + item as z.ZodType, + ]), + ...(schema.def.rest === null + ? [] + : [["tuple.rest", schema.def.rest as z.ZodType] as const]), + ]; + if (schema instanceof z.ZodRecord || schema instanceof z.ZodMap) + return [ + ["keys", schema.keyType as z.ZodType], + ["values", schema.valueType as z.ZodType], + ]; + if (schema instanceof z.ZodSet) + return [["set.values", schema.def.valueType as z.ZodType]]; + if (schema instanceof z.ZodPipe) + return [ + ["pipe.input", schema.in as z.ZodType], + ["pipe.output", schema.out as z.ZodType], + ]; + if (schema instanceof z.ZodLazy) + return [["lazy", schema.unwrap() as z.ZodType]]; + if ( + schema instanceof z.ZodOptional || + schema instanceof z.ZodExactOptional || + schema instanceof z.ZodNullable || + schema instanceof z.ZodDefault || + schema instanceof z.ZodPrefault || + schema instanceof z.ZodNonOptional || + schema instanceof z.ZodReadonly || + schema instanceof z.ZodCatch || + schema instanceof z.ZodPromise || + schema instanceof z.ZodSuccess + ) + return [[schema.type, schema.unwrap() as z.ZodType]]; + return []; +}; + +const declarationIssues = (root: z.ZodType): string[] => { + const problems = new Set(); + const walk = ( + schema: z.ZodType, + path: readonly string[], + ancestors: readonly z.ZodType[], + active: ReadonlySet, + uncompiledEdges: readonly string[], + ): void => { + if (isDeclaredEvidenceInput(schema)) { + // A root may compile one supported field while losing a second field + // inside an unsupported container. Check the complete path, not just it. + for (const owner of [...ancestors, schema]) + if (!getEvidenceInputContract(owner).hasEvidence) + problems.add( + `${path.join(".")}: declared Evidence hidden by unsupported ${owner.type}`, + ); + // A skipping default never records which borrowing union branch won. + // Use prefault when an authored default must enter source selection. + const skippedDefault = ancestors.findIndex( + (owner) => owner instanceof z.ZodDefault, + ); + if ( + skippedDefault !== -1 && + ancestors + .slice(skippedDefault + 1) + .some((owner) => owner instanceof z.ZodUnion) + ) + problems.add( + `${path.join(".")}: Evidence union selection requires prefault instead of a skipping default`, + ); + for (const edge of uncompiledEdges) + problems.add( + `${path.join(".")}: declared Evidence hidden by unsupported ${edge}`, + ); + return; + } + const owner = owners.get(schema)?.owner; + if (owner === "source-or-bundle") return; + if (owner === "record" || owner === "reference") { + problems.add( + `${path.join(".")}: bare ${owner} requires the shared Evidence input declaration`, + ); + return; + } + if (active.has(schema)) return; + const next = new Set(active); + next.add(schema); + for (const [location, child, edge] of children(schema)) + walk( + child, + [...path, location], + [...ancestors, schema], + next, + edge === undefined ? uncompiledEdges : [...uncompiledEdges, edge], + ); + }; + walk(root, ["input"], [], new Set(), []); + return [...problems].sort(); +}; + +describe("public Evidence input declarations", () => { + it("requires all catalog consumers to use the shared logical input owner", () => { + const problems = TOOL_CONTRACTS.flatMap((contract) => + declarationIssues(contract.inputSchema).map( + (issue) => `${contract.name}: ${issue}`, + ), + ); + expect(problems).toEqual([]); + }); + + it("detects bare/described records and independently declared references", () => { + expect( + declarationIssues( + z.strictObject({ + left: evidenceSchema.describe("Complete source"), + right: retainedEvidenceReferenceSchema.describe("Retained source"), + }), + ), + ).toEqual([ + "input.left: bare record requires the shared Evidence input declaration", + "input.right: bare reference requires the shared Evidence input declaration", + ]); + }); + + it("does not let a supported sibling conceal an unsupported marked input path", () => { + const schema = z.strictObject({ + supported: evidenceInputSchema, + records: z.record(z.string(), evidenceInputSchema), + }); + expect(getEvidenceInputContract(schema).hasEvidence).toBe(true); + expect(declarationIssues(schema)).toEqual([ + "input.records.values: declared Evidence hidden by unsupported record", + ]); + const dynamic = z + .object({ supported: evidenceInputSchema }) + .catchall(evidenceInputSchema); + expect(declarationIssues(dynamic)).toEqual([ + "input.catchall: declared Evidence hidden by unsupported object catchall", + ]); + }); + + it("follows nested optional/default/union declarations and stops at collection owners", () => { + const schema = z.strictObject({ + layers: z + .array( + z.strictObject({ + source: evidenceInputSchema.describe("Selected source").optional(), + }), + ) + .default([]), + choice: z.union([ + z.strictObject({ + kind: z.literal("source"), + source: evidenceInputSchema, + }), + z.strictObject({ kind: z.literal("none") }), + ]), + bundle: evidenceBundleSchema, + fileSource: evidenceSourceInputSchema.describe("Explicit file authority"), + }); + expect(declarationIssues(schema)).toEqual([]); + }); + + it("rejects skipping defaults above Evidence-bearing request unions", () => { + const choice = z.union([ + z.strictObject({ + kind: z.literal("source"), + source: evidenceInputSchema, + }), + z.strictObject({ kind: z.literal("none") }), + ]); + const fallback = { + kind: "source" as const, + source: { + kind: "retained-evidence" as const, + evidence_id: `ev_${"a".repeat(64)}`, + }, + }; + expect(declarationIssues(choice.default(fallback))).toEqual([ + "input.default.union[0].source: Evidence union selection requires prefault instead of a skipping default", + ]); + expect(declarationIssues(choice.prefault(fallback))).toEqual([]); + }); +}); diff --git a/src/contracts/investigationInputContracts.ts b/src/contracts/investigationInputContracts.ts new file mode 100644 index 0000000000..4ad548884d --- /dev/null +++ b/src/contracts/investigationInputContracts.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { callPathInputSchema } from "../domain/callPathSchemas.js"; +import { evidenceInputSchema } from "../domain/evidenceReference.js"; +import { staticRuntimeCorrelationInputSchema } from "../domain/staticRuntimeCorrelation.js"; + +/** Transport sources resolve to complete records before the pure algorithms run. */ +export const callPathRequestSchema = callPathInputSchema.extend({ + functions: z.array(evidenceInputSchema).min(1), +}); + +export const staticRuntimeCorrelationRequestSchema = + staticRuntimeCorrelationInputSchema.extend({ + static_comparisons: z.array(evidenceInputSchema).min(1), + runtime_comparisons: z.array(evidenceInputSchema).min(1), + }); diff --git a/src/contracts/javascript/applicationWorkflowInputContracts.ts b/src/contracts/javascript/applicationWorkflowInputContracts.ts index 848d5b50d6..2bf4e8737b 100644 --- a/src/contracts/javascript/applicationWorkflowInputContracts.ts +++ b/src/contracts/javascript/applicationWorkflowInputContracts.ts @@ -3,19 +3,17 @@ import { localPathStringSchema, } from "../../domain/localPath.js"; import { sourceToBundleViewRequestSchema } from "../../domain/analysisView/sourceToBundleView.js"; -import { evidenceSchema } from "../../domain/evidence.js"; import { z } from "zod"; import { evidenceInputSchema } from "../../domain/evidenceReference.js"; -import { compareApplicationVersionsInputSchema } from "../../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; import { compareJavaScriptExportShapesInputSchema } from "../../domain/javascript/javascriptExportShapeComparisonSchemas.js"; import { traceApplicationFeatureInputSchema } from "../../domain/javascript/javascriptFeatureTraceSchemas.js"; -import { javaScriptSemanticQueryInputSchema } from "../../domain/javascript/javascriptSemanticQuerySchemas.js"; +import { traceJavaScriptSemanticsInputSchema } from "../../domain/javascript/javascriptSemanticTraceSchemas.js"; import { compareSourceToBundleInputSchema } from "../../domain/javascript/sourceToBundleComparisonSchemas.js"; +import { reconcileJavaScriptRuntimeInputSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; const traceApplicationFeatureFacts = { - native_observations: - traceApplicationFeatureInputSchema.shape.native_observations, + native_observations: z.array(evidenceInputSchema).default([]), seed: traceApplicationFeatureInputSchema.shape.seed, semantic_page: traceApplicationFeatureInputSchema.shape.semantic_page, direction: traceApplicationFeatureInputSchema.shape.direction, @@ -28,16 +26,14 @@ export const traceApplicationFeatureRequestSchema = z.strictObject({ }); /** Semantic trace request accepting inline or same-session application Evidence. */ -export const traceJavaScriptSemanticsRequestSchema = z.strictObject({ - application: evidenceInputSchema, - query: javaScriptSemanticQueryInputSchema, -}); +export const traceJavaScriptSemanticsRequestSchema = + traceJavaScriptSemanticsInputSchema.extend({ + application: evidenceInputSchema, + }); const compareApplicationVersionsFacts = { - left_native_observations: - compareApplicationVersionsInputSchema.shape.left_native_observations, - right_native_observations: - compareApplicationVersionsInputSchema.shape.right_native_observations, + left_native_observations: z.array(evidenceInputSchema).default([]), + right_native_observations: z.array(evidenceInputSchema).default([]), } as const; /** Application comparison request accepting inline or same-session application Evidence. */ @@ -65,7 +61,7 @@ export const compareSourceToBundleCliInputSchema = z.strictObject({ compareSourceToBundleInputSchema.shape.reference, referenceFile, ]), - application: evidenceSchema, + application: evidenceInputSchema, }); /** The MCP host accepts absolute local paths; omitted view preserves the full report. */ export const compareSourceToBundleRequestSchema = z.strictObject({ @@ -94,3 +90,15 @@ export const compareJavaScriptExportShapesRequestSchema = z.strictObject({ left: evidenceInputSchema, right: evidenceInputSchema, }); + +/** Static layers and passive observations share the same logical Evidence inputs. */ +export const reconcileJavaScriptRuntimeRequestSchema = z.strictObject({ + static_layers: z + .array( + reconcileJavaScriptRuntimeInputSchema.shape.static_layers.element.extend({ + analysis: evidenceInputSchema, + }), + ) + .min(1), + runtime_observations: z.array(evidenceInputSchema).min(1), +}); diff --git a/src/contracts/javascript/electronToolContracts.ts b/src/contracts/javascript/electronToolContracts.ts index e276fa87ce..53c0d295b8 100644 --- a/src/contracts/javascript/electronToolContracts.ts +++ b/src/contracts/javascript/electronToolContracts.ts @@ -28,10 +28,8 @@ import { analyzeJavaScriptApplicationInputSchema, javascriptApplicationAnalysisResultSchema, } from "../../domain/javascript/javascriptApplicationAnalysis.js"; -import { - javascriptRuntimeReconciliationResultSchema, - reconcileJavaScriptRuntimeInputSchema, -} from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { javascriptRuntimeReconciliationResultSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js"; +import { reconcileJavaScriptRuntimeRequestSchema } from "./applicationWorkflowInputContracts.js"; import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "./javascriptRuntimeReconciliationExample.js"; const listOutputSchema = evidenceResultOf(electronTargetListSchema); @@ -149,7 +147,7 @@ export const ELECTRON_TOOL_CONTRACTS = [ description: "Reconcile verified static JavaScript application graphs with existing passive web/Electron CDP, passive V8 Inspector, or provider-owned active Electron Evidence. Active Electron captures contribute an explicitly partial target-only runtime record; they never invent renderer scripts, frames, workers, or execution claims. Exact captured-source digests take priority over caller-declared file/URL mappings; target, frame, script, and worker ambiguity remains explicit, and source-map authority stays separate.", kind: "electron-provider", - inputSchema: reconcileJavaScriptRuntimeInputSchema, + inputSchema: reconcileJavaScriptRuntimeRequestSchema, outputSchema: reconciliationOutputSchema, examples: [ { diff --git a/src/contracts/managed/managedWorkflowToolContracts.ts b/src/contracts/managed/managedWorkflowToolContracts.ts index dff0b0c55b..8423bceaf8 100644 --- a/src/contracts/managed/managedWorkflowToolContracts.ts +++ b/src/contracts/managed/managedWorkflowToolContracts.ts @@ -1,10 +1,9 @@ import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js"; -import { z } from "zod"; -import { evidenceSchema } from "../../domain/evidence.js"; -import { compareManagedMembersInputSchema } from "../../domain/managed/managedMemberComparison.js"; -import { managedNativeVerificationInputSchema } from "../../domain/managed/managedNativeVerificationSchemas.js"; +import { managedApplicationGraphReferenceInputSchema } from "../../domain/managed/managedApplicationGraph.js"; +import { compareManagedMembersRequestSchema } from "../../domain/managed/managedMemberComparison.js"; +import { managedNativeVerificationRequestSchema } from "../../domain/managed/managedNativeVerificationSchemas.js"; -import { managedReconstructionImportInputSchema } from "../../domain/managed/managedReconstruction.js"; +import { managedReconstructionImportRequestSchema } from "../../domain/managed/managedReconstruction.js"; import type { ToolContract } from "../toolContractTypes.js"; import { managedWorkflowOutputSchemas } from "../toolOutputSchemaGroups.js"; import { @@ -15,43 +14,6 @@ import { } from "./managedWorkflowExamples.js"; import { toolContractMetadata } from "../toolEffects.js"; -const managedApplicationGraphReferenceShape = z.strictObject({ - managed_artifact: evidenceSchema.optional(), - managed_members: evidenceSchema.optional(), - managed_native_boundaries: evidenceSchema.optional(), -}); - -/** Inline Evidence sources for managed application graph projection. */ -export const managedApplicationGraphReferenceInputSchema = z - .union([ - managedApplicationGraphReferenceShape.extend({ - managed_artifact: evidenceSchema, - }), - managedApplicationGraphReferenceShape.extend({ - managed_members: evidenceSchema, - }), - managedApplicationGraphReferenceShape.extend({ - managed_native_boundaries: evidenceSchema, - }), - ]) - .superRefine((input, context) => { - const ids = [ - input.managed_artifact?.evidence_id, - input.managed_members?.evidence_id, - input.managed_native_boundaries?.evidence_id, - ].filter((id): id is string => id !== undefined); - if (ids.length === 0) - context.addIssue({ - code: "custom", - message: "Provide at least one managed Evidence source", - }); - if (new Set(ids).size !== ids.length) - context.addIssue({ - code: "custom", - message: "Managed application graph Evidence IDs must be unique", - }); - }); - const comparisonOutputSchema = requireOutputSchema( managedWorkflowOutputSchemas, "compare_managed_members", @@ -75,9 +37,9 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [ name: "compare_managed_members", ...toolContractMetadata("compare_managed_members"), description: - "Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact declared type, name, and CIL/signature; exact declared type, name, and signature (decoded or not); exact declared type, name, and decoded structural method shape; then exact CIL/signature or decoded structural method shape alone, which never takes a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Signatures compare type operands by name, not by build-local metadata row. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Matched methods also compare aligned call-site shapes and observed target declaring-type/method names; missing or unaligned call edges and target names remain unknown. These static names do not prove overload identity or runtime dispatch. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.", + "Compare two authenticated inspect_managed_members Evidence records, supplied inline or by exact retained reference, using unique-only tiers: exact declared type, name, and CIL/signature; exact declared type, name, and signature (decoded or not); exact declared type, name, and decoded structural method shape; then exact CIL/signature or decoded structural method shape alone, which never takes a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Signatures compare type operands by name, not by build-local metadata row. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Matched methods also compare aligned call-site shapes and observed target declaring-type/method names; missing or unaligned call edges and target names remain unknown. These static names do not prove overload identity or runtime dispatch. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.", kind: "application", - inputSchema: compareManagedMembersInputSchema, + inputSchema: compareManagedMembersRequestSchema, outputSchema: comparisonOutputSchema, examples: [ { @@ -93,9 +55,9 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [ name: "verify_managed_native_boundaries", ...toolContractMetadata("verify_managed_native_boundaries"), description: - "Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states; a name candidate with missing managed or native module identity remains unresolved with its candidate evidence retained.", + "Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence, with inline or exact retained sources, without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states; a name candidate with missing managed or native module identity remains unresolved with its candidate evidence retained.", kind: "application", - inputSchema: managedNativeVerificationInputSchema, + inputSchema: managedNativeVerificationRequestSchema, outputSchema: nativeVerificationOutputSchema, examples: [ { @@ -113,9 +75,9 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [ name: "import_managed_reconstruction", ...toolContractMetadata("import_managed_reconstruction"), description: - "Import decompiler-produced managed reconstruction against authenticated inspect_managed_members Evidence. The workflow locks each method to artifact SHA-256, MVID, metadata token, signature hash, and the limited decoded-IL tuple hash, records the decompiler identity and options, and marks C# or pseudocode as analyst inference rather than canonical byte observation.", + "Import decompiler-produced managed reconstruction against authenticated inspect_managed_members Evidence supplied inline or by exact retained reference. The workflow locks each method to artifact SHA-256, MVID, metadata token, signature hash, and the limited decoded-IL tuple hash, records the decompiler identity and options, and marks C# or pseudocode as analyst inference rather than canonical byte observation.", kind: "application", - inputSchema: managedReconstructionImportInputSchema, + inputSchema: managedReconstructionImportRequestSchema, outputSchema: reconstructionOutputSchema, examples: [ { @@ -133,7 +95,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [ name: "project_managed_application_graph", ...toolContractMetadata("project_managed_application_graph"), description: - "Project authenticated managed artifact, member, and native-boundary Evidence into the provider-neutral application graph without executing managed code, loading assemblies, or translating managed metadata tokens to native addresses. The graph preserves managed static observations as managed-specific nodes linked to source Evidence, with in-module calls and field-access edges; other call and field-access sites are counted in limitations.", + "Project authenticated managed artifact, member, and native-boundary Evidence, supplied inline or by exact retained reference, into the provider-neutral application graph without executing managed code, loading assemblies, or translating managed metadata tokens to native addresses. The graph preserves managed static observations as managed-specific nodes linked to source Evidence, with in-module calls and field-access edges; other call and field-access sites are counted in limitations.", kind: "application", inputSchema: managedApplicationGraphReferenceInputSchema, outputSchema: managedApplicationGraphOutputSchema, diff --git a/src/contracts/native/functionMatchingContracts.ts b/src/contracts/native/functionMatchingContracts.ts index 0c703a5130..41c02b5fb8 100644 --- a/src/contracts/native/functionMatchingContracts.ts +++ b/src/contracts/native/functionMatchingContracts.ts @@ -1,14 +1,15 @@ import { z } from "zod"; -import { evidenceInputSchema } from "../../domain/evidenceReference.js"; +import { + borrowEvidenceInput, + borrowEvidenceArray, +} from "../../domain/evidenceReference.js"; // Borrow inline payloads until application admission; advertise the exact public // Evidence/reference union without a pre-admission recursive SDK clone. -const input = z.unknown().nonoptional(); -input._zod.processJSONSchema = evidenceInputSchema._zod.processJSONSchema; +const input = borrowEvidenceInput(); // Borrow the outer selections too: admission must account for their width before // any array traversal/copy. The owner authenticates each selected carrier later. -const observations = z.custom(Array.isArray); -observations._zod.processJSONSchema = z.array(input)._zod.processJSONSchema; +const observations = borrowEvidenceArray(); /** Inventories stay portable; optional observations avoid all-function decompilation. */ export const matchNativeFunctionsInputSchema = z.strictObject({ diff --git a/src/contracts/processComparisonContracts.ts b/src/contracts/processComparisonContracts.ts new file mode 100644 index 0000000000..9f87b6d660 --- /dev/null +++ b/src/contracts/processComparisonContracts.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; +import { evidenceInputSchema } from "../domain/evidenceReference.js"; +import { processTraceSpecificationSchema } from "../domain/process/processTraceSpecification.js"; + +/** Exact observation sources and the caller's process-comparison policy. */ +export const processComparisonInputSchema = z.strictObject({ + left: evidenceInputSchema, + right: evidenceInputSchema, + trace_spec: processTraceSpecificationSchema.optional(), + max_capture_age_ms: z.int().nonnegative().optional(), +}); diff --git a/src/contracts/sessionToolContracts.ts b/src/contracts/sessionToolContracts.ts index 1991401552..6370758c9a 100644 --- a/src/contracts/sessionToolContracts.ts +++ b/src/contracts/sessionToolContracts.ts @@ -3,7 +3,10 @@ import { matchNativeFunctionsInputSchema } from "./native/functionMatchingContra import { z } from "zod"; import { absoluteLocalPathStringSchema } from "../domain/localPath.js"; -import { callPathInputSchema } from "../domain/callPathSchemas.js"; +import { + callPathRequestSchema, + staticRuntimeCorrelationRequestSchema, +} from "./investigationInputContracts.js"; import { changedBehaviorInputSchema } from "../domain/changedBehavior.js"; import { functionComparisonInputSchema } from "../domain/functionComparisonSchemas.js"; import { processScenarioSchema } from "../domain/process/processScenario.js"; @@ -12,7 +15,6 @@ import { updateUnknownInputSchema, } from "../domain/residualUnknown.js"; import { reconstructionVerificationInputSchema } from "../domain/reconstructionVerificationSchemas.js"; -import { staticRuntimeCorrelationInputSchema } from "../domain/staticRuntimeCorrelation.js"; import { sessionOutputSchemas } from "./toolOutputSchemaGroups.js"; import { closeBinaryInputSchema, @@ -20,14 +22,14 @@ import { openBinaryInputSchema, } from "./sessionLifecycleInputs.js"; import { binarySessionInputSchema } from "./sessionStatusContract.js"; +import { processComparisonInputSchema } from "./processComparisonContracts.js"; +import { artifactComparisonInputSchema } from "./artifactComparisonContracts.js"; import { addressContextInputSchema, - artifactComparisonInputSchema, bundleComparisonMcpInputSchema, importEvidenceBundleInputSchema, listUnknownsInputSchema, navigationContextInputSchema, - processComparisonInputSchema, getEvidenceBundleInputSchema, verifyUnknownResolutionInputSchema, } from "./sessionToolSchemas.js"; @@ -130,13 +132,13 @@ export const SESSION_TOOL_CONTRACTS = [ ), session( "build_call_path", - "Build every shortest direct-callee path inline from complete analyze_function Evidence records using exact canonical addresses. Missing dossiers and provider mixing remain unknown; every node and edge cites source Evidence.", - callPathInputSchema, + "Build every shortest direct-callee path from complete analyze_function Evidence supplied inline or by exact retained-evidence references. References reuse the complete parent of an analyze_function summary without resending dossiers. Uses exact canonical addresses; missing dossiers and provider mixing remain unknown, and every node and edge cites source Evidence.", + callPathRequestSchema, ), session( "correlate_static_and_runtime", - "Evaluate every explicit caller-declared hypothesis between exact static comparison findings and runtime comparison dimensions. Similar names or paths are never auto-matched, consistent cochange never proves causality, and unknown or truncated inputs remain unresolved. Returns all correlations and their complete Evidence closure inline.", - staticRuntimeCorrelationInputSchema, + "Evaluate every explicit caller-declared hypothesis between static and runtime comparison Evidence supplied inline or by exact retained-evidence references. Similar names or paths are never auto-matched, consistent cochange never proves causality, and unknown or truncated inputs remain unresolved. Returns all correlations and their complete Evidence closure inline.", + staticRuntimeCorrelationRequestSchema, ), session( "verify_reconstruction", diff --git a/src/contracts/sessionToolSchemas.ts b/src/contracts/sessionToolSchemas.ts index 7e61b82f43..5f0926517c 100644 --- a/src/contracts/sessionToolSchemas.ts +++ b/src/contracts/sessionToolSchemas.ts @@ -3,9 +3,6 @@ import { z } from "zod"; import { absoluteLocalPathStringSchema } from "../domain/localPath.js"; import { bundleComparisonInputSchema } from "../domain/bundleComparison.js"; -import { processTraceSpecificationSchema } from "../domain/process/processTraceSpecification.js"; -import { evidenceSchema } from "../domain/evidence.js"; -import { evidenceSourceInputSchema } from "../domain/evidenceReference.js"; import { prefixedDigestSchema } from "./../domain/digests.js"; import { evidenceRecordFiltersSchema } from "../domain/evidenceRecordSummary.js"; import { residualUnknownSchema } from "../domain/residualUnknown.js"; @@ -45,23 +42,6 @@ export const bundleComparisonMcpInputSchema = ), }); -/** Evidence references for deterministic process comparison. */ -export const processComparisonInputSchema = z.strictObject({ - left: evidenceSchema, - right: evidenceSchema, - trace_spec: processTraceSpecificationSchema.optional(), - max_capture_age_ms: z.int().nonnegative().optional(), -}); - -/** Name each compared artifact inventory once: inline, retained, or in a local Evidence file. */ -export const artifactComparisonInputSchema = z.strictObject({ - left: evidenceSourceInputSchema, - right: evidenceSourceInputSchema, -}); -export type ArtifactComparisonInput = z.output< - typeof artifactComparisonInputSchema ->; - /** Residual-unknown list filters. */ export const listUnknownsInputSchema = z.strictObject({ status: residualUnknownSchema.shape.status.optional(), diff --git a/src/contracts/toolSchemaMetadata.ts b/src/contracts/toolSchemaMetadata.ts index 91044c0db0..da21833dfb 100644 --- a/src/contracts/toolSchemaMetadata.ts +++ b/src/contracts/toolSchemaMetadata.ts @@ -14,6 +14,7 @@ import { schemaLiteralValues, } from "./jsonSchemaKeywords.js"; import { digestCanonicalValue } from "../domain/canonicalDigest.js"; +import { getEvidenceInputContract } from "../domain/evidenceInputContract.js"; const PROPERTY_DESCRIPTIONS: Readonly> = { addresses: "Ordered provider-normalized procedure addresses to analyze.", @@ -75,11 +76,26 @@ export const toolInputSchemaWithMetadata = ( libraryOptions: { ...options.libraryOptions, reused: "inline" }, }); const examples = advertisedInputExamples(contract); + const presented = presentInputJsonSchema( + inline, + fallbackPropertyDescription, + ); + const sourceGuidance = getEvidenceInputContract(contract.inputSchema) + .hasEvidence + ? 'Declared Evidence fields accept complete records or {"kind":"retained-evidence","evidence_id":"ev_..."}. For a complete source, use the summary\'s parent_evidence_id.' + : undefined; return { // Keep advertised parameters self-contained for clients that do not // resolve local references. The registered canonical schema remains // responsible for runtime validation. - ...presentInputJsonSchema(inline, fallbackPropertyDescription), + ...presented, + ...(sourceGuidance === undefined + ? {} + : { + description: [presented.description, sourceGuidance] + .filter((text) => typeof text === "string") + .join(" "), + }), ...(examples.length === 0 ? {} : { examples }), }; }, diff --git a/src/contracts/wasm/wasmComparisonContracts.ts b/src/contracts/wasm/wasmComparisonContracts.ts index e99962f043..a54a2cd3ae 100644 --- a/src/contracts/wasm/wasmComparisonContracts.ts +++ b/src/contracts/wasm/wasmComparisonContracts.ts @@ -56,7 +56,7 @@ export const COMPARE_WASM_INTERFACES_CONTRACT = { ...toolContractMetadata("compare_wasm_interfaces"), kind: "application", description: - "Compare typed core WASM import/export names, kinds and exact signatures from two authenticated inspect_wasm_artifact Evidence records. Select imports, exports or both explicitly. Retained references resolve only in the current session; CLI accepts portable inline Evidence. Duplicate names, unsupported types and missing historical interface facets remain unknown. Preserve original module digests and every occurrence; no source acquisition, module execution or runtime-compatibility inference.", + "Compare typed core WASM import/export names, kinds and exact signatures from two authenticated inspect_wasm_artifact Evidence records. Select imports, exports or both explicitly. MCP references resolve in the current session; CLI references use an explicitly selected --evidence-bundle. Duplicate names, unsupported types and missing historical interface facets remain unknown. Preserve original module digests and every occurrence; no source acquisition, module execution or runtime-compatibility inference.", inputSchema: compareWasmInterfacesInputSchema, outputSchema: evidenceResultOf(wasmInterfaceComparisonSchema), examples: [], diff --git a/src/contracts/workflowInputContracts.test.ts b/src/contracts/workflowInputContracts.test.ts index 196918a22f..990be9aefc 100644 --- a/src/contracts/workflowInputContracts.test.ts +++ b/src/contracts/workflowInputContracts.test.ts @@ -11,7 +11,7 @@ import { MANAGED_MEMBER_COMPARISON_EXAMPLE, MANAGED_NATIVE_VERIFICATION_EXAMPLE, } from "./managed/managedWorkflowExamples.js"; -import { managedApplicationGraphReferenceInputSchema } from "./managed/managedWorkflowToolContracts.js"; +import { managedApplicationGraphReferenceInputSchema } from "../domain/managed/managedApplicationGraph.js"; describe("workflow input contracts", () => { it("rejects application Evidence ID references and duplicate inline records", () => { diff --git a/src/domain/evidenceInputContract.ts b/src/domain/evidenceInputContract.ts new file mode 100644 index 0000000000..bbd6fc67d1 --- /dev/null +++ b/src/domain/evidenceInputContract.ts @@ -0,0 +1,273 @@ +import { z } from "zod"; + +import type { Evidence } from "./evidence.js"; +import type { EvidenceInput } from "./evidenceReference.js"; +import { ok, type Result } from "./result.js"; + +type EvidenceInputDeclaration = + | { readonly kind: "evidence" } + | { readonly kind: "borrowed"; readonly owner: object }; + +// Zod registries carry owner metadata through describe/meta clones. This is a +// schema declaration registry, never a store of caller Evidence or references. +const declarations = z.registry(); +const borrowedDeclarations = new WeakMap(); + +/** Inspect authored declarations, including described clones, for catalog checks. */ +export const isDeclaredEvidenceInput = (schema: z.ZodType): boolean => + declarations.get(schema) !== undefined; + +/** Mark the canonical logical Evidence input, independently of its JSON shape. */ +export const declareEvidenceInput = ( + schema: Schema, +): Schema => { + declarations.add(schema, { kind: "evidence" }); + return schema; +}; + +/** Keep an SDK-borrowed selection associated with its owning declaration. */ +export const declareBorrowedEvidenceInput = ( + schema: Schema, + declaration: z.ZodType, +): Schema => { + const owner = {}; + borrowedDeclarations.set(owner, declaration); + declarations.add(schema, { kind: "borrowed", owner }); + return schema; +}; + +/** The application replaces only declared Evidence inputs, retaining ordinary fields. */ +export type ResolvedEvidenceRequestInput< + Input, + Seen = never, +> = Input extends EvidenceInput + ? Evidence + : Input extends Seen + ? Input + : Input extends readonly unknown[] + ? { + [Index in keyof Input]: ResolvedEvidenceRequestInput< + Input[Index], + Seen | Input + >; + } + : Input extends object + ? { + [Key in keyof Input]: ResolvedEvidenceRequestInput< + Input[Key], + Seen | Input + >; + } + : Input; + +/** Envelope facts are typed; each unadmitted Evidence carrier remains unknown. */ +export type EvidenceRequestFrameInput< + Input, + Seen = never, +> = Input extends EvidenceInput + ? unknown + : Input extends Seen + ? Input + : Input extends readonly unknown[] + ? { + [Index in keyof Input]: EvidenceRequestFrameInput< + Input[Index], + Seen | Input + >; + } + : Input extends object + ? { + [Key in keyof Input]: EvidenceRequestFrameInput< + Input[Key], + Seen | Input + >; + } + : Input; + +export type EvidenceInputPath = readonly (string | number)[]; +type Visitor = ( + input: unknown, + path: EvidenceInputPath, +) => Result; + +/** A borrowing envelope and the exact Evidence locations derived from one schema. */ +export interface EvidenceInputContract { + readonly frameSchema: z.ZodType; + readonly hasEvidence: boolean; + resolve( + input: unknown, + visit: Visitor, + path?: EvidenceInputPath, + ): Result; +} + +const contracts = new WeakMap(); +const identity = (schema: z.ZodType): EvidenceInputContract => ({ + frameSchema: schema, + hasEvidence: false, + resolve: (input) => ok(input), +}); + +/** + * Compile author-owned object/array/union/wrapper declarations once. Inline + * Evidence is borrowed, not parsed or authenticated here. Unsupported ordinary + * value schemas stay with their owner; no arbitrary JSON is searched for IDs. + */ +export const getEvidenceInputContract = ( + schema: Schema, +): EvidenceInputContract>> => { + const existing = contracts.get(schema); + if (existing !== undefined) + return existing as EvidenceInputContract< + EvidenceRequestFrameInput> + >; + const result = compile(schema); + contracts.set(schema, result); + return result as EvidenceInputContract< + EvidenceRequestFrameInput> + >; +}; + +const compile = (schema: z.ZodType): EvidenceInputContract => { + const declared = declarations.get(schema); + if (declared?.kind === "evidence") + return { + frameSchema: z.unknown().nonoptional(), + hasEvidence: true, + resolve: (input, visit, path = []) => visit(input, path), + }; + if (declared?.kind === "borrowed") { + const declaration = borrowedDeclarations.get(declared.owner); + if (declaration === undefined) + throw new TypeError("Borrowed Evidence schema has no owning declaration"); + const source = getEvidenceInputContract(declaration); + return { ...source, frameSchema: schema }; + } + if (schema instanceof z.ZodObject) return objectContract(schema); + if (schema instanceof z.ZodArray) return arrayContract(schema); + if (schema instanceof z.ZodUnion) return unionContract(schema); + if ( + schema instanceof z.ZodOptional || + schema instanceof z.ZodExactOptional || + schema instanceof z.ZodNullable || + schema instanceof z.ZodDefault || + schema instanceof z.ZodPrefault || + schema instanceof z.ZodNonOptional || + schema instanceof z.ZodReadonly + ) { + const child = getEvidenceInputContract(schema.unwrap() as z.ZodType); + if (!child.hasEvidence) return identity(schema); + return { + hasEvidence: true, + frameSchema: replaceWrappedSchema(schema, child.frameSchema), + resolve: (input, visit, path = []) => + input === undefined || + (input === null && schema instanceof z.ZodNullable) + ? ok(input) + : child.resolve(input, visit, path), + }; + } + return identity(schema); +}; + +/** Zod's public clone retains the concrete wrapper and its existing checks. */ +const replaceWrappedSchema = ( + schema: z.ZodType, + innerType: z.ZodType, +): z.ZodType => { + const definition = { ...schema.def, innerType }; + return z.clone(schema, definition, { parent: true }); +}; + +const objectContract = (schema: z.ZodObject): EvidenceInputContract => { + const children = Object.entries(schema.shape).map( + ([key, child]) => + [key, getEvidenceInputContract(child as z.ZodType)] as const, + ); + const selected = children.filter(([, child]) => child.hasEvidence); + if (selected.length === 0) return identity(schema); + return { + hasEvidence: true, + frameSchema: schema.clone( + { + ...schema.def, + shape: Object.fromEntries( + children.map(([key, child]) => [key, child.frameSchema]), + ), + }, + { parent: true }, + ), + resolve: (input, visit, path = []) => { + if (typeof input !== "object" || input === null || Array.isArray(input)) + return ok(input); + let result = input as Readonly>; + for (const [key, child] of selected) { + if (!Object.hasOwn(result, key)) continue; + const value = child.resolve(result[key], visit, [...path, key]); + if (!value.ok) return value; + if (value.value !== result[key]) + result = { ...result, [key]: value.value }; + } + return ok(result); + }, + }; +}; + +const arrayContract = (schema: z.ZodArray): EvidenceInputContract => { + const child = getEvidenceInputContract(schema.element as z.ZodType); + if (!child.hasEvidence) return identity(schema); + return { + hasEvidence: true, + frameSchema: schema.clone( + { ...schema.def, element: child.frameSchema }, + { parent: true }, + ), + resolve: (input, visit, path = []) => { + if (!Array.isArray(input)) return ok(input); + let result = input; + for (const [index, item] of input.entries()) { + const value = child.resolve(item, visit, [...path, index]); + if (!value.ok) return value; + if (value.value !== item) { + if (result === input) result = input.slice(); + result[index] = value.value; + } + } + return ok(result); + }, + }; +}; + +const unionContract = (schema: z.ZodUnion): EvidenceInputContract => { + const options = schema.options.map((option) => + getEvidenceInputContract(option as z.ZodType), + ); + if (!options.some((option) => option.hasEvidence)) return identity(schema); + // Capture the successful branch while parsing; choosing it again later would + // repeat defaults, overwrites and refinements. Keys are only parser results. + const branches = new WeakMap(); + const frame = z.union( + options.map((option) => + option.frameSchema.transform((value) => { + if (typeof value === "object" && value !== null) + branches.set(value, option); + return value; + }), + ), + ); + return { + hasEvidence: true, + frameSchema: frame.clone({ + ...frame.def, + ...(schema.def.error === undefined ? {} : { error: schema.def.error }), + ...(schema.def.checks === undefined ? {} : { checks: schema.def.checks }), + }), + resolve: (input, visit, path = []) => { + const selected = + typeof input === "object" && input !== null + ? branches.get(input) + : undefined; + return selected?.resolve(input, visit, path) ?? ok(input); + }, + }; +}; diff --git a/src/domain/evidenceReference.ts b/src/domain/evidenceReference.ts index 300f8a1411..51aed239d7 100644 --- a/src/domain/evidenceReference.ts +++ b/src/domain/evidenceReference.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { prefixedDigestSchema } from "./digests.js"; import { absoluteLocalPathStringSchema } from "./localPath.js"; +import { + declareBorrowedEvidenceInput, + declareEvidenceInput, +} from "./evidenceInputContract.js"; /** Exact reference to Evidence already retained by the current session. */ export const retainedEvidenceReferenceSchema = z.strictObject({ @@ -24,12 +28,26 @@ export const evidenceFileReferenceSchema = z.strictObject({ import { evidenceSchema } from "./evidence.js"; /** Portable observation or exact retained identity; semantic authentication follows admission. */ -export const evidenceInputSchema = z.union([ - evidenceSchema, - retainedEvidenceReferenceSchema, -]); +export const evidenceInputSchema = declareEvidenceInput( + z.union([evidenceSchema, retainedEvidenceReferenceSchema]), +); export type EvidenceInput = z.output; +/** Borrow a selected inline record until its resource and format owner admits it. */ +export const borrowEvidenceInput = () => { + const input = z.unknown().nonoptional(); + input._zod.processJSONSchema = evidenceInputSchema._zod.processJSONSchema; + return declareBorrowedEvidenceInput(input, evidenceInputSchema); +}; + +/** Borrow array width too; its service must admit capacity before resolution. */ +export const borrowEvidenceArray = () => { + const declaration = z.array(evidenceInputSchema); + const input = z.custom(Array.isArray); + input._zod.processJSONSchema = declaration._zod.processJSONSchema; + return declareBorrowedEvidenceInput(input, declaration); +}; + /** Evidence input, or one record read from a local Evidence file or bundle. */ export const evidenceSourceInputSchema = z.union([ ...evidenceInputSchema.options, diff --git a/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts b/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts index 6f2fff3640..772f2379cb 100644 --- a/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts +++ b/src/domain/javascript/javascriptApplicationVersionComparisonSchemas.ts @@ -1,7 +1,6 @@ import { z } from "zod"; import { emptyArraySchema } from "../emptyArraySchema.js"; -import { evidenceSchema } from "../evidence.js"; import { isValidatedImmutableJavaScriptApplicationGraph, javascriptApplicationGraphSchema, @@ -60,16 +59,6 @@ export const applicationVersionIdentitiesSchema = z }) .superRefine(validateApplicationVersionIdentities); -/** Two authenticated application versions and their native observations. */ -export const compareApplicationVersionsInputSchema = z - .strictObject({ - left: evidenceSchema, - right: evidenceSchema, - left_native_observations: z.array(evidenceSchema).default([]), - right_native_observations: z.array(evidenceSchema).default([]), - }) - .superRefine(validateApplicationVersionIdentities); - const candidateNodesSchema = z .tuple([nodeIdSchema], nodeIdSchema) .check(z.minLength(1)); diff --git a/src/domain/javascript/javascriptRuntimeReconciliation.ts b/src/domain/javascript/javascriptRuntimeReconciliation.ts index 443be04bb7..ba875d16e3 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliation.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliation.ts @@ -2,21 +2,23 @@ import { classifyStaticLoadStates } from "./javascriptRuntimeLoadState.js"; import { buildReconciledApplicationGraph } from "./javascriptRuntimeReconciliationGraph.js"; import { reconcileRuntimeEntities } from "./javascriptRuntimeReconciliationMatching.js"; import { - parseStaticLayers, + parseStaticLayer, type ParsedStaticLayer, } from "./javascriptRuntimeReconciliationParsing.js"; import { - parseRuntimeCaptures, + parseRuntimeCapture, type ParsedRuntimeCapture, } from "./javascriptRuntimeReconciliationCaptureParsing.js"; import { createJavaScriptRuntimeReconciliationResult } from "./javascriptRuntimeReconciliationResult.js"; import type { JavaScriptRuntimeReconciliationResult, + ReconcileJavaScriptRuntimeFrame, ReconcileJavaScriptRuntimeInput, } from "./javascriptRuntimeReconciliationSchemas.js"; import { projectRuntimeCaptures } from "./javascriptRuntimeReconciliationRuntime.js"; import { collectStaticRuntimeCandidates } from "./javascriptRuntimeStaticCandidates.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; +import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { err, ok, type Result } from "../result.js"; /** Verified caller input and the static and runtime facts parsed from it. */ @@ -28,15 +30,48 @@ export interface ParsedRuntimeReconciliationInput { /** Parse caller Evidence once into the owner consumed by reconciliation. */ export const readRuntimeReconciliationInput = ( - input: ReconcileJavaScriptRuntimeInput, + input: ReconcileJavaScriptRuntimeFrame, ): Result => { try { + const layers: ParsedStaticLayer[] = []; + const captures: ParsedRuntimeCapture[] = []; + // Preserve caller source order in Evidence parameters while the algorithm + // consumes deterministically sorted facts. Only authenticated records enter + // the canonical input returned to the Evidence constructor. + const staticLayers = input.static_layers.map((layer, index) => { + const parsed = parseStaticLayer(layer, [ + "static_layers", + index, + "analysis", + ]); + layers.push(parsed); + return { ...layer, analysis: parsed.evidence }; + }); + const runtimeObservations = input.runtime_observations.map( + (observation, index) => { + const parsed = parseRuntimeCapture(observation, [ + "runtime_observations", + index, + ]); + captures.push(parsed); + return parsed.evidence; + }, + ); return ok({ - input, - layers: parseStaticLayers(input.static_layers, ["static_layers"]), - captures: parseRuntimeCaptures(input.runtime_observations, [ - "runtime_observations", - ]), + input: { + ...input, + static_layers: staticLayers, + runtime_observations: runtimeObservations, + }, + layers: layers.sort((left, right) => + compareUnicodeCodePoints(left.layerId, right.layerId), + ), + captures: captures.sort((left, right) => + compareUnicodeCodePoints( + left.evidence.evidence_id, + right.evidence.evidence_id, + ), + ), }); } catch (cause: unknown) { if (cause instanceof AnalysisInputError) return err(cause); diff --git a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts index 1d05d20986..83f157dc9c 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts @@ -126,7 +126,7 @@ export type ParsedRuntimeCapture = /** Parse only supported passive web/Electron inspection Evidence. */ export const parseRuntimeCaptures = ( - observations: readonly Evidence[], + observations: readonly unknown[], path: readonly (string | number)[] = ["runtime_observations"], ): ParsedRuntimeCapture[] => observations @@ -140,8 +140,9 @@ export const parseRuntimeCaptures = ( ), ); -const parseRuntimeCapture = ( - input: Evidence, +/** Authenticate one source before interpreting its runtime capture. */ +export const parseRuntimeCapture = ( + input: unknown, evidencePath: readonly (string | number)[], ): ParsedRuntimeCapture => { const evidence = authenticateRuntimeEvidence(input, evidencePath); diff --git a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts index 928e004459..523ff03c3f 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationParsing.ts @@ -1,12 +1,11 @@ import { digestCanonicalValue } from "../canonicalDigest.js"; -import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import type { Evidence } from "../evidence.js"; import { javascriptApplicationAnalysisResultSchema, type JavaScriptApplicationAnalysisResult, } from "./javascriptApplicationAnalysis.js"; import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js"; -import type { ReconcileJavaScriptRuntimeInput } from "./javascriptRuntimeReconciliationSchemas.js"; +import type { ReconcileJavaScriptRuntimeFrame } from "./javascriptRuntimeReconciliationSchemas.js"; import { assertEvidenceIdentity, authenticateRuntimeEvidence, @@ -15,7 +14,7 @@ import { } from "./javascriptRuntimeReconciliationInputValidation.js"; type StaticLayerInput = - ReconcileJavaScriptRuntimeInput["static_layers"][number]; + ReconcileJavaScriptRuntimeFrame["static_layers"][number]; type RuntimeMapping = StaticLayerInput["runtime_mappings"][number]; export interface ParsedStaticLayer { @@ -27,20 +26,8 @@ export interface ParsedStaticLayer { readonly runtimeMappings: readonly RuntimeMapping[]; } -/** Parse and semantically bind every static analysis Evidence layer. */ -export const parseStaticLayers = ( - layers: readonly StaticLayerInput[], - path: readonly (string | number)[] = ["static_layers"], -): ParsedStaticLayer[] => - layers - .map((layer, index) => - parseStaticLayer(layer, [...path, index, "analysis"]), - ) - .sort((left, right) => - compareUnicodeCodePoints(left.layerId, right.layerId), - ); - -const parseStaticLayer = ( +/** Authenticate and semantically bind one admitted static analysis layer. */ +export const parseStaticLayer = ( layer: StaticLayerInput, evidencePath: readonly (string | number)[], ): ParsedStaticLayer => { diff --git a/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts b/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts index 46ee249c73..0d47a42b71 100644 --- a/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts +++ b/src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts @@ -3,10 +3,10 @@ import { z } from "zod"; import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { logicalPathEscapesRoot } from "../artifactIdentity.js"; import { evidenceSchema } from "../evidence.js"; +import type { EvidenceRequestFrameInput } from "../evidenceInputContract.js"; import { javascriptRuntimeUnresolvedLocationSchema } from "./javascriptRuntimeObservation.js"; import { javascriptApplicationGraphSchema } from "./javascriptApplicationGraph.js"; -import { digestSchema } from "../digests.js"; -import { prefixedDigestSchema } from "../digests.js"; +import { digestSchema, prefixedDigestSchema } from "../digests.js"; import { urlDeclaresQueryOrFragment } from "../urlComponents.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); @@ -95,41 +95,65 @@ function runtimeMappingKey(mapping: RuntimeLocationMapping): string { : `url-prefix\0${mapping.prefix}\0${mapping.artifact_prefix}`; } +const validateReconciliationIdentities = ( + input: { + readonly static_layers: readonly { + readonly role: "application" | "cache" | "assets"; + readonly analysis: { readonly evidence_id: string }; + }[]; + readonly runtime_observations: readonly { readonly evidence_id: string }[]; + }, + context: z.RefinementCtx, +): void => { + if ( + input.static_layers.filter(({ role }) => role === "application").length !== + 1 + ) + context.addIssue({ + code: "custom", + path: ["static_layers"], + message: "Exactly one application static layer is required", + }); + const staticIds = input.static_layers.map( + ({ analysis }) => analysis.evidence_id, + ); + if (new Set(staticIds).size !== staticIds.length) + context.addIssue({ + code: "custom", + path: ["static_layers"], + message: "Static layer Evidence must be unique", + }); + const runtimeIds = input.runtime_observations.map( + ({ evidence_id: id }) => id, + ); + if (new Set(runtimeIds).size !== runtimeIds.length) + context.addIssue({ + code: "custom", + path: ["runtime_observations"], + message: "Runtime observation Evidence must be unique", + }); +}; + +/** Verify layer roles and source uniqueness without copying retained payloads. */ +export const runtimeReconciliationIdentitiesSchema = z + .object({ + static_layers: z.array( + z.object({ + role: z.enum(["application", "cache", "assets"]), + analysis: z.object({ evidence_id: evidenceIdSchema }), + }), + ), + runtime_observations: z.array(z.object({ evidence_id: evidenceIdSchema })), + }) + .superRefine(validateReconciliationIdentities); + /** Evidence-backed static layers and passive captures to reconcile. */ export const reconcileJavaScriptRuntimeInputSchema = z .strictObject({ static_layers: z.array(staticLayerSchema).min(1), runtime_observations: z.array(evidenceSchema).min(1), }) - .superRefine((input, context) => { - if ( - input.static_layers.filter(({ role }) => role === "application") - .length !== 1 - ) - context.addIssue({ - code: "custom", - path: ["static_layers"], - message: "Exactly one application static layer is required", - }); - const staticIds = input.static_layers.map( - ({ analysis }) => analysis.evidence_id, - ); - if (new Set(staticIds).size !== staticIds.length) - context.addIssue({ - code: "custom", - path: ["static_layers"], - message: "Static layer Evidence must be unique", - }); - const runtimeIds = input.runtime_observations.map( - ({ evidence_id: id }) => id, - ); - if (new Set(runtimeIds).size !== runtimeIds.length) - context.addIssue({ - code: "custom", - path: ["runtime_observations"], - message: "Runtime observation Evidence must be unique", - }); - }); + .superRefine(validateReconciliationIdentities); const layerSummarySchema = z.strictObject({ layer_id: prefixedDigestSchema("jrl"), @@ -275,6 +299,9 @@ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({ export type ReconcileJavaScriptRuntimeInput = z.infer< typeof reconcileJavaScriptRuntimeInputSchema >; +/** Admitted ordinary request facts; each source still needs owner authentication. */ +export type ReconcileJavaScriptRuntimeFrame = + EvidenceRequestFrameInput; export type JavaScriptRuntimeReconciliationResult = z.infer< typeof javascriptRuntimeReconciliationResultSchema >; diff --git a/src/domain/managed/managedApplicationGraph.ts b/src/domain/managed/managedApplicationGraph.ts index 51811b7be5..2441e738e4 100644 --- a/src/domain/managed/managedApplicationGraph.ts +++ b/src/domain/managed/managedApplicationGraph.ts @@ -1,6 +1,8 @@ import { z } from "zod"; import type { Evidence } from "../evidence.js"; +import { evidenceInputSchema } from "../evidenceReference.js"; +import { getEvidenceInputContract } from "../evidenceInputContract.js"; import { createJavaScriptApplicationGraph, javascriptApplicationGraphSchema, @@ -44,26 +46,53 @@ import { err, ok, type Result } from "../result.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const textSchema = z.string().min(1); -/** Authenticated managed Evidence records projected into the application graph. */ -const managedGraphRequestSchema = z - .strictObject({ - managed_artifact: z.unknown().optional(), - managed_members: z.unknown().optional(), - managed_native_boundaries: z.unknown().optional(), - }) +const managedApplicationGraphReferenceShape = z.strictObject({ + managed_artifact: evidenceInputSchema.optional(), + managed_members: evidenceInputSchema.optional(), + managed_native_boundaries: evidenceInputSchema.optional(), +}); + +/** Inline or exact retained Evidence sources for managed application graph projection. */ +export const managedApplicationGraphReferenceInputSchema = z + .union([ + managedApplicationGraphReferenceShape.extend({ + managed_artifact: evidenceInputSchema, + }), + managedApplicationGraphReferenceShape.extend({ + managed_members: evidenceInputSchema, + }), + managedApplicationGraphReferenceShape.extend({ + managed_native_boundaries: evidenceInputSchema, + }), + ]) .superRefine((input, context) => { - if ( - input.managed_artifact === undefined && - input.managed_members === undefined && - input.managed_native_boundaries === undefined - ) + const sources = [ + input.managed_artifact, + input.managed_members, + input.managed_native_boundaries, + ].filter((source) => source !== undefined); + if (sources.length === 0) context.addIssue({ code: "custom", - message: - "At least one managed artifact, member, or native-boundary Evidence record is required", + message: "Provide at least one managed Evidence source", + }); + // The derived frame borrows unadmitted payloads. Their owner reports a + // malformed carrier rather than mistaking its missing ID for duplication. + if (sources.some((source) => typeof source?.evidence_id !== "string")) + return; + const ids = sources.map((source) => source.evidence_id); + if (new Set(ids).size !== ids.length) + context.addIssue({ + code: "custom", + message: "Managed application graph Evidence IDs must be unique", }); }); +/** Borrow source payloads until the managed owner authenticates each role. */ +const managedGraphRequestSchema = getEvidenceInputContract( + managedApplicationGraphReferenceInputSchema, +).frameSchema; + /** Managed-code projection result containing a validated application graph. */ export const managedApplicationGraphResultSchema = z.strictObject({ projection_id: prefixedDigestSchema("magp"), diff --git a/src/domain/managed/managedMemberComparison.ts b/src/domain/managed/managedMemberComparison.ts index dc2c9c69e1..ed4f45d8fc 100644 --- a/src/domain/managed/managedMemberComparison.ts +++ b/src/domain/managed/managedMemberComparison.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { emptyArraySchema } from "../emptyArraySchema.js"; import { evidenceSchema } from "../evidence.js"; +import { evidenceInputSchema } from "../evidenceReference.js"; import { cliMetadataGuidSchema, managedMetadataTokenSchema, @@ -28,20 +29,38 @@ import { digestSchema, prefixedDigestSchema } from "../digests.js"; const evidenceIdSchema = prefixedDigestSchema("ev"); const boundedTextSchema = z.string().min(1); -/** Two authenticated managed member observations. */ +const memberComparisonShape = (source: Source) => ({ + left: source, + right: source, +}); + +const distinctMemberSources = ( + input: { + readonly left: { readonly evidence_id: string }; + readonly right: { readonly evidence_id: string }; + }, + context: z.RefinementCtx, +): void => { + if ( + typeof input.left?.evidence_id === "string" && + input.left.evidence_id === input.right?.evidence_id + ) + context.addIssue({ + code: "custom", + path: ["right"], + message: "Managed member Evidence must be distinct", + }); +}; + +/** Two complete managed member observations for semantic consumers. */ export const compareManagedMembersInputSchema = z - .strictObject({ - left: evidenceSchema, - right: evidenceSchema, - }) - .superRefine((input, context) => { - if (input.left.evidence_id === input.right.evidence_id) - context.addIssue({ - code: "custom", - path: ["right"], - message: "Managed member Evidence must be distinct", - }); - }); + .strictObject(memberComparisonShape(evidenceSchema)) + .superRefine(distinctMemberSources); + +/** Public member comparison selects records inline or by exact retained identity. */ +export const compareManagedMembersRequestSchema = z + .strictObject(memberComparisonShape(evidenceInputSchema)) + .superRefine(distinctMemberSources); const concreteMatchBasisSchema = z.enum([ "exact-il-signature", diff --git a/src/domain/managed/managedNativeVerificationSchemas.ts b/src/domain/managed/managedNativeVerificationSchemas.ts index d2cdc875ea..1fa2f461b2 100644 --- a/src/domain/managed/managedNativeVerificationSchemas.ts +++ b/src/domain/managed/managedNativeVerificationSchemas.ts @@ -2,6 +2,7 @@ import { z } from "zod"; import { emptyArraySchema } from "../emptyArraySchema.js"; import { evidenceSchema } from "../evidence.js"; +import { evidenceInputSchema } from "../evidenceReference.js"; import { cliMetadataGuidSchema, managedNativeImportSchema, @@ -95,36 +96,57 @@ export const pinvokeVerificationSchema = z.union([ export type PinvokeVerification = z.infer; -/** Authenticated managed boundary Evidence plus native observations. */ +const nativeVerificationShape = (source: Source) => ({ + managed_boundaries: source, + native_observations: z + .array(source) + .min(1) + .describe("Native function or export Evidence records"), +}); + +const distinctNativeSources = ( + input: { + readonly managed_boundaries: { readonly evidence_id: string }; + readonly native_observations: readonly { readonly evidence_id: string }[]; + }, + context: z.RefinementCtx, +): void => { + // Borrowing frames defer malformed carriers to the semantic source owner. + if ( + typeof input.managed_boundaries?.evidence_id !== "string" || + input.native_observations.some( + (evidence) => typeof evidence?.evidence_id !== "string", + ) + ) + return; + const ids = new Set(); + for (const [index, evidence] of input.native_observations.entries()) { + if (evidence.evidence_id === input.managed_boundaries.evidence_id) + context.addIssue({ + code: "custom", + path: ["native_observations", index], + message: + "Native observation Evidence must be distinct from managed boundary Evidence", + }); + if (ids.has(evidence.evidence_id)) + context.addIssue({ + code: "custom", + path: ["native_observations", index], + message: "Native observation Evidence IDs must be unique", + }); + ids.add(evidence.evidence_id); + } +}; + +/** Complete record inputs used by the managed semantic owner and fixture builders. */ export const managedNativeVerificationInputSchema = z - .strictObject({ - managed_boundaries: evidenceSchema, - native_observations: z - .array(evidenceSchema) - .min(1) - .describe("Native function or export Evidence records"), - }) - .superRefine((input, context) => { - const ids = new Set(); - for (const [index, evidence] of input.native_observations.entries()) { - if (evidence.evidence_id === input.managed_boundaries.evidence_id) { - context.addIssue({ - code: "custom", - path: ["native_observations", index], - message: - "Native observation Evidence must be distinct from managed boundary Evidence", - }); - } - if (ids.has(evidence.evidence_id)) { - context.addIssue({ - code: "custom", - path: ["native_observations", index], - message: "Native observation Evidence IDs must be unique", - }); - } - ids.add(evidence.evidence_id); - } - }); + .strictObject(nativeVerificationShape(evidenceSchema)) + .superRefine(distinctNativeSources); + +/** Public workflow sources may be inline records or exact retained identities. */ +export const managedNativeVerificationRequestSchema = z + .strictObject(nativeVerificationShape(evidenceInputSchema)) + .superRefine(distinctNativeSources); const verificationCoverageSchema = z.strictObject({ status: z.enum(["complete-within-inputs", "partial"]), diff --git a/src/domain/managed/managedNativeVerificationSource.ts b/src/domain/managed/managedNativeVerificationSource.ts index e5fe294467..58e8a0bbe3 100644 --- a/src/domain/managed/managedNativeVerificationSource.ts +++ b/src/domain/managed/managedNativeVerificationSource.ts @@ -1,4 +1,5 @@ -import { z } from "zod"; +import { getEvidenceInputContract } from "../evidenceInputContract.js"; +import { managedNativeVerificationRequestSchema } from "./managedNativeVerificationSchemas.js"; import type { Evidence } from "../evidence.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; @@ -19,10 +20,9 @@ import type { NativeSymbol } from "./managedNativeVerificationSchemas.js"; const OPERATION = "verify_managed_native_boundaries" as const; const MANAGED_OPERATION = "inspect_managed_native_boundaries" as const; -const requestFrameSchema = z.strictObject({ - managed_boundaries: z.unknown(), - native_observations: z.array(z.unknown()).min(1), -}); +const requestFrameSchema = getEvidenceInputContract( + managedNativeVerificationRequestSchema, +).frameSchema; export interface ManagedNativeVerificationSource { readonly managedEvidence: Evidence; diff --git a/src/domain/managed/managedReconstruction.ts b/src/domain/managed/managedReconstruction.ts index 1ca256b8a7..994c17dc4e 100644 --- a/src/domain/managed/managedReconstruction.ts +++ b/src/domain/managed/managedReconstruction.ts @@ -3,6 +3,8 @@ import { createHash } from "node:crypto"; import { z } from "zod"; import { evidenceSchema, type Evidence } from "../evidence.js"; +import { evidenceInputSchema } from "../evidenceReference.js"; +import type { EvidenceRequestFrameInput } from "../evidenceInputContract.js"; import { cliMetadataGuidSchema, managedMetadataTokenSchema, @@ -47,22 +49,24 @@ type ManagedReconstructionMethodInput = z.infer< >; const managedReconstructionImportRequestShape = { - static_members: z.unknown(), decompiler: decompilerSchema, methods: z.array(managedReconstructionMethodInputSchema).min(1), notes: z.array(textSchema).default([]), }; -/** Request envelope parsed before interpreting the nested Evidence source. */ -export const managedReconstructionImportRequestSchema = z.strictObject( - managedReconstructionImportRequestShape, -); +const reconstructionInput = (source: Source) => + z.strictObject({ + ...managedReconstructionImportRequestShape, + static_members: source, + }); -/** Public reconstruction request with the complete source Evidence contract. */ -export const managedReconstructionImportInputSchema = z.strictObject({ - ...managedReconstructionImportRequestShape, - static_members: evidenceSchema, -}); +/** Complete semantic reconstruction input with its original source record. */ +export const managedReconstructionImportInputSchema = + reconstructionInput(evidenceSchema); + +/** Public reconstruction request accepts records inline or by exact retained identity. */ +export const managedReconstructionImportRequestSchema = + reconstructionInput(evidenceInputSchema); const importedMethodSchema = z.strictObject({ token: managedMetadataTokenSchema, @@ -123,9 +127,8 @@ export const managedReconstructionImportResultSchema = z.strictObject({ export type ManagedReconstructionImportInput = z.infer< typeof managedReconstructionImportInputSchema >; -export type ManagedReconstructionImportRequest = z.infer< - typeof managedReconstructionImportRequestSchema ->; +export type ManagedReconstructionImportRequest = + EvidenceRequestFrameInput; export type ManagedReconstructionImportResult = z.infer< typeof managedReconstructionImportResultSchema >; diff --git a/src/domain/mobileApplicationGraphProjection.ts b/src/domain/mobileApplicationGraphProjection.ts index b584eb6089..9927a8afb7 100644 --- a/src/domain/mobileApplicationGraphProjection.ts +++ b/src/domain/mobileApplicationGraphProjection.ts @@ -6,7 +6,8 @@ import { ArtifactInventoryCarrierError, type ParsedInventorySet, } from "./artifactInventoryEvidence.js"; -import { evidenceSchema } from "./evidence.js"; +import { evidenceInputSchema } from "./evidenceReference.js"; +import { getEvidenceInputContract } from "./evidenceInputContract.js"; import { AnalysisInputError } from "./analysisErrorCore.js"; import { EvidenceIntegrityError } from "./evidenceErrors.js"; import { analysisInputErrorFromIssues } from "./inputIssueProjection.js"; @@ -19,13 +20,12 @@ export const projectionPathSchema = z.string().min(1); /** Inventory pages accepted by every application graph projection. */ export const applicationInventoryProjectionInputSchema = z.strictObject({ - inventory_evidence: z.array(evidenceSchema).min(1), + inventory_evidence: z.array(evidenceInputSchema).min(1), }); -const applicationInventoryRequestSchema = - applicationInventoryProjectionInputSchema.extend({ - inventory_evidence: z.array(z.unknown()).min(1), - }); +const applicationInventoryRequestSchema = getEvidenceInputContract( + applicationInventoryProjectionInputSchema, +).frameSchema; /** Resolve the request and authenticate inventory pages once for all projections. */ export const readApplicationInventoryProjection = ( diff --git a/src/generatedPackageMetadata.ts b/src/generatedPackageMetadata.ts index 4249a7ca04..2d67071390 100644 --- a/src/generatedPackageMetadata.ts +++ b/src/generatedPackageMetadata.ts @@ -6,5 +6,5 @@ export const PACKAGE_METADATA = { serverSdkVersion: "2.3.1", clientSdkVersion: "2.3.1", coreSdkVersion: "2.3.1", - skillVersion: "58", + skillVersion: "59", } as const; diff --git a/src/server/registerApplicationTools/compareExportShapes.ts b/src/server/registerApplicationTools/compareExportShapes.ts index 81123d7b1e..b64c68d630 100644 --- a/src/server/registerApplicationTools/compareExportShapes.ts +++ b/src/server/registerApplicationTools/compareExportShapes.ts @@ -34,6 +34,7 @@ export const registerCompareJavaScriptExportShapesTool = ( compareJavaScriptExportShapesEvidence( input, options.evidenceById, + { signal: context.mcpReq.signal }, ), ), ); diff --git a/src/server/registerApplicationTools/compareSourceToBundle.ts b/src/server/registerApplicationTools/compareSourceToBundle.ts index 2f2ea545dc..4dcbaf3a9c 100644 --- a/src/server/registerApplicationTools/compareSourceToBundle.ts +++ b/src/server/registerApplicationTools/compareSourceToBundle.ts @@ -1,17 +1,15 @@ import { setImmediate } from "node:timers/promises"; import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; import { ok } from "../../domain/result.js"; -import { resolveSourceToBundleReference } from "../../application/javascript/SourceToBundleReference.js"; import { requireAnalysisViewRetention, inspectAnalysisViewValidated, } from "../../application/analysisView/AnalysisViewService.js"; import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; -import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordEvidenceSources } from "../../application/investigation/recordEvidenceSources.js"; -import { compareSourceToBundleEvidence } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; +import { executeSourceToBundleComparison } from "../../application/javascript/JavaScriptApplicationWorkflowService.js"; import { toolContract } from "../../contracts/toolContracts.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; @@ -43,27 +41,17 @@ export const registerCompareSourceToBundleTool = ( if (!retention.ok) return server.delivery.toCallToolResult(retention, contract); } - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const reference = await resolveSourceToBundleReference( - resolved.value.reference, - context.mcpReq.signal, - options.artifactResources, - ); - if (!reference.ok) - return server.delivery.toCallToolResult(reference, contract); - const parsed = { - ...reference.value, - application: resolved.value.application, - }; const result = await logToolExecution( options.logger, contract.name, - () => Promise.resolve(compareSourceToBundleEvidence(parsed)), + () => + executeSourceToBundleComparison( + input, + contract.inputSchema, + options.evidenceById, + options.artifactResources, + { signal: context.mcpReq.signal }, + ), ); if (!result.ok) return server.delivery.toCallToolResult(result, contract); diff --git a/src/server/registerApplicationTools/compareVersions.ts b/src/server/registerApplicationTools/compareVersions.ts index d2a5580dda..49fee7c07e 100644 --- a/src/server/registerApplicationTools/compareVersions.ts +++ b/src/server/registerApplicationTools/compareVersions.ts @@ -31,7 +31,11 @@ export const registerCompareApplicationVersionsTool = ( compareContract.name, () => Promise.resolve( - compareApplicationVersionsEvidence(input, options.evidenceById), + compareApplicationVersionsEvidence( + input, + options.evidenceById, + { signal: context.mcpReq.signal }, + ), ), ); if (!result.ok) diff --git a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts index 4ab5b4ff53..b1256009c8 100644 --- a/src/server/registerApplicationTools/projectMobileApplicationGraph.ts +++ b/src/server/registerApplicationTools/projectMobileApplicationGraph.ts @@ -1,3 +1,5 @@ +import type { EvidenceLookup } from "../../application/EvidenceInputResolver.js"; +import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import type { ServerContext } from "@modelcontextprotocol/server"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; @@ -47,6 +49,8 @@ const registerProjection = (context: { readonly contract: (typeof APPLICATION_TOOL_CONTRACTS)[number]; readonly project: ( input: unknown, + lookup?: EvidenceLookup, + options?: Pick, ) => Result; }): void => { const { server, options, contract, project } = context; @@ -63,7 +67,12 @@ const registerProjection = (context: { const result = await logToolExecution( options.logger, contract.name, - () => Promise.resolve(project(input)), + () => + Promise.resolve( + project(input, options.evidenceById, { + signal: request.mcpReq.signal, + }), + ), ); if (!result.ok) return server.delivery.toCallToolResult(result, contract); diff --git a/src/server/registerApplicationTools/traceFeature.ts b/src/server/registerApplicationTools/traceFeature.ts index c808afcebe..bab24de1c0 100644 --- a/src/server/registerApplicationTools/traceFeature.ts +++ b/src/server/registerApplicationTools/traceFeature.ts @@ -31,7 +31,9 @@ export const registerTraceFeatureTool = ( traceContract.name, () => Promise.resolve( - traceApplicationFeatureEvidence(input, options.evidenceById), + traceApplicationFeatureEvidence(input, options.evidenceById, { + signal: context.mcpReq.signal, + }), ), ); if (!result.ok) diff --git a/src/server/registerApplicationTools/traceSemantics.ts b/src/server/registerApplicationTools/traceSemantics.ts index 424a53f60f..4faa427c46 100644 --- a/src/server/registerApplicationTools/traceSemantics.ts +++ b/src/server/registerApplicationTools/traceSemantics.ts @@ -1,9 +1,8 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; -import { resolveApplicationEvidenceRequest } from "../../application/EvidenceInputResolver.js"; import { recordEvidenceSources } from "../../application/investigation/recordEvidenceSources.js"; -import { traceJavaScriptSemanticsEvidenceValidated } from "../../application/javascript/JavaScriptSemanticTraceService.js"; +import { traceJavaScriptSemanticsEvidence } from "../../application/javascript/JavaScriptSemanticTraceService.js"; import { toolContract } from "../../contracts/toolContracts.js"; import { logToolExecution } from "../toolLogging.js"; import { toolRegistrationOptions } from "../toolRegistrationOptions.js"; @@ -27,32 +26,28 @@ export const registerTraceJavaScriptSemanticsTool = ( contract.name, context.mcpReq.signal, async () => { - const resolved = resolveApplicationEvidenceRequest( - input, - options.evidenceById, - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const parsed = resolved.value; const result = await logToolExecution( options.logger, contract.name, () => Promise.resolve( - traceJavaScriptSemanticsEvidenceValidated(parsed), + traceJavaScriptSemanticsEvidence(input, options.evidenceById, { + signal: context.mcpReq.signal, + }), ), ); if (!result.ok) return server.delivery.toCallToolResult(result, contract); - const recorded = recordEvidenceSources(options.recordEvidence, [ - parsed.application, - ]); + const recorded = recordEvidenceSources( + options.recordEvidence, + result.value.sources, + ); if (!recorded.ok) return server.delivery.toCallToolResult(recorded, contract); return recordResult( { ...options, delivery: server.delivery }, contract, - result.value, + result.value.evidence, ); }, ), diff --git a/src/server/registerArtifactComparisonTool.ts b/src/server/registerArtifactComparisonTool.ts index 7df3c66b0a..5542f95fff 100644 --- a/src/server/registerArtifactComparisonTool.ts +++ b/src/server/registerArtifactComparisonTool.ts @@ -11,7 +11,7 @@ import { import type { RecordUnknownInput } from "../domain/residualUnknown.js"; import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; -import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; +import { applicationToolRegistrationOptions } from "./toolRegistrationOptions.js"; import { withAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js"; @@ -27,7 +27,7 @@ export const registerArtifactComparisonTool = ( const evidenceById = session.evidenceForAnalysis.bind(session); server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), async (input, context) => runAdmittedToolOperation( server, diff --git a/src/server/registerElectronTools.ts b/src/server/registerElectronTools.ts index e197f14ca8..4777a32f2e 100644 --- a/src/server/registerElectronTools.ts +++ b/src/server/registerElectronTools.ts @@ -16,7 +16,8 @@ import { } from "../application/javascript/ElectronObservationService.js"; import { analyzeJavaScriptApplicationValidated } from "../application/javascript/JavaScriptApplicationService.js"; import type { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; -import { reconcileJavaScriptRuntimeEvidenceValidated } from "../application/javascript/JavaScriptRuntimeReconciliationService.js"; +import { reconcileJavaScriptRuntimeEvidence } from "../application/javascript/JavaScriptRuntimeReconciliationService.js"; +import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js"; import type { ElectronFuseService } from "../application/electron/ElectronFuseService.js"; import { toolContract } from "../contracts/toolContracts.js"; import type { Logger } from "pino"; @@ -138,8 +139,21 @@ export const registerElectronTools = ( reconcileContract, undefined, { input, context }, - (parsed) => - Promise.resolve(reconcileJavaScriptRuntimeEvidenceValidated(parsed)), + async (parsed, { signal }) => { + const result = reconcileJavaScriptRuntimeEvidence( + parsed, + options.evidenceById, + { signal }, + ); + if (!result.ok) return result; + const recorded = recordEvidenceSources( + options.recordEvidence, + result.value.sources, + ); + return recorded.ok + ? { ok: true, value: result.value.evidence } + : recorded; + }, ), ); server.registerTool( diff --git a/src/server/registerFunctionComparisonTool.ts b/src/server/registerFunctionComparisonTool.ts index 5c036c4f9a..ccf4c24682 100644 --- a/src/server/registerFunctionComparisonTool.ts +++ b/src/server/registerFunctionComparisonTool.ts @@ -1,22 +1,18 @@ -import { resolvePairedEvidenceRequest } from "../application/EvidenceInputResolver.js"; import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; - import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; +import { compareFunctionEvidence } from "../application/investigation/FunctionComparisonEvidence.js"; +import { + recordEvidenceSources, + retainAnalysisResultSources, +} from "../application/investigation/recordEvidenceSources.js"; import { toolContract } from "../contracts/toolContracts.js"; -import { createEvidence } from "../domain/evidence.js"; -import { parseFunctionEvidence } from "../domain/functionDossierEvidence.js"; -import { FUNCTION_COMPARISON_PROVIDER } from "../domain/comparisonEvidenceIdentity.js"; -import type { RecordUnknownInput } from "../domain/residualUnknown.js"; import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; -import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { withAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; -import { compareFunctions } from "../domain/functionComparison.js"; -import { uniqueSorted } from "../domain/canonicalOrdering.js"; -/** Register explicit Evidence-backed function comparison. */ +/** Adapt the shared function-comparison workflow to the scoped MCP session. */ export const registerFunctionComparisonTool = ( server: EvidenceMcpServer, session: BinarySessionPort, @@ -33,151 +29,44 @@ export const registerFunctionComparisonTool = ( contract.name, context.mcpReq.signal, async () => { - const resolved = resolvePairedEvidenceRequest( - input, - session.evidenceForAnalysis.bind(session), - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); const computed = await runDerivedOperation( context, contract.name, () => { - const left = parseFunctionEvidence(resolved.value.left, { - operation: contract.name, - path: ["left"], - }); - if (!left.ok) throw left.error; - const right = parseFunctionEvidence(resolved.value.right, { - operation: contract.name, - path: ["right"], - }); - if (!right.ok) throw right.error; - return { - left: left.value, - right: right.value, - comparison: compareFunctions(left.value, right.value), - }; + const result = compareFunctionEvidence( + input, + session.evidenceForAnalysis.bind(session), + context.mcpReq.signal, + ); + if (!result.ok) throw result.error; + return result.value; }, + (value) => value, ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const { left, right, comparison } = computed.value; - const leftEvidence = left.evidence; - const rightEvidence = right.evidence; - const leftIds = [leftEvidence.evidence_id]; - const rightIds = [rightEvidence.evidence_id]; - const recordedSources = recordEvidenceSources( - (evidence) => session.recordEvidence(evidence), - [leftEvidence, rightEvidence], + const completed = retainAnalysisResultSources( + contract.name, + session.recordEvidence.bind(session), + computed, ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, contract); - const evidence = createEvidence( - undefined, - FUNCTION_COMPARISON_PROVIDER, - { - predicateType: "rea.function-comparison", - operation: contract.name, - parameters: { - left_evidence_id: leftEvidence.evidence_id, - right_evidence_id: rightEvidence.evidence_id, - }, - result: comparison, - confidence: "derived", - authority: "analyst-inference", - limitations: comparison.limitations, - evidenceLinks: [...leftIds, ...rightIds], - }, - ); - const recorded = recordDerivedEvidence( - session, - evidence, - comparisonUnknownInput( - comparison, - leftIds, - rightIds, - evidence.evidence_id, - ), + if (!completed.ok) + return server.delivery.toPartialErrorToolResult( + completed.error, + contract.returnsTargetContent, + session.recordEvidence.bind(session), + ); + const { evidence, sources, unknown } = completed.value; + const retained = recordEvidenceSources( + session.recordEvidence.bind(session), + sources, ); + if (!retained.ok) + return server.delivery.toCallToolResult(retained, contract); return server.delivery.toEvidenceToolResult( evidence, contract, - recorded, + recordDerivedEvidence(session, evidence, unknown), ); }, ), ); }; - -const comparisonUnknownInput = ( - comparison: ReturnType, - leftIds: readonly string[], - rightIds: readonly string[], - comparisonEvidenceId: string, -): RecordUnknownInput | undefined => - functionUnknownInput({ - status: comparison.status, - dimensions: comparison.dimensions - .filter(({ status }) => - comparison.status === "unknown" - ? status === "unknown" - : status === "changed", - ) - .map(({ dimension }) => dimension), - leftIds, - rightIds, - comparisonEvidenceId, - }); - -const functionUnknownInput = ({ - status, - dimensions, - leftIds, - rightIds, - comparisonEvidenceId, -}: { - status: ReturnType["status"]; - dimensions: readonly ReturnType< - typeof compareFunctions - >["dimensions"][number]["dimension"][]; - leftIds: readonly string[]; - rightIds: readonly string[]; - comparisonEvidenceId: string; -}): RecordUnknownInput | undefined => { - if (status === "unchanged") return undefined; - const supportingIds = [ - ...new Set([...leftIds, ...rightIds, comparisonEvidenceId]), - ]; - const contradictingIds = rightIds.filter((id) => !leftIds.includes(id)); - const contradictory = status === "changed"; - return { - question: `Function comparison is ${status} (comparison ${comparisonEvidenceId})`, - severity: status === "changed" ? "medium" : "high", - domain: "function-comparison", - comparison_conclusion_scope: { - comparison_evidence_id: comparisonEvidenceId, - dimensions: uniqueSorted([ - ...(status === "unknown" || dimensions.length === 0 - ? ["overall" as const] - : []), - ...dimensions, - ]), - }, - supporting_evidence_ids: contradictory - ? [...leftIds, comparisonEvidenceId] - : supportingIds, - contradicting_evidence_ids: contradictory ? contradictingIds : [], - required_authority: "shipped-artifact", - required_confidence: "observed", - required_environment: null, - recommended_probes: [ - { - operation: "analyze_function", - rationale: - "Capture complete dossiers for both functions under the same target context.", - }, - ], - relationships: [], - }; -}; diff --git a/src/server/registerInvestigationTools.ts b/src/server/registerInvestigationTools.ts index 6f0c8d61cc..66d5b63c92 100644 --- a/src/server/registerInvestigationTools.ts +++ b/src/server/registerInvestigationTools.ts @@ -1,51 +1,57 @@ -import { resolveInvestigationEvidenceClosure } from "../application/investigation/InvestigationEvidence.js"; -import { resolveChangedBehaviorEvidence } from "../application/investigation/ChangedBehaviorEvidence.js"; import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; - import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; +import { resolveInvestigationEvidenceClosure } from "../application/investigation/InvestigationEvidence.js"; +import { + buildCallPathEvidence, + findChangedBehaviorEvidence, + correlateStaticAndRuntimeEvidence, + type InvestigationWorkflowExecution, +} from "../application/investigation/InvestigationWorkflows.js"; +import { + recordEvidenceSources, + retainAnalysisResultSources, +} from "../application/investigation/recordEvidenceSources.js"; +import type { AnalysisError } from "../domain/analysisErrorBase.js"; +import type { Result } from "../domain/result.js"; import { toolContract } from "../contracts/toolContracts.js"; -import { buildCallPath } from "../domain/callPath.js"; -import { findChangedBehavior } from "../domain/changedBehavior.js"; import { createEvidence } from "../domain/evidence.js"; -import { correlateStaticAndRuntime } from "../domain/staticRuntimeCorrelation.js"; -import { - comparisonClosure, - isIncomplete, - recordWorkflowEvidence, -} from "./registerInvestigationTools/helpers.js"; +import { recordWorkflowEvidence } from "./registerInvestigationTools/helpers.js"; +import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; +import { RECONSTRUCTION_PROVIDER } from "../domain/investigationProviders.js"; import { - CALL_PATH_PROVIDER, - CHANGED_BEHAVIOR_PROVIDER, - RECONSTRUCTION_PROVIDER, - STATIC_RUNTIME_PROVIDER, -} from "../domain/investigationProviders.js"; -import { - applicationToolRegistrationOptions, toolRegistrationOptions, + applicationToolRegistrationOptions, } from "./toolRegistrationOptions.js"; import { withAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; - import { verifyReconstruction } from "../domain/reconstructionVerification.js"; -/** Register Evidence-composed differential investigation workflows. */ + export const registerInvestigationTools = ( server: EvidenceMcpServer, session: BinarySessionPort, ): void => { const admission = withAdmittedAnalysis({ kind: "session", session }); - registerChangedBehavior( + registerInvestigationWorkflow( server, session, admission, toolContract("find_changed_behavior"), + findChangedBehaviorEvidence, ); - registerCallPath(server, session, admission, toolContract("build_call_path")); - registerStaticRuntime( + registerInvestigationWorkflow( + server, + session, + admission, + toolContract("build_call_path"), + buildCallPathEvidence, + ); + registerInvestigationWorkflow( server, session, admission, toolContract("correlate_static_and_runtime"), + correlateStaticAndRuntimeEvidence, ); registerReconstruction( server, @@ -55,89 +61,24 @@ export const registerInvestigationTools = ( ); }; -const registerChangedBehavior = ( +type RecordWorkflowName = + | "find_changed_behavior" + | "build_call_path" + | "correlate_static_and_runtime"; +const registerInvestigationWorkflow = ( server: EvidenceMcpServer, session: BinarySessionPort, admission: ReturnType, - contract: ReturnType>, -): void => { - const registration = applicationToolRegistrationOptions(contract); - server.registerTool(contract.name, registration, async (input, context) => - runAdmittedToolOperation( - server, - admission, - contract.name, - context.mcpReq.signal, - async () => { - const resolved = resolveChangedBehaviorEvidence( - input, - session.evidenceForAnalysis.bind(session), - ); - if (!resolved.ok) - return server.delivery.toCallToolResult(resolved, contract); - const computed = await runDerivedOperation(context, contract.name, () => - findChangedBehavior(resolved.value), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const { result, comparisonEvidenceIds } = computed.value; - const closure = resolveInvestigationEvidenceClosure( - session, - result.evidence_links, - ); - if (!closure.ok) - return server.delivery.toCallToolResult(closure, contract); - const links = closure.value; - const evidence = createEvidence(undefined, CHANGED_BEHAVIOR_PROVIDER, { - predicateType: "rea.changed-behavior", - operation: contract.name, - parameters: { - comparison_evidence_ids: [...comparisonEvidenceIds], - }, - result, - confidence: "derived", - authority: "analyst-inference", - limitations: result.limitations, - evidenceLinks: links, - }); - const recorded = recordWorkflowEvidence( - session, - evidence, - isIncomplete(result.behavior_status), - { - question: - "Did both versions behave the same under a complete controlled replay?", - domain: "changed-behavior", - requiredAuthority: "controlled-replay", - requiredConfidence: "observed", - probes: [ - { - operation: "capture_process_scenario", - rationale: - "Capture both versions under the same bounded scenario and environment.", - }, - ], - }, - ); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recorded, - ); - }, - ), - ); -}; - -const registerCallPath = ( - server: EvidenceMcpServer, - session: BinarySessionPort, - admission: ReturnType, - contract: ReturnType>, + contract: ReturnType>, + execute: ( + input: unknown, + reader: BinarySessionPort, + signal?: AbortSignal, + ) => Result, ): void => { server.registerTool( contract.name, - toolRegistrationOptions(contract), + applicationToolRegistrationOptions(contract), async (input, context) => runAdmittedToolOperation( server, @@ -145,131 +86,38 @@ const registerCallPath = ( contract.name, context.mcpReq.signal, async () => { - const closure = resolveInvestigationEvidenceClosure( - session, - input.functions.map(({ evidence_id }) => evidence_id), - ); - if (!closure.ok) - return server.delivery.toCallToolResult(closure, contract); - const links = closure.value; const computed = await runDerivedOperation( context, contract.name, - () => buildCallPath(input), - ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const result = computed.value; - const evidence = createEvidence(undefined, CALL_PATH_PROVIDER, { - predicateType: "rea.call-path", - operation: contract.name, - parameters: { - start: input.start.address, - goal: input.goal.address, - }, - result, - confidence: "derived", - authority: "analyst-inference", - limitations: result.limitations, - evidenceLinks: links, - }); - const recorded = recordWorkflowEvidence( - session, - evidence, - result.status === "unknown", - { - question: - "Can the requested call path be established from complete analysis?", - domain: "call-path", - requiredAuthority: "shipped-artifact", - requiredConfidence: "derived", - probes: [ - { - operation: "analyze_function", - rationale: - "Collect complete callee dossiers for the reported frontier addresses.", - }, - ], + () => { + const result = execute(input, session, context.mcpReq.signal); + if (!result.ok) throw result.error; + return result.value; }, + (value) => value, ); - return server.delivery.toEvidenceToolResult( - evidence, - contract, - recorded, - ); - }, - ), - ); -}; - -const registerStaticRuntime = ( - server: EvidenceMcpServer, - session: BinarySessionPort, - admission: ReturnType, - contract: ReturnType>, -): void => { - server.registerTool( - contract.name, - toolRegistrationOptions(contract), - async (input, context) => - runAdmittedToolOperation( - server, - admission, - contract.name, - context.mcpReq.signal, - async () => { - const closure = resolveInvestigationEvidenceClosure( - session, - comparisonClosure([ - ...input.static_comparisons, - ...input.runtime_comparisons, - ]), - ); - if (!closure.ok) - return server.delivery.toCallToolResult(closure, contract); - const links = closure.value; - const computed = await runDerivedOperation( - context, + const completed = retainAnalysisResultSources( contract.name, - () => correlateStaticAndRuntime(input), + session.recordEvidence.bind(session), + computed, ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const result = computed.value; - const evidence = createEvidence(undefined, STATIC_RUNTIME_PROVIDER, { - predicateType: "rea.static-runtime-correlation", - operation: contract.name, - parameters: { - mapping_count: input.mappings.length, - }, - result, - confidence: "inferred", - authority: "analyst-inference", - limitations: result.limitations, - evidenceLinks: links, - }); + if (!completed.ok) + return server.delivery.toPartialErrorToolResult( + completed.error, + contract.returnsTargetContent, + session.recordEvidence.bind(session), + ); + const { evidence, sources, unknown } = completed.value; + const retained = recordEvidenceSources( + session.recordEvidence.bind(session), + sources, + ); + if (!retained.ok) + return server.delivery.toCallToolResult(retained, contract); return server.delivery.toEvidenceToolResult( evidence, contract, - recordWorkflowEvidence( - session, - evidence, - result.status === "unknown" || result.status === "truncated", - { - question: - "Does runtime behavior match the available static analysis?", - domain: "static-runtime-correlation", - requiredAuthority: null, - requiredConfidence: "derived", - probes: [ - { - operation: "capture_process_scenario", - rationale: - "Repeat runtime observations and complete the mapped static comparison Evidence.", - }, - ], - }, - ), + recordDerivedEvidence(session, evidence, unknown), ); }, ), diff --git a/src/server/registerInvestigationTools/helpers.ts b/src/server/registerInvestigationTools/helpers.ts index 9d1a8ea0d2..c74c3199a3 100644 --- a/src/server/registerInvestigationTools/helpers.ts +++ b/src/server/registerInvestigationTools/helpers.ts @@ -5,18 +5,6 @@ import type { Result } from "../../domain/result.js"; import { recordDerivedEvidence } from "../recordDerivedEvidence.js"; import { workflowUnknownInput } from "../../application/investigation/WorkflowUnknown.js"; import type { WorkflowUnknownInput } from "./types.js"; -import { uniqueSorted } from "../../domain/canonicalOrdering.js"; - -export const isIncomplete = (status: string): boolean => - status === "unknown" || status === "truncated"; - -export const comparisonClosure = (comparisons: readonly Evidence[]): string[] => - uniqueSorted( - comparisons.flatMap((evidence) => [ - evidence.evidence_id, - ...evidence.evidence_links, - ]), - ); export const recordWorkflowEvidence = ( ...[session, evidence, unresolved, input]: readonly [ diff --git a/src/server/registerManagedWorkflowTools/compareManagedMembers.ts b/src/server/registerManagedWorkflowTools/compareManagedMembers.ts index 5cde172ce1..1102bf831f 100644 --- a/src/server/registerManagedWorkflowTools/compareManagedMembers.ts +++ b/src/server/registerManagedWorkflowTools/compareManagedMembers.ts @@ -29,7 +29,14 @@ export const registerCompareManagedMembers = ( const result = await logToolExecution( options.logger, compareContract.name, - () => Promise.resolve(executeManagedMemberComparison(input)), + () => + Promise.resolve( + executeManagedMemberComparison( + input, + options.session.evidenceForAnalysis.bind(options.session), + { signal: context.mcpReq.signal }, + ), + ), ); if (!result.ok) return server.delivery.toCallToolResult(result, compareContract); diff --git a/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts b/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts index cf43482b3a..1193a555c4 100644 --- a/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts +++ b/src/server/registerManagedWorkflowTools/importManagedReconstruction.ts @@ -27,7 +27,11 @@ export const registerImportManagedReconstruction = ( reconstructionContract.name, context.mcpReq.signal, async () => { - const resolved = resolveManagedReconstructionEvidenceInput(input); + const resolved = resolveManagedReconstructionEvidenceInput( + input, + options.session.evidenceForAnalysis.bind(options.session), + { signal: context.mcpReq.signal }, + ); if (!resolved.ok) return server.delivery.toCallToolResult( resolved, diff --git a/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts b/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts index 7b185fa241..f35e5bf6f2 100644 --- a/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts +++ b/src/server/registerManagedWorkflowTools/projectManagedApplicationGraph.ts @@ -2,8 +2,10 @@ import type { EvidenceMcpServer } from "../EvidenceMcpServer.js"; import { runAdmittedToolOperation } from "../admittedToolOperation.js"; import { recordEvidenceSources } from "../../application/investigation/recordEvidenceSources.js"; -import { projectManagedGraphEvidence } from "../../application/managed/ManagedApplicationGraphService.js"; -import { readManagedGraphProjection } from "../../domain/managed/managedApplicationGraph.js"; +import { + projectManagedGraphEvidence, + resolveManagedGraphEvidenceInput, +} from "../../application/managed/ManagedApplicationGraphService.js"; import type { Evidence } from "../../domain/evidence.js"; import { ok } from "../../domain/result.js"; import { logToolExecution } from "../toolLogging.js"; @@ -28,7 +30,11 @@ export const registerProjectManagedApplicationGraph = ( graphContract.name, context.mcpReq.signal, async () => { - const request = readManagedGraphProjection(input); + const request = resolveManagedGraphEvidenceInput( + input, + options.session.evidenceForAnalysis.bind(options.session), + { signal: context.mcpReq.signal }, + ); if (!request.ok) return server.delivery.toCallToolResult(request, graphContract); const result = await logToolExecution( diff --git a/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts b/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts index d420c22dad..c97392a406 100644 --- a/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts +++ b/src/server/registerManagedWorkflowTools/verifyManagedNativeBoundaries.ts @@ -31,7 +31,14 @@ export const registerVerifyManagedNativeBoundaries = ( const result = await logToolExecution( options.logger, nativeVerificationContract.name, - () => Promise.resolve(executeManagedNativeBoundaries(input)), + () => + Promise.resolve( + executeManagedNativeBoundaries( + input, + options.session.evidenceForAnalysis.bind(options.session), + { signal: context.mcpReq.signal }, + ), + ), ); if (!result.ok) return server.delivery.toCallToolResult( diff --git a/src/server/registerProcessComparisonTool.ts b/src/server/registerProcessComparisonTool.ts index b711860a9d..37448d70a6 100644 --- a/src/server/registerProcessComparisonTool.ts +++ b/src/server/registerProcessComparisonTool.ts @@ -1,84 +1,18 @@ import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; - import type { BinarySessionPort } from "../application/binary/BinarySessionPort.js"; +import { compareProcessEvidence } from "../application/process/ProcessComparisonService.js"; +import { + recordEvidenceSources, + retainAnalysisResultSources, +} from "../application/investigation/recordEvidenceSources.js"; import { toolContract } from "../contracts/toolContracts.js"; -import { AnalysisInputError } from "../domain/analysisErrorCore.js"; -import type { AnalysisError } from "../domain/analysisErrorBase.js"; -import { describeValidationFailure } from "../domain/validationFailure.js"; -import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; -import { - PROCESS_COMPARISON_DIMENSIONS, - type ProcessCaptureComparison, -} from "../domain/process/processComparison.js"; -import { - compareProcessCaptureEvidence, - type ParsedProcessCaptureEvidence, -} from "../domain/process/processComparisonEvidence.js"; -import { parseProcessCaptureEvidence } from "../application/process/ProcessEvidence.js"; -import type { RecordUnknownInput } from "../domain/residualUnknown.js"; -import { err, type Result } from "../domain/result.js"; import { recordDerivedEvidence } from "./recordDerivedEvidence.js"; -import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js"; import { runDerivedOperation } from "./runDerivedOperation.js"; -import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js"; import { toolRegistrationOptions } from "./toolRegistrationOptions.js"; import { withAdmittedAnalysis } from "./analysisAdmission.js"; import { runAdmittedToolOperation } from "./admittedToolOperation.js"; -/** - * Parse one side and name the failed constraint: tampered Evidence, Evidence - * from another workflow, or an invalid capture result. - */ -const parseCaptureSide = ( - operation: string, - side: "left" | "right", - value: unknown, -): Result => { - const parsed = parseProcessCaptureEvidence(value); - if (parsed.ok) return parsed; - const failure = parsed.error; - switch (failure.kind) { - case "invalid-evidence": - return err( - new EvidenceIntegrityError(`${side} Evidence failed validation`, { - cause: failure.cause, - userMessage: `The ${side} Evidence failed validation (${describeValidationFailure(failure.cause)}). Supply unmodified capture Evidence from capture_process_scenario.`, - }), - ); - case "wrong-workflow": - return err( - new AnalysisInputError(operation, undefined, [ - { - path: [side], - reason: "invalid_value", - message: `Expected process capture Evidence (operation ${failure.expected.operation}, predicate ${failure.expected.predicate}); the ${side} Evidence has operation ${failure.actual.operation} and predicate ${failure.actual.predicate}.`, - }, - ]), - ); - case "unsupported-provider": - return err( - new AnalysisInputError(operation, undefined, [ - { - path: [side, "provider"], - reason: "invalid_value", - message: `The ${side} Evidence provider ${failure.provider.id} v${failure.provider.version} (${failure.provider.name}) is not the current or supported legacy ${PROCESS_PROVIDER.id} v${PROCESS_PROVIDER.version} process capture provider.`, - }, - ]), - ); - case "invalid-capture": - return err( - new EvidenceIntegrityError( - `${side} process capture failed validation`, - { - cause: failure.cause, - userMessage: `The ${side} Evidence has an invalid process capture result (${describeValidationFailure(failure.cause)}). Recreate the capture with capture_process_scenario.`, - }, - ), - ); - } -}; - -/** Register deterministic process-capture comparison and contradiction tracking. */ +/** Adapt the shared process comparison workflow to the scoped MCP session. */ export const registerProcessComparisonTool = ( server: EvidenceMcpServer, session: BinarySessionPort, @@ -96,92 +30,44 @@ export const registerProcessComparisonTool = ( contract.name, context.mcpReq.signal, async () => { - const left = parseCaptureSide(contract.name, "left", input.left); - if (!left.ok) return server.delivery.toCallToolResult(left, contract); - const right = parseCaptureSide(contract.name, "right", input.right); - if (!right.ok) - return server.delivery.toCallToolResult(right, contract); - const { record: leftRecord } = left.value; - const { record: rightRecord } = right.value; const computed = await runDerivedOperation( context, contract.name, - () => - compareProcessCaptureEvidence(left.value, right.value, { - ...(input.max_capture_age_ms === undefined - ? {} - : { maxCaptureAgeMs: input.max_capture_age_ms }), - ...(input.trace_spec === undefined - ? {} - : { traceSpecification: input.trace_spec }), - now, - }), + () => { + const result = compareProcessEvidence( + input, + session.evidenceForAnalysis.bind(session), + { signal: context.mcpReq.signal, now }, + ); + if (!result.ok) throw result.error; + return result.value; + }, + (value) => value, ); - if (!computed.ok) - return server.delivery.toCallToolResult(computed, contract); - const { comparison, evidence } = computed.value; - const recordedSources = recordEvidenceSources( - (evidence) => session.recordEvidence(evidence), - [leftRecord, rightRecord], + const completed = retainAnalysisResultSources( + contract.name, + session.recordEvidence.bind(session), + computed, ); - if (!recordedSources.ok) - return server.delivery.toCallToolResult(recordedSources, contract); + if (!completed.ok) + return server.delivery.toPartialErrorToolResult( + completed.error, + contract.returnsTargetContent, + session.recordEvidence.bind(session), + ); + const { evidence, sources, unknown } = completed.value; + const retained = recordEvidenceSources( + session.recordEvidence.bind(session), + sources, + ); + if (!retained.ok) + return server.delivery.toCallToolResult(retained, contract); return server.delivery.toEvidenceToolResult( evidence, contract, - recordDerivedEvidence( - session, - evidence, - comparisonUnknownInput( - { - left_evidence_id: leftRecord.evidence_id, - right_evidence_id: rightRecord.evidence_id, - comparison_evidence_id: evidence.evidence_id, - }, - comparison, - ), - ), + recordDerivedEvidence(session, evidence, unknown), ); }, ), ); }; - -const comparisonUnknownInput = ( - parsed: { - readonly left_evidence_id: string; - readonly right_evidence_id: string; - readonly comparison_evidence_id: string; - }, - comparison: ProcessCaptureComparison, -): RecordUnknownInput | undefined => { - if (comparison.status === "unchanged") return undefined; - const differingScopes = PROCESS_COMPARISON_DIMENSIONS.filter( - (dimension) => comparison[dimension] !== "unchanged", - ).join(", "); - const contradictory = - comparison.status === "changed" && - parsed.left_evidence_id !== parsed.right_evidence_id; - // Unknown identity uses the question, not its supporting records. Bind the - // question to this comparison so distinct capture pairs and policies coexist. - return { - question: `${contradictory ? "Process captures disagree" : `Process comparison is ${comparison.status}`} across: ${differingScopes} (comparison ${parsed.comparison_evidence_id})`, - severity: "high", - domain: "process-comparison", - supporting_evidence_ids: contradictory - ? [parsed.left_evidence_id] - : [...new Set([parsed.left_evidence_id, parsed.right_evidence_id])], - contradicting_evidence_ids: contradictory ? [parsed.right_evidence_id] : [], - required_authority: "controlled-replay", - required_confidence: "observed", - required_environment: null, - recommended_probes: [ - { - operation: "capture_process_scenario", - rationale: - "Repeat both scenarios under the same controlled environment.", - }, - ], - relationships: [], - }; -}; diff --git a/src/server/registerWasmTools.ts b/src/server/registerWasmTools.ts index b929e759c0..02b6ea34ce 100644 --- a/src/server/registerWasmTools.ts +++ b/src/server/registerWasmTools.ts @@ -1,14 +1,11 @@ import { compareWasmInterfaces } from "../application/wasm/WasmInterfaceComparisonService.js"; import { COMPARE_WASM_INTERFACES_CONTRACT } from "../contracts/wasm/wasmComparisonContracts.js"; -import { resolvePairedEvidenceRequest } from "../application/EvidenceInputResolver.js"; -import { recordEvidenceSources } from "../application/investigation/recordEvidenceSources.js"; +import { retainAnalysisResultSources } from "../application/investigation/recordEvidenceSources.js"; import type { EvidenceMcpServer } from "./EvidenceMcpServer.js"; import type { WasmArtifactService } from "../application/wasm/WasmArtifactService.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { EvidenceLookup } from "../application/EvidenceInputResolver.js"; -import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; -import { err } from "../domain/result.js"; import { wasmArtifactModule } from "../composition/wasm.js"; import type { Logger } from "pino"; import { @@ -34,24 +31,13 @@ export const registerWasmTools = ( const compareHandler = createEvidenceToolHandler( server, async (rawInput, options) => { - const input = comparison.inputSchema.safeParse(rawInput); - if (!input.success) - return err( - analysisInputErrorFromIssues( - comparison.name, - input.error.issues, - rawInput, - ), - ); - const sources = resolvePairedEvidenceRequest(input.data, evidenceById); - if (!sources.ok) return sources; - const result = await compareWasmInterfaces(sources.value, options); - if (!result.ok) return result; - const recorded = recordEvidenceSources(recordEvidence, [ - sources.value.left, - sources.value.right, - ]); - return recorded.ok ? result : recorded; + const result = retainAnalysisResultSources( + comparison.name, + recordEvidence, + await compareWasmInterfaces(rawInput, options, evidenceById), + { sourceOwner: "caller" }, + ); + return result.ok ? { ok: true, value: result.value.evidence } : result; }, logger, recordEvidence, diff --git a/src/server/runDerivedOperation.test.ts b/src/server/runDerivedOperation.test.ts index 8ee6308632..152c70752e 100644 --- a/src/server/runDerivedOperation.test.ts +++ b/src/server/runDerivedOperation.test.ts @@ -5,6 +5,9 @@ import { type DerivedOperationContext, } from "./runDerivedOperation.js"; import { EvidenceIntegrityError } from "../domain/evidenceErrors.js"; +import { FUNCTION_COMPARISON_EXAMPLE } from "../contracts/functionComparisonExample.js"; +import { EvidenceLedger } from "../application/investigation/EvidenceLedger.js"; +import { retainAnalysisResultSources } from "../application/investigation/recordEvidenceSources.js"; const context = ( controller: AbortController, @@ -25,6 +28,44 @@ const context = ( }); describe("derived MCP operation boundary", () => { + it("retains a completed observation and its source after terminal cancellation", async () => { + const controller = new AbortController(); + const reason = { requested: "completed-evidence-stop" }; + const source = FUNCTION_COMPARISON_EXAMPLE.left; + const completed = { + evidence: FUNCTION_COMPARISON_EXAMPLE.right, + sources: [source], + }; + const result = await runDerivedOperation( + context(controller, [], (message) => { + if (message.startsWith("complete:")) controller.abort(reason); + }), + "compare", + () => completed, + (value) => value, + ); + expect(result).toMatchObject({ + ok: false, + error: { + _tag: "AnalysisCancelledError", + cause: reason, + partialObservation: completed.evidence, + }, + sourceEvidence: [source], + }); + const ledger = new EvidenceLedger(); + const retained = retainAnalysisResultSources( + "compare", + ledger.record.bind(ledger), + result, + ); + expect(retained.ok).toBe(false); + expect(ledger.has(source.evidence_id)).toBe(true); + expect(retained.ok ? undefined : retained.error.cause).toBe(reason); + }); +}); + +describe("derived MCP operation progress and cancellation", () => { it("cancels before computation and never evaluates the operation", async () => { const controller = new AbortController(); const reason = Object.freeze({ requested: "pre-compute-stop" }); diff --git a/src/server/runDerivedOperation.ts b/src/server/runDerivedOperation.ts index f984e81ea5..6ede2f48f0 100644 --- a/src/server/runDerivedOperation.ts +++ b/src/server/runDerivedOperation.ts @@ -3,8 +3,13 @@ import { setImmediate } from "node:timers/promises"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; -import { err, ok, type Result } from "../domain/result.js"; +import { err, ok } from "../domain/result.js"; import { mcpProgressReporter, type McpProgressContext } from "./mcpProgress.js"; +import type { EvidenceWorkflowExecution } from "../application/investigation/EvidenceWorkflowExecution.js"; +import { + withEvidenceSources, + type AnalysisResultWithSources, +} from "../application/investigation/recordEvidenceSources.js"; export interface DerivedOperationContext extends McpProgressContext { readonly mcpReq: McpProgressContext["mcpReq"] & @@ -16,7 +21,8 @@ export const runDerivedOperation = async ( context: DerivedOperationContext, operation: string, compute: () => Value, -): Promise> => { + retainCompleted?: (value: Value) => EvidenceWorkflowExecution, +): Promise> => { const progress = mcpProgressReporter(context); await progress.report({ phase: "prepare", @@ -39,6 +45,20 @@ export const runDerivedOperation = async ( if (cause instanceof AnalysisError) return err(cause); throw cause; } + const cancelledCompletion = (): AnalysisResultWithSources => { + const completed = retainCompleted?.(value); + return withEvidenceSources( + err( + new AnalysisCancelledError(operation, { + cause: context.mcpReq.signal.reason, + ...(completed === undefined + ? {} + : { partialObservation: completed.evidence }), + }), + ), + completed?.sources ?? [], + ); + }; await progress.report({ phase: "compute", completed: 1, @@ -46,12 +66,7 @@ export const runDerivedOperation = async ( message: `Computed ${operation}`, }); await setImmediate(); - if (context.mcpReq.signal.aborted) - return err( - new AnalysisCancelledError(operation, { - cause: context.mcpReq.signal.reason, - }), - ); + if (context.mcpReq.signal.aborted) return cancelledCompletion(); await progress.report({ phase: "complete", completed: 2, @@ -59,11 +74,6 @@ export const runDerivedOperation = async ( message: `Completed ${operation}`, terminal: true, }); - if (context.mcpReq.signal.aborted) - return err( - new AnalysisCancelledError(operation, { - cause: context.mcpReq.signal.reason, - }), - ); + if (context.mcpReq.signal.aborted) return cancelledCompletion(); return ok(value); }; diff --git a/src/server/toolRegistrationOptions.ts b/src/server/toolRegistrationOptions.ts index 618d15f335..45723cef17 100644 --- a/src/server/toolRegistrationOptions.ts +++ b/src/server/toolRegistrationOptions.ts @@ -1,5 +1,10 @@ import type { ToolContract } from "../contracts/toolContractTypes.js"; import type { StandardSchemaWithJSON } from "@modelcontextprotocol/server"; +import type { z } from "zod"; +import { + getEvidenceInputContract, + type EvidenceRequestFrameInput, +} from "../domain/evidenceInputContract.js"; import { toolInputSchemaWithMetadata, toolOutputSchemaWithMetadata, @@ -11,16 +16,35 @@ export const toolRegistrationOptions = ( ): { readonly title: Contract["title"]; readonly description: Contract["description"]; - readonly inputSchema: Contract["inputSchema"]; + readonly inputSchema: StandardSchemaWithJSON< + z.input, + EvidenceRequestFrameInput> + >; readonly outputSchema: Contract["outputSchema"]; readonly annotations: Contract["annotations"]; -} => ({ - title: contract.title, - description: contract.description, - inputSchema: toolInputSchemaWithMetadata(contract), - outputSchema: toolOutputSchemaWithMetadata(contract), - annotations: contract.annotations, -}); +} => { + const advertised = toolInputSchemaWithMetadata(contract); + const evidenceInputs = getEvidenceInputContract( + contract.inputSchema, + ); + // The application owns source admission. Preserve the exact advertised + // declaration while the SDK validates only the borrowing request envelope. + const inputSchema = { + "~standard": { + version: advertised["~standard"].version, + vendor: advertised["~standard"].vendor, + jsonSchema: advertised["~standard"].jsonSchema, + validate: evidenceInputs.frameSchema["~standard"].validate, + }, + }; + return { + title: contract.title, + description: contract.description, + inputSchema, + outputSchema: toolOutputSchemaWithMetadata(contract), + annotations: contract.annotations, + }; +}; /** Register a workflow that validates raw input before effects and projects its own errors. */ export const applicationToolRegistrationOptions = < diff --git a/tests/acceptance/retainedComparisonAggregation.test.ts b/tests/acceptance/retainedComparisonAggregation.test.ts index 2ad6fceb77..61a63170cf 100644 --- a/tests/acceptance/retainedComparisonAggregation.test.ts +++ b/tests/acceptance/retainedComparisonAggregation.test.ts @@ -15,17 +15,27 @@ it("recovers a missing retained closure by importing the originating bundle", as const comparisons = [FUNCTION_COMPARISON_EVIDENCE]; const { call } = await connectLocalToolsMcp(); const input = { comparisons }; - for (const request of [ - input, + const retainedRequest = { + comparisons: [ + { + kind: "retained-evidence", + evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id, + }, + ], + }; + const requests = [ { - comparisons: [ - { - kind: "retained-evidence", - evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id, - }, - ], + input, + // The inline comparison is already supplied; its missing dependencies + // still require their originating retained Evidence closure. + missingIds: FUNCTION_COMPARISON_EVIDENCE.evidence_links, }, - ]) { + { + input: retainedRequest, + missingIds: [FUNCTION_COMPARISON_EVIDENCE.evidence_id], + }, + ]; + for (const { input: request, missingIds } of requests) { const missing = parseMcpToolError( await call("find_changed_behavior", request), ).error; @@ -35,10 +45,10 @@ it("recovers a missing retained closure by importing the originating bundle", as details: { reason: "missing", reference_scope: "retained-closure", - evidence_id: FUNCTION_COMPARISON_EVIDENCE.evidence_id, actual: null, }, }); + expect(missingIds).toContain(missing.details?.evidence_id); expect(missing.message).toContain("required retained Evidence closure"); expect(missing.remediation.action).toContain("originating Evidence bundle"); expect(JSON.stringify(missing)).not.toContain("inline Evidence"); @@ -72,6 +82,9 @@ it("recovers a missing retained closure by importing the originating bundle", as FUNCTION_COMPARISON_EVIDENCE.evidence_id, ]), ); + const retained = await call("find_changed_behavior", retainedRequest); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(aggregate); }); it("aggregates selected retained observations within a caller's input budget", async () => { diff --git a/tests/boundary/cli/managedEvidenceReferencesCli.test.ts b/tests/boundary/cli/managedEvidenceReferencesCli.test.ts new file mode 100644 index 0000000000..b33cbed4c8 --- /dev/null +++ b/tests/boundary/cli/managedEvidenceReferencesCli.test.ts @@ -0,0 +1,101 @@ +import { readFile, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { + MANAGED_APPLICATION_GRAPH_EXAMPLE, + MANAGED_NATIVE_VERIFICATION_EXAMPLE, + MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, +} from "../../../src/contracts/managed/managedWorkflowExamples.js"; +import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; +import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +const reference = (evidence: Evidence) => ({ + kind: "retained-evidence" as const, + evidence_id: evidence.evidence_id, +}); + +cliTest( + "uses an explicitly selected bundle for managed Evidence workflows without rewriting it", + async ({ cli }) => { + const directory = await createTestTempDirectory("rea-managed-ref-cli-"); + const bundleName = " selected evidence.json"; + const members = MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE.static_members; + const records = [ + members, + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + ...MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations, + ]; + const bytes = JSON.stringify(createEvidenceBundle(records)); + await writeFile(join(directory, bundleName), bytes); + + const scenarios = [ + { + command: "project-managed-application-graph", + inline: MANAGED_APPLICATION_GRAPH_EXAMPLE, + retained: { managed_members: reference(members) }, + }, + { + command: "import-managed-reconstruction", + inline: MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, + retained: { + ...MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, + static_members: reference(members), + }, + }, + { + command: "verify-managed-native-boundaries", + inline: MANAGED_NATIVE_VERIFICATION_EXAMPLE, + retained: { + managed_boundaries: reference( + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + ), + native_observations: + MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations.map( + reference, + ), + }, + }, + ]; + + for (const scenario of scenarios) { + const inline = await cli.run({ + arguments: [ + scenario.command, + JSON.stringify(scenario.inline), + "--json", + ], + cwd: directory, + }); + expect(inline.exitCode, inline.stderr).toBe(0); + const retained = await cli.run({ + arguments: [ + scenario.command, + JSON.stringify(scenario.retained), + "--evidence-bundle", + bundleName, + "--json", + ], + cwd: directory, + }); + expect(retained.exitCode, retained.stderr).toBe(0); + expect(parseEvidence(retained.json)).toEqual(parseEvidence(inline.json)); + } + + const missingContext = await cli.run({ + arguments: [ + "project-managed-application-graph", + JSON.stringify({ managed_members: reference(members) }), + "--json", + ], + cwd: directory, + }); + expect(missingContext.exitCode, missingContext.stderr).toBe(1); + expect(missingContext.json).toMatchObject({ + details: { evidence_id: members.evidence_id, reason: "missing" }, + }); + expect(await readFile(join(directory, bundleName), "utf8")).toBe(bytes); + }, +); diff --git a/tests/boundary/cli/processComparisonInputOrder.test.ts b/tests/boundary/cli/processComparisonInputOrder.test.ts new file mode 100644 index 0000000000..cd0f0b8139 --- /dev/null +++ b/tests/boundary/cli/processComparisonInputOrder.test.ts @@ -0,0 +1,44 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { PROCESS_CAPTURE_REFERENCE } from "../../../src/contracts/investigationExamples.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { cliTest } from "../../support/cli/cliFixture.js"; + +cliTest.for(["left", "right"] as const)( + "authenticates the %s capture before reading later selected files", + async (side, { cli }) => { + const root = await createTestTempDirectory("rea-process-input-order-"); + await writeFile( + join(root, "valid.json"), + JSON.stringify(PROCESS_CAPTURE_REFERENCE), + ); + await writeFile( + join(root, "invalid.json"), + JSON.stringify({ + ...PROCESS_CAPTURE_REFERENCE, + evidence_id: `ev_${"0".repeat(64)}`, + }), + ); + const result = await cli.run({ + arguments: [ + "compare-process-captures", + side === "left" ? "invalid.json" : "valid.json", + side === "left" ? "missing-right.json" : "invalid.json", + "missing-trace.json", + "--json", + ], + cwd: root, + }); + expect(JSON.parse(result.stdout)).toMatchObject({ + code: "invalid_request", + category: "invalid_input", + details: { + issues: expect.arrayContaining([ + expect.objectContaining({ path: ["evidence_id"] }), + ]), + }, + }); + }, +); diff --git a/tests/boundary/cli/workflowInputIdentities.test.ts b/tests/boundary/cli/workflowInputIdentities.test.ts index ade06a0736..7f45350f60 100644 --- a/tests/boundary/cli/workflowInputIdentities.test.ts +++ b/tests/boundary/cli/workflowInputIdentities.test.ts @@ -30,11 +30,22 @@ const runJson = async ( root: string, command: string, input: unknown, + evidenceBundle?: string, ) => { const path = join(root, "request.json"); await writeFile(path, JSON.stringify(input)); - return cli.run({ - arguments: [command, path, "--json"], + // These JSON commands share no provider state; reuse the fixture-owned CLI + // process so the matrix measures validation rather than repeated startup. + const run = cli.sharedCliRun?.bind(cli) ?? cli.run.bind(cli); + return run({ + arguments: [ + command, + path, + "--json", + ...(evidenceBundle === undefined + ? [] + : ["--evidence-bundle", evidenceBundle]), + ], cwd: root, environment: { REA_LOG_LEVEL: "silent" }, }); @@ -394,6 +405,18 @@ cliTest( }); expect(cliResult.exitCode, cliResult.stdout).toBe(0); expect(compareContract.outputSchema.parse(cliResult.json)).toEqual(result); + const bundlePath = join(root, "retained-workflow-evidence.json"); + const exported = await call("export_evidence_bundle", { path: bundlePath }); + expect(exported.isError).not.toBe(true); + const portable = await runJson( + cli, + root, + "compare-application-versions", + { left: reference, right: otherReference }, + bundlePath, + ); + expect(portable.exitCode, portable.stdout).toBe(0); + expect(compareContract.outputSchema.parse(portable.json)).toEqual(result); expect( ( await call("compare_javascript_export_shapes", { diff --git a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts index db8129fffa..f9295153c6 100644 --- a/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts +++ b/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts @@ -461,7 +461,10 @@ describe("Application graph Evidence envelopes", () => { MANAGED_APPLICATION_GRAPH_EXAMPLE, ); if (!sourceResult.ok) throw sourceResult.error; - const source = sourceResult.value; + const source = + "evidence" in sourceResult.value + ? sourceResult.value.evidence + : sourceResult.value; const result = sourceKind === "runtime reconciliation" ? javascriptRuntimeReconciliationResultSchema.parse( @@ -707,7 +710,7 @@ describe("JavaScript runtime reconciliation local paths", () => { }); if (!reconciled.ok) throw reconciled.error; const result = javascriptRuntimeReconciliationResultSchema.parse( - reconciled.value.normalized_result, + reconciled.value.evidence.normalized_result, ); expect( result.reconciliations.find( @@ -760,14 +763,14 @@ describe("JavaScript runtime reconciliation local paths", () => { expect(relativeResult.ok).toBe(true); expect(absoluteResult.ok).toBe(true); if (!relativeResult.ok || !absoluteResult.ok) return; - expect(relativeResult.value.evidence_id).toBe( - absoluteResult.value.evidence_id, + expect(relativeResult.value.evidence.evidence_id).toBe( + absoluteResult.value.evidence.evidence_id, ); - expect(relativeResult.value.normalized_result).toEqual( - absoluteResult.value.normalized_result, + expect(relativeResult.value.evidence.normalized_result).toEqual( + absoluteResult.value.evidence.normalized_result, ); const result = javascriptRuntimeReconciliationResultSchema.parse( - relativeResult.value.normalized_result, + relativeResult.value.evidence.normalized_result, ); expect(result.static_layers[0]?.runtime_mappings[0]).toMatchObject({ kind: "file-root", @@ -792,7 +795,7 @@ describe("JavaScript runtime reconciliation local paths", () => { if (!reconciled.ok) throw reconciled.error; const result = javascriptRuntimeReconciliationResultSchema.parse( - reconciled.value.normalized_result, + reconciled.value.evidence.normalized_result, ); expect(result.static_layers[0]?.runtime_mappings[0]).toMatchObject({ kind: "file-root", diff --git a/tests/boundary/inspector/javascriptRuntimeObservation.test.ts b/tests/boundary/inspector/javascriptRuntimeObservation.test.ts index 3e83ace512..11ff4aad28 100644 --- a/tests/boundary/inspector/javascriptRuntimeObservation.test.ts +++ b/tests/boundary/inspector/javascriptRuntimeObservation.test.ts @@ -688,7 +688,7 @@ describe("passive V8 Inspector evidence", () => { }); if (!reconciled.ok) throw reconciled.error; const result = javascriptRuntimeReconciliationResultSchema.parse( - reconciled.value.normalized_result, + reconciled.value.evidence.normalized_result, ); expect(result.runtime_captures[0]?.kind).toBe("v8-inspector"); expect(result.summary.runtime_scripts).toBe(1); @@ -749,7 +749,7 @@ describe("passive V8 Inspector evidence", () => { }); if (!reconciled.ok) throw reconciled.error; const result = javascriptRuntimeReconciliationResultSchema.parse( - reconciled.value.normalized_result, + reconciled.value.evidence.normalized_result, ); expect(result.summary.runtime_scripts).toBe(2); expect(result.reconciliations).toContainEqual( @@ -836,7 +836,7 @@ test("keeps Inspector parse failures separate and preserves file URL script iden }); if (!reconciled.ok) throw reconciled.error; const result = javascriptRuntimeReconciliationResultSchema.parse( - reconciled.value.normalized_result, + reconciled.value.evidence.normalized_result, ); const runtimeScripts = result.graph.nodes.filter( (node) => node.kind === "runtime-script-instance", diff --git a/tests/boundary/mcp/evidenceInputContractsMcp.test.ts b/tests/boundary/mcp/evidenceInputContractsMcp.test.ts new file mode 100644 index 0000000000..99f00e6beb --- /dev/null +++ b/tests/boundary/mcp/evidenceInputContractsMcp.test.ts @@ -0,0 +1,96 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { McpServer } from "@modelcontextprotocol/server"; +import { Ajv2020 } from "ajv/dist/2020.js"; +import { describe, expect, expectTypeOf, it } from "vitest"; + +import { COMPACT_INPUT_SCHEMA_BUDGET_BYTES } from "../../../src/config/mcpInputSchemaProfile.js"; +import { presentAdvertisedInputSchema } from "../../../src/contracts/compactInputSchema.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { transformAdvertisedInputJsonSchema } from "../../../src/contracts/toolSchemaMetadata.js"; +import { toolRegistrationOptions } from "../../../src/server/toolRegistrationOptions.js"; +import { advertisedSchemaErrors } from "../../fixtures/mcpAdvertisedSchemaValidity.js"; + +describe("Evidence input SDK boundaries", () => { + it("borrows SDK Evidence inputs while leaving ordinary request constraints active", async () => { + const contract = toolContract("build_call_path"); + const registration = toolRegistrationOptions(contract); + const source = Object.defineProperty({}, "normalized_result", { + get: () => { + throw new Error("SDK must not clone the unadmitted payload"); + }, + }); + const input = { + functions: [source], + start: { address: "0x1000" }, + goal: { address: "0x1001" }, + }; + const validated = + await registration.inputSchema["~standard"].validate(input); + expect(validated.issues).toBeUndefined(); + if (validated.issues !== undefined) throw new Error("valid frame rejected"); + expectTypeOf(validated.value.functions[0]).toBeUnknown(); + expect(validated.value.functions[0]).toBe(source); + const invalid = await registration.inputSchema["~standard"].validate({ + ...input, + start: { address: 1 }, + }); + expect(invalid.issues).toBeDefined(); + }); + + it.each(["full", "compact"] as const)( + "advertises valid reference inputs after actual SDK conversion (%s)", + async (profile) => { + const contract = toolContract("build_call_path"); + const registration = toolRegistrationOptions(contract); + const inputSchema = + profile === "compact" + ? transformAdvertisedInputJsonSchema( + registration.inputSchema, + (schema) => + presentAdvertisedInputSchema(schema, { + budgetBytes: COMPACT_INPUT_SCHEMA_BUDGET_BYTES, + }), + ) + : registration.inputSchema; + const server = new McpServer({ name: "evidence-contract", version: "1" }); + const client = new Client({ name: "evidence-contract", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + server.registerTool(contract.name, { inputSchema }, async () => ({ + content: [], + })); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const { tools } = await client.listTools(); + expect(advertisedSchemaErrors(tools)).toEqual([]); + const tool = tools.find(({ name }) => name === contract.name); + if (tool === undefined) + throw new Error("named contract not advertised"); + expect(tool.inputSchema.description).toContain( + '{"kind":"retained-evidence","evidence_id":"ev_..."}', + ); + expect(tool.inputSchema.description).toContain("parent_evidence_id"); + const validate = new Ajv2020({ + strict: false, + validateFormats: false, + }).compile(tool.inputSchema); + expect( + validate({ + functions: [ + { + kind: "retained-evidence", + evidence_id: `ev_${"a".repeat(64)}`, + }, + ], + start: { address: "0x1000" }, + goal: { address: "0x1001" }, + }), + JSON.stringify(validate.errors), + ).toBe(true); + } finally { + await Promise.allSettled([client.close(), server.close()]); + } + }, + ); +}); diff --git a/tests/boundary/mcp/fixedEvidenceConsumersMcp.test.ts b/tests/boundary/mcp/fixedEvidenceConsumersMcp.test.ts new file mode 100644 index 0000000000..f0b441209d --- /dev/null +++ b/tests/boundary/mcp/fixedEvidenceConsumersMcp.test.ts @@ -0,0 +1,327 @@ +import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; +import { afterAll, afterEach, beforeAll, expect, it, vi } from "vitest"; + +import type { AnalysisOperationPort } from "../../../src/application/AnalysisProvider.js"; +import { + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, +} from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; +import { JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE } from "../../../src/contracts/javascript/javascriptExportShapeComparisonExample.js"; +import { JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE } from "../../../src/contracts/javascript/javascriptRuntimeReconciliationExample.js"; +import { + ANDROID_APPLICATION_GRAPH_EXAMPLE, + APPLE_APPLICATION_GRAPH_EXAMPLE, + HARMONY_APPLICATION_GRAPH_EXAMPLE, +} from "../../../src/contracts/mobileApplicationGraphExamples.js"; +import { toolContract } from "../../../src/contracts/toolContracts.js"; +import { parseEvidence } from "../../../src/domain/evidence.js"; +import { createServer } from "../../../src/server/createServer.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; + +// These authenticated catalog examples are authored fixtures, not real-provider +// verification. Their complete requests fit the default 1,024-element policy; +// the largest is the 750-element export-shape pair. No input allowance is raised. +const application = JAVASCRIPT_FEATURE_TRACE_EXAMPLE.application; +const semanticInput = { + application, + query: { + seed: { kind: "literal", value: true }, + direction: "backward-provenance", + }, +}; +const scenarios = [ + { + name: "trace_application_feature", + input: JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + sourceIds: [application.evidence_id], + facts: { summary: { matched_seeds: 1, traced_nodes: 1 } }, + }, + { + name: "trace_javascript_semantics", + input: semanticInput, + sourceIds: [application.evidence_id], + facts: { source_evidence_id: application.evidence_id }, + }, + { + name: "compare_application_versions", + input: JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE, + sourceIds: [ + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left.evidence_id, + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id, + ], + facts: { + left: { + evidence_id: + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.left.evidence_id, + }, + right: { + evidence_id: + JAVASCRIPT_APPLICATION_VERSION_COMPARISON_EXAMPLE.right.evidence_id, + }, + }, + }, + { + name: "compare_source_to_bundle", + input: SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, + sourceIds: [SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application.evidence_id], + facts: { + application: { + evidence_id: + SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE.application.evidence_id, + }, + }, + }, + { + name: "compare_javascript_export_shapes", + input: JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE, + sourceIds: [ + JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE.left.evidence_id, + JAVASCRIPT_EXPORT_SHAPE_COMPARISON_EXAMPLE.right.evidence_id, + ], + facts: { coverage: { status: "partial" } }, + }, + { + name: "reconcile_javascript_runtime", + input: JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, + sourceIds: [ + ...JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.static_layers.map( + ({ analysis }) => analysis.evidence_id, + ), + ...JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.runtime_observations.map( + ({ evidence_id }) => evidence_id, + ), + ], + facts: { + source_map_authority: { used_for_primary_matching: false }, + }, + }, + ...( + [ + { + name: "project_android_application_graph", + input: ANDROID_APPLICATION_GRAPH_EXAMPLE, + }, + { + name: "project_apple_application_graph", + input: APPLE_APPLICATION_GRAPH_EXAMPLE, + }, + { + name: "project_harmony_application_graph", + input: HARMONY_APPLICATION_GRAPH_EXAMPLE, + }, + ] as const + ).map(({ name, input }) => ({ + name, + input, + sourceIds: input.inventory_evidence.map(({ evidence_id }) => evidence_id), + facts: { + source_evidence_ids: input.inventory_evidence.map( + ({ evidence_id }) => evidence_id, + ), + coverage: { status: "partial", inventory_complete: true }, + }, + })), +] as const; + +const execute = vi.fn(() => { + throw new Error("Fixed Evidence composition must not execute a provider"); +}); +const observe = vi.fn(() => { + throw new Error("Fixed Evidence composition must not observe a live runtime"); +}); +const providerIdentity = () => ({ + id: "fixed-composition-fixture", + name: "Fixed composition fixture", + version: "1", +}); +const server = createServer( + { kind: "fixed", analysis: { execute } }, + { + environment: { REA_MCP_TOOL_FAMILIES: "application,electron" }, + electronObservation: { + identity: providerIdentity, + listTargets: observe, + inspectPage: observe, + }, + javascriptRuntimeObservation: { + identity: providerIdentity, + listTargets: observe, + observe, + }, + }, +); +const client = new Client({ name: "fixed-evidence-fixtures", version: "1" }); + +beforeAll(async () => { + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + await server.connect(serverTransport); + await client.connect(clientTransport); + const names = new Set( + (await client.listTools()).tools.map(({ name }) => name), + ); + for (const scenario of scenarios) expect(names.has(scenario.name)).toBe(true); + expect(names.has("inspect_analysis_view")).toBe(true); + expect(names.has("get_evidence_bundle")).toBe(false); +}); + +afterEach(() => { + expect(execute).not.toHaveBeenCalled(); + expect(observe).not.toHaveBeenCalled(); +}); + +afterAll(async () => { + const closed = await Promise.allSettled([client.close(), server.close()]); + const failures = closed.flatMap((result) => + result.status === "rejected" ? [result.reason] : [], + ); + if (failures.length > 0) + throw new AggregateError(failures, "Fixed fixture MCP cleanup failed"); +}); + +it.each(scenarios)( + "composes complete caller-owned $name sources without a recorder", + async ({ name, input, sourceIds, facts }) => { + const reply = await client.callTool({ name, arguments: input }); + expect(reply.isError, JSON.stringify(reply.structuredContent)).not.toBe( + true, + ); + const evidence = parseEvidence(reply.structuredContent); + expect(toolContract(name).outputSchema.safeParse(evidence).success).toBe( + true, + ); + expect(evidence.operation).toBe(name); + expect(evidence.evidence_links.toSorted()).toEqual( + [...new Set(sourceIds)].sort(), + ); + expect(evidence.normalized_result).toMatchObject(facts); + }, +); + +it("projects a caller-owned inline source directly through inspect_analysis_view", async () => { + const reply = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { kind: "inline", evidence: application }, + view: { kind: "summary" }, + }, + }); + expect(reply.isError, JSON.stringify(reply.structuredContent)).not.toBe(true); + const evidence = parseEvidence(reply.structuredContent); + expect( + toolContract("inspect_analysis_view").outputSchema.safeParse(evidence) + .success, + ).toBe(true); + expect(evidence.evidence_links).toEqual([application.evidence_id]); + expect(evidence.normalized_result).toMatchObject({ + kind: "summary", + parent_evidence_id: application.evidence_id, + parent_operation: application.operation, + summary: { format: "directory" }, + }); + expect(evidence.normalized_result).not.toHaveProperty("summary.graph"); +}); + +it("keeps a direct inspect_analysis_view retained source missing on the fixed server", async () => { + const reply = await client.callTool({ + name: "inspect_analysis_view", + arguments: { + source: { + kind: "retained-evidence", + evidence_id: application.evidence_id, + }, + view: { kind: "summary" }, + }, + }); + expect(parseMcpToolError(reply)).toMatchObject({ + error: { + details: { evidence_id: application.evidence_id, reason: "missing" }, + }, + }); +}); + +it("does not turn caller-owned inline sources into a hidden fixed-server ledger", async () => { + const reply = await client.callTool({ + name: "trace_application_feature", + arguments: { + ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + application: { + kind: "retained-evidence", + evidence_id: application.evidence_id, + }, + }, + }); + expect(parseMcpToolError(reply)).toMatchObject({ + error: { + details: { evidence_id: application.evidence_id, reason: "missing" }, + }, + }); +}); + +it("requires an explicit retaining session for a source-to-bundle summary view", async () => { + const reply = await client.callTool({ + name: "compare_source_to_bundle", + arguments: { + ...SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE, + view: { kind: "summary" }, + }, + }); + expect(parseMcpToolError(reply)).toMatchObject({ + error: { + code: "capability_unavailable", + message: expect.stringContaining("does not retain Evidence"), + }, + }); +}); + +it.each([ + { + name: "trace_application_feature", + input: { + ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + application: ANDROID_APPLICATION_GRAPH_EXAMPLE.inventory_evidence[0], + }, + }, + { + name: "project_android_application_graph", + input: { inventory_evidence: [application] }, + }, + { + name: "reconcile_javascript_runtime", + input: { + ...JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE, + static_layers: [ + { + analysis: ANDROID_APPLICATION_GRAPH_EXAMPLE.inventory_evidence[0], + }, + ], + }, + }, +])( + "rejects an authenticated source with the wrong role in $name", + async ({ name, input }) => { + const reply = await client.callTool({ name, arguments: input }); + expect(parseMcpToolError(reply)).toMatchObject({ + error: { code: "invalid_request" }, + }); + await client.ping(); + }, +); + +it("keeps the default input admission before fixed-server source decoding", async () => { + const reply = await client.callTool({ + name: "trace_application_feature", + arguments: { + ...JAVASCRIPT_FEATURE_TRACE_EXAMPLE, + native_observations: Array.from({ length: 1_025 }, () => ({})), + }, + }); + expect(parseMcpToolError(reply)).toMatchObject({ + error: { + code: "resource_constraint", + details: { reported_limits: { max_input_elements: 1_024 } }, + }, + }); + await client.ping(); +}); diff --git a/tests/boundary/mcp/functionCallDirectionsMcp.test.ts b/tests/boundary/mcp/functionCallDirectionsMcp.test.ts index 5d1bfa5159..1e180e92b5 100644 --- a/tests/boundary/mcp/functionCallDirectionsMcp.test.ts +++ b/tests/boundary/mcp/functionCallDirectionsMcp.test.ts @@ -121,8 +121,14 @@ it.each(profiles)("preserves directional gaps (%s)", async (profile) => { name: "build_call_path", arguments: malformed, }); + // Source admission belongs to the Evidence owner in both profiles; + // malformed supplied records fail integrity instead of SDK validation. expect(parseMcpToolError(rejected)).toMatchObject({ - error: { code: "invalid_request", category: "invalid_input" }, + error: { + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + message: expect.stringContaining("relationship_coverage"), + }, }); } const result = await client.callTool({ diff --git a/tests/boundary/mcp/investigationEvidenceReferencesMcp.test.ts b/tests/boundary/mcp/investigationEvidenceReferencesMcp.test.ts new file mode 100644 index 0000000000..0e95800cf4 --- /dev/null +++ b/tests/boundary/mcp/investigationEvidenceReferencesMcp.test.ts @@ -0,0 +1,193 @@ +import { expect, it } from "vitest"; + +import { FUNCTION_COMPARISON_EXAMPLE } from "../../../src/contracts/functionComparisonExample.js"; +import { + FUNCTION_COMPARISON_EVIDENCE, + INVESTIGATION_EXAMPLES, + PROCESS_CAPTURE_REFERENCE, + PROCESS_CAPTURE_RECONSTRUCTION, + PROCESS_COMPARISON_EVIDENCE, +} from "../../../src/contracts/investigationExamples.js"; +import { + createEvidence, + parseEvidence, + type Evidence, +} from "../../../src/domain/evidence.js"; +import { functionDossierSchema } from "../../../src/domain/native/functionDossier.js"; +import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; + +const reference = (source: Evidence) => ({ + kind: "retained-evidence" as const, + evidence_id: source.evidence_id, +}); + +const harness = (inputElements?: string) => + createApplicationMcpHarness({ + session: createTestBinarySession(() => { + throw new Error("Composing completed Evidence must not open a provider"); + }), + ...(inputElements === undefined + ? {} + : { + serverOptions: { + environment: { REA_MCP_MAX_INPUT_ELEMENTS: inputElements }, + }, + }), + }); + +it("admits an independent inline dossier and composes its summary parent without producer work", async () => { + const { session, client } = await harness(); + const source = FUNCTION_COMPARISON_EXAMPLE.left; + const selection = { + start: { address: "0x1000" }, + goal: { address: "0x1000" }, + }; + expect(session.hasEvidence(source.evidence_id)).toBe(false); + const inline = await client.callTool({ + name: "build_call_path", + arguments: { ...selection, functions: [source] }, + }); + expect(inline.isError, JSON.stringify(inline)).not.toBe(true); + const expected = parseEvidence(inline.structuredContent); + expect(expected.normalized_result).toMatchObject({ + status: "found", + shortest_hops: 0, + }); + expect(session.hasEvidence(source.evidence_id)).toBe(true); + + const summary = await client.callTool({ + name: "inspect_analysis_view", + arguments: { source: reference(source), view: { kind: "summary" } }, + }); + expect(summary.isError, JSON.stringify(summary)).not.toBe(true); + const receipt = parseEvidence(summary.structuredContent); + expect(receipt.normalized_result).toMatchObject({ + parent_evidence_id: source.evidence_id, + }); + const retained = await client.callTool({ + name: "build_call_path", + arguments: { ...selection, functions: [reference(source)] }, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(expected); +}); + +it("keeps input capacity limits while composing dossiers that cannot be resent inline", async () => { + const { session, client } = await harness("1024"); + const base = FUNCTION_COMPARISON_EXAMPLE.left; + if (base.subject === null) throw new Error("Missing fixture subject"); + const dossier = functionDossierSchema.parse(base.normalized_result); + const source = createEvidence( + { + path: base.subject.local_path, + sha256: base.subject.digest.sha256, + format: base.subject.format, + }, + base.provider, + { + operation: "analyze_function", + parameters: base.parameters, + result: { + ...dossier, + assembly: Array.from({ length: 1200 }, () => "nop"), + }, + confidence: base.confidence, + authority: base.authority, + relationshipCoverage: { status: "complete" }, + }, + ); + expect(session.recordEvidence(source).ok).toBe(true); + const selection = { + start: { address: "0x1000" }, + goal: { address: "0x1000" }, + }; + const inline = await client.callTool({ + name: "build_call_path", + arguments: { ...selection, functions: [source] }, + }); + expect(parseMcpToolError(inline)).toMatchObject({ + error: { code: "resource_constraint" }, + }); + const retained = await client.callTool({ + name: "build_call_path", + arguments: { ...selection, functions: [reference(source)] }, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect( + parseEvidence(retained.structuredContent).normalized_result, + ).toMatchObject({ status: "found" }); +}); + +it("resolves comparison arrays and process pairs with complete source lineage", async () => { + const { session, client } = await harness(); + for (const source of [ + FUNCTION_COMPARISON_EXAMPLE.left, + FUNCTION_COMPARISON_EXAMPLE.right, + PROCESS_CAPTURE_REFERENCE, + PROCESS_CAPTURE_RECONSTRUCTION, + ]) + expect(session.recordEvidence(source).ok).toBe(true); + const pair = await client.callTool({ + name: "compare_process_captures", + arguments: { + left: reference(PROCESS_CAPTURE_REFERENCE), + right: reference(PROCESS_CAPTURE_RECONSTRUCTION), + }, + }); + expect(pair.isError, JSON.stringify(pair)).not.toBe(true); + expect(parseEvidence(pair.structuredContent)).toEqual( + PROCESS_COMPARISON_EVIDENCE, + ); + // Neither selected comparison has to be pre-inserted: inline admission owns it. + const inline = await client.callTool({ + name: "correlate_static_and_runtime", + arguments: INVESTIGATION_EXAMPLES.correlate_static_and_runtime, + }); + expect(inline.isError, JSON.stringify(inline)).not.toBe(true); + const expected = parseEvidence(inline.structuredContent); + const retained = await client.callTool({ + name: "correlate_static_and_runtime", + arguments: { + ...INVESTIGATION_EXAMPLES.correlate_static_and_runtime, + static_comparisons: [reference(FUNCTION_COMPARISON_EVIDENCE)], + runtime_comparisons: [reference(PROCESS_COMPARISON_EVIDENCE)], + }, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(expected); + for (const id of expected.evidence_links) + expect(session.hasEvidence(id), id).toBe(true); +}); + +it("reports missing or wrong-kind records without recording a successful workflow", async () => { + const { session, client } = await harness(); + const selection = { + start: { address: "0x1000" }, + goal: { address: "0x1000" }, + }; + const missing = await client.callTool({ + name: "build_call_path", + arguments: { + ...selection, + functions: [ + { kind: "retained-evidence", evidence_id: `ev_${"f".repeat(64)}` }, + ], + }, + }); + expect(parseMcpToolError(missing)).toMatchObject({ + error: { details: { reason: "missing" } }, + }); + expect(session.exportEvidenceBundle().records).toHaveLength(0); + expect(session.recordEvidence(PROCESS_CAPTURE_REFERENCE).ok).toBe(true); + const wrong = await client.callTool({ + name: "build_call_path", + arguments: { + ...selection, + functions: [reference(PROCESS_CAPTURE_REFERENCE)], + }, + }); + expect(wrong.isError).toBe(true); + expect(session.exportEvidenceBundle().records).toHaveLength(1); +}); diff --git a/tests/boundary/mcp/managedEvidenceReferencesMcp.test.ts b/tests/boundary/mcp/managedEvidenceReferencesMcp.test.ts new file mode 100644 index 0000000000..8918cba08a --- /dev/null +++ b/tests/boundary/mcp/managedEvidenceReferencesMcp.test.ts @@ -0,0 +1,115 @@ +import { expect, it } from "vitest"; + +import { + MANAGED_APPLICATION_GRAPH_EXAMPLE, + MANAGED_MEMBER_COMPARISON_EXAMPLE, + MANAGED_NATIVE_VERIFICATION_EXAMPLE, + MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, +} from "../../../src/contracts/managed/managedWorkflowExamples.js"; +import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; +import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; +import { createTestBinarySession } from "../../fixtures/binarySession.js"; +import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; + +const reference = (evidence: Evidence) => ({ + kind: "retained-evidence" as const, + evidence_id: evidence.evidence_id, +}); + +const cases = [ + { + name: "compare_managed_members", + inline: MANAGED_MEMBER_COMPARISON_EXAMPLE, + retained: { + left: reference(MANAGED_MEMBER_COMPARISON_EXAMPLE.left), + right: reference(MANAGED_MEMBER_COMPARISON_EXAMPLE.right), + }, + }, + { + name: "verify_managed_native_boundaries", + inline: MANAGED_NATIVE_VERIFICATION_EXAMPLE, + retained: { + managed_boundaries: reference( + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + ), + native_observations: + MANAGED_NATIVE_VERIFICATION_EXAMPLE.native_observations.map(reference), + }, + }, + { + name: "import_managed_reconstruction", + inline: MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, + retained: { + ...MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE, + static_members: reference( + MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE.static_members, + ), + }, + }, + { + name: "project_managed_application_graph", + inline: MANAGED_APPLICATION_GRAPH_EXAMPLE, + retained: { + managed_members: reference( + MANAGED_APPLICATION_GRAPH_EXAMPLE.managed_members, + ), + }, + }, +] as const; + +it("composes managed workflows from retained sources without opening an engine", async () => { + const { client, session } = await createApplicationMcpHarness({ + session: createTestBinarySession(() => { + throw new Error("Evidence composition must not open a provider"); + }), + }); + + for (const scenario of cases) { + const inline = await client.callTool({ + name: scenario.name, + arguments: scenario.inline, + }); + expect(inline.isError, JSON.stringify(inline)).not.toBe(true); + const original = parseEvidence(inline.structuredContent); + + const retained = await client.callTool({ + name: scenario.name, + arguments: scenario.retained, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(original); + for (const sourceId of original.evidence_links) + expect(session.hasEvidence(sourceId), sourceId).toBe(true); + } + + const before = session.exportEvidenceBundle(); + const missingId = `ev_${"f".repeat(64)}`; + const missing = await client.callTool({ + name: "project_managed_application_graph", + arguments: { + managed_members: { kind: "retained-evidence", evidence_id: missingId }, + }, + }); + expect(parseMcpToolError(missing)).toMatchObject({ + error: { details: { evidence_id: missingId, reason: "missing" } }, + }); + expect(session.exportEvidenceBundle()).toEqual(before); + + const wrongRole = await client.callTool({ + name: "project_managed_application_graph", + arguments: { + managed_members: reference( + MANAGED_NATIVE_VERIFICATION_EXAMPLE.managed_boundaries, + ), + }, + }); + expect(parseMcpToolError(wrongRole)).toMatchObject({ + error: { + code: "invalid_request", + details: { + issues: [expect.objectContaining({ path: ["managed_members"] })], + }, + }, + }); + expect(session.exportEvidenceBundle()).toEqual(before); +}); diff --git a/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts index b9bd3d2292..2ed4fbc581 100644 --- a/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts +++ b/tests/boundary/mcp/managedNativeVerificationAdapters.test.ts @@ -11,7 +11,7 @@ import { MANAGED_NATIVE_VERIFICATION_EXAMPLE, } from "../../../src/contracts/managed/managedWorkflowExamples.js"; import { managedMemberInspectionSchema } from "../../../src/domain/managed/managedArtifact.js"; -import { managedApplicationGraphReferenceInputSchema } from "../../../src/contracts/managed/managedWorkflowToolContracts.js"; +import { managedApplicationGraphReferenceInputSchema } from "../../../src/domain/managed/managedApplicationGraph.js"; import { managedNativeVerificationResultSchema } from "../../../src/domain/managed/managedNativeVerificationSchemas.js"; import { buildManagedPeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts index 8691ba8cce..48bfa8452a 100644 --- a/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts +++ b/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts @@ -105,6 +105,21 @@ it("advertises and executes Android and Apple inventory projections", async () = provider: { id: "rea-android-application" }, }); const androidEvidence = parseEvidence(android.structuredContent); + const retainedAndroid = await client.callTool({ + name: "project_android_application_graph", + arguments: { + inventory_evidence: apkSources.map(({ evidence_id }) => ({ + kind: "retained-evidence", + evidence_id, + })), + }, + }); + expect(retainedAndroid.isError, JSON.stringify(retainedAndroid)).not.toBe( + true, + ); + expect(parseEvidence(retainedAndroid.structuredContent)).toEqual( + androidEvidence, + ); const androidBundle = await client.callTool({ name: "get_evidence_bundle", arguments: {}, @@ -155,6 +170,18 @@ it("advertises and executes Android and Apple inventory projections", async () = operation: "project_apple_application_graph", provider: { id: "rea-apple-application" }, }); + const retainedApple = await client.callTool({ + name: "project_apple_application_graph", + arguments: { + inventory_evidence: [ + { kind: "retained-evidence", evidence_id: ipaInventory.evidence_id }, + ], + }, + }); + expect(retainedApple.isError, JSON.stringify(retainedApple)).not.toBe(true); + expect(parseEvidence(retainedApple.structuredContent)).toEqual( + parseEvidence(apple.structuredContent), + ); }); it("projects a single inventory page as partial and links only that source", async () => { @@ -219,6 +246,16 @@ it("projects a single inventory page as partial and links only that source", asy expect(unresolved).toContain(dex.artifact_format); expect(unresolved).toContain("unknown"); expect(projection.evidence_links).toEqual([page.evidence_id]); + const retained = await client.callTool({ + name: "project_android_application_graph", + arguments: { + inventory_evidence: [ + { kind: "retained-evidence", evidence_id: page.evidence_id }, + ], + }, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(projection); const bundle = await client.callTool({ name: "get_evidence_bundle", arguments: {}, @@ -379,6 +416,16 @@ it.each([ ], coverage: { status: "partial", inventory_complete: true }, }); + const retained = await client.callTool({ + name: operation, + arguments: { + inventory_evidence: [ + { kind: "retained-evidence", evidence_id: inventory.evidence_id }, + ], + }, + }); + expect(retained.isError, JSON.stringify(retained)).not.toBe(true); + expect(parseEvidence(retained.structuredContent)).toEqual(evidence); const bundle = await client.callTool({ name: "get_evidence_bundle", arguments: {}, diff --git a/tests/boundary/mcp/sessionMcp.test.ts b/tests/boundary/mcp/sessionMcp.test.ts index 74fab27725..5c40a8f7cb 100644 --- a/tests/boundary/mcp/sessionMcp.test.ts +++ b/tests/boundary/mcp/sessionMcp.test.ts @@ -713,11 +713,14 @@ describe("session filesystem path boundaries over MCP", () => { }); describe("json depth bounds over MCP", () => { - it("classifies deeply nested evidence parameters as an input validation error", async () => { + it("preserves source integrity and nesting-limit diagnostics for borrowed parameters", async () => { directory = await createTestTempDirectory("rea-mcp-depth-bound-"); + const createClient = vi.fn(() => { + throw new Error("Offline comparison must not open a provider"); + }); const { mcp } = await createSessionMcpHarness( directory, - provider, + (closed) => ({ ...provider(closed), createClient }), resources, 4_096, ); @@ -743,23 +746,23 @@ describe("json depth bounds over MCP", () => { expect(result.isError, JSON.stringify(result.content)).toBe(true); expect(parseMcpToolError(result)).toMatchObject({ error: { - code: "invalid_request", - recovery: { - kind: "inspect_input_contract", - operation: "compare_process_captures", - }, - details: { - operation: "compare_process_captures", - issues: [ - { - path: ["left", "parameters"], - reason: "invalid_value", - message: expect.stringContaining(String(MAX_JSON_DEPTH)), - }, - ], - }, + // The Evidence owner authenticates borrowed source records after + // envelope admission and preserves the JSON bound before cloning. + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + message: expect.stringContaining( + `The left Evidence failed validation (at parameters: JSON value exceeds maximum nesting depth of ${MAX_JSON_DEPTH}. This release has no runtime override.)`, + ), + details: { reason: "left Evidence failed validation" }, }, }); + const recovered = await mcp.callTool({ + name: "compare_process_captures", + arguments: input, + }); + expect(recovered.isError, JSON.stringify(recovered.content)).not.toBe(true); + expect(recovered.structuredContent).toEqual(baseline.structuredContent); + expect(createClient).not.toHaveBeenCalled(); }, 10_000); }); diff --git a/tests/boundary/mcp/toolInputCapacityHttp.test.ts b/tests/boundary/mcp/toolInputCapacityHttp.test.ts index eb605eb8ba..9ac16a46e7 100644 --- a/tests/boundary/mcp/toolInputCapacityHttp.test.ts +++ b/tests/boundary/mcp/toolInputCapacityHttp.test.ts @@ -126,16 +126,22 @@ processTest( const error = parseMcpToolError(result).error; if (count === 1_019) { // Three root members and two address members fill the remaining five. - // Keep ordinary canonical validation observable at the admitted edge. - expect(error.code).toBe("invalid_request"); + // Admitted source carriers reach the Evidence owner, which rejects + // the first invalid record without expanding every missing field. + expect(error).toMatchObject({ + code: "evidence_integrity_mismatch", + category: "integrity_mismatch", + message: expect.stringContaining("at evidence_id"), + details: { diagnostics: { input_path: ["functions", 0] } }, + }); } else { expect(error.code).toBe("resource_constraint"); expect(error.details).toMatchObject({ resource: "transport", reported_limits: { max_input_elements: 1_024 }, }); - expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(4096); } + expect(Buffer.byteLength(JSON.stringify(result))).toBeLessThan(4096); observations.push({ count, code: error.code, diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index f77eeff4da..0af626a51a 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -8,7 +8,6 @@ import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js import { readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js"; -import { toolContract } from "../../../src/contracts/toolContracts.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; import { createHash } from "node:crypto"; @@ -805,6 +804,92 @@ it("rejects malformed paths, refuses WASM summary delivery before provider work } }); +it("compares caller-owned complete inline WASM sources on a fixed server without a recorder", async () => { + const leftReport = wasmArtifactFixture("/artifacts/left.wasm"); + leftReport.interface = { state: "complete", issues: [], entries: [] }; + const rightReport = wasmArtifactFixture("/artifacts/right.wasm"); + rightReport.interface = { + state: "complete", + issues: [], + entries: [ + { + direction: "export", + module: null, + name: "new_export", + kind: "func", + index: 0, + signature: { kind: "func", params: [], results: [] }, + wat_form: '(export "new_export" (func 0))', + issue: null, + }, + ], + }; + const source = (report: ReturnType) => + createEvidence( + { + path: report.artifact.path, + sha256: report.artifact.sha256, + format: "file", + }, + { id: "fixture-wabt", name: "Source-owned WABT seam", version: "1" }, + { + operation: "inspect_wasm_artifact", + parameters: { path: report.artifact.path }, + result: report, + confidence: "observed", + }, + ); + const left = source(leftReport); + const right = source(rightReport); + const inspect = vi.fn(() => { + throw new Error( + "Comparison must reuse caller Evidence without provider work", + ); + }); + const service = new WasmArtifactService({ + close: () => Promise.resolve(), + inspect, + }); + const session = createTestBinarySession(() => { + throw new Error("Comparison must not start a deep provider"); + }); + const server = createServer( + { kind: "fixed", analysis: session }, + { wasmArtifact: service }, + ); + const client = new Client({ name: "wasm-fixed-comparison", version: "1" }); + const [clientTransport, serverTransport] = + InMemoryTransport.createLinkedPair(); + try { + await server.connect(serverTransport); + await client.connect(clientTransport); + const reply = await client.callTool({ + name: "compare_wasm_interfaces", + arguments: { left, right, scope: "both" }, + }); + expect(reply.isError, JSON.stringify(reply.structuredContent)).not.toBe( + true, + ); + const comparison = parseEvidence(reply.structuredContent); + expect(comparison.operation).toBe("compare_wasm_interfaces"); + expect(comparison.evidence_links).toEqual([ + left.evidence_id, + right.evidence_id, + ]); + expect(comparison.normalized_result).toMatchObject({ + coverage: "complete-within-inputs", + summary: { added: 1, changed: 0, removed: 0, unchanged: 0, unknown: 0 }, + changes: [{ name: "new_export", state: "added" }], + }); + expect(inspect).not.toHaveBeenCalled(); + expect(session.exportEvidenceBundle().records).toEqual([]); + } finally { + await client.close(); + await server.close(); + await session.close(); + } +}); + it("retries cleanup of the injected WASM module after server shutdown fails", async () => { const close = vi .fn() diff --git a/tests/boundary/wasm/interfaceComparison.test.ts b/tests/boundary/wasm/interfaceComparison.test.ts index 9905fa9de7..b692d9b291 100644 --- a/tests/boundary/wasm/interfaceComparison.test.ts +++ b/tests/boundary/wasm/interfaceComparison.test.ts @@ -34,7 +34,7 @@ it("does not call colliding rendered debug identifiers unchanged after a second }); if (!result.ok) throw result.error; const report = wasmInterfaceComparisonSchema.parse( - result.value.normalized_result, + result.value.evidence.normalized_result, ); expect(report.changes.map(({ state }) => state)).toEqual([ "unknown", diff --git a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts index e24b488566..5749168fea 100644 --- a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -1,13 +1,15 @@ import { describe, expect, it } from "vitest"; import { projectManagedApplicationGraphEvidence } from "../../../src/application/managed/ManagedApplicationGraphService.js"; -import { managedApplicationGraphReferenceInputSchema } from "../../../src/contracts/managed/managedWorkflowToolContracts.js"; +import { + managedApplicationGraphReferenceInputSchema, + managedApplicationGraphResultSchema, +} from "../../../src/domain/managed/managedApplicationGraph.js"; import { MANAGED_APPLICATION_GRAPH_EXAMPLE } from "../../../src/contracts/managed/managedWorkflowExamples.js"; import { traceApplicationFeatureEvidence } from "../../../src/application/javascript/JavaScriptApplicationWorkflowService.js"; import { MANAGED_STATIC_PROVIDER } from "../../../src/domain/investigationProviders.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { parseJavaScriptApplicationGraph } from "../../../src/domain/javascript/javascriptApplicationGraph.js"; -import { managedApplicationGraphResultSchema } from "../../../src/domain/managed/managedApplicationGraph.js"; import { managedMemberInspectionSchema, managedNativeBoundaryInspectionSchema, diff --git a/tests/process-global/sourceToBundleReferenceGuards.test.ts b/tests/process-global/sourceToBundleReferenceGuards.test.ts index 533ae14bb0..ca7a45ad5a 100644 --- a/tests/process-global/sourceToBundleReferenceGuards.test.ts +++ b/tests/process-global/sourceToBundleReferenceGuards.test.ts @@ -122,15 +122,15 @@ it.each(["validation", "comparison"] as const)( const actual = await vi.importActual( "../../src/application/javascript/JavaScriptApplicationWorkflowService.js", ); - vi.mocked(workflow.compareSourceToBundleEvidence).mockImplementationOnce( - (input) => { - const compared = actual.compareSourceToBundleEvidence(input); - setImmediate(() => - controller.abort(new Error("cancel during graph comparison")), - ); - return compared; - }, - ); + vi.mocked( + workflow.executeSourceToBundleComparison, + ).mockImplementationOnce(async (...args) => { + const compared = await actual.executeSourceToBundleComparison(...args); + setImmediate(() => + controller.abort(new Error("cancel during graph comparison")), + ); + return compared; + }); } let settle: (() => void) | undefined; const settled = new Promise((resolve) => { diff --git a/tests/support/analysis-efficiency/agent-trial.py b/tests/support/analysis-efficiency/agent-trial.py index 953f04c0e5..2d7d97be73 100644 --- a/tests/support/analysis-efficiency/agent-trial.py +++ b/tests/support/analysis-efficiency/agent-trial.py @@ -75,8 +75,9 @@ def main(): driver_repository = Path(__file__).resolve().parents[3] experiment = Path(__file__).resolve().parent node = shutil.which('node') - if subprocess.check_output([node, '--version'], text=True).strip() != 'v24.18.0': - raise SystemExit('Use pinned Node 24.18.0 on PATH') + expected_node = (repository / '.nvmrc').read_text().strip().removeprefix('v') + if subprocess.check_output([node, '--version'], text=True).strip() != 'v' + expected_node: + raise SystemExit('Use runtime-pinned Node ' + expected_node + ' on PATH') provider_env = {'REA_LOG_LEVEL': 'silent', 'NODE_OPTIONS': '--max-old-space-size=512'} if args.providers: declared = json.loads(args.providers.read_text()) @@ -186,6 +187,14 @@ def main(): 'decode_signal_recording', 'inspect_signal_decoders', 'inspect_managed_members', 'inspect_managed_native_boundaries', 'inspect_artifact', 'inspect_wasm_artifact', + 'compare_functions', 'build_call_path', 'compare_artifacts', + 'compare_process_captures', 'find_changed_behavior', + 'compare_managed_members', 'project_managed_application_graph', + 'compare_wasm_interfaces', 'project_apple_application_graph', + 'project_android_application_graph', 'project_harmony_application_graph', + 'trace_application_feature', 'trace_javascript_semantics', + 'compare_application_versions', 'compare_javascript_export_shapes', + 'reconcile_javascript_runtime', 'inspect_evm_interface', 'get_evidence_bundle', 'export_evidence_bundle', 'import_evidence_bundle', 'list_unknowns', 'verify_unknown_resolution', 'capture_browser_scenario', diff --git a/tests/support/analysis-efficiency/test_codex_context.py b/tests/support/analysis-efficiency/test_codex_context.py index 1b828aaf63..5db8af2075 100644 --- a/tests/support/analysis-efficiency/test_codex_context.py +++ b/tests/support/analysis-efficiency/test_codex_context.py @@ -651,6 +651,7 @@ class ContextCaptureTests(unittest.TestCase): (runtime / 'scripts').mkdir(parents=True) (runtime / 'scripts' / 'rea.mjs').write_text('') (runtime / 'package-lock.json').write_text('{}') + (runtime / '.nvmrc').write_text('24.22.1\n') auth_home = root / 'source-auth' auth_home.mkdir() (auth_home / 'auth.json').write_text('synthetic-credential') @@ -664,7 +665,7 @@ class ContextCaptureTests(unittest.TestCase): with patch.dict(os.environ, {'CODEX_HOME': str(auth_home)}, clear=True), \ patch.object(sys, 'argv', arguments), \ patch.object(agent.shutil, 'which', return_value='/fixture/node'), \ - patch.object(agent.subprocess, 'check_output', return_value='v24.18.0\n'), \ + patch.object(agent.subprocess, 'check_output', return_value='v24.22.1\n'), \ patch.object(agent.subprocess, 'Popen', return_value=child), \ patch.object(agent, 'capture_tool_context', side_effect=OSError('fixture capture failure')), \ patch.object(agent.Path, 'home', side_effect=AssertionError('Explicit CODEX_HOME must not resolve the default home')), \