Просмотр исходного кода

feat(kernel): R7a C/C++ walker — dual-lang ccpp module, preParse hoist, 7 new blanks, c/cpp default-routed (#1346)

Parity: 0 diffs on redis/git/fmt/protobuf/ALS sweeps; full-init dumps
byte-identical on all five + linux at kernel scale (10.4M dump lines,
same sha256 both arms). Linux 2c/6GB envelope: kernel-arm 19.1min vs
wasm-arm 22.9min (parse 356s vs 435s) on a much richer graph (the new
blanks recover error-swallowed code: git 2x nodes, linux kernel/+mm/ 3x).
Deferral guard corrected by measurement (C/C++ error incidence 9-42%;
--max-deferral flag); defer-reuse memo kills the 3x re-blank/re-parse
cost deferred files paid.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Colby Mchenry 1 месяц назад
Родитель
Сommit
2d72891b59

+ 2 - 1
CHANGELOG.md

@@ -11,7 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### New Features
 
-- Indexing TypeScript, TSX, JavaScript, JSX, Java, Python, and Go projects is faster: parsing and symbol extraction now run in a native engine when a prebuilt binary is available for your platform (release bundles include one), producing exactly the same graph — verified byte-for-byte against the previous engine on real repositories, from small libraries up to vscode-, dubbo-, and django-scale codebases (Lombok-generated members included). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
+- Indexing TypeScript, TSX, JavaScript, JSX, Java, Python, Go, C, and C++ projects is faster: parsing and symbol extraction now run in a native engine when a prebuilt binary is available for your platform (release bundles include one), producing exactly the same graph — verified byte-for-byte against the previous engine on real repositories, from small libraries up to vscode-, dubbo-, django-, git-, and protobuf-scale codebases (Lombok-generated members, C function-pointer tables, and Unreal-Engine-style macro-heavy headers included; CUDA and Metal sources ride the C++ path). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
 - Reference resolution now runs in parallel on large projects. When a project has enough pending references to make it worthwhile (roughly 150k+, typical for big Java/Kotlin/Spring codebases), resolution fans out across worker threads while results are applied in the exact order the single-threaded path would have used — the graph comes out byte-for-byte identical, about twice as fast end-to-end on a 4,000-file Java project in our testing. Small projects keep the single-threaded path automatically (the fan-out costs more than it saves there). Set `CODEGRAPH_NO_PARALLEL_RESOLVE=1` to disable, or `CODEGRAPH_PARALLEL_RESOLVE_MIN=<count>` to tune when it engages.
 - Indexing large projects got another sizeable speedup — about a quarter less wall-clock on the same 4,000-file Java project, with the graph still byte-for-byte identical. Two changes: the database no longer interleaves expensive checkpoint housekeeping into the middle of resolution on a fresh index (it's folded once at the end instead), and while one batch's results are being written out, the worker threads are already resolving the next batch instead of sitting idle.
 - The dynamic-dispatch analysis that runs at the end of indexing (callback, event, and framework wiring) now runs its passes in parallel on large projects, cutting that stage roughly in half there — and a pass that crashes now retries safely instead of failing the whole index, which also makes very large codebases that previously died in this stage more likely to index to completion. Graphs remain byte-for-byte identical.
@@ -34,6 +34,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Deleting a whole directory is now picked up by watch mode: the files inside it are removed from the index on the next auto-sync instead of lingering as stale records until an unrelated edit happened to trigger one. Operating systems often report a directory deletion as a single event on the directory itself (with no per-file events for its contents), which the watcher previously discarded. (#1285)
 - `codegraph sync` now gets the same slow-disk fix that made full indexing fast in 1.4.0: database checkpointing is deferred for the whole incremental run instead of firing every few megabytes of writes. On mechanical drives and other high-latency storage, a small sync on a large index no longer stalls for minutes at near-zero CPU — the cost of a sync scales with what changed, not with the size of the existing index. The same `CODEGRAPH_NO_WAL_DEFER=1` switch turns it off. (#1248)
 - C functions declared with a project-specific attribute macro in front of a typedef'd return type (`SEC_ATTR UINT32 MyFunc(VOID)` — common in embedded and kernel code) are now indexed under their real names. Previously the parser tripped over the unknown macro and stored the parameter list as the function name, leaving entries like `"(VOID)"` in the graph and making the real function unfindable. (#1211)
+- Macro-heavy C and C++ code indexes much more completely. Six ubiquitous idioms that previously tripped the parser into error recovery — dropping or garbling the surrounding symbols — now parse cleanly: the `#ifdef __cplusplus` / `extern "C" {` compatibility guard in C headers, iterator macros in statement position (`list_for_each_entry(pos, head, member) { … }` and the whole Linux-kernel/git/jemalloc family, braced or single-statement), the Linux/sparse declaration annotations (`static int __init foo(void)`, `void __user *buf`, `container_of(p, struct T, m)`), trailing parameter annotations (`int argc UNUSED`, git's house style), namespace-management macros alone on a line (`FMT_BEGIN_NAMESPACE`, Qt's `Q_OBJECT`), and function attribute macros in front of C++ return types. On git's own repository this nearly doubles the number of indexed symbols, and on the Linux kernel's `kernel/` and `mm/` directories it triples them; blast radius and callers get correspondingly more complete. A related fix stops the existing macro handling from corrupting `#define` lines that mention the same macro names, which removed a class of phantom parse errors in fmt-style headers.
 - C++ methods defined out-of-line inside a namespace (`namespace sim { Output MyClass::Apply(...) { ... } }`) now carry the namespace in their qualified name, matching their class. Fully-qualified call sites from other files (`sim::MyClass::Apply(...)`) resolve to the definition again, so `codegraph callers` and file impact no longer come up empty for this pattern. (#1291)
 - C++ methods defined out-of-line on a template class (`template <typename T> T Box<T>::get() { ... }`) no longer keep the template parameter list in their qualified name. They now index as `Box::get` — identical to an inline definition of the same method — so they link to their class and resolve from call sites again, and pathological multi-line template parameter lists can no longer blow the qualified name past filesystem name limits. (#1286)
 - Go route detection no longer misidentifies ordinary method calls that share HTTP verb names — `cache.Put("key", value)`, `store.Get("config", out)`, `bus.Handle("user.created", handler)` and the like were being indexed as HTTP routes, polluting route listings in cache-heavy codebases. A registration now has to look like one: its first argument must be a `/`-prefixed path (all routers) or a Go 1.22 `"METHOD /path"` pattern on `Handle`/`HandleFunc`, which now also extracts the method instead of listing the route as `ANY`. (#1259)

+ 136 - 0
__tests__/extraction.test.ts

@@ -11162,3 +11162,139 @@ import DataStore from '../data/DataStore';
     });
   });
 });
+
+// R7a preParse additions — the blanking passes added so macro-heavy C/C++
+// parses clean enough for the kernel route (each also improves the wasm
+// path's own graphs). Offset preservation is load-bearing everywhere.
+describe('C/C++ kernel-port preParse blanks (R7a)', () => {
+  it('blankCCplusplusGuardBodies blanks extern-C guard bodies, keeps directives', async () => {
+    const { blankCCplusplusGuardBodies } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      '#ifdef __cplusplus',
+      'extern "C" {',
+      '#endif',
+      'int real_decl(void);',
+      '#ifdef __cplusplus',
+      '}',
+      '#endif',
+      '',
+    ].join('\n');
+    const out = blankCCplusplusGuardBodies(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('extern "C"');
+    expect(out).toContain('#ifdef __cplusplus'); // directives stay
+    expect(out).toContain('int real_decl(void);');
+    // A guard with a nested directive bails (needs real preprocessing).
+    const nested = [
+      '#ifdef __cplusplus',
+      '#define EXTERNC extern "C"',
+      '#endif',
+      '',
+    ].join('\n');
+    expect(blankCCplusplusGuardBodies(nested)).toBe(nested);
+    // The `#ifndef` inverse guard is C-visible and must be untouched.
+    const inverse = ['#ifndef __cplusplus', 'int c_only(void);', '#endif', ''].join('\n');
+    expect(blankCCplusplusGuardBodies(inverse)).toBe(inverse);
+  });
+
+  it('blankLoneMacroLines blanks namespace-management macros, spares expression operands', async () => {
+    const { blankLoneMacroLines } = await import('../src/extraction/languages/c-cpp');
+    const src = ['FMT_BEGIN_NAMESPACE', 'struct S { int x; };', 'FMT_END_NAMESPACE', ''].join('\n');
+    const out = blankLoneMacroLines(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('FMT_BEGIN_NAMESPACE');
+    expect(out).toContain('struct S { int x; };');
+    // An ALL-CAPS operand alone on a line inside a multi-line expression is
+    // NOT a lone macro — the next line starts with an operator.
+    const expr = ['int x = 0', '  | FLAG_ONE', '  | FLAG_TWO;', ''].join('\n');
+    expect(blankLoneMacroLines(expr)).toBe(expr);
+    const cont = ['int y =', 'SOME_FLAG', '| OTHER;', ''].join('\n');
+    expect(blankLoneMacroLines(cont)).toBe(cont);
+    // Underscore-free solid words are too risky and stay.
+    const bare = ['NDEBUG', 'int z;', ''].join('\n');
+    expect(blankLoneMacroLines(bare)).toBe(bare);
+  });
+
+  it('blankCStatementMacroCalls blanks indented iterator macros, keeps the block', async () => {
+    const { blankCStatementMacroCalls } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'static void walk(struct list *head) {',
+      '\tlist_for_each_entry(pos, head, member) {',
+      '\t\tuse(pos);',
+      '\t}',
+      '}',
+      '',
+    ].join('\n');
+    const out = blankCStatementMacroCalls(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('list_for_each_entry');
+    expect(out).toContain('use(pos);');
+    // A real call statement ends with `;` — untouched.
+    expect(out).toContain('use(pos);');
+    const call = ['void f(void) {', '\tdo_thing(a, b);', '}', ''].join('\n');
+    expect(blankCStatementMacroCalls(call)).toBe(call);
+    // Column-0 `name(args) {` is an implicit-int function definition — untouched.
+    const kandr = ['main(argc, argv)', '{', '\treturn 0;', '}', ''].join('\n');
+    expect(blankCStatementMacroCalls(kandr)).toBe(kandr);
+    // Control-flow keywords are never macros.
+    const ctrl = ['void g(int x) {', '\twhile (x) {', '\t\tx--;', '\t}', '}', ''].join('\n');
+    expect(blankCStatementMacroCalls(ctrl)).toBe(ctrl);
+  });
+
+  it('blankCTrailingParamAttrMacros blanks `name UNUSED` params, spares call args', async () => {
+    const { blankCTrailingParamAttrMacros } = await import('../src/extraction/languages/c-cpp');
+    const src = 'static int run(int argc UNUSED, const char **argv UNUSED)\n{\n\treturn 0;\n}\n';
+    const out = blankCTrailingParamAttrMacros(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('UNUSED');
+    expect(out).toContain('int argc ');
+    // A macro CONSTANT as a call argument is preceded by `,`/`(`, never by a
+    // bare identifier — untouched.
+    const call = 'void f(void) {\n\tconnect(sock, DEFAULT_TIMEOUT);\n}\n';
+    expect(blankCTrailingParamAttrMacros(call)).toBe(call);
+  });
+
+  it('blankCKernelAnnotations blanks sparse/section dunders, spares parameterized ones and real types', async () => {
+    const { blankCKernelAnnotations } = await import('../src/extraction/languages/c-cpp');
+    const src = [
+      'static int __init audit_init(void) { return 0; }',
+      'void copy(void __user *dst, const char *src);',
+      '__bpf_kfunc void bpf_iter_destroy(struct bpf_iter_num *it);',
+      '__printf(1, 2) void log_fmt(const char *fmt, ...);',
+      'struct e *entry = container_of(r, struct audit_entry, rule);',
+      '__u32 count = 0;',
+      '',
+    ].join('\n');
+    const out = blankCKernelAnnotations(src);
+    expect(out.length).toBe(src.length);
+    expect(out).not.toContain('__init');
+    expect(out).not.toContain('__user');
+    expect(out).not.toContain('__bpf_kfunc');
+    // Parameterized annotations keep their name — blanking it would strand
+    // the argument list as a floating parenthesis.
+    expect(out).toContain('__printf(1, 2)');
+    // container_of's type-keyword argument blanks; other `struct` keywords stay.
+    expect(out).toContain('container_of(r,        audit_entry, rule)');
+    expect(out).toContain('struct e *entry');
+    // Real dunder TYPES are not annotations.
+    expect(out).toContain('__u32 count');
+  });
+
+  it('restoreDirectiveLines keeps #define lines out of the blanking blast radius', async () => {
+    const { extractFromSource } = await import('../src/extraction');
+    // FMT_API matches the _API-suffix member blank; without the directive
+    // restore the #define loses its NAME and the file gains a parse error.
+    const src = [
+      '#define FMT_API FMT_VISIBILITY("default")',
+      'class Widget {',
+      ' public:',
+      '  int size() const { return 1; }',
+      '};',
+      '',
+    ].join('\n');
+    const result = extractFromSource('lib.hpp', src, 'cpp');
+    expect(result.errors).toEqual([]);
+    expect(result.nodes.some((n) => n.kind === 'class' && n.name === 'Widget')).toBe(true);
+    expect(result.nodes.some((n) => n.kind === 'method' && n.name === 'size')).toBe(true);
+  });
+});

+ 84 - 0
__tests__/fixtures/kernel-parity/torture.c

@@ -0,0 +1,84 @@
+/* Torture fixture for the C kernel walker (R7a) — exercises every c-path
+ * branch of the checklist: fn-ptr tables, typedef enum/struct, file-scope
+ * consts incl. multi-declarator, the macro-prototype misparse skip,
+ * value-refs (+ shadow prune), the leading-attr-macro preParse blank, and
+ * the C call shapes. Must parse ERROR-FREE post-preParse or the kernel arm
+ * defers. */
+#include <stdio.h>
+#include <sys/socket.h>
+#include "local_ops.h"
+
+/** Retry budget for the poller. */
+static const int MAX_RETRIES = 3;
+static const int LOW_WATER = 2, HIGH_WATER = 8;
+static int counter = 0;
+static const char *BANNER = "torture";
+
+/* Bare identifier declarators are the macro-prototype misparse shape and are
+ * skipped by design (uninit scalars are the accepted loss). */
+int bare_global;
+MYLIB_API config_handle;
+
+/* Leading attribute macro — blanked by preParseCSource (#1211), so the real
+ * name survives on both arms. */
+SEC_ATTR UINT32 masked_entry(VOID) { return 0; }
+
+typedef enum { STATE_IDLE, STATE_RUNNING, STATE_DONE } run_state_t;
+
+typedef struct {
+  int fd;
+  void (*on_recv)(int);
+} conn_t;
+
+typedef struct conn_pool conn_pool_t;
+
+typedef int (*cb_t)(int);
+
+enum wire_flags { WIRE_A = 1, WIRE_B = 2 };
+
+struct packet {
+  int len;
+  unsigned char body[64];
+  struct packet *next;
+  cb_t *async_cb;
+};
+
+/** Sum helper (docstring). */
+static int add(int a, int b) { return a + b; }
+
+static int cb_a(int x) { return add(x, 1); }
+static int cb_b(int x) { return add(x, 2); }
+
+/* fn-ptr table at file scope — the ungated 'list' capture positions. */
+static cb_t DISPATCH_TABLE[] = { cb_a, cb_b };
+
+static void handle_recv(int fd);
+
+/* struct initializer — the ungated 'value' capture positions. */
+static const struct handler_ops OPS = { .recv = handle_recv, .flags = WIRE_A };
+
+static void handle_recv(int fd) {
+  struct packet pkt;
+  pkt.len = fd;
+  printf("fd=%d retries=%d\n", fd, MAX_RETRIES);
+}
+
+/* Local shadow of a file-scope const — the shadow prune drops HIGH_WATER as a
+ * value-ref target while LOW_WATER stays live. */
+static int shadowed_reader(void) {
+  int HIGH_WATER = 99;
+  return HIGH_WATER + LOW_WATER;
+}
+
+static int use_table(int idx, int v) {
+  cb_t fn = DISPATCH_TABLE[idx];
+  int r = (*fn)(v);
+  conn_t c = { 1, 0 };
+  c.on_recv(r);
+  return counter + r;
+}
+
+static void spawn_workers(void) {
+  register_handler(cb_a);
+  signal_connect(&cb_b);
+}

+ 140 - 0
__tests__/fixtures/kernel-parity/torture.cpp

@@ -0,0 +1,140 @@
+/// Torture fixture for the C++ kernel walker (R7a) — namespaces (incl. C++17
+/// nested + anonymous), out-of-line Cls::method defs, templates + template
+/// bases, operator definitions, stack construction, local fn-ptrs, UE-macro
+/// shapes THROUGH the hoisted preParse, using-aliases, access specifiers,
+/// static-member value reads, and the cpp call shapes. Must parse ERROR-FREE
+/// post-preParse or the kernel arm defers (spaced operator CALL SITES live in
+/// torture-defer.cpp — they produce ERROR nodes by design).
+#include <vector>
+#include "widget_base.hpp"
+
+namespace app {
+
+/** Engine config (docstring). */
+class Config {
+public:
+  int retries;
+  void apply();
+  int helper_count() const { return 2; }
+
+private:
+  int secret;
+};
+
+void Config::apply() { retries = helper_count(); }
+
+namespace detail {
+struct Counter {
+  int value;
+  Counter *next;
+};
+}  // namespace detail
+
+int detail_probe() { return 1; }
+
+}  // namespace app
+
+namespace app::net {
+class Session {
+public:
+  void open();
+  virtual ~Session() {}
+};
+void Session::open() {}
+}  // namespace app::net
+
+namespace {
+int hidden_helper() { return 3; }
+}  // namespace
+
+template <typename T>
+class Base {
+public:
+  T item;
+};
+
+template <typename T>
+class Box : public Base<T> {
+public:
+  T get() const { return value_; }
+  T unwrap();
+
+private:
+  T value_;
+};
+
+template <typename T>
+T Box<T>::unwrap() {
+  return value_;
+}
+
+class Derived : public Base<int>, private app::Config {
+public:
+  Derived() : total_(0) {}
+  int total() const { return total_; }
+
+private:
+  int total_;
+};
+
+struct Vec2 {
+  float x, y;
+  Vec2 operator+(const Vec2 &o) const { return {x + o.x, y + o.y}; }
+  explicit operator bool() const { return x != 0 || y != 0; }
+  Vec2 origin();
+};
+
+enum class Mode : unsigned char { Off, On };
+enum Legacy { LEGACY_A, LEGACY_B };
+typedef struct {
+  int id;
+} packet_t;
+using Handle = app::Config;
+
+// UE-macro shapes — every one below is recovered by the hoisted preParse
+// (export macro, reflection markup, inline specifier, API member prefix).
+class MYMODULE_API Widget : public app::Config {
+public:
+  UPROPERTY(EditAnywhere, Category = "State")
+  float Health;
+  FORCEINLINE float GetHealth() const { return Health; }
+  ENGINE_API virtual void Tick(float Delta);
+};
+
+void Widget::Tick(float Delta) { Health += Delta; }
+
+Config GlobalConfig;
+int build_number = 7;
+
+template <typename T>
+T compute_seed(T v) {
+  return v + 1;
+}
+
+float drive_helper(float v) { return v; }
+
+Widget *make_widget() { return new Widget(); }
+
+float drive() {
+  Widget local;
+  app::Config cfg;
+  Vec2 a{1, 2};
+  Vec2 b(a);
+  Vec2 c2(1.5f, 2.5f);
+  float f = a.x + b.y + c2.x;
+  make_widget()->Tick(0.5f);
+  auto kernel = &compute_seed<float>;
+  if (f > 1) {
+    kernel = &drive_helper;
+  }
+  float r = kernel(f);
+  int flags = GlobalConfig.retries;
+  Mode m = Mode::Off;
+  int leg = LEGACY_A;
+  app::detail_probe();
+  compute_seed<int>(2);
+  auto mp = &app::Config::apply;
+  (void)mp;
+  (void)m;
+  return r + f + flags + leg;
+}

+ 31 - 0
__tests__/fixtures/kernel-parity/torture.hpp

@@ -0,0 +1,31 @@
+// Torture header for the C++ kernel walker (R7a) — include guard, forward
+// declarations (skipped, #1093), extern "C" prototypes, header templates,
+// and a UE-reflection-shaped class recovered by the hoisted preParse.
+#ifndef TORTURE_HPP
+#define TORTURE_HPP
+
+class Forward;
+struct Opaque;
+
+extern "C" {
+int c_bridge(int value);
+}
+
+/// Reusable clamp helper.
+template <typename T>
+T clamp_value(T v, T lo, T hi) {
+  return v < lo ? lo : (v > hi ? hi : v);
+}
+
+class MYLIB_API Meter : public Forward {
+public:
+  UPROPERTY(BlueprintReadOnly)
+  int Reading;
+  FORCEINLINE int Peek() const { return Reading; }
+  void Calibrate(int target);
+  Forward *owner();
+};
+
+inline void Meter::Calibrate(int target) { Reading = clamp_value(target, 0, 100); }
+
+#endif

+ 182 - 0
__tests__/kernel-ccpp-parity.test.ts

@@ -0,0 +1,182 @@
+/**
+ * Kernel↔wasm C/C++ extraction parity (R7a of the kernel migration).
+ *
+ * Asserts the native walker (codegraph-kernel/src/ccpp/) produces the SAME
+ * ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
+ * unresolved refs compared as canonicalized multisets — over:
+ *   - the checked-in torture fixtures (torture.c / torture.cpp / torture.hpp:
+ *     fn-ptr tables, typedef enum/struct, multi-declarator consts, namespaces
+ *     incl. C++17 nested, out-of-line Cls::method defs, templates + template
+ *     bases, operators, stack construction, local fn-ptrs, UE-macro shapes
+ *     through the hoisted preParse, using-aliases, value-ref shadowing), and
+ *   - Metal/CUDA-shaped sources arriving as language 'cpp' — pinning that the
+ *     route point applies the SAME extension/content-gated preParse blanks to
+ *     the kernel arm (docs/design/ccpp-kernel-port-checklist.md, decision 1/2).
+ *
+ * Files with parse errors — including the spaced explicit-operator CALL-SITE
+ * shape (#1247), which rides an ERROR node — must DEFER to wasm (`defer:`),
+ * asserted below. The full-repo sweep lives in scripts/kernel-parity.mjs
+ * (redis/git/fmt et al., run for the §5 gate); this suite keeps the invariant
+ * alive in `npm test`. Skips when no kernel binary is staged;
+ * CODEGRAPH_KERNEL_EXPECT=1 turns that into a failure (kernel-scaffold.test.ts).
+ */
+
+import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
+import * as fs from 'fs';
+import * as path from 'path';
+import { extractFromSource } from '../src/extraction';
+import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
+import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
+import type { ExtractionResult, Language } from '../src/types';
+
+const KERNEL_PATH = path.join(
+  __dirname,
+  '..',
+  'codegraph-kernel',
+  'prebuilds',
+  `${process.platform}-${process.arch}`,
+  'codegraph-kernel.node'
+);
+const kernelBuilt = fs.existsSync(KERNEL_PATH);
+
+const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'kernel-parity');
+
+function canon(result: ExtractionResult): { nodes: string[]; edges: string[]; refs: string[] } {
+  return {
+    nodes: result.nodes
+      .map(({ updatedAt: _u, ...n }) => JSON.stringify(n, Object.keys(n).sort()))
+      .sort(),
+    edges: result.edges.map((e) => JSON.stringify(e, Object.keys(e).sort())).sort(),
+    refs: result.unresolvedReferences
+      .map((r) => JSON.stringify(r, Object.keys(r).sort()))
+      .sort(),
+  };
+}
+
+const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
+let savedEnv: Record<string, string | undefined>;
+
+describe.skipIf(!kernelBuilt)('kernel C/C++ extraction parity', () => {
+  beforeAll(async () => {
+    await initGrammars();
+    await loadGrammarsForLanguages(['c', 'cpp']);
+  });
+
+  beforeEach(() => {
+    savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
+    resetKernelForTests();
+  });
+
+  afterEach(() => {
+    for (const k of ENV_KEYS) {
+      if (savedEnv[k] === undefined) delete process.env[k];
+      else process.env[k] = savedEnv[k];
+    }
+    resetKernelForTests();
+  });
+
+  function assertParity(filePath: string, source: string, language: Language, minNodes = 3): void {
+    process.env.CODEGRAPH_KERNEL_LANGS = 'all';
+    delete process.env.CODEGRAPH_KERNEL;
+    const viaKernel = tryKernelExtract(filePath, source, language);
+    expect(viaKernel, `kernel extraction failed for ${filePath}`).not.toBeNull();
+
+    process.env.CODEGRAPH_KERNEL = '0';
+    const viaWasm = extractFromSource(filePath, source, language);
+    delete process.env.CODEGRAPH_KERNEL;
+
+    const k = canon(viaKernel!);
+    const w = canon(viaWasm);
+    expect(k.nodes, `${filePath}: nodes`).toEqual(w.nodes);
+    expect(k.edges, `${filePath}: edges`).toEqual(w.edges);
+    expect(k.refs, `${filePath}: refs`).toEqual(w.refs);
+    // Meaningful comparison, not empty-vs-empty (the inline Metal/CUDA
+    // sources are deliberately small — they pass their exact node count).
+    expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
+  }
+
+  it('torture fixture (c): fn-ptr tables, typedefs, file-scope consts, value-refs', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.c');
+    assertParity('fixtures/torture.c', fs.readFileSync(file, 'utf8'), 'c');
+  });
+
+  it('torture fixture (cpp): namespaces, out-of-line methods, templates, fn-ptrs, UE macros', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.cpp');
+    assertParity('fixtures/torture.cpp', fs.readFileSync(file, 'utf8'), 'cpp');
+  });
+
+  it('torture fixture (hpp): fwd decls, extern "C", header templates, reflection markup', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.hpp');
+    assertParity('fixtures/torture.hpp', fs.readFileSync(file, 'utf8'), 'cpp');
+  });
+
+  // Metal rides the cpp route: `.metal` maps to language 'cpp' and the
+  // extension-gated `[[attribute]]` blank must reach the kernel arm through
+  // the route-point preParse hoist (filePath rides along for the gate).
+  it('metal-shaped source (.metal → cpp): attribute blanks applied on both arms', () => {
+    const metal = [
+      'struct VertexIn {',
+      '  float3 position [[attribute(0)]];',
+      '  float2 uv [[attribute(1)]];',
+      '};',
+      'static float2 scale_uv(float2 uv) { return uv; }',
+      '',
+    ].join('\n');
+    assertParity('fixtures/shader.metal', metal, 'cpp');
+  });
+
+  // CUDA rides the cpp route too: specifier + launch-config blanks are gated
+  // by extension OR content, and both fire before the kernel call.
+  it('cuda-shaped source (.cu → cpp): specifier + launch blanks applied on both arms', () => {
+    const cuda = [
+      '__global__ void step_kernel(float *data) { data[0] += 1.0f; }',
+      'void launch(float *data) { step_kernel<<<1, 256>>>(data); }',
+      '',
+    ].join('\n');
+    assertParity('fixtures/kern.cu', cuda, 'cpp');
+  });
+
+  // Every torture fixture again with CRLF line endings — the shape every
+  // Windows autocrlf checkout has. Derived in memory (not a checked-in CRLF
+  // file) so no platform or editor can silently normalize it away. Pins the
+  // JS-multiline-^ docstring semantics for the C comment markers (#1329).
+  it.each([
+    ['torture.c', 'c'],
+    ['torture.cpp', 'cpp'],
+    ['torture.hpp', 'cpp'],
+  ] as const)('torture fixture CRLF parity: %s', (name, lang) => {
+    const file = path.join(FIXTURE_DIR, name);
+    const crlf = fs.readFileSync(file, 'utf8').replace(/(?<!\r)\n/g, '\r\n');
+    assertParity(`fixtures/${name} (crlf)`, crlf, lang);
+  });
+
+  it('spaced explicit-operator call sites defer to the wasm extractor (#1247 rides an ERROR node)', () => {
+    const source = [
+      'struct It { int operator*() const { return 1; } };',
+      'int read_it(const It &it) { return it.operator *(); }',
+      '',
+    ].join('\n');
+    process.env.CODEGRAPH_KERNEL_LANGS = 'all';
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(tryKernelExtract('src/op.cpp', source, 'cpp')).toBeNull();
+    // The seam still serves the file — through the wasm path, where the
+    // operator-call recovery emits the `it.operator*` ref.
+    process.env.CODEGRAPH_KERNEL = '0';
+    const viaWasm = extractFromSource('src/op.cpp', source, 'cpp');
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(
+      viaWasm.unresolvedReferences.some((r) => r.referenceName === 'it.operator*')
+    ).toBe(true);
+  });
+
+  it('files with parse errors defer to the wasm extractor (recovery is encoding-dependent)', () => {
+    const broken = 'void f( {\n  return }} 12 (\n';
+    process.env.CODEGRAPH_KERNEL_LANGS = 'all';
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(tryKernelExtract('src/broken.c', broken, 'c')).toBeNull();
+    process.env.CODEGRAPH_KERNEL = '0';
+    const viaWasm = extractFromSource('src/broken.c', broken, 'c');
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(viaWasm.nodes.some((n) => n.kind === 'file')).toBe(true);
+  });
+});

+ 1 - 1
__tests__/kernel-grammar-parity.test.ts

@@ -36,7 +36,7 @@ const kernelBuilt = fs.existsSync(KERNEL_PATH);
 
 // Every kernel-capable language. `jsx` shares the javascript grammar on BOTH
 // paths (langs.rs mirrors WASM_GRAMMAR_FILES), so the distinct grammars are:
-const GRAMMAR_LANGUAGES: Language[] = ['typescript', 'tsx', 'javascript', 'java', 'python', 'go'];
+const GRAMMAR_LANGUAGES: Language[] = ['typescript', 'tsx', 'javascript', 'java', 'python', 'go', 'c', 'cpp'];
 
 describe.skipIf(!kernelBuilt)('kernel↔wasm grammar parity', () => {
   beforeAll(async () => {

+ 2 - 0
codegraph-kernel/.gitignore

@@ -1,3 +1,5 @@
 target/
+# Cross-compile scratch dirs (e.g. target-linux for the cg1212 envelope runs)
+target-*/
 prebuilds/
 *.node

+ 22 - 0
codegraph-kernel/Cargo.lock

@@ -52,6 +52,8 @@ dependencies = [
  "regex",
  "sha2",
  "tree-sitter",
+ "tree-sitter-c",
+ "tree-sitter-cpp",
  "tree-sitter-go",
  "tree-sitter-java",
  "tree-sitter-javascript",
@@ -482,6 +484,26 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "tree-sitter-c"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b2eb57a55fed6b00812912e730b7a275cf4fe98bfd6a5d76263d4438371728"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
+[[package]]
+name = "tree-sitter-cpp"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2196ea9d47b4ab4a31b9297eaa5a5d19a0b121dceb9f118f6790ad0ab94743"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "tree-sitter-go"
 version = "0.23.4"

+ 5 - 0
codegraph-kernel/Cargo.toml

@@ -25,6 +25,11 @@ tree-sitter-javascript = "0.25"
 tree-sitter-java = "0.23"
 tree-sitter-python = "0.23"
 tree-sitter-go = "0.23"
+# Pinned exact: the vendored wasm (src/extraction/wasm/) was built from these
+# tags' checked-in parser.c, sha-matched against these registry tarballs
+# (R7a prep, #1345). A patch bump here without re-vendoring breaks the match.
+tree-sitter-c = "=0.24.2"
+tree-sitter-cpp = "=0.23.4"
 
 [build-dependencies]
 napi-build = "2"

+ 1990 - 0
codegraph-kernel/src/ccpp/mod.rs

@@ -0,0 +1,1990 @@
+//! C / C++ extraction — a faithful Rust port of `TreeSitterExtractor`'s c/cpp
+//! paths (src/extraction/tree-sitter.ts) plus languages/c-cpp.ts, one dual-
+//! language module flagged like tsjs/ (checklist:
+//! docs/design/ccpp-kernel-port-checklist.md — read it before editing).
+//!
+//! The seven preParse blanking passes are NOT here: the TS route point
+//! (src/extraction/kernel/index.ts) applies `extractor.preParse` before the
+//! kernel call, so this walker receives the SAME blanked bytes the wasm
+//! extractor parses (all blanks are equal-length-space replacements — every
+//! offset survives). `.metal`/`.cu`/`.cuh` arrive as language 'cpp' with their
+//! dialect blanks already applied.
+//!
+//! Quirks mirrored bug-for-bug (each pinned by the parity gates):
+//!  - cpp namespace prefix stack (#1291): named `namespace a::b {` pushes the
+//!    name AS WRITTEN onto the qualifiedName prefix; anonymous falls through.
+//!    No namespace NODE is minted (#1093 crowd-out).
+//!  - out-of-line `Cls::method` defs: name = LAST `::` segment of the
+//!    declarator's qualified_identifier (BFS that skips parameter_list +
+//!    trailing_return_type), receiver = the template-stripped qualifier,
+//!    qualifiedName composed against the namespace prefix with the re-spelled-
+//!    prefix anchor rule; owner `contains` edge to the FIRST earlier
+//!    struct/class/enum/trait of the receiver's bare name.
+//!  - macro-name salvage: recoverCppMacroDefinedName (ALL-CAPS macro def whose
+//!    real name is the lone first argument) at resolveName, and
+//!    recoverMangledCppName (glued "Ret name" → last token) as the universal
+//!    post-hoc net for BOTH c and cpp.
+//!  - `class MACRO Name` misparse residue: isMisparsedFunction drops the
+//!    phantom function (name starts `namespace`, C++ keywords, or the bodyless
+//!    class/struct `type` + non-function_declarator shape, #946/#1061) but
+//!    still walks the body.
+//!  - C file-scope variables: init/pointer/array declarators only — a BARE
+//!    identifier declarator is the macro-prototype misparse and is skipped
+//!    (loses uninit scalars by design); cpp declarations instead take the TS
+//!    GENERIC fallback (direct identifier children only → `int x;` extracts,
+//!    `int x = 5;` does not — bug-for-bug).
+//!  - inheritance quirk: extractInheritance recurses into
+//!    field_declaration_list, where a field_declaration with no DIRECT
+//!    field_identifier child (pointer/array/method members) but a direct
+//!    type_identifier emits an `extends` ref to that type (the Go-embedding
+//!    branch matching c/cpp shapes). Kept: the parity gate pins today's graph.
+//!  - static-member/value-read pass (cpp only): `field_expression` is in
+//!    MEMBER_ACCESS_TYPES (listed for Scala, same node kind in cpp), so
+//!    `Capitalized.member` / `Capitalized->member` VALUE reads emit
+//!    `references` refs; qualified_identifier is checked too but its scope
+//!    child is namespace_identifier/template_type/…, never a plain
+//!    identifier, so it can't emit.
+//!  - explicit operator calls (#1247) ride an ERROR child — but has_error()
+//!    defers the whole file to wasm, so the ported branch is a faithful no-op
+//!    here; kept so an error-free shape (if a grammar bump ever produces one)
+//!    stays parity-true.
+//!  - local fn-pointer fan-out (#932-adjacent): `auto k = &fn<…>;` records
+//!    per-caller targets (insertion-ordered, branch reassignments accumulate);
+//!    a later bare `k(args)` emits one `calls` ref PER target and suppresses
+//!    the local name. Template args stripped like base-class refs (#1043).
+//!  - stack construction (#1035): cpp `declaration` with class-like named
+//!    `type` and an init_declarator whose value is argument_list /
+//!    initializer_list → `instantiates` (most-vexing-parse excluded).
+//!  - value-reference edges: C only (VALUE_REF_LANGS has 'c', not 'cpp') —
+//!    shadow prune via init_declarator counts, MAX_VALUE_REF_NODES cap,
+//!    CODEGRAPH_VALUE_REFS=0 kill switch.
+//!  - fn-ref capture (#756): cFamilySpec for both; cpp adds addressOfOnly
+//!    (bare identifiers only qualify in file-scope value/list positions).
+//!
+//! Files with parse errors defer to wasm (`defer:`) — error recovery is
+//! encoding-dependent and the wasm recovery is canonical.
+
+use crate::buffers::{
+    build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
+    RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
+};
+use crate::docstring::preceding_docstring;
+use crate::ids;
+use crate::textutil as util;
+use regex::Regex;
+use std::collections::{HashMap, HashSet, VecDeque};
+use std::sync::OnceLock;
+use tree_sitter::{Node, Parser};
+
+const MAX_VALUE_REF_NODES: usize = 20_000;
+
+// --- compiled regexes (JS \w/\s spelled as ASCII classes for parity) ---------
+
+/// recoverCppMacroDefinedName: macro-shaped parsed name.
+fn macro_shaped_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$").unwrap())
+}
+fn has_lower_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"[a-z]").unwrap())
+}
+/// normalizeCppReturnType: smart-pointer/optional unwrap.
+fn ret_wrapper_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| {
+        Regex::new(r"\b(?:std\s*::\s*)?(?:unique_ptr|shared_ptr|weak_ptr|optional)\s*<\s*([^,>]+?)\s*>")
+            .unwrap()
+    })
+}
+fn ret_keyword_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"\b(?:const|volatile|typename|struct|class|enum)\b").unwrap())
+}
+fn angle_group_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
+}
+fn ptr_ref_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"[*&]+").unwrap())
+}
+fn ws_run_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"\s+").unwrap())
+}
+fn simple_ident_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
+}
+/// recoverMangledCppName's `Ret (name)` idiom guard.
+fn ret_paren_name_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^\S+\s+\([A-Za-z_][A-Za-z0-9_]*\)").unwrap())
+}
+/// Operator-call receiver: simple identifier / dotted member chain.
+fn operator_receiver_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_.]*$").unwrap())
+}
+/// Symbolic operator tail (`/^[^\w\s]/` in JS).
+fn symbolic_op_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[^A-Za-z0-9_\s]").unwrap())
+}
+/// extractStaticMemberRef's capitalized-receiver test.
+fn capitalized_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Z][A-Za-z0-9_]*$").unwrap())
+}
+/// normalizeValue's qualified `&Cls::m` member-pointer test (`/^[A-Za-z_][\w:]*$/`).
+fn qualified_ref_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_:]*$").unwrap())
+}
+
+/// CPP_NON_CLASS_RETURN (languages/c-cpp.ts).
+fn is_non_class_return(name: &str) -> bool {
+    matches!(
+        name,
+        "void" | "bool" | "char" | "short" | "int" | "long" | "float" | "double" | "unsigned"
+            | "signed" | "size_t" | "ssize_t" | "auto" | "wchar_t" | "char8_t" | "char16_t"
+            | "char32_t" | "int8_t" | "int16_t" | "int32_t" | "int64_t" | "uint8_t" | "uint16_t"
+            | "uint32_t" | "uint64_t" | "intptr_t" | "uintptr_t" | "nullptr_t"
+    )
+}
+
+/// CPP_PRIMITIVE_NAMES (languages/c-cpp.ts) — recoverMangledCppName's guard.
+fn is_cpp_primitive_name(name: &str) -> bool {
+    matches!(
+        name,
+        "bool" | "void" | "int" | "char" | "short" | "long" | "float" | "double" | "unsigned"
+            | "signed" | "wchar_t" | "char8_t" | "char16_t" | "char32_t" | "char_t" | "size_t"
+            | "auto" | "const" | "struct" | "class" | "enum" | "union" | "typename"
+    )
+}
+
+/// NAME_STOPLIST (function-ref.ts).
+fn is_stoplisted(name: &str) -> bool {
+    matches!(
+        name,
+        "this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
+            | "NULL" | "nullptr" | "None"
+    )
+}
+
+/// LITERAL_RECEIVER_TYPES (tree-sitter.ts) — full set; membership is what the
+/// TS code tests even though only a few kinds occur in the c/cpp grammars.
+fn is_literal_receiver(kind: &str) -> bool {
+    matches!(
+        kind,
+        "string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal"
+            | "template_string" | "concatenated_string" | "formatted_string" | "f_string"
+            | "line_string_literal" | "string_content" | "heredoc_body"
+            | "number" | "number_literal" | "integer" | "integer_literal" | "float"
+            | "float_literal" | "int_literal" | "decimal_integer_literal" | "real_literal"
+            | "char_literal" | "character_literal" | "rune_literal" | "regex" | "regex_literal"
+            | "true" | "false" | "boolean_literal" | "bool_literal" | "none" | "null" | "nil"
+            | "null_literal" | "undefined"
+            | "list" | "list_literal" | "array" | "array_literal" | "array_creation_expression"
+            | "dictionary" | "dict_literal" | "object" | "tuple" | "set"
+    )
+}
+
+/// stripCppTemplateArgs (languages/c-cpp.ts): depth-counted removal of every
+/// balanced `<…>` group; `<` and `>` never reach the output.
+fn strip_cpp_template_args(name: &str) -> String {
+    if !name.contains('<') {
+        return name.to_string();
+    }
+    let mut out = String::with_capacity(name.len());
+    let mut depth = 0u32;
+    for ch in name.chars() {
+        if ch == '<' {
+            depth += 1;
+        } else if ch == '>' {
+            depth = depth.saturating_sub(1);
+        } else if depth == 0 {
+            out.push(ch);
+        }
+    }
+    out.trim().to_string()
+}
+
+/// recoverMangledCppName (languages/c-cpp.ts) — universal post-hoc salvage for
+/// a name still mangled by an unblanked macro ("Ret name" → "name").
+fn recover_mangled_cpp_name(name: String) -> String {
+    if !name.chars().any(|c| c.is_whitespace())
+        || name.starts_with("operator")
+        || name.starts_with('~')
+    {
+        return name;
+    }
+    if ret_paren_name_re().is_match(&name) {
+        return name; // `Ret (name)` idiom — leave alone
+    }
+    let before_params = match name.find('(') {
+        Some(i) => &name[..i],
+        None => &name[..],
+    };
+    // (JS: `beforeParams.trim().split(/\s+/)` — split_whitespace already
+    // ignores leading/trailing whitespace, so no explicit trim.)
+    let candidate = before_params.split_whitespace().last().unwrap_or("");
+    if candidate.is_empty()
+        || !simple_ident_re().is_match(candidate)
+        || is_cpp_primitive_name(candidate)
+    {
+        return name;
+    }
+    candidate.to_string()
+}
+
+/// normalizeCppReturnType (languages/c-cpp.ts).
+fn normalize_cpp_return_type(raw: &str) -> Option<String> {
+    let mut t = raw.trim().to_string();
+    if t.is_empty() {
+        return None;
+    }
+    if let Some(c) = ret_wrapper_re().captures(&t) {
+        if let Some(inner) = c.get(1) {
+            t = inner.as_str().to_string();
+        }
+    }
+    let t = ret_keyword_re().replace_all(&t, " ");
+    let t = angle_group_re().replace_all(&t, " ");
+    let t = ptr_ref_re().replace_all(&t, " ");
+    let t = ws_run_re().replace_all(&t, " ");
+    let t = t.trim();
+    if t.is_empty() {
+        return None;
+    }
+    let parts: Vec<&str> = t.split("::").filter(|p| !p.is_empty()).collect();
+    let last = *parts.last()?;
+    if is_non_class_return(last) || !simple_ident_re().is_match(last) {
+        return None;
+    }
+    Some(last.to_string())
+}
+
+/// JS `String.replace(/->/g,'.').replace(/\s+/g,'')` used on receivers.
+fn arrow_dot_no_ws(s: &str) -> String {
+    s.replace("->", ".").chars().filter(|c| !c.is_whitespace()).collect()
+}
+
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum Variant {
+    C,
+    Cpp,
+}
+
+struct Scope {
+    row: u32,
+    kind: &'static str,
+    name: String,
+}
+
+#[derive(Default)]
+struct Extra {
+    docstring: Option<String>,
+    signature: Option<String>,
+    visibility: Option<u8>,
+    is_exported: Option<bool>,
+    return_type: Option<String>,
+    qualified_name: Option<String>,
+}
+
+struct ValueScope<'t> {
+    row: u32,
+    node: Node<'t>,
+    name: String,
+}
+
+/// Capture mode for a fn-ref candidate (gate policy keys on it).
+#[derive(Clone, Copy, PartialEq, Eq)]
+enum Mode {
+    Args,
+    Rhs,
+    Value,
+    List,
+    Varinit,
+}
+
+struct Cand {
+    from: u32,
+    name: String,
+    mode: Mode,
+    explicit_ref: bool,
+    line: u32,
+    column_byte: usize,
+    row: usize,
+}
+
+/// Per-node metadata for the receiver-method owner lookup (mirrors the TS
+/// side's scan over `this.nodes` — FIRST match wins, earlier-in-file only).
+struct NodeMeta {
+    kind: &'static str,
+    name: String,
+}
+
+pub struct Walker<'t> {
+    src: &'t str,
+    file_path: &'t str,
+    variant: Variant,
+    line_starts: Vec<usize>,
+    arena: Arena,
+    tables: Tables,
+    stack: Vec<Scope>,
+    nodes_meta: Vec<NodeMeta>,
+    node_ids: Vec<String>,
+    /// C/C++ enclosing `namespace ns { … }` names (cpp only ever non-empty).
+    namespace_prefix: Vec<String>,
+    /// cppLocalFnPtrs: caller row → local name → insertion-ordered targets.
+    local_fn_ptrs: HashMap<u32, HashMap<String, Vec<String>>>,
+    defined_fn_names: HashSet<String>,
+    imported_names: HashSet<String>,
+    fn_ref_cands: Vec<Cand>,
+    fs_values: HashMap<String, u32>,
+    fs_value_counts: HashMap<String, u32>,
+    value_scopes: Vec<ValueScope<'t>>,
+}
+
+pub fn extract(file_path: &str, source: &str, language: &str) -> Result<EmitOut, String> {
+    let variant = match language {
+        "c" => Variant::C,
+        "cpp" => Variant::Cpp,
+        other => return Err(format!("ccpp walker got language '{other}'")),
+    };
+    let grammar = crate::langs::grammar_for(language).ok_or("no c/cpp grammar")?;
+    let t0 = std::time::Instant::now();
+    let mut parser = Parser::new();
+    parser
+        .set_language(&grammar)
+        .map_err(|e| format!("set_language({language}) failed: {e}"))?;
+    let tree = parser
+        .parse(source, None)
+        .ok_or_else(|| "parser returned null tree".to_string())?;
+    // Measurement hatch (parity sweeps only — never set in production): skip
+    // the defer so the sweep can QUANTIFY how often UTF-8 vs UTF-16 error
+    // recovery actually diverges on this language's erroring files.
+    let no_defer = std::env::var("CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT").as_deref() == Ok("1");
+    if tree.root_node().has_error() && !no_defer {
+        return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
+    }
+
+    let mut w = Walker {
+        src: source,
+        file_path,
+        variant,
+        line_starts: util::line_starts(source),
+        arena: Arena::default(),
+        tables: Tables::default(),
+        stack: Vec::new(),
+        nodes_meta: Vec::new(),
+        node_ids: Vec::new(),
+        namespace_prefix: Vec::new(),
+        local_fn_ptrs: HashMap::new(),
+        defined_fn_names: HashSet::new(),
+        imported_names: HashSet::new(),
+        fn_ref_cands: Vec::new(),
+        fs_values: HashMap::new(),
+        fs_value_counts: HashMap::new(),
+        value_scopes: Vec::new(),
+    };
+
+    let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
+    let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
+    let mut flags = BoolFlags::default();
+    flags.set(FLAG_IS_EXPORTED, false);
+    let file_id = w.arena.put(&ids::file_node_id(file_path));
+    let name_ref = w.arena.put(base_name);
+    let qn_ref = w.arena.put(file_path);
+    w.tables.push_node(&NodeRow {
+        kind: node_kind_index("file").unwrap(),
+        visibility: 0,
+        flags,
+        start_line: 1,
+        end_line: line_count,
+        start_column: 0,
+        end_column: 0,
+        name: name_ref,
+        qualified_name: qn_ref,
+        id: file_id,
+        docstring: NONE_STR,
+        signature: NONE_STR,
+        decorators: NONE_STR,
+        type_parameters: NONE_STR,
+        return_type: NONE_STR,
+        extra_json: NONE_STR,
+    });
+    w.nodes_meta.push(NodeMeta { kind: "file", name: base_name.to_string() });
+    w.node_ids.push(ids::file_node_id(file_path));
+    w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
+
+    w.visit_node(tree.root_node());
+    w.flush_fn_ref_candidates();
+    w.flush_value_refs(tree.root_node());
+    w.stack.pop();
+
+    let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
+    let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
+    Ok(EmitOut {
+        meta,
+        nodes: w.tables.nodes,
+        edges: w.tables.edges,
+        refs: w.tables.refs,
+        arena: w.arena.into_vec(),
+    })
+}
+
+impl<'t> Walker<'t> {
+    fn text(&self, node: Node) -> &'t str {
+        &self.src[node.byte_range()]
+    }
+    fn line_of(&self, node: Node) -> u32 {
+        node.start_position().row as u32 + 1
+    }
+    fn col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
+    }
+    fn end_col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
+    }
+    fn top_row(&self) -> u32 {
+        self.stack.last().map(|s| s.row).unwrap_or(0)
+    }
+    fn inside_class_like(&self) -> bool {
+        self.stack
+            .last()
+            .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module"))
+            .unwrap_or(false)
+    }
+
+    fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
+        let name_ref = self.arena.put(name);
+        self.tables.push_ref(&RefRow {
+            from_idx: from_row,
+            kind: kind_code,
+            line: self.line_of(node),
+            column: self.col_of(node),
+            reference_name: name_ref,
+            candidates: NONE_STR,
+            from_id_str: NONE_STR,
+        });
+        if kind_code == edge_kind_index("imports").unwrap() {
+            if util::simple_name().is_match(name) {
+                self.imported_names.insert(name.to_string());
+            } else if let Some(c) = util::qualified_import().captures(name) {
+                self.imported_names.insert(c[1].to_string());
+            }
+        }
+    }
+
+    fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
+        if name.is_empty() {
+            return None;
+        }
+        let start_line = self.line_of(node);
+        let id = ids::node_id(self.file_path, kind, name, start_line);
+        // (c/cpp define no resolveBody hook, so createNode's endLine extension
+        // for sibling-body grammars never fires — endLine is the node's own.)
+        let end_line = node.end_position().row as u32 + 1;
+
+        let qualified = extra.qualified_name.unwrap_or_else(|| {
+            let mut parts: Vec<&str> = self.namespace_prefix.iter().map(|s| s.as_str()).collect();
+            for s in &self.stack {
+                if s.kind != "file" {
+                    parts.push(&s.name);
+                }
+            }
+            let mut qn = parts.join("::");
+            if !qn.is_empty() {
+                qn.push_str("::");
+            }
+            qn.push_str(name);
+            qn
+        });
+
+        let mut flags = BoolFlags::default();
+        if let Some(v) = extra.is_exported {
+            flags.set(FLAG_IS_EXPORTED, v);
+        }
+        let name_ref = self.arena.put(name);
+        let qn_ref = self.arena.put(&qualified);
+        let id_ref = self.arena.put(&id);
+        let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
+        let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
+        let ret_ref = opt_str(&mut self.arena, extra.return_type.as_deref());
+        let row = self.tables.push_node(&NodeRow {
+            kind: node_kind_index(kind).unwrap(),
+            visibility: extra.visibility.unwrap_or(0),
+            flags,
+            start_line,
+            end_line,
+            start_column: self.col_of(node),
+            end_column: self.end_col_of(node),
+            name: name_ref,
+            qualified_name: qn_ref,
+            id: id_ref,
+            docstring: doc_ref,
+            signature: sig_ref,
+            decorators: NONE_STR,
+            type_parameters: NONE_STR,
+            return_type: ret_ref,
+            extra_json: NONE_STR,
+        });
+        self.nodes_meta.push(NodeMeta { kind, name: name.to_string() });
+        self.node_ids.push(id);
+
+        let parent_row = self.top_row();
+        self.tables.push_edge(&EdgeRow {
+            source_idx: parent_row,
+            target_idx: row,
+            kind: edge_kind_index("contains").unwrap(),
+            provenance: 0,
+            line: NONE,
+            column: NONE,
+            metadata_json: NONE_STR,
+            source_id_str: NONE_STR,
+            target_id_str: NONE_STR,
+        });
+
+        if kind == "function" || kind == "method" {
+            self.defined_fn_names.insert(name.to_string());
+        }
+        // captureValueRefScope (capture is variant-agnostic like the TS side;
+        // flushValueRefs gates on the language — C only).
+        let target_kind_ok = kind == "constant" || kind == "variable";
+        if target_kind_ok
+            && util::utf16_len(name) >= 3
+            && util::has_upper_or_underscore().is_match(name)
+        {
+            let parent_ok = self
+                .stack
+                .last()
+                .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "enum"))
+                .unwrap_or(false);
+            if parent_ok {
+                self.fs_values.insert(name.to_string(), row);
+                *self.fs_value_counts.entry(name.to_string()).or_insert(0) += 1;
+            }
+        }
+        if matches!(kind, "function" | "method" | "constant" | "variable") {
+            self.value_scopes.push(ValueScope { row, node, name: name.to_string() });
+        }
+        Some(row)
+    }
+
+    // --- name extraction -----------------------------------------------------
+
+    /// extractName: extractNameRaw + the universal recoverMangledName net
+    /// (wired for BOTH c and cpp in languages/c-cpp.ts).
+    fn extract_name(&self, node: Node) -> String {
+        recover_mangled_cpp_name(self.extract_name_raw(node))
+    }
+
+    /// extractNameRaw for the c/cpp extractor configs (nameField 'declarator';
+    /// cpp resolveName = extractCppQualifiedMethodName).
+    fn extract_name_raw(&self, node: Node) -> String {
+        if self.variant == Variant::Cpp {
+            if let Some(hook) = self.extract_cpp_qualified_method_name(node) {
+                return hook;
+            }
+        }
+        if let Some(name_node) = node.child_by_field_name("declarator") {
+            let mut resolved = name_node;
+            // Unwrap pointer/reference declarators (`int* f()`, `T& f()`).
+            while matches!(resolved.kind(), "pointer_declarator" | "reference_declarator") {
+                let inner = resolved
+                    .child_by_field_name("declarator")
+                    .or_else(|| resolved.named_child(0));
+                match inner {
+                    Some(i) => resolved = i,
+                    None => break,
+                }
+            }
+            // C++ conversion operator: `operator <type>`.
+            if resolved.kind() == "operator_cast" {
+                return match resolved.named_child(0) {
+                    Some(t) => format!("operator {}", self.text(t).trim()),
+                    None => self.text(resolved).to_string(),
+                };
+            }
+            if resolved.kind() == "function_declarator" || resolved.kind() == "declarator" {
+                let inner = resolved
+                    .child_by_field_name("declarator")
+                    .or_else(|| resolved.named_child(0));
+                return match inner {
+                    Some(i) => self.text(i).to_string(),
+                    None => self.text(resolved).to_string(),
+                };
+            }
+            return self.text(resolved).to_string();
+        }
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                if matches!(c.kind(), "identifier" | "type_identifier" | "simple_identifier" | "constant") {
+                    return self.text(c).to_string();
+                }
+            }
+        }
+        "<anonymous>".to_string()
+    }
+
+    /// extractCppQualifiedMethodName (languages/c-cpp.ts:75).
+    fn extract_cpp_qualified_method_name(&self, node: Node) -> Option<String> {
+        if let Some(n) = self.recover_cpp_macro_defined_name(node) {
+            return Some(n);
+        }
+        let declarator = node.child_by_field_name("declarator")?;
+        let qid = find_declarator_qualified_id(declarator)?;
+        let text = self.text(qid).trim();
+        let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
+        parts.last().map(|s| s.to_string())
+    }
+
+    /// recoverCppMacroDefinedName (languages/c-cpp.ts:49).
+    fn recover_cpp_macro_defined_name(&self, node: Node) -> Option<String> {
+        if node.kind() != "function_definition" {
+            return None;
+        }
+        let declarator = node.child_by_field_name("declarator")?;
+        if declarator.kind() != "function_declarator" {
+            return None;
+        }
+        let inner = declarator.child_by_field_name("declarator")?;
+        if inner.kind() != "identifier" {
+            return None;
+        }
+        let macro_name = self.text(inner);
+        if !macro_shaped_re().is_match(macro_name) {
+            return None;
+        }
+        let params = declarator.child_by_field_name("parameters")?;
+        if params.named_child_count() < 2 {
+            return None;
+        }
+        let lone_ident_text = |p: Node| -> Option<&'t str> {
+            if p.kind() == "parameter_declaration"
+                && p.named_child_count() == 1
+                && p.named_child(0).map(|c| c.kind() == "type_identifier").unwrap_or(false)
+            {
+                Some(self.text(p.named_child(0).unwrap()))
+            } else {
+                None
+            }
+        };
+        let name = params.named_child(0).and_then(lone_ident_text)?;
+        if !has_lower_re().is_match(name) {
+            return None;
+        }
+        for i in 1..params.named_child_count() {
+            if let Some(p) = params.named_child(i) {
+                if lone_ident_text(p).is_some() {
+                    return None;
+                }
+            }
+        }
+        Some(name.to_string())
+    }
+
+    /// extractCppReceiverType (languages/c-cpp.ts:86).
+    fn receiver_type_of(&self, node: Node) -> Option<String> {
+        let declarator = node.child_by_field_name("declarator")?;
+        let qid = find_declarator_qualified_id(declarator)?;
+        let text = self.text(qid).trim();
+        let parts: Vec<&str> = text.split("::").filter(|p| !p.is_empty()).collect();
+        if parts.len() <= 1 {
+            return None;
+        }
+        let receiver = strip_cpp_template_args(&parts[..parts.len() - 1].join("::"));
+        if receiver.is_empty() {
+            None
+        } else {
+            Some(receiver)
+        }
+    }
+
+    /// extractCppReturnType: the `type` field, normalized.
+    fn return_type_of(&self, node: Node) -> Option<String> {
+        let type_node = node.child_by_field_name("type")?;
+        normalize_cpp_return_type(self.text(type_node))
+    }
+
+    /// cppExtractor.getVisibility: the FIRST access_specifier among the
+    /// parent's children decides (document order, not nearest-preceding —
+    /// bug-for-bug with the TS loop).
+    fn visibility_of(&self, node: Node) -> Option<u8> {
+        let parent = node.parent()?;
+        for i in 0..parent.child_count() {
+            let Some(child) = parent.child(i) else { continue };
+            if child.kind() == "access_specifier" {
+                let text = self.text(child);
+                if text.contains("public") {
+                    return Some(1);
+                }
+                if text.contains("private") {
+                    return Some(2);
+                }
+                if text.contains("protected") {
+                    return Some(3);
+                }
+            }
+        }
+        None
+    }
+
+    /// cExtractor.isConst: any named `type_qualifier` child reading "const".
+    fn is_const_declaration(&self, node: Node) -> bool {
+        (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .any(|c| c.kind() == "type_qualifier" && self.text(c) == "const")
+    }
+
+    /// cppExtractor.isMisparsedFunction (languages/c-cpp.ts:811). cpp only.
+    fn is_misparsed_function(&self, name: &str, node: Node) -> bool {
+        if self.variant != Variant::Cpp {
+            return false;
+        }
+        if name.starts_with("namespace") {
+            return true;
+        }
+        if matches!(name, "switch" | "if" | "for" | "while" | "do" | "case" | "return") {
+            return true;
+        }
+        is_macro_misparsed_type_decl(node)
+    }
+
+    /// composeReceiverQualifiedName (tree-sitter.ts:1424).
+    fn compose_receiver_qualified_name(&self, receiver_type: &str, name: &str) -> String {
+        let base = format!("{receiver_type}::{name}");
+        if self.namespace_prefix.is_empty() {
+            return base;
+        }
+        let receiver_head = receiver_type.split("::").next().unwrap_or("");
+        let anchor = self.namespace_prefix.iter().position(|p| p == receiver_head);
+        let prefix: &[String] = match anchor {
+            Some(i) => &self.namespace_prefix[..i],
+            None => &self.namespace_prefix[..],
+        };
+        if prefix.is_empty() {
+            base
+        } else {
+            format!("{}::{}", prefix.join("::"), base)
+        }
+    }
+
+    // --- visitNode -----------------------------------------------------------
+
+    fn visit_node(&mut self, node: Node<'t>) {
+        let kind = node.kind();
+        let mut skip_children = false;
+
+        // C++ namespace blocks: prefix-only, no node (#1291/#1093). Anonymous
+        // namespaces fall through to the generic walk.
+        if self.variant == Variant::Cpp && kind == "namespace_definition" {
+            let ns_name = node
+                .child_by_field_name("name")
+                .map(|n| self.text(n).to_string())
+                .unwrap_or_default();
+            if !ns_name.is_empty() {
+                self.namespace_prefix.push(ns_name);
+                for i in 0..node.named_child_count() {
+                    if let Some(c) = node.named_child(i) {
+                        self.visit_node(c);
+                    }
+                }
+                self.namespace_prefix.pop();
+                return;
+            }
+        }
+
+        self.maybe_capture_fn_refs(node);
+
+        if kind == "function_definition" {
+            // functionTypes for both; cpp's methodTypes also lists it, so
+            // inside a class-like scope it extracts as a method.
+            if self.inside_class_like() && self.variant == Variant::Cpp {
+                self.extract_method(node);
+            } else {
+                self.extract_function(node);
+            }
+            skip_children = true;
+        } else if self.variant == Variant::Cpp && kind == "class_specifier" {
+            self.extract_class(node);
+            skip_children = true;
+        } else if kind == "struct_specifier" {
+            self.extract_struct(node);
+            skip_children = true;
+        } else if kind == "enum_specifier" {
+            self.extract_enum(node);
+            skip_children = true;
+        } else if kind == "type_definition"
+            || (self.variant == Variant::Cpp && kind == "alias_declaration")
+        {
+            skip_children = self.extract_type_alias(node);
+        } else if kind == "declaration" && !self.inside_class_like() {
+            self.extract_variable(node);
+            self.scan_fn_ref_subtree(node, 0);
+            skip_children = true;
+        } else if kind == "preproc_include" {
+            self.extract_import(node);
+        } else if kind == "call_expression" {
+            self.extract_call(node);
+        } else if kind == "new_expression" {
+            // INSTANTIATION_KINDS: cpp `new Foo(...)`. (No anonymous-class
+            // body exists under new_expression in this grammar; children are
+            // still walked for nested calls.)
+            self.extract_instantiation(node);
+        }
+
+        if !skip_children {
+            for i in 0..node.named_child_count() {
+                if let Some(c) = node.named_child(i) {
+                    self.visit_node(c);
+                }
+            }
+        }
+    }
+
+    // --- extractors ----------------------------------------------------------
+
+    fn extract_function(&mut self, node: Node<'t>) {
+        // Receiver present (out-of-line `Cls::method` def) → method instead.
+        if self.variant == Variant::Cpp && self.receiver_type_of(node).is_some() {
+            self.extract_method(node);
+            return;
+        }
+
+        let name = self.extract_name(node);
+        if name == "<anonymous>" {
+            if let Some(body) = node.child_by_field_name("body") {
+                self.visit_function_body(body);
+            }
+            return;
+        }
+        // Misparse artifacts: drop the node, still walk the body (#946/#1061).
+        if self.is_misparsed_function(&name, node) {
+            if let Some(body) = node.child_by_field_name("body") {
+                self.visit_function_body(body);
+            }
+            return;
+        }
+
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
+            return_type: self.return_type_of(node),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("function", &name, node, extra) else { return };
+        // (extractTypeAnnotations + extractDecoratorsFor are structural no-ops
+        // for c/cpp: not in TYPE_ANNOTATION_LANGUAGES, and the decorator node
+        // kinds never appear as direct children/preceding siblings in these
+        // grammars — `attribute` only occurs under attribute_declaration.)
+        self.stack.push(Scope { row, kind: "function", name });
+        if let Some(body) = node.child_by_field_name("body") {
+            self.visit_function_body(body);
+        }
+        self.stack.pop();
+    }
+
+    fn extract_method(&mut self, node: Node<'t>) {
+        let receiver_type = if self.variant == Variant::Cpp { self.receiver_type_of(node) } else { None };
+
+        if !self.inside_class_like() && receiver_type.is_none() {
+            // (object-literal parents don't occur in c/cpp) — treat as function.
+            self.extract_function(node);
+            return;
+        }
+
+        let name = self.extract_name(node);
+        if self.is_misparsed_function(&name, node) {
+            if let Some(body) = node.child_by_field_name("body") {
+                self.visit_function_body(body);
+            }
+            return;
+        }
+
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
+            return_type: self.return_type_of(node),
+            qualified_name: receiver_type
+                .as_ref()
+                .map(|r| self.compose_receiver_qualified_name(r, &name)),
+            ..Extra::default() // extractMethod passes no isExported
+        };
+        let Some(row) = self.create_node("method", &name, node, extra) else { return };
+
+        // Out-of-line def: contains edge from the FIRST earlier-in-file
+        // struct/class/enum/trait node of the receiver's name.
+        if let Some(receiver_type) = &receiver_type {
+            if !self.inside_class_like() {
+                let owner_row = self
+                    .nodes_meta
+                    .iter()
+                    .position(|m| {
+                        m.name == *receiver_type
+                            && matches!(m.kind, "struct" | "class" | "enum" | "trait")
+                    })
+                    .map(|i| i as u32);
+                if let Some(owner_row) = owner_row {
+                    self.tables.push_edge(&EdgeRow {
+                        source_idx: owner_row,
+                        target_idx: row,
+                        kind: edge_kind_index("contains").unwrap(),
+                        provenance: 0,
+                        line: NONE,
+                        column: NONE,
+                        metadata_json: NONE_STR,
+                        source_id_str: NONE_STR,
+                        target_id_str: NONE_STR,
+                    });
+                }
+            }
+        }
+
+        self.stack.push(Scope { row, kind: "method", name });
+        if let Some(body) = node.child_by_field_name("body") {
+            self.visit_function_body(body);
+        }
+        self.stack.pop();
+    }
+
+    /// extractClass for cpp class_specifier (skipBodilessClass, #1093).
+    fn extract_class(&mut self, node: Node<'t>) {
+        let Some(body) = node.child_by_field_name("body") else { return };
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: self.visibility_of(node),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("class", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        self.stack.push(Scope { row, kind: "class", name });
+        for i in 0..body.named_child_count() {
+            if let Some(c) = body.named_child(i) {
+                self.visit_node(c);
+            }
+        }
+        self.stack.pop();
+    }
+
+    /// extractStruct: bodiless specifiers (fwd decls / elaborated refs) skip.
+    fn extract_struct(&mut self, node: Node<'t>) {
+        let Some(body) = node.child_by_field_name("body") else { return };
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("struct", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        self.stack.push(Scope { row, kind: "struct", name });
+        for i in 0..body.named_child_count() {
+            if let Some(c) = body.named_child(i) {
+                self.visit_node(c);
+            }
+        }
+        self.stack.pop();
+    }
+
+    fn extract_enum(&mut self, node: Node<'t>) {
+        let Some(body) = node.child_by_field_name("body") else { return };
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: if self.variant == Variant::Cpp { self.visibility_of(node) } else { None },
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("enum", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        self.stack.push(Scope { row, kind: "enum", name });
+        for i in 0..body.named_child_count() {
+            let Some(child) = body.named_child(i) else { continue };
+            if child.kind() == "enumerator" {
+                self.extract_enum_members(child);
+            } else {
+                self.visit_node(child);
+            }
+        }
+        self.stack.pop();
+    }
+
+    /// extractEnumMembers: enumerator's `name` field (C/C++ always has one;
+    /// the TS fallbacks for other grammars are unreachable here).
+    fn extract_enum_members(&mut self, node: Node<'t>) {
+        if let Some(name_node) = node.child_by_field_name("name") {
+            let name = self.text(name_node).to_string();
+            self.create_node("enum_member", &name, node, Extra::default());
+        }
+    }
+
+    /// extractTypeAlias for type_definition / alias_declaration. Returns true
+    /// when children were consumed (typedef struct/enum bodies).
+    fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
+        let name = self.extract_name(node);
+        if name == "<anonymous>" {
+            return false;
+        }
+        let docstring = preceding_docstring(node, self.src);
+
+        // resolveTypeAliasKind: first child that is an enum/struct specifier
+        // WITH a body decides the node kind (anon inner specifier takes the
+        // typedef's name).
+        let mut resolved: Option<&'static str> = None;
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            if child.kind() == "enum_specifier" && child.child_by_field_name("body").is_some() {
+                resolved = Some("enum");
+                break;
+            }
+            if child.kind() == "struct_specifier" && child.child_by_field_name("body").is_some() {
+                resolved = Some("struct");
+                break;
+            }
+        }
+
+        if resolved == Some("struct") {
+            let Some(row) = self.create_node(
+                "struct",
+                &name,
+                node,
+                Extra { docstring, ..Extra::default() },
+            ) else {
+                return true;
+            };
+            self.stack.push(Scope { row, kind: "struct", name });
+            let type_child = node
+                .child_by_field_name("type")
+                .or_else(|| self.find_child_by_kind(node, "struct_specifier"));
+            if let Some(tc) = type_child {
+                self.extract_inheritance(tc, row);
+                let body = tc.child_by_field_name("body").unwrap_or(tc);
+                for i in 0..body.named_child_count() {
+                    if let Some(c) = body.named_child(i) {
+                        self.visit_node(c);
+                    }
+                }
+            }
+            self.stack.pop();
+            return true;
+        }
+
+        if resolved == Some("enum") {
+            let Some(row) = self.create_node(
+                "enum",
+                &name,
+                node,
+                Extra { docstring, ..Extra::default() },
+            ) else {
+                return true;
+            };
+            self.stack.push(Scope { row, kind: "enum", name });
+            if let Some(inner) = self.find_child_by_kind(node, "enum_specifier") {
+                self.extract_inheritance(inner, row);
+                if let Some(body) = inner.child_by_field_name("body") {
+                    for i in 0..body.named_child_count() {
+                        let Some(child) = body.named_child(i) else { continue };
+                        if child.kind() == "enumerator" {
+                            self.extract_enum_members(child);
+                        } else {
+                            self.visit_node(child);
+                        }
+                    }
+                }
+            }
+            self.stack.pop();
+            return true;
+        }
+
+        self.create_node("type_alias", &name, node, Extra { docstring, ..Extra::default() });
+        false
+    }
+
+    fn find_child_by_kind(&self, node: Node<'t>, kind: &str) -> Option<Node<'t>> {
+        (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == kind)
+    }
+
+    /// extractVariable: C takes the dedicated branch (file-scope declarators,
+    /// tree-sitter.ts:2795); cpp takes the TS GENERIC fallback (direct
+    /// identifier children).
+    fn extract_variable(&mut self, node: Node<'t>) {
+        let is_const = self.variant == Variant::C && self.is_const_declaration(node);
+        let kind: &'static str = if is_const { "constant" } else { "variable" };
+        let docstring = preceding_docstring(node, self.src);
+        // isExported?.() ?? false — EXPLICIT false (tri-state flag set).
+        let is_exported = Some(false);
+
+        if self.variant == Variant::C {
+            if has_function_ancestor(node) {
+                return;
+            }
+            for i in 0..node.named_child_count() {
+                let Some(child) = node.named_child(i) else { continue };
+                if !matches!(
+                    child.kind(),
+                    "init_declarator" | "pointer_declarator" | "array_declarator"
+                ) {
+                    continue;
+                }
+                let Some(name_node) = c_declarator_identifier(child) else { continue };
+                let name = self.text(name_node);
+                if name.is_empty() {
+                    continue;
+                }
+                let value_node = if child.kind() == "init_declarator" {
+                    child.child_by_field_name("value")
+                } else {
+                    None
+                };
+                let signature = value_node.map(|v| util::init_signature(self.text(v)));
+                self.create_node(
+                    kind,
+                    name,
+                    child,
+                    Extra {
+                        docstring: docstring.clone(),
+                        signature,
+                        is_exported,
+                        ..Extra::default()
+                    },
+                );
+            }
+        } else {
+            // Generic fallback: direct identifier children only (`int x;`
+            // extracts; `int x = 5;` nests in an init_declarator and does not).
+            for i in 0..node.named_child_count() {
+                let Some(child) = node.named_child(i) else { continue };
+                if child.kind() != "identifier" {
+                    continue;
+                }
+                let name = self.text(child).to_string();
+                if !name.is_empty() && name != "<anonymous>" {
+                    self.create_node(
+                        kind,
+                        &name,
+                        child,
+                        Extra { docstring: docstring.clone(), is_exported, ..Extra::default() },
+                    );
+                }
+            }
+        }
+    }
+
+    /// extractImport via the c/cpp extractImport hook: `#include <sys.h>` /
+    /// `#include "local.h"`. A hook miss (`#include MACRO`) extracts nothing.
+    fn extract_import(&mut self, node: Node<'t>) {
+        let import_text = self.text(node).trim().to_string();
+        let module_name: Option<String> =
+            if let Some(sys) = self.find_child_by_kind(node, "system_lib_string") {
+                let t = self.text(sys);
+                let t = t.strip_prefix('<').unwrap_or(t);
+                let t = t.strip_suffix('>').unwrap_or(t);
+                Some(t.to_string())
+            } else if let Some(lit) = self.find_child_by_kind(node, "string_literal") {
+                self.find_child_by_kind(lit, "string_content").map(|sc| self.text(sc).to_string())
+            } else {
+                None
+            };
+        let Some(module_name) = module_name else { return };
+        self.create_node(
+            "import",
+            &module_name,
+            node,
+            Extra { signature: Some(import_text), ..Extra::default() },
+        );
+        if !module_name.is_empty() {
+            let parent = self.top_row();
+            self.push_ref_at(parent, &module_name, edge_kind_index("imports").unwrap(), node);
+        }
+    }
+
+    // --- calls / instantiation ----------------------------------------------
+
+    fn extract_call(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let caller_row = self.top_row();
+        let func = node
+            .child_by_field_name("function")
+            .or_else(|| node.named_child(0));
+        let calls_kind = edge_kind_index("calls").unwrap();
+
+        // C++ explicit operator call `a.operator+(b)` (#1247): the
+        // operator_name hides in an ERROR child. (has_error() defers such
+        // files to wasm, so this scan is a faithful no-op today.)
+        if self.variant == Variant::Cpp {
+            if let Some(func) = func {
+                let mut operator_name = String::new();
+                'err: for i in 0..node.named_child_count() {
+                    let Some(child) = node.named_child(i) else { continue };
+                    if child.kind() != "ERROR" {
+                        continue;
+                    }
+                    for j in 0..child.named_child_count() {
+                        if let Some(op) = child.named_child(j) {
+                            if op.kind() == "operator_name" {
+                                operator_name = self.text(op).to_string();
+                                break 'err;
+                            }
+                        }
+                    }
+                }
+                if !operator_name.is_empty() {
+                    let sym = operator_name["operator".len()..].trim().to_string();
+                    if symbolic_op_re().is_match(&sym) {
+                        let compact: String = sym.chars().filter(|c| !c.is_whitespace()).collect();
+                        operator_name = format!("operator{compact}");
+                    }
+                    let receiver = arrow_dot_no_ws(self.text(func));
+                    if receiver != "this" && !operator_receiver_re().is_match(&receiver) {
+                        return;
+                    }
+                    let callee = if receiver == "this" {
+                        operator_name
+                    } else {
+                        format!("{receiver}.{operator_name}")
+                    };
+                    self.push_ref_at(caller_row, &callee, calls_kind, node);
+                    return;
+                }
+            }
+        }
+
+        let mut callee_name = String::new();
+        if let Some(func) = func {
+            if func.kind() == "field_expression" {
+                // `obj.method()` / `ptr->method()` — the `field` field.
+                let property = func
+                    .child_by_field_name("property")
+                    .or_else(|| func.child_by_field_name("field"))
+                    .or_else(|| func.named_child(1));
+                if let Some(property) = property {
+                    let method_name = self.text(property);
+                    let receiver = func
+                        .child_by_field_name("object")
+                        .or_else(|| func.child_by_field_name("operand"))
+                        .or_else(|| func.child_by_field_name("argument"))
+                        .or_else(|| func.named_child(0));
+                    if let Some(r) = receiver {
+                        if is_literal_receiver(r.kind()) {
+                            return; // #1230: literal receivers emit nothing
+                        }
+                    }
+                    match receiver.map(|r| r.kind()) {
+                        Some("identifier") | Some("simple_identifier") | Some("field_identifier") => {
+                            let receiver_name = self.text(receiver.unwrap());
+                            if !matches!(receiver_name, "self" | "this" | "cls" | "super") {
+                                callee_name = format!("{receiver_name}.{method_name}");
+                            } else {
+                                callee_name = method_name.to_string();
+                            }
+                        }
+                        Some("call_expression") => {
+                            // Call-result receiver (#645/#608): re-encode as
+                            // `<innerCallee>().<method>` — C/C++ re-encode any inner.
+                            let inner_fn = receiver.unwrap().child_by_field_name("function");
+                            let inner_callee =
+                                inner_fn.map(|f| arrow_dot_no_ws(self.text(f))).unwrap_or_default();
+                            if !inner_callee.is_empty() {
+                                callee_name = format!("{inner_callee}().{method_name}");
+                            } else {
+                                callee_name = method_name.to_string();
+                            }
+                        }
+                        _ => {
+                            callee_name = method_name.to_string();
+                        }
+                    }
+                }
+            } else {
+                // Bare / qualified / templated / parenthesized callee.
+                callee_name = self.text(func).to_string();
+            }
+        }
+
+        if !callee_name.is_empty() {
+            // `(*fp)(x)` → `fp` (parenthesized-conversion normalization).
+            if let Some(c) = util::paren_conversion().captures(&callee_name) {
+                callee_name = c[1].to_string();
+            }
+        }
+
+        // Template-arg strip on callees (`fn<T, 256>(args)`, `ns::fn<T>()`).
+        if !callee_name.is_empty() && callee_name.contains('<') && !callee_name.contains("operator")
+        {
+            callee_name = strip_cpp_template_args(&callee_name);
+        }
+
+        // Local fn-pointer fan-out: a bare callee bound earlier from `&fn`
+        // emits one calls ref PER recorded target (insertion order).
+        if !callee_name.is_empty()
+            && self.variant == Variant::Cpp
+            && simple_ident_re().is_match(&callee_name)
+        {
+            let targets = self
+                .local_fn_ptrs
+                .get(&caller_row)
+                .and_then(|locals| locals.get(&callee_name))
+                .cloned();
+            if let Some(targets) = targets {
+                if !targets.is_empty() {
+                    for target in &targets {
+                        self.push_ref_at(caller_row, target, calls_kind, node);
+                    }
+                    return;
+                }
+            }
+        }
+
+        if !callee_name.is_empty() {
+            self.push_ref_at(caller_row, &callee_name, calls_kind, node);
+        }
+    }
+
+    /// extractInstantiation: `new Foo(...)` and stack constructions (both
+    /// read the type from the `type` field; template args + qualifiers strip).
+    fn extract_instantiation(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let from = self.top_row();
+        let ctor = node
+            .child_by_field_name("constructor")
+            .or_else(|| node.child_by_field_name("type"))
+            .or_else(|| node.child_by_field_name("name"))
+            .or_else(|| node.named_child(0));
+        let Some(ctor) = ctor else { return };
+
+        let mut class_name = self.text(ctor).to_string();
+        if let Some(lt) = class_name.find('<') {
+            if lt > 0 {
+                class_name.truncate(lt);
+            }
+        }
+        // Keep the trailing identifier of `ns::Foo` / `a.Foo`.
+        let last_dot = class_name.rfind('.').map(|i| i as isize).unwrap_or(-1);
+        let last_colons = class_name.rfind("::").map(|i| i as isize).unwrap_or(-1);
+        let cut = last_dot.max(last_colons);
+        if cut >= 0 {
+            class_name = class_name[(cut as usize + 1)..].to_string();
+            if class_name.starts_with(':') || class_name.starts_with('.') {
+                class_name.remove(0);
+            }
+        }
+        let class_name = class_name.trim().to_string();
+        if !class_name.is_empty() {
+            self.push_ref_at(from, &class_name, edge_kind_index("instantiates").unwrap(), node);
+        }
+    }
+
+    /// isCppStackConstruction (#1035).
+    fn is_cpp_stack_construction(&self, node: Node) -> bool {
+        let Some(type_node) = node.child_by_field_name("type") else { return false };
+        if !matches!(
+            type_node.kind(),
+            "type_identifier" | "template_type" | "qualified_identifier"
+        ) {
+            return false;
+        }
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            if child.kind() != "init_declarator" {
+                continue;
+            }
+            if let Some(value) = child.child_by_field_name("value") {
+                if matches!(value.kind(), "argument_list" | "initializer_list") {
+                    return true;
+                }
+            }
+        }
+        false
+    }
+
+    /// recordCppFnPtrBinding (tree-sitter.ts:5089).
+    fn record_cpp_fn_ptr_binding(&mut self, local_name: &str, value: Option<Node>) {
+        let Some(value) = value else { return };
+        if value.kind() != "pointer_expression" {
+            return;
+        }
+        if value.child(0).map(|c| c.kind() != "&").unwrap_or(true) {
+            return; // `*p` dereference, not address-of
+        }
+        let arg = value
+            .child_by_field_name("argument")
+            .or_else(|| value.named_child(0));
+        let Some(arg) = arg else { return };
+        if !matches!(arg.kind(), "identifier" | "template_function" | "qualified_identifier") {
+            return;
+        }
+        if self.stack.is_empty() {
+            return;
+        }
+        let caller_row = self.top_row();
+        let target = strip_cpp_template_args(self.text(arg));
+        if target.is_empty() || target == local_name {
+            return;
+        }
+        let targets = self
+            .local_fn_ptrs
+            .entry(caller_row)
+            .or_default()
+            .entry(local_name.to_string())
+            .or_default();
+        if !targets.contains(&target) {
+            targets.push(target); // Set semantics, insertion-ordered
+        }
+    }
+
+    /// extractStaticMemberRef — cpp only (c is not in STATIC_MEMBER_LANGS).
+    /// In this grammar the firing shape is `field_expression` (listed in
+    /// MEMBER_ACCESS_TYPES for Scala — same node kind here): a capitalized
+    /// simple receiver's value read.
+    fn extract_static_member_ref(&mut self, node: Node<'t>) {
+        if self.variant != Variant::Cpp {
+            return;
+        }
+        if self.stack.is_empty() {
+            return;
+        }
+        if !matches!(node.kind(), "field_expression" | "qualified_identifier") {
+            return;
+        }
+        // Skip `Type.method()` — the access is a call's callee, already linked.
+        if let Some(parent) = node.parent() {
+            if parent.kind() == "call_expression" {
+                let callee = parent
+                    .child_by_field_name("function")
+                    .or_else(|| parent.child_by_field_name("method"))
+                    .or_else(|| parent.named_child(0));
+                if let Some(callee) = callee {
+                    if callee.start_byte() == node.start_byte() {
+                        return;
+                    }
+                }
+            }
+        }
+        let recv = node
+            .child_by_field_name("object")
+            .or_else(|| node.child_by_field_name("expression"))
+            .or_else(|| node.child_by_field_name("scope"))
+            .or_else(|| node.named_child(0));
+        let Some(recv) = recv else { return };
+        if !matches!(
+            recv.kind(),
+            "identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier"
+        ) {
+            return;
+        }
+        let text = self.text(recv);
+        if capitalized_re().is_match(text) {
+            let owner = self.top_row();
+            let name = text.to_string();
+            self.push_ref_at(owner, &name, edge_kind_index("references").unwrap(), recv);
+        }
+    }
+
+    // --- function bodies -----------------------------------------------------
+
+    fn visit_function_body(&mut self, body: Node<'t>) {
+        self.visit_for_calls_and_structure(body);
+    }
+
+    fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        let kind = node.kind();
+        self.maybe_capture_fn_refs(node);
+
+        if kind == "call_expression" {
+            self.extract_call(node);
+        } else if kind == "new_expression" {
+            self.extract_instantiation(node);
+        }
+
+        // C++ stack construction `Calculator calc(0)` / `Widget w{1,2}` (#1035).
+        if kind == "declaration"
+            && self.variant == Variant::Cpp
+            && self.is_cpp_stack_construction(node)
+        {
+            self.extract_instantiation(node);
+        }
+
+        // C++ local fn-pointer bindings: declarations and branch reassignments.
+        if self.variant == Variant::Cpp && !self.stack.is_empty() {
+            if kind == "declaration" {
+                for i in 0..node.named_child_count() {
+                    let Some(child) = node.named_child(i) else { continue };
+                    if child.kind() != "init_declarator" {
+                        continue;
+                    }
+                    let Some(decl) = child.child_by_field_name("declarator") else { continue };
+                    if decl.kind() != "identifier" {
+                        continue;
+                    }
+                    let local = self.text(decl).to_string();
+                    self.record_cpp_fn_ptr_binding(&local, child.child_by_field_name("value"));
+                }
+            } else if kind == "assignment_expression" {
+                if let Some(left) = node.child_by_field_name("left") {
+                    if left.kind() == "identifier" {
+                        let local = self.text(left).to_string();
+                        self.record_cpp_fn_ptr_binding(&local, node.child_by_field_name("right"));
+                    }
+                }
+            }
+        }
+
+        // Static-member / value-read: `Foo.BAR`, `Foo->x` (cpp).
+        self.extract_static_member_ref(node);
+
+        // Nested NAMED functions become their own nodes.
+        if kind == "function_definition" {
+            let nested_name = self.extract_name(node);
+            if !nested_name.is_empty() && nested_name != "<anonymous>" {
+                self.extract_function(node);
+                return;
+            }
+        }
+
+        // Structural nodes inside bodies (local classes; macro-misparse rescue).
+        if self.variant == Variant::Cpp && kind == "class_specifier" {
+            self.extract_class(node);
+            return;
+        }
+        if kind == "struct_specifier" {
+            self.extract_struct(node);
+            return;
+        }
+        if kind == "enum_specifier" {
+            self.extract_enum(node);
+            return;
+        }
+
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.visit_for_calls_and_structure(c);
+            }
+        }
+    }
+
+    // --- inheritance ---------------------------------------------------------
+
+    /// extractInheritance — the branches whose node kinds occur in the c/cpp
+    /// grammars: base_class_clause (#1043), the field_declaration Go-embedding
+    /// shape, and the field_declaration_list recursion that reaches it.
+    fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        let extends_kind = edge_kind_index("extends").unwrap();
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            match child.kind() {
+                "base_class_clause" => {
+                    for j in 0..child.named_child_count() {
+                        let Some(t) = child.named_child(j) else { continue };
+                        if matches!(
+                            t.kind(),
+                            "type_identifier" | "qualified_identifier" | "template_type"
+                        ) {
+                            let name = strip_cpp_template_args(self.text(t));
+                            self.push_ref_at(class_row, &name, extends_kind, t);
+                        }
+                    }
+                }
+                "field_declaration" => {
+                    let has_field_identifier = (0..child.named_child_count())
+                        .filter_map(|j| child.named_child(j))
+                        .any(|c| c.kind() == "field_identifier");
+                    if !has_field_identifier {
+                        let type_id = (0..child.named_child_count())
+                            .filter_map(|j| child.named_child(j))
+                            .find(|c| c.kind() == "type_identifier");
+                        if let Some(type_id) = type_id {
+                            let name = self.text(type_id).to_string();
+                            self.push_ref_at(class_row, &name, extends_kind, type_id);
+                        }
+                    }
+                }
+                "field_declaration_list" | "class_heritage" => {
+                    self.extract_inheritance(child, class_row);
+                }
+                _ => {}
+            }
+        }
+    }
+
+    // --- fn-ref capture (#756, cFamilySpec) ----------------------------------
+
+    /// maybeCaptureFnRefs + captureFnRefCandidates for the cFamily dispatch:
+    /// argument_list(args), assignment_expression(rhs:right),
+    /// init_declarator(varinit:value), initializer_list(list),
+    /// initializer_pair(value:value).
+    fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
+        let mode = match node.kind() {
+            "argument_list" => Mode::Args,
+            "assignment_expression" => Mode::Rhs,
+            "init_declarator" => Mode::Varinit,
+            "initializer_list" => Mode::List,
+            "initializer_pair" => Mode::Value,
+            _ => return,
+        };
+        if self.stack.is_empty() {
+            return;
+        }
+        let from = self.top_row();
+
+        let mut values: Vec<Node<'t>> = Vec::new();
+        match mode {
+            Mode::Args | Mode::List => {
+                for i in 0..node.named_child_count() {
+                    if let Some(c) = node.named_child(i) {
+                        values.push(c);
+                    }
+                }
+            }
+            Mode::Rhs => {
+                if let Some(rhs) = node.child_by_field_name("right") {
+                    // Param-storage skip: `o->cb = cb` (LHS last name == RHS).
+                    let lhs_text = node
+                        .child_by_field_name("left")
+                        .map(|l| self.text(l))
+                        .unwrap_or("");
+                    let lhs_last = util::lhs_last_name()
+                        .captures(lhs_text)
+                        .and_then(|c| c.get(1))
+                        .map(|m| m.as_str());
+                    if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
+                        values.push(rhs);
+                    }
+                }
+            }
+            Mode::Value => {
+                let v = node.child_by_field_name("value").or_else(|| {
+                    if node.named_child_count() > 0 {
+                        node.named_child(node.named_child_count() - 1)
+                    } else {
+                        None
+                    }
+                });
+                if let Some(v) = v {
+                    values.push(v);
+                }
+            }
+            Mode::Varinit => {
+                // (init_declarator has no name/pattern field — no destructure skip)
+                if let Some(v) = node.child_by_field_name("value") {
+                    values.push(v);
+                }
+            }
+        }
+
+        for v in values {
+            let explicit_ref = v.kind() != "identifier"; // !idTypes.has(type)
+            self.normalize_fn_ref_value(v, from, mode, explicit_ref, 0);
+        }
+    }
+
+    /// normalizeValue for cFamilySpec: bare identifiers, and the
+    /// pointer_expression unwrap (`&fn`; `&Cls::m` keeps the qualified name).
+    fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, mode: Mode, explicit_ref: bool, depth: u32) {
+        if depth > 4 {
+            return;
+        }
+        match v.kind() {
+            "identifier" => {
+                let name = self.text(v);
+                if name.is_empty() || is_stoplisted(name) {
+                    return;
+                }
+                self.push_fn_ref_cand(from, name.to_string(), mode, explicit_ref, v);
+            }
+            "pointer_expression" => {
+                // `&x` is a function value; `*x` is a data read.
+                if v.child(0).map(|c| c.kind() != "&").unwrap_or(true) {
+                    return;
+                }
+                let Some(inner) = v.child_by_field_name("argument") else { return };
+                if inner.kind() == "qualified_identifier" {
+                    let text = self.text(inner).trim();
+                    if qualified_ref_re().is_match(text) && !is_stoplisted(text) {
+                        self.push_fn_ref_cand(from, text.to_string(), mode, explicit_ref, inner);
+                    }
+                    return;
+                }
+                self.normalize_fn_ref_value(inner, from, mode, explicit_ref, depth + 1);
+            }
+            _ => {}
+        }
+    }
+
+    fn push_fn_ref_cand(&mut self, from: u32, name: String, mode: Mode, explicit_ref: bool, node: Node) {
+        let p = node.start_position();
+        self.fn_ref_cands.push(Cand {
+            from,
+            name,
+            mode,
+            explicit_ref,
+            line: p.row as u32 + 1,
+            column_byte: node.start_byte(),
+            row: p.row,
+        });
+    }
+
+    /// scanFnRefSubtree: capture-only walk of subtrees the main walkers skip
+    /// (variable-declaration initializers). Halts at nested functions/lambdas.
+    fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        if depth > 12 {
+            return;
+        }
+        if depth > 0
+            && matches!(
+                node.kind(),
+                "function_definition" | "arrow_function" | "function_expression"
+                    | "lambda_literal" | "lambda_expression"
+            )
+        {
+            return;
+        }
+        self.maybe_capture_fn_refs(node);
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.scan_fn_ref_subtree(c, depth + 1);
+            }
+        }
+    }
+
+    /// flushFnRefCandidates with the cFamily gate policy: value/list positions
+    /// at FILE scope skip the same-file/import gate (C has no symbol imports);
+    /// cpp additionally requires explicit `&` forms outside those positions.
+    fn flush_fn_ref_candidates(&mut self) {
+        let cands = std::mem::take(&mut self.fn_ref_cands);
+        if cands.is_empty() || util::is_generated_file(self.file_path) {
+            return;
+        }
+        let address_of_only = self.variant == Variant::Cpp;
+        let mut seen: HashSet<(String, String)> = HashSet::new();
+        for c in cands {
+            let at_file_scope = self.node_ids[c.from as usize].starts_with("file:");
+            if address_of_only
+                && !c.explicit_ref
+                && !(at_file_scope && matches!(c.mode, Mode::Value | Mode::List))
+            {
+                continue;
+            }
+            if !c.name.starts_with("this.") && !c.name.contains("::") {
+                let skip_gate = matches!(c.mode, Mode::Value | Mode::List) && at_file_scope;
+                if !skip_gate
+                    && !self.defined_fn_names.contains(&c.name)
+                    && !self.imported_names.contains(&c.name)
+                {
+                    continue;
+                }
+            }
+            if !seen.insert((self.node_ids[c.from as usize].clone(), c.name.clone())) {
+                continue;
+            }
+            let column = util::col16(self.src, &self.line_starts, c.row, c.column_byte);
+            let name_ref = self.arena.put(&c.name);
+            self.tables.push_ref(&RefRow {
+                from_idx: c.from,
+                kind: FUNCTION_REF_CODE,
+                line: c.line,
+                column,
+                reference_name: name_ref,
+                candidates: NONE_STR,
+                from_id_str: NONE_STR,
+            });
+        }
+    }
+
+    // --- value refs (C only: VALUE_REF_LANGS has 'c', not 'cpp') -------------
+
+    fn flush_value_refs(&mut self, root: Node<'t>) {
+        let scopes = std::mem::take(&mut self.value_scopes);
+        let mut targets = std::mem::take(&mut self.fs_values);
+        let counts = std::mem::take(&mut self.fs_value_counts);
+        if self.variant != Variant::C {
+            return;
+        }
+        if std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
+            return;
+        }
+        if targets.is_empty() || scopes.is_empty() || util::is_generated_file(self.file_path) {
+            return;
+        }
+
+        // Shadow prune — the C declarator shape is init_declarator (a
+        // file-scope const AND the local that shadows it both count).
+        let mut decl_counts: HashMap<&str, u32> = HashMap::new();
+        let mut dstack: Vec<Node> = vec![root];
+        let mut dvisited = 0usize;
+        while let Some(n) = dstack.pop() {
+            if dvisited >= MAX_VALUE_REF_NODES {
+                break;
+            }
+            dvisited += 1;
+            if n.kind() == "init_declarator" {
+                if let Some(name_node) = c_declarator_identifier(n) {
+                    if matches!(name_node.kind(), "identifier" | "simple_identifier") {
+                        let nm = self.text(name_node);
+                        if targets.contains_key(nm) {
+                            *decl_counts.entry(nm).or_insert(0) += 1;
+                        }
+                    }
+                }
+            }
+            for i in 0..n.named_child_count() {
+                if let Some(c) = n.named_child(i) {
+                    dstack.push(c);
+                }
+            }
+        }
+        let shadowed: Vec<String> = decl_counts
+            .iter()
+            .filter(|(nm, c)| **c > counts.get(**nm).copied().unwrap_or(1))
+            .map(|(nm, _)| nm.to_string())
+            .collect();
+        for nm in shadowed {
+            targets.remove(&nm);
+        }
+        if targets.is_empty() {
+            return;
+        }
+
+        let refs_kind = edge_kind_index("references").unwrap();
+        for scope in &scopes {
+            let mut seen: HashSet<&str> = HashSet::new();
+            let mut stack: Vec<Node> = vec![scope.node];
+            // (No Dart/Pascal sibling-body pull-in: c/cpp bodies are children.)
+            let mut visited = 0usize;
+            while let Some(n) = stack.pop() {
+                if visited >= MAX_VALUE_REF_NODES {
+                    break;
+                }
+                visited += 1;
+                if matches!(n.kind(), "identifier" | "constant" | "name" | "simple_identifier") {
+                    let ref_name = self.text(n);
+                    if let Some(&target_row) = targets.get(ref_name) {
+                        let target_id = self.node_ids[target_row as usize].as_str();
+                        if target_id != self.node_ids[scope.row as usize]
+                            && ref_name != scope.name
+                            && !seen.contains(&target_id)
+                        {
+                            seen.insert(target_id);
+                            let meta = self.arena.put(r#"{"valueRef":true}"#);
+                            self.tables.push_edge(&EdgeRow {
+                                source_idx: scope.row,
+                                target_idx: target_row,
+                                kind: refs_kind,
+                                provenance: 0,
+                                line: NONE,
+                                column: NONE,
+                                metadata_json: meta,
+                                source_id_str: NONE_STR,
+                                target_id_str: NONE_STR,
+                            });
+                        }
+                    }
+                }
+                for i in 0..n.named_child_count() {
+                    if let Some(c) = n.named_child(i) {
+                        stack.push(c);
+                    }
+                }
+            }
+        }
+    }
+}
+
+// --- free helpers ------------------------------------------------------------
+
+/// findDeclaratorQualifiedId (languages/c-cpp.ts:13): BFS for the declarator's
+/// `qualified_identifier`, skipping parameter_list + trailing_return_type so a
+/// qualified PARAMETER type can't be mistaken for the method name.
+fn find_declarator_qualified_id(declarator: Node) -> Option<Node> {
+    let mut queue: VecDeque<Node> = VecDeque::new();
+    queue.push_back(declarator);
+    while let Some(current) = queue.pop_front() {
+        if current.kind() == "qualified_identifier" {
+            return Some(current);
+        }
+        for i in 0..current.named_child_count() {
+            if let Some(child) = current.named_child(i) {
+                if child.kind() != "parameter_list" && child.kind() != "trailing_return_type" {
+                    queue.push_back(child);
+                }
+            }
+        }
+    }
+    None
+}
+
+/// cDeclaratorIdentifier (tree-sitter.ts:234): resolve the declared identifier
+/// through init/pointer/array/parenthesized declarator wrappers; a
+/// function_declarator means prototype/fn-ptr — null. (The C grammar's
+/// parenthesized_declarator exposes no `declarator` field, so that arm always
+/// terminates — bug-for-bug with getChildByField returning null there.)
+fn c_declarator_identifier(node: Node) -> Option<Node> {
+    let mut cur = Some(node);
+    let mut guard = 0;
+    while let Some(n) = cur {
+        guard += 1;
+        if guard > 12 {
+            return None;
+        }
+        match n.kind() {
+            "identifier" => return Some(n),
+            "function_declarator" => return None,
+            "init_declarator" | "pointer_declarator" | "array_declarator"
+            | "parenthesized_declarator" => {
+                cur = n.child_by_field_name("declarator");
+            }
+            _ => return None,
+        }
+    }
+    None
+}
+
+/// isMacroMisparsedTypeDecl (languages/c-cpp.ts:261): `class MACRO Name {…}`
+/// misparse residue — bodyless class/struct specifier in `type` + a
+/// non-function_declarator declarator.
+fn is_macro_misparsed_type_decl(node: Node) -> bool {
+    let Some(type_node) = node.child_by_field_name("type") else { return false };
+    if type_node.kind() != "class_specifier" && type_node.kind() != "struct_specifier" {
+        return false;
+    }
+    let has_body = (0..type_node.named_child_count())
+        .filter_map(|i| type_node.named_child(i))
+        .any(|c| c.kind() == "field_declaration_list");
+    if has_body {
+        return false;
+    }
+    if let Some(declarator) = node.child_by_field_name("declarator") {
+        if declarator.kind() == "function_declarator" {
+            return false;
+        }
+    }
+    true
+}
+
+/// hasFunctionAncestor (tree-sitter.ts:295).
+fn has_function_ancestor(node: Node) -> bool {
+    let mut p = node.parent();
+    while let Some(n) = p {
+        if n.kind() == "function_definition" {
+            return true;
+        }
+        p = n.parent();
+    }
+    false
+}
+
+fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
+    match s {
+        Some(s) => arena.put(s),
+        None => NONE_STR,
+    }
+}

+ 7 - 2
codegraph-kernel/src/langs.rs

@@ -15,8 +15,8 @@ use tree_sitter::Language;
 
 /// Languages this kernel binary can extract (reported by contractInfo;
 /// TS-side routing policy decides what actually routes).
-pub const LANGUAGES: [&str; 7] =
-    ["typescript", "tsx", "javascript", "jsx", "java", "python", "go"];
+pub const LANGUAGES: [&str; 9] =
+    ["typescript", "tsx", "javascript", "jsx", "java", "python", "go", "c", "cpp"];
 
 pub fn grammar_for(language: &str) -> Option<Language> {
     match language {
@@ -26,6 +26,11 @@ pub fn grammar_for(language: &str) -> Option<Language> {
         "java" => Some(tree_sitter_java::LANGUAGE.into()),
         "python" => Some(tree_sitter_python::LANGUAGE.into()),
         "go" => Some(tree_sitter_go::LANGUAGE.into()),
+        // `.metal`/`.cu`/`.cuh` map to language 'cpp' at detectLanguage, so the
+        // dialects ride this grammar too (their blanking pre-passes stay
+        // TS-side — the route point applies preParse before the kernel call).
+        "c" => Some(tree_sitter_c::LANGUAGE.into()),
+        "cpp" => Some(tree_sitter_cpp::LANGUAGE.into()),
         _ => None,
     }
 }

+ 2 - 0
codegraph-kernel/src/lib.rs

@@ -17,6 +17,7 @@
 #![deny(clippy::all)]
 
 mod buffers;
+mod ccpp;
 mod docstring;
 mod ids;
 mod go;
@@ -103,6 +104,7 @@ pub fn extract_file(file_path: String, content: String, language: String) -> Res
         "java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
         "python" => python::extract(&file_path, &content).map_err(Error::from_reason)?,
         "go" => go::extract(&file_path, &content).map_err(Error::from_reason)?,
+        "c" | "cpp" => ccpp::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
         _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
     };
     Ok(ExtractBuffers {

+ 65 - 12
docs/design/ccpp-kernel-port-checklist.md

@@ -1,17 +1,70 @@
 # C/C++ kernel port (R7a) — the bug-for-bug checklist
 
-**Status:** survey COMPLETE; grammars VENDORED + suite-green (2026-07-17):
-tree-sitter-c v0.24.2 (`b780e47`, parser.c `f2883ff9…`) + tree-sitter-cpp
-v0.23.4 (`f41e1a0`, parser.c `2a35a43b…`, scanner.c `cf60387d…`), built with
-ts-cli 0.25.10 from checked-in parser.c, in `src/extraction/wasm/` +
-VENDORED_WASM_LANGS. The walker PR adds the SAME-version crates + kernel
-grammar registry (kernel-grammar-parity then pins the alignment). Walker +
-gates not started.
-This is §0a-recipe step 1's output for c/cpp: every TS-side branch the walker
-must mirror, with file:line anchors into the reference implementation. Read it
-WITH `docs/design/rust-kernel-migration-plan.md` (§0a recipe, §5 gates).
-Companion walkers to crib structure from: `codegraph-kernel/src/tsjs/` (multi-
-dialect module), `java.rs`, `go.rs` (receiver QNs), `python.rs`.
+**Status: COMPLETE — walker SHIPPED + gates PASSED, c/cpp DEFAULT-ROUTED
+(2026-07-17).** Walker: `codegraph-kernel/src/ccpp/mod.rs` (one dual-language
+module, every branch below mirrored; its header comment lists the quirks).
+Grammars: tree-sitter-c v0.24.2 (`b780e47`, parser.c `f2883ff9…`) +
+tree-sitter-cpp v0.23.4 (`f41e1a0`, parser.c `2a35a43b…`, scanner.c
+`cf60387d…`), crates pinned `=exact` in Cargo.toml, wasm vendored from the
+same tags, kernel-grammar-parity green. preParse HOISTED to the route point
+(`preParsedSource` in src/extraction/kernel/index.ts — both tryKernelExtract
+and the raw bulk path), so no blanking ported to Rust.
+
+**Gate results (2026-07-17):**
+- Parity sweeps — **0 diffs on every compared file**: redis 592, git 790,
+  fmt 42, protobuf 925, ALS-Community (UE spot-check) 40 files byte-parity.
+- Full-init dump-diffs — **byte-identical** kernel-arm vs wasm-arm: redis
+  (131,921 dump lines), git (160,844), fmt (35,433), protobuf (780,620),
+  ALS (3,694).
+- Torture fixtures torture.c/.cpp/.hpp + CRLF variants + Metal/CUDA
+  hoist-parity + defer tests in `__tests__/kernel-ccpp-parity.test.ts`; new
+  preParse blanks unit-tested in extraction.test.ts; full suite green with
+  `CODEGRAPH_KERNEL_EXPECT=1`.
+- **Deferral-rate guard — CORRECTED BY MEASUREMENT** (the §4f pattern): the
+  <10% bar was calibrated on ts/java/py/go (0–0.42% parse-error incidence).
+  Macro-heavy C/C++ genuinely parses with errors at double-digit file rates
+  (final sweeps: als 9%, git 16.1%, redis 25.3%, protobuf 25.8%, fmt 42% —
+  fmt's template metaprogramming + `.operator[]`-in-decltype shapes are
+  grammar-inherent), and every erroring file defers BY POLICY. Measured with
+  the defer disabled (`CODEGRAPH_KERNEL_CCPP_ERROR_EXTRACT=1`, sweep-only
+  hatch): recovery-divergence is real (21/207 redis, 8/382 git, 9/31 fmt
+  erroring files extract differently across UTF-8/UTF-16), so the defer
+  stays. The sweep harness now takes `--max-deferral` (default 0.1; use 0.5
+  for c/cpp — a broken walker still trips it by deferring ~everything).
+- **Seven NEW/extended preParse blanks** cut real incidence (from 32%/52%
+  starting points; linux `kernel/`+`mm/` subtrees 79% → 58%), each
+  offset-preserving, TS-side, shared by both arms, and a graph-quality win
+  for the wasm path itself (git 7.1k → 13.3k nodes; the linux subtrees
+  2.4k → 7.2k): `#ifdef __cplusplus` guard bodies, lone macro lines
+  (`FMT_BEGIN_NAMESPACE`, `Q_OBJECT`), C statement iterator macros
+  (`list_for_each_entry(…) { }` and brace-less bodies), C trailing param
+  attrs (`int argc UNUSED`), the curated Linux/sparse annotation list
+  (`__init`/`__user`/… — structural matching is impossible there: `__u32
+  count` is shape-identical; parameterized `__printf(1,2)` guarded out) +
+  `container_of`'s type-keyword argument, leading attr macros extended to
+  cpp, and directive-line restore (stops the older blanks corrupting
+  `#define` lines).
+- **Defer-reuse (the linux-economics fix):** a deferred file used to pay the
+  pipeline three times — the worker's raw kernel try, extractFromSource's
+  kernel RE-try, then wasm with a third preParse. A one-slot defer memo in
+  the route point short-circuits the repeat kernel attempt and hands the
+  already-blanked source to the wasm fallback (`sourceIsPreParsed`). On
+  linux this + the annotation blanks took the kernel-arm parse-loop from
+  560s (WORSE than the 426s wasm arm) to **356s vs 435s wasm-arm (−18%)**,
+  and the 2c/6GB envelope to **19.1 min kernel-arm vs 22.9 min wasm-arm
+  (−17%)** with a RICHER graph (2,048,295 nodes / 6,406,933 edges; two
+  independent kernel-arm runs byte-same counts).
+- **Linux-scale dump gate:** kernel-arm and wasm-arm full graphs are
+  **byte-identical** — `dump-graph.mjs` over both 5.2GB DBs:
+  10,444,551 dump lines each, sha256 `cd4182e6…` on both. (Dumping a 2M-node
+  DB needs a big-heap host run — `node --max-old-space-size=16000 …
+  > file` then hash the FILE; the in-container 6GB heap OOMs and a straight
+  pipe at GB scale dies with ENOBUFS, both of which silently hash truncated
+  output as the empty stream.)
+
+The sections below are §0a-recipe step 1's output — every TS-side branch the
+walker mirrors, with file:line anchors (as of `705e501`). Read WITH
+`docs/design/rust-kernel-migration-plan.md` (§0a recipe, §5 gates).
 
 ## Architecture decisions (already made by the plan)
 

+ 48 - 26
docs/design/rust-kernel-migration-plan.md

@@ -62,26 +62,44 @@ them are the ORIGINAL plan and carry expectations that measurement later correct
       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%)**. The <10min-on-8c target
-      remains open; levers left, ranked: **R7a C/C++ port (parse 338s — the
-      last big rock)** > backpressure ~120s (checkpoint I/O floor) > E-scan/
-      settle/read-mapping (~70–90s each, approaching honest work) > the 8c
-      re-run formality (est. ~15.5min).
-- [ ] **R7a. C/C++ port** — STARTED 2026-07-17: the §0a-recipe step-1 survey is
-      COMPLETE — every TS branch, quirk, and helper is enumerated with line
-      anchors in **`docs/design/ccpp-kernel-port-checklist.md`** (read it FIRST;
-      it also fixes the architecture: preParse hoisted to the route point so no
-      blanking ports to Rust, Metal/CUDA stay wasm this round, one dual-lang
-      walker module). Next: grammars (upgrade+vendor from matched tags, suite
-      green BEFORE the walker), then the walker, then the §5 gate ladder.
-      Original scope note: biggest single-language effort; unlocks cg1212's parse
-      expectation (6.2m → ~1.5–2m, 23% of that wall) + CARLA/UE/llvm-class repos;
-      Metal + CUDA ride along (their blanking pre-passes stay TS-side — `preParse`
-      is offset-preserving and the route point can apply it before the kernel call;
-      see the T2 note in `src/extraction/kernel/index.ts`). Largest per-language
-      surface in tree-sitter.ts: namespace prefix stacks (#1291), local fn-pointer
-      tables (#932), operator calls (#1247), stack construction (#1035), macro
-      salvage + `.h` content detection (stays at detectLanguage, upstream — free).
+      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). The <10min-on-8c target remains open;
+      levers left, ranked: **C/C++ deferral cuts (58% of linux files still
+      defer to wasm — each recovered idiom moves parse toward the native
+      floor)** > backpressure ~120s (checkpoint I/O floor) > E-scan/settle/
+      read-mapping (~70–90s each, approaching honest work) > the 8c re-run
+      formality.
+- [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.
@@ -98,7 +116,8 @@ 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`) mirroring `TreeSitterExtractor`'s
+  (`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`
@@ -106,10 +125,13 @@ and has the current build deployed at `/app` (tree at `/work/linux`).
   (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;
-  `CODEGRAPH_KERNEL_LANGS` REPLACES the set; `CODEGRAPH_KERNEL=0` kills), and the
+  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).
+  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),
@@ -490,8 +512,8 @@ parity before porting the language.
 | 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 | Keep as TS pre-passes: `blankCppExportMacros`/`blankCppInlineMacros` (UE `class MACRO Name` phantom-function misparse, #1096–#1102, CARLA 440→6), in-body reflection collapse guard (#1206), content-based `.h` C-vs-C++ detection. | ☐ |
-| metal, cuda | dialects over the cpp grammar | **T2** (rides c/cpp) | crates.io (cpp) | README-listed as first-class languages. Both are dialect-gated cpp: Metal = specifier/`[[attribute]]` blanking (#1121, the preParse-takes-filePath pattern); CUDA = `<<<>>>` blanking + content-gated `.h` (#1172). Their pre-passes must run before the kernel parse or stay TS-side; gate them WITH the c/cpp port, not separately. | ☐ |
+| 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. | ☐ |

+ 5 - 0
scripts/build-kernel.sh

@@ -78,5 +78,10 @@ esac
 
 DEST="$CRATE/prebuilds/$PLATFORM"
 mkdir -p "$DEST"
+# rm first so the copy lands on a FRESH inode: overwriting a signed dylib in
+# place leaves macOS's per-inode signature cache stale, and every process
+# that then dlopens the staged .node is SIGKILLed at load (the on-disk
+# signature still verifies, which makes it maddening to diagnose).
+rm -f "$DEST/codegraph-kernel.node"
 cp "$LIB" "$DEST/codegraph-kernel.node"
 echo "[kernel] staged $DEST/codegraph-kernel.node ($(du -h "$DEST/codegraph-kernel.node" | cut -f1))"

+ 40 - 11
scripts/kernel-parity.mjs

@@ -10,7 +10,14 @@
  *
  * Usage:
  *   node scripts/kernel-parity.mjs <file-or-dir>... [--lang typescript,tsx]
- *        [--max-samples N] [--list-files]
+ *        [--max-samples N] [--list-files] [--max-deferral 0.1]
+ *
+ * --max-deferral: the broken-kernel backstop (default 0.1). For C/C++ pass
+ * 0.5: macro-heavy C/C++ trees genuinely parse with errors at 10–40% file
+ * rates even after the preParse blanking family (git 19%, protobuf 26%, fmt
+ * 42% — measured 2026-07-17), and every erroring file defers BY POLICY, so
+ * the 10% bar calibrated on the 0–0.4% incidence of ts/java/py/go would fail
+ * healthy sweeps. A broken walker still trips 0.5 (it defers ~everything).
  *
  * Requires: npm run build (dist/) and a staged kernel (npm run build:kernel).
  * Exit code: 0 = parity, 1 = diffs found, 2 = setup error.
@@ -28,10 +35,12 @@ const paths = [];
 let langFilter = null;
 let maxSamples = 5;
 let listFiles = false;
+let maxDeferral = 0.1;
 for (let i = 0; i < args.length; i++) {
   if (args[i] === '--lang') langFilter = new Set(args[++i].split(','));
   else if (args[i] === '--max-samples') maxSamples = Number(args[++i]);
   else if (args[i] === '--list-files') listFiles = true;
+  else if (args[i] === '--max-deferral') maxDeferral = Number(args[++i]);
   else paths.push(args[i]);
 }
 if (paths.length === 0) {
@@ -39,11 +48,16 @@ if (paths.length === 0) {
   process.exit(2);
 }
 
-const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go']);
+const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', 'c', 'cpp']);
 const EXTS = new Map([
   ['.ts', 'typescript'], ['.mts', 'typescript'], ['.cts', 'typescript'],
   ['.tsx', 'tsx'], ['.js', 'javascript'], ['.mjs', 'javascript'],
   ['.cjs', 'javascript'], ['.jsx', 'jsx'], ['.java', 'java'], ['.py', 'python'], ['.pyw', 'python'], ['.go', 'go'],
+  // C/C++ (R7a). `.h` needs CONTENT sniffing (C vs C++) — resolved per file
+  // in the run loop via detectLanguage, matching the real indexer's routing.
+  ['.c', 'c'], ['.h', 'detect'],
+  ['.cpp', 'cpp'], ['.cc', 'cpp'], ['.cxx', 'cpp'], ['.hpp', 'cpp'], ['.hxx', 'cpp'],
+  ['.metal', 'cpp'], ['.cu', 'cpp'], ['.cuh', 'cpp'],
 ]);
 
 /** Collect candidate files. */
@@ -60,7 +74,12 @@ function collect(p, out) {
     for (const e of fs.readdirSync(p)) collect(path.join(p, e), out);
   } else if (EXTS.has(path.extname(p))) {
     const lang = EXTS.get(path.extname(p));
-    if (!langFilter || langFilter.has(lang)) out.push({ file: p, lang });
+    // 'detect' (.h) resolves per file in the run loop; under --lang it rides
+    // along whenever either C-family language is requested.
+    const passes =
+      !langFilter ||
+      (lang === 'detect' ? langFilter.has('c') || langFilter.has('cpp') : langFilter.has(lang));
+    if (passes) out.push({ file: p, lang });
   }
 }
 
@@ -73,7 +92,7 @@ if (files.length === 0) {
 
 // --- load the built engine ---------------------------------------------------
 const { extractFromSource } = await import(dist('extraction/tree-sitter.js'));
-const { initGrammars, loadGrammarsForLanguages } = await import(dist('extraction/grammars.js'));
+const { initGrammars, loadGrammarsForLanguages, detectLanguage } = await import(dist('extraction/grammars.js'));
 const kernel = await import(dist('extraction/kernel/index.js'));
 
 await initGrammars();
@@ -156,13 +175,19 @@ function report(category, sample) {
 let filesWithDiffs = 0;
 let filesOk = 0;
 let deferred = 0;
+let processed = 0; // collected files minus content-detect skips
 let totals = { nodes: 0, edges: 0, refs: 0 };
 
 process.env.CODEGRAPH_KERNEL_LANGS = 'all';
 
-for (const { file, lang } of files) {
+for (const { file, lang: extLang } of files) {
   const source = fs.readFileSync(file, 'utf8');
   const rel = path.relative(ROOT, file);
+  // `.h` resolves C vs C++ by content — the same call the indexer makes.
+  const lang = extLang === 'detect' ? detectLanguage(rel, source) : extLang;
+  if (!KERNEL_LANGS.has(lang)) continue;
+  if (langFilter && !langFilter.has(lang)) continue;
+  processed++;
 
   delete process.env.CODEGRAPH_KERNEL; // kernel path on
   const kres = kernel.tryKernelExtract(rel, source, lang);
@@ -222,7 +247,7 @@ for (const { file, lang } of files) {
   }
 }
 
-console.log(`\n=== kernel parity: ${filesOk}/${files.length} files byte-parity` +
+console.log(`\n=== kernel parity: ${filesOk}/${processed} files byte-parity` +
   ` (${filesWithDiffs} with diffs, ${deferred} deferred-to-wasm)` +
   ` | wasm totals: ${totals.nodes} nodes / ${totals.edges} edges / ${totals.refs} refs ===\n`);
 
@@ -232,11 +257,15 @@ for (const [cat, { count, samples }] of sorted) {
   for (const s of samples) console.log(`    ${s.length > 400 ? s.slice(0, 400) + '…' : s}`);
 }
 
-// Deferrals are per-file parse-error routing (expected, rare). A high rate
-// means the kernel is broken and hiding behind the fallback — fail loudly.
-const deferralRate = deferred / files.length;
-if (deferralRate > 0.1) {
-  console.error(`deferral rate ${(deferralRate * 100).toFixed(1)}% exceeds 10% — kernel likely broken`);
+// Deferrals are per-file parse-error routing (expected; rare for most
+// languages, routine for macro-heavy C/C++ — see --max-deferral above). A
+// rate past the threshold means the kernel is broken and hiding behind the
+// fallback — fail loudly.
+const deferralRate = deferred / Math.max(processed, 1);
+if (deferralRate > maxDeferral) {
+  console.error(
+    `deferral rate ${(deferralRate * 100).toFixed(1)}% exceeds ${(maxDeferral * 100).toFixed(0)}% — kernel likely broken`
+  );
   process.exit(1);
 }
 process.exit(filesWithDiffs > 0 ? 1 : 0);

+ 68 - 8
src/extraction/kernel/index.ts

@@ -15,6 +15,7 @@
  */
 
 import type { ExtractionResult, Language } from '../../types';
+import { EXTRACTORS } from '../languages';
 import { getKernel, kernelSupports } from './loader';
 import { decodeExtractBuffers } from './decode';
 import {
@@ -41,6 +42,12 @@ const DEFAULT_ROUTED: ReadonlySet<Language> = new Set<Language>([
   'java',
   'python',
   'go',
+  // R7a (2026-07-17): parity swept 0-diff on redis/git/fmt/protobuf/ALS
+  // (2,389 files compared) + full-init dump-diffs byte-identical; erroring
+  // files defer per-file to wasm (routine for macro-heavy C/C++ — see
+  // scripts/kernel-parity.mjs --max-deferral).
+  'c',
+  'cpp',
 ]);
 
 /**
@@ -55,6 +62,22 @@ const POST_PASSES: Partial<Record<Language, KernelPostPass>> = {
   // (none yet — R2+)
 };
 
+/**
+ * The preParse hoist (checklist §arch-1): languages with an offset-preserving
+ * `preParse` hook (c/cpp macro blanking, csharp #237, metal #1121, cuda #1172)
+ * apply it HERE, before the kernel call, so both arms parse identical blanked
+ * bytes and none of the blanking logic needs a Rust port. The wasm fallback
+ * path is untouched — TreeSitterExtractor applies the same hook itself on the
+ * RAW source it receives, so a kernel error/defer still extracts identically.
+ * Every blank is an equal-length-space replacement, so offsets, lines, and
+ * columns survive; `filePath` rides along for the extension-gated dialect
+ * blanks (`.metal` attributes; `.cu`/`.cuh` + content-gated CUDA).
+ */
+function preParsedSource(filePath: string, source: string, language: Language): string {
+  const pre = EXTRACTORS[language]?.preParse;
+  return pre ? pre(source, filePath) : source;
+}
+
 function isRouted(language: Language): boolean {
   const env = process.env.CODEGRAPH_KERNEL_LANGS;
   if (env === undefined || env === '') return DEFAULT_ROUTED.has(language);
@@ -73,6 +96,37 @@ export function kernelRoutes(language: Language): boolean {
 /** Warned-once registry so a broken language logs a single line, not one per file. */
 const warned = new Set<string>();
 
+/**
+ * One-slot defer memo. A file the kernel defers (parse errors → wasm) used to
+ * pay the full pipeline again at every seam: the worker's raw try blanked +
+ * native-parsed it, extractFromSource's kernel try blanked + native-parsed it
+ * AGAIN, and the wasm extractor then re-applied preParse a third time. On a
+ * high-deferral tree (the Linux kernel defers ~79% of files) that waste
+ * dominated the arm's parse phase. The slot remembers the LAST deferred
+ * (file, source, language) so (a) a repeat kernel attempt for the same file
+ * short-circuits to null, and (b) the wasm fallback can reuse the
+ * already-blanked source instead of re-running preParse. Source is matched by
+ * string identity — the worker passes the same string through every seam.
+ */
+let deferSlot: { filePath: string; source: string; language: Language; pre: string } | null = null;
+
+/** The hoisted preParse output for a just-deferred file, if it matches. */
+export function takeDeferredPreParse(
+  filePath: string,
+  source: string,
+  language: Language
+): string | null {
+  if (
+    deferSlot &&
+    deferSlot.filePath === filePath &&
+    deferSlot.source === source &&
+    deferSlot.language === language
+  ) {
+    return deferSlot.pre;
+  }
+  return null;
+}
+
 /** The raw table buffers + the cheap facts the orchestrator needs pre-decode. */
 export interface KernelRawResult {
   buffers: NonNullable<ExtractionResult['kernelBuffers']>;
@@ -96,8 +150,10 @@ export function tryKernelExtractRaw(
   if (!kernelRoutes(language) || POST_PASSES[language]) return null;
   const kernel = getKernel();
   if (!kernel) return null;
+  if (takeDeferredPreParse(filePath, source, language) !== null) return null; // already deferred
+  const pre = preParsedSource(filePath, source, language);
   try {
-    const buffers = kernel.extractFile(filePath, source, language);
+    const buffers = kernel.extractFile(filePath, pre, language);
     const meta = buffers.meta;
     if (meta.readUInt8(LAYOUT_META.version) !== LAYOUT_ABI) {
       throw new Error(`kernel buffer ABI ${meta.readUInt8(0)} != expected ${LAYOUT_ABI}`);
@@ -118,7 +174,10 @@ export function tryKernelExtractRaw(
     return { buffers, counts, errors };
   } catch (err) {
     const message = err instanceof Error ? err.message : String(err);
-    if (message.includes('defer:')) return null;
+    if (message.includes('defer:')) {
+      deferSlot = { filePath, source, language, pre };
+      return null;
+    }
     if (!warned.has(language)) {
       warned.add(language);
       process.stderr.write(
@@ -166,13 +225,11 @@ export function tryKernelExtract(
   if (!kernelRoutes(language)) return null;
   const kernel = getKernel();
   if (!kernel) return null;
+  if (takeDeferredPreParse(filePath, source, language) !== null) return null; // already deferred
   const t0 = Date.now();
+  const pre = preParsedSource(filePath, source, language);
   try {
-    // NOTE(T2 languages): when a preParse-carrying language (csharp #237,
-    // metal #1121, cuda #1172, c/cpp macro blanking) routes here, its
-    // offset-preserving preParse hook must be applied to `source` first —
-    // wire that alongside the language's port, gated WITH its equivalence run.
-    const buffers = kernel.extractFile(filePath, source, language);
+    const buffers = kernel.extractFile(filePath, pre, language);
     const result = decodeExtractBuffers(buffers, filePath, language);
     POST_PASSES[language]?.(result, source);
     result.durationMs = Date.now() - t0;
@@ -182,7 +239,10 @@ export function tryKernelExtract(
     // `defer:` is the kernel's expected-routing signal (files with parse
     // errors take the wasm path — its error RECOVERY is the canonical one;
     // recovery differs between UTF-8 and UTF-16 parsing). Silent by design.
-    if (message.includes('defer:')) return null;
+    if (message.includes('defer:')) {
+      deferSlot = { filePath, source, language, pre };
+      return null;
+    }
     if (!warned.has(language)) {
       warned.add(language);
       process.stderr.write(

+ 357 - 14
src/extraction/languages/c-cpp.ts

@@ -520,6 +520,63 @@ export function blankCppAnnotationMacroCalls(source: string): string {
   return chars.join('');
 }
 
+/**
+ * Blank a macro that is the ONLY token on its line — no parens, no semicolon:
+ * namespace-management macros (`FMT_BEGIN_NAMESPACE`, `FMT_END_EXPORT`,
+ * `JEMALLOC_DIAGNOSTIC_DISABLE_SPURIOUS`), Qt's `Q_OBJECT`, and friends. A
+ * bare identifier is not a statement or declaration in C or C++, so
+ * tree-sitter drops into error recovery at every one — and since the kernel
+ * path defers ANY erroring file to wasm, this single idiom deferred 13/73 fmt
+ * files and a comparable share of jemalloc, forfeiting the native-parse win
+ * on exactly the header-heavy trees it targets (the wasm path also mis-nests
+ * scopes around them today). Replacing the token with equal-length spaces
+ * preserves every byte offset and the surrounding declarations parse clean.
+ *
+ * Matched tightly so a real identifier can never be touched — ALL of:
+ *  - the line consists of ONE ALL-CAPS token (≥4 chars, with `_`), optionally
+ *    followed by a same-line comment — a lone lowercase identifier or any
+ *    second token disqualifies;
+ *  - the PREVIOUS non-blank line does not end in a continuation character
+ *    (`=`, an operator, `,`, `(`, `?`, `:`, or a `\` macro-definition
+ *    continuation) — so an ALL-CAPS operand split onto its own line inside a
+ *    multi-line expression (`int x =\n  SOME_CONST\n  | OTHER;`) is left
+ *    alone; and
+ *  - the NEXT non-blank line starts like a declaration/scope token
+ *    (letter, `_`, `#`, `{`, `}`, or `~`) or the file ends — an operator,
+ *    string literal, or `;` continuation rejects the match.
+ * Shared by C and C++ (the idiom is identical in both).
+ */
+const LONE_MACRO_LINE_RE = /^[ \t]*([A-Z][A-Z0-9_]{3,})[ \t]*(?:\/\/[^\n\r]*|\/\*[^\n\r]*\*\/[ \t]*)?\r?$/;
+const LONE_MACRO_CONTINUATION_END_RE = /[=+\-*/%&|^<>?:,(\\]$/;
+export function blankLoneMacroLines(source: string): string {
+  if (!/^[ \t]*[A-Z][A-Z0-9_]{3,}[ \t]*\r?$/m.test(source)) return source;
+  const lines = source.split('\n');
+  const content = (l: string): string => l.replace(/\r$/, '').trim();
+  let changed = false;
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i] as string;
+    const m = LONE_MACRO_LINE_RE.exec(line);
+    if (!m) continue;
+    // Underscore requirement rides the macro convention (FMT_BEGIN_NAMESPACE,
+    // Q_OBJECT); a solid all-caps word (`NDEBUG`-style) alone is too risky.
+    if (!(m[1] as string).includes('_')) continue;
+    let prev = i - 1;
+    while (prev >= 0 && content(lines[prev] as string) === '') prev--;
+    if (prev >= 0 && LONE_MACRO_CONTINUATION_END_RE.test(content(lines[prev] as string))) continue;
+    let next = i + 1;
+    while (next < lines.length && content(lines[next] as string) === '') next++;
+    if (next < lines.length) {
+      const first = content(lines[next] as string)[0];
+      if (!first || !/[A-Za-z_#{}~]/.test(first)) continue;
+    }
+    const start = line.indexOf(m[1] as string);
+    lines[i] =
+      line.slice(0, start) + ' '.repeat((m[1] as string).length) + line.slice(start + (m[1] as string).length);
+    changed = true;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
 /**
  * Blank an export/visibility macro sitting in front of a *member* or *method*
  * declaration inside a class/namespace (`ENGINE_API virtual void Tick(…)`,
@@ -689,24 +746,64 @@ function looksLikeCudaSource(source: string): boolean {
   );
 }
 
+/**
+ * Restore preprocessor-directive lines to their original bytes after the
+ * blanking passes ran. The token-level blanks match on shape, not context, so
+ * a macro name that happens to sit inside a DIRECTIVE gets blanked too — and
+ * blanking the name position of `#define FMT_API FMT_VISIBILITY("default")`
+ * leaves a nameless `#  define        FMT_VISIBILITY(…)`, which is a parse
+ * ERROR (fmt's base.h carries several). Inside a directive the blanks were
+ * never useful anyway: tree-sitter stores `#define` bodies as raw
+ * preproc_arg text it doesn't parse, so blanking there can only ever break
+ * the directive itself. Copying the original directive lines back (including
+ * `\`-continuation lines of multi-line defines) is offset-preserving by
+ * construction and strictly reduces parse errors on both extraction arms.
+ */
+function restoreDirectiveLines(original: string, blanked: string): string {
+  if (blanked === original || original.indexOf('#') === -1) return blanked;
+  const o = original.split('\n');
+  const b = blanked.split('\n');
+  let changed = false;
+  let continuation: boolean = false;
+  for (let i = 0; i < o.length && i < b.length; i++) {
+    const line = o[i] as string;
+    const isDirective: boolean = continuation || /^[ \t]*#/.test(line);
+    if (isDirective && b[i] !== line) {
+      b[i] = line;
+      changed = true;
+    }
+    continuation = isDirective && /\\\s*$/.test(line.replace(/\r$/, ''));
+  }
+  return changed ? b.join('\n') : blanked;
+}
+
 /** C/C++ source pre-processing before tree-sitter: recover macro-annotated class
  * definitions, macro-prefixed function definitions, macro-prefixed members, and
  * macro-decorated members (Unreal-Engine reflection markup) — plus the non-C++
  * surface of the dialects parsed with the C++ grammar: `.metal` MSL attribute
  * annotations, and CUDA specifiers + launch syntax (by `.cu`/`.cuh` extension
- * or by content, for CUDA living in `.h`/`.hpp` headers). Offset-preserving. */
+ * or by content, for CUDA living in `.h`/`.hpp` headers). Offset-preserving;
+ * directive lines are restored at the end (see restoreDirectiveLines). */
 function preParseCppSource(source: string, filePath?: string): string {
-  const blanked = blankCppAnnotationMacroCalls(
-    blankCppInlineAnnotationMacros(
-      blankCppApiPrefixMacros(blankCppInlineMacros(blankCppExportMacros(source)))
+  // blankCLeadingAttrMacros runs AFTER the api-prefix blank so a stacked
+  // `FMT_NORETURN FMT_API void f(…)` reduces to the `MACRO Ret name(` shape
+  // it matches (the _API token is already spaces by then).
+  let blanked = blankLoneMacroLines(
+    blankCLeadingAttrMacros(
+      blankCppAnnotationMacroCalls(
+        blankCppInlineAnnotationMacros(
+          blankCppApiPrefixMacros(blankCppInlineMacros(blankCppExportMacros(source)))
+        )
+      )
     )
   );
   const lower = filePath ? filePath.toLowerCase() : '';
-  if (lower.endsWith('.metal')) return blankMetalAttributes(blanked);
-  if (lower.endsWith('.cu') || lower.endsWith('.cuh') || looksLikeCudaSource(source)) {
-    return blankCudaConstructs(blanked);
+  if (lower.endsWith('.metal')) {
+    blanked = blankMetalAttributes(blanked);
+  } else if (lower.endsWith('.cu') || lower.endsWith('.cuh') || looksLikeCudaSource(source)) {
+    blanked = blankCudaConstructs(blanked);
   }
-  return blanked;
+  return restoreDirectiveLines(source, blanked);
 }
 
 /**
@@ -743,13 +840,259 @@ export function blankCLeadingAttrMacros(source: string): string {
   );
 }
 
-/** C source pre-processing: recover functions hidden behind a leading
- * attribute macro (#1211), then — for C-detected headers in CUDA projects
- * (llm.c keeps `__device__` helpers and kernel prototypes in plain `.h`) —
- * the same content-gated CUDA blank as C++. Offset-preserving. */
+/**
+ * Blank the body of `#ifdef __cplusplus … #endif` guard regions in C sources.
+ * The ubiquitous C-header compatibility idiom
+ *
+ *   #ifdef __cplusplus
+ *   extern "C" {
+ *   #endif
+ *
+ * is NOT valid C — `extern "C" {` (and any other C++-only line under the
+ * guard) drops tree-sitter-c into error recovery, so effectively every public
+ * C header carries parse errors. The wasm path shrugs (recovery keeps the
+ * rest); the kernel path defers EVERY erroring file to wasm by policy — so
+ * this one idiom pushed C-header deferral to ~32% on redis (vs the <10%
+ * gate) and forfeited the native-parse win exactly where C repos have the
+ * most files. A C compiler never sees the guarded lines (`__cplusplus` is
+ * only defined for C++), so blanking the region BODY mirrors the
+ * preprocessor's own view of the file.
+ *
+ * Matched conservatively, line-based and offset-preserving:
+ *  - the opener must be `#ifdef __cplusplus` / `#if defined(__cplusplus)`;
+ *  - the body may contain NO other preprocessor directive (a nested `#if`,
+ *    `#else`, or `#define` bails the whole region — those need real
+ *    preprocessing, so the file keeps its current behavior);
+ *  - the region must close with `#endif` within a few lines (guards are
+ *    tiny; a giant region is something else).
+ * The `#ifdef`/`#endif` directive lines themselves are kept — an empty
+ * preproc_ifdef parses clean — and every blanked byte becomes a space with
+ * `\r` preserved, so offsets, lines, and columns survive on CRLF checkouts.
+ */
+const C_CPLUSPLUS_GUARD_OPEN_RE =
+  /^[ \t]*#[ \t]*(?:ifdef[ \t]+__cplusplus\b|if[ \t]+defined[ \t]*\(?[ \t]*__cplusplus[ \t]*\)?)/;
+const C_PREPROC_DIRECTIVE_RE = /^[ \t]*#/;
+const C_PREPROC_ENDIF_RE = /^[ \t]*#[ \t]*endif\b/;
+const C_CPLUSPLUS_GUARD_MAX_BODY_LINES = 40;
+export function blankCCplusplusGuardBodies(source: string): string {
+  if (source.indexOf('__cplusplus') === -1) return source;
+  const lines = source.split('\n');
+  const stripCr = (l: string): string => (l.endsWith('\r') ? l.slice(0, -1) : l);
+  let changed = false;
+  for (let i = 0; i < lines.length; i++) {
+    if (!C_CPLUSPLUS_GUARD_OPEN_RE.test(stripCr(lines[i] as string))) continue;
+    let end = -1;
+    for (let j = i + 1; j < lines.length && j - i - 1 <= C_CPLUSPLUS_GUARD_MAX_BODY_LINES; j++) {
+      const line = stripCr(lines[j] as string);
+      if (C_PREPROC_ENDIF_RE.test(line)) {
+        end = j;
+        break;
+      }
+      if (C_PREPROC_DIRECTIVE_RE.test(line)) break; // nested directive — bail
+    }
+    if (end < 0) continue;
+    for (let k = i + 1; k < end; k++) {
+      lines[k] = (lines[k] as string).replace(/[^\r]/g, ' ');
+    }
+    changed = true;
+    i = end;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
+/**
+ * Blank a C iterator-macro call in STATEMENT position — `ql_foreach(iter,
+ * &arena->tcache_ql, link) { … }` (jemalloc), `for_each_string_list_item(item,
+ * &list) { … }` (git), `list_for_each_entry(pos, head, member) { … }` (the
+ * Linux kernel's core iteration idiom). A call followed by a brace block is
+ * not a C statement, so tree-sitter-c drops into error recovery at every use —
+ * these macros are the single largest source of parse errors in macro-heavy C
+ * trees (git: ~39% of files error; the kernel path defers each one to wasm).
+ * Blanking JUST the macro call leaves the brace block as a bare compound
+ * statement — valid C — so the body's calls/locals extract normally on both
+ * arms instead of riding error recovery.
+ *
+ * C-ONLY, and matched tightly:
+ *  - the call must be INDENTED (statement position; file-scope definitions
+ *    start at column 0, and an unbraced file-scope `name(args) { }` is a
+ *    valid implicit-int function definition that must not be touched);
+ *  - lowercase-led identifier (iterator macros are lowercase by convention;
+ *    this also excludes constructors if the file is really C++) that is not a
+ *    control keyword;
+ *  - the parens balance ON the line (string literals skipped), and after
+ *    them only `{` or end-of-line may follow — a `;` (a real call statement),
+ *    an operator, or any other token disqualifies;
+ *  - when the line ends at `)`, the NEXT non-blank line must begin with `{`.
+ * C++ deliberately does NOT get this pass: an indented snake_case
+ * constructor (`basic_string_view(const Char* s) : … {`) is exactly this
+ * shape, and blanking it would corrupt every STL-style class.
+ */
+const C_STMT_MACRO_KEYWORDS = new Set([
+  'if', 'while', 'for', 'switch', 'return', 'do', 'else', 'sizeof',
+]);
+export function blankCStatementMacroCalls(source: string): string {
+  const lines = source.split('\n');
+  let changed = false;
+  const content = (l: string): string => l.replace(/\r$/, '').trim();
+  for (let i = 0; i < lines.length; i++) {
+    const line = lines[i] as string;
+    const m = /^[ \t]+([a-z_][a-z0-9_]*)[ \t]*\(/.exec(line);
+    if (!m || C_STMT_MACRO_KEYWORDS.has(m[1] as string)) continue;
+    const open = line.indexOf('(', m[0].length - 1);
+    let depth = 0;
+    let close = -1;
+    for (let k = open; k < line.length; k++) {
+      const ch = line[k];
+      if (ch === '"' || ch === "'") {
+        const quote = ch;
+        k++;
+        while (k < line.length && line[k] !== quote) {
+          if (line[k] === '\\') k++;
+          k++;
+        }
+        continue;
+      }
+      if (ch === '(') depth++;
+      else if (ch === ')') {
+        depth--;
+        if (depth === 0) {
+          close = k;
+          break;
+        }
+      }
+    }
+    if (close < 0) continue; // parens don't balance on the line
+    const after = line.slice(close + 1).replace(/\r$/, '').trim();
+    if (after === '') {
+      // Brace on the next line (`ql_foreach(…)\n{`) or a brace-less
+      // single-statement body (`for_each_subsys(ss, i)\n\tstmt;` — blanking
+      // leaves the bare statement, valid C). A next line starting with an
+      // operator/string/`;` is an expression continuation — bail.
+      let next = i + 1;
+      while (next < lines.length && content(lines[next] as string) === '') next++;
+      if (next >= lines.length) continue;
+      const first = content(lines[next] as string)[0];
+      if (!first || !/[A-Za-z_{]/.test(first)) continue;
+    } else if (after !== '{') {
+      continue;
+    }
+    const identStart = line.indexOf(m[1] as string);
+    lines[i] =
+      line.slice(0, identStart) +
+      ' '.repeat(close + 1 - identStart) +
+      line.slice(close + 1);
+    changed = true;
+  }
+  return changed ? lines.join('\n') : source;
+}
+
+/**
+ * Blank a trailing parameter-attribute macro — `int argc UNUSED,` /
+ * `struct repository *repo UNUSED)` — git's house style for
+ * `__attribute__((unused))` on nearly every callback parameter (and the same
+ * shape as `MAYBE_UNUSED`/`G_GNUC_UNUSED` elsewhere). tree-sitter-c can't
+ * parse a second identifier after the parameter name, so every such
+ * SIGNATURE drops into error recovery — the single largest deferral bucket
+ * on git (~150 files). Blanking the macro leaves an ordinary parameter.
+ *
+ * Matched tightly: an identifier, whitespace, then an ALL-CAPS ≥3-char token
+ * immediately before `,` or `)`. Two juxtaposed identifiers in that position
+ * have no other valid-C reading — in a CALL the would-be macro is preceded
+ * by `,`/`(`, an operator, or a literal, never by a bare identifier. C-only:
+ * C++ grammars accept more juxtapositions (user-defined suffixes, macro'd
+ * `final`/`override`), so cpp keeps its existing recovery there.
+ */
+const C_TRAILING_PARAM_ATTR_RE = /\b([A-Za-z_]\w*)([ \t]+)([A-Z][A-Z0-9_]{2,})(?=[ \t]*[,)])/g;
+export function blankCTrailingParamAttrMacros(source: string): string {
+  if (!C_TRAILING_PARAM_ATTR_RE.test(source)) {
+    C_TRAILING_PARAM_ATTR_RE.lastIndex = 0;
+    return source;
+  }
+  C_TRAILING_PARAM_ATTR_RE.lastIndex = 0;
+  return source.replace(
+    C_TRAILING_PARAM_ATTR_RE,
+    (_m, name: string, ws: string, macro: string) => name + ws + ' '.repeat(macro.length)
+  );
+}
+
+/**
+ * Blank the Linux-kernel/sparse declaration-annotation macros — `static int
+ * __init audit_init(void)`, `void __user *buf`, `__bpf_kfunc void f(…)`,
+ * `int x __ro_after_init;`. These lowercase double-underscore annotations sit
+ * between storage/type tokens and the declarator, a position tree-sitter-c
+ * can't reconcile, and they blanket the Linux tree: measured on the kernel's
+ * own `kernel/` + `mm/` subtrees, they are the largest single deferral cause
+ * (the `__init` family alone heads ~37% of erroring files).
+ *
+ * A structural match is IMPOSSIBLE here: `__u32 count` (a real typedef) and
+ * `__init foo` (an annotation) are byte-shape identical — so unlike the
+ * shape-keyed blanks above, this is a CURATED list (the CPP_INLINE_MACROS
+ * precedent) of well-known sparse/section/compiler annotations that are
+ * reserved-namespace macros in every codebase that spells them. Whole-word,
+ * equal-length spaces, C-only (the C++ grammar's kernel exposure is
+ * negligible and cpp keeps its narrower blank set).
+ */
+const C_KERNEL_ANNOTATIONS = [
+  '__init', '__exit', '__initdata', '__initconst', '__exitdata',
+  '__devinit', '__devexit', '__cpuinit', '__meminit', '__meminitdata',
+  '__net_init', '__net_exit', '__init_or_module',
+  '__user', '__kernel', '__iomem', '__percpu', '__rcu', '__force', '__nocast',
+  '__must_check', '__maybe_unused', '__always_unused', '__used', '__cold',
+  '__hot', '__weak', '__pure', '__sched', '__malloc', '__visible',
+  '__deprecated', '__ro_after_init', '__read_mostly', '__refdata',
+  '__latent_entropy', '__randomize_layout', '__no_randomize_layout',
+  '__bpf_kfunc', '__function_aligned', '__always_inline', '__noreturn',
+] as const;
+// `(?!\s*\()` keeps the parameterized annotations (`__printf(1, 2)`,
+// `__aligned(8)`, `__section("x")`) intact — blanking just their name would
+// strand the argument list as a floating parenthesis and CREATE an error.
+const C_KERNEL_ANNOTATION_RE = new RegExp(
+  `\\b(${[...C_KERNEL_ANNOTATIONS].sort((a, b) => b.length - a.length).join('|')})\\b(?!\\s*\\()`,
+  'g'
+);
+export function blankCKernelAnnotations(source: string): string {
+  if (source.indexOf('__') === -1) return source;
+  C_KERNEL_ANNOTATION_RE.lastIndex = 0;
+  if (!C_KERNEL_ANNOTATION_RE.test(source)) return source;
+  let out = source.replace(C_KERNEL_ANNOTATION_RE, (m) => ' '.repeat(m.length));
+  // `container_of(ptr, struct T, member)` — the type-keyword argument is the
+  // one call shape tree-sitter-c cannot read (a macro taking a TYPE), and it
+  // is pervasive across the Linux tree. Blanking just the `struct`/`union`
+  // keyword leaves `container_of(ptr,        T, member)` — a plain
+  // identifier argument the grammar parses natively. Keyed to the macro name
+  // so no other `struct` keyword anywhere is ever touched.
+  if (out.indexOf('container_of') !== -1) {
+    out = out.replace(
+      /(\bcontainer_of\s*\([^;()]*?,\s*)(struct|union)(\s+)/g,
+      (_m, head: string, kw: string, ws: string) => head + ' '.repeat(kw.length) + ws
+    );
+  }
+  return out;
+}
+
+/** C source pre-processing: neutralize `#ifdef __cplusplus` compat-guard
+ * bodies (invisible to a C compiler; `extern "C" {` otherwise errors every
+ * public header), blank declaration-markup macro calls and lone macro lines
+ * (`REDIS_NO_SANITIZE("bounds")` before a definition, jemalloc's diagnostic
+ * toggles — the same structural shapes the C++ side already blanks), recover
+ * functions hidden behind a leading attribute macro (#1211), then — for
+ * C-detected headers in CUDA projects (llm.c keeps `__device__` helpers and
+ * kernel prototypes in plain `.h`) — the same content-gated CUDA blank as
+ * C++. Offset-preserving. */
 function preParseCSource(source: string): string {
-  const blanked = blankCLeadingAttrMacros(source);
-  return looksLikeCudaSource(blanked) ? blankCudaConstructs(blanked) : blanked;
+  let blanked = blankCLeadingAttrMacros(
+    blankLoneMacroLines(
+      blankCStatementMacroCalls(
+        blankCTrailingParamAttrMacros(
+          blankCppAnnotationMacroCalls(
+            blankCKernelAnnotations(blankCCplusplusGuardBodies(source))
+          )
+        )
+      )
+    )
+  );
+  if (looksLikeCudaSource(blanked)) blanked = blankCudaConstructs(blanked);
+  return restoreDirectiveLines(source, blanked);
 }
 
 export const cppExtractor: LanguageExtractor = {

+ 25 - 6
src/extraction/tree-sitter.ts

@@ -30,7 +30,7 @@ import { DfmExtractor } from './dfm-extractor';
 import { VueExtractor } from './vue-extractor';
 import { MyBatisExtractor } from './mybatis-extractor';
 import { CfmlExtractor } from './cfml-extractor';
-import { tryKernelExtract } from './kernel';
+import { tryKernelExtract, takeDeferredPreParse } from './kernel';
 import {
   getAllFrameworkResolvers,
   getApplicableFrameworks,
@@ -429,13 +429,23 @@ export class TreeSitterExtractor {
   private fnRefCandidates: Array<FnRefCandidate & { fromNodeId: string }> = [];
   // Memoized "is this a Vue store file" verdict (per-extractor = per-file).
   private vueStoreFile: boolean | null = null;
-
-  constructor(filePath: string, source: string, language?: Language) {
+  // Source already went through the extractor's preParse at the kernel route
+  // point (this instance is the wasm fallback for a kernel-deferred file) —
+  // don't blank it a second time.
+  private sourceIsPreParsed = false;
+
+  constructor(
+    filePath: string,
+    source: string,
+    language?: Language,
+    options?: { sourceIsPreParsed?: boolean }
+  ) {
     this.filePath = filePath;
     this.source = source;
     this.language = language || detectLanguage(filePath, source);
     this.extractor = EXTRACTORS[this.language] || null;
     this.fnRefSpec = FN_REF_SPECS[this.language];
+    this.sourceIsPreParsed = options?.sourceIsPreParsed === true;
   }
 
   /**
@@ -484,8 +494,9 @@ export class TreeSitterExtractor {
       // grammar gaps — e.g. C# blanks conditional-compilation directive lines
       // the grammar mis-parses inside enum bodies (#237). We reassign
       // this.source so downstream getNodeText reads the same bytes the parser
-      // saw (identical outside the blanked directive lines).
-      if (this.extractor?.preParse) {
+      // saw (identical outside the blanked directive lines). Skipped when the
+      // kernel route point already applied it (sourceIsPreParsed).
+      if (this.extractor?.preParse && !this.sourceIsPreParsed) {
         this.source = this.extractor.preParse(this.source, this.filePath);
       }
       this.tree = parser.parse(this.source) ?? null;
@@ -6708,7 +6719,15 @@ export function extractFromSource(
     if (kernelResult) {
       result = kernelResult;
     } else {
-      const extractor = new TreeSitterExtractor(filePath, source, detectedLanguage);
+      // A kernel-deferred file already paid the (offset-preserving) preParse
+      // at the route point — reuse those bytes instead of blanking again.
+      const deferredPre = takeDeferredPreParse(filePath, source, detectedLanguage);
+      const extractor = new TreeSitterExtractor(
+        filePath,
+        deferredPre ?? source,
+        detectedLanguage,
+        { sourceIsPreParsed: deferredPre != null }
+      );
       result = extractor.extract();
     }
   }