mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
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:
1 parent
c6893d1ac6
commit
7f65617064
6 files changed
+555
-19
No files matched your search
@@ -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" } },
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user