Repository navigation
Conversation
The cold-build parse loop was a single synchronous files.forEach — every tree-sitter extraction serialized on the main thread (~1.75ms/file on a 4k-file TS repo, ~7.8s of the 7.9s cold build). Cache-miss files now go to a small worker pool (default 4, GRAFT_PARSE_CONCURRENCY override, 1 = inline fallback); results merge strictly in file order, so node/edge/ entry output stays byte-identical to the sequential loop. - parse-worker.ts: worker half — warms its partition's WASM grammars once, parses sequentially, streams one result per file - parse-pool.ts: parent half — strided partition, per-call pool, loud failure when a worker dies mid-partition (no silent partial graph) - build.ts: classify/read/hash/replay on main (cache hits never spawn work), pool for misses, ordered merge restoring the exact per-file push/set/count semantics of the old loop Cold 4k-file repo: 7.85s → 3.95s at 4 workers (1.99×); 2.2s at 8. 1351 pass / 6 pre-existing env failures unchanged. Co-Authored-By: Claude Code <noreply@anthropic.com>
🌱 graft blast radius1 area changed → 6 areas can be affected. 16 dependent symbols, depth 2. flowchart TB
A0(("Pull Request Review<br/>8 symbols"))
A1(("Graph Freshness<br/>3 symbols"))
A2(("CLI Engine<br/>2 symbols"))
A3(("Viewer Build<br/>1 symbol"))
A4(("MCP Tools<br/>1 symbol"))
AX(("1 smaller area<br/>1 symbol"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2,A3,A4 reached;
classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
class AX tail;
Who knows this code — 3 people across 7 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 16 dependent symbols, grouped by areaPull Request Review — 8 symbols in 6 files
Graph Freshness — 3 symbols in 2 files
CLI Engine — 2 symbols in 2 files
Viewer Build — 1 symbol in 1 file
MCP Tools — 1 symbol in 1 file
Sync Execution — 1 symbol in 1 file
Test signal per changed area — 1 ⚠Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
33 test suites also reference this code41 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
new Worker(new URL('./parse-worker.js', import.meta.url)) assumed tsc's
dist layout. Under tsx from src/ the sibling is parse-worker.ts, and the
.js URL is opaque to module resolvers — tsx's remap of Worker URLs is
version-dependent and Node 20 CI resolved to a missing file: every
cache-miss build died with ERR_MODULE_NOT_FOUND and dragged the whole
ask/ranking test file set down with it.
Now the entry is whichever of parse-worker.{js,ts} actually exists next
to this module; a .ts entry spawns with an explicit --import tsx
execArgv, since workers don't reliably inherit the parent's loader
registration across Node versions (Node 20 didn't — the other half of
the same failure).
Co-Authored-By: Claude Code <noreply@anthropic.com>
ERR_UNKNOWN_FILE_EXTENSION '.ts' on Node 20 CI: the .ts fallback spawned with an explicit --import tsx execArgv, but loader registration inside worker threads is version-dependent and Node 20 didn't register tsx. Stop depending on worker-side loaders entirely: from the src layout, point the Worker at the compiled dist/graph/parse-worker.js (plain JS, always present — prepare builds it before tests/CLI), and give compiled entries a clean execArgv, the one configuration every supported Node agrees on. The .ts sibling stays only as a last-resort fallback with the loader stated. Co-Authored-By: Claude Code <noreply@anthropic.com>
'../../../dist/...' from src/graph/ climbed past the package root to /work/dist/, so on CI the resolver fell through to the .ts sibling again (and only Node ≤20's missing worker-side tsx registration made that fatal). Two levels: graph → src → root. Proven by hiding parse-worker.ts: a src-layout build now resolves the dist entry and succeeds. Co-Authored-By: Claude Code <noreply@anthropic.com>
Problem
The cold-build parse loop is a single synchronous
files.forEach— every tree-sitter extraction serializes on the main thread. On a 4,000-file TypeScript repo that is ~7.8s of the 7.9s cold build (graft build, no--deep), one core busy while the rest of the machine idles.Change
Cache-miss files now go to a small pool of worker threads:
graph/parse-worker.ts— the worker half. Receives one strided partition of the misses, warms the WASM grammars its partition needs once, parses strictly sequentially, streams one result message per file.graph/parse-pool.ts— the parent half. Partitions strided, spawns one pool per build, terminates after. A worker that dies mid-partition rejects loudly — no silent partial graph.graph/build.ts— the loop splits into three passes:Pass 3 is the determinism contract: node/edge/entry output is merged by the caller's file index, never by completion order, so a cold build is byte-identical to the single-threaded loop regardless of which worker finished first — the invariant
test/graph-incremental.test.tspins down.Knob
GRAFT_PARSE_CONCURRENCY(default 4, capped by CPUs and by the miss count).1skips the pool entirely and runs the misses inline — the old single-threaded behaviour, kept as an escape hatch.Measurements
Synthetic 4,000-file TS repo, cold build (3 runs each, medians):
GRAFT_PARSE_CONCURRENCY=1)Default kept at 4 (not 8) deliberately — graft often runs alongside other work (MCP refresh, the Stop hook), and the env knob is there for dedicated boxes.
Output equality
wiring.jsonis sha256-identical across sequential / inline / pool on the same input. (The extract-cache filename/stamp differs between build environments — it hashes build artifacts, not extractor behaviour; cache contents are JSON-parse-identical.)Tests
1351 pass / 6 fail — the same 6 fail on clean unmodified
mainin this environment (the git worktree/seed/refresh set:graph-refresh.test.ts:102etc.). Baseline recorded before any change; the set is unchanged after this patch.🤖 Generated with Claude Code