refactor(evidence): carry producer-owned source records

Carry the final artifact inspection's exact nested source Evidence through
AnalysisExecution. Derive links in the shared Evidence builder instead of
reparsing the result in CLI and MCP adapters. Retain validation for unknown
and historical Evidence inputs.

Move the shared source recorder into application ownership and migrate its
consumers. Record sources before parents; keep source recording failures
from publishing summaries. Decline snapshot entries whose source closure the
current entry format cannot represent, while retaining investigation Evidence.

Validation: actual ASAR regression failed before the change with missing
links and passes afterward; exact nested identity, source recorder refusal,
final-close cancellation and cleanup failure, historical authentication,
snapshot controls and public CLI/MCP workflows. Fresh runtime/catalog build,
full static/docs, discovery and duplication checks pass. Installed native
providers and platform-specific skipped cases remain unverified.
This commit is contained in:
morluto committed 2026-10-25 04:37:11 +08:00
1 parent 5ea2c280b8
commit 835904e4ee
30 files changed
+394 -139

No files matched your search

@@ -930,6 +930,63 @@ it("does not replay pre-envelope call graphs under the revised workflow profile"
).toBe(false);
});
it("declines source-bearing snapshot entries while replaying omitted and empty source controls", () => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const source = createEvidence(target, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "source",
parameters: {},
result: "observed source",
});
const parameters = { address: "0x1000", document: "fixture" };
cache.record({
target,
profile,
operation: "procedure_info",
parameters,
execution: createAnalysisExecution(
"parent observation",
ANALYSIS_SNAPSHOT_PROVIDER,
{
subject: target,
analysisProfile: profile,
sourceEvidence: [source],
},
),
});
expect(
cache.lookup(target, profile, "procedure_info", parameters),
).toBeUndefined();
expect(cache.entries()).toEqual([]);
for (const sourceEvidence of [undefined, []] as const) {
const control = {
address: sourceEvidence === undefined ? "0x2000" : "0x3000",
document: "fixture",
};
cache.record({
target,
profile,
operation: "procedure_info",
parameters: control,
execution: createAnalysisExecution(
"source-free observation",
ANALYSIS_SNAPSHOT_PROVIDER,
{
subject: target,
analysisProfile: profile,
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
},
),
});
expect(
cache.lookup(target, profile, "procedure_info", control)?.result,
).toBe("source-free observation");
}
expect(cache.entries()).toHaveLength(2);
});
describe("executions that report truncated references", () => {
const record = (referencesTruncated: boolean | undefined) => {
const cache = new AnalysisSnapshotCache();
@@ -436,6 +436,9 @@ export class AnalysisSnapshotCache {
// The snapshot format does not carry reference truncation; a replay would
// present a capped read as complete, so it is not recorded.
if (execution.referencesTruncated === true) return;
// Provider snapshot entries do not carry execution source records. Replay
// must not discard the source closure that supplies the parent's Evidence links.
if ((execution.sourceEvidence?.length ?? 0) > 0) return;
const queryId = analysisQueryId(
snapshotTarget(target),
snapshotBinding(profile),
+16 -17
View File
@@ -13,7 +13,7 @@ import {
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { createEvidence } from "../../domain/evidence.js";
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
import type {
AnalysisClient,
AnalysisExecution,
@@ -33,6 +33,7 @@ import {
type SessionProviderRoute,
} from "./SessionProviderRouter.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { recordEvidenceSources } from "../investigation/recordEvidenceSources.js";
import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import {
@@ -1256,26 +1257,24 @@ export class BinarySession
name !== "inspect_keyed_archive" &&
name !== "trace_dylib_resolution"
) {
const evidence = createEvidence(
execution.value.subject === undefined
? active.target
: (execution.value.subject ?? undefined),
execution.value.provider,
const evidence = createAnalysisExecutionEvidence(
name,
arguments_,
{
operation: name,
parameters: arguments_,
result: execution.value.result,
...execution.value,
analysisProfile: profile,
rawResult: execution.value.rawResult,
limitations: execution.value.limitations,
...(execution.value.relationshipCoverage === undefined
? {}
: {
relationshipCoverage: execution.value.relationshipCoverage,
}),
locations: execution.value.locations,
subject:
execution.value.subject === undefined
? active.target
: execution.value.subject,
},
"observed",
);
const sourcesRecorded = recordEvidenceSources(
(source) => this.recordEvidence(source),
execution.value.sourceEvidence ?? [],
);
if (!sourcesRecorded.ok) return sourcesRecorded;
const recorded = this.recordEvidence(evidence);
if (!recorded.ok) return recorded;
const retained = { ...execution.value, evidenceId: evidence.evidence_id };