[GH-ISSUE #7527] [Bug] A recovered Number boundary has no semantic path to its result #4472

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

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

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 const n = Number(raw); return n;, the public analyze-javascript-application and trace-javascript-semantics CLI commands recover the coercion but cannot trace its influence to n, or recover that coercion when tracing the provenance of the later n read.

Using the unchanged application Evidence, node scripts/rea.mjs trace-javascript-semantics <request.json> --json with seed: {kind: "boundary-field", field: "Number"} and direction: "forward-influence" returns only the Number boundary: status: "found", one node, zero relations, zero relevant unknowns. The result still reports partial coverage. Starting at parameter raw reaches that boundary and the call argument, but never reaches n or its read. Backward provenance from the return n read omits both Number and raw, with a generic unsupported-call unknown.

Enabling include_ambiguous_dynamic_edges does not recover this connection in either direction. Backward provenance directly from the Number boundary does reach raw, establishing that boundary selection and input linkage work.

Reproduction

  1. Use source revision 65f1c447344a423fa2785847dbcea1e9a9e2454a, its locked dependencies and a built runtime, with Node.js 24.22.1 on PATH. The verification used the prepared build; dependency installation and building were not rerun.
  2. From the repository root, run the following block. It creates the entire inert fixture, isolates HOME/XDG state, invokes the public CLI, and prints the graph and trace comparisons. It writes the exact returned Evidence and stderr into the chosen scratch directory. REA does not execute the fixture.
repro_root=$(mktemp -d)
python3 - "$repro_root" <<'PYREPRO'
import json, os, pathlib, subprocess, sys
root = pathlib.Path(sys.argv[1]).resolve()
env = os.environ.copy()
for name in ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME']:
    directory = root / name.lower()
    directory.mkdir(parents=True, exist_ok=True)
    env[name] = str(directory)
app = root / 'app'
app.mkdir(exist_ok=True)
source = 'function parse(raw) {\n  const n = Number(raw);\n  return n;\n}\nmodule.exports = parse;\n'
(app / 'main.js').write_text(source)
def cli(name, command, argument):
    run = subprocess.run(['node', 'scripts/rea.mjs', command, str(argument), '--json'],
                         env=env, capture_output=True, text=True, check=True, timeout=60)
    (root / (name + '.json')).write_text(run.stdout)
    (root / (name + '.stderr')).write_text(run.stderr)
    return json.loads(run.stdout)
application = cli('application', 'analyze-javascript-application', app)
graph = application['normalized_result']['semantic_graph']
boundary = next(n for n in graph['nodes'] if n['kind'] == 'boundary')
raw = next(n for n in graph['nodes'] if n['kind'] == 'parameter' and n['label'] == 'raw')
read = next(n for n in graph['nodes'] if n['identity']['role_key'].endswith('read:n'))
incident = [e for e in graph['relations'] if boundary['node_id'] in
            [e['source_node_id'], e['target_node_id']]]
print(json.dumps({'boundary_properties': boundary['properties'],
                  'incident_relations': [e['relation'] for e in incident]}))
assert len(incident) == 1 and incident[0]['relation'] == 'coerces'
for name, seed, direction in [
    ('boundary-forward', {'kind': 'boundary-field', 'field': 'Number'}, 'forward-influence'),
    ('raw-forward', {'kind': 'semantic-node', 'node_id': raw['node_id']}, 'forward-influence'),
    ('read-backward', {'kind': 'semantic-node', 'node_id': read['node_id']}, 'backward-provenance'),
    ('boundary-backward', {'kind': 'boundary-field', 'field': 'Number'}, 'backward-provenance')]:
    for candidates in [False, True]:
        label = name + ('-candidates' if candidates else '')
        request = root / (label + '-input.json')
        request.write_text(json.dumps({'application': application, 'query': {
            'seed': seed, 'direction': direction,
            'include_ambiguous_dynamic_edges': candidates}}))
        result = cli(label, 'trace-javascript-semantics', request)['normalized_result']
        print(json.dumps({'query': label, 'status': result['status'],
                          'coverage': result['coverage'], 'summary': result['summary'],
                          'nodes': [[n['kind'], n['label']] for n in result['nodes']],
                          'unknown_details': [u['detail'] for u in result['unknowns']]}))
        if name == 'boundary-forward':
            assert result['nodes'] == [boundary] and not result['relations']
        if name == 'raw-forward':
            assert not any(n['label'] == 'n' for n in result['nodes'])
        if name == 'read-backward':
            assert boundary['node_id'] not in [n['node_id'] for n in result['nodes']]
            assert raw['node_id'] not in [n['node_id'] for n in result['nodes']]
        if name == 'boundary-backward':
            assert raw['node_id'] in [n['node_id'] for n in result['nodes']]
assert (app / 'main.js').read_text() == source
PYREPRO
  1. Compare the forward boundary trace with the result read's backward trace. The Number boundary has only its incoming coercion edge; admitting candidate edges does not reconnect it. The final assertions also check that boundary-backward reaches raw and that the input source bytes did not change.

The failure reproduced in two independently run analyses of the same inline fixture. The second analysis ran all four queries with candidate edges disabled and enabled; its nine public commands exited zero and the assertions passed.

Expected behavior

A recognized coercion with a retained result binding should preserve that static influence relationship: forward tracing from Number or raw should reach its result n and the later read, and backward provenance from that read should include the coercion and its input. This does not require evaluating the result or proving execution. Preserve applicable unknowns and partial coverage.

The documented semantic relation workflow covers boundary-field selection, backward provenance, forward influence, complete reachable subgraphs, coercion boundaries, and argument/return flow. The omission occurs while constructing the graph used by that workflow: the result relationship is recovered by analysis but is absent from its projected relations. The expected contract is more specific than traversing all edges of an already incomplete graph.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: An analyst's supported static trace omits a recognized coercion from result provenance and omits the result from its forward influence. The failure is repeatable on the inline unshadowed Number call. Partial coverage remains visible; no severe downstream incident or data loss was demonstrated.
  • Workaround status: unverified
  • Workaround basis: Selecting Number directly and tracing backward recovers raw, but requires already knowing the boundary and does not restore a path from the result to that boundary. Candidate-edge opt-in was tested and did not restore either missing connection. No complete query-only workaround was established.

Other parsing, validation, nested-call, resource-handle and real-application shapes were not tested. This report concerns the single coercion-result relationship above. No security boundary is crossed.

REA version

Exact node scripts/rea.mjs --version output:

6.8.0

Source baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a. This was a built source checkout; the published npm artifact was not independently run.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1; Darwin arm64. No external analysis provider was required.

Target and artifact context

One five-line CommonJS JavaScript source file with no package manifest, reproduced inline above. Analysis reads it as source text. Trace requests carry the original complete application Evidence returned by the analysis command.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed summaries from the public CLI, with default candidate exclusion:

query               status     coverage  nodes  relations  relevant_unknowns
boundary-forward    found      partial   1      0          0
raw-forward         found      partial   4      3          1
read-backward       ambiguous  partial   3      2          1
boundary-backward   found      partial   3      2          1
  • boundary-forward returns only boundary:Number.
  • raw-forward returns the raw parameter, its read, argument 0 and the Number boundary. Its unknown says Binding raw has no constant initializer. Unknown value at binding.
  • read-backward returns binding n, its read and its variable definition. Its unknown says Unsupported CallExpression value. Unknown value at binding.
  • With candidates admitted, read-backward adds the call-site (four nodes, three relations) but still omits Number and raw; the other three query summaries are unchanged.
  • The complete retained graph has 18 nodes and 11 relations. The Number boundary has exactly one incident relation: the resolved raw-to-Number coerces edge. Both the result binding n and the call-site at Number's source range are present.

Source checks at the stated baseline:

  • Expected source: docs/javascript-application-workflows.md:181-199 documents boundary method-field identity, both trace directions, reachable subgraphs, argument/return flow and coercion boundaries. Lines 208-212 restrict built-in effects when globals may be written; this fixture has no such writes or local Number binding. Lines 260-269 preserve unknowns and distinguish static relations from execution. The repository's AGENTS.md Architecture and Cleanup section requires identity, omission, uncertainty and source evidence to survive boundaries.
  • Recovered fact: src/domain/javascript/javascriptSemanticIr.ts:353-362 defines the boundary's source/result binding fields. src/domain/javascript/javascriptSemanticDataEffects.ts:370-413 recognizes this supported unshadowed global call and captures both bindings. A separate executed source-owner probe on the exact same fixture returned resolution: "complete", sourceBindingId: "scope:function:0:60:binding:raw" and resultBindingId: "scope:function:0:60:binding:n".
  • Failure source: src/application/javascript/JavaScriptSemanticGraphDataProjection.ts:218-257 creates the source-to-boundary relation but never uses the recovered result binding or links the boundary to its call-site. Boundary-node construction at lines 259-278 preserves its identity and field. The public graph's absent outgoing edge confirms this is not just a trace filter excluding an existing connection.
  • Public entry and guards: src/application/javascript/JavaScriptSemanticTraceService.ts:21-68 parses application Evidence before querying. src/domain/javascript/javascriptSemanticQuery.ts:115-179 applies candidate/relation admission and directed traversal. The reproduction uses unchanged valid Evidence and no relation restriction.

npm run test:local -- src/application/javascript/JavaScriptSemanticGraphResources.test.ts passed all 12 tests. Those tests cover retained relation families and partial coverage, without checking this boundary-to-result connection. No temporary test or checkout modification was needed.

Typed outcome or recovery expected

On the unchanged fixture, forward influence from the recovered Number boundary should reach the retained n result and its later read; backward provenance from that read should recover Number and raw. Preserve the successful boundary-backward input path and existing uncertainty. If this supported result relationship cannot be represented, expose that specific missing output relationship as a typed limitation rather than leaving the boundary's forward trace with no relevant unknowns. No implementation or post-fix restoration was tested.

Analysis or suspected cause

Source-derived explanation: the boundary producer retains resultBindingId, but the boundary graph projector only emits its input relation. Generic candidate call-result linkage can reach the call-site from n with opt-in; it cannot cross to the separate boundary node. The observed graph therefore lacks a usable connection regardless of candidate admission. No particular new relation name or schema change is prescribed.

Additional context

Open #7500 is related but has a different closure signal: it requires following binary/template operands into produced values. This fixture has neither expression kind. Fixing those operator paths alone would leave the recovered Number boundary disconnected from its result. A boundary-field forward trace reaching n and its read independently tests this defect.

Closed #3170 repaired boundary seed lookup; lookup succeeds here. Closed #6193 and merged #6206 concern reassignment inputs. Closed #5585, #5648 and merged #5651 concern direct member-read flow into returns and arguments. Those restoration checks do not cover a recognized boundary's retained output binding. The other plausible open/closed issue and PR hits were checked without finding this same contract and closure signal.

NOT_ENOUGH_INFO: last known good revision, introduction point, published-package behavior, MCP transport behavior, other platforms and frequency on real targets. These were not established by the source-checkout CLI reproduction.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7527 ### 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 `const n = Number(raw); return n;`, the public `analyze-javascript-application` and `trace-javascript-semantics` CLI commands recover the coercion but cannot trace its influence to `n`, or recover that coercion when tracing the provenance of the later `n` read. Using the unchanged application Evidence, `node scripts/rea.mjs trace-javascript-semantics <request.json> --json` with `seed: {kind: "boundary-field", field: "Number"}` and `direction: "forward-influence"` returns only the Number boundary: `status: "found"`, one node, zero relations, zero relevant unknowns. The result still reports partial coverage. Starting at parameter `raw` reaches that boundary and the call argument, but never reaches `n` or its read. Backward provenance from the `return n` read omits both Number and raw, with a generic unsupported-call unknown. Enabling `include_ambiguous_dynamic_edges` does not recover this connection in either direction. Backward provenance directly from the Number boundary does reach raw, establishing that boundary selection and input linkage work. ### Reproduction 1. Use source revision `65f1c447344a423fa2785847dbcea1e9a9e2454a`, its locked dependencies and a built runtime, with Node.js 24.22.1 on PATH. The verification used the prepared build; dependency installation and building were not rerun. 2. From the repository root, run the following block. It creates the entire inert fixture, isolates HOME/XDG state, invokes the public CLI, and prints the graph and trace comparisons. It writes the exact returned Evidence and stderr into the chosen scratch directory. REA does not execute the fixture. ```sh repro_root=$(mktemp -d) python3 - "$repro_root" <<'PYREPRO' import json, os, pathlib, subprocess, sys root = pathlib.Path(sys.argv[1]).resolve() env = os.environ.copy() for name in ['HOME', 'XDG_CONFIG_HOME', 'XDG_CACHE_HOME', 'XDG_DATA_HOME', 'XDG_STATE_HOME']: directory = root / name.lower() directory.mkdir(parents=True, exist_ok=True) env[name] = str(directory) app = root / 'app' app.mkdir(exist_ok=True) source = 'function parse(raw) {\n const n = Number(raw);\n return n;\n}\nmodule.exports = parse;\n' (app / 'main.js').write_text(source) def cli(name, command, argument): run = subprocess.run(['node', 'scripts/rea.mjs', command, str(argument), '--json'], env=env, capture_output=True, text=True, check=True, timeout=60) (root / (name + '.json')).write_text(run.stdout) (root / (name + '.stderr')).write_text(run.stderr) return json.loads(run.stdout) application = cli('application', 'analyze-javascript-application', app) graph = application['normalized_result']['semantic_graph'] boundary = next(n for n in graph['nodes'] if n['kind'] == 'boundary') raw = next(n for n in graph['nodes'] if n['kind'] == 'parameter' and n['label'] == 'raw') read = next(n for n in graph['nodes'] if n['identity']['role_key'].endswith('read:n')) incident = [e for e in graph['relations'] if boundary['node_id'] in [e['source_node_id'], e['target_node_id']]] print(json.dumps({'boundary_properties': boundary['properties'], 'incident_relations': [e['relation'] for e in incident]})) assert len(incident) == 1 and incident[0]['relation'] == 'coerces' for name, seed, direction in [ ('boundary-forward', {'kind': 'boundary-field', 'field': 'Number'}, 'forward-influence'), ('raw-forward', {'kind': 'semantic-node', 'node_id': raw['node_id']}, 'forward-influence'), ('read-backward', {'kind': 'semantic-node', 'node_id': read['node_id']}, 'backward-provenance'), ('boundary-backward', {'kind': 'boundary-field', 'field': 'Number'}, 'backward-provenance')]: for candidates in [False, True]: label = name + ('-candidates' if candidates else '') request = root / (label + '-input.json') request.write_text(json.dumps({'application': application, 'query': { 'seed': seed, 'direction': direction, 'include_ambiguous_dynamic_edges': candidates}})) result = cli(label, 'trace-javascript-semantics', request)['normalized_result'] print(json.dumps({'query': label, 'status': result['status'], 'coverage': result['coverage'], 'summary': result['summary'], 'nodes': [[n['kind'], n['label']] for n in result['nodes']], 'unknown_details': [u['detail'] for u in result['unknowns']]})) if name == 'boundary-forward': assert result['nodes'] == [boundary] and not result['relations'] if name == 'raw-forward': assert not any(n['label'] == 'n' for n in result['nodes']) if name == 'read-backward': assert boundary['node_id'] not in [n['node_id'] for n in result['nodes']] assert raw['node_id'] not in [n['node_id'] for n in result['nodes']] if name == 'boundary-backward': assert raw['node_id'] in [n['node_id'] for n in result['nodes']] assert (app / 'main.js').read_text() == source PYREPRO ``` 3. Compare the forward boundary trace with the result read's backward trace. The Number boundary has only its incoming coercion edge; admitting candidate edges does not reconnect it. The final assertions also check that boundary-backward reaches raw and that the input source bytes did not change. The failure reproduced in two independently run analyses of the same inline fixture. The second analysis ran all four queries with candidate edges disabled and enabled; its nine public commands exited zero and the assertions passed. ### Expected behavior A recognized coercion with a retained result binding should preserve that static influence relationship: forward tracing from Number or raw should reach its result `n` and the later read, and backward provenance from that read should include the coercion and its input. This does not require evaluating the result or proving execution. Preserve applicable unknowns and partial coverage. The documented semantic relation workflow covers boundary-field selection, backward provenance, forward influence, complete reachable subgraphs, coercion boundaries, and argument/return flow. The omission occurs while constructing the graph used by that workflow: the result relationship is recovered by analysis but is absent from its projected relations. The expected contract is more specific than traversing all edges of an already incomplete graph. ### Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: An analyst's supported static trace omits a recognized coercion from result provenance and omits the result from its forward influence. The failure is repeatable on the inline unshadowed Number call. Partial coverage remains visible; no severe downstream incident or data loss was demonstrated. - Workaround status: unverified - Workaround basis: Selecting Number directly and tracing backward recovers raw, but requires already knowing the boundary and does not restore a path from the result to that boundary. Candidate-edge opt-in was tested and did not restore either missing connection. No complete query-only workaround was established. Other parsing, validation, nested-call, resource-handle and real-application shapes were not tested. This report concerns the single coercion-result relationship above. No security boundary is crossed. ### REA version Exact `node scripts/rea.mjs --version` output: ```text 6.8.0 ``` Source baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. This was a built source checkout; the published npm artifact was not independently run. ### Runtime and platform Node.js `v24.22.1`; macOS 27.0.1; Darwin arm64. No external analysis provider was required. ### Target and artifact context One five-line CommonJS JavaScript source file with no package manifest, reproduced inline above. Analysis reads it as source text. Trace requests carry the original complete application Evidence returned by the analysis command. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed summaries from the public CLI, with default candidate exclusion: ```text query status coverage nodes relations relevant_unknowns boundary-forward found partial 1 0 0 raw-forward found partial 4 3 1 read-backward ambiguous partial 3 2 1 boundary-backward found partial 3 2 1 ``` - `boundary-forward` returns only `boundary:Number`. - `raw-forward` returns the raw parameter, its read, argument 0 and the Number boundary. Its unknown says `Binding raw has no constant initializer. Unknown value at binding.` - `read-backward` returns binding n, its read and its variable definition. Its unknown says `Unsupported CallExpression value. Unknown value at binding.` - With candidates admitted, `read-backward` adds the call-site (four nodes, three relations) but still omits Number and raw; the other three query summaries are unchanged. - The complete retained graph has 18 nodes and 11 relations. The Number boundary has exactly one incident relation: the resolved raw-to-Number `coerces` edge. Both the result binding n and the call-site at Number's source range are present. Source checks at the stated baseline: - Expected source: `docs/javascript-application-workflows.md:181-199` documents boundary method-field identity, both trace directions, reachable subgraphs, argument/return flow and coercion boundaries. Lines 208-212 restrict built-in effects when globals may be written; this fixture has no such writes or local Number binding. Lines 260-269 preserve unknowns and distinguish static relations from execution. The repository's `AGENTS.md` Architecture and Cleanup section requires identity, omission, uncertainty and source evidence to survive boundaries. - Recovered fact: `src/domain/javascript/javascriptSemanticIr.ts:353-362` defines the boundary's source/result binding fields. `src/domain/javascript/javascriptSemanticDataEffects.ts:370-413` recognizes this supported unshadowed global call and captures both bindings. A separate executed source-owner probe on the exact same fixture returned `resolution: "complete"`, `sourceBindingId: "scope:function:0:60:binding:raw"` and `resultBindingId: "scope:function:0:60:binding:n"`. - Failure source: `src/application/javascript/JavaScriptSemanticGraphDataProjection.ts:218-257` creates the source-to-boundary relation but never uses the recovered result binding or links the boundary to its call-site. Boundary-node construction at lines 259-278 preserves its identity and field. The public graph's absent outgoing edge confirms this is not just a trace filter excluding an existing connection. - Public entry and guards: `src/application/javascript/JavaScriptSemanticTraceService.ts:21-68` parses application Evidence before querying. `src/domain/javascript/javascriptSemanticQuery.ts:115-179` applies candidate/relation admission and directed traversal. The reproduction uses unchanged valid Evidence and no relation restriction. `npm run test:local -- src/application/javascript/JavaScriptSemanticGraphResources.test.ts` passed all 12 tests. Those tests cover retained relation families and partial coverage, without checking this boundary-to-result connection. No temporary test or checkout modification was needed. ### Typed outcome or recovery expected On the unchanged fixture, forward influence from the recovered Number boundary should reach the retained n result and its later read; backward provenance from that read should recover Number and raw. Preserve the successful boundary-backward input path and existing uncertainty. If this supported result relationship cannot be represented, expose that specific missing output relationship as a typed limitation rather than leaving the boundary's forward trace with no relevant unknowns. No implementation or post-fix restoration was tested. ### Analysis or suspected cause Source-derived explanation: the boundary producer retains resultBindingId, but the boundary graph projector only emits its input relation. Generic candidate call-result linkage can reach the call-site from n with opt-in; it cannot cross to the separate boundary node. The observed graph therefore lacks a usable connection regardless of candidate admission. No particular new relation name or schema change is prescribed. ### Additional context Open [#7500](https://github.com/morluto/rea/issues/7500) is related but has a different closure signal: it requires following binary/template operands into produced values. This fixture has neither expression kind. Fixing those operator paths alone would leave the recovered Number boundary disconnected from its result. A boundary-field forward trace reaching n and its read independently tests this defect. Closed [#3170](https://github.com/morluto/rea/issues/3170) repaired boundary seed lookup; lookup succeeds here. Closed [#6193](https://github.com/morluto/rea/issues/6193) and merged [#6206](https://github.com/morluto/rea/pull/6206) concern reassignment inputs. Closed [#5585](https://github.com/morluto/rea/issues/5585), [#5648](https://github.com/morluto/rea/issues/5648) and merged [#5651](https://github.com/morluto/rea/pull/5651) concern direct member-read flow into returns and arguments. Those restoration checks do not cover a recognized boundary's retained output binding. The other plausible open/closed issue and PR hits were checked without finding this same contract and closure signal. NOT_ENOUGH_INFO: last known good revision, introduction point, published-package behavior, MCP transport behavior, other platforms and frequency on real targets. These were not established by the source-checkout CLI reproduction.
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#4472
No description provided.