Repository navigation
Description
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
analyze-javascript-application reports a resolved CommonJS dependency after a with assignment has replaced the loader. The trigger is a quoted, non-computed "__proto__" setter in the with object's Symbol.unscopables literal. Its prototype supplies require: true, so the assignment reaches the outer loader. Separately executing the owned fixture in Node prints LOCAL; REA still emits a semantic require import to dep.cjs, with resolution_status: "resolved" and local edge coverage complete.
The equivalent unquoted prototype setter and an explicit own require: true entry correctly leave that later dependency unresolved. Controls where the assignment stays on the with object retain the real dependency. Both the provided source build and a separately installed latest published package reproduce the failure. REA analyzes these fixtures statically; the separate Node runs supply the runtime comparison.
Reproduction
Use Node 24.22.1 and a built checkout at c3647ade1e557c4179a8b825a37e0c2193cd2526. Fresh-checkout preparation is npm ci followed by npm run build:cached; verification used an already prepared build and did not rerun those preparation commands. From the checkout root, run this reproduction. It creates only temporary fixtures, isolates the consumer home and environment, calls the real CLI, and removes its temporary fixtures afterward:
python3 - <<'PY'
import json
import os
from pathlib import Path
import subprocess
import tempfile
runner = Path("scripts/rea.mjs").resolve()
variants = {
"quoted": '{"__proto__": {require: true}}',
"unquoted": '{__proto__: {require: true}}',
"own-true": '{require: true}',
"own-false": '{require: false}',
"computed": '{["__proto__"]: {require: true}}',
}
with tempfile.TemporaryDirectory(prefix="rea-unscopables-") as work:
root = Path(work)
home = root / "home"
home.mkdir()
env = {"PATH": os.environ["PATH"], "HOME": str(home), "TMPDIR": str(root)}
for name, unscopables in variants.items():
app = root / name
app.mkdir()
(app / "package.json").write_text(json.dumps({
"name": "fixture", "version": "1.0.0",
"main": "index.cjs", "type": "commonjs",
}))
(app / "dep.cjs").write_text('module.exports = "OUTER";\n')
(app / "index.cjs").write_text(
'with ({require: null, [Symbol.unscopables]: ' + unscopables + '}) {\n'
' require = () => "LOCAL";\n'
'}\nconsole.log(require("./dep.cjs"));\n'
)
runtime = subprocess.run(
["node", str(app / "index.cjs")], env=env,
capture_output=True, text=True, timeout=30, check=True,
)
analysis = subprocess.run(
["node", str(runner), "analyze-javascript-application", str(app),
"--format", "json", "--full-output"], env=env,
capture_output=True, text=True, timeout=60, check=True,
)
result = json.loads(analysis.stdout)
assert result["ok"] is True
edges = result["data"]["normalized_result"]["graph"]["edges"]
requires = [
{"specifier": e["properties"]["specifier"],
"resolved_path": e["properties"]["resolved_path"],
"resolution_status": e["properties"]["resolution_status"],
"coverage": e["evidence"]["coverage"]["status"]}
for e in edges
if e.get("properties", {}).get("module_link_kind") == "require"
]
print(name, "runtime=" + runtime.stdout.strip(),
"require_edges=" + json.dumps(requires))
PYThe quoted case's generated index.cjs is:
with ({require: null, [Symbol.unscopables]: {"__proto__": {require: true}}}) {
require = () => "LOCAL";
}
console.log(require("./dep.cjs"));The companion dep.cjs exports "OUTER". The manifest selects CommonJS and index.cjs as its entry point. All runtime and analysis subprocesses exited successfully. The five-case matrix reproduced twice through the source CLI, including the exact script above, and once through the separately installed published package.
Expected behavior
A write that can reach the outer require must prevent the later call from being identified as an unchanged CommonJS loader. The quoted and unquoted non-computed prototype setters should produce equivalent conservative dependency handling. Omitting the resolved dependency or retaining the unresolved call is sufficient; exact execution modeling is unnecessary.
Preserve the positive controls: an own false unscopables entry leaves the with object's binding available, and a computed ["__proto__"] key creates an own data property rather than setting the unscopables prototype. In both controls the assignment stays on the with object, and the later real CommonJS dependency should remain resolved.
Impact and scope
P2: a bounded, consequential static-analysis error for a valid non-strict CommonJS source shape. The caller receives a false resolved dependency with high-confidence relationship evidence and complete local edge coverage. The analysis also retains a dynamic-scope unknown elsewhere; this report does not claim globally complete coverage or runtime execution by REA.
No external application incident, prevalence measurement or severe downstream harm was established. No input-preserving public workaround was verified. Replacing the quoted setter with its unquoted spelling demonstrates the working control but changes the analyzed artifact. This is an analysis correctness defect; no security boundary is crossed.
REA version
Source build: rea-agents 6.8.0 at c3647ade1e557c4179a8b825a37e0c2193cd2526. Separately installed npm rea-agents@6.8.0, reporting 6.8.0, also reproduces. npm view rea-agents version returned 6.8.0.
Fetched origin/main at 71f680d7c1aa7a45ba0325fe87b104744b0c105f retains the same affected implementation. That revision was inspected, not rebuilt or executed.
Runtime and platform
Node.js v24.22.1; Darwin arm64. No Hopper, Ghidra or other native provider was needed. Other platforms and MCP were not exercised.
Target and artifact context
Owned synthetic directory applications, each containing a manifest, a non-strict CommonJS index.cjs, and dep.cjs. REA parsed their inert UTF-8 source through the normal isolated analysis worker. Separate Node execution was limited to these owned fixtures.
Interface
CLI
Earliest failure stage
Native analysis or reconstruction
Relevant output or evidence
The exact final reproduction printed:
quoted runtime=LOCAL require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]
unquoted runtime=LOCAL require_edges=[]
own-true runtime=LOCAL require_edges=[]
own-false runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]
computed runtime=OUTER require_edges=[{"specifier": "./dep.cjs", "resolved_path": "dep.cjs", "resolution_status": "resolved", "coverage": "complete"}]
The published package produced the same five semantic-edge outcomes. The incorrect quoted-case semantic relationship contains:
{
"relation": "imports",
"properties": {
"module_link_kind": "require",
"specifier": "./dep.cjs",
"resolved_path": "dep.cjs",
"resolution_status": "resolved"
},
"evidence": {
"authority": "static-relationship-inference",
"state": "inferred",
"confidence": "high",
"coverage": {"status": "complete", "truncated": false, "omitted_count": 0, "limits": []}
}
}This excerpt selects fields from the real CLI JSON; the complete edge additionally retains artifact identity, source location and static-analysis limitations. A separate static kind: "require" relationship also resolves dep.cjs in this case. Generic limitations about execution do not justify identifying a demonstrably replaced function as the CommonJS loader.
Expected source: the existing module-link controls omit uncertain or shadowed require links. The origin guard rejects prior ambient loader writes, and the assignment owner distinguishes captured writes from possible outer writes. These are source-derived contract references; the CLI and Node outputs above are direct observations.
The existing focused suite npm run test:focused -- src/domain/javascript/javascriptSemanticAnalysis.require_links.test.ts passed all 33 tests. It does not contain the quoted-prototype write fixture. No whole-suite result is claimed.
Typed outcome or recovery expected
For the unchanged quoted fixture, stop asserting the resolved semantic require relationship to dep.cjs; retaining an unresolved static call is acceptable. Keep the unquoted and own-true controls unresolved, and retain the resolved dependency in the own-false and computed-key controls. A general dynamic-scope limitation elsewhere in the result does not repair the false local loader inference.
Analysis or suspected cause
Source-derived: semanticWithPrototypeKind recognizes a non-computed prototype setter only when its key is an identifier. The quoted setter therefore reaches its ordinary return. semanticWithRequireLookup then treats the missing own unscopables.require as proof of capture, despite the custom prototype's inherited true value. The neighboring property helpers already normalize string-literal keys.
addAssignment discards captured writes. As a result, semanticRequireOrigin finds no recorded ambient replacement at the later call. The semantic projection emits a require link, and module relationship construction resolves it to the companion file.
The classifier, assignment owner, origin guards, semantic projection, worker and application workflow files inspected here are unchanged between the baseline and fetched main. No repair was implemented or tested.
Additional context
Open and closed issue/PR searches included unscopables, Symbol.unscopables, the prototype helper name, quoted prototype keys, loader replacement and with-scope binding terms. No current open exact duplicate was found.
Closed #3982 and #4072 concern earlier loader replacement handling. The issue closure records its shared-ownership fix; the PR was closed as superseded. Its explicit unscopables-entry cases do not cover a quoted custom prototype setter. Closed #2890 and #2892 address local shadowing. Closed #7418 and merged #7914 address uncertain member writes to outer objects. This fixture instead loses a scalar loader assignment because its lookup is incorrectly classified as captured.
Read-only calls inside a with body alone are not the decisive failure: both quoted and unquoted custom-prototype read-only controls remain unresolved. The reproduction above places a loader write inside with and its later call outside, exposing the concrete consequence without requiring exact resolution of the with-body read.
Activity