fix(artifacts): resolve historical retained summaries

Authenticate exact retained parents against current and documented historical summary projections, including caller-selected display paths. Keep source facts, Evidence commitments and unsupported-projection rejection intact.
This commit is contained in:
morluto committed 2026-10-27 06:21:50 +08:00
1 parent c6893d1ac6
commit 7f65617064
6 files changed
+555 -19

No files matched your search

+4
View File
@@ -26,6 +26,10 @@ can be passed directly to `compare_functions`, and `inspect_artifact` Evidence
to `compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
retained session history or an explicit bundle for transfer.
`compare_artifacts` can also resolve an artifact summary to its exact retained
parent in the session. Supported historical summaries keep their original
Evidence identity and omissions; a missing or mismatched parent is rejected.
`analyze_function` does not deliver its complete record by default. Without
`detail`, it retains the complete dossier in the session and returns
`inspect_analysis_view` summary Evidence whose
@@ -2,10 +2,14 @@ import { expect, it } from "vitest";
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js";
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
import {
artifactInventorySummaryViewResultSchema,
sealAnalysisView,
} from "../../domain/analysisView/analysisView.js";
import {
createEvidence,
parseEvidence,
rebindEvidenceToTargetPath,
type Evidence,
} from "../../domain/evidence.js";
import {
@@ -13,6 +17,11 @@ import {
EvidenceReferenceError,
} from "../../domain/evidenceErrors.js";
import { resolveArtifactComparisonEvidence } from "./ArtifactComparisonEvidence.js";
import { artifactInventoryResultSchema } from "../../domain/artifactGraph.js";
import {
artifactGraphDigest,
artifactManifestId,
} from "../../domain/artifactIdentity.js";
const artifactSummary = async () => {
const parent = ARTIFACT_COMPARISON_EXAMPLE.left;
@@ -164,3 +173,281 @@ it("accepts display-only subject changes excluded from Evidence identity", async
);
expect(resolved).toEqual({ ok: true, value: parent });
});
const traversalUnknown =
"Producer traversal coverage is unknown; missing matches do not prove source absence.";
const hashesUnknown =
"Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.";
type SummaryEra = "pre-ce1f4b1bb" | "pre-b395190e4" | "current";
/** Recommit an inventory whose two independent unknowns exercise both migrations. */
const unknownInventory = () => {
const original = ARTIFACT_COMPARISON_EXAMPLE.left;
const inventory = artifactInventoryResultSchema.parse(
original.normalized_result,
);
inventory.coverage.status = "unknown";
inventory.occurrences[0]!.hash_status = "unavailable";
const graphSha256 = artifactGraphDigest({
nodes: inventory.nodes,
occurrences: inventory.occurrences,
edges: inventory.edges,
contradictions: inventory.integrity_contradictions,
});
inventory.manifest.graph_sha256 = graphSha256;
inventory.manifest.manifest_id = artifactManifestId(
inventory.manifest.root_artifact_id,
graphSha256,
);
return createEvidence(
{
path: "historical-inventory",
sha256: inventory.manifest.root_sha256,
format: "file",
},
original.provider,
{
operation: "inventory_artifact",
parameters: {},
result: inventory,
confidence: "observed",
authority: "shipped-artifact",
},
);
};
/**
* Exact non-PCK/non-PyInstaller summary deltas checked against the projectors at
* ce1f4b1bb^ and b395190e4^. The first omitted coverage and both caveats; the
* second included coverage/traversal, but still omitted the hash caveat.
*/
const resealSummary = (
result: ReturnType<typeof artifactInventorySummaryViewResultSchema.parse>,
) => {
const { view_digest: _digest, ...unsigned } = result;
return artifactInventorySummaryViewResultSchema.parse(
sealAnalysisView(unsigned),
);
};
const summaryAtEra = async (era: SummaryEra, aliasPath?: string) => {
const parent = unknownInventory();
const selectedParent =
aliasPath === undefined
? parent
: rebindEvidenceToTargetPath(parent, {
path: aliasPath,
sha256: parent.subject!.digest.sha256,
format: parent.subject!.format,
});
const projected = await inspectAnalysisView({
source: { kind: "inline", evidence: selectedParent },
view: { kind: "summary" },
});
if (!projected.ok) throw projected.error;
const current = artifactInventorySummaryViewResultSchema.parse(
projected.value.normalized_result,
);
expect(current.summary.source_coverage).toEqual({
scope: "root",
status: "unknown",
});
expect(current.unknowns).toContain(traversalUnknown);
expect(current.unknowns).toContain(hashesUnknown);
const { source_coverage: _coverage, ...withoutCoverage } = current.summary;
const result = {
...current,
summary: era === "pre-ce1f4b1bb" ? withoutCoverage : current.summary,
unknowns: current.unknowns.filter(
(unknown) =>
era === "current" ||
(unknown !== hashesUnknown &&
(era !== "pre-ce1f4b1bb" || unknown !== traversalUnknown)),
),
};
return {
parent,
summary: recreateSummary(projected.value, resealSummary(result)),
};
};
it.each<SummaryEra>(["pre-ce1f4b1bb", "pre-b395190e4", "current"])(
"authenticates the exact %s summary against its retained inventory",
async (era) => {
const { parent, summary } = await summaryAtEra(era);
expect(resolveArtifactComparisonEvidence(summary, () => parent)).toEqual({
ok: true,
value: parent,
});
},
);
it.each<SummaryEra>(["pre-ce1f4b1bb", "pre-b395190e4", "current"])(
"authenticates a %s summary projected from the caller alias against the original retained parent",
async (era) => {
const { parent, summary } = await summaryAtEra(era, "/selected/alias.bin");
const result = artifactInventorySummaryViewResultSchema.parse(
summary.normalized_result,
);
expect(result.artifact?.path).toBe("/selected/alias.bin");
expect(parent.subject?.local_path).toBe("historical-inventory");
expect(resolveArtifactComparisonEvidence(summary, () => parent)).toEqual({
ok: true,
value: parent,
});
},
);
it.each<SummaryEra>(["pre-ce1f4b1bb", "pre-b395190e4", "current"])(
"authenticates a %s summary after its retained parent is rebound to an alias path",
async (era) => {
const { parent, summary } = await summaryAtEra(era);
const rebound = rebindEvidenceToTargetPath(parent, {
path: "/selected/alias.bin",
sha256: parent.subject!.digest.sha256,
format: parent.subject!.format,
});
expect(rebound.evidence_id).toBe(parent.evidence_id);
expect(resolveArtifactComparisonEvidence(summary, () => rebound)).toEqual({
ok: true,
value: rebound,
});
},
);
it.each([
"coverage-only",
"traversal-only",
"hash-only-without-traversal",
"all-unknowns",
"wrong-count",
"wrong-parent-digest",
"wrong-parameters",
] as const)(
"rejects an unsupported historical summary: %s",
async (variant) => {
const { parent, summary } = await summaryAtEra("current");
const result = artifactInventorySummaryViewResultSchema.parse(
summary.normalized_result,
);
let parameters = summary.parameters;
switch (variant) {
case "coverage-only":
delete result.summary.source_coverage;
break;
case "traversal-only":
result.unknowns = result.unknowns.filter(
(unknown) => unknown !== traversalUnknown,
);
break;
case "hash-only-without-traversal":
delete result.summary.source_coverage;
result.unknowns = result.unknowns.filter(
(unknown) => unknown !== traversalUnknown,
);
break;
case "all-unknowns":
result.unknowns = [];
break;
case "wrong-count":
result.summary.available_counts.occurrences += 1;
break;
case "wrong-parent-digest":
result.parent_digest = "f".repeat(64);
break;
case "wrong-parameters":
parameters = {
...parameters,
parent_evidence_id: `ev_${"f".repeat(64)}`,
};
break;
}
const forged = recreateSummary(summary, resealSummary(result), parameters);
const resolved = resolveArtifactComparisonEvidence(forged, () => parent);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
},
);
it.each<SummaryEra>(["pre-ce1f4b1bb", "pre-b395190e4"])(
"rejects a %s summary when lookup returns another inventory",
async (era) => {
const { summary } = await summaryAtEra(era);
const resolved = resolveArtifactComparisonEvidence(
summary,
() => ARTIFACT_COMPARISON_EXAMPLE.right,
);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toMatchObject({ reason: "identity_mismatch" });
},
);
it.each(["provider", "authority", "locations", "links"] as const)(
"rejects a validly recommitted historical summary with altered %s",
async (field) => {
const { parent, summary } = await summaryAtEra("pre-ce1f4b1bb");
const altered: Evidence = {
...summary,
...(field === "provider"
? { provider: { ...summary.provider, version: "altered" } }
: field === "authority"
? { authority: "analyst-inference" as const }
: field === "locations"
? {
locations: [
{ kind: "artifact-path" as const, path: "different.bin" },
],
}
: {
evidence_links: [ARTIFACT_COMPARISON_EXAMPLE.right.evidence_id],
}),
};
const forged = recreateSummary(altered, summary.normalized_result);
expect(parseEvidence(forged).evidence_id).toBe(forged.evidence_id);
expect(forged.evidence_id).not.toBe(summary.evidence_id);
const resolved = resolveArtifactComparisonEvidence(forged, () => parent);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
},
);
it("authenticates a retained parent's content instead of trusting its original ID", async () => {
const { parent, summary } = await summaryAtEra("pre-b395190e4");
const tampered: Evidence = {
...parent,
limitations: ["changed without recomputing the Evidence ID"],
};
expect(tampered.evidence_id).toBe(parent.evidence_id);
const resolved = resolveArtifactComparisonEvidence(summary, () => tampered);
expect(resolved.ok).toBe(false);
if (!resolved.ok)
expect(resolved.error).toBeInstanceOf(EvidenceIntegrityError);
});
it.each<SummaryEra>(["pre-ce1f4b1bb", "pre-b395190e4", "current"])(
"preserves both immutable Evidence commitments when resolving %s",
async (era) => {
const { parent, summary } = await summaryAtEra(era, "/selected/alias.bin");
const before = {
parentId: parent.evidence_id,
summaryId: summary.evidence_id,
parentResult: JSON.stringify(parent.normalized_result),
summaryResult: JSON.stringify(summary.normalized_result),
};
const resolved = resolveArtifactComparisonEvidence(summary, () => parent);
expect(resolved.ok).toBe(true);
if (resolved.ok) expect(resolved.value.evidence_id).toBe(before.parentId);
expect(parent.evidence_id).toBe(before.parentId);
expect(summary.evidence_id).toBe(before.summaryId);
expect(JSON.stringify(parent.normalized_result)).toBe(before.parentResult);
expect(JSON.stringify(summary.normalized_result)).toBe(
before.summaryResult,
);
expect(parseEvidence(parent).evidence_id).toBe(before.parentId);
expect(parseEvidence(summary).evidence_id).toBe(before.summaryId);
},
);
@@ -1,16 +1,32 @@
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
import {
resolveEvidenceInput,
type EvidenceLookup,
} from "../EvidenceInputResolver.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewService.js";
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
import {
artifactInventorySummaryViewResultSchema,
sealAnalysisView,
} from "../../domain/analysisView/analysisView.js";
import {
EvidenceIntegrityError,
EvidenceReferenceError,
} from "../../domain/evidenceErrors.js";
import type { Evidence } from "../../domain/evidence.js";
import {
evidenceSemanticContent,
rebindEvidenceToTargetPath,
type Evidence,
} from "../../domain/evidence.js";
import { readEvidence } from "../../domain/evidenceValidation.js";
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import { ARTIFACT_INVENTORY_VIEW_CAVEATS } from "../../domain/analysisView/artifactInventoryViewCaveats.js";
import { err, ok, type Result } from "../../domain/result.js";
/** Resolve only an artifact summary's exact retained owner before comparison. */
/**
* Resolve an already-authenticated artifact summary's exact retained owner.
* The caller must authenticate the supplied Evidence before invoking this helper.
*/
export const resolveArtifactComparisonEvidence = (
evidence: Evidence,
evidenceById: EvidenceLookup | undefined,
@@ -43,19 +59,95 @@ export const resolveArtifactComparisonEvidence = (
null,
),
);
const selected = resolveEvidenceInput(
{ kind: "retained-evidence", evidence_id: parentId },
() => parent,
);
if (!selected.ok) return selected;
const authenticated = readEvidence(selected.value);
if (!authenticated.ok) return authenticated;
const owner = authenticated.value;
const subject = evidence.subject;
if (
subject === null ||
owner.subject === null ||
subject.digest.sha256 !== owner.subject.digest.sha256 ||
subject.format !== owner.subject.format ||
subject.architecture !== owner.subject.architecture
)
return mismatch();
// The ledger preserves its original display path; summary delivery may use a
// later caller-selected alias for this same authenticated owner identity.
const presentation = rebindEvidenceToTargetPath(owner, {
path: summary.data.artifact.path,
sha256: subject.digest.sha256,
format: subject.format,
...(subject.architecture === null
? {}
: { architecture: subject.architecture }),
});
const reprojected = inspectAnalysisViewValidated(
{
source: { kind: "retained-evidence", evidence_id: parentId },
view: { kind: "summary" },
},
(evidenceId) => (evidenceId === parentId ? parent : undefined),
(evidenceId) => (evidenceId === parentId ? presentation : undefined),
);
if (!reprojected.ok) return reprojected;
if (reprojected.value.evidence_id !== evidence.evidence_id)
return err(
new EvidenceIntegrityError(
"Artifact summary does not match its retained parent inventory.",
),
);
return ok(parent);
if (!summaryProjectionIds(reprojected.value).includes(evidence.evidence_id))
return mismatch();
return ok(owner);
};
const mismatch = (): Result<never, EvidenceIntegrityError> =>
err(
new EvidenceIntegrityError(
"Artifact summary does not match its retained parent inventory.",
),
);
/** Exact shipped projections, rather than arbitrary omission or prose tolerance. */
const summaryProjectionIds = (current: Evidence): readonly string[] => {
const result = artifactInventorySummaryViewResultSchema.parse(
current.normalized_result,
);
const format = result.summary.manifest.root_format;
if (
format === "godot-pck" ||
format === "pyinstaller" ||
format === "node-sea"
)
return [current.evidence_id];
const { view_digest: _digest, ...projection } = result;
// b395190e4 began disclosing unavailable hashes for ordinary root formats.
const beforeHashDisclosure = {
...projection,
unknowns: projection.unknowns.filter(
(value) => value !== ARTIFACT_INVENTORY_VIEW_CAVEATS.unknownContentHashes,
),
};
// ce1f4b1bb began carrying traversal coverage and its caveat for these roots.
const { source_coverage: _coverage, ...summary } = projection.summary;
const beforeTraversalDisclosure = {
...beforeHashDisclosure,
summary,
unknowns: beforeHashDisclosure.unknowns.filter(
(value) => value !== ARTIFACT_INVENTORY_VIEW_CAVEATS.unknownTraversal,
),
};
return [
current.evidence_id,
...[beforeHashDisclosure, beforeTraversalDisclosure].map(
(variant) =>
`ev_${digestCanonicalValue(
evidenceSemanticContent({
...current,
normalized_result: artifactInventorySummaryViewResultSchema.parse(
sealAnalysisView(variant),
),
}),
"Evidence",
)}`,
),
];
};
@@ -37,6 +37,7 @@ import {
pageViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewResultInputError } from "./analysisViewParent.js";
import { ARTIFACT_INVENTORY_VIEW_CAVEATS } from "./artifactInventoryViewCaveats.js";
const page = {
kind: z.literal("inventory"),
@@ -284,14 +285,10 @@ export const projectArtifactInventoryView = (
unknowns: [
"Inventory does not establish application runtime behavior or complete format-specific analysis.",
...(inventory.coverage.status === "unknown"
? [
"Producer traversal coverage is unknown; missing matches do not prove source absence.",
]
? [ARTIFACT_INVENTORY_VIEW_CAVEATS.unknownTraversal]
: []),
...(counts.hashes.unavailable > 0 || counts.hashes["not-hashed"] > 0
? [
"Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.",
]
? [ARTIFACT_INVENTORY_VIEW_CAVEATS.unknownContentHashes]
: []),
...(inventory.complete
? []
@@ -0,0 +1,13 @@
/**
* These immutable projection facts also identify exact historical summaries.
* Preserve their shipped wording: changing it requires another explicit
* historical projection variant rather than rewriting retained Evidence.
*/
export const ARTIFACT_INVENTORY_VIEW_CAVEATS = Object.freeze({
/** Added for ordinary root formats by ce1f4b1bb. */
unknownTraversal:
"Producer traversal coverage is unknown; missing matches do not prove source absence.",
/** Added for ordinary root formats by b395190e4. */
unknownContentHashes:
"Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.",
});
@@ -0,0 +1,143 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
artifactInventorySummaryViewResultSchema,
sealAnalysisView,
} from "../../src/domain/analysisView/analysisView.js";
import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js";
import { digestCanonicalValue } from "../../src/domain/canonicalDigest.js";
import {
evidenceSemanticContent,
parseEvidence,
rebindEvidenceToTargetPath,
type Evidence,
} from "../../src/domain/evidence.js";
import { parseMcpToolError } from "../fixtures/mcpToolError.js";
import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js";
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
const resealSummary = (
evidence: Evidence,
result: ReturnType<typeof artifactInventorySummaryViewResultSchema.parse>,
): Evidence => {
const { view_digest: _digest, ...unsigned } = result;
const revised = {
...evidence,
normalized_result: artifactInventorySummaryViewResultSchema.parse(
sealAnalysisView(unsigned),
),
};
return parseEvidence({
...revised,
evidence_id: `ev_${digestCanonicalValue(evidenceSemanticContent(revised), "Evidence")}`,
});
};
it("compares a historical alias summary through its retained complete MCP parent and rejects resealed false facts", async () => {
const root = await createTestTempDirectory("rea-historical-summary-mcp-");
const path = join(root, "original.plist");
await writeFile(
path,
'<?xml version="1.0"?><plist version="1.0"><dict><key>label</key><string>hello</string></dict></plist>',
);
const { call } = await connectLocalToolsMcp();
const opened = await call("open_binary", { path });
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const inspected = await call("inspect_artifact", { detail: "complete" });
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
const parent = parseEvidence(inspected.structuredContent);
const originalParent = JSON.stringify(parent);
const alias = join(root, "caller-selected-alias.plist");
const aliasedParent = rebindEvidenceToTargetPath(parent, {
path: alias,
sha256: parent.subject!.digest.sha256,
format: parent.subject!.format,
});
expect(aliasedParent.evidence_id).toBe(parent.evidence_id);
const projected = await call("inspect_analysis_view", {
source: { kind: "inline", evidence: aliasedParent },
view: { kind: "summary" },
});
expect(projected.isError, JSON.stringify(projected)).not.toBe(true);
const current = parseEvidence(projected.structuredContent);
const result = artifactInventorySummaryViewResultSchema.parse(
current.normalized_result,
);
expect(result).toMatchObject({
parent_evidence_id: parent.evidence_id,
artifact: { path: alias },
summary: { source_coverage: { scope: "root", status: "complete" } },
});
// Exact ordinary-root projector before ce1f4b1bb: no source_coverage or
// traversal caveat. Before b395190e4 the hash caveat was also absent; this
// fully hashed root has no such caveat in either era.
const { source_coverage: _coverage, ...summary } = result.summary;
const historical = resealSummary(current, {
...result,
summary,
unknowns: result.unknowns.filter(
(value) =>
value !==
"Producer traversal coverage is unknown; missing matches do not prove source absence." &&
value !==
"Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.",
),
});
expect(historical.evidence_id).not.toBe(current.evidence_id);
const originalHistorical = JSON.stringify(historical);
const compared = await call("compare_artifacts", {
left: historical,
right: parent,
});
expect(compared.isError, JSON.stringify(compared)).not.toBe(true);
const comparison = parseEvidence(compared.structuredContent);
expect(
artifactComparisonResultSchema.parse(comparison.normalized_result),
).toMatchObject({
status: "unchanged",
summary: { unchanged: 1, unknown: 0 },
changes: [],
});
expect(comparison.evidence_links).toContain(parent.evidence_id);
expect(comparison.evidence_links).not.toContain(historical.evidence_id);
const historicalResult = artifactInventorySummaryViewResultSchema.parse(
historical.normalized_result,
);
const forged = resealSummary(historical, {
...historicalResult,
summary: {
...historicalResult.summary,
available_counts: {
...historicalResult.summary.available_counts,
nodes: historicalResult.summary.available_counts.nodes + 1,
},
},
});
const rejected = await call("compare_artifacts", {
left: forged,
right: parent,
});
expect(rejected.isError, JSON.stringify(rejected)).toBe(true);
expect(parseMcpToolError(rejected)).toMatchObject({
error: { code: "evidence_integrity_mismatch" },
});
expect(JSON.stringify(parent)).toBe(originalParent);
expect(JSON.stringify(historical)).toBe(originalHistorical);
const retained = await call("inspect_analysis_view", {
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "summary" },
});
expect(retained.isError, JSON.stringify(retained)).not.toBe(true);
expect(
parseEvidence(retained.structuredContent).normalized_result,
).toMatchObject({
parent_evidence_id: parent.evidence_id,
artifact: { path },
summary: { source_coverage: { scope: "root", status: "complete" } },
});
});