From 77be87db992cfbc54b1f554b506962f15fabcfee Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 15:12:57 +0800 Subject: [PATCH] fix(artifacts): describe observed inventory scope accurately --- docs/mcp-contracts.md | 13 ++++--- src/artifacts/inventory/reader.ts | 4 +- .../artifactComparisonCoverage.test.ts | 6 +-- .../artifactComparisonPlist.test.ts | 8 +++- .../artifactInventoryRegularRoot.test.ts | 39 +++++++++++++++++++ 5 files changed, 59 insertions(+), 11 deletions(-) diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index 734e908f27..a915bb066c 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -68,11 +68,14 @@ 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 -traversal coverage before concluding equality. A fully observed root without -container children can be `unchanged`; its scope notice stays in `limitations` -without creating a gap. Content and executable metadata changes still produce -root deltas. Missing graph members yield `truncated`; unknown traversal coverage -or unavailable occurrence hashes retain uncertainty. Integrity mismatches remain +traversal coverage before concluding equality. Fully observed roots can be +`unchanged` within root scope; their scope notice stays in `limitations` without +creating a gap. Root-scoped equality establishes equality of the observed root +bytes. Child-container presence and unexamined format details remain unresolved, +including when the root format is `unknown`. Content and executable metadata +changes still produce root deltas. Missing graph members yield `truncated`; +unknown traversal coverage or unavailable occurrence hashes retain uncertainty. +Integrity mismatches remain contradictions. Static artifact equality does not establish unchanged runtime behavior. Source-map relation deltas include the content identity of the mapped source, diff --git a/src/artifacts/inventory/reader.ts b/src/artifacts/inventory/reader.ts index c483848676..6e17e2ade5 100644 --- a/src/artifacts/inventory/reader.ts +++ b/src/artifacts/inventory/reader.ts @@ -106,6 +106,8 @@ export const inventoryCoverage = ( }; return { coverage: { scope: "root", status: "complete" }, - limitations: ["Artifact has no child container entries."], + limitations: [ + "Inventory covers root bytes only; child container entries were not examined.", + ], }; }; diff --git a/tests/acceptance/applications/artifactComparisonCoverage.test.ts b/tests/acceptance/applications/artifactComparisonCoverage.test.ts index 42ab385205..94c0ceab1b 100644 --- a/tests/acceptance/applications/artifactComparisonCoverage.test.ts +++ b/tests/acceptance/applications/artifactComparisonCoverage.test.ts @@ -174,7 +174,7 @@ cliTest( edge_count: 0, }); expect(inventory.limitations).toEqual([ - "Artifact has no child container entries.", + "Inventory covers root bytes only; child container entries were not examined.", ]); const equal = await compare(left, right); expect(equal.normalized_result).toMatchObject({ @@ -182,8 +182,8 @@ cliTest( summary: { unchanged: 1, unknown: 0 }, changes: [], limitations: [ - "Left: Artifact has no child container entries.", - "Right: Artifact has no child container entries.", + "Left: Inventory covers root bytes only; child container entries were not examined.", + "Right: Inventory covers root bytes only; child container entries were not examined.", ], }); const behavior = await call("find_changed_behavior", { diff --git a/tests/acceptance/artifactComparisonPlist.test.ts b/tests/acceptance/artifactComparisonPlist.test.ts index 2559d0b473..872b1e3fc1 100644 --- a/tests/acceptance/artifactComparisonPlist.test.ts +++ b/tests/acceptance/artifactComparisonPlist.test.ts @@ -36,7 +36,9 @@ it("compares identical complete plist inventories across CLI and MCP and creates throw new Error("CLI inspection did not retain inventory Evidence"); expect(cliInventory.normalized_result).toMatchObject({ coverage: { scope: "root", status: "complete" }, - limitations: ["Artifact has no child container entries."], + limitations: [ + "Inventory covers root bytes only; child container entries were not examined.", + ], }); const { call } = await connectLocalToolsMcp(); @@ -53,7 +55,9 @@ it("compares identical complete plist inventories across CLI and MCP and creates throw new Error("MCP inspection did not retain inventory Evidence"); expect(mcpInventory.normalized_result).toMatchObject({ coverage: { scope: "root", status: "complete" }, - limitations: ["Artifact has no child container entries."], + limitations: [ + "Inventory covers root bytes only; child container entries were not examined.", + ], }); const compared = await call("compare_artifacts", { diff --git a/tests/boundary/filesystem/artifactInventoryRegularRoot.test.ts b/tests/boundary/filesystem/artifactInventoryRegularRoot.test.ts index 08bc19230d..a11b3b9289 100644 --- a/tests/boundary/filesystem/artifactInventoryRegularRoot.test.ts +++ b/tests/boundary/filesystem/artifactInventoryRegularRoot.test.ts @@ -20,6 +20,11 @@ import { asarHeaderBytes } from "../../fixtures/asarHeader.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { readWithoutFifoWriter } from "../../fixtures/fifoInput.js"; import { DEFAULT_ASAR_MAX_HEADER_BYTES } from "../../../src/config/asarHeaderAdmission.js"; +import { + CPU, + arArchive, + fatImage, +} from "../../../src/artifacts/apple/MachoImage.fixture.js"; it.skipIf(process.platform === "win32")( "rejects a FIFO artifact root without waiting for a writer", @@ -81,6 +86,40 @@ it("keeps unavailable container traversal distinct from observed root bytes", as }); }); +it("retains root identity without asserting absent children for an unclassified FAT archive", async () => { + const root = await createTestTempDirectory("rea-fat-archive-root-scope-"); + const path = join(root, "libfat.a"); + const archive = arArchive(); + const bytes = fatImage([ + { cpu: CPU.x86_64, bytes: archive }, + { cpu: CPU.arm64, bytes: archive }, + ]); + await writeFile(path, bytes); + + const inventory = await inventoryArtifact(path); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + expect(inventory.manifest).toMatchObject({ + root_format: "unknown", + root_sha256: sha256, + node_count: 1, + occurrence_count: 1, + edge_count: 0, + }); + expect(inventory.nodes).toEqual([ + expect.objectContaining({ format: "unknown", sha256, size: bytes.length }), + ]); + expect(inventory.occurrences).toEqual([ + expect.objectContaining({ + artifact_format: "unknown", + hash_status: "verified", + }), + ]); + expect(inventory.coverage).toEqual({ scope: "root", status: "complete" }); + expect(inventory.limitations).toEqual([ + "Inventory covers root bytes only; child container entries were not examined.", + ]); +}); + it("admits a FAT root when a valid slice header starts at the prefix boundary", async () => { const root = await createTestTempDirectory("rea-fat-root-header-boundary-"); const path = join(root, "universal");