diff --git a/docs/evm-bytecode.md b/docs/evm-bytecode.md index 67c543257b..0b4d97bd65 100644 --- a/docs/evm-bytecode.md +++ b/docs/evm-bytecode.md @@ -80,8 +80,10 @@ without bytecode text or duplicated raw stdout: } ``` -The result is distinct derived Evidence linked to its complete parent. Its -`normalized_result.summary` preserves every producer-returned selector, decoded +The result is distinct view Evidence linked to its complete parent. Like the +parent, it has `confidence: inferred`; projection never strengthens inferred +candidates. Its `normalized_result.summary` preserves every producer-returned +selector, decoded byte offset, dispatch, argument/mutability string including null, carrier/decoded identities, original limitations and explicit unknowns. Payload `evidence_kind` remains `inferred`. It preserves stderr/truncation and records stdout text length. diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 4b5dd9e09b..81f3106648 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -482,6 +482,9 @@ session retention refuses summary detail with `capability_unavailable`; For `inspect_artifact`, omission also keeps the original request parameters and complete output shape. Delivery selection never reaches the inventory engine; the complete inspection and its atomic inventory Evidence remain retained. +A view is never more certain than its parent: a summary or other view of +`inferred` Evidence is `inferred`. Other views are `derived`, unless the +view kind itself infers, as described below. `inspect_analysis_view` also accepts `api-surface` for retained HAR capture Evidence, grouping request lines by method, origin and format-rule path template. Templated diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 5ceb730528..87a725ef9c 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -10,7 +10,9 @@ import { analysisViewJavaScriptAnalysisWithSource, analysisViewJavaScriptEvidence, analysisViewLayoutEvidence, + analysisViewLayoutFixture, } from "../../../tests/fixtures/analysisView.js"; +import { BINARY_LAYOUT_TEST_PROVIDER } from "../../../tests/fixtures/binaryDiagnostics/layout.js"; import { describe, expect, it, vi } from "vitest"; import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js"; @@ -450,6 +452,54 @@ it("projects inline layout Evidence and retains a derived view record", async () }); }); +it("never seals a view as more certain than its parent Evidence", async () => { + const layout = analysisViewLayoutFixture(); + for (const [confidence, expected] of [ + ["observed", "derived"], + ["derived", "derived"], + ["inferred", "inferred"], + ] as const) { + const parent = createEvidence( + { + path: layout.artifact.path, + format: "elf", + architecture: "x86_64", + sha256: layout.artifact.sha256, + }, + BINARY_LAYOUT_TEST_PROVIDER, + { + operation: "inspect_binary_layout", + parameters: { path: layout.artifact.path }, + result: layout, + confidence, + limitations: layout.limitations, + }, + ); + // Inline CLI/MCP views and retained summary delivery share one sealing owner. + const views = [ + await inspectAnalysisView({ + source: { kind: "inline", evidence: parent }, + view: { kind: "item", collection: "sections", selector: { index: 2 } }, + }), + projectRetainedAnalysis( + parent, + { + recordEvidence: () => ok("added"), + evidenceById: (id) => + id === parent.evidence_id ? parent : undefined, + }, + { kind: "summary" }, + ), + ]; + for (const view of views) { + if (!view.ok) throw view.error; + expect(view.value.confidence).toBe(expected); + expect(view.value.authority).toBe(parent.authority); + expect(view.value.evidence_links).toEqual([parent.evidence_id]); + } + } +}); + it("resolves same-session retained JavaScript Evidence without re-running analysis", async () => { const parent = analysisViewJavaScriptEvidence( analysisViewJavaScriptAnalysisWithSource(), diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index e0576ac98e..93e4f9f0b1 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -416,8 +416,10 @@ const createAnalysisViewEvidence = ( result, rawResult: null, confidence: + // A projection never strengthens its parent: inferred facts stay inferred. // Grouping is whole-capture, so any templated group or parameter makes the result inferred; // entity types and identity in an extracted state, and the coverage summarizing it, always are. + parent.confidence === "inferred" || result.kind === "environment-state" || result.kind === "environment-coverage" || ((result.kind === "api-surface" || diff --git a/tests/boundary/cli/analysisViewCli.test.ts b/tests/boundary/cli/analysisViewCli.test.ts index e8407f14e3..e5b111c5f4 100644 --- a/tests/boundary/cli/analysisViewCli.test.ts +++ b/tests/boundary/cli/analysisViewCli.test.ts @@ -30,6 +30,7 @@ import { environmentStateCaptureFixture, } from "../../fixtures/analysisView.js"; import { binaryLayoutFixture } from "../../fixtures/binaryDiagnostics/layout.js"; +import { evmInterfaceFixture } from "../../fixtures/evm/interface.js"; import { createEvidence, parseEvidence, @@ -854,6 +855,48 @@ cliTest( }, ); +cliTest( + "keeps an inferred parent's confidence on every view through the compiled CLI", + async ({ cli }) => { + const value = evmInterfaceFixture(); + const parent = createEvidence( + { + path: value.artifact.path, + sha256: value.artifact.sha256, + format: "file", + }, + { id: "fixture-evm", name: "EVMole", version: "1" }, + { + operation: "inspect_evm_interface", + parameters: { path: value.artifact.path, encoding: "hex" }, + result: value, + confidence: "inferred", + }, + ); + for (const view of [ + { kind: "summary" }, + { kind: "evm", facet: "bytecode_hex", offset: 0, limit: 4 }, + ]) { + const result = await cli.run({ + arguments: [ + "inspect-analysis-view", + JSON.stringify({ + source: { kind: "inline", evidence: parent }, + view, + }), + "--json", + ], + }); + expect(result.exitCode, result.stdout).toBe(0); + expect(parseEvidence(result.json)).toMatchObject({ + confidence: "inferred", + authority: parent.authority, + evidence_links: [parent.evidence_id], + }); + } + }, +); + cliTest("rejects malformed JSON before projection", async ({ cli }) => { const root = await createTestTempDirectory("rea-analysis-view-cli-bad-"); const result = await cli.run({ diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 2869185d26..c463e59629 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -398,7 +398,9 @@ it("returns the exact inferred interface summary while preserving complete defau ); expect(derived.evidence_id).not.toBe(original.evidence_id); expect(derived.evidence_links).toEqual([original.evidence_id]); - expect(derived.confidence).toBe("derived"); + // Retention and paging do not make inferred candidates more certain. + expect(original.confidence).toBe("inferred"); + expect(derived.confidence).toBe("inferred"); const { hex: _hex, ...bytecode } = value.bytecode; expect(view).toMatchObject({ parent_operation: "inspect_evm_interface", @@ -517,6 +519,7 @@ it.each(["valid", "wrong-subject", "no-subject"])( exhausted: true, }); expect(derived.evidence_links).toEqual([source.evidence_id]); + expect(derived.confidence).toBe("inferred"); } else expect(parseMcpToolError(response).error.code).toBe("invalid_request"); expect(inspect).not.toHaveBeenCalled(); diff --git a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts index bcdae16a93..d835e47f04 100644 --- a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts @@ -22,6 +22,8 @@ it.each(["full", "compact"] as const)( parent.normalized_result, ); expect(full.summary).toMatchObject({ split: 1, unknown: 2 }); + // Static source-to-bundle mappings are inferences; no view may strengthen them. + expect(parent.confidence).toBe("inferred"); const fileParent = await call("compare_source_to_bundle", { ...inputs, reference: { kind: "file", path }, @@ -45,6 +47,7 @@ it.each(["full", "compact"] as const)( ...inputs, view: { kind: "summary" }, }); + expect(summary.confidence).toBe("inferred"); expect(summary.normalized_result).toMatchObject({ kind: "summary", parent_evidence_id: parent.evidence_id, @@ -80,6 +83,7 @@ it.each(["full", "compact"] as const)( }); const outputSchema: Record = comparisonTool.outputSchema!; expect(ajv.compile(outputSchema)(selected)).toBe(true); + expect(selected.confidence).toBe("inferred"); const page = analysisViewResultSchema.parse(selected.normalized_result); expect(page).toMatchObject({ kind: "source-to-bundle", @@ -101,6 +105,10 @@ it.each(["full", "compact"] as const)( kind: "retained-evidence", evidence_id: fileParent.evidence_id, }; + for (const view of [{ kind: "summary" }, request]) + expect( + (await call("inspect_analysis_view", { source, view })).confidence, + ).toBe("inferred"); await assertRetainedPages(call, source, request, full); await writeFile( path,