From fead4fdaa7410ed2f7421fbce37e546c9cd6d7e4 Mon Sep 17 00:00:00 2001 From: Wenyu Chiou <162016108+WenyuChiou@users.noreply.github.com> Date: Sun, 25 Oct 2026 01:47:41 -0400 Subject: [PATCH] fix(artifacts): preserve integrity contradictions for one-sided members (#7076) --- src/domain/artifactComparison.ts | 14 +++--- tests/boundary/mcp/artifactMcp.test.ts | 61 +++++++++++++++++++++----- 2 files changed, 60 insertions(+), 15 deletions(-) diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index 55420b523f..d499c9f1a9 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -324,17 +324,20 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({ evidence_links: [...input.evidenceLinks], }); -const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => { +const classifyPresence = ( + input: ClassifyPathInput, + base: ReturnType, +): ArtifactChange | null => { const covered = input.path === "." ? input.rootCovered : input.entriesCovered; if (input.leftOccurrence === undefined) return { - ...buildArtifactChangeBase(input), + ...base, classification: covered ? "added" : "unknown", dimensions: [covered ? "content" : "availability"], }; if (input.rightOccurrence === undefined) return { - ...buildArtifactChangeBase(input), + ...base, classification: covered ? "removed" : "unknown", dimensions: [covered ? "content" : "availability"], }; @@ -372,11 +375,12 @@ const classifyAvailability = ( }; const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => { - const presence = classifyPresence(input); - if (presence !== null) return presence; const base = buildArtifactChangeBase(input); + // A declared/observed mismatch is known even without an opposite occurrence. const integrity = classifyIntegrity(input, base); if (integrity !== null) return integrity; + const presence = classifyPresence(input, base); + if (presence !== null) return presence; const declaration = classifyPyInstallerDeclaration(input, base); if (declaration.handled) return declaration.change; const availability = classifyAvailability(input, base); diff --git a/tests/boundary/mcp/artifactMcp.test.ts b/tests/boundary/mcp/artifactMcp.test.ts index 4930077f4d..deb282cee5 100644 --- a/tests/boundary/mcp/artifactMcp.test.ts +++ b/tests/boundary/mcp/artifactMcp.test.ts @@ -21,6 +21,7 @@ import { occurrenceIdForLocation, } from "../../../src/domain/artifactIdentity.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; +import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js"; import { ok as resultOk, err as resultErr, @@ -1258,6 +1259,9 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold const root = await createTestTempDirectory("rea-asar-attribution-"); const source = join(root, "source"); await mkdir(source); + await writeFile(join(source, "keep.txt"), "unchanged sibling\n"); + const absentArchive = join(root, "absent.asar"); + await createPackageWithOptions(source, absentArchive, {}); const original = "console.log('ok');\n"; const changed = "console.log('no');\n"; await writeFile(join(source, "main.js"), original); @@ -1280,7 +1284,12 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold await server.connect(st); await client.connect(ct); const observations = []; - for (const path of [originalArchive, staleArchive, currentArchive]) { + for (const path of [ + originalArchive, + staleArchive, + currentArchive, + absentArchive, + ]) { expect( (await client.callTool({ name: "open_binary", arguments: { path } })) .isError, @@ -1298,20 +1307,32 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold PROCESS_COMPARISON_EVIDENCE, ]) expect(session.recordEvidence(record).ok).toBe(true); - const [originalEvidence, stale, current] = observations; + const [originalEvidence, stale, current, absent] = observations; if ( originalEvidence === undefined || stale === undefined || - current === undefined + current === undefined || + absent === undefined ) - throw new Error("Expected three artifact observations"); - for (const [left, right, mismatches] of [ - [stale, current, [stale.evidence_id]], - [current, stale, [stale.evidence_id]], - [stale, stale, [stale.evidence_id]], - [originalEvidence, current, []], + throw new Error("Expected four artifact observations"); + for (const [left, right, mismatches, presence] of [ + [stale, current, [stale.evidence_id], "changed"], + [current, stale, [stale.evidence_id], "changed"], + [stale, stale, [stale.evidence_id], "changed"], + [originalEvidence, current, [], "changed"], + [absent, stale, [stale.evidence_id], "added"], + [stale, absent, [stale.evidence_id], "removed"], + [absent, current, [], "added"], + [current, absent, [], "removed"], ] as const) - await assertIntegrityComparison(client, session, left, right, mismatches); + await assertIntegrityComparison( + client, + session, + left, + right, + mismatches, + presence, + ); } finally { await Promise.allSettled([client.close(), server.close(), session.close()]); } @@ -1323,6 +1344,7 @@ const assertIntegrityComparison = async ( left: ReturnType, right: ReturnType, mismatches: readonly string[], + presence: "added" | "removed" | "changed", ) => { const compared = await client.callTool({ name: "compare_artifacts", @@ -1332,12 +1354,31 @@ const assertIntegrityComparison = async ( const comparison = toolEvidence(compared.structuredContent); expect(comparison.normalized_result).toMatchObject({ status: mismatches.length === 0 ? "changed" : "contradiction", + summary: { contradiction: mismatches.length === 0 ? 0 : 1 }, integrity_mismatch_evidence_ids: mismatches, }); const unknown = session .listUnknowns({ domain: "artifact-comparison" }) .find((item) => item.question.includes(comparison.evidence_id)); expect(unknown).toBeDefined(); + const [leftMember, rightMember] = [left, right].map((record) => + parseArtifactInventoryEvidence(record).inventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js", + ), + ); + expect(comparison.normalized_result).toMatchObject({ + changes: expect.arrayContaining([ + expect.objectContaining({ + logical_path: "main.js", + classification: mismatches.length > 0 ? "contradiction" : presence, + ...(mismatches.length > 0 ? { dimensions: ["integrity"] } : {}), + left_occurrence_id: leftMember?.occurrence_id ?? null, + left_artifact_id: leftMember?.artifact_id ?? null, + right_occurrence_id: rightMember?.occurrence_id ?? null, + right_artifact_id: rightMember?.artifact_id ?? null, + }), + ]), + }); if (mismatches.length > 0) { expect(unknown?.contradicting_evidence_ids).toEqual(mismatches); expect(unknown?.supporting_evidence_ids).toEqual(