[GH-ISSUE #6620] [Bug] variable array spread creates a phantom CommonJS export #4003

Closed
opened 2026-10-31 00:45:58 +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/6620

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 the valid CommonJS source const sources = [{alpha: "value", beta: "x"}]; Object.assign(exports, ...sources);, analyze-javascript-application returns an export binding named 0. Its Evidence says authority: "ast-static-analysis", state: "observed", confidence: "exact" and coverage.status: "complete". Node's actual CommonJS loader returns only alpha and beta for the same self-authored file.

The distinction is visible in two controls. An inline call spread returns the correct names in both REA and Node. Passing the array without spread returns 0 in both. Each CLI command exited 0. The false numeric name reproduced in two independent fixture-directory rounds at the source baseline; a source semantic-analyzer probe also produced the same incorrect module link.

Reproduction

Run this Python 3 block from the baseline source checkout with its locked dependencies and compiled runtime already prepared, using the Node version below. It creates three disposable CommonJS applications, isolates each child from ambient REA configuration, invokes the public CLI and independently loads only these inert self-authored files through Node. This exact block was executed; all six child commands exited 0.

python3 - <<'PY'
import json, os, pathlib, subprocess, tempfile

cases = {
    'variable': 'const sources = [{alpha: "value", beta: "x"}];\nObject.assign(exports, ...sources);\n',
    'inline': 'Object.assign(exports, ...[{alpha: "value", beta: "x"}]);\n',
    'ordinary': 'const sources = [{alpha: "value", beta: "x"}];\nObject.assign(exports, sources);\n',
}
with tempfile.TemporaryDirectory(prefix='assign-control-') as directory:
    root = pathlib.Path(directory)
    for name in ('home', 'config', 'cache', 'temp'):
        (root / name).mkdir()
    env = {'PATH': os.environ['PATH'], 'HOME': str(root / 'home'),
           'TMPDIR': str(root / 'temp'),
           'XDG_CONFIG_HOME': str(root / 'config'),
           'XDG_CACHE_HOME': str(root / 'cache')}
    for name, source in cases.items():
        app = root / name
        app.mkdir()
        (app / 'package.json').write_text(json.dumps({
            'name': 'assign-control', 'version': '1.0.0',
            'type': 'commonjs', 'main': 'main.js'}))
        (app / 'main.js').write_text(source)
        analysis = subprocess.run([
            'node', 'scripts/rea.mjs', 'analyze-javascript-application',
            str(app), '--format', 'json'], env=env, capture_output=True,
            text=True, timeout=60, check=True)
        evidence = json.loads(analysis.stdout)
        exports = []
        for node in evidence['normalized_result']['graph']['nodes']:
            for observation in node['observations']:
                if observation['properties'].get('relationship_kind') == 'commonjs-export':
                    fact = observation['evidence']
                    exports.append({
                        'name': observation['properties']['exported_name'],
                        'authority': fact['authority'], 'state': fact['state'],
                        'confidence': fact['confidence'],
                        'coverage': fact['coverage']['status']})
        runtime = subprocess.run([
            'node', '-e',
            'console.log(JSON.stringify(Object.keys(require(process.argv[1])).sort()))',
            str(app / 'main.js')], env=env, capture_output=True,
            text=True, timeout=10, check=True)
        print(json.dumps({'case': name, 'cli_exit': analysis.returncode,
              'exports': sorted(exports, key=lambda row: row['name']),
              'runtime_keys': json.loads(runtime.stdout)}))
PY

Read each printed exports list beside runtime_keys. The variable-spread case reports ["0"] versus ["alpha","beta"]; the inline-spread case reports ["alpha","beta"] on both sides; the ordinary-array case reports ["0"] on both sides.

Expected behavior

Do not publish the array's own index 0 as an exact CommonJS export for the call-spread fixture. Spreading the array passes its element object as the source argument; it does not pass the array itself. If variable-spread resolution is unsupported, retain explicit uncertainty instead of asserting a different exact name. Recovering both real names is also a valid result. Keep the inline-spread and ordinary-array controls correct.

This expectation requires conservative facts, not arbitrary JavaScript execution or general variable-spread support. The documented treatment of unsupported API shapes as unknown applies even when full resolution is unavailable.

Impact and scope

The demonstrated consequence is an incorrect named export in retained application Evidence, while the actual alpha and beta export bindings are missing. Consumers of that inventory can receive a nonexistent API name. Downstream comparison failures and real-application prevalence are NOT_ENOUGH_INFO; neither was measured. The source and source-range Evidence remain available for inspection. No graph-preserving workaround on the unchanged input was established; rewriting the fixture is a diagnostic control. The published stable runtime did not reproduce this phantom name, so the finding is scoped to the source baseline.

REA version

Source build at bcd1f95e68. Its package manifest says 6.7.0, but it contains changes after the release tag. The actual latest published rea-agents 6.7.0 tarball was separately integrity-checked and exercised with matching prepared dependencies: all three principal controls exited 0 and omitted Object.assign export bindings, so the phantom numeric export was not reproduced in that published runtime. This was not a fresh npm installation.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; arm64. No native analysis engine or external target service was used.

Target and artifact context

Three local directory artifacts, each containing package.json with type commonjs and main main.js, plus one tiny main.js. The failing case has one unchanged local const array, the native Object.assign and a connected exports receiver. It has no getters, custom iterator, array holes or preceding mutations. REA statically reads the source; only the separate Node control executes these inert fixtures.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: docs/javascript-application-workflows.md:234-240 keeps unsupported API shapes explicitly unknown; docs/javascript-application-graph.md:144-156 separates AST authority, confidence and uncertainty. ECMA-262 Object.assign copies enumerable own properties from each source argument, and ArgumentListEvaluation appends iterator values for a call spread. src/domain/javascript/javascriptIntrinsicInvocation.ts:15-41 only expands proven literal-array spreads, so variable resolution is not presumed supported.
  • Failure source: observed public-CLI export Evidence below; src/domain/javascript/javascriptCommonJsExportReceivers.ts:236-271 unwraps a surviving SpreadElement to its whole argument, :67-164 inventories an array's present own index fields, and :290-313 emits complete named exports; src/domain/javascript/javascriptSemanticProjection.ts:473-488 retains that module link, and src/application/javascript/JavaScriptModuleRelationships.ts:180-235 publishes the export observation. The independent source probe already returns exportedName 0.
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Reduced output from the executed block:

{"case":"variable","cli_exit":0,"exports":[{"name":"0","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["alpha","beta"]}
{"case":"inline","cli_exit":0,"exports":[{"name":"alpha","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"},{"name":"beta","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["alpha","beta"]}
{"case":"ordinary","cli_exit":0,"exports":[{"name":"0","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["0"]}

Two additional public-CLI controls exited 0: shadowing Object with an assign no-op produced no export bindings, and replacing module.exports before the assignment suppressed the numeric binding while retaining the explicit default replacement. Those controls distinguish intrinsic identity and receiver lifetime from this spread interpretation failure.

Typed outcome or recovery expected

Return valid application Evidence without an exact phantom 0 export for the variable-spread call. A conservative unresolved result with meaningful uncertainty is sufficient if the source arguments cannot be proven. The inline-spread control should retain alpha/beta, and the ordinary-array control should retain its real numeric export.

Analysis or suspected cause

Source-derived: literal call spreads are expanded before CommonJS export extraction, while a variable spread survives normalization. The export consumer then treats its argument array as one ordinary Object.assign source and inventories the array's own index slots. That matches the observed equivalence between the failing spread result and the valid non-spread-array result. The report does not prescribe an implementation or require expanding uncertain iterators.

Additional context

Canonical related issues were checked: #6553 concerns a shadowed property-definition method in a bundle; #3541 concerns detached CommonJS receivers and is closed; #3551 concerns renamed Webpack factory module parameters. This fixture has no bundle wrapper, shadowed intrinsic or detached receiver. #6457 loses return-field values through object spread; #3907 misindexes Vite import dependencies after an array spread. Neither has this CommonJS call-spread export-name signal. No REA source edits, broad tests, native-provider runs or MCP transport checks were performed.

Restoration check

Rerun the three-case block against the repaired runtime. The variable-spread case must stop reporting an exact numeric export absent from Node's result; report the real names or explicit uncertainty. The inline-spread case must still report alpha/beta and the ordinary-array case must still report its genuine 0. Keep the no-op shadowed Object and detached-receiver controls free of the phantom binding. These are proposed checks for a future repair; no repair was made.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded valid CommonJS source form deterministically creates a nonexistent exact export binding in retained Evidence, while discriminating controls work; no severe application failure or real-world prevalence was established.
  • Workaround status: unverified
  • Workaround basis: No supported option was shown to correct the graph while preserving the original input. Source inspection permits manual checking, and the inline rewrite is a control, but neither demonstrates restoration of the retained export facts for the unchanged artifact.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6620 ## 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 the valid CommonJS source `const sources = [{alpha: "value", beta: "x"}]; Object.assign(exports, ...sources);`, `analyze-javascript-application` returns an export binding named `0`. Its Evidence says `authority: "ast-static-analysis"`, `state: "observed"`, `confidence: "exact"` and `coverage.status: "complete"`. Node's actual CommonJS loader returns only `alpha` and `beta` for the same self-authored file. The distinction is visible in two controls. An inline call spread returns the correct names in both REA and Node. Passing the array without spread returns `0` in both. Each CLI command exited 0. The false numeric name reproduced in two independent fixture-directory rounds at the source baseline; a source semantic-analyzer probe also produced the same incorrect module link. ## Reproduction Run this Python 3 block from the baseline source checkout with its locked dependencies and compiled runtime already prepared, using the Node version below. It creates three disposable CommonJS applications, isolates each child from ambient REA configuration, invokes the public CLI and independently loads only these inert self-authored files through Node. This exact block was executed; all six child commands exited 0. ```sh python3 - <<'PY' import json, os, pathlib, subprocess, tempfile cases = { 'variable': 'const sources = [{alpha: "value", beta: "x"}];\nObject.assign(exports, ...sources);\n', 'inline': 'Object.assign(exports, ...[{alpha: "value", beta: "x"}]);\n', 'ordinary': 'const sources = [{alpha: "value", beta: "x"}];\nObject.assign(exports, sources);\n', } with tempfile.TemporaryDirectory(prefix='assign-control-') as directory: root = pathlib.Path(directory) for name in ('home', 'config', 'cache', 'temp'): (root / name).mkdir() env = {'PATH': os.environ['PATH'], 'HOME': str(root / 'home'), 'TMPDIR': str(root / 'temp'), 'XDG_CONFIG_HOME': str(root / 'config'), 'XDG_CACHE_HOME': str(root / 'cache')} for name, source in cases.items(): app = root / name app.mkdir() (app / 'package.json').write_text(json.dumps({ 'name': 'assign-control', 'version': '1.0.0', 'type': 'commonjs', 'main': 'main.js'})) (app / 'main.js').write_text(source) analysis = subprocess.run([ 'node', 'scripts/rea.mjs', 'analyze-javascript-application', str(app), '--format', 'json'], env=env, capture_output=True, text=True, timeout=60, check=True) evidence = json.loads(analysis.stdout) exports = [] for node in evidence['normalized_result']['graph']['nodes']: for observation in node['observations']: if observation['properties'].get('relationship_kind') == 'commonjs-export': fact = observation['evidence'] exports.append({ 'name': observation['properties']['exported_name'], 'authority': fact['authority'], 'state': fact['state'], 'confidence': fact['confidence'], 'coverage': fact['coverage']['status']}) runtime = subprocess.run([ 'node', '-e', 'console.log(JSON.stringify(Object.keys(require(process.argv[1])).sort()))', str(app / 'main.js')], env=env, capture_output=True, text=True, timeout=10, check=True) print(json.dumps({'case': name, 'cli_exit': analysis.returncode, 'exports': sorted(exports, key=lambda row: row['name']), 'runtime_keys': json.loads(runtime.stdout)})) PY ``` Read each printed `exports` list beside `runtime_keys`. The variable-spread case reports `["0"]` versus `["alpha","beta"]`; the inline-spread case reports `["alpha","beta"]` on both sides; the ordinary-array case reports `["0"]` on both sides. ## Expected behavior Do not publish the array's own index `0` as an exact CommonJS export for the call-spread fixture. Spreading the array passes its element object as the source argument; it does not pass the array itself. If variable-spread resolution is unsupported, retain explicit uncertainty instead of asserting a different exact name. Recovering both real names is also a valid result. Keep the inline-spread and ordinary-array controls correct. This expectation requires conservative facts, not arbitrary JavaScript execution or general variable-spread support. The documented treatment of unsupported API shapes as unknown applies even when full resolution is unavailable. ## Impact and scope The demonstrated consequence is an incorrect named export in retained application Evidence, while the actual `alpha` and `beta` export bindings are missing. Consumers of that inventory can receive a nonexistent API name. Downstream comparison failures and real-application prevalence are NOT_ENOUGH_INFO; neither was measured. The source and source-range Evidence remain available for inspection. No graph-preserving workaround on the unchanged input was established; rewriting the fixture is a diagnostic control. The published stable runtime did not reproduce this phantom name, so the finding is scoped to the source baseline. ## REA version Source build at bcd1f95e688a2ac020a9d13f9c85e0ce321cb6b6. Its package manifest says 6.7.0, but it contains changes after the release tag. The actual latest published `rea-agents` 6.7.0 tarball was separately integrity-checked and exercised with matching prepared dependencies: all three principal controls exited 0 and omitted Object.assign export bindings, so the phantom numeric export was not reproduced in that published runtime. This was not a fresh npm installation. ## Runtime and platform Node.js v24.22.1; macOS 27.0.1; arm64. No native analysis engine or external target service was used. ## Target and artifact context Three local directory artifacts, each containing package.json with type commonjs and main main.js, plus one tiny main.js. The failing case has one unchanged local const array, the native Object.assign and a connected exports receiver. It has no getters, custom iterator, array holes or preceding mutations. REA statically reads the source; only the separate Node control executes these inert fixtures. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence - Expected source: `docs/javascript-application-workflows.md:234-240` keeps unsupported API shapes explicitly unknown; `docs/javascript-application-graph.md:144-156` separates AST authority, confidence and uncertainty. [ECMA-262 Object.assign](https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-object.assign) copies enumerable own properties from each source argument, and [ArgumentListEvaluation](https://tc39.es/ecma262/multipage/ecmascript-language-expressions.html#sec-argument-lists-runtime-semantics-argumentlistevaluation) appends iterator values for a call spread. `src/domain/javascript/javascriptIntrinsicInvocation.ts:15-41` only expands proven literal-array spreads, so variable resolution is not presumed supported. - Failure source: observed public-CLI export Evidence below; `src/domain/javascript/javascriptCommonJsExportReceivers.ts:236-271` unwraps a surviving SpreadElement to its whole argument, `:67-164` inventories an array's present own index fields, and `:290-313` emits complete named exports; `src/domain/javascript/javascriptSemanticProjection.ts:473-488` retains that module link, and `src/application/javascript/JavaScriptModuleRelationships.ts:180-235` publishes the export observation. The independent source probe already returns exportedName `0`. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete Reduced output from the executed block: ```json {"case":"variable","cli_exit":0,"exports":[{"name":"0","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["alpha","beta"]} {"case":"inline","cli_exit":0,"exports":[{"name":"alpha","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"},{"name":"beta","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["alpha","beta"]} {"case":"ordinary","cli_exit":0,"exports":[{"name":"0","authority":"ast-static-analysis","state":"observed","confidence":"exact","coverage":"complete"}],"runtime_keys":["0"]} ``` Two additional public-CLI controls exited 0: shadowing Object with an assign no-op produced no export bindings, and replacing module.exports before the assignment suppressed the numeric binding while retaining the explicit default replacement. Those controls distinguish intrinsic identity and receiver lifetime from this spread interpretation failure. ## Typed outcome or recovery expected Return valid application Evidence without an exact phantom `0` export for the variable-spread call. A conservative unresolved result with meaningful uncertainty is sufficient if the source arguments cannot be proven. The inline-spread control should retain alpha/beta, and the ordinary-array control should retain its real numeric export. ## Analysis or suspected cause Source-derived: literal call spreads are expanded before CommonJS export extraction, while a variable spread survives normalization. The export consumer then treats its argument array as one ordinary Object.assign source and inventories the array's own index slots. That matches the observed equivalence between the failing spread result and the valid non-spread-array result. The report does not prescribe an implementation or require expanding uncertain iterators. ## Additional context Canonical related issues were checked: [#6553](https://github.com/morluto/rea/issues/6553) concerns a shadowed property-definition method in a bundle; [#3541](https://github.com/morluto/rea/issues/3541) concerns detached CommonJS receivers and is closed; [#3551](https://github.com/morluto/rea/issues/3551) concerns renamed Webpack factory module parameters. This fixture has no bundle wrapper, shadowed intrinsic or detached receiver. [#6457](https://github.com/morluto/rea/issues/6457) loses return-field values through object spread; [#3907](https://github.com/morluto/rea/issues/3907) misindexes Vite import dependencies after an array spread. Neither has this CommonJS call-spread export-name signal. No REA source edits, broad tests, native-provider runs or MCP transport checks were performed. ## Restoration check Rerun the three-case block against the repaired runtime. The variable-spread case must stop reporting an exact numeric export absent from Node's result; report the real names or explicit uncertainty. The inline-spread case must still report alpha/beta and the ordinary-array case must still report its genuine 0. Keep the no-op shadowed Object and detached-receiver controls free of the phantom binding. These are proposed checks for a future repair; no repair was made. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A bounded valid CommonJS source form deterministically creates a nonexistent exact export binding in retained Evidence, while discriminating controls work; no severe application failure or real-world prevalence was established. - Workaround status: unverified - Workaround basis: No supported option was shown to correct the graph while preserving the original input. Source inspection permits manual checking, and the inline rewrite is a control, but neither demonstrates restoration of the retained export facts for the unchanged artifact.
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#4003
No description provided.