Task #5 step 2. The fuse-then-link refactor (#1364) left the extraction sweep as a clean per-file boundary: raw text in → collected facts out. This ports that sweep to the native kernel: `cfnptr_scan_files` (codegraph-kernel/src/cfnptr.rs) strips and scans a batch of 16 files per NAPI call, and the TS side only reads files, ships batches, interns the returned facts, and resolves include paths. The JS sweep remains as the fallback (no binary, feature detection against older binaries, CODEGRAPH_KERNEL=0, or CODEGRAPH_KERNEL_CFNPTR=0). Parity discipline: the JS regexes are the spec, so the scanners are hand-rolled byte machines reproducing that engine — ASCII \w/\b next to UNICODE \s (NBSP/U+2000-200A/FEFF decoded from UTF-8), alternation order, lastIndex resume, and the observable backtracking dimensions (INIT/ARRAY modifier and struct/star/bracket optionals, DISPATCH's greedy segment loop); greedy shortcuts only where backtracking provably can't rescue a match. The native stripper blanks per UTF-16 code unit, so its output is string-identical to the TS stripper — pinned by a new kernel arm on the strip differential oracle (fixtures + 500 seeded random cases). Gates, all green: new differential suite (adversarial fixture project — CRLF, NBSP, continuations, decoy strings, unterminated comments, backtracking shapes — indexed native-vs-JS: identical edge streams, plus a record-level scanner check); repo differential on git/redis/vim/SameBoy (identical, 705/852/433/180 edges); probe-hash on the live linux kernel DB reproduced f6e1713d… (279,335 rows); linux init counts exact 2,049,153/6,413,518; dump sha 6dd1185b… reproduced (10,446,478 lines); full suite green ×2 (153 files / 2588 tests). Measured (8c cg1212, quiet host): cFnPtr sub A=47.9s B=1.1 C=40.9 D=24.1 E=36.8 = 150.9s vs step 1's 179s and the pre-arc 230s (−34% cumulative); the sweep itself halved (94.5→47.9s, JS strips 132.4k→68.9k). callback-synthesis phase 199.9→171.1s. E's attributed wall grew from overlap shift under parallel synthesis; the phase total is the honest number. Full record: plan §7a.10. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1096 lines
76 KiB
Markdown
1096 lines
76 KiB
Markdown
# Rust extraction-kernel migration plan + post-kernel roadmap
|
||
|
||
**Audience:** the agent/engineer executing the native-kernel project. Self-contained handoff:
|
||
context, current state, per-language tracker, gates, and the follow-on roadmap.
|
||
**Companion:** `docs/design/native-extraction-kernel.md` (architecture + spike detail).
|
||
**Written:** 2026-06-12 planning → executed 2026-07-16/17. **R1–R6 ARE DONE.** The shipped
|
||
records live in §3a and §4a–§4f; the per-language tracker is current; §0a is the
|
||
cold-start handoff for the next session. Read §0 + §0a first — parts of §1/§6 below
|
||
them are the ORIGINAL plan and carry expectations that measurement later corrected
|
||
(each is annotated where superseded).
|
||
|
||
---
|
||
|
||
## 0. Status checklist (R1–R6 done; what remains)
|
||
|
||
- [x] **R1. Scaffold the napi-rs crate** — done 2026-07-16, §3a. Buffer contract v1,
|
||
routing + per-file wasm fallback, kill switch, build/release wiring,
|
||
grammar-source-parity CI.
|
||
- [x] **R2. Port TypeScript/JavaScript (tsx/jsx)** — done 2026-07-16, §4a. The generic
|
||
`.scm` emitter was SUPERSEDED by bespoke per-language walkers (queries can't
|
||
express extraction parity); byte-parity from day one of the harness.
|
||
- [x] **R3. TS/JS equivalence gate → DEFAULT-ON** — done 2026-07-16, §4b. Dumps
|
||
byte-identical (express/excalidraw/vscode + flask control). Found + fixed:
|
||
encoding-dependent error recovery → per-file `defer:` policy.
|
||
- [x] **R4. Java (incl. Lombok synthesis) → DEFAULT-ON** — done 2026-07-16, §4c.
|
||
dubbo 441k-row dump byte-identical. Found + fixed: node-ID-collision dedupe
|
||
(cross-language). Found: many-core parse-loop wall is NOT extraction (→ §4d).
|
||
- [x] **Direct-to-store decode** — done 2026-07-16, §4d. Main thread never
|
||
materializes nodes; measured: the many-core fresh-index wall is single-writer
|
||
SQLite ingest (94% of dubbo's parse-loop) — a store-architecture arc, out of
|
||
scope here.
|
||
- [x] **R5. Python + Go → DEFAULT-ON** — done 2026-07-16, §4e. django 360.8k /
|
||
prometheus 213.8k row dumps byte-identical; 2-CPU envelope 1.32× / 1.46×.
|
||
- [x] **R6. Kernel-scale re-validation (cg1212)** — done 2026-07-17, §4f. No
|
||
regression (26.4min vs ~27min, identical 2.05M-node graph). The "parse 6m→2m"
|
||
premise was wrong for THIS repo: the Linux tree is ~99% C (unported T2) —
|
||
the expectation transfers to the C/C++ port.
|
||
|
||
**Open, in recommended order (rationale in §0a):**
|
||
|
||
- [x] **O1. Merge the `rust-kernel` branch** — DONE 2026-07-17: PR #1326, **merge
|
||
commit** (the integration-branch exception — 9 milestone commits preserved),
|
||
main tip `c1dc78d`. Suite green pre-merge (2,472 passed / 4 skipped,
|
||
`CODEGRAPH_KERNEL_EXPECT=1`).
|
||
- [x] **O2. Windows VM validation** — DONE 2026-07-17. Guest (ARM64 Win11):
|
||
rustc 1.97.1 aarch64-pc-windows-msvc + MSVC Build Tools (VCTools workload +
|
||
VC.Tools.ARM64 + Win11 SDK; installed via **scheduled task** — Windows sshd
|
||
kills detached children on session close, `schtasks` is the survival
|
||
pattern). `build-kernel.sh --target aarch64-pc-windows-msvc` builds native
|
||
win32-arm64 in ~2min; **all three kernel suites green with
|
||
`CODEGRAPH_KERNEL_EXPECT=1` (33/33)**. The leg EARNED ITS KEEP: the guest's
|
||
autocrlf checkout exposed a real CRLF parity bug (docstring cleaning; JS
|
||
multiline `^` anchors after `\r` — §0a traps) — fixed + CRLF fixtures pinned
|
||
cross-platform in #1329. Every prebuild target platform is now validated.
|
||
- [ ] **P1. Kernel-scale resolution speed** (§7a) — measurement round RUN 2026-07-17
|
||
and it RESHAPED the arc (full record §7a.1); items (1) WAL containment and
|
||
(2) memory-aware/cgroup-honest sizing **SHIPPED same day (#1332–#1335,
|
||
§7a.2)** after an implementation arc whose three failed/diagnostic
|
||
kernel-scale runs each corrected the design (WAL file ≠ WAL backlog;
|
||
cgroup cache credit; pool net-negative at 2 cores; parse floor). The
|
||
2c/6GB envelope already improved 26.4 → 21.6min with counts byte-exact.
|
||
Record runs DONE (§7a.2): 2c/6GB 20.4min, 8c/7GB 18.3min NO-OOM — byte-exact.
|
||
Batch-loop profile round DONE (§7a.3, #1339): countGuard quadratic killed,
|
||
19.3min. cFnPtr round DONE (§7a.4, #1341): 2.07× standalone, edge set
|
||
hash-identical, envelope **17.6min (R6 −33%)**. R7a landed 2026-07-17:
|
||
envelope now **19.1min on a substantially RICHER graph** (the new
|
||
preParse blanks recover previously-error-swallowed code; wasm-arm on
|
||
the same graph is 22.9min — the 17.6 record was the old smaller graph
|
||
and isn't directly comparable). 8c re-run DONE post-R7a (§7a.5):
|
||
**16.4min** (pre-R7a record 18.3min), EXIT 0, counts == both 2c arms,
|
||
WAL 1.34GB. The <10min-on-8c target remains open, and the re-run
|
||
re-ranked the levers honestly: at 8c the parse-loop (202.6s) is
|
||
already AT the single-writer floor, so **the target gap is ~entirely
|
||
the core-invariant resolution superphase (715s ≈ 12 of the 16.4min)
|
||
— the per-ref path is THE 8c lever**. C/C++ deferral round 2 DONE
|
||
2026-07-18 (full record: checklist doc): eight new C-only preParse
|
||
passes + word-list extensions took kernel/+mm/ deferral
|
||
**58.6% → 33.9%** (git 16.1 → 12.2%, redis 25.3 → 24.1%,
|
||
fmt/protobuf unchanged — cpp-dominant, correct no-op), five-repo
|
||
sweeps 0-diff, linux full-tree both arms **2,049,153 / 6,413,518**
|
||
with **byte-identical dumps** (10,446,478 lines, sha `6dd1185b…`);
|
||
kernel-arm parse-loop **356 → 306s** at 2c, envelope ~17.1min
|
||
(host-contaminated, indicative). Honesty note: full-graph node
|
||
deltas are small (+858) — wasm error recovery was already salvaging
|
||
most SYMBOLS on deferred files; the real win is EDGES (+6,585),
|
||
phantom cleanup, and native-path coverage. Remaining deferral is
|
||
policy-skips (CONFIG interleaves, TP_PROTO DSL, module_init-no-semi)
|
||
+ small buckets — this lever is largely SPENT. Per-ref measurement
|
||
round DONE 2026-07-18 (§7a.6): fresh 2c/8c stage tables; the pool
|
||
double-buffer WORKS (8c settle 3.6s — "core-invariant" superseded);
|
||
2c record now 16.5min, 8c range 15.0–16.4 (n=2). Two cache
|
||
experiments killed by measurement same-day (nameCache scaling, lazy
|
||
candidates — §7a.6 has the numbers; code reverted).
|
||
Writes-under-readers PROBED + FIXED 2026-07-18 (§7a.7): mechanism =
|
||
WAL read-through depth under reader pins (proven by pool-off/dose/
|
||
valve discrimination); fix = worker connection recycling at the
|
||
pool-idle boundary (cadence 8); superphase **715 → 633.6s (−11.4%)**,
|
||
8c envelope best **14.8min**, byte-neutral at every gate. Queue now:
|
||
**cFnPtr native site extraction** (synthesis ~230s) >
|
||
continuous-shallow WAL (the remaining ~45s to the valve floor) >
|
||
backpressure byte volume > recreate.
|
||
- [x] **R7a. C/C++ port** — DONE 2026-07-17, same-day walker+gates after the
|
||
survey (#1344) and grammar vendoring (#1345). One dual-language walker
|
||
(`codegraph-kernel/src/ccpp/`), preParse HOISTED to the route point
|
||
(both tryKernelExtract and the raw bulk path — no blanking ported to
|
||
Rust; Metal/CUDA ride the cpp route through the same hoist). Gates:
|
||
parity sweeps **0 diffs** on redis/git/fmt/protobuf/ALS (2,389 files
|
||
compared); full-init dump-diffs **byte-identical** on all five;
|
||
DEFAULT_ROUTED += c, cpp. Three measurement corrections recorded in
|
||
the checklist doc: (1) C/C++ parse-error incidence is 9–42% per repo
|
||
(vs 0–0.42% for prior languages), so erroring-file deferral is
|
||
routine, not a broken-kernel signal — the sweep gained
|
||
`--max-deferral` (0.5 for c/cpp) after confirming recovery-divergence
|
||
is real with the sweep-only no-defer hatch; (2) seven new/extended
|
||
TS-side preParse blanks (extern-C guard bodies, lone macro lines,
|
||
statement iterator macros, trailing `UNUSED` params, the curated
|
||
Linux/sparse `__init`-family annotations + `container_of` type args,
|
||
cpp leading-attr, directive-line restore) cut real incidence (linux
|
||
subtrees 79% → 58%) AND grew the wasm path's own graphs (git
|
||
7.1k → 13.3k nodes) — so cg1212's "counts must stay
|
||
2,048,664/6,405,964" expectation is superseded: the graph legitimately
|
||
changes with the blanks; the invariant is kernel-arm == wasm-arm at
|
||
every scale (held: five byte-identical dumps + the linux dump-hash
|
||
pair); (3) at high deferral the kernel arm initially LOST arm-vs-arm
|
||
on linux (deferred files ran the pipeline 3×) — fixed with the
|
||
one-slot defer memo + blanked-source reuse; final cg1212 envelope
|
||
**19.1 min kernel-arm** (parse-loop 560 → 356s; R6 26.4 → P1 17.6 on
|
||
the old smaller graph → 19.1 on the new richer one:
|
||
2,048,295 nodes / 6,406,933 edges, two runs byte-same).
|
||
- [ ] **R7b. Remaining long tail** per the tracker (§4) — ruby/php/csharp/rust/… T1s
|
||
are now ~1-day-each with the walker pattern; T3 may stay TS forever (fine).
|
||
- [ ] **P2. Arc 3, graph richness** (§7b) — product-priority call, standard gates.
|
||
- [ ] **P3. Parked items** (§7c) — only with explicit maintainer approval.
|
||
|
||
## 0a. Cold-start handoff (state as of 2026-07-17)
|
||
|
||
**Where the work lives:** MERGED to `main` 2026-07-17 (PR #1326, merge commit
|
||
`c1dc78d`; the 9 milestone commits `c5eebe6` R1 → `2a79432` R6 are preserved in
|
||
history). All scratchpad clones
|
||
(excalidraw/vscode/dubbo/django/…) were throwaway; re-clone fresh for new gate runs.
|
||
The cg1212 docker container (Linux kernel, 2 CPU/6GB) is long-lived on the dev Mac
|
||
and has the current build deployed at `/app` (tree at `/work/linux`).
|
||
|
||
**What exists:**
|
||
- `codegraph-kernel/` — napi-rs crate. One WALKER MODULE per language
|
||
(`tsjs/`, `java.rs`, `python.rs`, `go.rs`, `ccpp/` for c+cpp) mirroring
|
||
`TreeSitterExtractor`'s
|
||
per-language paths bug-for-bug; shared `buffers.rs` (wire contract — twin of
|
||
`src/extraction/kernel/layout.ts`, byte-matched, ABI-versioned), `ids.rs`
|
||
(sha node ids, test-pinned to `generateNodeId`), `docstring.rs`, `textutil.rs`
|
||
(UTF-16 columns/slices, generated-file patterns, shared regexes), `langs.rs`
|
||
(grammar registry).
|
||
- `src/extraction/kernel/` — loader (contract-verifies before routing; a stale
|
||
.node silently degrades to wasm; `CODEGRAPH_KERNEL_DEBUG=1` explains), decode,
|
||
routing (`DEFAULT_ROUTED` = ts/tsx/js/jsx/java/python/go/c/cpp;
|
||
`CODEGRAPH_KERNEL_LANGS` REPLACES the set; `CODEGRAPH_KERNEL=0` kills), the
|
||
deferred-decode transport (`tryKernelExtractRaw` → buffers ride to the store
|
||
worker; files with applicable framework `extract()` hooks keep the decoded
|
||
path), and the **preParse hoist** (`preParsedSource` — a language's
|
||
offset-preserving `preParse` hook runs before BOTH kernel entry points, so
|
||
c/cpp/metal/cuda blanking stays TS-side and both arms parse identical bytes).
|
||
- Gates in-repo: `scripts/kernel-parity.mjs` (per-file kernel↔wasm diff,
|
||
ORDER-sensitive, full-object; deferral-rate guard), `scripts/dump-graph.mjs`
|
||
(natural-key full-DB dump for the byte-identical diff),
|
||
`__tests__/kernel-{scaffold,grammar-parity,tsjs-parity}.test.ts` (+ torture
|
||
fixtures under `__tests__/fixtures/kernel-parity/`) — all in `npm test`;
|
||
the release workflow builds a 6-target prebuild matrix (continue-on-error;
|
||
kernel is optional everywhere) and runs the suites with
|
||
`CODEGRAPH_KERNEL_EXPECT=1`.
|
||
|
||
**Build/run:** `npm run build:kernel` (needs rustup; stages
|
||
`codegraph-kernel/prebuilds/<plat>-<arch>/codegraph-kernel.node`) → `npm run build`
|
||
→ `npm test`. Parity sweep: `node scripts/kernel-parity.mjs <dir>`. Dump gate:
|
||
init twice (kernel arm vs `CODEGRAPH_KERNEL=0`), `dump-graph.mjs` each, `cmp`.
|
||
|
||
**Adding a language (the proven recipe, ~a day for a T1):**
|
||
1. Read its `languages/<lang>.ts` config AND every branch of tree-sitter.ts it
|
||
exercises (visitNode dispatch, extractCall's language branch, inheritance
|
||
clauses, fn-ref spec in function-ref.ts, value-ref prune cases). Port
|
||
bug-for-bug — quirks included (each walker's header comments list its own).
|
||
2. Add the crates.io grammar; **vendor the wasm from the SAME tag** (clone tag,
|
||
sha-match parser.c against the cargo registry copy, `tree-sitter-cli 0.25.10
|
||
build --wasm` from CHECKED-IN parser.c, drop into `src/extraction/wasm/`, add
|
||
to VENDORED_WASM_LANGS) — tree-sitter-wasms is 2023-era for most languages.
|
||
3. Torture fixture + parity sweeps (small/medium/large real repos) → full-init
|
||
dump-diffs byte-identical → add to DEFAULT_ROUTED + tests + changelog.
|
||
|
||
**Traps already paid for (do not relearn):**
|
||
- **Error recovery is ENCODING-dependent** (UTF-8 native vs UTF-16 web-tree-sitter,
|
||
same grammar bytes + same core) → every walker defers `has_error()` files via
|
||
the `defer:` signal. Incidence 0–0.42%; the harness fails >10% deferral.
|
||
- **Node IDs collide** for same-(kind,name,line) — routine in minified one-liners.
|
||
Any dedupe/self-check that the TS side keys on node IDs must compare ID STRINGS,
|
||
not table rows (`node_ids` vec in every walker).
|
||
- **Positions and JS string slices are UTF-16** (`textutil::col16`/`slice_utf16`) —
|
||
that's what web-tree-sitter reports and what `.slice(0,100)` means.
|
||
- The extraction seam contract is **exactly what extractFromSource returns** — e.g.
|
||
refs carry NO denormalized filePath/language (the store fills them). The strict
|
||
full-object parity compare exists because a loose one masked precisely this.
|
||
- Grammar bumps: crate + vendored wasm move TOGETHER or kernel-grammar-parity fails.
|
||
- **JS multiline `^` anchors after `\r` (and U+2028/U+2029); the regex crate's
|
||
`(?m)^` is `\n`-only** — on CRLF checkouts (Windows autocrlf) the JS reference's
|
||
greedy `\s*` eats the `\n` of a CRLF pair and the cleaned docstring keeps a bare
|
||
`\r`. Caught by the O2 Windows leg (6 parity failures), fixed via
|
||
`js_multiline_strip` in `docstring.rs`; CRLF variants of every torture fixture
|
||
are pinned in `kernel-tsjs-parity` (derived in-memory — normalization-proof).
|
||
Any future walker regex with `(?m)` needs the same scrutiny.
|
||
- Perf claims: measure before believing — the plan's own §1/§6 expectations were
|
||
corrected twice (many-core parse-loop wall = store-writer, §4d; cg1212 parse =
|
||
C-bound, §4f).
|
||
|
||
---
|
||
|
||
## 1. Mission and the numbers that motivate it
|
||
|
||
CodeGraph's remaining fresh-index gap vs codebase-memory-mcp (cbm) is the parse+extract
|
||
phase, and its floor is per-node JS↔WASM marshaling — proven, not suspected:
|
||
|
||
| Measurement (2026-07-16, M3 Pro) | Result |
|
||
|---|---|
|
||
| dubbo (4,402 Java files) parse-loop, current 7-wasm-worker pipeline | 4,700ms |
|
||
| Same files, Rust tree-sitter parse+walk, rayon (spike) | **202ms** |
|
||
| Same, single Rust thread | 1,067ms |
|
||
| dubbo fresh init today / cbm | 11.1s / 7.1s (1.55×) |
|
||
| Linux kernel, same 2-CPU/6GB container | **we complete 27min; cbm dies at 0.16%, twice** |
|
||
|
||
Spike source: session scratchpad `cg-kernel-spike/` (tree-sitter 0.25 + tree-sitter-java,
|
||
TreeCursor walk touching kind/range/name-field, flat-row output). Reproduce before starting —
|
||
it's ~80 lines and doubles as the emitter's seed.
|
||
|
||
Expected end state: parse-loop 4.7s → ~1.0–1.5s on dubbo-class repos → total ≈ 7.5s,
|
||
**parity with cbm on their best surface**, while keeping every win we already hold
|
||
(sync 2.4–2.8×, agent A/B decisive, call-graph density 1.3–2.3×, byte-identical
|
||
determinism, constrained-hardware envelope).
|
||
|
||
> **SUPERSEDED BY MEASUREMENT (§4c/§4d):** the many-core parse-loop wall turned out
|
||
> to be the single-writer SQLite ingest (94% of it on dubbo), not extraction — 8 wasm
|
||
> workers already hid extraction CPU behind the main thread on big-core machines. So
|
||
> the Mac dubbo total stays ~11s and closing the remaining cbm gap there is a
|
||
> STORE-ARCHITECTURE arc, not a kernel task. The kernel's wins are real where worker
|
||
> CPU binds: the 2-CPU/6GB CI envelope (excalidraw ~1.5×, dubbo ~1.25×, django 1.32×,
|
||
> prometheus 1.46×) and vscode-scale-on-Mac (1.28×). Every "keep" item held —
|
||
> byte-identical determinism is now enforced per language by the dump gate.
|
||
|
||
## 2. What the kernel is — and the boundary that makes it safe
|
||
|
||
One napi-rs crate (`codegraph-kernel`) linking tree-sitter's C library and native grammars.
|
||
Input `(filePath, content, language)` per file; output **flat typed buffers** (nodes, edges,
|
||
unresolved refs) — one boundary crossing per file. It replaces ONLY the parse+extract walk
|
||
inside the parse workers, behind the existing `ExtractionResult` contract.
|
||
|
||
**Never ported (works unchanged for all languages from day one):** name-matcher +
|
||
import-resolver, all framework resolvers (`src/resolution/frameworks/`), all 36 synthesis
|
||
passes, MCP/explore, sync/watcher, installer. They consume the graph and raw source, not
|
||
the parse tree.
|
||
|
||
**Coexistence is permanent:** a language routes to the kernel only after its gate passes;
|
||
everything else stays on the wasm path forever if need be. No flag-day. Rollback per
|
||
language = flipping the route.
|
||
|
||
**Distribution:** prebuilt `.node` per platform through the existing release-bundle
|
||
pipeline (`scripts/build-bundle.sh` + per-platform npm packages); the same crate compiled
|
||
to wasm is the universal fallback. Zero-native-build-on-install stays true.
|
||
|
||
## 3. Phase 0 — scaffold (do first, ~days)
|
||
|
||
1. `codegraph-kernel/` crate: napi-rs, tree-sitter C, rayon optional (workers already
|
||
parallelize per-file — start synchronous per call, one kernel call per file from the
|
||
existing `ParseWorkerPool` workers; do NOT rebuild the pool).
|
||
2. Buffer contract: decide the flat encoding (suggest: one `Buffer` per table,
|
||
fixed-width rows + a string arena; version byte first). Write the TS decoder next to
|
||
`parse-worker.ts`.
|
||
3. Generic emitter driven by per-language `.scm` query files + a small per-language Rust
|
||
config (node-kind → NodeKind mapping, name-field conventions). Escape hatch: a
|
||
per-language `post(buffers, source)` TS hook for logic queries can't express.
|
||
4. Build integration: napi prebuilds wired into the release workflow next to the Node
|
||
bundles; `CODEGRAPH_KERNEL=0` kill switch; wasm fallback auto-selected when the
|
||
`.node` is absent (source runs, unsupported platforms).
|
||
5. CI: assert native grammars and wasm grammars are built from the SAME grammar source
|
||
revisions (ABI drift between paths would make per-language routing non-deterministic).
|
||
|
||
### 3a. Phase 0 — SHIPPED 2026-07-16 (what exists and the decisions made)
|
||
|
||
- **Crate:** `codegraph-kernel/` (napi 3, tree-sitter 0.25, no CLI dependency —
|
||
`scripts/build-kernel.sh` does cargo build + stage into
|
||
`codegraph-kernel/prebuilds/<platform>-<arch>/codegraph-kernel.node`; `npm run
|
||
build:kernel`). Exports `extractFile`, `contractInfo`, `grammarInfo`.
|
||
- **Buffer contract v1:** five Buffers (meta/nodes/edges/refs/arena), fixed-width LE rows,
|
||
string arena with `(offset,len)` refs, `0xFFFFFFFF` = absent, version byte first, node
|
||
IDs computed Rust-side (sha256, byte-identical to `generateNodeId` — pinned by test),
|
||
tri-state bool flags, `extraJson` escape slot per node row, and a RESERVED u32 metrics
|
||
slot (Arc 3.2). Layout doc lives twice and must match: `codegraph-kernel/src/buffers.rs`
|
||
↔ `src/extraction/kernel/layout.ts`. NODE_KINDS/EDGE_KINDS array ORDER in src/types.ts
|
||
is wire contract now (EDGE_KINDS became a runtime array for this).
|
||
- **Emitter:** generic, `.scm`-driven (`@def.<NodeKind>` + `@name` + `@ref.<EdgeKind>`
|
||
capture convention), scope stack by byte-range nesting → `::`-joined qualifiedNames,
|
||
contains edges, refs attached to innermost enclosing def (file node fallback) — the
|
||
TreeSitterExtractor conventions. Seed TS/JS queries are SMOKE-level only; R2 replaces.
|
||
- **Routing:** inside `extractFromSource` (tree-sitter.ts) — `tryKernelExtract` first,
|
||
wasm `TreeSitterExtractor` as fallback (also per-FILE fallback on any kernel error).
|
||
DEFAULT_ROUTED is EMPTY; dev opt-in via `CODEGRAPH_KERNEL_LANGS=<langs|all>`; global
|
||
kill switch `CODEGRAPH_KERNEL=0`; loader verifies ABI + kind tables before routing
|
||
(stale .node → silent wasm, `CODEGRAPH_KERNEL_DEBUG=1` to see why). The escape hatch
|
||
landed as `post(result, source)` over the DECODED result (not raw buffers) — decoded
|
||
is what TS logic wants; see POST_PASSES in `src/extraction/kernel/index.ts`.
|
||
- **Grammar parity (the §3.5 CI) — and a decision that changed the wasm path:** the
|
||
parity test (`__tests__/kernel-grammar-parity.test.ts`, behavioral: ABI + node-kind +
|
||
field tables compared id-by-id) caught on day one that tree-sitter-wasms ships
|
||
2023-era TS/JS grammars (^0.20.x) vs crates.io current. Resolution: **vendored fresh
|
||
wasm into `src/extraction/wasm/` built from the exact crate revisions** —
|
||
tree-sitter-typescript v0.23.2 (f975a62) for typescript+tsx, tree-sitter-javascript
|
||
v0.25.0 (44c892e) for javascript+jsx — from each repo's CHECKED-IN parser.c (no
|
||
`generate`), tree-sitter-cli 0.25.10, emcc. So the production wasm TS/JS grammars are
|
||
UPGRADED as of this change (full suite green, 2456 tests) and **R2/R3 parity diffs
|
||
are grammar-neutral**. Bump crate + vendored wasm together, or the parity test fails.
|
||
- **Release wiring:** `kernel` matrix job in release.yml (macos-14 ×2 targets,
|
||
ubuntu-22.04, ubuntu-22.04-arm, windows-latest ×2 — all continue-on-error: kernel is
|
||
optional, a toolchain flake never blocks a release) → artifacts → `release/kernel/` →
|
||
build-bundle.sh stages `lib/kernel/codegraph-kernel.node` when present. The release
|
||
job runs the kernel tests with `CODEGRAPH_KERNEL_EXPECT=1` (missing binary = FAILURE
|
||
there, skip elsewhere).
|
||
- **Loader search order:** `CODEGRAPH_KERNEL_PATH` → `<pkgroot>/kernel/` (bundle) →
|
||
`<pkgroot>/codegraph-kernel/prebuilds/<plat>-<arch>/` (source runs).
|
||
- **Known R2 gate item:** native columns are UTF-8 byte offsets; web-tree-sitter's are
|
||
UTF-16-derived — column NUMBERS on non-ASCII lines will differ in parity dumps
|
||
(text, lines, IDs unaffected). Classify or normalize when it shows up.
|
||
**RESOLVED in R2:** the walker emits UTF-16 columns natively (util::col16), and JS
|
||
string-slicing semantics (signature truncation at 100/80/120 units) are reproduced in
|
||
UTF-16 units too — no column/slice diff class exists.
|
||
|
||
### 4a. R2 — TS/JS port SHIPPED 2026-07-16 (and a §3 design revision)
|
||
|
||
- **The generic `.scm` emitter is superseded.** Real TS/JS parity needs logic queries
|
||
can't express (extractCall's receiver-qualified callees, store/RTK/component
|
||
recognition, fn-ref capture+gating, value-ref shadow pruning, docstring wrapper
|
||
climbs) — so R2 replaced the R1 query emitter with a **bespoke per-language walker**
|
||
(`codegraph-kernel/src/tsjs/`, ~1,900 lines) that mirrors `TreeSitterExtractor`'s
|
||
TS/JS paths function-for-function, bug-for-bug. emitter.rs + queries/ are deleted
|
||
(git has them); expect T1 languages (java/python/go) to be walkers too. The
|
||
`post(result, source)` TS escape hatch remains available but TS/JS needed none.
|
||
- **Parity evidence (macOS):** `scripts/kernel-parity.mjs` (multiset diff of
|
||
canonicalized nodes/edges/refs per file, FULL objects) — this repo 353/353 files,
|
||
excalidraw 643/643 files (10,650 nodes / 10,726 edges / 68,307 refs), plus
|
||
checked-in torture fixtures (`__tests__/fixtures/kernel-parity/`) covering
|
||
components/HOCs/styled, zustand-through-middleware, RTK endpoints+hooks, vuex/pinia,
|
||
fn-refs (incl `this.x` + shadowing gates), value-refs (incl the shadow prune),
|
||
decorators, enums, type-alias members + tuple contracts, re-exports, JSX. Kept alive
|
||
in `npm test` by `__tests__/kernel-tsjs-parity.test.ts` (strict full-object compare).
|
||
- **One decoder bug found by the strict compare:** decode.ts pre-filled
|
||
`filePath`/`language` on refs; wasm extractors leave them unset (the store
|
||
denormalizes via `?? filePath`). Fixed — the seam contract is "exactly what
|
||
extractFromSource returns", not "what the store makes of it".
|
||
- **Perf (M3 Pro, excalidraw 643 files / 7MB):** extraction single-thread 487ms kernel
|
||
vs 1,255ms wasm (**2.6×**, identical outputs). End-to-end `init` on an 11-core host
|
||
moves only ~3.4s → ~3.2s — parse is a small, already-pool-parallelized slice there;
|
||
the win concentrates on constrained hardware (2-core CI class) and kernel-scale
|
||
parse (R6). Headroom if R4's dubbo target needs it: arena interning, memoized
|
||
UTF-16 line prefixes, and skipping wasm-grammar loads in workers for kernel-routed
|
||
languages (worker cold-start).
|
||
- **Not yet done (R3 gate):** large-repo parity (vscode-class), full-repo dump-diff
|
||
through the DB, retrieval invariants, agent A/B, Linux docker + Windows VM parity
|
||
runs, control-repo perf. Routing stays opt-in (`CODEGRAPH_KERNEL_LANGS`) until then.
|
||
**→ Done same day, §4b.**
|
||
|
||
### 4b. R3 — gate PASSED, TS/JS DEFAULT-ON (2026-07-16)
|
||
|
||
Evidence (tools: `scripts/kernel-parity.mjs` now ORDER-sensitive — identical multisets
|
||
in a different emission order would shift rowids and change resolution — and
|
||
`scripts/dump-graph.mjs`, natural-key full-DB dumps):
|
||
|
||
1. **Graph parity — byte-identical, not ≤0.5%:** full `init` dump-diff kernel-vs-wasm:
|
||
express (13,712 rows), excalidraw (89,898), **vscode (2,378,238 rows)** — all
|
||
byte-identical. Control repo (flask, Python) byte-identical + timing unchanged.
|
||
Extraction-level order-sensitive sweeps: repo 352/354 (+2 deferred), express
|
||
141/141, excalidraw 643/643, vscode 12,055/12,106 (+51 deferred), 0 diffs.
|
||
2. **The one real find — encoding-dependent error recovery:** same grammar bytes
|
||
(sha-verified parser.c/scanner.h), same tree-sitter core (0.25.10), but error
|
||
RECOVERY on files with parse errors differs between UTF-8 (native) and UTF-16
|
||
(web-tree-sitter) parsing — proven by parsing the divergent vscode file natively
|
||
in UTF-16, which reproduced the wasm tree exactly. Incidence: 0% (express) /
|
||
0.31% (excalidraw) / 0.42% (vscode) of files. **Policy: the kernel defers any
|
||
file whose tree `has_error()` to the wasm extractor** (`defer:` signal, silent,
|
||
per-file) — parity by construction on erroring files, 99.6%+ keep the fast path,
|
||
and the harness fails if deferrals exceed 10% (a broken kernel can't hide).
|
||
3. **Retrieval invariants:** kernel-indexed excalidraw — `mutateElement →
|
||
renderStaticScene` connects end-to-end via explore (callback + react-render +
|
||
jsx hops shown); synthesized-edge families present (408 jsx-render / 46
|
||
react-render / 14 interface-impl / 1 callback); byte-identical DB ⇒ counts equal
|
||
by construction.
|
||
4. **Agent A/B:** byte-identical DBs make the A/B vacuous (identical graph, identical
|
||
MCP server) — same justification as the #1320–#1322 perf PRs, which shipped on the
|
||
dump-diff gate. Not burned.
|
||
5. **Perf:** vscode init 105.4s → 82.1s (**1.28×**) on the 11-core Mac; excalidraw on
|
||
a 2-CPU/6GB Linux container (the CI-runner envelope) 6.2–7.1s → 4.3–4.8s
|
||
(**~1.5×**, n=2 interleaved); Mac excalidraw ≈ neutral-to-slightly-better (parse
|
||
already a small pool-parallelized slice at 11 cores). Control unchanged.
|
||
6. **Platforms:** Linux (arm64 bookworm container, in-container cargo build): all 22
|
||
kernel tests green under `CODEGRAPH_KERNEL_EXPECT=1`. **Windows VM: deferred** —
|
||
VM stopped and `prlctl start` needs Parallels Pro; benign because a missing/broken
|
||
`.node` falls back to wasm, and the release workflow builds + gates win32
|
||
prebuilds. Run the kernel suites on the VM when it's next up.
|
||
7. **Suite:** 2,465 tests pass WITH default-on routing — the entire extraction test
|
||
corpus now exercises the kernel for TS/JS on machines with a staged `.node`.
|
||
|
||
Default routing: `DEFAULT_ROUTED = {typescript, tsx, javascript, jsx}` in
|
||
`src/extraction/kernel/index.ts`. `CODEGRAPH_KERNEL_LANGS` REPLACES the set;
|
||
`CODEGRAPH_KERNEL=0` kills. Changelog entry added under [Unreleased].
|
||
|
||
### 4c. R4 — Java PORTED + gate PASSED + DEFAULT-ON (2026-07-16)
|
||
|
||
- **Walker:** `codegraph-kernel/src/java.rs` (self-contained, sharing the crate-level
|
||
docstring/textutil modules) — package namespaces, imports, javadoc, annotations →
|
||
decorates, type_list inheritance, fields/constants (static-final → constant),
|
||
enum_constant members, anonymous classes (`<T$anon@line>` incl. the TS side's
|
||
0-based-line quirk, mirrored bug-for-bug), method_invocation calls with the
|
||
`this.field` unwrap + the `Foo.getInstance().bar()` chain encoding, static-member
|
||
value reads, method_reference fn-refs (`this::x` / `Type::m`), value refs, and the
|
||
**full Lombok member synthesizer** (#912: @Getter/@Setter/@Data/@Value/@Builder/
|
||
@ToString/@EqualsAndHashCode/@Slf4j-family, taken-member dedup by exact
|
||
`classQN::name`). Grammar: tree-sitter-java crate 0.23.5; wasm vendored from the
|
||
SAME tag (94703d5, parser.c sha-matched), replacing tree-sitter-wasms' ^0.20.2 build.
|
||
- **Parity:** extraction sweeps — gson 262/262, retrofit 341/341, dubbo 4,048/4,048,
|
||
torture fixture (`__tests__/fixtures/kernel-parity/Torture.java`, in `npm test`).
|
||
Full-init dump-diffs byte-identical: gson (49,766 rows), retrofit (62,735),
|
||
**dubbo (441,266 rows)**. All R2/R3 repos re-verified after the fix below.
|
||
- **The gate caught a REAL cross-language bug:** retrofit's minified website JS
|
||
exposed that fn-ref dedupe and value-ref self-checks must compare node **ID
|
||
strings**, not table rows — IDs collide for same-(kind,name,line) nodes (routine in
|
||
minified one-liners: many `function e` on line 3) and the TS side keys on
|
||
`${fromNodeId}|${name}`. Fixed in BOTH walkers (`node_ids` per row); this affected
|
||
tsjs too (latent since R2, never released).
|
||
- **Benchmark honesty (the §6 expectation was wrong about WHERE the win lands):**
|
||
dubbo fresh-init on the 11-core M3 Pro is ~FLAT end-to-end (11.3–11.5 wasm →
|
||
11.0–11.6 kernel; parse-loop wall 5,020→4,394ms) because that phase's wall is
|
||
**main-thread-bound** (file reads + result store + SQLite), not worker-CPU-bound —
|
||
8 wasm workers already hide extraction CPU behind the main thread on big-core
|
||
machines. Where worker CPU binds, the kernel delivers: **dubbo on 2-CPU/6GB Linux
|
||
27.8–28.6s → 22.3–22.8s (~1.25×)**; excalidraw same envelope ~1.5×; vscode-on-Mac
|
||
1.28×. The **cbm-parity Mac headline therefore needs the next lever: decode the
|
||
kernel's buffers DIRECTLY into store rows** (skip per-node JS object
|
||
materialization on the main thread) — buffer contract already carries everything;
|
||
tracked as the top §7a-adjacent follow-up.
|
||
- **Platforms:** Linux container (arm64): all 23 kernel tests green EXPECT=1;
|
||
Windows VM still deferred (same fallback rationale as §4b).
|
||
- Default routing now includes `java`.
|
||
|
||
### 4e. R5 — Python + Go PORTED + gates PASSED + DEFAULT-ON (2026-07-16)
|
||
|
||
- **Walkers:** `codegraph-kernel/src/python.rs` + `src/go.rs` (the java.rs pattern).
|
||
Python: decorated_definition docstring/decorator handling (decorates only for
|
||
bare-identifier decorators — the `call`-kind quirk mirrored), fn-in-class → method,
|
||
module assignments always `variable` (no isConst hook), from-import binding refs,
|
||
`self.x` fn-ref candidates as BARE names, attribute callees via the namedChild(1)
|
||
fallback. Go: receiver methods with `Recv::name` QNs + first-earlier-struct
|
||
contains edges, type_spec → struct/interface classification (embedding → extends;
|
||
interface method_elems → method nodes), composite-literal instantiates keeping the
|
||
package qualifier, top-level var/const initializer walks attributed to the symbol
|
||
(#693), 2-hop field chains (#1276), `New().Method()` re-encode (#645/#608),
|
||
GO_SPEC fn-ref layers (literal_element/expression_list fan-out).
|
||
- **Grammars:** crates tree-sitter-python 0.23.6 (bffb65a) + tree-sitter-go 0.23.4
|
||
(3c3775f); wasm vendored from the same tags, parser.c sha-matched (both were
|
||
2023-era in tree-sitter-wasms).
|
||
- **Parity:** extraction sweeps 100% — flask 83/83, django 3,035/3,038 (+3 error-file
|
||
deferrals), gin 99/99, prometheus 978/979 (+1). Full-init dumps byte-identical:
|
||
flask (10,833 rows), gin (17,540), **django (360,794)**, **prometheus (213,758)**.
|
||
Torture fixtures in `npm test`. Even Mac-side init already moves where extraction
|
||
matters: prometheus 5.7→4.5s, django 9.0→8.7s. On the 2-CPU/6GB envelope (the
|
||
CI-runner class): **django 22.0→16.7s (1.32×), prometheus 15.0→10.3s (1.46×)**.
|
||
- Default routing now: typescript, tsx, javascript, jsx, java, python, go.
|
||
|
||
### 4f. R6 — kernel-scale re-validation (2026-07-17)
|
||
|
||
Fresh init of the Linux kernel in the cg1212 container (2 CPUs / 6GB), current build
|
||
(R5 kernel + direct-to-store active), CODEGRAPH_SYNTH_TIMINGS:
|
||
|
||
- **Completes, exit 0: 1,586s (26.4min) vs the ~27min #1212/#1323 baseline — no
|
||
regression** with per-file routing checks, error-file deferral, and the d2s store
|
||
path live. Graph scale identical: 2,048,664 nodes / 6,405,964 edges (baseline
|
||
2.05M/6.4M).
|
||
- Phase walls: scan 1.3s (70,239 files), **parse-loop 371.9s (6.2m — unchanged)**,
|
||
fts-rebuild 6.4s, edge-index-recreate 78.9s, callback-synthesis 350.3s,
|
||
**resolution 1,149.7s (19.2m — the wall, P1's territory)**, maintenance 47.2s.
|
||
- **The §6 parse expectation (6m → ~2m) was mis-premised:** the Linux tree is
|
||
63,810 C/H files vs 422 Python — ~99% C, an UNPORTED T2 language, so the kernel
|
||
can't touch its parse time. The expectation transfers to the C/C++ port (R7,
|
||
with the blanking pre-passes staying TS-side per §4). The tree's own Python
|
||
tooling: 99/99 files byte-parity.
|
||
- Kernel-scale priority order after this run: **P1 resolution (73% of the wall)** >
|
||
C/C++ port (23%) > everything else.
|
||
|
||
### 4d. Direct-to-store decode (2026-07-16) — and where the wall ACTUALLY is
|
||
|
||
Kernel-routed files now ship their flat buffers from the parse worker all the way
|
||
to the STORE WORKER, which decodes + finalizes them there (`tryKernelExtractRaw` →
|
||
`ExtractionResult.kernelBuffers` → `KernelStoreBundle` → `decodeKernelBundle`;
|
||
filter semantics shared via `finalizeStoreBundle`). The main thread's per-file work
|
||
drops to O(1) + the content hash — it never materializes per-node objects, and both
|
||
postMessage hops move flat bytes instead of object graphs. Files whose applicable
|
||
frameworks carry an `extract()` hook keep the decoded path (hooks merge into decoded
|
||
results); non-writer paths (main-thread store, tests) materialize via
|
||
`materializeKernelResult`. Byte-identical dumps re-verified on dubbo, excalidraw,
|
||
express, gson.
|
||
|
||
**Measurement that closes the §4c question:** with the store worker instrumented,
|
||
dubbo's parse-loop wall is **94% store-writer busy time** (4,202ms of 4,493ms on the
|
||
kernel arm). The many-core fresh-index wall is the single-writer SQLite ingest —
|
||
not extraction, not main-thread work. d2s still improves the writer lane ~11%
|
||
(4,726→4,202ms: buffers skip structured-clone deserialization ON the writer) and
|
||
frees the main thread, but the remaining cbm gap on many-core medium repos is a
|
||
STORE-ARCHITECTURE question (their RAM-first design defers all durability). Next
|
||
levers there (a separate perf arc, not this project): deferred/bulk index builds
|
||
during the parse phase, multi-file write transactions, buffer→bind without object
|
||
materialization. Note the #1320-arc post-mortem already measured statement batching
|
||
and sorted inserts as ~zero on this path — B-tree maintenance is the floor.
|
||
|
||
## 4. Per-language tracker
|
||
|
||
Tiers: **T1** = mostly `.scm` + mapping config. **T2** = needs bespoke pre/post passes kept
|
||
in TS (listed). **T3** = not a plain tree-sitter walk (standalone/multi-grammar extractor)
|
||
— migrate last or never; wasm/TS path is a fine permanent home.
|
||
|
||
The user-facing language contract is `README.md → Language Support` (34 logos incl.
|
||
Metal, CUDA, Terraform/OpenTofu, Pascal/Delphi). Keep this tracker in sync with it —
|
||
every README language must have a row here, even the ones that only ride another
|
||
language's port.
|
||
|
||
Grammar column: `crates.io` = mainstream native grammar crate exists; `vendored` = we ship
|
||
a rebuilt/patched wasm (ABI-15) and the kernel must compile OUR fork natively — verify
|
||
parity before porting the language.
|
||
|
||
| Language(s) | Today | Tier | Grammar source | Migration notes / known traps | Status |
|
||
|---|---|---|---|---|---|
|
||
| typescript, tsx, javascript, jsx | `languages/typescript.ts`, `javascript.ts` + shared branches | T1 | crates.io | First target. Value-reference edges (#895/#897) and component recognition (#841 forwardRef/memo/styled) must survive — they're extraction-side. Largest test surface; gate is strictest here. **PORTED + GATE PASSED + DEFAULT-ON (§4a/§4b); erroring files defer to wasm per-file.** | ✅ |
|
||
| java | `languages/java.ts` | T1 | crates.io | Second target; unlocks the dubbo-parity claim. Lombok member synthesis (#912) is a NODE synthesizer hook in extraction (`synthesizeMembers`) — port or keep as TS post-pass. **PORTED incl. Lombok + gate passed + DEFAULT-ON (§4c).** | ✅ |
|
||
| python | `languages/python.ts` | T1 | crates.io | Third. Decorator extraction feeds framework route detection — parity required. **PORTED + DEFAULT-ON (§4e).** | ✅ |
|
||
| go | `languages/go.ts` | T1 | crates.io | Third (tie). Value-reference edges ship here too (#897). **PORTED + DEFAULT-ON (§4e).** | ✅ |
|
||
| ruby, php | dedicated files | T1 | crates.io | Straightforward; PHP property-receiver shapes (#1220/#1251) are RESOLUTION-side, unaffected. | ☐ |
|
||
| csharp | `languages/csharp.ts` | T1 | crates.io | Plain. | ☐ |
|
||
| rust, dart, scala, lua, luau, r | dedicated files | T1 | crates.io (luau/r/scala: verify crate freshness vs our wasm) | Long-tail T1; port opportunistically after the big five. | ☐ |
|
||
| kotlin | `languages/kotlin.ts` | T1½ | crates.io | Expect/actual pairing is synthesis-side (fine); extraction is clean but validate against a KMP repo. | ☐ |
|
||
| swift | shared + dedicated branch | T1½ | crates.io | **Trap:** in-class property extraction lives in `tree-sitter.ts`'s DEDICATED branch, not `swift.ts` (#1020 — Alamofire went 0→348 props). Gate on Alamofire. | ☐ |
|
||
| c, cpp | `languages/c-cpp.ts` | **T2** | crates.io | **DONE (R7a, 2026-07-17)** — `ccpp/` walker; ALL pre-passes stayed TS-side via the route-point preParse hoist (+6 new blanks added during gating — see the checklist doc); content-based `.h` C-vs-C++ detection stays upstream at detectLanguage. Parity 0-diff + dump byte-identical on redis/git/fmt/protobuf/ALS. | ☑ |
|
||
| metal, cuda | dialects over the cpp grammar | **T2** (rides c/cpp) | crates.io (cpp) | **DONE (rides R7a)** — `.metal`/`.cu`/`.cuh` map to 'cpp' and their blanks run in the hoisted preParse (filePath rides along for the extension gates); hoist-parity pinned in kernel-ccpp-parity.test.ts + the metal/cuda suites. | ☑ |
|
||
| objc | `languages/objc.ts` | T2 | crates.io | Rides the c-cpp trap family; RN bridge extraction feeds `rnCrossPlatformEdges` (synthesis-side, fine). | ☐ |
|
||
| arkts | `languages/arkts.ts` | T2 | **vendored** (harmony-contrib) | Dot-prefixed refs + decorator-gated matching fixed 36,840 wrong edges — that logic must port exactly or stay TS-side. Compile our grammar fork natively. | ☐ |
|
||
| pascal | `languages/pascal.ts` | T2 | **vendored** | Paired with dfm-extractor (T3); `extractPascalDefProc` indexed lookups. | ☐ |
|
||
| vbnet | `languages/vbnet.ts` | T2 | **vendored, patched + external scanner** | Our wasm is a patched grammar WITH a C external scanner — the kernel must build that scanner; ts-cli 0.24 dropped `\p{...}` classes during the original build (#1164). Highest grammar-build risk of any language. | ☐ |
|
||
| cobol | `languages/cobol.ts` | T2 | **vendored fork** | Paragraph-extent reconstruction + copybook resolution are extraction logic (#1161, CardDemo 43/44). Port carefully or keep TS post-pass. | ☐ |
|
||
| erlang | `languages/erlang.ts` | T2 | **vendored (WhatsApp/ELP)** | npm `tree-sitter-erlang` is HIJACKED — never source from it (#1165). gen_server dispatch is synthesis-side (fine). | ☐ |
|
||
| nix | `languages/nix.ts` | T2 | **vendored (ABI-15 rebuild)** | Option-path synthesizer is synthesis-side; the `===`-always-false → `.equals()` lesson (#1190) is wasm-binding-specific and disappears natively — still gate on nixpkgs (44k files). | ☐ |
|
||
| solidity | `languages/solidity.ts` | T2 | **vendored** | `modifier_invocation` outside body walk (#1170) is extraction-side; port it. | ☐ |
|
||
| terraform | `languages/terraform.ts` | T2 | **vendored** | `:`-scoped refs for module-boundary bridging (#1173); metadata does NOT persist — re-read source (#1174). | ☐ |
|
||
| cfml, cfscript, cfquery | `cfml-extractor.ts` + 3 grammar files | **T3** | **vendored ×3** | 3-grammar family with BOM-sensitive dialect sniffing (#1118/#1153–55). Leave on wasm until the very end, possibly forever. | ☐ |
|
||
| svelte, vue, astro, liquid | standalone extractors | **T3** | n/a (custom/embedded parsing) | Not tree-sitter walks. Permanent TS home is acceptable — file counts are small and these repos are small. | ☐ |
|
||
| dfm (Delphi forms), razor, mybatis XML | standalone extractors | **T3** | n/a | Same as above. mybatis pairs with a synthesis pass (fine). | ☐ |
|
||
|
||
**Do-not-regress invariants during any port** (extraction-side, will show up in the gate):
|
||
node metadata is re-read from source, never persisted; parse commits stay in FILE ORDER
|
||
(#1015); `MAX_FILE_SIZE` skip; generated-file detection; `CODEGRAPH_PARSE_WORKERS`
|
||
semantics; framework `extract()` hooks keep running TS-side per file after the kernel pass.
|
||
|
||
## 5. Equivalence gate (run per language, no exceptions)
|
||
|
||
Byte-identity vs hand-written extractors is NOT expected — the gate is behavioral parity:
|
||
|
||
1. **Graph parity:** fresh-index 3 real repos (small/medium/large for the language) on
|
||
wasm-path vs kernel-path builds. Dump with the `dump-graph.mjs` pattern (natural keys).
|
||
Node/edge/ref deltas ≤0.5% AND every diff category manually classified (the 13-edge
|
||
supertype-visibility bug this week was caught exactly this way — small diffs are real).
|
||
2. **Retrieval invariants:** the language's canonical flows still connect end-to-end in
|
||
`codegraph_explore` (playbook: `docs/design/dynamic-dispatch-coverage-playbook.md`);
|
||
node counts stable; synthesized-edge spot-check.
|
||
3. **Agent A/B non-regression** per the standard methodology (CLAUDE.md): `--model sonnet
|
||
--effort high` ALWAYS, ≥2 runs/arm, pre-warmed daemon, `CODEGRAPH_NO_PROMPT_HOOK=1`,
|
||
forbid subagent delegation in the prompt.
|
||
4. **Perf:** fresh-index improves on the language's repos; a NON-migrated control repo is
|
||
unchanged; suite green; Linux docker + Windows VM passes for platform-sensitive bits.
|
||
|
||
## 6. Rollout order and expected wins
|
||
|
||
> **Executed 2026-07-16/17; outcomes vs these expectations are in §4a–§4f.** Two
|
||
> expectations below were corrected by measurement: (2) the dubbo-on-Mac headline is
|
||
> store-writer-bound, not extraction-bound (§4c/§4d — the win lands on the low-core
|
||
> envelope instead); (4) cg1212 is ~99% C, an unported T2 language, so its parse
|
||
> expectation belongs to the C/C++ port (§4f).
|
||
|
||
1. **TS/JS/TSX/JSX** — most indexed files in the funnel; excalidraw 3.3s → ~2.3s expected.
|
||
2. **Java** — dubbo 11.1s → ~7.5s expected (**the cbm-parity headline**).
|
||
3. **Python, Go** — rounds out ~90% of real-world indexed files.
|
||
4. Kernel-scale re-run in the cg1212 container after (2): parse 6.0m → ~1.5–2m expected.
|
||
5. Long tail opportunistically; T3 possibly never — that's fine by design.
|
||
|
||
Measurement discipline (hard-won this week — do NOT relearn these):
|
||
- Profile first. Ideas killed by measurement this week: sorted-chunk inserts (zero),
|
||
statement-batching the persist (zero — B-tree maintenance is the cost), RAM-disk/
|
||
in-memory DB build (SLOWER — fastInit already writes at page-cache speed).
|
||
- `CODEGRAPH_SYNTH_TIMINGS=1` now emits full phase walls (`[phase-timing]`) + pool/batch
|
||
timings. UI distorts phase walls — pipe stdout away.
|
||
- Check host load before timing (iOS simulators inflated every phase ~30%); the
|
||
Monitor-on-loadavg pattern (fire <3.5) gives clean windows.
|
||
- `grep` is aliased to ugrep and silently treats `callback-synthesizer.ts` as binary —
|
||
use `grep -a`.
|
||
|
||
## 7. AFTER the kernel: the follow-on roadmap (in order)
|
||
|
||
### 7a. Kernel-scale resolution speed — NOW THE TOP OPEN PERF ITEM
|
||
Confirmed by the R6 run (§4f): resolution is 19.2min of the 26.4min Linux-kernel
|
||
wall (73%) — ~~sequential BY DESIGN in the 2-CPU container (the resolver pool requires
|
||
≥4 cores to engage)~~ **(premise corrected in §7a.1: it was pooled all along)**.
|
||
Parse is 6.2min (23%) and belongs to the C/C++ port (R7a).
|
||
Steps: re-run cg1212 validation on ≥4-core allocation (pool + parallel synthesis
|
||
#1321/#1322 engage — this first measurement is cheap and may reshape the whole
|
||
problem); profile; likely levers: worker count scaling, batch size at scale,
|
||
`warmCachesYielding` on multi-GB DBs. Target: kernel <10min on a normal 8-core host.
|
||
|
||
#### 7a.1 First measurement round (2026-07-17) — the arc reshaped
|
||
|
||
The cheap first measurement was run and did exactly what it was for: it invalidated
|
||
the premise and surfaced two structural defects that now gate any speed work.
|
||
|
||
- **Premise correction — resolution was NEVER sequential in cg1212.** Pool sizing
|
||
(`resolver-pool.ts` `tryCreate`: `min(os.cpus().length − 2, 6)`, engage at ≥2)
|
||
uses `os.cpus()`, which is **cpuset-blind** — inside the 2-CPU container it saw
|
||
the Docker VM's 8 CPUs and ran **6 workers time-slicing 2 cores** (r6 log: 6×
|
||
`worker open`, 14k pool-timing lines). A real <4-CPU host (`os.cpus()` < 4) gets
|
||
no pool at all. This also explains why the earlier "19.5m sequential" and R6's
|
||
19.2m match: same 6-on-2 topology. `os.availableParallelism()` (cgroup/affinity-
|
||
aware) is the honest sizing input — candidate fix rides item (2) below.
|
||
- **Failure 1 — container at 8 real cores / 7GB: cgroup OOM (`oom_kill=5`,
|
||
`OOMKilled=true`), silent `EXIT=1`** (SIGKILL inside the liftoff re-exec surfaces
|
||
as code 1, no output). Died mid-parallel-synthesis, 4 passes in. At 8 real cores
|
||
all 6 workers hold peak anon memory *simultaneously* — the 2-core runs survived
|
||
only because time-slicing kept concurrent peak lower. **The pool sizes by cores
|
||
only; there is no memory-aware term and no size knob** (`CODEGRAPH_NO_PARALLEL_
|
||
RESOLVE` is all-or-nothing).
|
||
- **Failure 2 — WAL blowup at kernel scale: 22.2GB WAL on a 4.6GB DB** (container,
|
||
at death; the Mac-native attempt was watchdog-killed at 5GB free disk with the
|
||
WAL already 2.8GB at resolution *start*). Mechanism: the pooled resolution/
|
||
synthesis superphase writes continuously while 6 workers hold overlapping read
|
||
snapshots — checkpointing can never truncate past the oldest reader, so the WAL
|
||
accretes ~the phase's entire write volume. Invisible on medium repos (writes are
|
||
~100s of MB); at kernel scale it is a ~5× disk blowup and a page-cache pressure
|
||
source that feeds Failure 1. The #1231 WAL valve doesn't contain it (valve
|
||
checkpoints can't truncate past pinned readers either). Fix direction: workers
|
||
recycle their DB connection between batch rounds (release snapshots at a
|
||
checkpoint barrier), or an equivalent writer-coordinated `wal_checkpoint(RESTART)`
|
||
window; instrument to confirm the starvation point before building.
|
||
- **What did move: parse-loop 371.9s → 199.2s (1.87×) at 2→8 cores** (container,
|
||
clean window) — already riding the single-writer store floor (§4d), so the C/C++
|
||
port (R7a) will cut worker CPU but the parse wall won't drop below the writer
|
||
lane on many-core hosts.
|
||
- **Parity spot-check:** Mac-native post-parse node count 2,048,675 vs R6's final
|
||
2,048,664 (+11 across 2.05M; post-parse vs post-maintenance and a contended run —
|
||
not a parity gate, just no red flag; the dump-diff gates remain the authority).
|
||
- **Host truth for the target claim:** the dev Mac is 8 logical cores / 24GB —
|
||
literally the P1 target class. Its constraint is transient DISK (fresh kernel-
|
||
scale init currently needs ~25GB+ free for tree+DB+WAL until the WAL fix lands).
|
||
|
||
**Revised P1 order: (1) WAL containment → (2) memory-aware, cgroup-honest pool
|
||
sizing → (3) re-run the 8-core measurement (container at ≥12GB or the Mac with
|
||
disk headroom) → then profile what remains.** The <10min-on-8-cores target stands.
|
||
|
||
#### 7a.2 P1 items (1)+(2) SHIPPED 2026-07-17 — the implementation arc (#1332–#1335)
|
||
|
||
Four PRs, each carrying its measurement; the arc took three failed/diagnostic
|
||
kernel-scale runs to get right, and every failure taught a design fact:
|
||
|
||
| Run (2c/6GB unless noted) | Build | Outcome |
|
||
|---|---|---|
|
||
| R6 baseline | pre-P1 | 26.4min, EXIT 0; WAL unbounded (mid-run peak unmeasured); pooled 6-on-2 (cpuset-blind) |
|
||
| run 1 | #1332 hook | **EXIT 137 (OOM)** — WAL 22.2GB, 0 of 5.4M frames ever backfilled; futile 20-pass parks amplified memory churn |
|
||
| diagnostic | +latch/debug | EXIT 0, ~24min; pool KILLED by mis-measured 57MB cgroup budget → exposed **sequential resolution 853s vs 1,150s pooled** and cFnPtrEdges = 306s of synthesis's 358s |
|
||
| instrumented | +sizing fixes | EXIT 0, **21.6min (R6 −18%)**; parse floor restores 373.5s; passives complete but the FILE marched 361→721MB → named the wrap-never-happens gap; peak 17.2GB |
|
||
| record (first attempt) | #1335 | **EXIT 1: "database is locked"** — the timer-path truncate won the lock race after the recreate's multi-GB burst and stalled the writer past its 5s busy_timeout → truncate is barrier-only now (#1336). Bonus data: recreate 7.9s (vs 68–95s) once the WAL stays folded |
|
||
| **record** | **#1336** | **EXIT 0, 20.4min (R6 −23%); WAL peak 1.57GB (−14×); counts byte-exact 2,048,664/6,405,964.** parse 354.9s · resolution 812.5s · synthesis 329.0s · recreate 57.5s · maintenance 43.5s |
|
||
| **8-core retry (8c/7GB)** | **#1336** | **EXIT 0, NO OOM — 18.3min; WAL peak 1.09GB; pool sized 4 by the memory term (ap=8, budget 5.1GB, db 4.1GB); counts byte-exact.** parse 208.7s · resolution 835.9s · synthesis 338.7s |
|
||
|
||
**The 8-core verdict (the question P1 set out to ask): 18.3min vs the <10min
|
||
target — infrastructure fixed, speed target NOT met, and the gap is now
|
||
precisely characterized. Resolution is CORE-INVARIANT at kernel scale: 835.9s
|
||
pooled-4-on-8 ≈ 812.5s sequential-on-2 — worker parallelism buys nothing, so
|
||
the bottleneck is the per-ref main-thread path (admission + persist + per-ref
|
||
resolver work), not topology. Of the 18.3min, ~14min is core-invariant
|
||
resolution+synthesis. Next levers, in order: (a) profile the per-ref path
|
||
inside resolution (the 812–836s floor), (b) `cFnPtrEdges` (306s, 86% of
|
||
synthesis — parallelize/window WITHIN the pass), (c) the R7a C/C++ port
|
||
(parse 209s → kernel-native). 4× cores currently buys only 2min end-to-end
|
||
(20.4 → 18.3) because parse is the only core-scaling phase left.**
|
||
|
||
**Design facts these runs established (each now enforced in code + tests):**
|
||
|
||
1. **WAL backlog and WAL file are different resources.** Passive backfills bound
|
||
the backlog; the FILE only stops growing when a commit finds zero reader
|
||
marks — observed never in practice. Containment = backfill + **TRUNCATE at a
|
||
parked barrier** (the one guaranteed no-reader window) + a raw file-size
|
||
trigger at 4× the soft cap (#1334/#1335). Dubbo: 251MB → 69MB peak, dumps
|
||
byte-identical under aggressive fold cycling.
|
||
2. **cgroup v2 `memory.current` counts reclaimable page cache** — post-parse it
|
||
read 57MB free on a 6GB box and silently disabled the pool. `inactive_file`
|
||
is credited back (#1335); the same box reads 4.4GB.
|
||
3. **The pool loses to sequential at 2 real cores** (853s vs 1,150s resolution;
|
||
cold worker caches + serialization + time-slicing exceed the parallelism),
|
||
and pooled synthesis is Amdahl-bound by `cFnPtrEdges` (306s of 358s) at
|
||
kernel scale. Sizing: `min(availableParallelism − 1, 6)` + memory term +
|
||
`CODEGRAPH_RESOLVE_WORKERS` knob (#1333/#1335); ap=2 → sequential by choice.
|
||
4. **Parse needs ≥2 workers even on 2 cores** (1 worker = +34%; main + store
|
||
worker don't fill the second core). Floored (#1335): 373.5s ≈ the 369s
|
||
oversubscribed baseline, at a fraction of the memory.
|
||
5. **Silent failure modes burned three 25-minute cycles**: give-ups were
|
||
verbose-gated, sizing's null path logged nothing, the timer path logged
|
||
nothing. All valve/sizing decisions now print under `CODEGRAPH_SYNTH_TIMINGS`
|
||
/ `CODEGRAPH_WAL_VALVE_DEBUG` — the armed line answers "is it even alive"
|
||
in one glance.
|
||
|
||
**New synthesis lever surfaced:** `cFnPtrEdges` is 86% of kernel-scale synthesis
|
||
wall — parallelizing WITHIN that one pass (or windowing its scan) is worth more
|
||
than pooling all 36 passes. Filed under the next P1 profiling round.
|
||
|
||
#### 7a.3 Batch-loop profile + de-quadratic round (2026-07-17, #1339)
|
||
|
||
`CODEGRAPH_RESOLVE_PROFILE` (shipped in #1339: per-outcome resolveOne histogram
|
||
+ loop-stage attribution) overturned the arc's founding assumption — resolveOne
|
||
owns only **~93s** of the ~433s kernel-scale batch loop. Stage attribution and
|
||
what happened to each:
|
||
|
||
| Stage | Before | After #1339 | Note |
|
||
|---|---|---|---|
|
||
| countGuard | 93.9s | **0.0s** | per-batch COUNT(*) was O(remaining) — replaced by summed SQLite `changes` (zero-removals IS the runaway signal; real COUNT only arbitrates the suspicious path) |
|
||
| read | 54.6s | 57.2s | keyset replaced OFFSET, but the cost is row MAPPING (5000-row materialization + candidates JSON), not prefix-walking — theory falsified, keyset kept as hygiene; lever = leaner row mapping |
|
||
| backpressure | 111.2s | 121.2s | DB-scaled caps didn't help: the fold tax is TOTAL checkpoint I/O (write set is cold pages, not re-dirtied hot ones) — a disk-I/O floor ≈ WAL bytes written |
|
||
| settle (resolveOne) | 85.7s | 88.0s | the real work; exact-match 3.17M×13µs=41s is the biggest legit class |
|
||
| inserts/deletes/marks | ~84s | ~84s | B-tree floor (#1320 post-mortem) |
|
||
|
||
**2c/6GB envelope: 26.4min (R6) → 20.4 (#1336) → 19.3min (#1339), counts
|
||
byte-exact every run; dubbo dump byte-identical.** The 8-core re-run post-#1339
|
||
is pending (est. ~17.5min from the stage arithmetic). Remaining levers by size:
|
||
parse 351s→R7a C/C++ port; cFnPtrEdges 306s; backpressure 121s (I/O floor —
|
||
shrinks only by writing fewer bytes); settle 88s; read-mapping 57s.
|
||
|
||
#### 7a.4 cFnPtrEdges round (2026-07-17, #1341) — 2.07× standalone, probe-driven
|
||
|
||
Iterated with a STANDALONE in-container probe against the live kernel DB
|
||
(readonly; ~4min/cycle instead of 25-min inits) with per-sweep sub-timings
|
||
(`CODEGRAPH_SYNTH_TIMINGS` prints the `cFnPtr sub:` line):
|
||
|
||
| Iteration | Standalone total | What moved |
|
||
|---|---|---|
|
||
| baseline | 278.8s | attribution: E 112s, D 92s, strip 71.8s (4.4×/file), C 41s |
|
||
| regex hoists + D field-name pre-gate + incremental line count | 249.3s | D −24s |
|
||
| budget-aware strip cache (first cut) | — | **thrash lesson: a partial LRU on cyclic sweeps ≈ 0% cross-sweep hits** — cap ~61k AND cap == files.length both lost (includes push the working set over) |
|
||
| all-or-nothing cache + 5% slack | 187.8s | strips exactly 1.0/file; `getNodesInFile` theory killed (10s, not 127s) |
|
||
| `sliceLines` → split-once-per-file | **134.8s** | ~1.6M full-file splits eliminated (D 46→20.5s, E 94.5→69.1s) |
|
||
|
||
**Identity proof at full scale:** optimized edge set (merge-dedup + canonical
|
||
sort, 274,762 edges) SHA256 `21c2a971…` == the pre-optimization edges
|
||
extracted from the live kernel DB — this pass never runs on the dubbo gate
|
||
repo (C-gated), so the DB comparison is the right gate. Suite 2,491.
|
||
|
||
**In-run validation (2c/6GB): total 17.6min (from 19.3; −33% cumulative vs
|
||
R6), synthesis 336→251s, counts byte-exact, WAL 1.09GB.** Honest caveat: the
|
||
full strip cache did NOT engage in-run at 6GB (mid-run memory budget below
|
||
the 2×-cache safety threshold → deliberate fallback to the 128 floor; strips
|
||
283k, costing ~60s vs the probe) — the memory-safe degradation working as
|
||
designed. Boxes with headroom get the full 2.07×; the 6GB envelope gets the
|
||
algorithmic wins only.
|
||
|
||
**Levers remaining, re-ranked:** parse 338s (R7a C/C++ port — the last big
|
||
rock) > backpressure ~120s (checkpoint I/O floor) > E-scan 69–93s (approaching
|
||
honest regex work over 1.5GB) > settle 88s > read-mapping 57s.
|
||
|
||
#### 7a.5 8-core re-run, post-R7a (2026-07-17) — 16.4min; the 8c gap is now all resolution
|
||
|
||
Same provisioning as the §7a.2 retry (cg1212 at cpuset 0-7 / 7GB), the deployed
|
||
R7a build, fresh init of the v7.2-rc2 tree: **EXIT 0, envelope 981s = 16.4min**
|
||
(pre-R7a 8c record: 18.3min — and that was the smaller pre-blank graph).
|
||
Counts **2,048,295 / 6,406,933 == both 2c arms**; WAL peak 1.34GB (same
|
||
contained regime as 1.09–1.57GB records). Phases: parse-loop **202.6s**
|
||
(pre-R7a all-wasm 8c: 208.7s — both sit ON the single-writer store floor, so
|
||
8c parse is writer-bound, not extraction-bound) · resolution superphase
|
||
**715.0s** (was 835.9s) containing callback-synthesis **257.4s** (was 338.7s)
|
||
and edge-index-recreate 52.0s · maintenance 47.6s. The −1.9min vs the record
|
||
is the post-#1336 rounds (#1339 countGuard, #1341 cFnPtr, R7a native parse +
|
||
defer-reuse) landing at 8c for the first time.
|
||
|
||
**Consequence for the <10min target:** ~12 of the 16.4 minutes are the
|
||
core-invariant resolution superphase. Deferral cuts can't materially move the
|
||
8c envelope (parse is already at the writer lane); they remain queued for
|
||
graph richness + the 2c/low-core envelope. The 8c target now lives or dies on
|
||
the per-ref resolution path (§7a.2's lever (a)).
|
||
|
||
#### 7a.6 Per-ref path measurement round (2026-07-18) — fresh tables, two falsifications, two live levers
|
||
|
||
Fresh `CODEGRAPH_RESOLVE_PROFILE` tables on the round-2 build (v7.2-rc2 tree,
|
||
cg1212), then two cache experiments run against them — both killed by
|
||
measurement, code reverted same-day; this section is what survives.
|
||
|
||
| stage (batch loop) | 2c sequential (clean host) | 8c pool-4 |
|
||
|---|---|---|
|
||
| read | 37.0s | 33.9s |
|
||
| settle (resolveOne) | 79.7s | **3.6s** |
|
||
| backpressure | 138.3s | 121.9s |
|
||
| createEdges | 3.4s | 7.6s |
|
||
| insertEdges | 33.8s | **55.3s** |
|
||
| deletes | 37.7s | **118.8s** |
|
||
| marks | 5.3s | 6.5s |
|
||
| **loop total** | **339s** | **357s** |
|
||
|
||
2c: superphase 645s (loop + synth 251.6s [cFnPtr ~230: E 95.0 + strip 78.5
|
||
at n=283k, budget-declined again + C/D 88.8] + recreate 54.5); envelope
|
||
**16.5min — the new 2c record** (the 17.1 r2-gate figure carried host
|
||
contamination). Settle decomposition: exact-match 35.1s @ 11µs × 3.17M,
|
||
import 16.9s, fail:calls 9.2s × 1.63M. 8c: superphase 655.5s (+ synth 242.6
|
||
+ recreate 56.1), envelope 14.95min — **n=2 range 15.0–16.4min with the
|
||
morning's run; report ranges, never single runs on this box.** 8c parse
|
||
178.5s: round 2's deferral cuts DID move the 8c parse wall (202.6 → 178.5)
|
||
— §7a.5's "writer-floor won't move" prediction was partly wrong.
|
||
|
||
- **The pool double-buffer WORKS.** settle 3.6s at 8c — the workers absorb
|
||
the entire 3.17M exact-match population (12–17s per worker, parallel).
|
||
§7a.2's "resolution is core-invariant" framing is superseded: the 8c cost
|
||
was never resolveOne.
|
||
- **THE 8c anomaly — writes-under-readers:** deletes 37.7 → 118.8s (+81)
|
||
and insertEdges 33.8 → 55.3s (+22) with 4 readonly workers attached.
|
||
Main-thread B-tree writes run ~3× slower under the pool. Mechanism
|
||
UNPROVEN — candidates: page-cache competition (4 × 32MB worker caches +
|
||
reads), WAL read-through depth while readers hold positions, wal-index
|
||
lock contention. Next probe: instrument (per-op delete timing vs worker
|
||
activity windows), then either shorten reader hold-times (worker
|
||
connection recycling at the barrier — §7a.1's original fix direction,
|
||
never built) or cut delete volume. Potential ≈ −100s at 8c.
|
||
- **Killed by measurement #1 — nameCache scaling (the 5k-thrash theory).**
|
||
v1: budget-scaled classic LRU (~478k entries) → settle 102.7s,
|
||
exact-match 52.7s @ 17µs — WORSE; delete+set-per-get churn on a huge Map
|
||
plus resident-array GC ate more than the SQLite statements saved. v2:
|
||
mutation-free second-chance cache at 250k → exact-match 37.9s @ 12µs ≈
|
||
the 35.1s baseline. Verdict: the 11µs is NOT refetch overhead — the 5k
|
||
cache already holds the true Zipf head, the tail doesn't repeat enough
|
||
to cache at any size, and the floor is the per-ref JS around one indexed
|
||
lookup. Both variants byte-correct (counts 2,049,153/6,413,518; git dumps
|
||
byte-identical) — correctness was never the issue. Code reverted; the
|
||
second-chance design lives in this entry if a big-RAM-validated attempt
|
||
ever wants it.
|
||
- **Killed by measurement #2 — lazy `candidates` JSON parse:** read stage
|
||
37.0 → 38–40s across variants (flat). The eager parse was never the read
|
||
cost; row materialization + the statement walk is. Reverted.
|
||
- **Levers, re-ranked:** writes-under-readers probe (+102s at 8c — the
|
||
single biggest attributed delta) > cFnPtr NATIVE SITE EXTRACTION
|
||
(synthesis ~230s: emit fn-ptr assignment sites from the C walker at parse
|
||
time for the now-66% kernel-routed population — E-scan 95s + reads + much
|
||
of strip 78s die; needs bug-for-bug regex-semantics parity in Rust and
|
||
the raw-vs-preParsed scan-text question settled first) > backpressure
|
||
byte volume (~122–138s I/O floor; value-neutral schema interning is
|
||
migration-wide — parked) > recreate 54–70s.
|
||
- Box note: cg1212's 6–7GB deliberately degrades the cFnPtr strip cache
|
||
(~80s paid in-container that a 24GB target-class box gets back free) —
|
||
container numbers UNDERSTATE the true 8-core-class target.
|
||
|
||
#### 7a.7 Writes-under-readers probe + fix (2026-07-18) — WAL depth named, worker connection recycling shipped
|
||
|
||
Five discriminating runs (all 8c pool-4 unless noted, same tree/build family;
|
||
each ~16min), then the fix in two cadence iterations:
|
||
|
||
| run | deletes | insertEdges | read | backpressure | settle | superphase |
|
||
|---|---|---|---|---|---|---|
|
||
| pool-4 baseline | 118.8 | 55.3 | 33.9 | 121.9 | 3.6 | 715.0 |
|
||
| pool-OFF | 42.6 | 38.3 | 32.6 | 120.1 | 108.5 (main) | 685.9 |
|
||
| workers=2 (dose) | 58.9 | 54.0 | 23.9 | 174.7 | 10.3 | 670.0 |
|
||
| v2 caches @8c | 108.0 | 50.3 | 31.4 | 168.0 | 3.9 | 687.3 |
|
||
| valve 64MB | **56.5** | **27.8** | **16.8** | 297.2 | 3.8 | 739.2 |
|
||
| **recycle c25** | 99.8 | 42.9 | 32.7 | 148.8 | 3.7 | 663.3 |
|
||
| **recycle c8 (SHIPPED)** | 104.3 | 47.8 | 32.5 | 127.6 | 4.0 | **633.6** |
|
||
|
||
- **Mechanism proven: WAL read-through depth under reader pins.** Pool-off
|
||
restores writes on identical hardware (deletes 118.8 → 42.6); the dose
|
||
scales with reader count (knee between 2 and 4); the aggressive valve —
|
||
which forces a shallow WAL — recovers deletes/inserts/read to their floors
|
||
(56.5/27.8/16.8) but overpays +129s in full-park folds. Reconciliation:
|
||
checkpoints run in every topology, but READERS PIN their progress — the
|
||
WAL runs deep exactly when workers are attached, and deep-WAL page
|
||
operations tax the writer everywhere (including the unattributed
|
||
between-stage spans, recreate, and synthesis reads).
|
||
- **v2-at-8c falsified the cache resurrection** (deletes 108.0 ≈ baseline):
|
||
name-lookup traffic is long-tail-dominated — uncacheable at any capacity —
|
||
so reader traffic can't be reduced by caching. The caching family is
|
||
triple-dead (2c settle, 8c writes).
|
||
- **The fix: worker connection recycling at the pool-idle boundary**
|
||
(`ResolverPool.recycleWorkers` + `QueryBuilder.rebind` + a cadence call at
|
||
the double-buffer's worker-idle point). Workers close/reopen their
|
||
read-only connections every 8 batches (~40k refs) — reopens are
|
||
sub-millisecond, resolver caches survive (only prepared statements
|
||
re-prepare), and the existing checkpoints advance instead of parking.
|
||
Cadence 25 → 8 iterated by measurement; 8 wins via diffuse gains
|
||
(backpressure −21, recreate 59.7 → 45.3). Attributed deletes stay ~100
|
||
(the WAL still re-deepens between recycles — the valve's 56.5 floor
|
||
needs continuous shallowness), but the SUPERPHASE captures the true win:
|
||
**715.0 → 633.6s (−11.4%)**; envelope best-of **14.8min at 8c**
|
||
(890s; band across the day's runs 14.8–16.4). Byte-neutral: git dumps
|
||
byte-identical old-vs-new, linux dump sha `6dd1185b…` reproduced, counts
|
||
2,049,153/6,413,518 every run, suite 2517 green. 2c unaffected by
|
||
construction (no pool → no recycling).
|
||
- Levers after this round: cFnPtr native site extraction (~230s synthesis,
|
||
§7a.6 ranking stands) > continuous-shallow WAL (close the remaining
|
||
~45s gap between recycling's ~100s deletes and the valve's 56.5 floor
|
||
without full-park folds — e.g. passive-checkpoint nudges at the recycle
|
||
boundary) > backpressure byte volume > recreate.
|
||
|
||
#### 7a.8 cFnPtr calibration round (2026-07-18) — the 230s decomposed; fuse-then-link is step 1
|
||
|
||
Three quick measurements before any port, two of them killing assumptions:
|
||
|
||
- **JS strip rewrite: killed by measurement.** stripCStyle's `split('')`
|
||
looked like allocator pathology; a segment-builder rewrite (byte-identical,
|
||
pinned by `__tests__/strip-cstyle-differential.test.ts` — kept as the
|
||
oracle for any future rewrite) measured **1.0×** on 15.1M chars of linux
|
||
C. V8's scan rate is the honest cost: **~73MB/s**, and 283k strips ≈ 4
|
||
strips/file × ~20KB × that rate ≈ the observed 78s. The strip lever is
|
||
the **4× redundancy** (all-or-nothing cache declined at 6–7GB → every
|
||
sweep re-strips), not the scanner.
|
||
- **E-stage regexes alone: ~46MB/s → ~30s of E's 95s.** DISPATCH_RE +
|
||
ARRAY_DISPATCH_RE over stripped kernel/ text yield 1,112 matches / 15.1M
|
||
chars. The other ~65s is per-match logic, body slicing, lineAt, and
|
||
getNodesInFile. A native regex scan alone caps at −30s.
|
||
- **Calibrated attack for the ~230s, re-ordered:**
|
||
1. **Fuse-then-link refactor (TS, step 1):** one per-file extraction pass
|
||
computes strip ONCE and collects {function macros, object macros,
|
||
defined sets, struct fields, raw registration matches, raw dispatch
|
||
matches, per-function declared-receiver types}; a text-free global
|
||
linking pass then builds registries and edges. Kills the 4× strip
|
||
(−~58s) + repeated reads (−~8s) + part of E's slicing overhead.
|
||
Parity discipline: collectors insert in the same file order the
|
||
global passes iterate today (Map insertion order = current registry
|
||
order), FANOUT_CAP and match-evaluation order preserved per function;
|
||
gate = edge-set hash vs the live kernel DB (§7a.4 probe) + linux dump
|
||
sha. The chain/receiver resolution must be pre-collected as per-file
|
||
declared-type tables so linking never touches text.
|
||
2. **Native per-file extractor (step 2):** the same boundary then accepts
|
||
a Rust implementation of the per-file pass (raw text in, collected
|
||
records out — no preParse interaction; the synthesizer reads raw disk
|
||
text). Bug-for-bug regex semantics required; worth it only for the
|
||
remaining ~100s of per-file scan+logic after step 1 lands.
|
||
- Note for step 2 sizing: strip at native memchr rates (~500MB/s+) would
|
||
be ~6-10s for the full corpus even before redundancy cuts — but marshal
|
||
(UTF-16↔UTF-8 across napi) eats seconds at GB scale; batch the calls.
|
||
|
||
#### 7a.9 cFnPtr fuse-then-link step 1 landed (2026-07-19) — pass −22%, strips halved
|
||
|
||
Step 1 shipped, with one deliberate deviation from the §7a.8 sketch. The
|
||
"text-free global linking" ideal is unreachable at byte-parity without
|
||
retaining per-file text or macro tables, and a sizing probe on the linux tree
|
||
killed retention: **6.1M `#define` lines** (the amdgpu register headers alone
|
||
are most of them — 565MB of define text), and unrestricted initializer-body
|
||
capture is a #1212-class hazard. What ships instead:
|
||
|
||
- **One extraction sweep** (read+strip per file exactly once): typedef names,
|
||
per-struct-node field declarations parsed structurally with fn-pointer
|
||
classification DEFERRED (typedef sets aren't complete mid-sweep), resolved
|
||
local includes, an alias-shaped-object-macro name set, and per-file
|
||
SURVIVAL FILTERS — distinct init type tokens, array element types,
|
||
inline-struct summaries, field-assign pairs, dispatch fields/array names.
|
||
All interned; a few MB at kernel scale (measured distinct: 110k assign
|
||
pairs, 87.6k init tokens, 38.7k dispatch fields; only 16% of files have any
|
||
dispatch-shaped match at all).
|
||
- **Linking replays the ORIGINAL pass bodies verbatim**, gated by the
|
||
filters: struct layouts register by replaying the struct kind-scan (rowid
|
||
order — same-name precedence is order-sensitive), and the
|
||
registration/propagation/dispatch loops run only for surviving files, whose
|
||
text is lazily re-stripped (LRU-served). Filters only ever over-approximate
|
||
(full-file no-skip scans ⊇ the jump-cursor/per-body scans the real passes
|
||
run), and a filtered-out file is one where every match fails the pass's own
|
||
gates before any side effect — so parity is by construction, not by hope.
|
||
Macro tables stay lazy (LRU + strip-on-miss) per the 6.1M-defines probe.
|
||
- **Why not pure one-sweep C:** the inline-struct scan's cursor jump is gated
|
||
on the fn-ptr-field test, which needs the complete typedef sets — a
|
||
collect-time emulation diverges on the gate-fail rescan path. Keeping
|
||
today's scan code and paying a filtered second strip is the parity-safe
|
||
trade.
|
||
|
||
Measured (8c cg1212, quiet host, fresh kernel init): cFnPtr sub
|
||
**A=94.5s B=1.5s C=39.7s D=24.8s E=18.5s = 179s vs the §7a.8 ~230s (−22%)**;
|
||
strips **283.5k → 132.4k** (4.44 → 2.08/file; 78s → 46.6s); stage E collapsed
|
||
95 → 18.5s (survivor-only slicing/getNodesInFile), C+D 89 → 64.5s;
|
||
callback-synthesis phase 250 → **199.9s**. Standalone probe-to-probe on the
|
||
same live DB (warm cache): 139 → 122s. Resolution superphase unaffected
|
||
(622.7s ≈ #1362's 633.6). Under the −70-90s hope — the honest ledger is that
|
||
~0.6 sweeps of survivor re-strips + the unchanged include-unit machinery stay,
|
||
and A now carries all regex scans.
|
||
|
||
Gates, all green: probe-hash identical on the live kernel DB (279,335 edge
|
||
rows, `f6e1713d…` both builds); git/redis/vim/SameBoy full dumps
|
||
byte-identical old-vs-new (705/852/433/180 fn-ptr edges — macro tables,
|
||
`commands.def`, `#ifdef` include-units, inline structs, bare arrays all
|
||
exercised); kernel-parity 0-diffs on git/redis/fmt/protobuf with deferral
|
||
unchanged (12.2/24.1/42.5/25.7%); linux counts exact 2,049,153/6,413,518;
|
||
linux dump sha reproduced (`6dd1185b…`); full suite green.
|
||
|
||
Step 2's boundary is now stage A verbatim: raw text in → records out, no
|
||
graph access inside the sweep except `getNodesInFile` for struct extents. Its
|
||
94.5s (46.6s strip + scans) is the native-extractor prize; C's 39.7s
|
||
(macro-env + include units) and D's 24.8s stay TS.
|
||
|
||
#### 7a.10 cFnPtr native sweep landed (2026-07-19) — step 2 done, pass 230→151s across the arc
|
||
|
||
Step 2 shipped: `cfnptr_scan_files` in the kernel (codegraph-kernel/src/
|
||
cfnptr.rs) runs the entire extraction sweep natively — strip + all ten
|
||
scanners — batched 16 files per NAPI call; the TS sweep remains as the
|
||
fallback (no binary, feature detection against older binaries,
|
||
`CODEGRAPH_KERNEL=0`, or the scanner's own `CODEGRAPH_KERNEL_CFNPTR=0`).
|
||
What made it land at byte-parity:
|
||
|
||
- **Hand-rolled byte machines, not the regex crate.** The JS engine's
|
||
semantics are the spec: `\w`/`\b` are ASCII while `\s` is the Unicode
|
||
class (NBSP/U+2000-200A/FEFF — decoded explicitly from UTF-8), alternation
|
||
order, `lastIndex` resume, and the observable backtracking dimensions
|
||
(INIT/ARRAY modifier-count and `struct`/star/bracket optionals, DISPATCH's
|
||
greedy segment loop) are reproduced structurally; greedy-only shortcuts are
|
||
taken solely where analysis shows backtracking can never rescue a match
|
||
(documented per scanner).
|
||
- **The native stripper blanks per UTF-16 code unit** (two spaces for an
|
||
astral char), so its output is string-identical to the TS stripper — the
|
||
scanners run over the very character stream the JS regexes see, and the
|
||
strip differential oracle gained a kernel arm pinning that equality on the
|
||
same fixtures + 500 seeded random cases.
|
||
- **Gates, all green:** record/edge differential suite (adversarial fixture
|
||
project indexed native-vs-JS: identical edge streams; CRLF, NBSP,
|
||
continuations, decoy strings, unterminated comments, backtracking shapes);
|
||
repo differential on git/redis/vim/SameBoy (edge streams IDENTICAL,
|
||
705/852/433/180); probe-hash on the live kernel DB reproduced
|
||
`f6e1713d…` (279,335 rows) exactly; linux init counts exact
|
||
2,049,153/6,413,518 and dump sha `6dd1185b…` reproduced; full suite green
|
||
×2 (153 files / 2588).
|
||
|
||
Measured (8c cg1212, quiet host — one contaminated run discarded: the Mac
|
||
slept mid-init on battery and froze the VM, inflating resolution 4×; pmset
|
||
log confirmed, re-run caffeinated): cFnPtr sub **A=47.9s B=1.1 C=40.9 D=24.1
|
||
E=36.8 = 150.9s** vs step 1's 179s (−28s) and the pre-arc 230s (**−79s
|
||
cumulative, −34%**). The sweep itself halved (94.5 → 47.9s; JS strips
|
||
132.4k → 68.9k — exactly the sweep's share moved native). E's attributed
|
||
wall grew (18.5 → 36.8s): parallel synthesis overlap shifted as A finishes
|
||
earlier — stage walls absorb concurrent passes' contention; the phase total
|
||
is the honest number and **callback-synthesis fell 199.9 → 171.1s**.
|
||
Remaining cFnPtr ledger: C 40.9s (macro envs + include units, TS),
|
||
E's replay + overlap, D 24.1s, A's remaining 47.9s (reads, batching,
|
||
interning, DB struct extents — diminishing). The pass is no longer the
|
||
dominant synthesis lever; next per §7a.7 ranking: continuous-shallow WAL,
|
||
backpressure bytes.
|
||
|
||
Deploy note: this change ships RUST code — dist-only deploys are no longer
|
||
sufficient for it; rebuild the `.node` per platform (cg1212: cargo build in
|
||
`rust:1-bookworm` with `CARGO_TARGET_DIR=target-linux`, stage the `.so` as
|
||
`prebuilds/linux-arm64/codegraph-kernel.node`).
|
||
|
||
### 7b. Arc 3 — graph richness (forensics-backed; adopt cbm's real extras, skip inflation)
|
||
Priority order, each gated by the standard A/B + node-explosion probes:
|
||
1. **Test→subject edges** (first-class `tests` edges at index time; we compute covering
|
||
tests at query time today; cbm materializes 14.8k on dubbo). Feeds test-gap detection
|
||
(Lite headline) + Pro risk signals. Cheapest, do first.
|
||
2. **Per-node code metrics** (complexity, cognitive, `is_test`, `is_entry_point`,
|
||
param counts) — computed during extraction (the kernel makes this nearly free —
|
||
design the buffer contract with a metrics slot!). Feeds Pro risk-ranking verdicts +
|
||
explore ranking de-noise.
|
||
3. **Read/write distinction on references** (`USAGE` vs `WRITES`). The measured agent
|
||
frontier ("who mutates this state" — the canvasNonce class). HIGHEST value, HIGHEST
|
||
risk: scope to exported/state-relevant symbols; the tracking-every-local explosion is
|
||
the known failure mode (#999/#1212 class). Full validation methodology.
|
||
4. **Exception-flow edges** (`raises`) — throw→handler; moderate.
|
||
5. **Doc Section nodes** (markdown headings as nodes, linked to code) — maps onto Pro's
|
||
synced-business-docs story.
|
||
6. **IaC nodes** (k8s/docker/kustomize as graph nodes with cross-references).
|
||
NOT worth chasing (verified in their cache schema): per-variable node inflation (85% of
|
||
their node count), DB size parity (theirs is ~60% allocation slack), similarity vectors
|
||
in the core engine.
|
||
|
||
### 7c. Deferred/parked (needs explicit approval before starting)
|
||
- Single-file SEA binary (distribution polish; zero speed).
|
||
- Team-shared graph artifact (cbm's `graph.db.zst` idea — good, but design it for the
|
||
Pro shared-worker story, not as an OSS clone).
|
||
- Full native rewrite: rejected with data — the moat (2,444 tests, byte-identical
|
||
determinism, this week's two caught-by-gate bugs) lives in the TS reference.
|
||
|
||
## 8. Context for the executing agent
|
||
|
||
- House rules live in `CLAUDE.md` (repo root) — the retrieval invariants, A/B model
|
||
policy, release rules (never `npm publish`/push tags), changelog format.
|
||
- This week's PR trail tells the story and the style: #1305, #1320 (checkpoint deferral +
|
||
double-buffered persist; THE invariant: batch k+1 READS batch k's edges — supertype
|
||
walks — so edges insert before fan-out), #1321 (parallel synthesis via pool reuse,
|
||
registry order = merge order), #1322 (bulk edge load, identity index stays), #1323
|
||
(kernel-scale hardening: skip-don't-retry-on-main >1.5M nodes, yielding index recreate).
|
||
- Every perf PR shipped byte-identical with the dump-diff gate; keep that bar.
|
||
- Competitive context (validated 2026-07-16): cbm wins medium-repo fresh index 1.55–1.8×
|
||
(their RAM-first design); we win sync 2.4–2.8×, agent A/B (their 14 tools drew ZERO
|
||
calls in 8/8 runs), call-graph density 1.3–2.3×, and the constrained-hardware envelope
|
||
(Linux kernel on 2-CPU/6GB: we complete in 27min, they die at 0.16% — their speed IS
|
||
their memory floor). The kernel project closes their last number without giving up any
|
||
of ours.
|