[GH-ISSUE #3191] [Bug] Source-to-bundle comparison reports an unchanged JSON source file as removed #1130

Closed
opened 2026-10-15 18:31:20 +00:00 by bsakaguchi · 2 comments
Owner

Originally created by @coygeek on GitHub (Oct 14, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/3191

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

compare-source-to-bundle reports src/data.json as removed with high confidence when a supplied complete historical inventory and a complete application graph contain its exact bytes at the same path. The application graph retains a content-digest artifact node for the file, but the comparison returns current_node_ids: [] and candidates: [].

The public CLI exits successfully and declares complete-within-inputs comparison coverage. It reports one unchanged JavaScript source and one removed JSON source. The matching JSON artifact is also absent from unmapped_current_node_ids. The analyzer returns normal successful Evidence with complete graph coverage, no truncation, zero omitted nodes and no coverage limits.

Reproduction

Start from a clean checkout at 58300885207133422af235e0f2b10658cf958b03 and run these commands at its root with the repository's pinned npm toolchain. npm ci and npm run build:cached are clean-checkout prerequisites. The verifier used prepared dependencies and the compiled baseline runtime; it did not rerun installation or build.

  1. Create a disposable fixture and an isolated home, then install and build:
mkdir -p repro/home/config repro/home/cache repro/home/data repro/reference/src repro/application/src
run_repro() {
  env HOME="$PWD/repro/home"     XDG_CONFIG_HOME="$PWD/repro/home/config"     XDG_CACHE_HOME="$PWD/repro/home/cache"     XDG_DATA_HOME="$PWD/repro/home/data" "$@"
}
run_repro npm ci
run_repro npm run build:cached
cat > repro/reference/package.json <<'EOF'
{"name":"source-comparison-fixture","version":"1.0.0","main":"src/main.js"}
EOF
cat > repro/reference/src/main.js <<'EOF'
module.exports = 7;
EOF
cat > repro/reference/src/data.json <<'EOF'
{"label":"retained"}
EOF
cp repro/reference/package.json repro/application/package.json
cp repro/reference/src/main.js repro/application/src/main.js
cp repro/reference/src/data.json repro/application/src/data.json
  1. Import the reference and analyze the identical application through the public CLI:
run_repro node scripts/rea.mjs import-reference-source "$PWD/repro/reference" --json > repro/reference.json
run_repro node scripts/rea.mjs analyze-javascript-application "$PWD/repro/application" --json > repro/application.json
  1. Build a complete historical fixture and verify the application's exact evidence before comparison. The importer returns a partial graph because of its general pathname-race limitation. This step explicitly supplies a synthetic complete inventory for the fixed fixture; it does not claim that the importer returned complete coverage. It preserves the imported entries and classifications and uses the graph creator to validate and commit the new inventory.
run_repro node --input-type=module <<'JS'
import fs from 'node:fs';
import assert from 'node:assert/strict';
import { createHistoricalSourceGraph } from './dist/domain/referenceSourceGraph.js';
const imported = JSON.parse(fs.readFileSync('repro/reference.json', 'utf8'));
const application = JSON.parse(fs.readFileSync('repro/application.json', 'utf8'));
const entry = imported.entries.find(e => e.path === 'src/data.json');
assert(entry.classifications.includes('source'));
assert.deepEqual(fs.readFileSync('repro/reference/src/data.json'),
                 fs.readFileSync('repro/application/src/data.json'));
const graph = application.normalized_result.graph;
assert.equal(graph.coverage.status, 'complete');
assert.equal(graph.coverage.truncated, false);
assert.equal(graph.coverage.omitted_count, 0);
const node = graph.nodes.find(n => n.kind === 'artifact' &&
  n.identity.strategy === 'content-digest' && n.identity.sha256 === entry.sha256 &&
  n.observations.some(o => o.properties.path === entry.path));
assert(node);
assert.equal(imported.exclusions.length, 0);
assert.equal(imported.limitations.length, 1);
assert(imported.limitations[0].includes('syscall-boundary pathname race'));
const { root_sha256, ...body } = imported;
const reference = createHistoricalSourceGraph({
  ...body, inventory_state: 'complete', limitations: []
});
fs.writeFileSync('repro/comparison-input.json', JSON.stringify({ reference, application }));
fs.writeFileSync('repro/partial-input.json', JSON.stringify({ reference: imported, application }));
console.log(JSON.stringify({ path: entry.path, digest: entry.sha256,
  currentKind: node.kind, currentNode: node.node_id, coverage: graph.coverage }));
JS
  1. Run the comparison and inspect the source item:
run_repro node scripts/rea.mjs compare-source-to-bundle repro/comparison-input.json --json > repro/comparison.json
run_repro node --input-type=module <<'JS'
import fs from 'node:fs';
const result = JSON.parse(fs.readFileSync('repro/comparison.json', 'utf8')).normalized_result;
console.log(JSON.stringify({ summary: result.summary, coverage: result.coverage,
  item: result.items.find(i => i.source_path === 'src/data.json') }, null, 2));
JS
  1. As a coverage control, compare the unmodified partial import:
run_repro node scripts/rea.mjs compare-source-to-bundle repro/partial-input.json --json

The complete case returns removed with high confidence for the JSON file. The partial case returns unknown with no candidates for that same file.

Expected behavior

docs/javascript-application-workflows.md:164-183 says exact digest takes precedence over location inference, each historical source file receives a classification, and the comparison returns all source files, candidate nodes and matching signals represented in the supplied graphs. src/contracts/applicationToolContracts.ts:144-149 also describes the exact-digest and current-path signals. Neither contract restricts historical source to JavaScript extensions.

The importer classifies this JSON file as both config and source, and the application graph retains its exact digest and exact path. The comparison should return the exact candidate and classify this single unchanged file as unchanged.

Impact and scope

Callers comparing a supplied complete historical inventory can receive a false high-confidence source-removal result for an existing JSON source file. This can mislead source-retention analysis. No file deletion, live application failure or security boundary crossing was observed.

The ordinary importer returns a partial historical inventory for this fixture because of its portable pathname-race limitation. Comparing that unmodified import returns unknown with no candidates for the JSON file. The false high-confidence removal requires the explicitly constructed, schema-valid complete historical fixture in step 3. Only JSON was reproduced; this report does not claim executed coverage for other non-JavaScript file types.

Keeping the reference partial avoids the false absence assertion but still loses the available exact JSON mapping. No practical workaround preserving the complete comparison result was verified.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node v26.10.0; macOS arm64. The verifier used the prepared baseline dependencies and compiled runtime with isolated home and XDG directories. No external analysis engine or live network service was used.

Target and artifact context

Two inert local application directories contain identical package.json, src/main.js and src/data.json files. The JSON bytes are {"label":"retained"} followed by a newline. The comparison consumes a cryptographically committed HistoricalSourceGraph and the analyzer's unmodified authenticated application Evidence. The complete historical inventory is a synthetic fixture built with the exported graph creator from the imported entries; it is not a claim that the importer returned complete coverage.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The following excerpt contains the verified matching premises and selected comparison fields, rather than a full Evidence record:

Reference path: src/data.json
Reference classifications: ["config", "source"]
Reference and current SHA-256: ceb228df669be15e263bb14794107984281c77d9866f2c0d1d12184d3296fb33
Current node kind: artifact
Current observation path: src/data.json
Application graph coverage: complete; truncated false; omitted_count 0; limits []
Comparison coverage: complete-within-inputs
Summary: unchanged 1; removed 1; all other counts 0
JSON item: status "removed"; confidence "high"; current_node_ids []; candidates []
Limitation: No retained current node matched exact digest or path signals within complete static inventories.
Partial-reference control: JSON item "unknown"; candidates []
Actually absent JSON control: JSON item "removed"; confidence "high"; candidates []
JavaScript control: "unchanged"; confidence "exact"; exact-source-digest signal

Typed outcome or recovery expected

For the present-file input, return the retained exact JSON artifact candidate and classify the historical source as unchanged. Keep the exact JavaScript match. When the application JSON file is actually absent and both inventories are complete, continue to return removed. Partial inventories must continue to avoid unsupported absence assertions.

Deleting the historical source entry or returning unknown without its exact candidate would discard available evidence and would not restore the documented contract.

Analysis or suspected cause

Source-derived reasoning at the reported baseline explains the verified result. src/domain/javascript/sourceToBundleSignals.ts:47-51 and :71-78 admit current nodes only when their kinds are javascript-asset, javascript-module or source-module. Its :81-89 includes every historical file classified as source. src/application/javascript/JavaScriptArtifactGraphContext.ts:287-297 assigns the JSON file the artifact kind.

src/domain/javascript/sourceToBundleComparison.ts:96-103 filters the current graph through sourceBearingNodes before building the candidate index. The exact JSON node therefore does not reach digest or path matching. The empty candidate set takes the complete-inventory removed branch at :268-276.

The public command routes through src/cli/applicationCommands.ts:68-78 and application Evidence authentication at src/application/javascript/JavaScriptApplicationWorkflowService.ts:186-208. The complete fixture creator validates inventory premises and its cryptographic commitment at src/domain/referenceSourceGraph.ts:396-427 and :543-551; the public comparison accepted that fixture and the analyzer's unmodified Evidence.

Additional context

P2 is supported by a bounded analytical result error for JSON source entries with supplied complete inventories. The result can mislead source-retention analysis, while the observed run deletes no bytes and affects no live application. The common unmodified importer workflow returns unknown rather than high-confidence removal.

Related: #3188 reports a separate failure in the same area; the two have independent fixes.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3191 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `compare-source-to-bundle` reports `src/data.json` as `removed` with high confidence when a supplied complete historical inventory and a complete application graph contain its exact bytes at the same path. The application graph retains a content-digest `artifact` node for the file, but the comparison returns `current_node_ids: []` and `candidates: []`. The public CLI exits successfully and declares `complete-within-inputs` comparison coverage. It reports one unchanged JavaScript source and one removed JSON source. The matching JSON artifact is also absent from `unmapped_current_node_ids`. The analyzer returns normal successful Evidence with complete graph coverage, no truncation, zero omitted nodes and no coverage limits. ### Reproduction Start from a clean checkout at `58300885207133422af235e0f2b10658cf958b03` and run these commands at its root with the repository's pinned npm toolchain. `npm ci` and `npm run build:cached` are clean-checkout prerequisites. The verifier used prepared dependencies and the compiled baseline runtime; it did not rerun installation or build. 1. Create a disposable fixture and an isolated home, then install and build: ```sh mkdir -p repro/home/config repro/home/cache repro/home/data repro/reference/src repro/application/src run_repro() { env HOME="$PWD/repro/home" XDG_CONFIG_HOME="$PWD/repro/home/config" XDG_CACHE_HOME="$PWD/repro/home/cache" XDG_DATA_HOME="$PWD/repro/home/data" "$@" } run_repro npm ci run_repro npm run build:cached cat > repro/reference/package.json <<'EOF' {"name":"source-comparison-fixture","version":"1.0.0","main":"src/main.js"} EOF cat > repro/reference/src/main.js <<'EOF' module.exports = 7; EOF cat > repro/reference/src/data.json <<'EOF' {"label":"retained"} EOF cp repro/reference/package.json repro/application/package.json cp repro/reference/src/main.js repro/application/src/main.js cp repro/reference/src/data.json repro/application/src/data.json ``` 2. Import the reference and analyze the identical application through the public CLI: ```sh run_repro node scripts/rea.mjs import-reference-source "$PWD/repro/reference" --json > repro/reference.json run_repro node scripts/rea.mjs analyze-javascript-application "$PWD/repro/application" --json > repro/application.json ``` 3. Build a complete historical fixture and verify the application's exact evidence before comparison. The importer returns a partial graph because of its general pathname-race limitation. This step explicitly supplies a synthetic complete inventory for the fixed fixture; it does not claim that the importer returned complete coverage. It preserves the imported entries and classifications and uses the graph creator to validate and commit the new inventory. ```sh run_repro node --input-type=module <<'JS' import fs from 'node:fs'; import assert from 'node:assert/strict'; import { createHistoricalSourceGraph } from './dist/domain/referenceSourceGraph.js'; const imported = JSON.parse(fs.readFileSync('repro/reference.json', 'utf8')); const application = JSON.parse(fs.readFileSync('repro/application.json', 'utf8')); const entry = imported.entries.find(e => e.path === 'src/data.json'); assert(entry.classifications.includes('source')); assert.deepEqual(fs.readFileSync('repro/reference/src/data.json'), fs.readFileSync('repro/application/src/data.json')); const graph = application.normalized_result.graph; assert.equal(graph.coverage.status, 'complete'); assert.equal(graph.coverage.truncated, false); assert.equal(graph.coverage.omitted_count, 0); const node = graph.nodes.find(n => n.kind === 'artifact' && n.identity.strategy === 'content-digest' && n.identity.sha256 === entry.sha256 && n.observations.some(o => o.properties.path === entry.path)); assert(node); assert.equal(imported.exclusions.length, 0); assert.equal(imported.limitations.length, 1); assert(imported.limitations[0].includes('syscall-boundary pathname race')); const { root_sha256, ...body } = imported; const reference = createHistoricalSourceGraph({ ...body, inventory_state: 'complete', limitations: [] }); fs.writeFileSync('repro/comparison-input.json', JSON.stringify({ reference, application })); fs.writeFileSync('repro/partial-input.json', JSON.stringify({ reference: imported, application })); console.log(JSON.stringify({ path: entry.path, digest: entry.sha256, currentKind: node.kind, currentNode: node.node_id, coverage: graph.coverage })); JS ``` 4. Run the comparison and inspect the source item: ```sh run_repro node scripts/rea.mjs compare-source-to-bundle repro/comparison-input.json --json > repro/comparison.json run_repro node --input-type=module <<'JS' import fs from 'node:fs'; const result = JSON.parse(fs.readFileSync('repro/comparison.json', 'utf8')).normalized_result; console.log(JSON.stringify({ summary: result.summary, coverage: result.coverage, item: result.items.find(i => i.source_path === 'src/data.json') }, null, 2)); JS ``` 5. As a coverage control, compare the unmodified partial import: ```sh run_repro node scripts/rea.mjs compare-source-to-bundle repro/partial-input.json --json ``` The complete case returns `removed` with high confidence for the JSON file. The partial case returns `unknown` with no candidates for that same file. ### Expected behavior `docs/javascript-application-workflows.md:164-183` says exact digest takes precedence over location inference, each historical source file receives a classification, and the comparison returns all source files, candidate nodes and matching signals represented in the supplied graphs. `src/contracts/applicationToolContracts.ts:144-149` also describes the exact-digest and current-path signals. Neither contract restricts historical source to JavaScript extensions. The importer classifies this JSON file as both `config` and `source`, and the application graph retains its exact digest and exact path. The comparison should return the exact candidate and classify this single unchanged file as `unchanged`. ### Impact and scope Callers comparing a supplied complete historical inventory can receive a false high-confidence source-removal result for an existing JSON source file. This can mislead source-retention analysis. No file deletion, live application failure or security boundary crossing was observed. The ordinary importer returns a partial historical inventory for this fixture because of its portable pathname-race limitation. Comparing that unmodified import returns `unknown` with no candidates for the JSON file. The false high-confidence removal requires the explicitly constructed, schema-valid complete historical fixture in step 3. Only JSON was reproduced; this report does not claim executed coverage for other non-JavaScript file types. Keeping the reference partial avoids the false absence assertion but still loses the available exact JSON mapping. No practical workaround preserving the complete comparison result was verified. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node v26.10.0; macOS arm64. The verifier used the prepared baseline dependencies and compiled runtime with isolated home and XDG directories. No external analysis engine or live network service was used. ### Target and artifact context Two inert local application directories contain identical `package.json`, `src/main.js` and `src/data.json` files. The JSON bytes are `{"label":"retained"}` followed by a newline. The comparison consumes a cryptographically committed `HistoricalSourceGraph` and the analyzer's unmodified authenticated application Evidence. The complete historical inventory is a synthetic fixture built with the exported graph creator from the imported entries; it is not a claim that the importer returned complete coverage. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence The following excerpt contains the verified matching premises and selected comparison fields, rather than a full Evidence record: ```text Reference path: src/data.json Reference classifications: ["config", "source"] Reference and current SHA-256: ceb228df669be15e263bb14794107984281c77d9866f2c0d1d12184d3296fb33 Current node kind: artifact Current observation path: src/data.json Application graph coverage: complete; truncated false; omitted_count 0; limits [] Comparison coverage: complete-within-inputs Summary: unchanged 1; removed 1; all other counts 0 JSON item: status "removed"; confidence "high"; current_node_ids []; candidates [] Limitation: No retained current node matched exact digest or path signals within complete static inventories. Partial-reference control: JSON item "unknown"; candidates [] Actually absent JSON control: JSON item "removed"; confidence "high"; candidates [] JavaScript control: "unchanged"; confidence "exact"; exact-source-digest signal ``` ### Typed outcome or recovery expected For the present-file input, return the retained exact JSON artifact candidate and classify the historical source as `unchanged`. Keep the exact JavaScript match. When the application JSON file is actually absent and both inventories are complete, continue to return `removed`. Partial inventories must continue to avoid unsupported absence assertions. Deleting the historical source entry or returning `unknown` without its exact candidate would discard available evidence and would not restore the documented contract. ### Analysis or suspected cause Source-derived reasoning at the reported baseline explains the verified result. `src/domain/javascript/sourceToBundleSignals.ts:47-51` and `:71-78` admit current nodes only when their kinds are `javascript-asset`, `javascript-module` or `source-module`. Its `:81-89` includes every historical file classified as source. `src/application/javascript/JavaScriptArtifactGraphContext.ts:287-297` assigns the JSON file the `artifact` kind. `src/domain/javascript/sourceToBundleComparison.ts:96-103` filters the current graph through `sourceBearingNodes` before building the candidate index. The exact JSON node therefore does not reach digest or path matching. The empty candidate set takes the complete-inventory `removed` branch at `:268-276`. The public command routes through `src/cli/applicationCommands.ts:68-78` and application Evidence authentication at `src/application/javascript/JavaScriptApplicationWorkflowService.ts:186-208`. The complete fixture creator validates inventory premises and its cryptographic commitment at `src/domain/referenceSourceGraph.ts:396-427` and `:543-551`; the public comparison accepted that fixture and the analyzer's unmodified Evidence. ### Additional context P2 is supported by a bounded analytical result error for JSON source entries with supplied complete inventories. The result can mislead source-retention analysis, while the observed run deletes no bytes and affects no live application. The common unmodified importer workflow returns `unknown` rather than high-confidence removal. Related: #3188 reports a separate failure in the same area; the two have independent fixes.
bsakaguchi 2026-10-15 18:31:20 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@xu91102 commented on GitHub (Oct 14, 2026):

Independently reproduced on a clean pinned Node 24.18.0 CLI checkout (44d98de2367b99808d78db732ec7d72533266233). The analysis/import/comparison owners and lock were checked byte-for-byte against captured main f75b1a416ba67b8421e7dbd248160afedb79581f; the newer checkout itself has not yet been executed in this reproduction.

An identical src/data.json is present with its exact path, digest, observed provenance and file_kind: json. The ordinary partial historical import returns unknown with zero candidates. The explicitly validated, fixed-fixture complete inventory returns removed / high with zero candidates. The complete inventory is a synthetic input built from the real imported entries and is not a claim that the importer returned complete coverage. Actual-absence controls still return removed for complete input and unknown for partial input; JavaScript controls retain their exact unchanged mappings.

The producing graph already keeps the parsed file-kind fact. The shared candidate projection filters on coarse graph node kinds and excludes that JSON file before the existing exact-digest scorer, which can match it. I am preparing a bounded correction using those existing source/file facts at the shared comparison owner, with complete/partial absence, root/container/non-source, digest ambiguity and public CLI/MCP controls. This records the issue-first verification; it does not claim a completed fix or PR.

<!-- gh-comment-id:6167443315 --> @xu91102 commented on GitHub (Oct 14, 2026): Independently reproduced on a clean pinned Node 24.18.0 CLI checkout (`44d98de2367b99808d78db732ec7d72533266233`). The analysis/import/comparison owners and lock were checked byte-for-byte against captured main `f75b1a416ba67b8421e7dbd248160afedb79581f`; the newer checkout itself has not yet been executed in this reproduction. An identical `src/data.json` is present with its exact path, digest, observed provenance and `file_kind: json`. The ordinary partial historical import returns `unknown` with zero candidates. The explicitly validated, fixed-fixture complete inventory returns `removed` / `high` with zero candidates. The complete inventory is a synthetic input built from the real imported entries and is not a claim that the importer returned complete coverage. Actual-absence controls still return removed for complete input and unknown for partial input; JavaScript controls retain their exact unchanged mappings. The producing graph already keeps the parsed file-kind fact. The shared candidate projection filters on coarse graph node kinds and excludes that JSON file before the existing exact-digest scorer, which can match it. I am preparing a bounded correction using those existing source/file facts at the shared comparison owner, with complete/partial absence, root/container/non-source, digest ambiguity and public CLI/MCP controls. This records the issue-first verification; it does not claim a completed fix or PR.
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Delivered on main in fffb07419.

Source-to-bundle matching excluded JSON inputs because their actual producer kind is artifact. Matching now accepts that existing kind without rewriting source identity.

Validation: Source-to-bundle comparison and application version CLI regressions. Full exact-revision qualification passed.

<!-- gh-comment-id:6177633707 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). Source-to-bundle matching excluded JSON inputs because their actual producer kind is artifact. Matching now accepts that existing kind without rewriting source identity. Validation: Source-to-bundle comparison and application version CLI regressions. Full exact-revision [qualification](https://github.com/morluto/rea/actions/runs/38629313022) passed.
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#1130
No description provided.