[GH-ISSUE #6226] [Bug] Incoming native handoffs lose known JavaScript-requested members #3790

Closed
opened 2026-10-31 00:44:05 +00:00 by bsakaguchi · 2 comments
Owner

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

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

Analysis recorded a content-digest native add-on and one native-export observation with requested_members: ["openProject"]. Tracing the same add-on node with direction: "outgoing" and "both" returned requested_exports: ["openProject"] and one export node ID. With direction: "incoming", the handoff returned requested_exports: [] and export_node_ids: []. All three handoffs retained the same artifact digest and artifact_path: "addon.node", with status: "requires-provider-analysis". Each trace reported coverage.status: "complete-within-source"; no limitation identified the requested-export set as unexamined.

Reproduction

From the source checkout with locked dependencies installed, run npm run build:cached once if the compiled runtime is missing. Save the following script outside the checkout, set REPRO_SCRIPT to its absolute location, and run this command from the checkout:

node "$REPRO_SCRIPT"
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
const root = mkdtempSync(join(process.env.REPRO_BASE ?? tmpdir(), 'native-direction-'));
const app = join(root, 'app');
mkdirSync(app);
writeFileSync(join(app, 'main.js'), 'const { openProject } = require("./addon.node"); openProject();\n');
writeFileSync(join(app, 'addon.node'), 'inert native addon fixture');
const home = join(root, 'home');
mkdirSync(home);
const env = { ...process.env, HOME: home, DSH_HOME: home, XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home };
function cli(args) {
  const response = spawnSync(process.execPath, [resolve('scripts/rea.mjs'), ...args, '--json'], { env, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 });
  if (response.status !== 0) throw new Error(response.stderr || response.stdout);
  return JSON.parse(response.stdout);
}
const application = cli(['analyze-javascript-application', app, '--artifact-format', 'directory']);
writeFileSync(join(root, 'application.json'), JSON.stringify(application));
const graph = application.normalized_result.graph;
const addon = graph.nodes.find(node => node.kind === 'native-addon');
if (!addon) throw new Error('Missing addon node');
console.log(JSON.stringify({ requested_members: graph.nodes.filter(node => node.kind === 'native-export').map(node => node.observations.map(o => o.properties.requested_members)), addon_identity: addon.identity.strategy }));
for (const direction of ['outgoing', 'incoming', 'both']) {
  const path = join(root, `${direction}.json`);
  writeFileSync(path, JSON.stringify({ application, seed: { kind: 'node-id', value: addon.node_id, match: 'exact', case_sensitive: true }, direction }));
  const trace = cli(['trace-application-feature', path]);
  writeFileSync(join(root, `${direction}-result.json`), JSON.stringify(trace));
  console.log(JSON.stringify({ direction, native_handoffs: trace.normalized_result.native_handoffs.map(({ export_node_ids, requested_exports, status, artifact_path, artifact_sha256 }) => ({ export_node_ids, requested_exports, status, artifact_path, artifact_sha256 })), coverage: trace.normalized_result.coverage }));
}

The script creates an owned temporary directory, isolates HOME, DSH_HOME, XDG_CONFIG_HOME, and XDG_CACHE_HOME for every REA subprocess, writes inert input files, runs public CLI analysis, and traces the exact returned native add-on node in three directions. The .node bytes are inert text: this tests static graph analysis, not native loading or symbol validity. It leaves the owned directory and saved Evidence available for inspection. Two runs reproduced the same direction-dependent result; all eight REA commands exited 0. No JavaScript or native target code was executed.

Expected behavior

For an available add-on with a known JavaScript-requested member in the source application graph, its native handoff should preserve that binding, or explicitly report that the requested-export set was not examined. Direction filtering should still control the reachable trace graph. Returning an actionable handoff with empty export arrays and no export-coverage limitation loses the known member needed for native follow-up; this expectation does not claim that JavaScript syntax proves the binary implements that member.

The feature-tracing contract states that a handoff binds the exact native artifact digest and requested exports from the application graph. The tool-design contract requires missing coverage to remain unknown rather than absence.

Impact and scope

P2: the incoming trace loses known JavaScript-requested member context needed to guide native follow-up, while preserving add-on identity and the rest of the trace. This reproduced twice with a directory containing one CommonJS require and one inert add-on file. A practical observed workaround is to repeat the same add-on seed with outgoing or both, which returned openProject in both runs. No security boundary crossing, real native-provider behavior, or application execution was demonstrated. Wider application and platform frequency is NOT_ENOUGH_INFO; it was not measured.

REA version

Source commit c465ff5a81 (main)

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0; this reproduction used the installed version stated here.

Target and artifact context

Owned directory with main.js requiring openProject from ./addon.node. The add-on file contains inert text. The graph and Evidence came from the normal static application-analysis CLI; no hand-authored graph or native-provider result was supplied.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced from the two observed CLI runs:

analysis requested_members: ["openProject"]
analysis addon identity strategy: content-digest
outgoing requested_exports: ["openProject"]; export_node_ids.length: 1
incoming requested_exports: []; export_node_ids.length: 0
both requested_exports: ["openProject"]; export_node_ids.length: 1
all three artifact_path: addon.node
all three artifact_sha256: 37a6a693a3dc8e18f488159ba5687b8e2bf9e70153497c26056be4fcede712ed
all three status: requires-provider-analysis
all three coverage.status: complete-within-source

Typed outcome or recovery expected

Repeat the three-direction CLI sequence on the same generated application Evidence. The incoming trace must preserve the known requested member in its handoff or explicitly distinguish an unexamined set from an empty one. Outgoing and both must continue to return openProject with the exact add-on digest, while the reachable trace graph remains faithful to the selected direction. This does not require analyzing a provider or executing the target.

Analysis or suspected cause

Source-derived explanation: the trace projection supplies traversal.nodes and traversal.edges to the handoff builder. The requested-member collector derives export nodes and names from those inputs. Incoming traversal omits the add-on's outgoing exposes relationship. This explains the observed lost member; no implementation choice is prescribed.

Additional context

Related #3926 and merged #4179 address unavailable add-on byte identity, and #2667 addresses erased TypeScript names in requested bindings. #4027 is a broad JavaScript tracker. These are distinct from losing a known member under incoming traversal. Searches covered open and closed issues and PRs with trace_application_feature/native handoff, requested_exports/incoming, javascriptNativeHandoff, native handoff, and incoming/exports keywords. Fresh upstream main at 34fd7bdf7d has no changes to the cited trace, handoff, or workflow documentation files since the tested baseline. No separate MCP transport run is claimed.

Originally created by @coygeek on GitHub (Oct 22, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6226 ### 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 Analysis recorded a content-digest native add-on and one native-export observation with `requested_members: ["openProject"]`. Tracing the same add-on node with `direction: "outgoing"` and `"both"` returned `requested_exports: ["openProject"]` and one export node ID. With `direction: "incoming"`, the handoff returned `requested_exports: []` and `export_node_ids: []`. All three handoffs retained the same artifact digest and `artifact_path: "addon.node"`, with `status: "requires-provider-analysis"`. Each trace reported `coverage.status: "complete-within-source"`; no limitation identified the requested-export set as unexamined. ### Reproduction From the source checkout with locked dependencies installed, run `npm run build:cached` once if the compiled runtime is missing. Save the following script outside the checkout, set `REPRO_SCRIPT` to its absolute location, and run this command from the checkout: ```sh node "$REPRO_SCRIPT" ``` ```js import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; const root = mkdtempSync(join(process.env.REPRO_BASE ?? tmpdir(), 'native-direction-')); const app = join(root, 'app'); mkdirSync(app); writeFileSync(join(app, 'main.js'), 'const { openProject } = require("./addon.node"); openProject();\n'); writeFileSync(join(app, 'addon.node'), 'inert native addon fixture'); const home = join(root, 'home'); mkdirSync(home); const env = { ...process.env, HOME: home, DSH_HOME: home, XDG_CONFIG_HOME: home, XDG_CACHE_HOME: home }; function cli(args) { const response = spawnSync(process.execPath, [resolve('scripts/rea.mjs'), ...args, '--json'], { env, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }); if (response.status !== 0) throw new Error(response.stderr || response.stdout); return JSON.parse(response.stdout); } const application = cli(['analyze-javascript-application', app, '--artifact-format', 'directory']); writeFileSync(join(root, 'application.json'), JSON.stringify(application)); const graph = application.normalized_result.graph; const addon = graph.nodes.find(node => node.kind === 'native-addon'); if (!addon) throw new Error('Missing addon node'); console.log(JSON.stringify({ requested_members: graph.nodes.filter(node => node.kind === 'native-export').map(node => node.observations.map(o => o.properties.requested_members)), addon_identity: addon.identity.strategy })); for (const direction of ['outgoing', 'incoming', 'both']) { const path = join(root, `${direction}.json`); writeFileSync(path, JSON.stringify({ application, seed: { kind: 'node-id', value: addon.node_id, match: 'exact', case_sensitive: true }, direction })); const trace = cli(['trace-application-feature', path]); writeFileSync(join(root, `${direction}-result.json`), JSON.stringify(trace)); console.log(JSON.stringify({ direction, native_handoffs: trace.normalized_result.native_handoffs.map(({ export_node_ids, requested_exports, status, artifact_path, artifact_sha256 }) => ({ export_node_ids, requested_exports, status, artifact_path, artifact_sha256 })), coverage: trace.normalized_result.coverage })); } ``` The script creates an owned temporary directory, isolates HOME, DSH_HOME, XDG_CONFIG_HOME, and XDG_CACHE_HOME for every REA subprocess, writes inert input files, runs public CLI analysis, and traces the exact returned native add-on node in three directions. The `.node` bytes are inert text: this tests static graph analysis, not native loading or symbol validity. It leaves the owned directory and saved Evidence available for inspection. Two runs reproduced the same direction-dependent result; all eight REA commands exited 0. No JavaScript or native target code was executed. ### Expected behavior For an available add-on with a known JavaScript-requested member in the source application graph, its native handoff should preserve that binding, or explicitly report that the requested-export set was not examined. Direction filtering should still control the reachable trace graph. Returning an actionable handoff with empty export arrays and no export-coverage limitation loses the known member needed for native follow-up; this expectation does not claim that JavaScript syntax proves the binary implements that member. The [feature-tracing contract](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/docs/javascript-application-workflows.md#L142) states that a handoff binds the exact native artifact digest and requested exports from the application graph. The [tool-design contract](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/docs/tool-design.md#L50) requires missing coverage to remain unknown rather than absence. ### Impact and scope P2: the incoming trace loses known JavaScript-requested member context needed to guide native follow-up, while preserving add-on identity and the rest of the trace. This reproduced twice with a directory containing one CommonJS require and one inert add-on file. A practical observed workaround is to repeat the same add-on seed with outgoing or both, which returned openProject in both runs. No security boundary crossing, real native-provider behavior, or application execution was demonstrated. Wider application and platform frequency is NOT_ENOUGH_INFO; it was not measured. ### REA version Source commit c465ff5a814b8e1a386fc547cbcd94df5c35f086 (main) ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64. The repository pins Node 24.18.0; this reproduction used the installed version stated here. ### Target and artifact context Owned directory with main.js requiring openProject from ./addon.node. The add-on file contains inert text. The graph and Evidence came from the normal static application-analysis CLI; no hand-authored graph or native-provider result was supplied. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced from the two observed CLI runs: ```text analysis requested_members: ["openProject"] analysis addon identity strategy: content-digest outgoing requested_exports: ["openProject"]; export_node_ids.length: 1 incoming requested_exports: []; export_node_ids.length: 0 both requested_exports: ["openProject"]; export_node_ids.length: 1 all three artifact_path: addon.node all three artifact_sha256: 37a6a693a3dc8e18f488159ba5687b8e2bf9e70153497c26056be4fcede712ed all three status: requires-provider-analysis all three coverage.status: complete-within-source ``` ### Typed outcome or recovery expected Repeat the three-direction CLI sequence on the same generated application Evidence. The incoming trace must preserve the known requested member in its handoff or explicitly distinguish an unexamined set from an empty one. Outgoing and both must continue to return openProject with the exact add-on digest, while the reachable trace graph remains faithful to the selected direction. This does not require analyzing a provider or executing the target. ### Analysis or suspected cause Source-derived explanation: the [trace projection](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/domain/javascript/javascriptFeatureTrace.ts#L60) supplies traversal.nodes and traversal.edges to the handoff builder. The [requested-member collector](https://github.com/morluto/rea/blob/c465ff5a814b8e1a386fc547cbcd94df5c35f086/src/domain/javascript/javascriptNativeHandoff.ts#L72) derives export nodes and names from those inputs. Incoming traversal omits the add-on's outgoing exposes relationship. This explains the observed lost member; no implementation choice is prescribed. ### Additional context Related #3926 and merged #4179 address unavailable add-on byte identity, and #2667 addresses erased TypeScript names in requested bindings. #4027 is a broad JavaScript tracker. These are distinct from losing a known member under incoming traversal. Searches covered open and closed issues and PRs with trace_application_feature/native handoff, requested_exports/incoming, javascriptNativeHandoff, native handoff, and incoming/exports keywords. Fresh upstream main at 34fd7bdf7d7aeec83d0380254ca9e0f7149c98ef has no changes to the cited trace, handoff, or workflow documentation files since the tested baseline. No separate MCP transport run is claimed.
Author
Owner

@XinwangTao commented on GitHub (Oct 22, 2026):

确认根因:requested-member collector 完全依赖 traversal 提供的 nodes/edges,而 incoming 遍历省略了 add-on 的 exposes 出边,导致已知的 requested_members 在 handoff 中被静默清空。风险点在于 incoming 返回 [] 与"确实无请求成员"无法区分,下游会误判。建议在 handoff 中为 incoming 保留已知 requested 成员(与 outgoing/both 一致),或显式增加 unexamined 标记区分空集与未检查集,并补三方向一致性回归测试。

<!-- gh-comment-id:6332768394 --> @XinwangTao commented on GitHub (Oct 22, 2026): 确认根因:requested-member collector 完全依赖 traversal 提供的 nodes/edges,而 incoming 遍历省略了 add-on 的 exposes 出边,导致已知的 requested_members 在 handoff 中被静默清空。风险点在于 incoming 返回 [] 与"确实无请求成员"无法区分,下游会误判。建议在 handoff 中为 incoming 保留已知 requested 成员(与 outgoing/both 一致),或显式增加 unexamined 标记区分空集与未检查集,并补三方向一致性回归测试。
Author
Owner

@coygeek commented on GitHub (Oct 24, 2026):

Checked the reported fix on main at d87e73f2db.

Trace direction selects reached native-add-on IDs, but handoff collection now receives input.graph, so incoming does not lose exposes edges or known members. A production-source probe confirmed openProject and one export ID across incoming/outgoing/both, equal handoffs, and different incoming/outgoing trace edges. Regression assertions match that trigger.

Implementation: javascriptFeatureTrace.ts, javascriptNativeHandoff.ts.

The relevant regression checks in javascriptApplicationWorkflows.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380726420 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. Trace direction selects reached native-add-on IDs, but handoff collection now receives input.graph, so incoming does not lose exposes edges or known members. A production-source probe confirmed openProject and one export ID across incoming/outgoing/both, equal handoffs, and different incoming/outgoing trace edges. Regression assertions match that trigger. Implementation: [javascriptFeatureTrace.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptFeatureTrace.ts#L54), [javascriptNativeHandoff.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptNativeHandoff.ts#L36). The relevant regression checks in [javascriptApplicationWorkflows.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3790
No description provided.