[GH-ISSUE #5710] [Bug] analyze-javascript-application still counts exposed const-channel sends twice after #5586 #3462

Closed
opened 2026-10-31 00:40:43 +00:00 by bsakaguchi · 1 comment
Owner

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

Originally assigned to: @kaoru0822-kitauji on GitHub.

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 succeeds but counts one ipcRenderer.send(PING) inside a contextBridge member as two renderer transmissions when a preceding local const PING = "ping" defines the channel. It retains both a direct dynamic-channel observation and a wrapper-derived literal-channel observation for the same send range. The inline-literal control counts that send once.

This still reproduces after #5586 and its fix commit 506400748eb1d6c674f0cf51330073929c45f939, which is an ancestor of the tested baseline. This is a surviving const-channel gap, not evidence that that fix introduced it. These are static callsite counts; Electron was not executed.

Reproduction

Run from a source checkout at the baseline under REA version, with dependencies installed and the CLI built. These commands create an inert Electron source fixture and isolate REA's profile and temporary state. They analyze source without launching Electron.

work_dir="$(mktemp -d)"
S="$work_dir"
mkdir -p "$S/home" "$S/cfg" "$S/data" "$S/cache" "$S/state" "$S/tmp" "$S/fixture"
printf '%s\n' '{"name":"ipc-const-probe","version":"1.0.0","main":"main.js"}' > "$S/fixture/package.json"
cat > "$S/fixture/main.js" <<'JS'
const { app, BrowserWindow, ipcMain } = require("electron");
function createWindow() {
  const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } });
  window.loadFile("index.html");
}
ipcMain.on("ping", () => {});
app.whenReady().then(createWindow);
JS
cat > "$S/fixture/preload.js" <<'JS'
const { contextBridge, ipcRenderer } = require("electron");
const PING = "ping";
contextBridge.exposeInMainWorld("api", {
  sendPing: () => ipcRenderer.send(PING),
});
JS
printf '%s\n' '<!doctype html><html><body></body></html>' > "$S/fixture/index.html"
analyze_fixture() {
  env -i PATH="$PATH" HOME="$S/home" XDG_CONFIG_HOME="$S/cfg" \
    XDG_DATA_HOME="$S/data" XDG_CACHE_HOME="$S/cache" \
    XDG_STATE_HOME="$S/state" TMPDIR="$S/tmp" REA_LOG_LEVEL=silent \
    node scripts/rea.mjs analyze-javascript-application "$S/fixture" --json
}
analyze_fixture > "$S/const.json"
jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/const.json"
jq '[.normalized_result.graph.nodes[] | select(.kind == "ipc-channel") | .observations[] | select(.properties.side == "renderer") | {channel: .properties.channel, expression: .properties.channel_expression, resolution: .properties.resolution, range: .evidence.location.value}]' "$S/const.json"

For the inline-literal control, replace only the preload and repeat:

cat > "$S/fixture/preload.js" <<'JS'
const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("api", {
  sendPing: () => ipcRenderer.send("ping"),
});
JS
analyze_fixture > "$S/literal.json"
jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/literal.json"

For a control that preserves distinct physical send sites, use this preload and repeat:

cat > "$S/fixture/preload.js" <<'JS'
const { contextBridge, ipcRenderer } = require("electron");
const PING = "ping";
contextBridge.exposeInMainWorld("api", {
  sendPing: () => ipcRenderer.send(PING),
  sendAgain: () => ipcRenderer.send(PING),
});
JS
analyze_fixture > "$S/two-sites.json"
jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/two-sites.json"

The independent verifier replayed these shell blocks. A submission-time isolated CLI replay of the same three fixtures also reproduced the counts below; every invocation exited 0.

Expected behavior

One renderer send plus one main listener should report operations: 2 and renderer_transmissions: 1, as the inline-literal control does. Two distinct renderer send sites plus that listener should report three operations and two renderer transmissions. Retain the resolved channel and bridge-path provenance without counting either physical send twice.

Source-derived contract at the tested baseline: src/domain/javascript/electronWrappedCallPaths.test.ts:372-387 keeps an exposed renderer operation singular while retaining its bridge path; src/domain/javascript/electronWrappedCallPaths.ts:404-405 states the singular-transmission rule. docs/javascript-artifact-reconstruction.md:370-375 documents lexical substitution and retained literal-channel/source-range provenance. docs/javascript-application-graph.md:164-167 separates entity identity from observations.

Impact and scope

P2: this bounded local-constant bridge case silently inflates IPC operation and renderer-transmission counts and retains duplicate operation records. Analysis completes and preserves inspectable source locations. Both the independent verification and submission replay reproduced it. Broader frequency, other platforms, MCP transport, and downstream feature-tracing effects were not tested. Manual source reconciliation identifies the duplicate in this fixture; no general automatic workaround preserving distinct wrapper invocations was verified. Inlining the channel is a diagnostic control that changes the analyzed source.

REA version

rea-agents 6.7.0; source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68, built source CLI. Rechecked freshly fetched origin/main at ccc7da309d7db385d5c6d73b0a3a8e093be33504: no changes since the baseline in the cited expected/failure source files. The CLI replay ran at the baseline; current main was checked by source comparison, not a separate rebuild.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64. No native provider or target execution.

Target and artifact context

Inert four-file Electron source fixture: package.json, main.js, preload.js, and index.html. HOME, XDG directories, and temporary state are isolated by the reproduction. No Electron installation is required.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed projections of .normalized_result.summary.ipc:

one const-channel send: {"operations":3,"renderer_transmissions":2,"dynamic_channel_operations":1}
one inline-literal send: {"operations":2,"renderer_transmissions":1,"dynamic_channel_operations":0}
two const-channel sends: {"operations":5,"renderer_transmissions":4,"dynamic_channel_operations":2}

In the first fixture, two renderer send observations refer to preload.js line 4, columns 18-40:

  • Direct syntax record: channel: null, channel_expression: "PING", resolution: "dynamic", with observation-fingerprint identity scoped to electron-ipc-channel.
  • Wrapper-derived record: channel: "ping", channel_expression: null, resolution: "literal", with artifact-local identity literal:ping in electron-inferred-ipc-channel and an invocation location covering the bridge exposure.

The two-site control retains the same dynamic/literal pair at each distinct renderer send range. Evidence provenance: observed; local verification: independently reproduced and replayed at submission.

Typed outcome or recovery expected

Successful analysis with accurate counts and retained resolved-channel/bridge provenance. Proposed restoration check: rerun all three fixtures and obtain operation/transmission totals of 2/1, 2/1, and 3/2 respectively. Preserve both physical send sites in the two-site control, distinct wrapper invocation identities where meaningful, and genuinely unresolved dynamic channels. These checks do not prescribe a deduplication algorithm.

Analysis or suspected cause

Source-derived explanation: src/domain/javascript/electronStaticAnalysisIpc.ts:40-57,89-100 records the identifier channel as dynamic. src/domain/javascript/electronWrappedCallPaths.ts:338-377 derives the literal channel through substitution; its endpoint key at lines 46-47 includes the channel value, so duplicate lookup/suppression at lines 407-413 cannot match null against "ping". src/application/javascript/ElectronBoundaryAnalysis.ts:134-145 counts both retained records. This agrees with the CLI result; no fix was tested.

Additional context

Prepared with AI assistance and independently reproduced. #5586 covered duplicate inline-literal listener observations and was closed by the direct fix commit linked above; its tests do not explicitly exclude const-channel aliases. No last known-good version for this alias case was established. Merged #3783 preserves the channel-bearing duplicate predicate. Open #2094 concerns constant folding/wrapper recovery and #4915 concerns attribution of unknown coverage; neither describes this surviving same-site const-channel double count.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5710 Originally assigned to: @kaoru0822-kitauji on GitHub. ### 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 `analyze-javascript-application` succeeds but counts one `ipcRenderer.send(PING)` inside a contextBridge member as two renderer transmissions when a preceding local `const PING = "ping"` defines the channel. It retains both a direct dynamic-channel observation and a wrapper-derived literal-channel observation for the same send range. The inline-literal control counts that send once. This still reproduces after #5586 and its fix commit `506400748eb1d6c674f0cf51330073929c45f939`, which is an ancestor of the tested baseline. This is a surviving const-channel gap, not evidence that that fix introduced it. These are static callsite counts; Electron was not executed. ### Reproduction Run from a source checkout at the baseline under REA version, with dependencies installed and the CLI built. These commands create an inert Electron source fixture and isolate REA's profile and temporary state. They analyze source without launching Electron. ```sh work_dir="$(mktemp -d)" S="$work_dir" mkdir -p "$S/home" "$S/cfg" "$S/data" "$S/cache" "$S/state" "$S/tmp" "$S/fixture" printf '%s\n' '{"name":"ipc-const-probe","version":"1.0.0","main":"main.js"}' > "$S/fixture/package.json" cat > "$S/fixture/main.js" <<'JS' const { app, BrowserWindow, ipcMain } = require("electron"); function createWindow() { const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } }); window.loadFile("index.html"); } ipcMain.on("ping", () => {}); app.whenReady().then(createWindow); JS cat > "$S/fixture/preload.js" <<'JS' const { contextBridge, ipcRenderer } = require("electron"); const PING = "ping"; contextBridge.exposeInMainWorld("api", { sendPing: () => ipcRenderer.send(PING), }); JS printf '%s\n' '<!doctype html><html><body></body></html>' > "$S/fixture/index.html" analyze_fixture() { env -i PATH="$PATH" HOME="$S/home" XDG_CONFIG_HOME="$S/cfg" \ XDG_DATA_HOME="$S/data" XDG_CACHE_HOME="$S/cache" \ XDG_STATE_HOME="$S/state" TMPDIR="$S/tmp" REA_LOG_LEVEL=silent \ node scripts/rea.mjs analyze-javascript-application "$S/fixture" --json } analyze_fixture > "$S/const.json" jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/const.json" jq '[.normalized_result.graph.nodes[] | select(.kind == "ipc-channel") | .observations[] | select(.properties.side == "renderer") | {channel: .properties.channel, expression: .properties.channel_expression, resolution: .properties.resolution, range: .evidence.location.value}]' "$S/const.json" ``` For the inline-literal control, replace only the preload and repeat: ```sh cat > "$S/fixture/preload.js" <<'JS' const { contextBridge, ipcRenderer } = require("electron"); contextBridge.exposeInMainWorld("api", { sendPing: () => ipcRenderer.send("ping"), }); JS analyze_fixture > "$S/literal.json" jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/literal.json" ``` For a control that preserves distinct physical send sites, use this preload and repeat: ```sh cat > "$S/fixture/preload.js" <<'JS' const { contextBridge, ipcRenderer } = require("electron"); const PING = "ping"; contextBridge.exposeInMainWorld("api", { sendPing: () => ipcRenderer.send(PING), sendAgain: () => ipcRenderer.send(PING), }); JS analyze_fixture > "$S/two-sites.json" jq '.normalized_result.summary.ipc | {operations, renderer_transmissions, dynamic_channel_operations}' "$S/two-sites.json" ``` The independent verifier replayed these shell blocks. A submission-time isolated CLI replay of the same three fixtures also reproduced the counts below; every invocation exited 0. ### Expected behavior One renderer send plus one main listener should report `operations: 2` and `renderer_transmissions: 1`, as the inline-literal control does. Two distinct renderer send sites plus that listener should report three operations and two renderer transmissions. Retain the resolved channel and bridge-path provenance without counting either physical send twice. Source-derived contract at the tested baseline: `src/domain/javascript/electronWrappedCallPaths.test.ts:372-387` keeps an exposed renderer operation singular while retaining its bridge path; `src/domain/javascript/electronWrappedCallPaths.ts:404-405` states the singular-transmission rule. `docs/javascript-artifact-reconstruction.md:370-375` documents lexical substitution and retained literal-channel/source-range provenance. `docs/javascript-application-graph.md:164-167` separates entity identity from observations. ### Impact and scope P2: this bounded local-constant bridge case silently inflates IPC operation and renderer-transmission counts and retains duplicate operation records. Analysis completes and preserves inspectable source locations. Both the independent verification and submission replay reproduced it. Broader frequency, other platforms, MCP transport, and downstream feature-tracing effects were not tested. Manual source reconciliation identifies the duplicate in this fixture; no general automatic workaround preserving distinct wrapper invocations was verified. Inlining the channel is a diagnostic control that changes the analyzed source. ### REA version `rea-agents` 6.7.0; source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`, built source CLI. Rechecked freshly fetched `origin/main` at `ccc7da309d7db385d5c6d73b0a3a8e093be33504`: no changes since the baseline in the cited expected/failure source files. The CLI replay ran at the baseline; current main was checked by source comparison, not a separate rebuild. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64. No native provider or target execution. ### Target and artifact context Inert four-file Electron source fixture: `package.json`, `main.js`, `preload.js`, and `index.html`. HOME, XDG directories, and temporary state are isolated by the reproduction. No Electron installation is required. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed projections of `.normalized_result.summary.ipc`: ```text one const-channel send: {"operations":3,"renderer_transmissions":2,"dynamic_channel_operations":1} one inline-literal send: {"operations":2,"renderer_transmissions":1,"dynamic_channel_operations":0} two const-channel sends: {"operations":5,"renderer_transmissions":4,"dynamic_channel_operations":2} ``` In the first fixture, two renderer send observations refer to `preload.js` line 4, columns 18-40: - Direct syntax record: `channel: null`, `channel_expression: "PING"`, `resolution: "dynamic"`, with observation-fingerprint identity scoped to `electron-ipc-channel`. - Wrapper-derived record: `channel: "ping"`, `channel_expression: null`, `resolution: "literal"`, with artifact-local identity `literal:ping` in `electron-inferred-ipc-channel` and an invocation location covering the bridge exposure. The two-site control retains the same dynamic/literal pair at each distinct renderer send range. Evidence provenance: observed; local verification: independently reproduced and replayed at submission. ### Typed outcome or recovery expected Successful analysis with accurate counts and retained resolved-channel/bridge provenance. Proposed restoration check: rerun all three fixtures and obtain operation/transmission totals of 2/1, 2/1, and 3/2 respectively. Preserve both physical send sites in the two-site control, distinct wrapper invocation identities where meaningful, and genuinely unresolved dynamic channels. These checks do not prescribe a deduplication algorithm. ### Analysis or suspected cause Source-derived explanation: `src/domain/javascript/electronStaticAnalysisIpc.ts:40-57,89-100` records the identifier channel as dynamic. `src/domain/javascript/electronWrappedCallPaths.ts:338-377` derives the literal channel through substitution; its endpoint key at lines 46-47 includes the channel value, so duplicate lookup/suppression at lines 407-413 cannot match null against `"ping"`. `src/application/javascript/ElectronBoundaryAnalysis.ts:134-145` counts both retained records. This agrees with the CLI result; no fix was tested. ### Additional context Prepared with AI assistance and independently reproduced. #5586 covered duplicate inline-literal listener observations and was closed by the direct fix commit linked above; its tests do not explicitly exclude const-channel aliases. No last known-good version for this alias case was established. Merged #3783 preserves the channel-bearing duplicate predicate. Open #2094 concerns constant folding/wrapper recovery and #4915 concerns attribution of unknown coverage; neither describes this surviving same-site const-channel double count.
Author
Owner

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

An additional trigger of the same duplicate-observation problem is forwarding the channel through a helper parameter. I independently reproduced this at 9d8e7803326a382cbbb3ffdb900f5e93e5442d68, REA 6.7.0, with fresh inert fixtures and isolated HOME/XDG/temp state. The fixture below reports operations: 4, renderer_transmissions: 3, and renderer_listeners: 0, versus expected 2/1/0. Its single send range, preload line 2, columns 27–50, has two literal forward-chan observations differing only in invocation_location, plus one dynamic ch observation. Controls report operations/transmissions of 3/2 with a local constant and 2/1 with an inline literal. Replacing the helper's send with on and calling forward("forward-chan", () => {}) reports operations: 4, renderer_transmissions: 0, and renderer_listeners: 3, versus expected 2/0/1. All four analyses exited 0; Electron was not executed. Freshly fetched main is d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f and has JavaScript changes: verified at baseline; main not re-run. Put these sources in a directory with package.json containing {"name":"independent-forward-probe","version":"1.0.0","main":"main.js"} and an empty index.html:

// main.js
const { app, BrowserWindow, ipcMain } = require("electron");
ipcMain.on("forward-chan", () => {});
app.whenReady().then(() => {
  const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } });
  window.loadFile("index.html");
});

// preload.js
const { contextBridge, ipcRenderer } = require("electron");
const forward = (ch, v) => ipcRenderer.send(ch, v);
contextBridge.exposeInMainWorld("api", { forwarded: () => forward("forward-chan") });
node scripts/rea.mjs analyze-javascript-application ./fixture --json
<!-- gh-comment-id:6305089508 --> @coygeek commented on GitHub (Oct 21, 2026): An additional trigger of the same duplicate-observation problem is forwarding the channel through a helper parameter. I independently reproduced this at `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`, REA 6.7.0, with fresh inert fixtures and isolated HOME/XDG/temp state. The fixture below reports `operations: 4`, `renderer_transmissions: 3`, and `renderer_listeners: 0`, versus expected 2/1/0. Its single send range, preload line 2, columns 27–50, has two literal `forward-chan` observations differing only in `invocation_location`, plus one dynamic `ch` observation. Controls report operations/transmissions of 3/2 with a local constant and 2/1 with an inline literal. Replacing the helper's `send` with `on` and calling `forward("forward-chan", () => {})` reports `operations: 4`, `renderer_transmissions: 0`, and `renderer_listeners: 3`, versus expected 2/0/1. All four analyses exited 0; Electron was not executed. Freshly fetched main is `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f` and has JavaScript changes: verified at baseline; main not re-run. Put these sources in a directory with `package.json` containing `{"name":"independent-forward-probe","version":"1.0.0","main":"main.js"}` and an empty `index.html`: ```js // main.js const { app, BrowserWindow, ipcMain } = require("electron"); ipcMain.on("forward-chan", () => {}); app.whenReady().then(() => { const window = new BrowserWindow({ webPreferences: { preload: "./preload.js" } }); window.loadFile("index.html"); }); // preload.js const { contextBridge, ipcRenderer } = require("electron"); const forward = (ch, v) => ipcRenderer.send(ch, v); contextBridge.exposeInMainWorld("api", { forwarded: () => forward("forward-chan") }); ``` ```sh node scripts/rea.mjs analyze-javascript-application ./fixture --json ```
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#3462
No description provided.