[GH-ISSUE #7318] [Bug] A prior unmodified require.resolve call invalidates the next resolver edge #4348

Closed
opened 2026-10-31 00:49:06 +00:00 by bsakaguchi · 0 comments
Owner

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

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

For a CommonJS file containing two consecutive require.resolve() calls with literal relative paths, analyze-javascript-application resolves the first call but records the second as operation: "load" with a null specifier, resolution status and resolved path. The second call's original expression remains available, but its structured resolver relationship is lost. The file also receives one unknown finding and partial semantics coverage. Neither call replaces or exposes the loader.

Reproduction

Use an already prepared checkout with its compiled runtime and Node on PATH. Set REA to that checkout's absolute path. The following reduced fixture and supported CLI command were executed; two earlier runs with additional controls produced the same failure. All three CLI runs exited 0, including cleanup. The separate Node control resolves filenames without executing any fixture module.

work="$(mktemp -d)"
mkdir -p "$work/app" "$work/home/config" "$work/home/cache" "$work/tmp"
printf '%s\n' '{"name":"resolve-fixture","private":true}' > "$work/app/package.json"
printf '%s\n' 'module.exports = "first";' > "$work/app/first.js"
printf '%s\n' 'module.exports = "second";' > "$work/app/second.js"
cat > "$work/app/main.cjs" <<'JS'
require.resolve("./first.js");
require.resolve("./second.js");
JS
printf '%s\n' 'require.resolve("./first.js");' > "$work/app/single.cjs"
(
  cd "$work/app"
  node -e 'const p=require("node:path"),{createRequire}=require("node:module");const r=createRequire(p.join(process.cwd(),"main.cjs"));const original=r.resolve;console.log(JSON.stringify({first:p.basename(r.resolve("./first.js")),second:p.basename(r.resolve("./second.js")),identityPreserved:original===r.resolve}));'
)
HOME="$work/home" XDG_CONFIG_HOME="$work/home/config" XDG_CACHE_HOME="$work/home/cache" TMPDIR="$work/tmp" \
  node "$REA/scripts/rea.mjs" analyze-javascript-application "$work/app" --format json --full-output > "$work/result.json" 2> "$work/progress.jsonl"
node - "$work/result.json" <<'JS'
const fs = require('node:fs');
const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).data.normalized_result;
const rows = result.graph.edges.filter(e => e.relation === 'imports' && e.properties.kind === 'require').map(e => ({source:e.evidence.location.value.source,line:e.evidence.location.value.start.line,operation:e.properties.operation,specifier:e.properties.specifier,resolved_path:e.properties.resolved_path,resolution_status:e.properties.resolution_status}));
rows.sort((a,b) => a.source.localeCompare(b.source) || a.line-b.line);
console.log(JSON.stringify(rows,null,2));
for (const n of result.graph.nodes.filter(n => n.kind === 'javascript-asset')) for (const o of n.observations) if (['main.cjs','single.cjs'].includes(o.properties.path)) console.log(JSON.stringify({source:o.properties.path,parse_status:o.properties.parse_status,semantics:o.properties.analysis_coverage.semantics}));
JS

Expected behavior

Both intact native resolver calls should retain operation: "resolve", their literal specifiers and the corresponding resolved paths. The repeated-call file should retain complete semantics coverage, as the single-call control does. REA's documented lookup/load distinction applies to proven require.resolve() calls (docs/javascript-artifact-reconstruction.md:933-941), and the member-effect producer explicitly exempts known commonjs-loader and create-require resolver invocations from receiver escape (src/domain/javascript/javascriptSemanticMemberMutations.ts:1477-1495).

The expectation is limited to this proven native resolver with literal arguments and no prior mutation or escape. Preserve conservative uncertainty for genuinely replaced or escaped loaders; they are separate negative controls.

Impact and scope

The demonstrated result loses the later lookup's structured target and incorrectly degrades the file's coverage. The source expression and inventoried files remain available, and analysis completes. The sequential fixture reproduced in all three public CLI runs. Real-project prevalence and downstream application-ownership effects were not measured. A result-preserving REA workaround for the unchanged input was not verified.

REA version

6.8.0; prepared source checkout at revision e89192653402222c5b099216f0d83a2d1bceace0, using its existing compiled runtime.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1, build 26A434; arm64. No external analysis provider.

Target and artifact context

A local directory containing the authored CommonJS fixtures, two inert dependency files and a package manifest. REA parses the JavaScript as text. No ASAR, Electron runtime, custom loader or target-module execution is involved.

Interface

CLI

Earliest failure stage

Artifact or evidence registration

Relevant output or evidence

Observed Node control:

{"first":"first.js","second":"second.js","identityPreserved":true}

Observed CLI edge projection:

[
  {"source":"main.cjs","line":1,"operation":"resolve","specifier":"./first.js","resolved_path":"first.js","resolution_status":"resolved"},
  {"source":"main.cjs","line":2,"operation":"load","specifier":null,"resolved_path":null,"resolution_status":null},
  {"source":"single.cjs","line":1,"operation":"resolve","specifier":"./first.js","resolved_path":"first.js","resolution_status":"resolved"}
]

Observed file coverage:

{"source":"main.cjs","parse_status":"partial","semantics":{"unknown_findings":1,"uncertainty":["dynamic-findings"]}}
{"source":"single.cjs","parse_status":"complete","semantics":{"unknown_findings":0,"uncertainty":[]}}

Additional controls, each analyzed through the same public CLI in two runs:

Source shape Result
Read require.resolve into a constant, then call it through require.resolve once Literal target resolves
import { createRequire } from "node:module"; const load = createRequire(import.meta.url); followed by two load.resolve calls for these targets Both targets resolve
Assign a replacement to require.resolve, then call it Uncertain load with null specifier
opaque(require) followed by a resolver call Uncertain load with null specifier
  • Expected source: the reconstruction contract and explicit known-resolver exemption cited under Expected behavior; the resolve-only Node oracle confirms both local target names and stable resolver identity.
  • Failure source: the public CLI edge and coverage projections above, obtained from the complete JSON result of the executed command.
  • Evidence provenance: observed CLI, Node-oracle and unchanged compiled-module guard diagnostics; the producing source explanation below is static reasoning.
  • Local verification: reproduced.
  • Reproduction completeness: complete for the stated source CLI and local fixture; MCP and a separately installed package were not run.

Typed outcome or recovery expected

For the unchanged fixture, both edges should resolve to their corresponding files with exact specifiers and no spurious unknown finding. Keep the replacement and opaque-escape controls uncertain; simply disabling member-change guards would not restore the contract.

Analysis or suspected cause

Observed guard diagnostics distinguish this from an unknown-loader case: after indexing, both calls retain commonjs-loader identity and semanticRequireCallAt returns operation: "resolve", while semanticModuleRequireCallAt is false. However, the ambient effect state contains an escape at the first call with path ["global", "require", null]. The member-effects query at the second call reports that escape with timing before; the first call sees only after effects.

Source-derived explanation: src/domain/javascript/javascriptSemanticAmbientWrites.ts:675-686 collects the member receiver as escaped without the known-resolver exemption. The separate member-mutation exemption does not remove that ambient effect. src/domain/javascript/javascriptStaticAnalysisCalls.ts:293-305 treats the preceding escape as a changed .resolve member; its requireCallSpecifier guard at lines 267-276 discards the specifier, changes the operation to load and increments unknown findings. These observations support a missed exemption in ambient receiver tracking rather than genuine uncertainty introduced by the first native lookup.

Additional context

Related closed reports address different contracts: #3393 concerns an actually overwritten resolver, #6678 concerns search-path options, and #6892 concerns optional resolver syntax in historical import. Open #4915 concerns coverage attribution from Electron call-path expansion, whereas this fixture loses a structured resolver edge through the settled member-change guard. No last-known-good revision or real-application impact is established. Prepared with AI assistance.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Valid consecutive native resolver calls reproducibly lose the later lookup's structured target and add false partial coverage, while the source remains available and the CLI completes. No severe downstream incident was demonstrated.
  • Workaround status: unverified
  • Workaround basis: Single-call and createRequire controls discriminate the failure but do not establish a supported REA workflow that restores the original artifact's structured result without changing its contents.
Originally created by @coygeek on GitHub (Oct 26, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7318 ## 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 For a CommonJS file containing two consecutive `require.resolve()` calls with literal relative paths, `analyze-javascript-application` resolves the first call but records the second as `operation: "load"` with a null specifier, resolution status and resolved path. The second call's original expression remains available, but its structured resolver relationship is lost. The file also receives one unknown finding and partial semantics coverage. Neither call replaces or exposes the loader. ## Reproduction Use an already prepared checkout with its compiled runtime and Node on PATH. Set `REA` to that checkout's absolute path. The following reduced fixture and supported CLI command were executed; two earlier runs with additional controls produced the same failure. All three CLI runs exited 0, including cleanup. The separate Node control resolves filenames without executing any fixture module. ```sh work="$(mktemp -d)" mkdir -p "$work/app" "$work/home/config" "$work/home/cache" "$work/tmp" printf '%s\n' '{"name":"resolve-fixture","private":true}' > "$work/app/package.json" printf '%s\n' 'module.exports = "first";' > "$work/app/first.js" printf '%s\n' 'module.exports = "second";' > "$work/app/second.js" cat > "$work/app/main.cjs" <<'JS' require.resolve("./first.js"); require.resolve("./second.js"); JS printf '%s\n' 'require.resolve("./first.js");' > "$work/app/single.cjs" ( cd "$work/app" node -e 'const p=require("node:path"),{createRequire}=require("node:module");const r=createRequire(p.join(process.cwd(),"main.cjs"));const original=r.resolve;console.log(JSON.stringify({first:p.basename(r.resolve("./first.js")),second:p.basename(r.resolve("./second.js")),identityPreserved:original===r.resolve}));' ) HOME="$work/home" XDG_CONFIG_HOME="$work/home/config" XDG_CACHE_HOME="$work/home/cache" TMPDIR="$work/tmp" \ node "$REA/scripts/rea.mjs" analyze-javascript-application "$work/app" --format json --full-output > "$work/result.json" 2> "$work/progress.jsonl" node - "$work/result.json" <<'JS' const fs = require('node:fs'); const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).data.normalized_result; const rows = result.graph.edges.filter(e => e.relation === 'imports' && e.properties.kind === 'require').map(e => ({source:e.evidence.location.value.source,line:e.evidence.location.value.start.line,operation:e.properties.operation,specifier:e.properties.specifier,resolved_path:e.properties.resolved_path,resolution_status:e.properties.resolution_status})); rows.sort((a,b) => a.source.localeCompare(b.source) || a.line-b.line); console.log(JSON.stringify(rows,null,2)); for (const n of result.graph.nodes.filter(n => n.kind === 'javascript-asset')) for (const o of n.observations) if (['main.cjs','single.cjs'].includes(o.properties.path)) console.log(JSON.stringify({source:o.properties.path,parse_status:o.properties.parse_status,semantics:o.properties.analysis_coverage.semantics})); JS ``` ## Expected behavior Both intact native resolver calls should retain `operation: "resolve"`, their literal specifiers and the corresponding resolved paths. The repeated-call file should retain complete semantics coverage, as the single-call control does. REA's documented lookup/load distinction applies to proven `require.resolve()` calls (`docs/javascript-artifact-reconstruction.md:933-941`), and the member-effect producer explicitly exempts known `commonjs-loader` and `create-require` resolver invocations from receiver escape (`src/domain/javascript/javascriptSemanticMemberMutations.ts:1477-1495`). The expectation is limited to this proven native resolver with literal arguments and no prior mutation or escape. Preserve conservative uncertainty for genuinely replaced or escaped loaders; they are separate negative controls. ## Impact and scope The demonstrated result loses the later lookup's structured target and incorrectly degrades the file's coverage. The source expression and inventoried files remain available, and analysis completes. The sequential fixture reproduced in all three public CLI runs. Real-project prevalence and downstream application-ownership effects were not measured. A result-preserving REA workaround for the unchanged input was not verified. ## REA version 6.8.0; prepared source checkout at revision `e89192653402222c5b099216f0d83a2d1bceace0`, using its existing compiled runtime. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1, build 26A434; arm64. No external analysis provider. ## Target and artifact context A local directory containing the authored CommonJS fixtures, two inert dependency files and a package manifest. REA parses the JavaScript as text. No ASAR, Electron runtime, custom loader or target-module execution is involved. ## Interface CLI ## Earliest failure stage Artifact or evidence registration ## Relevant output or evidence Observed Node control: ```json {"first":"first.js","second":"second.js","identityPreserved":true} ``` Observed CLI edge projection: ```json [ {"source":"main.cjs","line":1,"operation":"resolve","specifier":"./first.js","resolved_path":"first.js","resolution_status":"resolved"}, {"source":"main.cjs","line":2,"operation":"load","specifier":null,"resolved_path":null,"resolution_status":null}, {"source":"single.cjs","line":1,"operation":"resolve","specifier":"./first.js","resolved_path":"first.js","resolution_status":"resolved"} ] ``` Observed file coverage: ```json {"source":"main.cjs","parse_status":"partial","semantics":{"unknown_findings":1,"uncertainty":["dynamic-findings"]}} {"source":"single.cjs","parse_status":"complete","semantics":{"unknown_findings":0,"uncertainty":[]}} ``` Additional controls, each analyzed through the same public CLI in two runs: | Source shape | Result | | --- | --- | | Read `require.resolve` into a constant, then call it through `require.resolve` once | Literal target resolves | | `import { createRequire } from "node:module"; const load = createRequire(import.meta.url);` followed by two `load.resolve` calls for these targets | Both targets resolve | | Assign a replacement to `require.resolve`, then call it | Uncertain load with null specifier | | `opaque(require)` followed by a resolver call | Uncertain load with null specifier | - Expected source: the reconstruction contract and explicit known-resolver exemption cited under Expected behavior; the resolve-only Node oracle confirms both local target names and stable resolver identity. - Failure source: the public CLI edge and coverage projections above, obtained from the complete JSON result of the executed command. - Evidence provenance: observed CLI, Node-oracle and unchanged compiled-module guard diagnostics; the producing source explanation below is static reasoning. - Local verification: reproduced. - Reproduction completeness: complete for the stated source CLI and local fixture; MCP and a separately installed package were not run. ## Typed outcome or recovery expected For the unchanged fixture, both edges should resolve to their corresponding files with exact specifiers and no spurious unknown finding. Keep the replacement and opaque-escape controls uncertain; simply disabling member-change guards would not restore the contract. ## Analysis or suspected cause Observed guard diagnostics distinguish this from an unknown-loader case: after indexing, both calls retain `commonjs-loader` identity and `semanticRequireCallAt` returns `operation: "resolve"`, while `semanticModuleRequireCallAt` is false. However, the ambient effect state contains an escape at the first call with path `["global", "require", null]`. The member-effects query at the second call reports that escape with timing `before`; the first call sees only `after` effects. Source-derived explanation: `src/domain/javascript/javascriptSemanticAmbientWrites.ts:675-686` collects the member receiver as escaped without the known-resolver exemption. The separate member-mutation exemption does not remove that ambient effect. `src/domain/javascript/javascriptStaticAnalysisCalls.ts:293-305` treats the preceding escape as a changed `.resolve` member; its `requireCallSpecifier` guard at lines 267-276 discards the specifier, changes the operation to `load` and increments unknown findings. These observations support a missed exemption in ambient receiver tracking rather than genuine uncertainty introduced by the first native lookup. ## Additional context Related closed reports address different contracts: [#3393](https://github.com/morluto/rea/issues/3393) concerns an actually overwritten resolver, [#6678](https://github.com/morluto/rea/issues/6678) concerns search-path options, and [#6892](https://github.com/morluto/rea/issues/6892) concerns optional resolver syntax in historical import. Open [#4915](https://github.com/morluto/rea/pull/4915) concerns coverage attribution from Electron call-path expansion, whereas this fixture loses a structured resolver edge through the settled member-change guard. No last-known-good revision or real-application impact is established. Prepared with AI assistance. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Valid consecutive native resolver calls reproducibly lose the later lookup's structured target and add false partial coverage, while the source remains available and the CLI completes. No severe downstream incident was demonstrated. - Workaround status: unverified - Workaround basis: Single-call and createRequire controls discriminate the failure but do not establish a supported REA workflow that restores the original artifact's structured result without changing its contents.
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#4348
No description provided.