[GH-ISSUE #3188] [Bug] Unchanged source-directory modules appear removed and added after a sibling edit #1127

Closed
opened 2026-10-15 18:31:18 +00:00 by bsakaguchi · 1 comment
Owner

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

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

Comparing two analyzed source directories reports an unchanged JavaScript module as a removal and an addition after a sibling file changes the directory digest. Each graph has exactly one javascript-module with the unchanged main file's digest, yet the comparison returns no exact-module-source-digest match. The corresponding javascript-asset matches as unchanged by exact-content-digest.

The independent CLI reproduction completed both analyses and the comparison with exit code 0. Both graphs reported complete coverage, with no omissions, truncation, or limits. The left main module was removed and the right main module was added; both were unmatched with basis none and empty candidate lists.

Reproduction

  1. From a clean checkout at 58300885207133422af235e0f2b10658cf958b03, install the locked dependencies and build the runtime. These are setup prerequisites; the verifier used the baseline's prepared dependencies and build.
npm ci
npm run build:cached
  1. Create two local source directories. The unchanged main file has no exports, so its digest belongs to exactly one javascript-module node in each graph. Run the CLI with isolated home and configuration directories through the helper below.
mkdir -p repro/home/config repro/home/cache repro/home/data
mkdir -p repro/app-a/src repro/app-b/src
repro_cli() {
  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" \
    node scripts/rea.mjs "$@"
}
printf '%s\n' '{"name":"sample-app","version":"1.0.0","main":"src/main.js"}' > repro/app-a/package.json
cp repro/app-a/package.json repro/app-b/package.json
printf '%s\n' "require('./util.js');" > repro/app-a/src/main.js
cp repro/app-a/src/main.js repro/app-b/src/main.js
printf '%s\n' "module.exports = 'a';" > repro/app-a/src/util.js
printf '%s\n' "module.exports = 'b';" > repro/app-b/src/util.js
  1. Analyze both directories through the CLI, retain their complete Evidence records, assemble the comparison request, and compare them.
repro_cli analyze-javascript-application "$PWD/repro/app-a" --json > repro/left.json
repro_cli analyze-javascript-application "$PWD/repro/app-b" --json > repro/right.json
python3 - <<'REPRO'
import json
from pathlib import Path
root = Path('repro')
left = json.loads((root / 'left.json').read_text())
right = json.loads((root / 'right.json').read_text())
(root / 'compare.json').write_text(json.dumps({'left': left, 'right': right}))
REPRO
repro_cli compare-application-versions repro/compare.json --json > repro/result.json
  1. Inspect the main-file module candidates and their comparison items alongside the matching asset.
python3 - <<'REPRO'
import json
from pathlib import Path
root = Path('repro')
left = json.loads((root / 'left.json').read_text())['normalized_result']['graph']
right = json.loads((root / 'right.json').read_text())['normalized_result']['graph']
result = json.loads((root / 'result.json').read_text())['normalized_result']
print('coverage', left['coverage']['status'], right['coverage']['status'])
modules = []
for graph in (left, right):
    main = next(n for n in graph['nodes']
                if n['kind'] == 'javascript-module'
                and n['identity'].get('namespace') == 'source-module'
                and n['observations'][0]['properties'].get('path') == 'src/main.js')
    digest = main['identity']['artifact_sha256']
    count = sum(n['kind'] == 'javascript-module'
                and n['identity'].get('artifact_sha256') == digest
                for n in graph['nodes'])
    print('main module digest', digest, 'candidates', count)
    modules.append(main)
for main in modules:
    for item in result['items']:
        if main['node_id'] in (item['left_node_id'], item['right_node_id']):
            print('main module', item['status'], item['match']['basis'])
asset = next(n for n in left['nodes'] if n['kind'] == 'javascript-asset'
             and n['identity'].get('sha256') == modules[0]['identity']['artifact_sha256'])
item = next(i for i in result['items'] if i['left_node_id'] == asset['node_id'])
print('main asset', item['status'], item['match']['basis'])
print('summary', result['summary'])
print('matching', result['matching'])
REPRO

Expected behavior

docs/javascript-application-workflows.md:12-17 admits directory-analyzer Evidence into these workflows, and lines 299-313 explicitly support comparison of supplied directories or ASARs. The version comparison contract at lines 141-147 includes exact module-source digest matching when there is one unique candidate on each side and requires duplicate candidates to remain ambiguous. src/contracts/applicationToolContracts.ts:117-122 also advertises that tier.

The unique unchanged main module should appear in one paired comparison item with basis exact-module-source-digest. It should have no one-sided removed or added item. A paired item may still report changed dimensions because its container provenance differs.

Impact and scope

This produces false module removals and additions in a supported directory-version comparison even though the module's own bytes remain equal and its digest is unique on both sides. The observed asset match provides a bounded workaround for file-presence and content questions, but it does not restore module correspondence.

An independently analyzed byte-identical-directory control produced distinct Evidence IDs with equal root digests. It paired the main module by exact-node-identity, reported all nine items unchanged, and had no unmatched items. The reproduced failure is scoped to changed containers with an unchanged unique source module. Files with exports may produce several same-kind nodes sharing a digest; this fixture deliberately avoids that ambiguity and establishes no expectation for arbitrary pairing of those nodes.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node v26.10.0; macOS. The verifier did not record the OS version or architecture.

Target and artifact context

Two inert local source-directory applications with identical package metadata and identical src/main.js containing require('./util.js'); plus a newline. Only src/util.js differs, exporting 'a' on the left and 'b' on the right. The comparison uses the complete Evidence returned by the public directory analyzer. No application code, native-analysis provider, or external API was invoked.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

coverage complete complete
main module digest b3cdcf15be5b93228c27d5186fe03e742afdf733395d2b0e5e547ab28692c729 candidates 1
main module digest b3cdcf15be5b93228c27d5186fe03e742afdf733395d2b0e5e547ab28692c729 candidates 1
main module removed none
main module added none
main asset unchanged exact-content-digest
exact_module_source_digest: 0
ambiguous: 0
unmatched: 8

Typed outcome or recovery expected

Return a successful comparison with one paired main-module item whose match basis is exact-module-source-digest. Preserve duplicate ambiguity. Keep the identical-directory control paired, and do not give the changed util file an exact source-digest match. This check concerns module correspondence; the paired main item need not have unchanged status when its provenance dimensions differ.

Analysis or suspected cause

Source-derived reasoning at the reported baseline: src/domain/javascript/javascriptApplicationVersionKeys.ts:201-205 reads only the source_sha256 observation property for the module-source digest key. The source-module producer at src/application/javascript/JavaScriptModuleRelationships.ts:96-122 stores the own-file digest in artifact-local identity and emits no source_sha256 property. Its identity key includes the container digest, which changes after the sibling edit.

src/cli/applicationCommands.ts:57-65 invokes the production comparison service. src/application/javascript/JavaScriptApplicationWorkflowService.ts:115-170 validates distinct Evidence identities, authenticates both graphs, and calls the comparer. The successful CLI reproduction confirms that these guards admitted the inputs.

Other tiers do not rescue this reproduced module: its cross-container node IDs differ, its identity is not a content-digest identity, and it has no source-map-original identity or structural fingerprint. The permitted semantic key excludes javascript-module. The own-file digest is retained in the authenticated graph but does not enter the advertised module-source tier.

Additional context

P2 is supported by a consequential, bounded correspondence error. The asset-level exact match supplies a practical way to recover the unchanged file-presence/content result while treating module correspondence as unreliable. No security impact or target mutation was observed.

Related: #3191 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/3188 ### 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 Comparing two analyzed source directories reports an unchanged JavaScript module as a removal and an addition after a sibling file changes the directory digest. Each graph has exactly one `javascript-module` with the unchanged main file's digest, yet the comparison returns no `exact-module-source-digest` match. The corresponding `javascript-asset` matches as unchanged by `exact-content-digest`. The independent CLI reproduction completed both analyses and the comparison with exit code 0. Both graphs reported complete coverage, with no omissions, truncation, or limits. The left main module was `removed` and the right main module was `added`; both were unmatched with basis `none` and empty candidate lists. ### Reproduction 1. From a clean checkout at `58300885207133422af235e0f2b10658cf958b03`, install the locked dependencies and build the runtime. These are setup prerequisites; the verifier used the baseline's prepared dependencies and build. ```sh npm ci npm run build:cached ``` 2. Create two local source directories. The unchanged main file has no exports, so its digest belongs to exactly one `javascript-module` node in each graph. Run the CLI with isolated home and configuration directories through the helper below. ```sh mkdir -p repro/home/config repro/home/cache repro/home/data mkdir -p repro/app-a/src repro/app-b/src repro_cli() { 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" \ node scripts/rea.mjs "$@" } printf '%s\n' '{"name":"sample-app","version":"1.0.0","main":"src/main.js"}' > repro/app-a/package.json cp repro/app-a/package.json repro/app-b/package.json printf '%s\n' "require('./util.js');" > repro/app-a/src/main.js cp repro/app-a/src/main.js repro/app-b/src/main.js printf '%s\n' "module.exports = 'a';" > repro/app-a/src/util.js printf '%s\n' "module.exports = 'b';" > repro/app-b/src/util.js ``` 3. Analyze both directories through the CLI, retain their complete Evidence records, assemble the comparison request, and compare them. ```sh repro_cli analyze-javascript-application "$PWD/repro/app-a" --json > repro/left.json repro_cli analyze-javascript-application "$PWD/repro/app-b" --json > repro/right.json python3 - <<'REPRO' import json from pathlib import Path root = Path('repro') left = json.loads((root / 'left.json').read_text()) right = json.loads((root / 'right.json').read_text()) (root / 'compare.json').write_text(json.dumps({'left': left, 'right': right})) REPRO repro_cli compare-application-versions repro/compare.json --json > repro/result.json ``` 4. Inspect the main-file module candidates and their comparison items alongside the matching asset. ```sh python3 - <<'REPRO' import json from pathlib import Path root = Path('repro') left = json.loads((root / 'left.json').read_text())['normalized_result']['graph'] right = json.loads((root / 'right.json').read_text())['normalized_result']['graph'] result = json.loads((root / 'result.json').read_text())['normalized_result'] print('coverage', left['coverage']['status'], right['coverage']['status']) modules = [] for graph in (left, right): main = next(n for n in graph['nodes'] if n['kind'] == 'javascript-module' and n['identity'].get('namespace') == 'source-module' and n['observations'][0]['properties'].get('path') == 'src/main.js') digest = main['identity']['artifact_sha256'] count = sum(n['kind'] == 'javascript-module' and n['identity'].get('artifact_sha256') == digest for n in graph['nodes']) print('main module digest', digest, 'candidates', count) modules.append(main) for main in modules: for item in result['items']: if main['node_id'] in (item['left_node_id'], item['right_node_id']): print('main module', item['status'], item['match']['basis']) asset = next(n for n in left['nodes'] if n['kind'] == 'javascript-asset' and n['identity'].get('sha256') == modules[0]['identity']['artifact_sha256']) item = next(i for i in result['items'] if i['left_node_id'] == asset['node_id']) print('main asset', item['status'], item['match']['basis']) print('summary', result['summary']) print('matching', result['matching']) REPRO ``` ### Expected behavior `docs/javascript-application-workflows.md:12-17` admits directory-analyzer Evidence into these workflows, and lines 299-313 explicitly support comparison of supplied directories or ASARs. The version comparison contract at lines 141-147 includes exact module-source digest matching when there is one unique candidate on each side and requires duplicate candidates to remain ambiguous. `src/contracts/applicationToolContracts.ts:117-122` also advertises that tier. The unique unchanged main module should appear in one paired comparison item with basis `exact-module-source-digest`. It should have no one-sided removed or added item. A paired item may still report changed dimensions because its container provenance differs. ### Impact and scope This produces false module removals and additions in a supported directory-version comparison even though the module's own bytes remain equal and its digest is unique on both sides. The observed asset match provides a bounded workaround for file-presence and content questions, but it does not restore module correspondence. An independently analyzed byte-identical-directory control produced distinct Evidence IDs with equal root digests. It paired the main module by `exact-node-identity`, reported all nine items unchanged, and had no unmatched items. The reproduced failure is scoped to changed containers with an unchanged unique source module. Files with exports may produce several same-kind nodes sharing a digest; this fixture deliberately avoids that ambiguity and establishes no expectation for arbitrary pairing of those nodes. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node v26.10.0; macOS. The verifier did not record the OS version or architecture. ### Target and artifact context Two inert local source-directory applications with identical package metadata and identical `src/main.js` containing `require('./util.js');` plus a newline. Only `src/util.js` differs, exporting `'a'` on the left and `'b'` on the right. The comparison uses the complete Evidence returned by the public directory analyzer. No application code, native-analysis provider, or external API was invoked. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence ```text coverage complete complete main module digest b3cdcf15be5b93228c27d5186fe03e742afdf733395d2b0e5e547ab28692c729 candidates 1 main module digest b3cdcf15be5b93228c27d5186fe03e742afdf733395d2b0e5e547ab28692c729 candidates 1 main module removed none main module added none main asset unchanged exact-content-digest exact_module_source_digest: 0 ambiguous: 0 unmatched: 8 ``` ### Typed outcome or recovery expected Return a successful comparison with one paired main-module item whose match basis is `exact-module-source-digest`. Preserve duplicate ambiguity. Keep the identical-directory control paired, and do not give the changed util file an exact source-digest match. This check concerns module correspondence; the paired main item need not have `unchanged` status when its provenance dimensions differ. ### Analysis or suspected cause Source-derived reasoning at the reported baseline: `src/domain/javascript/javascriptApplicationVersionKeys.ts:201-205` reads only the `source_sha256` observation property for the module-source digest key. The source-module producer at `src/application/javascript/JavaScriptModuleRelationships.ts:96-122` stores the own-file digest in artifact-local identity and emits no `source_sha256` property. Its identity key includes the container digest, which changes after the sibling edit. `src/cli/applicationCommands.ts:57-65` invokes the production comparison service. `src/application/javascript/JavaScriptApplicationWorkflowService.ts:115-170` validates distinct Evidence identities, authenticates both graphs, and calls the comparer. The successful CLI reproduction confirms that these guards admitted the inputs. Other tiers do not rescue this reproduced module: its cross-container node IDs differ, its identity is not a content-digest identity, and it has no source-map-original identity or structural fingerprint. The permitted semantic key excludes `javascript-module`. The own-file digest is retained in the authenticated graph but does not enter the advertised module-source tier. ### Additional context P2 is supported by a consequential, bounded correspondence error. The asset-level exact match supplies a practical way to recover the unchanged file-presence/content result while treating module correspondence as unreliable. No security impact or target mutation was observed. Related: #3191 reports a separate failure in the same area; the two have independent fixes.
bsakaguchi 2026-10-15 18:31:18 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

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

Delivered on main in fffb07419.

Modules did not carry their own source digest, and comparison also treated the enclosing container digest as a module semantic property. Each module now retains source_sha256; container-scoped identity remains distinct from module contents.

Validation: Module relationship producer and complete application-version CLI regression for an unchanged module beside a changed sibling. Full exact-revision qualification passed.

<!-- gh-comment-id:6177631024 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). Modules did not carry their own source digest, and comparison also treated the enclosing container digest as a module semantic property. Each module now retains source_sha256; container-scoped identity remains distinct from module contents. Validation: Module relationship producer and complete application-version CLI regression for an unchanged module beside a changed sibling. 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#1127
No description provided.