import { describe, expect, it } from "vitest"; import { createEvidence, parseEvidence, type Evidence, type EvidenceObservation, type EvidenceProvider, type EvidenceSubjectTarget, } from "./evidence.js"; import { createEvidenceBundle, parseEvidenceBundle } from "./evidenceBundle.js"; import { functionDossierSchema } from "./hopperValues.js"; import { jsonObjectSchema, jsonValueSchema, type JsonValue, } from "./jsonValue.js"; import { createResidualUnknown } from "./residualUnknown.js"; import { compareFunctions } from "./functionComparison.js"; import { verifyReconstruction } from "./reconstructionVerification.js"; const dossier = jsonValueSchema.parse( functionDossierSchema.parse({ procedure: { address: "0x1000", name: "main", classification: null, body: { available: false, reason: "Hopper's public Python API does not expose complete function body ranges", }, signature: "int main(void)", locals: [], }, pseudocode: "return 0;", assembly: [], comments: [], callers: [], callees: [], incoming_references: [], outgoing_references: [], referenced_strings: [], referenced_names: [], native_api: null, native_value_flow: null, facet_availability: { pseudocode: { available: true }, assembly: { available: true }, comments: { available: true }, callers: { available: true }, callees: { available: true }, references: { available: true }, strings_names: { available: true }, cfg: { available: true }, }, basic_blocks: [{ start: "0x1000", end: "0x1001", successors: [] }], limitations: [], }), ); const source = ( digit: string, observation: Partial = {}, hasSubject = true, hasCoverage = true, ): Evidence => createEvidence( hasSubject ? { path: `/fixture/function-${digit}`, sha256: digit.repeat(64), format: "file", } : undefined, { id: "fixture", name: "Fixture function analysis", version: "1" }, { operation: "analyze_function", parameters: { procedure: "main" }, result: dossier, confidence: "derived", authority: "shipped-artifact", ...(hasCoverage ? { relationshipCoverage: { status: "complete" as const } } : {}), ...observation, }, ); const left = source("1"); const right = source("2"); const extra = source("3"); const singular = { left_evidence_id: left.evidence_id, right_evidence_id: right.evidence_id, }; const plural = { left_evidence_ids: [left.evidence_id], right_evidence_ids: [right.evidence_id], }; const provider = { id: "rea-function-comparison", name: "REA function comparison", version: "1", }; const baselineResult = compareFunctions(left, right); const comparison = ( parameters: Record, observation: Partial = {}, sources: readonly Evidence[] = [left, right], identity: { provider?: EvidenceProvider; target?: EvidenceSubjectTarget; } = {}, resultInput = baselineResult, ): Evidence => createEvidence(identity.target, identity.provider ?? provider, { predicateType: "rea.function-comparison", operation: "compare_functions", parameters, result: jsonValueSchema.parse({ ...resultInput, dimensions: resultInput.dimensions.map((dimension) => ({ ...dimension, evidence_links: sources.map(({ evidence_id: id }) => id), })), }), confidence: "derived", authority: "analyst-inference", evidenceLinks: sources.map(({ evidence_id: id }) => id), ...observation, }); const specification = (evidence: Evidence, dimension = "pseudocode") => ({ name: "Function reconstruction", claims: [ { kind: "structural-function", claim_id: "function", title: "Function agrees", comparison_evidence_id: evidence.evidence_id, dimension, }, ], }); const admittedVerification = ( evidence: Evidence, sources: readonly Evidence[] = [left, right], dimension = "pseudocode", ) => { // Admission controls use newly hashed Evidence and a valid bundle, not a stale digest. expect(parseEvidence(evidence)).toEqual(evidence); const bundle = createEvidenceBundle([...sources, evidence]); expect(parseEvidenceBundle(bundle)).toEqual(bundle); return () => verifyReconstruction(specification(evidence, dimension), bundle); }; const verify = ( evidence: Evidence, sources: readonly Evidence[] = [left, right], ) => admittedVerification(evidence, sources)(); describe("function reconstruction source parameters", () => { it.each([ ["singular", singular], ["plural", plural], ["coherent aliases", { ...singular, ...plural }], ])( "accepts %s and preserves exact sides and unrelated metadata", (_name, parameters) => { const evidence = comparison({ ...parameters, context: { label: "retained", revision: 7 }, }); const before = JSON.stringify(evidence); const result = verify(evidence); expect(result.status).toBe("pass"); expect(result.claims.items).toMatchObject([ { observed_status: "unchanged", left_evidence_ids: [left.evidence_id], right_evidence_ids: [right.evidence_id], }, ]); expect(JSON.stringify(evidence)).toBe(before); }, ); it("preserves plural-only multi-ID sides and their ordering", () => { const fourth = source("4"); const evidence = comparison( { left_evidence_ids: [extra.evidence_id, left.evidence_id], right_evidence_ids: [fourth.evidence_id, right.evidence_id], }, {}, [left, right, extra, fourth], ); expect( verify(evidence, [left, right, extra, fourth]).claims.items, ).toMatchObject([ { status: "pass", left_evidence_ids: [extra.evidence_id, left.evidence_id], right_evidence_ids: [fourth.evidence_id, right.evidence_id], }, ]); }); }); describe("historical calls-only reconstruction changes", () => { it.each(["missing", "unknown", "partial", "unresolved"] as const)( "keeps historical calls-only changes unknown with %s call coverage", (coverage) => { const changedDossier = functionDossierSchema.parse({ ...functionDossierSchema.parse(dossier), callees: [ { address: "0x2000", name: "puts", classification: null, body: { available: false, reason: "External function." }, }, ], }); const complete = source("8", { result: jsonValueSchema.parse(changedDossier), }); const incomplete = source( "9", { result: jsonValueSchema.parse({ ...changedDossier, ...(coverage === "unresolved" ? { unresolved_calls: [ { address: "0x1004", reason: "Computed target unresolved.", }, ], } : {}), }), ...(coverage === "missing" ? {} : { relationshipCoverage: coverage === "unknown" ? { status: "unknown", reason: "Call graph unavailable." } : coverage === "partial" ? { status: "partial", limitations: ["Additional edges omitted."], } : { status: "complete" }, }), }, true, coverage !== "missing", ); const historicalResult = compareFunctions(left, complete); expect(historicalResult).toMatchObject({ status: "changed", summary: { changed: 1 }, }); expect(historicalResult.changes).toMatchObject([ { dimension: "calls", status: "changed" }, ]); const completeComparison = comparison( { left_evidence_id: left.evidence_id, right_evidence_id: complete.evidence_id, }, {}, [left, complete], {}, historicalResult, ); expect( admittedVerification(completeComparison, [left, complete], "overall")() .status, ).toBe("fail"); const evidence = comparison( { left_evidence_id: left.evidence_id, right_evidence_id: incomplete.evidence_id, }, {}, [left, incomplete], {}, historicalResult, ); const before = JSON.stringify(evidence); for (const dimension of ["calls", "overall"] as const) { expect( admittedVerification(evidence, [left, incomplete], dimension)(), ).toMatchObject({ status: "unknown", summary: { total: 1, passed: 0, failed: 0, unknown: 1 }, claims: { items: [ expect.objectContaining({ status: "unknown", observed_status: "changed", limitations: expect.arrayContaining([ expect.stringContaining(`Source ${incomplete.evidence_id}:`), ]), }), ], }, }); } expect( admittedVerification(evidence, [left, incomplete], "pseudocode")() .status, ).toBe("pass"); expect(JSON.stringify(evidence)).toBe(before); }, ); }); describe("function reconstruction call coverage", () => { it("rechecks old unchanged comparisons without affecting unrelated finite dimensions", () => { const incomplete = source("7", { relationshipCoverage: { status: "unknown", reason: "Call graph unavailable.", }, }); const incompleteSides = { left_evidence_id: left.evidence_id, right_evidence_id: incomplete.evidence_id, }; const oldUnchanged = comparison(incompleteSides, {}, [left, incomplete]); for (const dimension of ["calls", "overall"] as const) { const result = admittedVerification( oldUnchanged, [left, incomplete], dimension, )(); expect(result).toMatchObject({ status: "unknown", summary: { total: 1, passed: 0, failed: 0, unknown: 1 }, claims: { items: [ expect.objectContaining({ dimension, status: "unknown", observed_status: "unchanged", limitations: expect.arrayContaining([ `Source ${incomplete.evidence_id}: Call graph unavailable.`, ]), }), ], }, }); } expect( admittedVerification(oldUnchanged, [left, incomplete], "pseudocode")(), ).toMatchObject({ status: "pass", claims: { items: [ expect.objectContaining({ dimension: "pseudocode", status: "pass", observed_status: "unchanged", }), ], }, }); }); it("keeps a confirmed historical overall change as a failure despite unknown calls", () => { const changedDossier = jsonValueSchema.parse({ ...functionDossierSchema.parse(dossier), pseudocode: "return 1;", }); const changedComplete = source("8", { result: changedDossier }); const incomplete = source("9", { result: changedDossier, relationshipCoverage: { status: "unknown", reason: "Call graph unavailable.", }, }); const changedResult = compareFunctions(left, changedComplete); const oldChanged = comparison( { left_evidence_id: left.evidence_id, right_evidence_id: incomplete.evidence_id, }, {}, [left, incomplete], {}, changedResult, ); expect(oldChanged.normalized_result).toMatchObject({ status: "changed" }); expect( admittedVerification(oldChanged, [left, incomplete], "overall")(), ).toMatchObject({ status: "fail", summary: { total: 1, passed: 0, failed: 1, unknown: 0 }, claims: { items: [ expect.objectContaining({ status: "fail", observed_status: "changed", }), ], }, }); }); }); describe("function reconstruction source aliases", () => { it.each([ ["no source declarations", {}], ["missing right singular", { left_evidence_id: left.evidence_id }], ["missing left singular", { right_evidence_id: right.evidence_id }], ["missing right plural", { left_evidence_ids: [left.evidence_id] }], ["missing left plural", { right_evidence_ids: [right.evidence_id] }], [ "mixed left singular/right plural", { left_evidence_id: left.evidence_id, right_evidence_ids: [right.evidence_id], }, ], [ "mixed left plural/right singular", { left_evidence_ids: [left.evidence_id], right_evidence_id: right.evidence_id, }, ], [ "partial singular alias with plural pair", { ...plural, left_evidence_id: left.evidence_id }, ], [ "other partial singular alias with plural pair", { ...plural, right_evidence_id: right.evidence_id }, ], [ "partial plural alias with singular pair", { ...singular, left_evidence_ids: [left.evidence_id] }, ], [ "other partial plural alias with singular pair", { ...singular, right_evidence_ids: [right.evidence_id] }, ], [ "contradictory left alias", { ...singular, ...plural, left_evidence_ids: [extra.evidence_id] }, ], [ "contradictory right alias", { ...singular, ...plural, right_evidence_ids: [extra.evidence_id] }, ], [ "reversed aliases", { ...singular, left_evidence_ids: [right.evidence_id], right_evidence_ids: [left.evidence_id], }, ], [ "extra left alias ID", { ...singular, ...plural, left_evidence_ids: [left.evidence_id, extra.evidence_id], }, ], [ "extra right alias ID", { ...singular, ...plural, right_evidence_ids: [right.evidence_id, extra.evidence_id], }, ], ] satisfies [string, Record][])( "rejects %s after valid digest admission", (_name, parameters) => { expect(admittedVerification(comparison(parameters))).toThrow(); }, ); }); describe("function reconstruction malformed source IDs", () => { it.each( ( [ null, false, 7, "", "ev_invalid", [], [left.evidence_id], { id: left.evidence_id }, ] satisfies JsonValue[] ).map((invalid) => ({ invalid })), )( "rejects malformed singular values $invalid without falling back to plural aliases", ({ invalid }) => { for (const field of ["left_evidence_id", "right_evidence_id"]) { for (const aliases of [{}, plural]) { expect( admittedVerification( comparison({ ...singular, ...aliases, [field]: invalid }), ), ).toThrow(); } } }, ); it.each( ( [ null, false, 7, "", left.evidence_id, [], ["ev_invalid"], [null], { id: left.evidence_id }, ] satisfies JsonValue[] ).map((invalid) => ({ invalid })), )( "rejects malformed plural values $invalid even with valid singular aliases", ({ invalid }) => { for (const field of ["left_evidence_ids", "right_evidence_ids"]) { for (const aliases of [{}, singular]) { expect( admittedVerification( comparison({ ...plural, ...aliases, [field]: invalid }), ), ).toThrow(); } } }, ); it.each([ [ "duplicate left plural source", { ...plural, left_evidence_ids: [left.evidence_id, left.evidence_id] }, ], [ "duplicate right plural source", { ...plural, right_evidence_ids: [right.evidence_id, right.evidence_id] }, ], ])("rejects %s", (_name, parameters) => { expect(admittedVerification(comparison(parameters))).toThrow( /repeats a source within one side/u, ); }); }); describe("function reconstruction admission controls", () => { it.each([singular, plural, { ...singular, ...plural }])( "requires exact source closure for %j", (parameters) => { expect( admittedVerification( comparison(parameters, { evidenceLinks: [left.evidence_id] }), ), ).toThrow(/closure disagrees/u); expect( admittedVerification( comparison(parameters, { evidenceLinks: [ left.evidence_id, right.evidence_id, extra.evidence_id, ], }), [left, right, extra], ), ).toThrow(/closure disagrees/u); expect(admittedVerification(comparison(parameters), [left])).toThrow( /Missing source Evidence/u, ); }, ); it.each([ { operation: "inventory_artifact" }, { predicateType: "rea.process-capture" }, ])("rejects source observation type %j", (observation) => { const wrong = source("1", observation); const evidence = comparison( { ...singular, left_evidence_id: wrong.evidence_id }, {}, [wrong, right], ); expect(admittedVerification(evidence, [wrong, right])).toThrow( /unexpected observation type/u, ); }); it.each(["procedure", "pseudocode", "assembly", "basic_blocks"])( "rejects a function dossier missing %s", (field) => { const incomplete = jsonObjectSchema.parse(dossier); delete incomplete[field]; const wrong = source("1", { result: incomplete }); const evidence = comparison( { ...singular, left_evidence_id: wrong.evidence_id }, {}, [wrong, right], ); expect(admittedVerification(evidence, [wrong, right])).toThrow(); }, ); it.each([ { authority: "historical-reference" as const }, { confidence: "inferred" as const }, ])("keeps insufficient source authority unknown: %j", (observation) => { const weak = source("1", observation); const evidence = comparison( { ...singular, left_evidence_id: weak.evidence_id }, {}, [weak, right], ); expect(verify(evidence, [weak, right]).claims.items).toMatchObject([ { status: "unknown", observed_status: "unchanged", limitations: expect.arrayContaining([ "Source Evidence lacks qualifying shipped-artifact authority.", ]), }, ]); }); it("keeps a source without artifact identity unknown", () => { const unbound = source("1", {}, false); const evidence = comparison( { ...singular, left_evidence_id: unbound.evidence_id }, {}, [unbound, right], ); expect(verify(evidence, [unbound, right]).claims.items).toMatchObject([ { status: "unknown", limitations: expect.arrayContaining([ "Structural source artifact identity is unavailable.", ]), }, ]); }); it("rejects selected-dimension citations outside comparison closure", () => { const evidence = comparison(singular, { result: jsonValueSchema.parse({ ...baselineResult, dimensions: baselineResult.dimensions.map((dimension) => dimension.dimension === "pseudocode" ? { ...dimension, evidence_links: [left.evidence_id, extra.evidence_id], } : dimension, ), }), }); expect(admittedVerification(evidence, [left, right, extra])).toThrow( /dimension cites Evidence outside comparison closure/u, ); }); }); describe("function reconstruction evidence integrity", () => { it.each([ { operation: "compare_artifacts" }, { predicateType: "rea.artifact-comparison" }, { confidence: "observed" as const }, { authority: "shipped-artifact" as const }, ])("rejects changed comparison identity or authority %j", (observation) => { expect(admittedVerification(comparison(singular, observation))).toThrow( /identity or authority disagrees/u, ); }); it.each([ { ...provider, id: "other-comparison" }, { ...provider, name: "Other comparison" }, { ...provider, version: "2" }, ])("rejects comparison provider %j", (identity) => { expect( admittedVerification( comparison(singular, {}, [left, right], { provider: identity }), ), ).toThrow(/identity or authority disagrees/u); }); it("rejects an artifact-bound comparison", () => { const evidence = comparison(singular, {}, [left, right], { target: { path: "/fixture/comparison", sha256: "4".repeat(64), format: "file", }, }); expect(admittedVerification(evidence)).toThrow( /identity or authority disagrees/u, ); }); it("retains active source unknowns as a gating result", () => { const evidence = comparison(singular); const mutation = createEvidence(undefined, provider, { predicateType: "rea.residual-unknown-mutation", operation: "record_unknown", parameters: {}, result: { action: "record" }, confidence: "derived", authority: "analyst-inference", evidenceLinks: [left.evidence_id], }); const unknown = createResidualUnknown( { question: "Does the source analysis cover this function?", severity: "high", domain: "function-comparison", supporting_evidence_ids: [left.evidence_id], contradicting_evidence_ids: [], required_authority: "shipped-artifact", required_confidence: "observed", required_environment: null, recommended_probes: [], relationships: [], }, mutation.evidence_id, null, ); const result = verifyReconstruction( specification(evidence), createEvidenceBundle([left, right, evidence, mutation], [unknown]), ); expect(result.claims.items).toMatchObject([ { status: "unknown", observed_status: "unchanged", unknown_ids: [unknown.unknown_id], }, ]); }); it("separately rejects parameter tampering without a recomputed digest", () => { const evidence = comparison(singular); const tampered = { ...evidence, parameters: plural }; expect(() => verifyReconstruction( specification(evidence), createEvidenceBundle([left, right, tampered]), ), ).toThrow(/semantic identifier does not match/u); }); });