[GH-ISSUE #3541] [Bug] Detached CommonJS receivers produce phantom public exports #1316

Closed
opened 2026-10-16 21:04:09 +00:00 by bsakaguchi · 0 comments
Owner

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

Before submitting

  • I searched open and closed issues and PRs for related reports.
  • The reproducer contains only self-owned inert source and no credentials, private paths or sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

Replacing the CommonJS public exports object leaves an old exports receiver alive, but REA still presents writes to it as exports of the current module. Node has no parse property; actual CLI and stdio MCP select parse and report a definite /count change between two versions, with zero unknowns and complete-within-inputs coverage.

Reproduction

On clean source revision ca1b2f8608c52ed721d3576ba5eeb1d66717b08c, create left/right directories with this app.cjs; change only count: 1 to count: 2 on the right:

module.exports = {};
exports.parse = () => ({ kind: "result", count: 1 });
  1. Run Node's actual require() for each file: Object.hasOwn(require(FILE), "parse") is false and typeof ...parse is undefined.
  2. Run node scripts/rea.mjs analyze-javascript-application DIR --json for each directory and save the complete Evidence.
  3. Pass those two complete Evidence records to compare-javascript-export-shapes INPUT.json --json, selecting app.cjs and parse on both sides:
{"left": "<full left Evidence object>", "right": "<full right Evidence object>", "left_module_path": "app.cjs", "right_module_path": "app.cjs", "left_export_name": "parse", "right_export_name": "parse"}

The strings above denote the full Evidence objects to insert, not accepted Evidence-ID-only input. Actual reproduction used those full objects.

Expected behavior

A provably detached receiver must not create a public module export or trusted callable. The existing same-assignment receiver regression already excludes such exports. Preserve unknown when connection or execution order cannot be established. Node's exports shortcut documentation explains that the local exports variable initially refers to module.exports; replacing one does not reconnect the other.

Impact and scope

Four straight-line replacement/rebinding cases reproduced the phantom callable. The ordinary noncallable exports.status = "enabled" case also creates an erroneous export binding. Untouched/current module.exports and explicitly reconnected exports = module.exports controls correctly export a callable; writing through a saved old alias remains missing. A separate parent reproduction confirmed the smallest case and later replacement, with both positive controls.

Actual CLI and real stdio MCP agree on identical full Evidence inputs. All loader/CLI subprocesses exited0; business assertions on the missing export separately exited1. No third-party targets or analysis engines were executed. Frequency in real third-party packages is NOT_ENOUGH_INFO; this report establishes behavior on reduced fixtures, not prevalence.

REA version

6.5.0, clean main ca1b2f8608c52ed721d3576ba5eeb1d66717b08c

Runtime and platform

Node24.18.0; macOS arm64. Windows/Linux/native providers not verified by these local checks.

Target and artifact context

Self-owned CommonJS source directories; actual Node loader controls for these generated files only.

Interface / earliest failure stage

CLI and MCP / transport or result projection.

Relevant output or evidence

Node: hasParse:false, type:"undefined". Comparison: left.status:"selected", right.status:"selected", /count: changed(1,2), summary.changed:1, summary.unknown:0, coverage.status:"complete-within-inputs".

Analysis or suspected cause

Source-derived: javascriptSemanticProjection.ts recognizes unshadowed receiver syntax; its guard checks replacement only inside the same RHS assignment chain, rather than the receiver's lifetime across statements. The downstream export/return-shape consumers trust that module link. Existing issues/PRs #3223, #1351, #1357 and open #2041/#2094/#2881 were checked; none supplies the sequential public export lifetime rule on the tested main. Proposed fix belongs in that common producing boundary, retaining its existing chain, lexical, unknown and source-provenance semantics.

Originally created by @xu91102 on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3541 ### Before submitting - [x] I searched open and closed issues and PRs for related reports. - [x] The reproducer contains only self-owned inert source and no credentials, private paths or sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior Replacing the CommonJS public exports object leaves an old `exports` receiver alive, but REA still presents writes to it as exports of the current module. Node has no `parse` property; actual CLI and stdio MCP select `parse` and report a definite `/count` change between two versions, with zero unknowns and complete-within-inputs coverage. ### Reproduction On clean source revision `ca1b2f8608c52ed721d3576ba5eeb1d66717b08c`, create left/right directories with this `app.cjs`; change only `count: 1` to `count: 2` on the right: ```js module.exports = {}; exports.parse = () => ({ kind: "result", count: 1 }); ``` 1. Run Node's actual `require()` for each file: `Object.hasOwn(require(FILE), "parse")` is false and `typeof ...parse` is `undefined`. 2. Run `node scripts/rea.mjs analyze-javascript-application DIR --json` for each directory and save the complete Evidence. 3. Pass those two complete Evidence records to `compare-javascript-export-shapes INPUT.json --json`, selecting `app.cjs` and `parse` on both sides: ```json {"left": "<full left Evidence object>", "right": "<full right Evidence object>", "left_module_path": "app.cjs", "right_module_path": "app.cjs", "left_export_name": "parse", "right_export_name": "parse"} ``` The strings above denote the full Evidence objects to insert, not accepted Evidence-ID-only input. Actual reproduction used those full objects. ### Expected behavior A provably detached receiver must not create a public module export or trusted callable. The existing same-assignment receiver regression already excludes such exports. Preserve unknown when connection or execution order cannot be established. [Node's exports shortcut documentation](https://nodejs.org/api/modules.html#exports-shortcut) explains that the local `exports` variable initially refers to `module.exports`; replacing one does not reconnect the other. ### Impact and scope Four straight-line replacement/rebinding cases reproduced the phantom callable. The ordinary noncallable `exports.status = "enabled"` case also creates an erroneous export binding. Untouched/current `module.exports` and explicitly reconnected `exports = module.exports` controls correctly export a callable; writing through a saved old alias remains missing. A separate parent reproduction confirmed the smallest case and later replacement, with both positive controls. Actual CLI and real stdio MCP agree on identical full Evidence inputs. All loader/CLI subprocesses exited0; business assertions on the missing export separately exited1. No third-party targets or analysis engines were executed. Frequency in real third-party packages is NOT_ENOUGH_INFO; this report establishes behavior on reduced fixtures, not prevalence. ### REA version 6.5.0, clean main `ca1b2f8608c52ed721d3576ba5eeb1d66717b08c` ### Runtime and platform Node24.18.0; macOS arm64. Windows/Linux/native providers not verified by these local checks. ### Target and artifact context Self-owned CommonJS source directories; actual Node loader controls for these generated files only. ### Interface / earliest failure stage CLI and MCP / transport or result projection. ### Relevant output or evidence Node: `hasParse:false`, `type:"undefined"`. Comparison: `left.status:"selected"`, `right.status:"selected"`, `/count: changed(1,2)`, `summary.changed:1`, `summary.unknown:0`, `coverage.status:"complete-within-inputs"`. ### Analysis or suspected cause Source-derived: `javascriptSemanticProjection.ts` recognizes unshadowed receiver syntax; its guard checks replacement only inside the same RHS assignment chain, rather than the receiver's lifetime across statements. The downstream export/return-shape consumers trust that module link. Existing issues/PRs #3223, #1351, #1357 and open #2041/#2094/#2881 were checked; none supplies the sequential public export lifetime rule on the tested main. Proposed fix belongs in that common producing boundary, retaining its existing chain, lexical, unknown and source-provenance semantics.
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#1316
No description provided.