diff --git a/scripts/lib/catalog-core.mjs b/scripts/lib/catalog-core.mjs index 700b049fce..446d2d5474 100644 --- a/scripts/lib/catalog-core.mjs +++ b/scripts/lib/catalog-core.mjs @@ -130,8 +130,6 @@ const SOURCE_PATHS = { v8InspectorProvider: "dist/inspector/providerIdentity.js", evidence: "dist/domain/evidence.js", evidenceBundle: "dist/domain/evidenceBundle.js", - evidenceCompletion: "dist/domain/evidenceCompletionLedger.js", - completionGeneration: "dist/domain/completionLedgerGeneration.js", processCapture: "dist/domain/process/processCapture.js", analysisSnapshot: "dist/domain/analysisSnapshot.js", artifactGraph: "dist/domain/artifactGraph.js", diff --git a/src/domain/completionLedgerGeneration.test.ts b/src/domain/completionLedgerGeneration.test.ts index 9eaf4e431e..8d9f2d6acf 100644 --- a/src/domain/completionLedgerGeneration.test.ts +++ b/src/domain/completionLedgerGeneration.test.ts @@ -2,11 +2,11 @@ import { describe, expect, it } from "vitest"; import { completionVerifierReportSchema, + completionManifestSchema, createCompletionLedgerArtifacts, - parseCompletionManifest, type CompletionVerifierReport, } from "./completionLedgerGeneration.js"; -import { parseEvidenceCompletionLedger } from "./evidenceCompletionLedger.js"; +import { evidenceCompletionLedgerSchema } from "./evidenceCompletionLedger.js"; const digest = (digit: string): string => digit.repeat(64); const evidenceId = (digit: string): string => `ev_${digest(digit)}`; @@ -246,13 +246,13 @@ describe("completion ledger identity validation", () => { const generated = generate(); expect(() => - parseCompletionManifest({ + completionManifestSchema.parse({ ...generated.manifest, manifest_id: `ecm_${digest("0")}`, }), ).toThrow(); expect(() => - parseEvidenceCompletionLedger({ + evidenceCompletionLedgerSchema.parse({ ...generated.ledger, records: generated.ledger.records.slice(1), }), diff --git a/src/domain/completionLedgerGeneration.ts b/src/domain/completionLedgerGeneration.ts index 1c25969115..9931ce8950 100644 --- a/src/domain/completionLedgerGeneration.ts +++ b/src/domain/completionLedgerGeneration.ts @@ -226,7 +226,3 @@ export const createCompletionLedgerArtifacts = ( ), }; }; - -/** Parse generated completion commitments and reject manual tampering. */ -export const parseCompletionManifest = (input: unknown): CompletionManifest => - completionManifestSchema.parse(input); diff --git a/src/domain/evidenceCompletionLedger.test.ts b/src/domain/evidenceCompletionLedger.test.ts index 10b49539f4..08e848349d 100644 --- a/src/domain/evidenceCompletionLedger.test.ts +++ b/src/domain/evidenceCompletionLedger.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { createEvidenceCompletionLedger, - parseEvidenceCompletionLedger, + evidenceCompletionLedgerSchema, type EvidenceCompletionStatus, } from "./evidenceCompletionLedger.js"; @@ -103,13 +103,13 @@ describe("Evidence completion ledger", () => { record("scenario.valid", "pass"), ]); expect(() => - parseEvidenceCompletionLedger({ + evidenceCompletionLedgerSchema.parse({ ...ledger, summary: { ...ledger.summary, complete: false }, }), ).toThrow(); expect(() => - parseEvidenceCompletionLedger({ + evidenceCompletionLedgerSchema.parse({ ...ledger, ledger_id: `ecl_${"0".repeat(64)}`, }), @@ -121,7 +121,7 @@ describe("Evidence completion ledger", () => { record("scenario.path-free", "pass"), ]); expect(() => - parseEvidenceCompletionLedger({ + evidenceCompletionLedgerSchema.parse({ ...ledger, records: [ { diff --git a/src/domain/evidenceCompletionLedger.ts b/src/domain/evidenceCompletionLedger.ts index ee806b4b4a..49ef503b43 100644 --- a/src/domain/evidenceCompletionLedger.ts +++ b/src/domain/evidenceCompletionLedger.ts @@ -144,8 +144,3 @@ export const createEvidenceCompletionLedger = ( summary: summarize(records), }); }; - -/** Parse a completion ledger and reject non-canonical or tampered content. */ -export const parseEvidenceCompletionLedger = ( - input: unknown, -): EvidenceCompletionLedger => evidenceCompletionLedgerSchema.parse(input); diff --git a/src/domain/referenceSourceGraph.test.ts b/src/domain/referenceSourceGraph.test.ts index 758a373ec7..37cfa4f8f8 100644 --- a/src/domain/referenceSourceGraph.test.ts +++ b/src/domain/referenceSourceGraph.test.ts @@ -1,13 +1,14 @@ +import { createHash } from "node:crypto"; + +import canonicalize from "canonicalize"; import { describe, expect, it } from "vitest"; import { - computeHistoricalSourceGraphSha256, createHistoricalSourceGraph, createHistoricalSourceManifest, historicalSourceGraphSchema, historicalSourceParseFailureKey, parseHistoricalSourceGraph, - parseHistoricalSourceManifest, type HistoricalSourceGraphInput, } from "./referenceSourceGraph.js"; @@ -108,11 +109,27 @@ const registerSourceCommitmentTests = (): void => { const graph = createHistoricalSourceGraph(graphInput()); const first = createHistoricalSourceManifest(graph); const second = createHistoricalSourceManifest(structuredClone(graph)); + const graphJson = canonicalize(graph); + if (graphJson === undefined) throw new TypeError("Unserializable graph"); + const expectedSemantic = { + schema: "HistoricalSourceManifest", + authority: "historical-reference", + graph_sha256: createHash("sha256").update(graphJson).digest("hex"), + root_sha256: graph.root_sha256, + inventory_state: "complete", + entry_count: 3, + relationship_count: 2, + }; + const semanticJson = canonicalize(expectedSemantic); + if (semanticJson === undefined) + throw new TypeError("Unserializable manifest semantics"); + expect(first).toEqual({ + ...expectedSemantic, + manifest_id: `hsm_${createHash("sha256").update(semanticJson).digest("hex")}`, + }); expect(first).toEqual(second); expect(parseHistoricalSourceGraph(graph)).toEqual(graph); - expect(parseHistoricalSourceManifest(first)).toEqual(first); - expect(first.graph_sha256).toBe(computeHistoricalSourceGraphSha256(graph)); expect(() => createHistoricalSourceGraph({ ...graphInput(), @@ -301,8 +318,8 @@ const registerLocalDiagnosticCommitmentTests = (): void => { }; const revised = createHistoricalSourceGraph(changedMessage); expect(revised.root_sha256).toBe(graph.root_sha256); - expect(computeHistoricalSourceGraphSha256(revised)).not.toBe( - computeHistoricalSourceGraphSha256(graph), + expect(createHistoricalSourceManifest(revised).graph_sha256).not.toBe( + createHistoricalSourceManifest(graph).graph_sha256, ); }); @@ -333,8 +350,9 @@ const registerLocalDiagnosticCommitmentTests = (): void => { "File could not be read safely: EIO. Check permissions.", ]; expect( - computeHistoricalSourceGraphSha256(createHistoricalSourceGraph(moved)), - ).not.toBe(computeHistoricalSourceGraphSha256(graph)); + createHistoricalSourceManifest(createHistoricalSourceGraph(moved)) + .graph_sha256, + ).not.toBe(createHistoricalSourceManifest(graph).graph_sha256); }); }; @@ -370,8 +388,8 @@ describe("partial historical source graphs", () => { }; const revised = createHistoricalSourceGraph(changedMessage); expect(revised.root_sha256).toBe(graph.root_sha256); - expect(computeHistoricalSourceGraphSha256(revised)).not.toBe( - computeHistoricalSourceGraphSha256(graph), + expect(createHistoricalSourceManifest(revised).graph_sha256).not.toBe( + createHistoricalSourceManifest(graph).graph_sha256, ); expect(() => createHistoricalSourceGraph({ @@ -452,7 +470,7 @@ describe("partial historical source graphs", () => { }); describe("historical source graph validation", () => { - it("rejects strict unknown fields, invalid VCS, dangling internal edges, and false manifests", () => { + it("rejects strict unknown fields, invalid VCS, and dangling internal edges", () => { expect(() => createHistoricalSourceGraph({ ...graphInput(), surprise: true }), ).toThrow(); @@ -510,13 +528,6 @@ describe("historical source graph validation", () => { extra: true, }), ).toThrow(); - - const manifest = createHistoricalSourceManifest( - createHistoricalSourceGraph(graphInput()), - ); - expect(() => - parseHistoricalSourceManifest({ ...manifest, entry_count: 999 }), - ).toThrow(/identifier does not match/u); }); }); diff --git a/src/domain/referenceSourceGraph.ts b/src/domain/referenceSourceGraph.ts index 80a1ce6dbb..0d881540f9 100644 --- a/src/domain/referenceSourceGraph.ts +++ b/src/domain/referenceSourceGraph.ts @@ -573,9 +573,10 @@ export const parseHistoricalSourceGraph = ( ): HistoricalSourceGraph => historicalSourceGraphSchema.parse(input); /** Commit semantic graph facts while retaining host-local diagnostic metadata separately. */ -export const computeHistoricalSourceGraphSha256 = (input: unknown): string => { - const graph = parseHistoricalSourceGraph(input); - return digestCanonicalValue( +const computeHistoricalSourceGraphSha256 = ( + graph: HistoricalSourceGraph, +): string => + digestCanonicalValue( { ...graph, entries: graph.entries.map( @@ -584,7 +585,6 @@ export const computeHistoricalSourceGraphSha256 = (input: unknown): string => { }, "Reference source graph", ); -}; /** Build a deterministic, relocation-independent manifest for a source graph. */ export const createHistoricalSourceManifest = ( @@ -605,17 +605,3 @@ export const createHistoricalSourceManifest = ( manifest_id: `hsm_${digestCanonicalValue(semantic, "Reference source graph")}`, }); }; - -/** Parse and verify a historical source manifest's semantic identifier. */ -export const parseHistoricalSourceManifest = ( - input: unknown, -): HistoricalSourceManifest => { - const manifest = historicalSourceManifestBaseSchema.parse(input); - const { manifest_id: manifestId, ...semantic } = manifest; - if ( - `hsm_${digestCanonicalValue(semantic, "Reference source graph")}` !== - manifestId - ) - throw new TypeError("Historical source manifest identifier does not match"); - return manifest; -}; diff --git a/tests/boundary/cli/referenceSourceSummary.test.ts b/tests/boundary/cli/referenceSourceSummary.test.ts index aed8cb8719..ba5398a866 100644 --- a/tests/boundary/cli/referenceSourceSummary.test.ts +++ b/tests/boundary/cli/referenceSourceSummary.test.ts @@ -1,12 +1,11 @@ +import { createHash } from "node:crypto"; import { mkdir, writeFile } from "node:fs/promises"; import { join } from "node:path"; +import canonicalize from "canonicalize"; import { describe, expect } from "vitest"; -import { - createHistoricalSourceManifest, - parseHistoricalSourceGraph, -} from "../../../src/domain/referenceSourceGraph.js"; +import { parseHistoricalSourceGraph } from "../../../src/domain/referenceSourceGraph.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; @@ -39,6 +38,25 @@ describe.skipIf(process.platform === "win32")( const complete = await run(); expect(complete.exitCode).toBe(0); const graph = parseHistoricalSourceGraph(complete.json); + const graphJson = canonicalize(graph); + if (graphJson === undefined) + throw new TypeError("Unserializable complete graph"); + const expectedSemantic = { + schema: "HistoricalSourceManifest", + authority: "historical-reference", + graph_sha256: createHash("sha256").update(graphJson).digest("hex"), + root_sha256: graph.root_sha256, + inventory_state: graph.inventory_state, + entry_count: 7, + relationship_count: 1, + }; + const semanticJson = canonicalize(expectedSemantic); + if (semanticJson === undefined) + throw new TypeError("Unserializable manifest semantics"); + const expectedManifest = { + ...expectedSemantic, + manifest_id: `hsm_${createHash("sha256").update(semanticJson).digest("hex")}`, + }; expect((await run("--detail", "full")).stdout).toBe(complete.stdout); for (const depth of [undefined, 0, 1, 10]) { const result = await run( @@ -47,6 +65,7 @@ describe.skipIf(process.platform === "win32")( ...(depth === undefined ? [] : ["--outline-depth", String(depth)]), ); expect(result.exitCode).toBe(0); + expect(result.json).toHaveProperty("manifest", expectedManifest); const selected = graph.entries.filter( (entry) => entry.path.split("/").length <= (depth ?? 2), ); @@ -54,7 +73,6 @@ describe.skipIf(process.platform === "win32")( schema: "HistoricalSourceSummary", authority: graph.authority, root_alias: graph.root_alias, - manifest: createHistoricalSourceManifest(graph), inventory_state: graph.inventory_state, provenance: graph.provenance, vcs: graph.vcs,