From 499e2f54efc65aff427978613284c3e429398d44 Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:52:54 -0700 Subject: [PATCH] perf(evidence): stop copying normalized results into raw_result (#1402) createAnalysisExecution defaulted an omitted rawResult to a second complete copy of the result. Every caller that had no distinct provider representation therefore doubled its Evidence: decode_interface_builder, inspect_keyed_archive, inspect_asset_catalog, trace_dylib_resolution, inspect_native_dispatch_metadata, observe_native_calls, observe_native_ui/capture_native_ui_scenario (including screenshots), and small Ghidra/IDA lifecycle records. Sibling operations of the same providers already recorded null. An omitted rawResult now records null. Normalized results are unchanged. MCP responses repeat Evidence in text and structured content, so each response and its share of the 10 MiB transport budget shrink by the same half; e.g. trace_dylib_resolution on a real app bundle drops from 3,217,119 to 1,612,524 bytes per representation. Co-authored-by: Claude Opus 5.5 --- docs/mcp-contracts.md | 4 +++- src/application/AnalysisProvider.test.ts | 22 +++++++++++++++++++ src/application/AnalysisProvider.ts | 10 +++++++-- tests/boundary/cli/dylibResolutionCli.test.ts | 3 +++ .../composition/directAnalysisCleanup.test.ts | 4 ++-- 5 files changed, 38 insertions(+), 5 deletions(-) create mode 100644 src/application/AnalysisProvider.test.ts diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 66487bd010..5eedd9c14c 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -198,7 +198,9 @@ Custom clients upgrading from 6.1 should follow the Evidence-producing tools return the complete canonical Evidence record in both text and structured content. Read `structuredContent.normalized_result` for the -operation result and `structuredContent.evidence_id` for its identity. The same +operation result and `structuredContent.evidence_id` for its identity. +`raw_result` holds a provider representation distinct from that result, such +as an upstream tool's original report, and is `null` when there is none. The same record is retained in the session bundle. `analysis_profile` is always present: a concrete profile object or `null`; either value participates in semantic identity. Records omitting this field are rejected. Pass the returned Evidence diff --git a/src/application/AnalysisProvider.test.ts b/src/application/AnalysisProvider.test.ts new file mode 100644 index 0000000000..01b6afc17a --- /dev/null +++ b/src/application/AnalysisProvider.test.ts @@ -0,0 +1,22 @@ +import { expect, it } from "vitest"; + +import { createAnalysisExecution } from "./AnalysisProvider.js"; + +const PROVIDER = { id: "fixture", name: "Fixture", version: "1" }; + +it("records no raw result unless the provider supplies a distinct one", () => { + const result = { entries: [{ value: "normalized" }] }; + + expect(createAnalysisExecution(result, PROVIDER)).toMatchObject({ + result, + rawResult: null, + }); + expect( + createAnalysisExecution(result, PROVIDER, { + rawResult: { stdout: "raw" }, + }).rawResult, + ).toEqual({ stdout: "raw" }); + expect( + createAnalysisExecution(result, PROVIDER, { rawResult: null }).rawResult, + ).toBeNull(); +}); diff --git a/src/application/AnalysisProvider.ts b/src/application/AnalysisProvider.ts index 99328c1a57..a922cdb400 100644 --- a/src/application/AnalysisProvider.ts +++ b/src/application/AnalysisProvider.ts @@ -41,7 +41,13 @@ export interface AnalysisExecution { readonly subject: EvidenceSubjectTarget | null; } -/** Build a validated atomic provider observation at an adapter boundary. */ +/** + * Build a validated atomic provider observation at an adapter boundary. + * + * Pass `rawResult` only for a provider representation distinct from + * `result`. Omitting it records `null` rather than a second complete copy of + * the normalized result in every Evidence record and tool response. + */ export const createAnalysisExecution = ( result: unknown, provider: ProviderIdentity, @@ -56,7 +62,7 @@ export const createAnalysisExecution = ( result: jsonValueSchema.parse(result), rawResult: options.rawResult === undefined - ? jsonValueSchema.parse(result) + ? null : jsonValueSchema.parse(options.rawResult), provider, ...(options.analysisProfile === undefined diff --git a/tests/boundary/cli/dylibResolutionCli.test.ts b/tests/boundary/cli/dylibResolutionCli.test.ts index 7a6b12a4d6..c92d265b29 100644 --- a/tests/boundary/cli/dylibResolutionCli.test.ts +++ b/tests/boundary/cli/dylibResolutionCli.test.ts @@ -120,6 +120,9 @@ describe("trace-dylib-resolution CLI", () => { "Contents/Helpers/tool", "Contents/MacOS/App", ]); + // The normalized result is the provider's only representation; Evidence + // must not carry it a second time as raw_result. + expect(result.json).toMatchObject({ raw_result: null }); expect( trace.edges.map(({ loader, install_name: name, resolution }) => [ loader, diff --git a/tests/composition/directAnalysisCleanup.test.ts b/tests/composition/directAnalysisCleanup.test.ts index 22fd849428..fcbdbddcba 100644 --- a/tests/composition/directAnalysisCleanup.test.ts +++ b/tests/composition/directAnalysisCleanup.test.ts @@ -84,7 +84,7 @@ describe("one-shot analysis cleanup outcomes", () => { operation: "read_bytes", subject: { local_path: path }, normalized_result: { observation: "retained result" }, - raw_result: { observation: "retained result" }, + raw_result: null, }, }, }); @@ -198,7 +198,7 @@ describe("managed one-shot cleanup outcomes", () => { operation: "inspect_managed_artifact", observation: "retained result", }, - rawResult: { observation: "retained result" }, + rawResult: null, provider: { id: "fixture" }, }, },