[GH-ISSUE #3982] [Bug] Reference-source import reports dependencies after CommonJS require is replaced #1577

Closed
opened 2026-10-16 21:07:21 +00:00 by bsakaguchi · 1 comment
Owner

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

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

import-reference-source reports a parsed internal CommonJS dependency when the apparent loader call actually targets a replacement local function. This reproduces with a for...of declaration assigning require and with an assignment inside with followed by a call outside it. Both valid CommonJS fixtures replace the loader before the call, yet their historical-source graphs identify dep.cjs as a dependency. The overall inventory remains partial, but the erroneous relationship itself is parsed and internal.

Each replacement fixture exited 0 and printed only local with a trailing newline. Neither printed the dependency's dep-loaded marker. Both CLI imports exited 0 with empty stderr and produced this relationship:

{
  "from_path": "main.cjs",
  "to": "dep.cjs",
  "kind": "requires",
  "resolution": "internal",
  "parse_state": "parsed"
}

The uninitialized declaration control exited 0, printed dep-loaded followed by real, and retained that relationship. The ordinary-assignment control exited 0, printed only local, and correctly returned an empty relationships array. All four imports returned no parse failures and a partial inventory. Each case was executed twice during this independent verification with matching results; no broader frequency or downstream comparison impact was measured.

Reproduction

Use the checkout and runtime under Runtime or environment, with its dependencies installed and compiled runtime available. From the repository root, run the following Python reproduction. It creates isolated source and home directories, executes only the benign inline fixtures, calls the public CLI, and removes its disposable files afterward. The exact reproduction below was also executed after the initial independent run.

python3 - <<'PYTHON'
import json
import os
import pathlib
import subprocess
import tempfile

cases = {
    "for-of": 'for (var require of [() => "local"]) {}\nconsole.log(require("./dep.cjs"));\n',
    "with-write": 'with ({}) { require = () => "local"; }\nconsole.log(require("./dep.cjs"));\n',
    "uninitialized-control": 'var require;\nconsole.log(require("./dep.cjs"));\n',
    "assigned-control": 'require = () => "local";\nconsole.log(require("./dep.cjs"));\n',
}
with tempfile.TemporaryDirectory(prefix="rea-reference-verify-") as work:
    base = pathlib.Path(work)
    (base / "home").mkdir()
    child_env = dict(os.environ, HOME=str(base / "home"))
    for name, source in cases.items():
        root = base / name
        root.mkdir()
        (root / "main.cjs").write_text(source)
        (root / "dep.cjs").write_text(
            'console.log("dep-loaded");\nmodule.exports = "real";\n'
        )
        executed = subprocess.run(
            ["node", str(root / "main.cjs")], env=child_env,
            capture_output=True, text=True, timeout=10,
        )
        imported = subprocess.run(
            ["node", "scripts/rea.mjs", "import-reference-source", str(root), "--json"],
            env=child_env, capture_output=True, text=True, timeout=30,
        )
        print(name, "fixture exit:", executed.returncode,
              "stdout:", repr(executed.stdout), "stderr:", repr(executed.stderr))
        print("CLI exit:", imported.returncode, "stderr:", repr(imported.stderr))
        graph = json.loads(imported.stdout)
        print("relationships:", json.dumps(graph["relationships"]))
        print("parse_failures:", graph["parse_failures"])
        print("inventory_state:", graph["inventory_state"])
PYTHON

Expected behavior

The importer should attribute a Node requires relationship only to a call whose binding still identifies the Node loader. Both replacement cases should omit the edge to dep.cjs, as the existing ordinary-assignment control does. The uninitialized var require control should retain its valid dependency. These expectations follow the existing binding-identity regression. They do not require the importer to execute arbitrary source or guarantee that every static dependency executes at runtime.

Impact and scope

Valid CommonJS sources receive false dependency facts in the historical-source graph for these two bounded inputs. Source bytes remain available; broader downstream effects and real-world prevalence were not tested.

Workaround status: unverified. No public import option or equivalent procedure was verified that preserves the original source identity while removing only these false edges. Rewriting the input would change the historical artifact being imported.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Local CommonJS main.cjs and dep.cjs fixtures with deterministic loader replacement.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts:11 defines lexical-binding and source-mode behavior. Its assigned-loader fixture at line 51 is excluded from the dependency list at line 116, which retains the uninitialized declaration. src/domain/referenceSourceRequireBindings.ts:22 limits loader attribution to Node loader bindings. AGENTS.md:19 requires preservation of identity and uncertainty.
  • Failure source: The public CLI reproduction above independently returned the quoted parsed internal edge for both replacement cases. Source-derived admission locators are src/domain/referenceSourceRequireBindings.ts:51, which filters recognized loader writes, line 82, which excludes calls inside with, and line 178, which checks uninitialized declarations. src/domain/referenceSourceImportParsing.ts:245 emits relationships for admitted loader calls.

The executable fixture results and CLI graph fields are observations. The admission explanation is source-derived. The tested entry point uses the production CLI and requires no provider, device, or network access. The positive and negative controls distinguish loader replacement from an unsupported file mode or a blanket rule against calls named require.

Typed outcome or recovery expected

Omit the false parsed/internal requires edge while preserving the valid uninitialized-var control.

Analysis or suspected cause

Source-derived: src/domain/referenceSourceRequireBindings.ts:51,82,178 does not account for these loop and with-body writes before admitting the later call. src/domain/referenceSourceImportParsing.ts:245 emits the admitted relationship.

Additional context

Freshness check: the cited failing implementation is unchanged on origin/main at fc712b3750. The only change to a cited reproduction entry point normalizes global CLI flag syntax. The baseline reproduction was not rerun on that revision.

Restoration check: Repeat all four cases with their source unchanged. The two replacement cases must no longer assert the parsed internal Node dependency on dep.cjs. The ordinary-assignment control must continue to omit it, and the uninitialized declaration control must retain it. Preserve the existing lexical-binding behavior for shadowed parameters and the direct ESM createRequire control. Removing every require relationship would fail the positive control and would not restore this contract.

Related closed #3393 concerns assignment to require.resolve, rather than replacing the require binding with a loop declaration or a write in with. The inputs and restoration checks differ.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3982 ### 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 `import-reference-source` reports a parsed internal CommonJS dependency when the apparent loader call actually targets a replacement local function. This reproduces with a `for...of` declaration assigning `require` and with an assignment inside `with` followed by a call outside it. Both valid CommonJS fixtures replace the loader before the call, yet their historical-source graphs identify `dep.cjs` as a dependency. The overall inventory remains partial, but the erroneous relationship itself is parsed and internal. Each replacement fixture exited 0 and printed only `local` with a trailing newline. Neither printed the dependency's `dep-loaded` marker. Both CLI imports exited 0 with empty stderr and produced this relationship: ```json { "from_path": "main.cjs", "to": "dep.cjs", "kind": "requires", "resolution": "internal", "parse_state": "parsed" } ``` The uninitialized declaration control exited 0, printed `dep-loaded` followed by `real`, and retained that relationship. The ordinary-assignment control exited 0, printed only `local`, and correctly returned an empty relationships array. All four imports returned no parse failures and a partial inventory. Each case was executed twice during this independent verification with matching results; no broader frequency or downstream comparison impact was measured. ### Reproduction Use the checkout and runtime under Runtime or environment, with its dependencies installed and compiled runtime available. From the repository root, run the following Python reproduction. It creates isolated source and home directories, executes only the benign inline fixtures, calls the public CLI, and removes its disposable files afterward. The exact reproduction below was also executed after the initial independent run. ```sh python3 - <<'PYTHON' import json import os import pathlib import subprocess import tempfile cases = { "for-of": 'for (var require of [() => "local"]) {}\nconsole.log(require("./dep.cjs"));\n', "with-write": 'with ({}) { require = () => "local"; }\nconsole.log(require("./dep.cjs"));\n', "uninitialized-control": 'var require;\nconsole.log(require("./dep.cjs"));\n', "assigned-control": 'require = () => "local";\nconsole.log(require("./dep.cjs"));\n', } with tempfile.TemporaryDirectory(prefix="rea-reference-verify-") as work: base = pathlib.Path(work) (base / "home").mkdir() child_env = dict(os.environ, HOME=str(base / "home")) for name, source in cases.items(): root = base / name root.mkdir() (root / "main.cjs").write_text(source) (root / "dep.cjs").write_text( 'console.log("dep-loaded");\nmodule.exports = "real";\n' ) executed = subprocess.run( ["node", str(root / "main.cjs")], env=child_env, capture_output=True, text=True, timeout=10, ) imported = subprocess.run( ["node", "scripts/rea.mjs", "import-reference-source", str(root), "--json"], env=child_env, capture_output=True, text=True, timeout=30, ) print(name, "fixture exit:", executed.returncode, "stdout:", repr(executed.stdout), "stderr:", repr(executed.stderr)) print("CLI exit:", imported.returncode, "stderr:", repr(imported.stderr)) graph = json.loads(imported.stdout) print("relationships:", json.dumps(graph["relationships"])) print("parse_failures:", graph["parse_failures"]) print("inventory_state:", graph["inventory_state"]) PYTHON ``` ### Expected behavior The importer should attribute a Node `requires` relationship only to a call whose binding still identifies the Node loader. Both replacement cases should omit the edge to `dep.cjs`, as the existing ordinary-assignment control does. The uninitialized `var require` control should retain its valid dependency. These expectations follow the existing binding-identity regression. They do not require the importer to execute arbitrary source or guarantee that every static dependency executes at runtime. ### Impact and scope Valid CommonJS sources receive false dependency facts in the historical-source graph for these two bounded inputs. Source bytes remain available; broader downstream effects and real-world prevalence were not tested. Workaround status: unverified. No public import option or equivalent procedure was verified that preserves the original source identity while removing only these false edges. Rewriting the input would change the historical artifact being imported. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Local CommonJS main.cjs and dep.cjs fixtures with deterministic loader replacement. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: `tests/boundary/filesystem/referenceSourceShadowedRequire.test.ts:11` defines lexical-binding and source-mode behavior. Its assigned-loader fixture at line 51 is excluded from the dependency list at line 116, which retains the uninitialized declaration. `src/domain/referenceSourceRequireBindings.ts:22` limits loader attribution to Node loader bindings. `AGENTS.md:19` requires preservation of identity and uncertainty. - Failure source: The public CLI reproduction above independently returned the quoted parsed internal edge for both replacement cases. Source-derived admission locators are `src/domain/referenceSourceRequireBindings.ts:51`, which filters recognized loader writes, line 82, which excludes calls inside `with`, and line 178, which checks uninitialized declarations. `src/domain/referenceSourceImportParsing.ts:245` emits relationships for admitted loader calls. The executable fixture results and CLI graph fields are observations. The admission explanation is source-derived. The tested entry point uses the production CLI and requires no provider, device, or network access. The positive and negative controls distinguish loader replacement from an unsupported file mode or a blanket rule against calls named `require`. ### Typed outcome or recovery expected Omit the false parsed/internal requires edge while preserving the valid uninitialized-var control. ### Analysis or suspected cause Source-derived: src/domain/referenceSourceRequireBindings.ts:51,82,178 does not account for these loop and with-body writes before admitting the later call. src/domain/referenceSourceImportParsing.ts:245 emits the admitted relationship. ### Additional context Freshness check: the cited failing implementation is unchanged on origin/main at fc712b3750cbe16755c4f78c1f90fdf09fbaca31. The only change to a cited reproduction entry point normalizes global CLI flag syntax. The baseline reproduction was not rerun on that revision. Restoration check: Repeat all four cases with their source unchanged. The two replacement cases must no longer assert the parsed internal Node dependency on `dep.cjs`. The ordinary-assignment control must continue to omit it, and the uninitialized declaration control must retain it. Preserve the existing lexical-binding behavior for shadowed parameters and the direct ESM `createRequire` control. Removing every `require` relationship would fail the positive control and would not restore this contract. Related closed #3393 concerns assignment to require.resolve, rather than replacing the require binding with a loop declaration or a write in with. The inputs and restoration checks differ.
Author
Owner

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

Fixed on main in 1b9b725c4. CommonJS import discovery now consumes shared binding identity, mutation and evaluation timing instead of reinterpreting require independently. Replaced and uncertain loaders no longer become certain internal edges.

Node-oracle source and public CLI regressions cover initialized loop bindings, writes and uninitialized positive controls.

Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.

<!-- gh-comment-id:6206023993 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [1b9b725c4](https://github.com/morluto/rea/commit/1b9b725c4c83b7c345c5cb5eabdb4c57c50e1daf). CommonJS import discovery now consumes shared binding identity, mutation and evaluation timing instead of reinterpreting require independently. Replaced and uncertain loaders no longer become certain internal edges. Node-oracle source and public CLI regressions cover initialized loop bindings, writes and uninitialized positive controls. Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.
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#1577
No description provided.