diff --git a/src/domain/artifactComparison.ts b/src/domain/artifactComparison.ts index a9f69be471..b3542d7f98 100644 --- a/src/domain/artifactComparison.ts +++ b/src/domain/artifactComparison.ts @@ -538,6 +538,11 @@ const relationsByPath = ( ? null : (occurrences.get(occurrence.parent_occurrence_id)?.logical_path ?? null), + // Containment roots are path-normalized to avoid graph-wide deltas, but + // a source map's target is a semantic content identity, not its parent + // directory. Preserve that identity in the relation comparison. + mapped_artifact_id: + edge.relation === "maps-source" ? edge.parent_artifact_id : null, child_artifact_id: occurrence.artifact_id, relation: edge.relation, logical_path: occurrence.logical_path, @@ -556,6 +561,8 @@ const relationsByPath = ( interface RelationProjection { /** Logical parent is stable across graph-root ID changes. */ readonly parent_logical_path: string | null; + /** Content identity is material for a maps-source edge. */ + readonly mapped_artifact_id: string | null; readonly child_artifact_id: string; readonly relation: ArtifactInventoryResult["edges"][number]["relation"]; readonly logical_path: string | null; diff --git a/tests/boundary/filesystem/artifactComparison.test.ts b/tests/boundary/filesystem/artifactComparison.test.ts index a34cb46758..4b9b07321f 100644 --- a/tests/boundary/filesystem/artifactComparison.test.ts +++ b/tests/boundary/filesystem/artifactComparison.test.ts @@ -571,6 +571,49 @@ describe("artifact comparison completeness", () => { }); describe("artifact source-map relationship coverage", () => { + it("reports when an unchanged map points to changed source content", async () => { + const root = await createTestTempDirectory("rea-map-target-change-"); + const leftPath = join(root, "left"); + const rightPath = join(root, "right"); + await Promise.all([mkdir(leftPath), mkdir(rightPath)]); + await Promise.all([ + writeFile(join(leftPath, "main.js"), "/* first source */\n"), + writeFile(join(rightPath, "main.js"), "/* second source */\n"), + writeFile( + join(leftPath, "main.js.map"), + '{"version":3,"sources":[],"names":[],"mappings":""}', + ), + writeFile( + join(rightPath, "main.js.map"), + '{"version":3,"sources":[],"names":[],"mappings":""}', + ), + ]); + const [left, right] = await Promise.all([ + observe(leftPath), + observe(rightPath), + ]); + + const comparison = compareArtifacts(left, right); + const leftInventory = parseArtifactInventoryEvidence(left).inventory; + const rightInventory = parseArtifactInventoryEvidence(right).inventory; + const leftMap = leftInventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js.map", + ); + const rightMap = rightInventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js.map", + ); + expect(leftMap?.artifact_id).toBe(rightMap?.artifact_id); + expect(comparison.changes).toContainEqual( + expect.objectContaining({ + logical_path: "main.js.map", + classification: "changed", + dimensions: ["relations"], + left_artifact_id: leftMap?.artifact_id, + right_artifact_id: rightMap?.artifact_id, + }), + ); + }); + it("preserves unknown source-map relationships for duplicate ZIP paths", async () => { const root = await createTestTempDirectory("rea-duplicate-source-map-"); const path = join(root, "duplicate-source-map.zip");