[GH-ISSUE #1844] [Bug] Partial artifact inventories count a changed root twice #621

Closed
opened 2026-10-15 18:26:41 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Bortlesboat on GitHub (Oct 11, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/1844

Area

compare_artifacts: partial artifact inventory Evidence and root summary counts.

Observed behavior

Comparing two partial inventories with different observed manifest root digests reports the single root path twice:

{
  "status": "truncated",
  "summary": { "unchanged": 0, "added": 0, "removed": 0, "changed": 1, "unknown": 1, "contradiction": 0 },
  "changes": [
    { "logical_path": ".", "classification": "unknown", "dimensions": ["availability"] },
    { "logical_path": ".", "classification": "changed", "dimensions": ["content"] }
  ]
}

Each manifest has occurrence_count: 1. Thus the same root is counted as both changed and unknown. The output above shows the affected fields; the complete rows also retain their occurrence/artifact IDs and Evidence citations.

Real partial-inventory reproducer

Source revision 0b4ce83618bb800627264ae6221e2e35140417b5, Windows x64, pinned Node 24.18.0 / npm 11.16.0. No native analysis engine is required.

The executed regression uses the existing filesystem inventory fixture and temporary-directory cleanup in tests/boundary/filesystem/artifactComparisonSummary.test.ts:

  1. Create two real files, left.txt containing before and right.txt containing after.
  2. Run inventoryArtifact(path) for each. Each inventory has one root occurrence, one node, and its actual file digest.
  3. Create derived inventory_artifact Evidence with the original subject, provider and manifest, retaining occurrences but replacing nodes with []. Use createEvidence; this recomputes each Evidence ID rather than editing an authenticated record in place.
  4. Run compareArtifacts(leftPartial, rightPartial).

The relevant Evidence construction is:

const inventory = await inventoryArtifact(path);
const partial = createEvidence(
  {
    path,
    sha256: inventory.manifest.root_sha256,
    format: inventory.manifest.root_format,
  },
  { id: "rea-artifact-graph", name: "REA artifact graph", version: "1" },
  {
    operation: "inventory_artifact",
    parameters: {},
    result: jsonValueSchema.parse({ ...inventory, nodes: [] }),
    confidence: "derived",
    authority: "shipped-artifact",
  },
);

With the added test and unmodified production code, this command executes the real filesystem boundary:

npm run test:focused -- tests/boundary/filesystem/artifactComparisonSummary.test.ts

Observed result: the node-omission regression fails on summary.unknown (1 received, 0 expected). The all-member-omission and existing balanced add/remove summary controls pass.

Expected behavior

One logical path has one change classification. The differing manifest digests establish a root content change even though node details are unavailable. Keep the overall truncated status and availability information, but report one root row and count it once:

{
  "status": "truncated",
  "summary": { "changed": 1, "unknown": 0 },
  "changes": [
    { "logical_path": ".", "classification": "changed", "dimensions": ["availability", "content"] }
  ]
}

Earliest failing stage and suggested scope

classifyAvailability emits the unknown root row. rootHashChange suppresses its fallback only when an existing root row already includes content, so it appends another row for this admitted partial inventory.

A narrow correction can combine the manifest content difference with an existing root row before summary aggregation. Preserve all dimensions, both citations, occurrence/artifact IDs, contradiction precedence, and the truncated status. Keep the fallback root row when partial pages omit occurrences entirely. No schema, tool catalog, provider, package or authority change appears necessary.

Two regression controls should remain:

  • Omit both nodes and occurrences: different manifest digests still produce one content row, changed: 1, and a truncated result.
  • Compare identical partial Evidence: do not infer a content change; retain changed: 0, unknown: 1, and the truncated result.

Verification and limits

The local two-file correction passes all three filesystem summary cases, including the new node-omission regression and both controls. An exact neighboring-test run passed seven tests; its existing 501-file case timed out at the unchanged 15-second deadline during host resource pressure. The full static gate remains unverified; no native-provider behavior is claimed. This report concerns the executed one-file comparison, which does not depend on that larger timeout case.

Prior work / duplication

Searched current open issues and PRs for artifact/root inventory comparison work. Merged #569 addresses a different unchanged formula for additions/removals. No matching report or active claim was found.

Originally created by @Bortlesboat on GitHub (Oct 11, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/1844 ### Area `compare_artifacts`: partial artifact inventory Evidence and root summary counts. ### Observed behavior Comparing two partial inventories with different observed manifest root digests reports the single root path twice: ```json { "status": "truncated", "summary": { "unchanged": 0, "added": 0, "removed": 0, "changed": 1, "unknown": 1, "contradiction": 0 }, "changes": [ { "logical_path": ".", "classification": "unknown", "dimensions": ["availability"] }, { "logical_path": ".", "classification": "changed", "dimensions": ["content"] } ] } ``` Each manifest has `occurrence_count: 1`. Thus the same root is counted as both changed and unknown. The output above shows the affected fields; the complete rows also retain their occurrence/artifact IDs and Evidence citations. ### Real partial-inventory reproducer Source revision `0b4ce83618bb800627264ae6221e2e35140417b5`, Windows x64, pinned Node 24.18.0 / npm 11.16.0. No native analysis engine is required. The executed regression uses the existing filesystem inventory fixture and temporary-directory cleanup in `tests/boundary/filesystem/artifactComparisonSummary.test.ts`: 1. Create two real files, `left.txt` containing `before` and `right.txt` containing `after`. 2. Run `inventoryArtifact(path)` for each. Each inventory has one root occurrence, one node, and its actual file digest. 3. Create derived `inventory_artifact` Evidence with the original subject, provider and manifest, retaining occurrences but replacing `nodes` with `[]`. Use `createEvidence`; this recomputes each Evidence ID rather than editing an authenticated record in place. 4. Run `compareArtifacts(leftPartial, rightPartial)`. The relevant Evidence construction is: ```ts const inventory = await inventoryArtifact(path); const partial = createEvidence( { path, sha256: inventory.manifest.root_sha256, format: inventory.manifest.root_format, }, { id: "rea-artifact-graph", name: "REA artifact graph", version: "1" }, { operation: "inventory_artifact", parameters: {}, result: jsonValueSchema.parse({ ...inventory, nodes: [] }), confidence: "derived", authority: "shipped-artifact", }, ); ``` With the added test and unmodified production code, this command executes the real filesystem boundary: ```sh npm run test:focused -- tests/boundary/filesystem/artifactComparisonSummary.test.ts ``` Observed result: the node-omission regression fails on `summary.unknown` (`1` received, `0` expected). The all-member-omission and existing balanced add/remove summary controls pass. ### Expected behavior One logical path has one change classification. The differing manifest digests establish a root content change even though node details are unavailable. Keep the overall `truncated` status and availability information, but report one root row and count it once: ```json { "status": "truncated", "summary": { "changed": 1, "unknown": 0 }, "changes": [ { "logical_path": ".", "classification": "changed", "dimensions": ["availability", "content"] } ] } ``` ### Earliest failing stage and suggested scope [`classifyAvailability`](https://github.com/morluto/rea/blob/0b4ce83618bb800627264ae6221e2e35140417b5/src/domain/artifactComparison.ts#L254) emits the unknown root row. [`rootHashChange`](https://github.com/morluto/rea/blob/0b4ce83618bb800627264ae6221e2e35140417b5/src/domain/artifactComparison.ts#L444) suppresses its fallback only when an existing root row already includes `content`, so it appends another row for this admitted partial inventory. A narrow correction can combine the manifest content difference with an existing root row before summary aggregation. Preserve all dimensions, both citations, occurrence/artifact IDs, contradiction precedence, and the truncated status. Keep the fallback root row when partial pages omit occurrences entirely. No schema, tool catalog, provider, package or authority change appears necessary. Two regression controls should remain: - Omit both nodes and occurrences: different manifest digests still produce one content row, `changed: 1`, and a truncated result. - Compare identical partial Evidence: do not infer a content change; retain `changed: 0`, `unknown: 1`, and the truncated result. ### Verification and limits The local two-file correction passes all three filesystem summary cases, including the new node-omission regression and both controls. An exact neighboring-test run passed seven tests; its existing 501-file case timed out at the unchanged 15-second deadline during host resource pressure. The full static gate remains unverified; no native-provider behavior is claimed. This report concerns the executed one-file comparison, which does not depend on that larger timeout case. ### Prior work / duplication Searched current open issues and PRs for artifact/root inventory comparison work. Merged #569 addresses a different `unchanged` formula for additions/removals. No matching report or active claim was found.
bsakaguchi 2026-10-15 18:26:41 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#621
No description provided.