[GH-ISSUE #6790] [Bug] JavaScript module views hide retained paths of byte-identical assets #4143

Closed
opened 2026-10-31 00:47:07 +00:00 by bsakaguchi · 0 comments
Owner

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

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

Observed at the production graph/projection boundary: two static JavaScript file records, a.js and b.js, with identical bytes become one javascript-asset containing both path observations. inspectAnalysisView then returns zero rows with exhausted: true for a module-reference query selecting b.js, and rejects a module item selected by that path. Selecting a.js works and returns the observations for both paths. A javascript-facts query for b.js finds the same asset, so the path was retained rather than omitted during analysis.

Changing only the second file's bytes produces two assets, and both path selectors work.

Reproduction

With locked dependencies installed at the revision below, run from the checkout root using Node 24.22.1. This is an in-process production-boundary probe: it uses the real static parser, file-to-graph producer, graph authentication, and shared inspectAnalysisView workflow. An existing valid analysis fixture supplies the surrounding Evidence and companion semantic graph. It does not run the target JavaScript or an external provider.

Save this as /tmp/rea-module-alias.mjs:

Executed probe
import { createHash } from 'node:crypto';
import { addJavaScriptArtifactFiles, createJavaScriptArtifactRootNode } from './src/application/javascript/JavaScriptArtifactGraphStructure.ts';
import { JavaScriptArtifactGraphAccumulator } from './src/application/javascript/JavaScriptArtifactGraphAccumulator.ts';
import { JavaScriptJsonDocuments } from './src/application/javascript/JavaScriptJsonDocuments.ts';
import { analyzeJavaScriptStaticSource } from './src/domain/javascript/javascriptStaticAnalysis.ts';
import { createJavaScriptApplicationGraph } from './src/domain/javascript/javascriptApplicationGraph.ts';
import { analysisViewJavaScriptAnalysis, analysisViewBindJavaScriptGraphs, analysisViewJavaScriptEvidence } from './tests/fixtures/analysisView.ts';
import { inspectAnalysisView } from './src/application/analysisView/AnalysisViewService.ts';

const source = 'export const answer = 42;\n';
const sha256 = createHash('sha256').update(source).digest('hex');
const rootHash = 'a'.repeat(64);
const distinct = process.env.DISTINCT_BYTES === '1';
const files = ['a.js', 'b.js'].map((path, i) => {
  const value = distinct && i === 1 ? 'export const answer = 43;\n' : source;
  const sha256 = createHash('sha256').update(value).digest('hex');
  return { path, sha256, container_sha256: rootHash, bytes: Buffer.byteLength(value), inventory_artifact_id: `art_${sha256}`, kind: 'javascript', unpacked: false, text: { included: true, value } };
});
const accumulator = new JavaScriptArtifactGraphAccumulator();
const snapshot = { manifest: { root_artifact_id: `art_${rootHash}`, root_sha256: rootHash, root_format: 'directory', manifest_id: `agm_${rootHash}`, graph_sha256: rootHash }, nodes: [{ artifact_id: `art_${rootHash}`, sha256: rootHash, size: 1 }] };
const fileSet = { files, unavailable_relevant_files: [], containers: [], text_bytes_read: source.length * 2, invalid_utf8_files: 0 };
const analysis = { jsonDocuments: new JavaScriptJsonDocuments(), files: files.map(file => ({ file, javascript: analyzeJavaScriptStaticSource(file.text.value), semantic: null })), packages: [], json_modules: [], html_scripts: [], html_inline_scripts: [], source_maps: [], visited_ast_nodes: 0, findings: 0, modules: 0, parse_failures: 0, limitations: [] };
const root = createJavaScriptArtifactRootNode(accumulator, snapshot);
const context = { accumulator, snapshot, fileSet, analysis, directoryPaths: new Set(), root, filesByPath: new Map(files.map(f => [f.path, f])), unanalyzedFiles: undefined, fileNodes: new Map(), assetNodes: new Map(), sourceModuleNodes: new Map(), chunkNodes: new Map(), moduleNodes: new Map(), containerNodes: new Map(), applicationRootsBySource: new Map(), applicationOwnershipUnknownContainers: new Set(), applicationModuleGapSources: new Map(), applicationPossibleRootsByContainer: new Map(), applicationModuleReferencesRetained: new Set() };
addJavaScriptArtifactFiles(context);
const base = analysisViewJavaScriptAnalysis();
const graph = createJavaScriptApplicationGraph({ schema: 'JavaScriptApplicationGraph', root_node_ids: [...base.graph.root_node_ids, root.node_id], nodes: [...base.graph.nodes, ...accumulator.nodes()], edges: [...base.graph.edges, ...accumulator.edges()], coverage: base.graph.coverage, limitations: [] });
const evidence = analysisViewJavaScriptEvidence(analysisViewBindJavaScriptGraphs(base, graph));
const assets = accumulator.nodes().filter(n => n.kind === 'javascript-asset');
console.log(JSON.stringify({ distinct, producedAssetNodes: assets.length, retainedPaths: assets.flatMap(a => a.observations.map(o => o.properties.path)).sort() }));
for (const path of ['a.js', 'b.js']) {
  for (const view of [
    { kind: 'javascript', collection: 'modules', projection: 'references', where: { kind: 'javascript-asset', path } },
    { kind: 'javascript-facts', where: { kinds: ['javascript-asset'], paths: [path] }, property_keys: ['path'] }
  ]) {
    const result = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view });
    if (!result.ok) throw result.error;
    const projected = result.value.normalized_result;
    console.log(JSON.stringify({ query: view.kind, path, rows: projected.rows.map(r => ({ node_id: r.node_id, path: r.path ?? r.properties?.path })), coverage: projected.coverage }));
  }
}

for (const path of ['a.js', 'b.js']) {
  const result = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view: { kind: 'item', collection: 'modules', selector: { path } } });
  console.log(JSON.stringify({ query: 'item', path, ok: result.ok, ...(result.ok ? { itemPaths: result.value.normalized_result.item.observations.map(o => o.properties.path) } : { message: result.error.message }) }));
}
const byId = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view: { kind: 'item', collection: 'modules', selector: { node_id: assets[0].node_id } } });
if (!byId.ok) throw byId.error;
console.log(JSON.stringify({ query: 'item-by-node-id', itemPaths: byId.value.normalized_result.item.observations.map(o => o.properties.path) }));
node --import ./node_modules/tsx/dist/loader.mjs --input-type=module < /tmp/rea-module-alias.mjs
DISTINCT_BYTES=1 node --import ./node_modules/tsx/dist/loader.mjs --input-type=module < /tmp/rea-module-alias.mjs

The duplicate-content case reproduced during pre-draft investigation; the distinct-content control passed. Complete CLI/MCP transport runs and a filesystem-to-analysis run were not performed.

Expected behavior

Exact retained-path selection should find an asset at either of its observed paths, or explicitly qualify any path selection restriction instead of reporting an empty, exhausted match set. Item selection should also recognize either path, with the existing ambiguity handling when multiple modules match.

The module-reference contract describes exact filters over retained owner facts. The file producer retains each file's path on a content-digest asset. Facts projection already selects the corresponding observation by its retained path.

Impact and scope

A caller filtering retained module references by an ordinary duplicate file's path receives a false negative, while item selection fails for that same path. This can hide byte-identical copied/vendored assets during static analysis. The demonstrated failure affects one path of the two-path asset; full Evidence remains intact. Different bytes remove the failure in the executed control. Broader frequency, ASAR input, and adapter-specific behavior are NOT_ENOUGH_INFO because they were not exercised.

Verified workaround: use javascript-facts with where.paths: ["b.js"] and where.kinds: ["javascript-asset"], then select the returned node_id using the module item view. Both steps succeeded and the item retained both original path observations.

REA version

6.7.0; source a7180370332350bc0f998fbd643888221759570e.

Runtime and platform

Node.js v24.22.1; Linux 7.2.8-9-omarchy; x86_64. No Hopper/Ghidra or live target.

Target and artifact context

Inert JavaScript file records whose source is export const answer = 42; plus a newline at both paths. The control changes b.js to export const answer = 43; plus a newline. Actual production file-to-graph code creates their asset nodes; no source patch was applied.

Interface

Other

Earliest failure stage

Transport or result projection

Relevant output or evidence

Selected exact output from the executed duplicate-content probe:

{"distinct":false,"producedAssetNodes":1,"retainedPaths":["a.js","b.js"]}
{"query":"javascript","path":"b.js","rows":[],"coverage":{"status":"empty","examined":0,"total":0,"next_offset":null,"exhausted":true}}
{"query":"javascript-facts","path":"b.js","rows":[{"node_id":"jag_node_50fb137cd66bd41d9a644d32528ee2e7d06279fa213398a650ead0b13d9d3743","path":"b.js"}],"coverage":{"status":"page","examined":1,"total":1,"next_offset":null,"exhausted":true}}
{"query":"item","path":"a.js","ok":true,"itemPaths":["a.js","b.js"]}
{"query":"item","path":"b.js","ok":false,"message":"Invalid analysis input for inspect_analysis_view"}
{"query":"item-by-node-id","itemPaths":["a.js","b.js"]}

The distinct-content control printed producedAssetNodes: 2; both module-reference queries returned one row, and both path-selected item calls returned ok: true.

Typed outcome or recovery expected

A successful reference/item result for the retained selected path, preserving the shared asset identity and its path evidence.

Analysis or suspected cause

Source-derived: javascriptModulePath picks the first path observation for a content-digest node. Both module filtering and path-based item selection compare only that single path. The production accumulator intentionally merges same-identity assets and retains their observations. The view therefore discards path multiplicity at projection time.

Additional context

The supplied open-issue inventory was searched. #6456 concerns comparison classifying several graph representations of one file as split/duplicated; this report concerns selecting two distinct retained paths of one content-deduplicated asset. #5407 concerns an unselected-source limitation, and #5595 concerns CLI analysis-lane dispatch. Open and closed searches also covered javascriptModulePath, inspect_analysis_view with identical, javascript-asset with path, and JavaScript path aliases; no matching report was found.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The executed production-boundary fixture returns an empty exhausted match set and a failed item request for an exact path demonstrably retained on an ordinary byte-identical asset; different-content controls succeed. The defect affects bounded result selection without losing the complete Evidence.
  • Workaround status: available
  • Workaround basis: The executed javascript-facts query finds the hidden path and its node_id; selecting that node_id returns the complete item with both path observations.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6790 ## 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 Observed at the production graph/projection boundary: two static JavaScript file records, `a.js` and `b.js`, with identical bytes become one `javascript-asset` containing both path observations. `inspectAnalysisView` then returns **zero rows with `exhausted: true`** for a module-reference query selecting `b.js`, and rejects a module item selected by that path. Selecting `a.js` works and returns the observations for both paths. A `javascript-facts` query for `b.js` finds the same asset, so the path was retained rather than omitted during analysis. Changing only the second file's bytes produces two assets, and both path selectors work. ## Reproduction With locked dependencies installed at the revision below, run from the checkout root using Node 24.22.1. This is an in-process production-boundary probe: it uses the real static parser, file-to-graph producer, graph authentication, and shared `inspectAnalysisView` workflow. An existing valid analysis fixture supplies the surrounding Evidence and companion semantic graph. It does not run the target JavaScript or an external provider. Save this as `/tmp/rea-module-alias.mjs`: <details> <summary>Executed probe</summary> ```js import { createHash } from 'node:crypto'; import { addJavaScriptArtifactFiles, createJavaScriptArtifactRootNode } from './src/application/javascript/JavaScriptArtifactGraphStructure.ts'; import { JavaScriptArtifactGraphAccumulator } from './src/application/javascript/JavaScriptArtifactGraphAccumulator.ts'; import { JavaScriptJsonDocuments } from './src/application/javascript/JavaScriptJsonDocuments.ts'; import { analyzeJavaScriptStaticSource } from './src/domain/javascript/javascriptStaticAnalysis.ts'; import { createJavaScriptApplicationGraph } from './src/domain/javascript/javascriptApplicationGraph.ts'; import { analysisViewJavaScriptAnalysis, analysisViewBindJavaScriptGraphs, analysisViewJavaScriptEvidence } from './tests/fixtures/analysisView.ts'; import { inspectAnalysisView } from './src/application/analysisView/AnalysisViewService.ts'; const source = 'export const answer = 42;\n'; const sha256 = createHash('sha256').update(source).digest('hex'); const rootHash = 'a'.repeat(64); const distinct = process.env.DISTINCT_BYTES === '1'; const files = ['a.js', 'b.js'].map((path, i) => { const value = distinct && i === 1 ? 'export const answer = 43;\n' : source; const sha256 = createHash('sha256').update(value).digest('hex'); return { path, sha256, container_sha256: rootHash, bytes: Buffer.byteLength(value), inventory_artifact_id: `art_${sha256}`, kind: 'javascript', unpacked: false, text: { included: true, value } }; }); const accumulator = new JavaScriptArtifactGraphAccumulator(); const snapshot = { manifest: { root_artifact_id: `art_${rootHash}`, root_sha256: rootHash, root_format: 'directory', manifest_id: `agm_${rootHash}`, graph_sha256: rootHash }, nodes: [{ artifact_id: `art_${rootHash}`, sha256: rootHash, size: 1 }] }; const fileSet = { files, unavailable_relevant_files: [], containers: [], text_bytes_read: source.length * 2, invalid_utf8_files: 0 }; const analysis = { jsonDocuments: new JavaScriptJsonDocuments(), files: files.map(file => ({ file, javascript: analyzeJavaScriptStaticSource(file.text.value), semantic: null })), packages: [], json_modules: [], html_scripts: [], html_inline_scripts: [], source_maps: [], visited_ast_nodes: 0, findings: 0, modules: 0, parse_failures: 0, limitations: [] }; const root = createJavaScriptArtifactRootNode(accumulator, snapshot); const context = { accumulator, snapshot, fileSet, analysis, directoryPaths: new Set(), root, filesByPath: new Map(files.map(f => [f.path, f])), unanalyzedFiles: undefined, fileNodes: new Map(), assetNodes: new Map(), sourceModuleNodes: new Map(), chunkNodes: new Map(), moduleNodes: new Map(), containerNodes: new Map(), applicationRootsBySource: new Map(), applicationOwnershipUnknownContainers: new Set(), applicationModuleGapSources: new Map(), applicationPossibleRootsByContainer: new Map(), applicationModuleReferencesRetained: new Set() }; addJavaScriptArtifactFiles(context); const base = analysisViewJavaScriptAnalysis(); const graph = createJavaScriptApplicationGraph({ schema: 'JavaScriptApplicationGraph', root_node_ids: [...base.graph.root_node_ids, root.node_id], nodes: [...base.graph.nodes, ...accumulator.nodes()], edges: [...base.graph.edges, ...accumulator.edges()], coverage: base.graph.coverage, limitations: [] }); const evidence = analysisViewJavaScriptEvidence(analysisViewBindJavaScriptGraphs(base, graph)); const assets = accumulator.nodes().filter(n => n.kind === 'javascript-asset'); console.log(JSON.stringify({ distinct, producedAssetNodes: assets.length, retainedPaths: assets.flatMap(a => a.observations.map(o => o.properties.path)).sort() })); for (const path of ['a.js', 'b.js']) { for (const view of [ { kind: 'javascript', collection: 'modules', projection: 'references', where: { kind: 'javascript-asset', path } }, { kind: 'javascript-facts', where: { kinds: ['javascript-asset'], paths: [path] }, property_keys: ['path'] } ]) { const result = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view }); if (!result.ok) throw result.error; const projected = result.value.normalized_result; console.log(JSON.stringify({ query: view.kind, path, rows: projected.rows.map(r => ({ node_id: r.node_id, path: r.path ?? r.properties?.path })), coverage: projected.coverage })); } } for (const path of ['a.js', 'b.js']) { const result = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view: { kind: 'item', collection: 'modules', selector: { path } } }); console.log(JSON.stringify({ query: 'item', path, ok: result.ok, ...(result.ok ? { itemPaths: result.value.normalized_result.item.observations.map(o => o.properties.path) } : { message: result.error.message }) })); } const byId = await inspectAnalysisView({ source: { kind: 'inline', evidence }, view: { kind: 'item', collection: 'modules', selector: { node_id: assets[0].node_id } } }); if (!byId.ok) throw byId.error; console.log(JSON.stringify({ query: 'item-by-node-id', itemPaths: byId.value.normalized_result.item.observations.map(o => o.properties.path) })); ``` </details> ```sh node --import ./node_modules/tsx/dist/loader.mjs --input-type=module < /tmp/rea-module-alias.mjs DISTINCT_BYTES=1 node --import ./node_modules/tsx/dist/loader.mjs --input-type=module < /tmp/rea-module-alias.mjs ``` The duplicate-content case reproduced during pre-draft investigation; the distinct-content control passed. Complete CLI/MCP transport runs and a filesystem-to-analysis run were not performed. ## Expected behavior Exact retained-path selection should find an asset at either of its observed paths, or explicitly qualify any path selection restriction instead of reporting an empty, exhausted match set. Item selection should also recognize either path, with the existing ambiguity handling when multiple modules match. The [module-reference contract](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/analysisView/javascriptModuleReferences.ts#L14-L26) describes exact filters over retained owner facts. The [file producer](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/application/javascript/JavaScriptArtifactGraphStructure.ts#L587-L625) retains each file's path on a content-digest asset. [Facts projection](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/analysisView/javascriptFactsView.ts#L83-L111) already selects the corresponding observation by its retained path. ## Impact and scope A caller filtering retained module references by an ordinary duplicate file's path receives a false negative, while item selection fails for that same path. This can hide byte-identical copied/vendored assets during static analysis. The demonstrated failure affects one path of the two-path asset; full Evidence remains intact. Different bytes remove the failure in the executed control. Broader frequency, ASAR input, and adapter-specific behavior are NOT_ENOUGH_INFO because they were not exercised. Verified workaround: use `javascript-facts` with `where.paths: ["b.js"]` and `where.kinds: ["javascript-asset"]`, then select the returned `node_id` using the module item view. Both steps succeeded and the item retained both original path observations. ## REA version 6.7.0; source `a7180370332350bc0f998fbd643888221759570e`. ## Runtime and platform Node.js v24.22.1; Linux 7.2.8-9-omarchy; x86_64. No Hopper/Ghidra or live target. ## Target and artifact context Inert JavaScript file records whose source is `export const answer = 42;` plus a newline at both paths. The control changes `b.js` to `export const answer = 43;` plus a newline. Actual production file-to-graph code creates their asset nodes; no source patch was applied. ## Interface Other ## Earliest failure stage Transport or result projection ## Relevant output or evidence Selected exact output from the executed duplicate-content probe: ```json {"distinct":false,"producedAssetNodes":1,"retainedPaths":["a.js","b.js"]} {"query":"javascript","path":"b.js","rows":[],"coverage":{"status":"empty","examined":0,"total":0,"next_offset":null,"exhausted":true}} {"query":"javascript-facts","path":"b.js","rows":[{"node_id":"jag_node_50fb137cd66bd41d9a644d32528ee2e7d06279fa213398a650ead0b13d9d3743","path":"b.js"}],"coverage":{"status":"page","examined":1,"total":1,"next_offset":null,"exhausted":true}} {"query":"item","path":"a.js","ok":true,"itemPaths":["a.js","b.js"]} {"query":"item","path":"b.js","ok":false,"message":"Invalid analysis input for inspect_analysis_view"} {"query":"item-by-node-id","itemPaths":["a.js","b.js"]} ``` The distinct-content control printed `producedAssetNodes: 2`; both module-reference queries returned one row, and both path-selected item calls returned `ok: true`. ## Typed outcome or recovery expected A successful reference/item result for the retained selected path, preserving the shared asset identity and its path evidence. ## Analysis or suspected cause Source-derived: [javascriptModulePath](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/analysisView/javascriptApplicationView.ts#L42-L56) picks the first path observation for a content-digest node. Both [module filtering](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/analysisView/javascriptApplicationView.ts#L94-L103) and [path-based item selection](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/domain/analysisView/javascriptApplicationView.ts#L274-L287) compare only that single path. The [production accumulator](https://github.com/morluto/rea/blob/a7180370332350bc0f998fbd643888221759570e/src/application/javascript/JavaScriptArtifactGraphAccumulator.ts#L20-L37) intentionally merges same-identity assets and retains their observations. The view therefore discards path multiplicity at projection time. ## Additional context The supplied open-issue inventory was searched. #6456 concerns comparison classifying several graph representations of one file as split/duplicated; this report concerns selecting two distinct retained paths of one content-deduplicated asset. #5407 concerns an unselected-source limitation, and #5595 concerns CLI analysis-lane dispatch. Open and closed searches also covered javascriptModulePath, inspect_analysis_view with identical, javascript-asset with path, and JavaScript path aliases; no matching report was found. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The executed production-boundary fixture returns an empty exhausted match set and a failed item request for an exact path demonstrably retained on an ordinary byte-identical asset; different-content controls succeed. The defect affects bounded result selection without losing the complete Evidence. - Workaround status: available - Workaround basis: The executed javascript-facts query finds the hidden path and its node_id; selecting that node_id returns the complete item with both path observations.
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#4143
No description provided.