Repository navigation
Description
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 caps semantic graph nodes at the 20,000-per-file ceiling, but keeps every semantic unknown. Unknowns grow with the source after the node ceiling has cut in, so they become most of the result.
On a generated 1.66 MB file, the graph keeps 20,000 nodes and 160,000 unknowns. The unknowns are 99.9 MB of a 126.5 MB JSON result. Coverage reports truncated: true and lists only the two node ceilings under limits.
| statements | source bytes | nodes | unknowns | unknowns JSON bytes | result bytes | wall s | peak RSS |
|---|---|---|---|---|---|---|---|
| 1,000 | 39,690 | 7,001 | 4,000 | 2,625,835 | 9,074,770 | 5.6 | 410 MiB |
| 10,000 | 406,890 | 20,000 | 40,000 | 24,908,043 | 41,914,821 | 18.5 | 594 MiB |
| 40,000 | 1,660,890 | 20,000 | 160,000 | 99,932,043 | 126,543,218 | 45.0 | 903 MiB |
Every statement gives four unknowns (two ambiguous-target and one dynamic-property in object-flow, one dynamic-call in call-flow), whether or not its nodes were kept. Each unknown ID then appears a second time under coverage.families[].unknown_ids.
Real Electron apps show the same shape. Discord's Linux app.asar (3.5 MB, 5 files, 3,074,650 text bytes read) gives 94,013 unknowns, about 59.7 MB of a 108 MB normalized_result, beside 20,968 nodes. Teams for Linux (17.7 MB app.asar, run with --integrity-policy record-and-continue) and Antigravity (2.1 MB app.asar beside 19 MB unpacked) gave 289 MB and 303 MB of TOON output, with 3.5 GiB and 3.0 GiB peak RSS.
Reproduction
case_root="$(mktemp -d)"
mkdir "$case_root/fixture"
python3 -c '
for i in range(40000):
print(f"globalThis.u{i%50}.p{i}(globalThis.v{i%7}[k{i%3}]);")
' > "$case_root/fixture/main.js"
rea analyze-javascript-application "$case_root/fixture" --format json > "$case_root/result.json"
jq -r '.normalized_result.semantic_graph
| "nodes=\(.nodes|length) unknowns=\(.unknowns|length) unknown_bytes=\(.unknowns|tojson|length) truncated=\(.coverage.truncated)"' \
"$case_root/result.json"
# nodes=20000 unknowns=160000 unknown_bytes=99932043 truncated=trueChange range(40000) to range(1000) or range(10000) for the other rows.
Expected behavior
Unknowns have a budget, as nodes do. Past it, the graph records how many unknowns of each family and reason were left out, and lists the budget under coverage.limits. Then the result size follows the configured ceilings and not the source length.
Impact and scope
Any large or minified bundle. CLI users get a result tens of times larger than the input (76x for the 1.66 MB file). Agents reading the CLI output, and anything that keeps the complete Evidence, pay for the unknowns in time, memory and bytes. --max-semantic-nodes and --max-semantic-nodes-per-file do not reduce them: with both at 1,000, the 10,000-statement file keeps 1,000 nodes and all 40,000 unknowns.
REA version
[email protected] (npm), and main at 2cf9c28
Runtime and platform
Node 24.18.0, Fedora 44 x86_64
Target and artifact context
Generated JavaScript (recipe above). Real-app figures come from the Flathub packages of Discord and Teams for Linux, and from Antigravity's Linux install.
Interface
CLI
Earliest failure stage
Native analysis or reconstruction
Relevant output or evidence
Unknown reasons on the 40,000-statement file:
80000 ambiguous-target object-flow
40000 dynamic-call call-flow
40000 dynamic-property object-flow
The same file on 6.5.0: 126,545,939 bytes, 20,000 nodes, 160,000 unknowns, 69.5 s, 1.24 GiB peak RSS.
I also ran it on open PR #2092 (head b6a097b), which replaces the default node ceilings. That run kept 280,001 nodes and the same 160,000 unknowns: 353,160,381 bytes, 160 s, 2.7 GiB peak RSS. On Discord, #2092 kept 317 unknowns, but 190,560 nodes and 249 MB. So #2092 does not bound this case.
No reference in unknowns dangles: every evidence.context_id, node_id and candidate_node_ids entry resolves, at 1,000 and at 40,000 statements.
Typed outcome or recovery expected
A completed result with coverage.truncated: true, a named unknowns limit in coverage.limits, and per-family omitted counts for the dropped unknowns.
Analysis or suspected cause
adoptFileProjection in src/application/javascript/JavaScriptSemanticGraphBuilder.ts (lines 334 to 355) checks new nodes against min(fileNodeCeiling, nodeCeiling - state.nodes.size) and throws if they exceed it. The loop just below adopts every projection.unknowns entry with no budget. recordUnavailableFile (lines 388 to 405) and the getter-expansion path (lines 570 to 592) add unknowns without one too.
I have not looked at where the projection decides to emit an unknown for a statement whose nodes were dropped.
Additional context
#1065 says complete Evidence keeps its unknowns, so if keeping all of them is intended, a page or a count-only view for unknowns would work instead. I can send a PR for whichever shape you prefer.
Activity
fix(javascript): bound semantic unknown retention at projection
morluto commentedon Oct 17, 2026
Fixed on main in f4322b1.
Unknown admission now happens at one producer owner before canonical IDs and provenance are interned. Every projection path uses it, including unavailable sources and getter-expansion frontiers. Independent defaults are 100000 unknowns/application and 20000/source; callers can select
max_semantic_unknowns/max_semantic_unknowns_per_file(CLI flags use hyphens). A typed capacity offer travels from the parent to its owned worker. The streamed decoder and graph accumulator independently reject excess rows.Coverage is partial/truncated when unknown retention drops emissions, names both unknown ceilings, and includes bounded
omitted_unknown_occurrencesrows by family/reason. These count examined emission occurrences, including repeats—not distinct discarded identities or frontiers in unexamined source. No discarded-ID set is retained. Raw source and existing node/execution/memory coverage remain authoritative. The node-only file truncation flag was renamed to distinguish it from overall graph truncation. Omission-free graph shape and identity remain unchanged.Executed checks: 89 focused checks across ten files passed, including real owned-worker lifecycle controls, authenticated transfer over-allocation, direct/transferred equivalence, zero remaining capacity, graph traversal, CLI/stdio MCP exact Evidence/result parity and SDK-converted schema validity. A final three-file-capacity control rerun passed. Build, typecheck/lint, docs checks (five README checks), commit and push hooks passed. Two independent read-only source reviews found no blockers.
Before/after actual CLI control: four inert dynamic statements with a one-node ceiling retained 16 unknowns on baseline 926e18b. With the added two-unknown offer, the fix retains two and reports 14 omitted emission occurrences. Two-file CLI/MCP control retains three and reports 29 omitted occurrences, with matching Evidence identity across adapters.
18 completed small CLI cost tasks compared ordinary, unrestricted dynamic and explicitly bounded dynamic sources; first processes were separated from two alternating repeats. Ordinary/unrestricted replies, including Evidence IDs, were byte-identical. Bounded reply estimate: 8813→5492 cl100k tokens, intentionally reduced retained detail. Repeat medians for that task: elapsed 1.213→1.226s, CPU 1.262→1.269s, sampled process-family peak 524.1→522.0 MB. No speedup, true-cold or equivalent-retained-coverage claim; whole-agent/catalog/MCP and large-input costs unmeasured.
The reported 40000-statement stress run and existing heavy builder fixtures were not executed. All sources used here were tiny and inert; no investigated target execution or heavy profiling. This verifies the capacity mechanism, not real-app peak memory or upstream IR boundedness. Context7's Zod optionality docs informed omission preservation, checked against installed Zod 4.4.3 and actual schema/client tests; DeepWiki's compiler diagnostic analogy did not establish a resource bound and was not used as proof.
fix(javascript): bound semantic unknown retention at projection