[GH-ISSUE #3316] [Bug] Runtime reconciliation hides mapped-file digest mismatch when another asset matches #1188

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

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

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

reconcile-javascript-runtime omits a mapped-file content mismatch when captured script bytes match a different asset in the same static layer. It reports only a high-confidence matched correspondence to the other path even though the caller-declared mapping identifies a file with different bytes. This hides the documented mismatch signal from callers comparing shipped files with a runtime capture.

All three reconciliation commands in the reproduction exited successfully. In the first case, the mapping points to a/main.js, whose source declares value = 1, while the captured source declares value = 2. Only b/main.js has the captured bytes. The result contains an observed_as edge to b/main.js with high confidence and no captured-content-disagrees-with-static-location reconciliation item.

The second case consistently maps to b/main.js and matches on content and location. The third case maps to a/main.js with captured bytes declaring value = 3, absent from both static assets, and exposes the mapped-path mismatch. The runtime location and mapping remain unchanged between the first and third cases; only the captured bytes and availability of an alternate digest match differ.

Reproduction

  1. Create a clean source checkout, select the reproduced baseline, install locked dependencies, and build the runtime.
git clone https://github.com/morluto/rea.git rea-repro
cd rea-repro
git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063
npm ci
npm run build:cached
  1. From the repository root, save the following as repro-runtime.mjs. It creates two static JavaScript files and schema-valid synthetic passive Electron Evidence with captured source bytes, then invokes the real analysis and reconciliation CLI commands. It does not start Electron, contact CDP, or execute the analyzed scripts. Every input file is created inline.
cat > repro-runtime.mjs <<'JS'
import { mkdirSync, writeFileSync } from 'node:fs';
import { resolve, join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { execFileSync } from 'node:child_process';
const repo = process.cwd();
const root = resolve(process.argv[2] ?? 'runtime-case');
const { createElectronEvidence } = await import(pathToFileURL(join(repo, 'dist/application/javascript/ElectronEvidence.js')));
const { createWebTextArtifact } = await import(pathToFileURL(join(repo, 'dist/domain/webContentArtifact.js')));
const app = join(root, 'app');
const live = join(root, 'live');
for (const prefix of ['a', 'b']) mkdirSync(join(app, prefix), { recursive: true });
writeFileSync(join(app, 'a/main.js'), 'export const value = 1;\n');
writeFileSync(join(app, 'b/main.js'), 'export const value = 2;\n');
const run = (...args) => JSON.parse(execFileSync(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }));
const analysis = run('analyze-javascript-application', app);
for (const [prefix, value] of [['a', 2], ['b', 2], ['a', 3]]) {
  const source = `export const value = ${value};\n`;
  const runtime = createElectronEvidence('inspect_electron_page', {
    cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'target-main', observation_ms: 100, include_script_sources: true
  }, {
    browser: { product: 'Electron/fixture', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: '13' },
    target: { target_id: 'target-main', type: 'page', title: 'Fixture', file_path: join(live, 'index.html'), attached: false },
    capture_window: { started_at: '2026-07-15T00:00:00.000Z', ended_at: '2026-07-15T00:00:00.100Z', observation_ms: 100 },
    completeness: { status: 'complete_within_window', conditions: ['complete_within_window'], policy_filtered_sections: [], attach_limited_sections: [], truncated_sections: [], unavailable_sections: [], excluded: [], dropped_events: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0, total: 0 } },
    frames: [], dom: { total_nodes: 0, nodes: [] },
    scripts: { total: 1, items: [{ script_key: 'electron_script_' + '1'.repeat(64), frame_id: null, file_path: join(live, 'main.js'), cdp_hash: 'fixture', length: Buffer.byteLength(source), is_module: true, language: 'JavaScript', source: { included: true, artifact: createWebTextArtifact(source, 'text/javascript') } }] },
    resources: [], workers: [], limitations: ['Synthetic passive capture fixture.']
  }, { id: 'rea-cdp-electron', name: 'REA Electron file-page CDP observation provider', version: '1' });
  const inputPath = join(root, `input-${prefix}-${value}.json`);
  writeFileSync(inputPath, JSON.stringify({ static_layers: [{ role: 'application', analysis, runtime_mappings: [{ kind: 'file-root', root: live, artifact_prefix: prefix }] }], runtime_observations: [runtime] }));
  const evidence = run('reconcile-javascript-runtime', inputPath);
  writeFileSync(join(root, `result-${prefix}-${value}.json`), JSON.stringify(evidence));
  const result = evidence.normalized_result;
  const script = result.reconciliations.find(item => item.entity_kind === 'script');
  const matched = result.graph.nodes.find(node => node.node_id === script.static_node_id);
  console.log(JSON.stringify({ prefix, captured_value: value, status: script.status, basis: script.basis, reason: script.reason, matched_paths: matched?.observations.map(item => item.properties.path) ?? [] }));
}
JS
  1. Run the reproduction and compare all three printed results.
node repro-runtime.mjs

The first case maps the captured main.js location to a/main.js while its captured bytes match only b/main.js. The second case uses a consistent mapping to b/main.js. The third case maps to a/main.js with captured bytes absent from both assets. The script retains each generated reconciliation input and output as runtime-case/input-<prefix>-<value>.json and runtime-case/result-<prefix>-<value>.json.

Expected behavior

docs/javascript-runtime-reconciliation.md:102, matching rule 5, promises an explicit mismatch when captured bytes disagree with the static candidate at the mapped path. Rule 2 at docs/javascript-runtime-reconciliation.md:94 permits a unique content match when no location mapping is available.

A disagreement between captured bytes and the static candidate at the mapped path must remain an explicit mismatch even when the captured digest occurs at another path. An alternate digest match may provide candidate information, but it must not erase the contradiction at the caller-declared mapped path. The consistent second case should still match on both content and location, and the third case should retain its mismatch.

Impact and scope

Impact level: P2. Assessment status: supported. A valid reconciliation input hides the promised mapped-path mismatch and emits only a high-confidence correspondence to another asset. This affects runtime/static mismatch analysis when another asset has the captured digest. It does not alter the retained source bytes.

Workaround status: available. The original analysis and runtime Evidence retain their digests and paths. Callers can compare the captured source digest with the candidate at the declared mapped path directly, without changing the source files or discarding Evidence. The supplied cases demonstrate that both original byte identities remain available.

REA version

6.4.0 (main at 8dde6370d0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64. Static analysis used the compiled runtime.

Target and artifact context

Two locally created static JavaScript assets in one application layer and synthetic, schema-valid passive Electron Evidence fixtures with included captured source bytes. The declared file-root mapping selects an artifact prefix. The executed CLI reproduction used locally created files and synthetic passive capture Evidence; no real Electron capture was performed. The loopback CDP endpoint is fixture metadata and is not contacted.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

The executed compiled CLI reproduction returned the following selected fields. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete.

{"prefix":"a","captured_value":2,"status":"matched","basis":"content-sha256","reason":"unique-content-match","matched_paths":["b/main.js"]}
{"prefix":"b","captured_value":2,"status":"matched","basis":"content-and-location","reason":"content-and-location-match","matched_paths":["b/main.js"]}
{"prefix":"a","captured_value":3,"status":"unmatched","basis":"operator-file-mapping","reason":"captured-content-disagrees-with-static-location","matched_paths":[]}

The first case has a high-confidence observed_as edge to b/main.js and no mapped-path disagreement reconciliation item. Removing the alternate digest match, as in the third case, makes the mismatch observable without changing the runtime location or mapping.

Typed outcome or recovery expected

Run the same three cases. The first must expose the disagreement with a/main.js rather than reporting only a successful match to b/main.js. The second must preserve its content-and-location match, and the third must preserve its mapped-path mismatch. Any alternate content candidate must remain distinct from the mapped-path contradiction.

Analysis or suspected cause

Source-derived reasoning: src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:99 selects the unique content candidate and returns its match before the mapped-path disagreement branch at src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:119. This ordering explains why an alternate digest match suppresses the documented mismatch in the reproduced input.

The existing mapped-digest-disagreement regression at tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts:227 covers captured bytes with no matching alternate asset. This reproduction adds that alternate asset without changing the mapped candidate. The first and third observed output rows establish the behavioral difference independently of this source-derived explanation.

Additional context

Related issue #3192 concerns false absence load states after multiple static candidates remain ambiguous; this report concerns a unique alternate content match hiding a mapped-path digest contradiction.

Related completed issue #2524 concerns Windows case folding of recorded POSIX roots when captured source bytes are absent; this report uses a valid mapped path and included source bytes whose digest disagrees with that path.

Related completed issue #1234 concerns path-specific classification loss for byte-identical inventory entries; this report concerns precedence between mapped-path disagreement and a unique alternate digest match during runtime reconciliation.

Related completed issue #2454 corrects the runtime reconciliation documentation's producer list; it does not concern digest matching precedence or mapped-path mismatches.

Related completed feature #851 adds browser module-specifier tracing with URL and captured-source identities; it does not report a runtime reconciliation mismatch suppressed by an alternate digest match.

Related completed feature #117 adds JavaScript bundle and source-semantic analysis; it does not report this specific runtime reconciliation failure.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3316 ### 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 `reconcile-javascript-runtime` omits a mapped-file content mismatch when captured script bytes match a different asset in the same static layer. It reports only a high-confidence `matched` correspondence to the other path even though the caller-declared mapping identifies a file with different bytes. This hides the documented mismatch signal from callers comparing shipped files with a runtime capture. All three reconciliation commands in the reproduction exited successfully. In the first case, the mapping points to `a/main.js`, whose source declares `value = 1`, while the captured source declares `value = 2`. Only `b/main.js` has the captured bytes. The result contains an `observed_as` edge to `b/main.js` with high confidence and no `captured-content-disagrees-with-static-location` reconciliation item. The second case consistently maps to `b/main.js` and matches on content and location. The third case maps to `a/main.js` with captured bytes declaring `value = 3`, absent from both static assets, and exposes the mapped-path mismatch. The runtime location and mapping remain unchanged between the first and third cases; only the captured bytes and availability of an alternate digest match differ. ### Reproduction 1. Create a clean source checkout, select the reproduced baseline, install locked dependencies, and build the runtime. ```sh git clone https://github.com/morluto/rea.git rea-repro cd rea-repro git checkout 8dde6370d0809ad04cda3342d4e6f290f48aa063 npm ci npm run build:cached ``` 2. From the repository root, save the following as `repro-runtime.mjs`. It creates two static JavaScript files and schema-valid synthetic passive Electron Evidence with captured source bytes, then invokes the real analysis and reconciliation CLI commands. It does not start Electron, contact CDP, or execute the analyzed scripts. Every input file is created inline. ```sh cat > repro-runtime.mjs <<'JS' import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve, join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { execFileSync } from 'node:child_process'; const repo = process.cwd(); const root = resolve(process.argv[2] ?? 'runtime-case'); const { createElectronEvidence } = await import(pathToFileURL(join(repo, 'dist/application/javascript/ElectronEvidence.js'))); const { createWebTextArtifact } = await import(pathToFileURL(join(repo, 'dist/domain/webContentArtifact.js'))); const app = join(root, 'app'); const live = join(root, 'live'); for (const prefix of ['a', 'b']) mkdirSync(join(app, prefix), { recursive: true }); writeFileSync(join(app, 'a/main.js'), 'export const value = 1;\n'); writeFileSync(join(app, 'b/main.js'), 'export const value = 2;\n'); const run = (...args) => JSON.parse(execFileSync(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })); const analysis = run('analyze-javascript-application', app); for (const [prefix, value] of [['a', 2], ['b', 2], ['a', 3]]) { const source = `export const value = ${value};\n`; const runtime = createElectronEvidence('inspect_electron_page', { cdp_endpoint: 'http://127.0.0.1:9223', target_id: 'target-main', observation_ms: 100, include_script_sources: true }, { browser: { product: 'Electron/fixture', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: '13' }, target: { target_id: 'target-main', type: 'page', title: 'Fixture', file_path: join(live, 'index.html'), attached: false }, capture_window: { started_at: '2026-07-15T00:00:00.000Z', ended_at: '2026-07-15T00:00:00.100Z', observation_ms: 100 }, completeness: { status: 'complete_within_window', conditions: ['complete_within_window'], policy_filtered_sections: [], attach_limited_sections: [], truncated_sections: [], unavailable_sections: [], excluded: [], dropped_events: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0, total: 0 } }, frames: [], dom: { total_nodes: 0, nodes: [] }, scripts: { total: 1, items: [{ script_key: 'electron_script_' + '1'.repeat(64), frame_id: null, file_path: join(live, 'main.js'), cdp_hash: 'fixture', length: Buffer.byteLength(source), is_module: true, language: 'JavaScript', source: { included: true, artifact: createWebTextArtifact(source, 'text/javascript') } }] }, resources: [], workers: [], limitations: ['Synthetic passive capture fixture.'] }, { id: 'rea-cdp-electron', name: 'REA Electron file-page CDP observation provider', version: '1' }); const inputPath = join(root, `input-${prefix}-${value}.json`); writeFileSync(inputPath, JSON.stringify({ static_layers: [{ role: 'application', analysis, runtime_mappings: [{ kind: 'file-root', root: live, artifact_prefix: prefix }] }], runtime_observations: [runtime] })); const evidence = run('reconcile-javascript-runtime', inputPath); writeFileSync(join(root, `result-${prefix}-${value}.json`), JSON.stringify(evidence)); const result = evidence.normalized_result; const script = result.reconciliations.find(item => item.entity_kind === 'script'); const matched = result.graph.nodes.find(node => node.node_id === script.static_node_id); console.log(JSON.stringify({ prefix, captured_value: value, status: script.status, basis: script.basis, reason: script.reason, matched_paths: matched?.observations.map(item => item.properties.path) ?? [] })); } JS ``` 3. Run the reproduction and compare all three printed results. ```sh node repro-runtime.mjs ``` The first case maps the captured `main.js` location to `a/main.js` while its captured bytes match only `b/main.js`. The second case uses a consistent mapping to `b/main.js`. The third case maps to `a/main.js` with captured bytes absent from both assets. The script retains each generated reconciliation input and output as `runtime-case/input-<prefix>-<value>.json` and `runtime-case/result-<prefix>-<value>.json`. ### Expected behavior `docs/javascript-runtime-reconciliation.md:102`, matching rule 5, promises an explicit mismatch when captured bytes disagree with the static candidate at the mapped path. Rule 2 at `docs/javascript-runtime-reconciliation.md:94` permits a unique content match when no location mapping is available. A disagreement between captured bytes and the static candidate at the mapped path must remain an explicit mismatch even when the captured digest occurs at another path. An alternate digest match may provide candidate information, but it must not erase the contradiction at the caller-declared mapped path. The consistent second case should still match on both content and location, and the third case should retain its mismatch. ### Impact and scope Impact level: P2. Assessment status: supported. A valid reconciliation input hides the promised mapped-path mismatch and emits only a high-confidence correspondence to another asset. This affects runtime/static mismatch analysis when another asset has the captured digest. It does not alter the retained source bytes. Workaround status: available. The original analysis and runtime Evidence retain their digests and paths. Callers can compare the captured source digest with the candidate at the declared mapped path directly, without changing the source files or discarding Evidence. The supplied cases demonstrate that both original byte identities remain available. ### REA version 6.4.0 (main at 8dde6370d0809ad04cda3342d4e6f290f48aa063) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64. Static analysis used the compiled runtime. ### Target and artifact context Two locally created static JavaScript assets in one application layer and synthetic, schema-valid passive Electron Evidence fixtures with included captured source bytes. The declared file-root mapping selects an artifact prefix. The executed CLI reproduction used locally created files and synthetic passive capture Evidence; no real Electron capture was performed. The loopback CDP endpoint is fixture metadata and is not contacted. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence The executed compiled CLI reproduction returned the following selected fields. Evidence provenance: observed. Local verification: reproduced. Reproduction completeness: complete. ```text {"prefix":"a","captured_value":2,"status":"matched","basis":"content-sha256","reason":"unique-content-match","matched_paths":["b/main.js"]} {"prefix":"b","captured_value":2,"status":"matched","basis":"content-and-location","reason":"content-and-location-match","matched_paths":["b/main.js"]} {"prefix":"a","captured_value":3,"status":"unmatched","basis":"operator-file-mapping","reason":"captured-content-disagrees-with-static-location","matched_paths":[]} ``` The first case has a high-confidence `observed_as` edge to `b/main.js` and no mapped-path disagreement reconciliation item. Removing the alternate digest match, as in the third case, makes the mismatch observable without changing the runtime location or mapping. ### Typed outcome or recovery expected Run the same three cases. The first must expose the disagreement with `a/main.js` rather than reporting only a successful match to `b/main.js`. The second must preserve its `content-and-location` match, and the third must preserve its mapped-path mismatch. Any alternate content candidate must remain distinct from the mapped-path contradiction. ### Analysis or suspected cause Source-derived reasoning: `src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:99` selects the unique content candidate and returns its match before the mapped-path disagreement branch at `src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:119`. This ordering explains why an alternate digest match suppresses the documented mismatch in the reproduced input. The existing mapped-digest-disagreement regression at `tests/boundary/filesystem/javascript-applications/javascriptRuntimeReconciliation.test.ts:227` covers captured bytes with no matching alternate asset. This reproduction adds that alternate asset without changing the mapped candidate. The first and third observed output rows establish the behavioral difference independently of this source-derived explanation. ### Additional context Related issue [#3192](https://github.com/morluto/rea/issues/3192) concerns false absence load states after multiple static candidates remain ambiguous; this report concerns a unique alternate content match hiding a mapped-path digest contradiction. Related completed issue [#2524](https://github.com/morluto/rea/issues/2524) concerns Windows case folding of recorded POSIX roots when captured source bytes are absent; this report uses a valid mapped path and included source bytes whose digest disagrees with that path. Related completed issue [#1234](https://github.com/morluto/rea/issues/1234) concerns path-specific classification loss for byte-identical inventory entries; this report concerns precedence between mapped-path disagreement and a unique alternate digest match during runtime reconciliation. Related completed issue [#2454](https://github.com/morluto/rea/issues/2454) corrects the runtime reconciliation documentation's producer list; it does not concern digest matching precedence or mapped-path mismatches. Related completed feature [#851](https://github.com/morluto/rea/issues/851) adds browser module-specifier tracing with URL and captured-source identities; it does not report a runtime reconciliation mismatch suppressed by an alternate digest match. Related completed feature [#117](https://github.com/morluto/rea/issues/117) adds JavaScript bundle and source-semantic analysis; it does not report this specific runtime reconciliation failure.
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#1188
No description provided.