Repository navigation
Description
Summary
The parallel pipeline silently drops CALLS edges. On a 122-file Next.js/TypeScript repo it loses 21% of the call graph (519 -> 408), and the default configuration is the broken one.
You already know the two pipelines disagree — d9ea73f says so:
a new deliberately-RED
repro_seq_parallel_equivalencetracks the remaining OPEN bug that the sequential and parallel pipelines systematically disagree (~3459 USAGE / ~1666 WRITES lines on xfs) -- separate code paths, fix deferred.
I could not find a GitHub issue for it, so here is one — with the exact failure rule, a public corpus that reproduces it, and the file-count threshold below which it is invisible.
tests/repro/repro_parallel_determinism.c:54 hardcodes RPD_CORPUS "/Users/martinvogel/perf-bench/linux/fs/xfs" and SKIPs when absent, so CI never runs it. The header notes synthetic corpora failed to reproduce. Both problems are solved below.
Version: v0.9.0, macOS arm64.
The failure rule
An edge is dropped if and only if the target is a JSX component imported through a tsconfig paths alias.
I classified all 519 sequential CALLS edges by import style x usage. The confusion matrix is perfect:
| import style | usage | kept | lost | loss |
|---|---|---|---|---|
@/ tsconfig alias |
JSX | 0 | 122 | 100% |
@/ tsconfig alias |
call | 115 | 0 | 0% |
relative ./ |
JSX | 37 | 0 | 0% |
relative ./ |
call | 4 | 0 | 0% |
| external (npm) | call | 14 | 0 | 0% |
Alias-imported function calls resolve fine. Relative-imported JSX resolves fine. Only the intersection fails. The parallel import_map heuristic appears to apply alias mapping to call expressions but not to JSX element references.
Concrete case — src/app/(dashboard)/_components/dashboard-kpis.tsx:
import { KpiCard } from "@/components/ui/kpi-card"; // alias import
...
<KpiCard ... /> // rendered at lines 32, 40, 47sequential: DashboardKpis -> KpiCard (strategy=unique_name)
parallel: (no edge)
USAGE does not compensate. So on a default index, "who renders <KpiCard>?" silently returns nothing. For a React/Next.js codebase, where most of the call graph is JSX composition through aliases, this removes the majority of the component graph.
It is a code-path split, not a race
Do not look for a lock. I fingerprinted the full CALLS edge set (src,tgt,line,strategy -> sha256) across 21 runs (workers 1,2,3,4,6,8,12 x 3):
| workers | CALLS (3 runs) | CALLS fingerprint | Routes | self-loops |
|---|---|---|---|---|
| 1 | 519 519 519 | 30eff468830d627a |
1 | 0 |
| 2 | 408 408 408 | d704b71f31522558 |
5 | 8 |
| 3 | 408 408 408 | d704b71f31522558 |
5 | 8 |
| 4 | 408 408 408 | d704b71f31522558 |
5 | 8 |
| 6 | 408 408 408 | d704b71f31522558 |
5 | 8 |
| 8 | 408 408 408 | d704b71f31522558 |
5 | 8 |
| 12 | 408 408 408 | d704b71f31522558 |
5 | 8 |
Every parallel run is bit-identical, at every worker count. Two workers is already fully broken; twelve is no worse. A data race cannot produce that. This is consistent with pipeline.c:1336-1337 dispatching to two separate implementations:
? run_parallel_pipeline(p, ctx, files, file_count, worker_count, &t)
: run_sequential_pipeline(p, ctx, files, file_count, &t);Nothing resource-related helps, which fits: at workers=8, all of CBM_TS_TYPE_BUDGET=100000000, CBM_MEM_BUDGET_MB=12000, CBM_RETAIN_TOTAL_MB=8000, CBM_WALK_DEFS_MAX=1000000, and CBM_INDEX_SUPERVISOR=0 still yield exactly 408.
Threshold: the parallel path engages at 51 File nodes
Bisected: 50 files -> sequential path, 51 files -> parallel path. Below 51 the bug is invisible.
This is almost certainly why the synthetic corpora in the test header failed to reproduce. A small fixture will pass and look fixed. Any regression test must exceed 51 files.
Minimal shape: one Consumer.tsx importing three things — a JSX component via alias, a JSX component via relative path, and a plain function via alias — plus >=48 padding files.
| files | workers | AliasCard (JSX+alias) | RelCard (JSX+rel) | aliasHelper (call+alias) |
|---|---|---|---|---|
| <=52 | 1 or 8 | unique_name |
lsp_ts_jsx_import |
lsp_ts_import |
| >=53 | 1 | unique_name |
lsp_ts_jsx_import |
lsp_ts_import |
| >=53 | 8 | LOST | import_map |
import_map |
Public corpus: this repository reproduces it
No local checkout needed — the tool reproduces the divergence on its own source:
git clone https://github.com/DeusData/codebase-memory-mcp
for w in 1 8; do
CBM_WORKERS=$w codebase-memory-mcp cli index_repository \
--repo-path ./codebase-memory-mcp --mode full --name "si-$w"
codebase-memory-mcp cli query_graph --project "si-$w" \
--query "MATCH ()-[r:CALLS]->() RETURN count(*)"
done| workers | CALLS (3 runs) |
|---|---|
| 1 | 35534 / 35534 / 35534 |
| 8 | 35510 / 35510 / 35510 |
Deterministic, 24 edges lost, on a corpus already present in CI. This can replace the hardcoded RPD_CORPUS.
Fabricated edges and nodes (parallel only)
519 -> 408 decomposes as 391 survive, 128 lost, 17 fabricated.
8 phantom self-loops (f -> f), all strategy=callee_suffix, confidence=0.50, candidates=0. Every callee is an unresolvable builtin/library member call — templateCache.get, formData.get, request.headers.get, response.headers.get, promptMap.get, Drawer.Handle, test.use. The resolver finds zero candidates and still emits an edge, pointing at the enclosing function itself. This sets Function.recursive = true on 7 functions in a repo with zero recursive functions (all with self_recursive: false).
Fix: on an empty candidate set, emit no edge. The README already states this invariant for the Perl resolver ("unresolved receivers emit no edge (zero-edge guarantee)"); the TS path does not honor it.
4 fabricated Route nodes (/api/sendText, /api/sessions, /api/sessions/:name, /api/:session/auth/qr) with empty source and empty file_path. These are outbound URLs passed to a local helper: request("POST", "/api/sendText", payload). Sequentially this resolves to a normal CALLS edge and creates no Route. In parallel the call goes unresolved and a heuristic promotes the path-like string literal into a Route the project supposedly serves.
Mode does not interact
Constant -111 CALLS penalty in every mode. Route fabrication and self-loops appear in all three.
| mode | workers=1 | workers=8 |
|---|---|---|
| fast | 431 | 320 |
| moderate | 431 | 320 |
| full | 519 | 408 |
Sequential is not a perfect oracle either
Worth knowing before you "fix" parallel by making it match sequential: parallel actually corrects 9 edges that sequential gets wrong.
Sequential's lsp_ts_namespace collapses multiple call sites sharing a method name into a single edge, keeping the first target and the last line. In src/actions/appointments.ts, appointmentRepository.create (L58) and reminderRepository.create (L62, L63) collapse into ONE edge pointing at appointment-repository.create at line 63 — wrong receiver, wrong line — and L58 is dropped. Parallel resolves both correctly. Same pattern for findAll in src/app/(dashboard)/page.tsx:31,32.
So the correct framing is: sequential is correct for JSX/alias resolution; parallel is correct for same-named-method receiver disambiguation. Neither is a superset. Converging the paths should take the union, not simply adopt one.
Two adjacent traps
CBM_INDEX_SINGLE_THREAD is strict-parsed as "1". =true is accepted without error and silently does nothing:
CBM_INDEX_SINGLE_THREAD=1 -> CALLS=519 (also overrides CBM_WORKERS=8)
CBM_INDEX_SINGLE_THREAD=true -> CALLS=408 (silently ignored)
Re-indexing an existing project is a silent no-op. index_repository --mode full on an unchanged repo returns the existing graph without re-resolving. Re-indexing a workers=8 project (408) with CBM_WORKERS=1 still yields 408; you must delete_project first. Anyone trying the single-threaded workaround on an already-corrupted index will see no change and conclude it does not work.
Impact
trace_path is the flagship feature, and under the default configuration it silently omits ~21% of call relationships on real TypeScript — including essentially the entire JSX component graph. There is no signal to the user: node counts are identical, nothing is logged, and the bad edges carry confidence: 0.95 (import_map), so confidence cannot be used to detect a degraded graph either.
Suggestions
- Fix alias resolution for JSX element references on the parallel path (the whole 21%).
- On
candidates=0, emit no edge — kills the phantom self-loops, the falserecursiveflags, and the fabricated Routes. - Unpin
RPD_CORPUS; this repository reproduces it. Ensure any regression fixture exceeds 51 files. - Accept
true/yes/onforCBM_INDEX_SINGLE_THREAD, or error on unparseable values. - Until the paths converge, consider defaulting to sequential or logging that the parallel graph is known-lossy. Serial costs 0.43s vs 0.33s on 122 files.
Separate, lower severity
SEMANTICALLY_RELATED is nondeterministic across identical runs at every worker count including 1 (observed 18-22 on repeated full indexes of one tree). All structural edges and node counts are bit-stable. This is not the 519-vs-408 bug and looks like #998, whose fix (#1014) appears not to have shipped in v0.9.0.
Activity
DeusData commentedon Jul 14, 2026
Thanks for the precise public repro and the sequential/parallel comparison. This is a high-priority 0.9.1-rc parsing-correctness bug because the default pipeline silently loses a material part of the call graph. The fix needs a portable regression that exercises the JSX alias case without a machine-local corpus path and proves sequential/parallel edge equivalence for this class.
zavyrylin commentedon Jul 15, 2026
Looks wider than JSX — I'm hitting this in Python too
Ran into this on v0.9.0 (macOS), mixed Python/TS repo, ~8.6k nodes. Did an A/B to be sure: same commit, clean tree, same mode, fresh index each time (separate
--name, so nothing merges into an existing graph). Only thing that changes isCBM_INDEX_SINGLE_THREAD.CBM_INDEX_SINGLE_THREAD=1The interesting part: the LSP tier comes out identical.
lsp_direct907/907,lsp_method781/781,lsp_ts_local315/315,lsp_builtin290/290,same_module196/196. Not one edge lost there. Everything that goes missing is the registry/fallback tier:unique_name1880 → 248 (−87%)suffix_match587 → 291lsp_ts_import277 → 92fastapi_depends29 → 0lsp_ts_namespace59 → 0And they don't just differ in counts, they emit different strategies altogether. Parallel is the only one producing
import_map(586),callee_suffix(49),field_type_hint(13), plus 117 edges with an emptystrategy. Sequential is the only one producinglsp_ts_namespaceandfastapi_depends. Reads like two code paths to me, not a race.Smallest repro I've got — plain relative import, nothing aliased, no JSX anywhere near it:
The IMPORTS edge is there in both, it's the CALLS edge that goes missing. Import isn't aliased, so I don't think #875/#979/#1049 cover this one.
What makes it nasty is how quiet it is.
trace_pathhands you an emptycallerslist instead of erroring, so anything over the 50-file threshold will cheerfully tell you a function has no callers when it does.Can post full histograms or test a patch if that's useful.
DeusData commentedon Jul 15, 2026
Thanks, this confirms that the defect is broader than JSX aliases and strengthens the two-code-path diagnosis. The clean A/B comparison with fresh project names is exactly the evidence we need.
Please post the full per-strategy histograms. If you can test a patch, the regression fixture should remain above the 50-file parallel-pipeline threshold and assert both Python relative-import CALLS and TypeScript JSX-alias CALLS. We should require sequential and parallel output to converge through a shared pipeline-correctness fix rather than patching only the JSX heuristic.
DeusData commentedon Jul 15, 2026
Fixed on main by PR #1106 (merged in 548927a), shipping in v0.9.1.
Your diagnosis was exactly right, and I pinned it to the precise line: the parallel resolver drops a CALLS edge whenever the LSP produces a resolution whose
callee_qnisn't a node in the graph buffer — the registry fallback lived in anelsethat ran only when there was no LSP resolution at all. So for an alias-imported JSX component, the TS LSP resolves<KpiCard>to an alias-path QN that never matches a def node (lsp_target == NULL), and the parallel path discarded the edge instead of falling through to the registry's import_map/unique_name resolution — which is exactly what the sequential path does. That's the seq/parallel divergence.The fix runs the registry fallback whenever the LSP didn't yield a gbuf-resolvable target, restoring parity. Verified on a 55-file alias+JSX fixture: 0 → 55 CALLS edges on the parallel path, and the reproduce-first guard (
calls_jsx_component_via_tsconfig_alias_parallel_issue1085) pads past the 50-file threshold so it actually exercises the parallel path — RED before, GREEN after.Thanks for the exceptional report — the confusion matrix (alias×JSX = 100% loss, every other cell 0%) and the ">51 files" threshold note are what made this fast to isolate. Retest on v0.9.1 welcome.
fix(pipeline): restore parallel CALLS resolution for LSP-unresolvable…
fix(pipeline): restore parallel CALLS resolution for LSP-unresolvable…
feat(diagnostics): make superlinear passes visible from an ordinary run