diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index c85f790e8c..e11910c896 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -47,6 +47,11 @@ derived metadata keep their upstream representation. Read function results from `normalized_result`; older retained Evidence keeps its original representation and identity. +Artifact source-map filename relationships require a unique, identified regular +file at the sibling path. A known directory cannot be the mapped file; the map +retains its actual container relation. This admission does not establish source +byte correspondence or decode the map's original source paths. + Artifact inventories declare `coverage.scope` (`root` or `container-entries`) and `coverage.status` (`complete` or `unknown`) independently of diagnostic wording. Comparison requires matching scopes, complete graph members and complete diff --git a/src/artifacts/inventory/ArtifactGraphConstruction.ts b/src/artifacts/inventory/ArtifactGraphConstruction.ts index 354299b502..c74368b13f 100644 --- a/src/artifacts/inventory/ArtifactGraphConstruction.ts +++ b/src/artifacts/inventory/ArtifactGraphConstruction.ts @@ -332,7 +332,9 @@ export const createArtifactEdges = ( const sourceCandidates = sourcePath === undefined ? [] : (byPath.get(sourcePath) ?? []); const mappedSource = - sourceCandidates.length === 1 && sourceCandidates[0]?.artifact_id !== null + sourceCandidates.length === 1 && + sourceCandidates[0]?.entry_kind === "file" && + sourceCandidates[0].artifact_id !== null ? sourceCandidates[0] : undefined; if (sourcePath !== undefined) { @@ -342,7 +344,9 @@ export const createArtifactEdges = ( occurrence: `Source-map target is unknown because multiple occurrences share ${JSON.stringify(sourcePath)}.`, coverage: `Source-map relationships are unknown because multiple occurrences share ${JSON.stringify(sourcePath)}.`, } - : sourceCandidates.length === 1 && mappedSource === undefined + : sourceCandidates.length === 1 && + sourceCandidates[0]?.entry_kind === "file" && + mappedSource === undefined ? { occurrence: `Source-map target identity is unknown because the matching occurrence at ${JSON.stringify(sourcePath)} has no content identity.`, coverage: `Source-map relationships are unknown because the matching occurrence at ${JSON.stringify(sourcePath)} has no content identity.`, diff --git a/tests/boundary/filesystem/artifactSourceMapTargets.test.ts b/tests/boundary/filesystem/artifactSourceMapTargets.test.ts new file mode 100644 index 0000000000..6327ce9529 --- /dev/null +++ b/tests/boundary/filesystem/artifactSourceMapTargets.test.ts @@ -0,0 +1,96 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { TextReader, Uint8ArrayWriter, ZipWriter } from "@zip.js/zip.js"; +import { expect, it } from "vitest"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; +import { inventoryArtifact } from "../../fixtures/artifactInventory.js"; + +it.each([ + "directory", + "directory-with-child", + "file", + "directory-map", + "missing", +])( + "admits only a regular mapped file while retaining ZIP containment (%s)", + async (target) => { + const root = await createTestTempDirectory("rea-map-target-kind-"); + const path = join(root, "sample.zip"); + const writer = new ZipWriter(new Uint8ArrayWriter()); + if (target.startsWith("directory") && target !== "directory-map") + await writer.add("main.js/", undefined, { directory: true }); + if (target === "directory-with-child") + await writer.add("main.js/child.txt", new TextReader("child")); + if (target === "file" || target === "directory-map") + await writer.add("main.js", new TextReader("export const answer = 42;")); + if (target === "directory-map") + await writer.add("main.js.map/", undefined, { directory: true }); + else + await writer.add( + "main.js.map", + new TextReader( + JSON.stringify({ + version: 3, + file: "main.js", + sources: ["input.ts"], + sourcesContent: ["export const answer = 42;"], + names: [], + mappings: "AAAA", + }), + ), + ); + await writeFile(path, await writer.close()); + const inventory = await inventoryArtifact(path); + const map = inventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js.map", + ); + const main = inventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js", + ); + const carrier = inventory.occurrences.find( + ({ logical_path }) => logical_path === ".", + ); + if (map === undefined || carrier === undefined) + throw new Error("Expected map and carrier"); + expect(inventory.coverage.status).toBe("complete"); + expect(map.parent_occurrence_id).toBe(carrier.occurrence_id); + expect( + inventory.edges.find( + ({ occurrence_id }) => occurrence_id === map.occurrence_id, + ), + ).toMatchObject({ + relation: target === "file" ? "maps-source" : "contains", + parent_artifact_id: + target === "file" ? main?.artifact_id : carrier.artifact_id, + }); + expect(map.limitations).toEqual([]); + if (target.startsWith("directory") && target !== "directory-map") { + expect(main).toMatchObject({ + entry_kind: "directory", + artifact_kind: "container", + }); + expect( + inventory.edges.find( + ({ occurrence_id }) => occurrence_id === main?.occurrence_id, + ), + ).toMatchObject({ + relation: "contains", + parent_artifact_id: carrier.artifact_id, + }); + } + if (target === "directory-with-child") { + const child = inventory.occurrences.find( + ({ logical_path }) => logical_path === "main.js/child.txt", + ); + expect(child?.parent_occurrence_id).toBe(main?.occurrence_id); + expect( + inventory.edges.find( + ({ occurrence_id }) => occurrence_id === child?.occurrence_id, + ), + ).toMatchObject({ + relation: "contains", + parent_artifact_id: main?.artifact_id, + }); + } + }, +);