[GH-ISSUE #7740] [Feature] Keep literal call arguments on call sites, such as the name in client.rpc("name") #4631

Open
opened 2026-10-31 00:51:37 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @jgt87 on GitHub (Oct 28, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/7740

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Reproduced on rea-agents 6.8.0 (released) and on main 6a226bd09.

Same fixture (7 files, 905 api.rpc("proc_…") calls) 6.8.0 (npm, Node 26.12.1) main 6a226bd09 (source, Node 24.22.1)
Occurrences of proc_ in the complete Evidence (76,557,994 bytes on 6.8.0) 0 0
call-site nodes / with empty properties 4,563 / 4,563 4,563 / 4,563

Area

JavaScript application analysis, semantic graph (analyze_javascript_application).

Current limitation

String literals passed as call arguments are not retained anywhere in the
application or semantic graph unless a specific extractor owns the call
(fetch, storage, IPC, and similar). A remote-procedure wrapper is a common
case in bundled web applications:

export function f0_0(a,b){ /* ... */ return api.rpc("proc_0_0",{v:c,d}).then(r=>r.data); }

On a synthetic 7-file build with 905 such calls (generator
gen-vite-like.mjs), the complete Evidence (76 MB) does not contain the string
proc_ at all. The semantic graph has 4,563 call-site nodes with empty
properties, and 7 nodes labeled rpc (one function, six property-slot),
none with an argument value. Storage keys in the same functions are retained
(storage nodes, identity.key), because the storage extractor owns them.

In the original (non-synthetic) case on 6.4.0, the semantic graph had 101
literal values in 21,527 nodes, and calls such as .rpc('name') showed
without the name.

Use cases

  • Map the server procedures or commands a bundle can call, without running it.
  • Seed trace_javascript_semantics or trace_application_feature with an
    operation name that appears only as a call argument.

Desired outcome and success criteria

A call site keeps its literal arguments (strings, numbers, booleans; or a
bounded prefix with a truncation flag) as properties, for example
arguments: [{index: 0, kind: "string", value: "proc_0_0"}], with the same
authority and coverage rules as other static facts. Success: in the synthetic
fixture, each api.rpc call site carries its procedure name.

Expected interaction with targets

Reads existing files only (static analysis of inert text).

  • #3539 (surface path- and URL-shaped literals passed to project request wrappers; narrower, analyze_web_bundle)
  • #7500 (forward influence stops at + and template-literal operands)

Verification available so far

Reproduced on rea-agents 6.8.0 (npm, Node 26.12.1) and upstream main at 6a226bd09 (source, Node 24.22.1), Windows 11
Pro (10.0.26300):

node gen-vite-like.mjs ./fx/small 6 40
rea analyze-javascript-application ./fx/small/assets --artifact-format directory --format json > out.json
grep -c "proc_" out.json   # 0
gen-vite-like.mjs (the synthetic fixture generator)
// Generates a synthetic Vite-like build output. No real application code.
// Usage: node gen-vite-like.mjs <outDir> [chunkCount] [bigChunkKB] [first|last]
// Writes <outDir>/index.html and <outDir>/assets/*.js. Each chunk has an
// absolute-base __vite__mapDeps table, real dynamic imports, localStorage
// get/set calls and api.rpc("proc_N_M") calls. Deterministic output.
import { mkdirSync, writeFileSync, rmSync } from "node:fs";
import { join } from "node:path";

const outDir = process.argv[2];
const chunkCount = Number(process.argv[3] ?? 87);
const bigKB = Number(process.argv[4] ?? 456);
if (!outDir) throw new Error("outDir required");
rmSync(outDir, { recursive: true, force: true });
const assets = join(outDir, "assets");
mkdirSync(assets, { recursive: true });

let seed = 7;
const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648);
const names = Array.from({ length: chunkCount }, (_, i) => `chunk${String(i).padStart(2, "0")}-${(i * 2654435761 >>> 0).toString(36).slice(0, 6)}.js`);
const entry = "index-a1b2c3.js";

function body(i, targetBytes) {
  const parts = [];
  let n = 0;
  let size = 0;
  while (size < targetBytes) {
    const f = `f${i}_${n}`;
    const key = `demo.pref.${i}.${n % 5}`;
    const fn = `export function ${f}(a,b){const c=a*${n}+b;` +
      `if(c>${Math.floor(rand() * 1000)}){localStorage.setItem("${key}",String(c));}` +
      `const d=localStorage.getItem("${key}");` +
      `return api.rpc("proc_${i}_${n % 7}",{v:c,d}).then(r=>r.data);}\n`;
    parts.push(fn);
    size += fn.length;
    n++;
  }
  return parts.join("");
}

const total = [];
const deps = names.map((n) => `assets/${n}`);
for (let i = 0; i < chunkCount; i++) {
  const bigIndex = process.argv[5] === "last" ? chunkCount - 1 : 0;
  const target = i === bigIndex ? bigKB * 1024 : 20 * 1024 + Math.floor(rand() * 12 * 1024);
  const next = (i + 1) % chunkCount;
  const other = (i + 7) % chunkCount;
  const head =
    `import{api}from"./${entry}";\n` +
    `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify([deps[next], deps[other]])})))=>i.map(i=>d[i]);\n` +
    `export const load${i}=()=>import("./${names[next]}");\n` +
    `export const pre${i}=()=>api.preload(()=>import("./${names[other]}"),__vite__mapDeps([0,1]));\n`;
  const text = head + body(i, target);
  writeFileSync(join(assets, names[i]), text);
  total.push(text.length);
}
const entryText =
  `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify(deps)})))=>i.map(i=>d[i]);\n` +
  `export const api={rpc(name,args){return fetch("/rpc/"+name,{method:"POST",body:JSON.stringify(args)}).then(r=>r.json());},preload(f,d){return f();}};\n` +
  names.map((n, i) => `export const route${i}=()=>api.preload(()=>import("./${n}"),__vite__mapDeps([${i}]));\n`).join("") +
  `const saved=localStorage.getItem("demo.session.token");if(saved){route0();}\n`;
writeFileSync(join(assets, entry), entryText);
writeFileSync(join(outDir, "index.html"), `<!doctype html><script type="module" src="/assets/${entry}"></script>`);
const sum = total.reduce((a, b) => a + b, 0) + entryText.length;
console.log(JSON.stringify({ files: chunkCount + 1, bytes: sum, biggest: Math.max(...total) }));
Originally created by @jgt87 on GitHub (Oct 28, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7740 ## 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. Reproduced on rea-agents 6.8.0 (released) and on main 6a226bd09. | Same fixture (7 files, 905 `api.rpc("proc_…")` calls) | 6.8.0 (npm, Node 26.12.1) | main 6a226bd09 (source, Node 24.22.1) | | --- | --- | --- | | Occurrences of `proc_` in the complete Evidence (76,557,994 bytes on 6.8.0) | 0 | 0 | | `call-site` nodes / with empty `properties` | 4,563 / 4,563 | 4,563 / 4,563 | ## Area JavaScript application analysis, semantic graph (`analyze_javascript_application`). ## Current limitation String literals passed as call arguments are not retained anywhere in the application or semantic graph unless a specific extractor owns the call (`fetch`, storage, IPC, and similar). A remote-procedure wrapper is a common case in bundled web applications: ```js export function f0_0(a,b){ /* ... */ return api.rpc("proc_0_0",{v:c,d}).then(r=>r.data); } ``` On a synthetic 7-file build with 905 such calls (generator `gen-vite-like.mjs`), the complete Evidence (76 MB) does not contain the string `proc_` at all. The semantic graph has 4,563 `call-site` nodes with empty `properties`, and 7 nodes labeled `rpc` (one `function`, six `property-slot`), none with an argument value. Storage keys in the same functions are retained (`storage` nodes, `identity.key`), because the storage extractor owns them. In the original (non-synthetic) case on 6.4.0, the semantic graph had 101 literal values in 21,527 nodes, and calls such as `.rpc('name')` showed without the name. ## Use cases - Map the server procedures or commands a bundle can call, without running it. - Seed `trace_javascript_semantics` or `trace_application_feature` with an operation name that appears only as a call argument. ## Desired outcome and success criteria A call site keeps its literal arguments (strings, numbers, booleans; or a bounded prefix with a truncation flag) as properties, for example `arguments: [{index: 0, kind: "string", value: "proc_0_0"}]`, with the same authority and coverage rules as other static facts. Success: in the synthetic fixture, each `api.rpc` call site carries its procedure name. ## Expected interaction with targets Reads existing files only (static analysis of inert text). ## Related - #3539 (surface path- and URL-shaped literals passed to project request wrappers; narrower, `analyze_web_bundle`) - #7500 (forward influence stops at `+` and template-literal operands) ## Verification available so far Reproduced on rea-agents 6.8.0 (npm, Node 26.12.1) and upstream `main` at 6a226bd09 (source, Node 24.22.1), Windows 11 Pro (10.0.26300): ```sh node gen-vite-like.mjs ./fx/small 6 40 rea analyze-javascript-application ./fx/small/assets --artifact-format directory --format json > out.json grep -c "proc_" out.json # 0 ``` <details><summary><code>gen-vite-like.mjs</code> (the synthetic fixture generator)</summary> ```js // Generates a synthetic Vite-like build output. No real application code. // Usage: node gen-vite-like.mjs <outDir> [chunkCount] [bigChunkKB] [first|last] // Writes <outDir>/index.html and <outDir>/assets/*.js. Each chunk has an // absolute-base __vite__mapDeps table, real dynamic imports, localStorage // get/set calls and api.rpc("proc_N_M") calls. Deterministic output. import { mkdirSync, writeFileSync, rmSync } from "node:fs"; import { join } from "node:path"; const outDir = process.argv[2]; const chunkCount = Number(process.argv[3] ?? 87); const bigKB = Number(process.argv[4] ?? 456); if (!outDir) throw new Error("outDir required"); rmSync(outDir, { recursive: true, force: true }); const assets = join(outDir, "assets"); mkdirSync(assets, { recursive: true }); let seed = 7; const rand = () => ((seed = (seed * 1103515245 + 12345) % 2147483648) / 2147483648); const names = Array.from({ length: chunkCount }, (_, i) => `chunk${String(i).padStart(2, "0")}-${(i * 2654435761 >>> 0).toString(36).slice(0, 6)}.js`); const entry = "index-a1b2c3.js"; function body(i, targetBytes) { const parts = []; let n = 0; let size = 0; while (size < targetBytes) { const f = `f${i}_${n}`; const key = `demo.pref.${i}.${n % 5}`; const fn = `export function ${f}(a,b){const c=a*${n}+b;` + `if(c>${Math.floor(rand() * 1000)}){localStorage.setItem("${key}",String(c));}` + `const d=localStorage.getItem("${key}");` + `return api.rpc("proc_${i}_${n % 7}",{v:c,d}).then(r=>r.data);}\n`; parts.push(fn); size += fn.length; n++; } return parts.join(""); } const total = []; const deps = names.map((n) => `assets/${n}`); for (let i = 0; i < chunkCount; i++) { const bigIndex = process.argv[5] === "last" ? chunkCount - 1 : 0; const target = i === bigIndex ? bigKB * 1024 : 20 * 1024 + Math.floor(rand() * 12 * 1024); const next = (i + 1) % chunkCount; const other = (i + 7) % chunkCount; const head = `import{api}from"./${entry}";\n` + `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify([deps[next], deps[other]])})))=>i.map(i=>d[i]);\n` + `export const load${i}=()=>import("./${names[next]}");\n` + `export const pre${i}=()=>api.preload(()=>import("./${names[other]}"),__vite__mapDeps([0,1]));\n`; const text = head + body(i, target); writeFileSync(join(assets, names[i]), text); total.push(text.length); } const entryText = `const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=${JSON.stringify(deps)})))=>i.map(i=>d[i]);\n` + `export const api={rpc(name,args){return fetch("/rpc/"+name,{method:"POST",body:JSON.stringify(args)}).then(r=>r.json());},preload(f,d){return f();}};\n` + names.map((n, i) => `export const route${i}=()=>api.preload(()=>import("./${n}"),__vite__mapDeps([${i}]));\n`).join("") + `const saved=localStorage.getItem("demo.session.token");if(saved){route0();}\n`; writeFileSync(join(assets, entry), entryText); writeFileSync(join(outDir, "index.html"), `<!doctype html><script type="module" src="/assets/${entry}"></script>`); const sum = total.reduce((a, b) => a + b, 0) + entryText.length; console.log(JSON.stringify({ files: chunkCount + 1, bytes: sum, biggest: Math.max(...total) })); ``` </details>
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#4631
No description provided.