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

Merge remote-tracking branch 'origin/main' into HEAD

Colby McHenry 1 неделя назад
Родитель
Сommit
8564106471
41 измененных файлов с 2176 добавлено и 247 удалено
  1. 7 0
      CHANGELOG.md
  2. 3 1
      README.md
  3. 108 0
      __tests__/cli-install-init.test.ts
  4. 140 0
      __tests__/extraction.test.ts
  5. 75 0
      __tests__/fixtures/kernel-parity/torture.rs
  6. 281 0
      __tests__/kernel-deep-nesting.test.ts
  7. 2 2
      __tests__/kernel-rustlang-parity.test.ts
  8. 287 0
      __tests__/resolution.test.ts
  9. 29 0
      __tests__/sync.test.ts
  10. 128 0
      __tests__/watcher.test.ts
  11. 1 0
      codegraph-kernel/Cargo.lock
  12. 6 0
      codegraph-kernel/Cargo.toml
  13. 12 0
      codegraph-kernel/src/ccpp/mod.rs
  14. 13 0
      codegraph-kernel/src/csharp.rs
  15. 8 0
      codegraph-kernel/src/dart.rs
  16. 9 0
      codegraph-kernel/src/go.rs
  17. 11 0
      codegraph-kernel/src/java.rs
  18. 10 0
      codegraph-kernel/src/kotlin.rs
  19. 37 17
      codegraph-kernel/src/lib.rs
  20. 6 0
      codegraph-kernel/src/lua.rs
  21. 12 0
      codegraph-kernel/src/php.rs
  22. 7 0
      codegraph-kernel/src/python.rs
  23. 6 0
      codegraph-kernel/src/rlang.rs
  24. 9 0
      codegraph-kernel/src/ruby.rs
  25. 80 60
      codegraph-kernel/src/rustlang.rs
  26. 12 0
      codegraph-kernel/src/scala.rs
  27. 273 0
      codegraph-kernel/src/stack.rs
  28. 15 0
      codegraph-kernel/src/swift.rs
  29. 5 0
      codegraph-kernel/src/tsjs/extractors.rs
  30. 3 0
      codegraph-kernel/src/tsjs/mod.rs
  31. 13 3
      docs/design/rust-kernel-migration-plan.md
  32. 25 17
      docs/design/rust-lang-kernel-port-checklist.md
  33. 110 79
      src/bin/codegraph.ts
  34. 54 2
      src/extraction/index.ts
  35. 43 26
      src/extraction/languages/rust.ts
  36. 6 0
      src/extraction/parse-pool.ts
  37. 35 32
      src/extraction/tree-sitter.ts
  38. 3 2
      src/installer/index.ts
  39. 15 0
      src/resolution/import-resolver.ts
  40. 224 0
      src/resolution/name-matcher.ts
  41. 53 6
      src/sync/watcher.ts

+ 7 - 0
CHANGELOG.md

@@ -23,6 +23,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 - A new `deprioritize` setting in `codegraph.json` keeps the paths you name from outranking your product code in search and `codegraph_explore` answers, without removing anything from the index. It takes gitignore-style patterns just like `exclude`, but is ranking-only: helper-script trees, generated output, or optional add-on directories whose generic symbol names (`usage`, `run`, `status`) would otherwise crowd out the code that actually answers a query stay fully indexed and findable — and a query that genuinely targets such a tree still returns it. Thanks @maxmilian. (#982)
 
+- `codegraph install --init` wires up your agents and builds the current project's index in one command, and `codegraph init --yes` runs without any prompts — so a fresh container or CI job can bootstrap CodeGraph with a single non-interactive line (`codegraph install --yes --init`). The installer still never indexes anything unless you ask for it with the flag, and the usual safety refusal for a home directory or filesystem root applies. (#1578)
+
 ### Fixes
 
 - Indexing no longer hangs on a Swift Vapor project containing a call with a long argument list. A single `.get(...)`-style call with many labeled arguments and no `use:` handler — the shape generated request builders produce — could stall `codegraph index`, `codegraph sync`, and the MCP server indefinitely. Route detection now handles such files in milliseconds, and every previously-recognized route shape still parses exactly as before. Thanks @maxmilian. (#1544) (Swift)
@@ -72,6 +74,11 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - C/C++ function-pointer analysis now bounds its compiled-pattern caches, so very large repositories can no longer exhaust the JavaScript engine's regular-expression code space during indexing. (#1559)
 - JSX rendering analysis now runs only on JavaScript-family files, so JSX-looking strings in C/C++ (or any other language) no longer create impossible call edges — in pure-C projects and in mixed-language monorepos alike. (#1560)
 - A C++ `.h` header whose only C++ construct is a plain derived type — `struct Derived : Base` with no export macro, `class` keyword, or access section — is now recognized as C++ (previously only the export-macro form was). Such a header was read as C, so the derived struct vanished from the index and a phantom function named after the base type appeared in its place. The check now also covers the whole file rather than its first few kilobytes, so a long C-compatible preamble no longer hides the signal. Re-index after upgrading to pick up affected headers. Thanks @Jaysenpeng. (#1592)
+- Editing `codegraph.json`'s `exclude` or `include` (or a `.gitignore`) while the MCP server is running now takes effect immediately. Previously the running file watcher kept the scope it had when it started, so a newly excluded file was removed by `codegraph sync` and then quietly re-added by the watcher seconds later — which looked like `exclude` not working at all — until the server was restarted. A scope change now refreshes the watcher and triggers a full reconcile, and a changed file the watcher hands to sync is re-checked against the current scope first, so the CLI and the live server can no longer disagree about what belongs in the index. Thanks @K1nG11. (#1590)
+- Indexing no longer crashes the whole process — a segmentation fault with no message and no partial index — on a C/C++ (or any other) file with extremely deep nesting, such as the parser stress-test fixtures in the clang and gcc test suites or a fuzzer corpus. Such a file is now handed to the fallback parser and recorded with a parse warning while the rest of the repository indexes normally. Thanks @apollo600 for the exact diagnosis. (#1581)
+- Calls to the methods of an exported object-literal constant — `export const api = { call() { … } }` used as a module's namespace, a common way to organize a TypeScript API surface — now resolve to the method, both in the defining file and through imports. Previously such a call linked to nothing (or to the constant itself), so `codegraph callers` and impact analysis reported zero callers for methods that are called from everywhere. Re-index after upgrading to pick up the edges. Thanks @IAliceBobI for the precise report and root-cause. (#1573)
+- Methods implemented in a generic or lifetime-parameterized `impl` block (`impl<T> Source for BufSource<T>`, `impl<'a> Iterator for Parents<'a>`) are now recorded under the implementing type instead of the trait. Previously such a method could not be found by its type — "who calls `BufSource::read`" had no answer — and it collided with the trait's own declaration, which could even invent a call-graph edge out of an impl body that contains no call at all. Impls on a reference (`impl Trait for &Foo`) and on a module-qualified type (`impl Trait for m::Foo`) are attributed to their type too. Re-index after upgrading. Thanks @Dshuishui. (#1588) (Rust)
+- A method call on a struct field — `self.inner.run()` with `inner: Inner` — now resolves to the method on the field's declared type. Previously the call was reduced to the bare method name and matched whichever same-named method was nearest, which was often the calling method itself, recording recursion that isn't in the source (a few hundred such self-edges in ripgrep alone), or a method of an unrelated type. References and `Box`/`Rc`/`Arc` fields are looked through, as Rust's own method calls are; a field whose type is external (a std or third-party type), a generic parameter, or a container like `Option`/`Vec` is left unresolved rather than guessed. Re-index after upgrading. Thanks @Dshuishui. (#1585) (Rust)
 
 ## [1.5.0] - 2026-07-21
 

+ 3 - 1
README.md

@@ -388,6 +388,7 @@ The installer **wires up your agents only — it does not index your code.** Aft
 
 ```bash
 codegraph install --yes                              # auto-detect agents, install global
+codegraph install --yes --init                       # same, then build the current project's index (one-shot bootstrap)
 codegraph install --target=cursor,claude --yes       # explicit target list
 codegraph install --target=auto --location=local     # detected agents, project-local
 codegraph install --target=copilot-vscode,copilot-cli,copilot-jetbrains --yes  # GitHub Copilot everywhere
@@ -400,6 +401,7 @@ codegraph install --print-config copilot-vscode      # same, for Copilot in VS C
 | `--target` | `auto`, `all`, `none`, or csv (`claude,cursor,...`) | prompt |
 | `--location` | `global`, `local` | prompt |
 | `--yes` | (boolean) | prompt every step |
+| `--init` | (boolean) run `codegraph init` in the current directory after wiring agents | — |
 | `--no-permissions` | (boolean) skip Claude auto-allow list | permissions on |
 | `--print-config <id>` | dump snippet for one agent and exit | — |
 
@@ -414,7 +416,7 @@ cd your-project
 codegraph init
 ```
 
-Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project.
+Builds the per-project knowledge graph index, which then auto-syncs on every file change. A single global `codegraph install` works in every project you open — no need to re-run the installer per project. Add `--yes` to skip every prompt (scripts / CI / container bootstraps).
 
 That's it — your agent will use CodeGraph tools automatically when a `.codegraph/` directory exists.
 

+ 108 - 0
__tests__/cli-install-init.test.ts

@@ -0,0 +1,108 @@
+/**
+ * `codegraph install --init` and `codegraph init --yes` (#1578): the one-shot,
+ * non-interactive "wire agents + build this project's index" bootstrap a fresh
+ * container / CI job needs.
+ *
+ * Exercised end-to-end against the built binary so the CLI wiring (the shared
+ * `runInit` flow, the flag plumbing, exit codes) is what's covered. Every run
+ * uses `--target none`, so the installer touches no agent config on the
+ * machine running the suite; the only side effect is the temp project's
+ * `.codegraph/`.
+ */
+
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { execFileSync } from 'child_process';
+import * as fs from 'fs';
+import * as path from 'path';
+import * as os from 'os';
+
+const BIN = path.resolve(__dirname, '../dist/bin/codegraph.js');
+
+interface RunResult {
+  status: number;
+  stdout: string;
+  stderr: string;
+}
+
+/** Run the CLI with stdin closed — a prompt that blocks would hang / fail here. */
+function runCodegraph(args: string[], cwd: string): RunResult {
+  try {
+    const stdout = execFileSync(process.execPath, [BIN, ...args], {
+      cwd,
+      encoding: 'utf-8',
+      env: {
+        ...process.env,
+        CODEGRAPH_NO_DAEMON: '1',
+        CODEGRAPH_TELEMETRY: '0',
+        DO_NOT_TRACK: '1',
+        NO_COLOR: '1',
+      },
+      stdio: ['ignore', 'pipe', 'pipe'],
+      timeout: 120_000,
+    });
+    return { status: 0, stdout, stderr: '' };
+  } catch (err) {
+    const e = err as { status?: number | null; stdout?: string | Buffer; stderr?: string | Buffer };
+    return {
+      status: e.status ?? -1,
+      stdout: String(e.stdout ?? ''),
+      stderr: String(e.stderr ?? ''),
+    };
+  }
+}
+
+describe('codegraph install --init / init --yes (#1578)', () => {
+  let tempDir: string;
+
+  beforeEach(() => {
+    tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-install-init-'));
+    fs.writeFileSync(
+      path.join(tempDir, 'a.ts'),
+      `export function greet(name: string) { return hello(name); }\n` +
+        `export function hello(n: string) { return 'hi ' + n; }\n`,
+    );
+  });
+
+  afterEach(() => {
+    fs.rmSync(tempDir, { recursive: true, force: true });
+  });
+
+  it('install --yes --target none --init builds the current project\'s index in one command', () => {
+    const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
+    expect(r.status, r.stdout + r.stderr).toBe(0);
+    // The installer ran (and had nothing to wire) …
+    expect(r.stdout).toContain('No agent targets selected');
+    // … and the init ran afterwards, in cwd.
+    expect(r.stdout).toContain(`Initialized in ${fs.realpathSync(tempDir)}`);
+    expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
+  });
+
+  it('install --init on an already-initialized project reports that and still exits 0', () => {
+    expect(runCodegraph(['init', '--yes'], tempDir).status).toBe(0);
+    const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], tempDir);
+    expect(r.status, r.stdout + r.stderr).toBe(0);
+    expect(r.stdout).toContain('Already initialized');
+  });
+
+  it('install --init refuses an unsafe root (filesystem root) with exit code 1, like init does', () => {
+    // `/` (or the drive root on Windows) is the canonical unsafe root: the
+    // refusal fires before anything is created, so nothing is written there.
+    const root = path.parse(process.cwd()).root;
+    const r = runCodegraph(['install', '--yes', '--target', 'none', '--init'], root);
+    expect(r.status).toBe(1);
+    expect(r.stdout).toContain('Refusing to initialize');
+    expect(fs.existsSync(path.join(root, '.codegraph'))).toBe(false);
+  });
+
+  it('init --yes runs non-interactively with stdin closed and builds the index', () => {
+    const r = runCodegraph(['init', '--yes'], tempDir);
+    expect(r.status, r.stdout + r.stderr).toBe(0);
+    expect(r.stdout).toContain('Initialized in');
+    expect(fs.existsSync(path.join(tempDir, '.codegraph', 'codegraph.db'))).toBe(true);
+  });
+
+  it('documents the new flags in --help', () => {
+    expect(runCodegraph(['init', '--help'], tempDir).stdout).toMatch(/-y, --yes\b/);
+    expect(runCodegraph(['install', '--help'], tempDir).stdout).toMatch(/-i, --init\b/);
+  });
+});

+ 140 - 0
__tests__/extraction.test.ts

@@ -1178,6 +1178,146 @@ impl Cache for MyCache {
     expect(implRef?.fromNodeId).toBe(myCacheNode?.id);
   });
 
+  it('qualifies methods of a generic or lifetime impl by the implementing type, not the trait (#1588)', () => {
+    const code = `
+pub trait Source {
+    fn read(&mut self) -> usize;
+}
+
+pub struct FileSource { pub n: usize }
+impl Source for FileSource {
+    fn read(&mut self) -> usize { self.n }
+}
+
+pub struct BufSource<T> { pub inner: T }
+impl<T> Source for BufSource<T> {
+    fn read(&mut self) -> usize { 0 }
+}
+
+pub struct Parents<'a> { cur: &'a u32 }
+impl<'a> Iterator for Parents<'a> {
+    type Item = u32;
+    fn next(&mut self) -> Option<u32> { None }
+}
+
+pub struct Wrapper { pub n: usize }
+impl Source for &Wrapper {
+    fn read(&mut self) -> usize { 1 }
+}
+
+pub mod m { pub struct Scoped { pub n: usize } }
+impl Source for m::Scoped {
+    fn read(&mut self) -> usize { 2 }
+}
+
+pub struct Own { pub n: usize }
+impl From<u32> for Own {
+    fn from(n: u32) -> Self { Own { n: n as usize } }
+}
+`;
+    const result = extractFromSource('src.rs', code);
+
+    // Every impl method is qualified by the IMPLEMENTING type. Before, a
+    // parameterized implementing type (`BufSource<T>`, `Parents<'a>`, `&Wrapper`)
+    // left the trait's identifier as the only bare type_identifier child of the
+    // impl, so those methods were recorded as `Source::read` / `Iterator::next`.
+    const methodQns = result.nodes
+      .filter((n) => n.kind === 'method')
+      .map((n) => n.qualifiedName)
+      .sort();
+    expect(methodQns).toEqual([
+      'BufSource::read',
+      'FileSource::read',
+      'Own::from',
+      'Parents::next',
+      'Scoped::read',
+      'Source::read',
+      'Wrapper::read',
+    ]);
+    // The trait's qualified name now names exactly one node: its declaration.
+    const traitRead = result.nodes.filter((n) => n.qualifiedName === 'Source::read');
+    expect(traitRead).toHaveLength(1);
+    expect(traitRead[0]!.startLine).toBe(3);
+
+    // The implements back-reference comes FROM the implementing type's node
+    // for every impl shape, named by the trait's full text.
+    const implementsFrom = (typeName: string): string[] => {
+      const typeNode = result.nodes.find((n) => n.name === typeName && n.kind === 'struct');
+      expect(typeNode, typeName).toBeDefined();
+      return result.unresolvedReferences
+        .filter((r) => r.referenceKind === 'implements' && r.fromNodeId === typeNode!.id)
+        .map((r) => r.referenceName);
+    };
+    expect(implementsFrom('FileSource')).toEqual(['Source']);
+    expect(implementsFrom('BufSource')).toEqual(['Source']);
+    expect(implementsFrom('Parents')).toEqual(['Iterator']);
+    expect(implementsFrom('Wrapper')).toEqual(['Source']);
+    expect(implementsFrom('Scoped')).toEqual(['Source']);
+    expect(implementsFrom('Own')).toEqual(['From<u32>']);
+
+    // …and the owner `contains` edge lands on the implementing type too.
+    const buf = result.nodes.find((n) => n.name === 'BufSource' && n.kind === 'struct')!;
+    const bufRead = result.nodes.find((n) => n.qualifiedName === 'BufSource::read')!;
+    expect(
+      result.edges.some((e) => e.kind === 'contains' && e.source === buf.id && e.target === bufRead.id)
+    ).toBe(true);
+  });
+
+  it('keeps the owner-field shape for `self.<field>.<method>()` and collapses every other receiver (#1585)', () => {
+    const code = `
+pub struct Outer { pub inner: Inner, pub deep: Deep }
+impl Outer {
+    pub fn run(&mut self) {
+        self.inner.run();
+        self.deep.inner.run();
+        self.make().run();
+        (self.inner).run();
+        self.run();
+        let local = Inner { n: 0 };
+        local.run();
+    }
+}
+`;
+    const result = extractFromSource('outer.rs', code);
+    const calls = result.unresolvedReferences
+      .filter((r) => r.referenceKind === 'calls')
+      .map((r) => r.referenceName);
+    // Exactly one call keeps the `self.<field>` prefix — the single-hop field
+    // receiver whose type the resolver can read off the owner struct.
+    expect(calls.filter((c) => c.startsWith('self.'))).toEqual(['self.inner.run']);
+    // A local receiver keeps its name as before…
+    expect(calls).toContain('local.run');
+    // …and the deeper chain, the call receiver, the parenthesized receiver and
+    // the bare `self` receiver all still collapse to the method name.
+    expect(calls.filter((c) => c === 'run')).toHaveLength(4);
+    expect(calls).toContain('make');
+    const outerRun = result.nodes.find((n) => n.qualifiedName === 'Outer::run');
+    expect(outerRun).toBeDefined();
+    const fieldRef = result.unresolvedReferences.find((r) => r.referenceName === 'self.inner.run');
+    expect(fieldRef?.fromNodeId).toBe(outerRun!.id);
+    expect(fieldRef?.line).toBe(5);
+  });
+
+  it('gives no receiver to an impl whose target names no single type', () => {
+    // A tuple / `dyn Trait` / primitive implementing type has no struct to
+    // hang the methods off, so they are extracted as plain functions — the
+    // pre-#1588 behavior for these shapes, minus the trait mis-qualification.
+    const code = `
+pub trait Base { fn id(&self) -> u32; }
+impl Base for (u32, u32) {
+    fn id(&self) -> u32 { 0 }
+}
+impl Base for dyn Base {
+    fn id(&self) -> u32 { 1 }
+}
+`;
+    const result = extractFromSource('src.rs', code);
+    const ids = result.nodes.filter((n) => n.name === 'id');
+    expect(ids.map((n) => n.qualifiedName).sort()).toEqual(['Base::id', 'id', 'id']);
+    expect(ids.filter((n) => n.kind === 'function')).toHaveLength(2);
+    expect(result.unresolvedReferences.filter((r) => r.referenceKind === 'implements')).toHaveLength(0);
+  });
+
   it('should extract trait supertraits as extends references', () => {
     const code = `
 pub trait Display {}

+ 75 - 0
__tests__/fixtures/kernel-parity/torture.rs

@@ -72,6 +72,16 @@ impl Widget {
         self.n * mul()
     }
 
+    /// Receiver shapes (#1585): only `self.<field>.<method>()` keeps the
+    /// owner-field prefix; deeper / parenthesized / call / bare-self collapse.
+    fn via_field(&self) -> u32 {
+        self.field.deep_call();
+        self.field.z.clone();
+        self.method_a().chain_b();
+        (self.field).deep_call();
+        self.area()
+    }
+
     fn clone_self(&self) -> Self {
         Self::assoc();
         Widget {
@@ -107,6 +117,71 @@ impl Render for Container<u32> {
     fn render(&self) {}
 }
 
+/// Receiver = the impl_item's `type` field (#1588): generic, lifetime,
+/// reference, scoped, and generic-trait impls all qualify by the TYPE.
+pub trait Source {
+    fn read(&mut self) -> usize;
+}
+
+pub struct FileSource {
+    pub n: usize,
+}
+
+impl Source for FileSource {
+    fn read(&mut self) -> usize {
+        self.n
+    }
+}
+
+pub struct BufSource<T> {
+    pub inner: T,
+}
+
+impl<T> Source for BufSource<T> {
+    fn read(&mut self) -> usize {
+        0
+    }
+}
+
+pub struct Parents<'a> {
+    cur: &'a u32,
+}
+
+impl<'a> Iterator for Parents<'a> {
+    type Item = u32;
+    fn next(&mut self) -> Option<u32> {
+        None
+    }
+}
+
+impl<T: Clone> Container<T> {
+    fn dup(&self) -> T {
+        self.item.clone()
+    }
+}
+
+impl Base for &Widget {}
+
+impl<T> Render for &mut BufSource<T> {
+    fn render(&self) {}
+}
+
+impl Base for self::Deep {}
+
+impl From<u32> for FileSource {
+    fn from(n: u32) -> Self {
+        FileSource { n: n as usize }
+    }
+}
+
+impl Base for (u32, u32) {}
+
+impl Render for dyn Base {
+    fn render(&self) {}
+}
+
+impl Base for u32 {}
+
 impl Later {
     fn touch(&self) {}
 }

+ 281 - 0
__tests__/kernel-deep-nesting.test.ts

@@ -0,0 +1,281 @@
+/**
+ * Deep-nesting safety for the native kernel (#1581).
+ *
+ * The kernel's per-language walkers recurse once per AST level. tree-sitter's
+ * parser is iterative, so a pathologically nested file — clang's
+ * `clang/test/Parser/parser_overflow.c` nests 16,384 `{`; fuzzer corpora go
+ * deeper — parses fine and then overflowed the WALKER's native stack. A native
+ * overflow is uncatchable: the parse worker is a thread of the `codegraph`
+ * process, so the SIGSEGV killed the whole indexer with no message, no partial
+ * index, no per-file fallback. Worker threads get Node's 4 MiB default stack;
+ * the 8 MiB main thread only moved the cliff (100k levels still died).
+ *
+ * The kernel now guards its recursion against the calling thread's real stack
+ * bounds (codegraph-kernel/src/stack.rs) and turns an imminent overflow into
+ * its `defer:` routing signal, so the file takes the wasm path — whose walker
+ * catches its own JS `RangeError` per file and stores a partial result with a
+ * `parse_error`. These tests pin that contract on every default-routed
+ * language, on the main thread AND inside a default-sized worker, and
+ * end-to-end through the built CLI.
+ *
+ * Like the other kernel suites: skipped without a staged .node; CI that
+ * builds the kernel sets CODEGRAPH_KERNEL_EXPECT=1 so a missing binary FAILS.
+ */
+
+import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
+import * as fs from 'fs';
+import * as os from 'os';
+import * as path from 'path';
+import { execFileSync } from 'child_process';
+import { Worker } from 'worker_threads';
+import { extractFromSource } from '../src/extraction';
+import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
+import { kernelRoutes, resetKernelForTests } from '../src/extraction/kernel';
+import type { Language } from '../src/types';
+
+const REPO = path.resolve(__dirname, '..');
+const KERNEL_PATH = path.join(
+  REPO,
+  'codegraph-kernel',
+  'prebuilds',
+  `${process.platform}-${process.arch}`,
+  'codegraph-kernel.node'
+);
+const kernelBuilt = fs.existsSync(KERNEL_PATH);
+const expectKernel = process.env.CODEGRAPH_KERNEL_EXPECT === '1';
+const BIN = path.join(REPO, 'dist', 'bin', 'codegraph.js');
+const DIST_KERNEL = path.join(REPO, 'dist', 'extraction', 'kernel');
+const distBuilt = fs.existsSync(BIN) && fs.existsSync(path.join(DIST_KERNEL, 'index.js'));
+
+/** Deep enough to overflow an 8 MiB main-thread stack on every walker. */
+const PARENS_DEPTH = 60_000;
+/** The reporter's exact shape: clang's parser_overflow.c nests 16,384 `{`. */
+const BRACES_DEPTH = 16_384;
+
+const CANDIDATES: Language[] = [
+  'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', 'c', 'cpp',
+  'rust', 'csharp', 'ruby', 'php', 'swift', 'kotlin', 'r', 'lua', 'luau', 'scala', 'dart',
+];
+
+const EXT: Record<string, string> = {
+  typescript: 'ts', tsx: 'tsx', javascript: 'js', jsx: 'jsx', java: 'java', python: 'py',
+  go: 'go', c: 'c', cpp: 'cpp', rust: 'rs', csharp: 'cs', ruby: 'rb', php: 'php',
+  swift: 'swift', kotlin: 'kt', r: 'R', lua: 'lua', luau: 'luau', scala: 'scala', dart: 'dart',
+};
+
+/** A function `f` whose body is a `depth`-deep parenthesized expression. */
+function deepParens(language: Language, depth: number): string {
+  const open = '('.repeat(depth);
+  const close = ')'.repeat(depth);
+  switch (language) {
+    case 'typescript': case 'tsx': case 'javascript': case 'jsx':
+      return `function f() { return ${open}1${close}; }\n`;
+    case 'java':
+      return `class A {\n  int f() { return ${open}1${close}; }\n}\n`;
+    case 'python':
+      return `def f():\n    return ${open}1${close}\n`;
+    case 'go':
+      return `package p\n\nfunc f() int { return ${open}1${close} }\n`;
+    case 'c':
+      return `int f(void) { return ${open}1${close}; }\n`;
+    case 'cpp':
+      return `int f() { return ${open}1${close}; }\n`;
+    case 'rust':
+      return `fn f() -> i32 { ${open}1${close} }\n`;
+    case 'csharp':
+      return `class A {\n  int f() { return ${open}1${close}; }\n}\n`;
+    case 'ruby':
+      return `def f\n  ${open}1${close}\nend\n`;
+    case 'php':
+      return `<?php\nfunction f() { return ${open}1${close}; }\n`;
+    case 'swift':
+      return `func f() -> Int { return ${open}1${close} }\n`;
+    case 'kotlin':
+      return `fun f(): Int { return ${open}1${close} }\n`;
+    case 'r':
+      return `f <- function() {\n  ${open}1${close}\n}\n`;
+    case 'lua': case 'luau':
+      return `local function f()\n  return ${open}1${close}\nend\n`;
+    case 'scala':
+      return `object A {\n  def f(): Int = ${open}1${close}\n}\n`;
+    case 'dart':
+      return `int f() { return ${open}1${close}; }\n`;
+    default:
+      throw new Error(`no deep fixture for ${language}`);
+  }
+}
+
+/** The reporter's repro: a C function body of `depth` nested blocks. */
+function deepBraces(depth: number): string {
+  return `void foo(void) {\n${'{'.repeat(depth)}${'}'.repeat(depth)}\n}\n`;
+}
+
+const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS', 'CODEGRAPH_KERNEL_PATH'] as const;
+let savedEnv: Record<string, string | undefined>;
+
+describe.skipIf(!kernelBuilt)('kernel deep-nesting guard (#1581)', () => {
+  let routed: Language[] = [];
+
+  beforeAll(async () => {
+    resetKernelForTests();
+    routed = CANDIDATES.filter((l) => kernelRoutes(l));
+    expect(routed.length).toBeGreaterThan(0);
+    await initGrammars();
+    await loadGrammarsForLanguages(routed);
+  });
+
+  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();
+  });
+
+  it('every default-routed language survives a 60k-deep expression on the main thread', () => {
+    const failures: string[] = [];
+    for (const language of routed) {
+      const file = `deep.${EXT[language]}`;
+      const source = deepParens(language, PARENS_DEPTH);
+      // The ONLY acceptable outcomes: a clean result (the thread's stack was
+      // big enough for the walk), or the wasm fallback's partial result with
+      // its parse_error. A native overflow would have killed this process.
+      const result = extractFromSource(file, source, language);
+      const fn = result.nodes.find((n) => n.name === 'f' && (n.kind === 'function' || n.kind === 'method'));
+      // R's wasm walker mints `f <- function()` only after walking the
+      // assignment's value, so its partial result for a file this deep holds
+      // just the file node — the same shape main's wasm-only path produces
+      // (verified with CODEGRAPH_KERNEL=0). Pre-existing and out of scope
+      // here; what this test pins for R is that the process survives.
+      if (!fn && language !== 'r') failures.push(`${language}: no function node 'f' (nodes=${result.nodes.map((n) => `${n.kind}:${n.name}`).join(',')})`);
+      for (const e of result.errors) {
+        if (!/Maximum call stack|parse_error|Parse error/.test(`${e.code} ${e.message}`)) {
+          failures.push(`${language}: unexpected error ${e.message}`);
+        }
+      }
+    }
+    expect(failures).toEqual([]);
+  }, 120_000);
+
+  it("the reporter's 16,384-brace C file is indexed (partial) instead of killing the process", () => {
+    const result = extractFromSource('deep.c', deepBraces(BRACES_DEPTH), 'c');
+    expect(result.nodes.some((n) => n.kind === 'function' && n.name === 'foo')).toBe(true);
+  }, 60_000);
+
+  it('shallow files still take the kernel path (the guard never trips on normal code)', () => {
+    // Sanity for the perf-neutral claim: a 200-deep expression is far inside
+    // any thread's stack, so it must come back clean with no parse_error.
+    for (const language of routed) {
+      const result = extractFromSource(`ok.${EXT[language]}`, deepParens(language, 200), language);
+      expect(result.errors, language).toEqual([]);
+      expect(result.nodes.some((n) => n.name === 'f'), language).toBe(true);
+    }
+  }, 60_000);
+
+  describe.skipIf(!distBuilt)('inside a default-sized (4 MiB) parse worker, through dist/', () => {
+    let tmp: string;
+    beforeEach(() => {
+      tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-deep-'));
+    });
+    afterEach(() => {
+      fs.rmSync(tmp, { recursive: true, force: true });
+    });
+
+    /**
+     * Run the kernel's raw extraction for `file` inside a Worker with Node's
+     * DEFAULT resourceLimits — exactly how ParseWorkerPool runs it. Resolves
+     * with the worker's exit code and what it reported; a native overflow
+     * would SIGSEGV/SIGILL this whole vitest process instead.
+     */
+    function runInWorker(file: string, source: string, language: Language): Promise<{ exitCode: number; outcome: string }> {
+      const script = path.join(tmp, 'worker.cjs');
+      fs.writeFileSync(
+        script,
+        [
+          `const { parentPort, workerData } = require('worker_threads');`,
+          `const { tryKernelExtractRaw } = require(${JSON.stringify(DIST_KERNEL)});`,
+          `const raw = tryKernelExtractRaw(workerData.file, workerData.source, workerData.language);`,
+          `parentPort.postMessage(raw ? 'kernel:' + raw.counts.nodes : 'deferred');`,
+        ].join('\n')
+      );
+      return new Promise((resolve, reject) => {
+        let outcome = 'no message';
+        const w = new Worker(script, { workerData: { file, source, language } });
+        w.on('message', (m: string) => { outcome = m; });
+        w.on('error', reject);
+        w.on('exit', (exitCode) => resolve({ exitCode, outcome }));
+      });
+    }
+
+    it("defers the reporter's deep.c instead of crashing the worker", async () => {
+      const r = await runInWorker('deep.c', deepBraces(BRACES_DEPTH), 'c');
+      expect(r.exitCode).toBe(0);
+      expect(r.outcome).toBe('deferred');
+    }, 60_000);
+
+    it('defers a 60k-deep expression in every default-routed language', async () => {
+      for (const language of routed) {
+        const r = await runInWorker(`deep.${EXT[language]}`, deepParens(language, PARENS_DEPTH), language);
+        expect(r.exitCode, language).toBe(0);
+        // Either the guard tripped (deferred) or the walk fit — never a crash.
+        expect(['deferred', 'kernel'].some((p) => r.outcome.startsWith(p)), `${language}: ${r.outcome}`).toBe(true);
+      }
+    }, 180_000);
+
+    it('still extracts a normal file natively in the worker', async () => {
+      const r = await runInWorker('ok.c', 'int add(int a, int b) { return a + b; }\n', 'c');
+      expect(r.exitCode).toBe(0);
+      expect(r.outcome).toMatch(/^kernel:/);
+    }, 30_000);
+  });
+
+  describe.skipIf(!distBuilt)('end-to-end: codegraph init on a repo holding the deep file', () => {
+    let tmp: string;
+    beforeEach(() => {
+      tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-deep-cli-'));
+    });
+    afterEach(() => {
+      fs.rmSync(tmp, { recursive: true, force: true });
+    });
+
+    it('exits 0 and records deep.c alongside the normal files', () => {
+      fs.writeFileSync(path.join(tmp, 'deep.c'), deepBraces(BRACES_DEPTH));
+      fs.writeFileSync(path.join(tmp, 'ok.c'), 'int add(int a, int b) { return a + b; }\n');
+      execFileSync(process.execPath, [BIN, 'init', '.'], {
+        cwd: tmp,
+        encoding: 'utf-8',
+        stdio: ['ignore', 'pipe', 'pipe'],
+        timeout: 120_000,
+        env: {
+          ...process.env,
+          CODEGRAPH_NO_DAEMON: '1',
+          CODEGRAPH_WASM_RELAUNCHED: '1',
+          CODEGRAPH_TELEMETRY: '0',
+          DO_NOT_TRACK: '1',
+          CODEGRAPH_NO_PROMPT_HOOK: '1',
+        },
+      });
+      const { DatabaseSync } = require('node:sqlite') as typeof import('node:sqlite');
+      const db = new DatabaseSync(path.join(tmp, '.codegraph', 'codegraph.db'), { readOnly: true });
+      try {
+        const files = (db.prepare('SELECT path FROM files ORDER BY path').all() as Array<{ path: string }>).map((r) => r.path);
+        expect(files).toEqual(['deep.c', 'ok.c']);
+        const fns = (db.prepare("SELECT name FROM nodes WHERE kind = 'function' ORDER BY name").all() as Array<{ name: string }>).map((r) => r.name);
+        expect(fns).toEqual(['add', 'foo']);
+      } finally {
+        db.close();
+      }
+    }, 180_000);
+  });
+});
+
+describe.skipIf(!expectKernel)('kernel presence (CODEGRAPH_KERNEL_EXPECT=1)', () => {
+  it('the staged .node exists so the deep-nesting suite actually ran', () => {
+    expect(kernelBuilt).toBe(true);
+  });
+});

+ 2 - 2
__tests__/kernel-rustlang-parity.test.ts

@@ -4,8 +4,8 @@
  * Asserts the native walker (codegraph-kernel/src/rustlang.rs) produces the
  * SAME ExtractionResult as the wasm TreeSitterExtractor — nodes, edges, and
  * unresolved refs compared as canonicalized multisets — over the checked-in
- * torture fixture (torture.rs: impl/trait quirks incl. the
- * `impl Trait for Generic<T>` trait-receiver bug, unit-struct skip, phantom
+ * torture fixture (torture.rs: impl/trait quirks incl. generic / lifetime /
+ * reference / scoped / generic-trait impl receivers (#1588), unit-struct skip, phantom
  * const identifiers, use-binding refs incl. nested groups + wildcard-emits-
  * nothing, chained-call re-encode, turbofish, Rocket route macros body-only,
  * fn-ref shapes, value-ref shadowing, attribute-broken docstrings, dead-code

+ 287 - 0
__tests__/resolution.test.ts

@@ -1119,6 +1119,168 @@ impl Describe for Ctl { fn describe(&self) -> String { "ctl".into() } }
       ).toBe('interface-impl');
     });
 
+    it('qualifies a generic impl by its type, so trait dispatch reaches it and no edge is invented from its body (#1588)', async () => {
+      // `impl<T> Source for BufSource<T>`: the implementing type parses as a
+      // generic_type, so the old positional receiver scan picked the TRAIT.
+      // The impl's `read` was recorded as `Source::read` — unaddressable as
+      // `BufSource::read` — and, carrying the trait's name, the interface-impl
+      // synthesizer treated its body (`{ 0 }`, no call at all) as a second
+      // declaration and gave it a dispatch edge to FileSource's implementation.
+      fs.writeFileSync(
+        path.join(tempDir, 'lib.rs'),
+        `pub trait Source {
+    fn read(&mut self) -> usize;
+}
+
+pub struct FileSource { pub n: usize }
+impl Source for FileSource {
+    fn read(&mut self) -> usize { self.n }
+}
+
+pub struct BufSource<T> { pub inner: T }
+impl<T> Source for BufSource<T> {
+    fn read(&mut self) -> usize { 0 }
+}
+`
+      );
+
+      cg = await CodeGraph.init(tempDir, { index: true });
+
+      const methods = cg.getNodesByKind('method');
+      const traitDecls = methods.filter((n) => n.qualifiedName === 'Source::read');
+      expect(traitDecls, 'only the declaration carries the trait-qualified name').toHaveLength(1);
+      const traitMethod = traitDecls[0]!;
+      expect(traitMethod.startLine).toBe(2);
+      const fileImpl = methods.find((n) => n.qualifiedName === 'FileSource::read');
+      const bufImpl = methods.find((n) => n.qualifiedName === 'BufSource::read');
+      expect(fileImpl).toBeDefined();
+      expect(bufImpl, 'the generic impl is addressable by its type').toBeDefined();
+
+      const synth = (id: string) =>
+        cg.getOutgoingEdges(id).filter((e) => e.kind === 'calls' && e.provenance === 'heuristic');
+      // Dispatch fans out from the declaration to BOTH implementations…
+      const fromTrait = synth(traitMethod.id);
+      expect(new Set(fromTrait.map((e) => e.target))).toEqual(new Set([fileImpl!.id, bufImpl!.id]));
+      for (const e of fromTrait) {
+        expect(
+          (e.metadata as { synthesizedBy?: string } | undefined)?.synthesizedBy
+        ).toBe('interface-impl');
+        expect(e.line, 'registered at the declaration, never at an impl body').toBe(2);
+      }
+      // …and neither implementation body sprouts a synthesized call of its own.
+      expect(synth(fileImpl!.id)).toHaveLength(0);
+      expect(synth(bufImpl!.id)).toHaveLength(0);
+    });
+
+    // ── Rust `self.<field>.<method>()` receivers (#1585) ───────────────────
+    // A Cargo layout (Cargo.toml + src/) so `use crate::…` paths resolve.
+    function writeRustCrate(root: string, files: Record<string, string>): void {
+      fs.writeFileSync(
+        path.join(root, 'Cargo.toml'),
+        '[package]\nname = "repro"\nversion = "0.1.0"\nedition = "2021"\n'
+      );
+      fs.mkdirSync(path.join(root, 'src'), { recursive: true });
+      for (const [rel, content] of Object.entries(files)) {
+        fs.writeFileSync(path.join(root, 'src', rel), content);
+      }
+    }
+    const callsFrom = (qualifiedName: string) => {
+      const from = cg.getNodesByKind('method').find((n) => n.qualifiedName === qualifiedName);
+      expect(from, qualifiedName).toBeDefined();
+      return cg
+        .getOutgoingEdges(from!.id)
+        .filter((e) => e.kind === 'calls')
+        .map((e) => ({
+          target: cg.getNode(e.target)?.qualifiedName,
+          resolvedBy: (e.metadata as { resolvedBy?: string } | undefined)?.resolvedBy,
+          provenance: e.provenance ?? undefined, // a resolved (non-synthesized) edge stores NULL
+        }));
+    };
+
+    it("resolves `self.field.method()` to the method on the field's declared type, never to the caller itself (#1585)", async () => {
+      // The issue's repro: `Outer::run` forwards to `Inner::run` through the
+      // typed field `inner`. The call used to collapse to the bare name `run`
+      // and exact-match the nearest same-named method — the calling method —
+      // recording recursion the source does not contain.
+      writeRustCrate(tempDir, {
+        'lib.rs': 'pub mod inner;\npub mod outer;\n',
+        'inner.rs': 'pub struct Inner {\n    pub n: usize,\n}\n\nimpl Inner {\n    pub fn run(&mut self) {\n        self.n += 1;\n    }\n}\n',
+        'outer.rs': 'use crate::inner::Inner;\n\npub struct Outer {\n    pub inner: Inner,\n}\n\nimpl Outer {\n    pub fn run(&mut self) {\n        self.inner.run();\n    }\n}\n',
+      });
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callsFrom('Outer::run')).toEqual([
+        { target: 'Inner::run', resolvedBy: 'instance-method', provenance: undefined },
+      ]);
+    });
+
+    it('leaves a `self.field.method()` call unresolved when the field type is external, instead of guessing a same-named local method', async () => {
+      // `its` is a std type with no project node. Before, `self.its.next()`
+      // became the bare `next`, which exact-matched a local `next` — the
+      // calling method (self-edge) or the unrelated `Other::next` decoy.
+      writeRustCrate(tempDir, {
+        'lib.rs':
+          'pub struct Scanner {\n    its: std::vec::IntoIter<u8>,\n}\n\nimpl Scanner {\n    pub fn next(&mut self) -> Option<u8> {\n        self.its.next()\n    }\n}\n\n' +
+          'pub struct Other { pub n: u8 }\nimpl Other {\n    pub fn next(&mut self) -> Option<u8> {\n        None\n    }\n}\n',
+      });
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callsFrom('Scanner::next')).toEqual([]);
+    });
+
+    it('looks through references and owning smart pointers, but not through containers (#1585)', async () => {
+      // Method-call auto-deref reaches the pointee of `Box`/`&mut`, so those
+      // fields resolve to `Inner::run`. `Option<Inner>` does not auto-deref —
+      // `self.inner.take()` is Option's method, so it must NOT become
+      // `Inner::take` even though Inner declares a `take` too.
+      writeRustCrate(tempDir, {
+        'lib.rs':
+          'pub struct Inner { pub n: usize }\nimpl Inner {\n    pub fn run(&mut self) { self.n += 1; }\n    pub fn take(&mut self) {}\n}\n\n' +
+          'pub struct Boxed { inner: Box<Inner> }\nimpl Boxed {\n    pub fn go(&mut self) { self.inner.run(); }\n}\n\n' +
+          "pub struct Borrowed<'a> { inner: &'a mut Inner }\nimpl<'a> Borrowed<'a> {\n    pub fn go(&mut self) { self.inner.run(); }\n}\n\n" +
+          'pub struct Optional { inner: Option<Inner> }\nimpl Optional {\n    pub fn go(&mut self) { self.inner.take(); }\n}\n',
+      });
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callsFrom('Boxed::go').map((c) => c.target)).toEqual(['Inner::run']);
+      expect(callsFrom('Borrowed::go').map((c) => c.target)).toEqual(['Inner::run']);
+      expect(callsFrom('Optional::go')).toEqual([]);
+    });
+
+    it('leaves a call through a generic-typed field unresolved, and keeps genuine `self.method()` recursion (#1585)', async () => {
+      writeRustCrate(tempDir, {
+        'lib.rs':
+          'pub struct Inner { pub n: usize }\nimpl Inner {\n    pub fn run(&mut self) {}\n}\n\n' +
+          'pub struct Holder<T> { item: T }\nimpl<T> Holder<T> {\n    pub fn go(&mut self) { self.item.run(); }\n}\n\n' +
+          'pub struct Countdown { pub n: usize }\nimpl Countdown {\n    pub fn run(&mut self) {\n        if self.n > 0 {\n            self.n -= 1;\n            self.run();\n        }\n    }\n}\n',
+      });
+      cg = await CodeGraph.init(tempDir, { index: true });
+      // `T` names no project type: no edge, and in particular not `Inner::run`.
+      expect(callsFrom('Holder::go')).toEqual([]);
+      // A bare `self` receiver is untouched — real recursion stays a self-edge.
+      expect(callsFrom('Countdown::run').map((c) => c.target)).toEqual(['Countdown::run']);
+    });
+
+    it('resolves a trait-object field to the trait method and typed fields to the right implementation (#1585, #1588)', async () => {
+      // The #1588 repro's second half: `UsesFile::go` / `UsesBuf::go` each
+      // forward through a typed field, and a `Box<dyn Source>` field lands on
+      // the trait's declaration — from which the interface-impl synthesizer
+      // fans out to every implementation.
+      writeRustCrate(tempDir, {
+        'lib.rs':
+          'pub trait Source {\n    fn read(&mut self) -> usize;\n}\n\n' +
+          'pub struct FileSource { pub n: usize }\nimpl Source for FileSource {\n    fn read(&mut self) -> usize { self.n }\n}\n\n' +
+          'pub struct BufSource<T> { pub inner: T }\nimpl<T> Source for BufSource<T> {\n    fn read(&mut self) -> usize { 0 }\n}\n\n' +
+          'pub struct UsesFile { pub src: FileSource }\nimpl UsesFile {\n    pub fn go(&mut self) -> usize { self.src.read() }\n}\n\n' +
+          'pub struct UsesBuf { pub src: BufSource<u8> }\nimpl UsesBuf {\n    pub fn go(&mut self) -> usize { self.src.read() }\n}\n\n' +
+          'pub struct UsesDyn { pub src: Box<dyn Source> }\nimpl UsesDyn {\n    pub fn go(&mut self) -> usize { self.src.read() }\n}\n',
+      });
+      cg = await CodeGraph.init(tempDir, { index: true });
+      expect(callsFrom('UsesFile::go').map((c) => c.target)).toEqual(['FileSource::read']);
+      expect(callsFrom('UsesBuf::go').map((c) => c.target)).toEqual(['BufSource::read']);
+      expect(callsFrom('UsesDyn::go').map((c) => c.target)).toEqual(['Source::read']);
+      // …and dispatch continues from the trait declaration to both impls.
+      const fanOut = callsFrom('Source::read').filter((c) => c.provenance === 'heuristic').map((c) => c.target).sort();
+      expect(fanOut).toEqual(['BufSource::read', 'FileSource::read']);
+    });
+
     it('records instantiates for C++ stack/brace construction, targeting the class (#1035)', async () => {
       // `Calculator calc(0)` (direct-init) and `Widget w{1, 2}` (brace-init)
       // carry the constructor args directly on the declarator — there's no
@@ -2913,6 +3075,131 @@ export function callFromImportedFile(): void {
     }, 30000);
   });
 
+  describe('Object-literal namespace members (#1573)', () => {
+    // `export const api = { call() {…}, get: () => {…} }` used as the module's
+    // API surface: the members are plain functions with bare names inside the
+    // constant's extent, so `api.call()` resolved to nothing in the defining
+    // file and to the CONSTANT through an import — zero callers everywhere.
+    const setup = (files: Record<string, string>) => {
+      const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-1573-'));
+      for (const [name, content] of Object.entries(files)) {
+        fs.mkdirSync(path.dirname(path.join(tmpDir, name)), { recursive: true });
+        fs.writeFileSync(path.join(tmpDir, name), content);
+      }
+      return tmpDir;
+    };
+    const callersOf = async (cg: CodeGraph, name: string, kind: string, filePath?: string) => {
+      const target = (await cg.searchNodes(name, { limit: 20 })).find(
+        (r) => r.node.kind === kind && r.node.name === name && (!filePath || r.node.filePath === filePath)
+      );
+      expect(target).toBeDefined();
+      return (await cg.getCallers(target!.node.id)).map((c) => c.node.name).sort();
+    };
+
+    it('resolves same-file and imported calls to the literal member, never to the constant (#1573)', async () => {
+      const tmpDir = setup({
+        'a.ts': `export const obj = { m() { return 1; } };
+export class C { static s() { return 2; } }
+export function sameFileCallers() { return obj.m() + C.s(); }
+`,
+        'b.ts': `import { obj, C } from "./a";
+export function crossFileCaller() { return obj.m() + C.s(); }
+`,
+        // A same-named top-level function elsewhere must never be chosen.
+        'decoy.ts': `export function m() { return 'decoy'; }
+`,
+      });
+      try {
+        const cg = CodeGraph.initSync(tmpDir);
+        await cg.indexAll();
+
+        expect(await callersOf(cg, 'm', 'function', 'a.ts')).toEqual(['crossFileCaller', 'sameFileCallers']);
+        expect(await callersOf(cg, 'm', 'function', 'decoy.ts')).toEqual([]);
+        // The class static next to it resolves exactly as before (#825).
+        expect(await callersOf(cg, 's', 'method')).toEqual(['crossFileCaller', 'sameFileCallers']);
+
+        // The import edge no longer lands on the constant itself.
+        const obj = (await cg.searchNodes('obj', { limit: 5 })).find((r) => r.node.kind === 'constant');
+        expect(obj).toBeDefined();
+        const caller = (await cg.searchNodes('crossFileCaller', { limit: 5 })).find((r) => r.node.kind === 'function');
+        const toConstant = cg
+          .getOutgoingEdges(caller!.node.id)
+          .filter((e) => e.kind === 'calls' && e.target === obj!.node.id);
+        expect(toConstant).toHaveLength(0);
+        cg.close();
+      } finally {
+        fs.rmSync(tmpDir, { recursive: true, force: true });
+      }
+    }, 30000);
+
+    it('covers method and arrow-property members, and skips a declaration nested in a member body', async () => {
+      const tmpDir = setup({
+        'src/api.ts': `export const api = {
+  call: () => { return 1; },
+  get() {
+    function call() { return 'nested in get, not a member'; }
+    return call();
+  },
+};
+`,
+        'src/use.ts': `import { api } from './api';
+export function useCall() { return api.call(); }
+export function useGet() { return api.get(); }
+`,
+      });
+      try {
+        const cg = CodeGraph.initSync(tmpDir);
+        await cg.indexAll();
+
+        const calls = (await cg.searchNodes('call', { limit: 20 }))
+          .map((r) => r.node)
+          .filter((n) => n.name === 'call' && n.filePath === 'src/api.ts' && (n.kind === 'function' || n.kind === 'method'));
+        // The member is the arrow on line 2; the nested declaration sits
+        // inside `get`'s body on line 4 and must never be taken for it.
+        const member = calls.find((n) => n.startLine === 2);
+        const nested = calls.find((n) => n.startLine === 4);
+        expect(member).toBeDefined();
+        expect(nested).toBeDefined();
+        expect((await cg.getCallers(member!.id)).map((c) => c.node.name)).toContain('useCall');
+        expect((await cg.getCallers(nested!.id)).map((c) => c.node.name)).not.toContain('useCall');
+        expect(await callersOf(cg, 'get', 'function')).toEqual(['useGet']);
+        cg.close();
+      } finally {
+        fs.rmSync(tmpDir, { recursive: true, force: true });
+      }
+    }, 30000);
+
+    it('leaves a non-literal value receiver on its existing path', async () => {
+      const tmpDir = setup({
+        'src/mk.ts': `export function m() { return 'top-level, unrelated to obj'; }
+export const obj = makeObj();
+export function makeObj(): { m(): number } { return { m: () => 1 } as { m(): number }; }
+export function localUse() { return obj.m(); }
+`,
+        'src/use.ts': `import { obj } from './mk';
+export function remoteUse() { return obj.m(); }
+`,
+      });
+      try {
+        const cg = CodeGraph.initSync(tmpDir);
+        await cg.indexAll();
+        // `obj` holds a call result, not a literal: the same-named top-level
+        // `m` lies outside its declaration, so containment finds nothing and
+        // both calls keep today's behavior (unresolved in the defining file;
+        // the constant edge through the import) rather than guessing.
+        expect(await callersOf(cg, 'm', 'function')).toEqual([]);
+        const obj = (await cg.searchNodes('obj', { limit: 5 })).find((r) => r.node.kind === 'constant');
+        const remote = (await cg.searchNodes('remoteUse', { limit: 5 })).find((r) => r.node.kind === 'function');
+        expect(
+          cg.getOutgoingEdges(remote!.node.id).some((e) => e.kind === 'calls' && e.target === obj!.node.id)
+        ).toBe(true);
+        cg.close();
+      } finally {
+        fs.rmSync(tmpDir, { recursive: true, force: true });
+      }
+    }, 30000);
+  });
+
   describe('C++ namespace-qualified static method calls to out-of-line definitions (#1291)', () => {
     // The issue's exact shape: nested types + out-of-line static method
     // definition inside `namespace simulator { }` in the .cpp, called via the

+ 29 - 0
__tests__/sync.test.ts

@@ -851,4 +851,33 @@ describe('Scoped sync parity (#watcher-scoped)', () => {
     // b.ts untouched and still present
     expect(cg.searchNodes('beta').length).toBeGreaterThan(0);
   });
+
+  it('a scoped path that codegraph.json now excludes is removed, never re-parsed (#1590)', async () => {
+    // The daemon's watcher hands sync the exact edited path. If the project's
+    // scope changed underneath it, that path must be treated the way the full
+    // scan treats it — out of scope, hence gone — never parsed on trust.
+    const cfg = path.join(testDir, 'codegraph.json');
+    fs.writeFileSync(cfg, JSON.stringify({ exclude: ['src/b.ts'] }));
+    fs.writeFileSync(path.join(testDir, 'src', 'b.ts'), `export function beta() { return 2; }\nexport function gamma() { return 3; }`);
+    const scoped = await cg.sync({ paths: ['src/b.ts'] });
+    expect(scoped.filesRemoved).toBe(1);
+    expect(scoped.filesModified).toBe(0);
+    expect(scoped.filesAdded).toBe(0);
+    expect(cg.searchNodes('gamma').length).toBe(0);
+    expect(cg.searchNodes('beta').filter((r) => r.node.filePath === 'src/b.ts').length).toBe(0);
+    // Idempotent: the file stays out on a repeat scoped sync.
+    const again = await cg.sync({ paths: ['src/b.ts'] });
+    expect(again.filesRemoved).toBe(0);
+    expect(again.filesAdded).toBe(0);
+
+    // Dropping the exclude readmits it through the same scoped path. The
+    // scope matcher is mtime-keyed, so give the rewrite a distinct mtime even
+    // on a coarse-timestamp filesystem.
+    fs.writeFileSync(cfg, JSON.stringify({}));
+    const later = new Date(Date.now() + 5000);
+    fs.utimesSync(cfg, later, later);
+    const readmitted = await cg.sync({ paths: ['src/b.ts'] });
+    expect(readmitted.filesAdded).toBe(1);
+    expect(cg.searchNodes('gamma').length).toBe(1);
+  });
 });

+ 128 - 0
__tests__/watcher.test.ts

@@ -545,6 +545,134 @@ describe('FileWatcher', () => {
     });
   });
 
+  describe('scope config refresh (#1590)', () => {
+    // The matcher used to be built once in start() and kept for the watcher's
+    // lifetime, so a `codegraph.json` written AFTER the daemon started was
+    // invisible to the live watcher while `codegraph sync` honoured it: the
+    // CLI removed a newly excluded file and the watcher re-added it.
+    it('a codegraph.json edit rebuilds the matcher and forces a full sync', async () => {
+      const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
+      const watcher = newWatcher(syncFn, { debounceMs: 100 });
+      watcher.start();
+      await watcher.waitUntilReady();
+
+      // Scope the project after the watcher is already running.
+      fs.mkdirSync(path.join(testDir, 'skipme'));
+      fs.writeFileSync(path.join(testDir, 'skipme', 'b.ts'), 'export const b = 1;\n');
+      fs.writeFileSync(path.join(testDir, 'codegraph.json'), JSON.stringify({ exclude: ['skipme/'] }));
+      __emitWatchEventForTests(testDir, 'codegraph.json');
+
+      // The config edit schedules a FULL sync (no scoped path list): only the
+      // scan-diff can find the files the new scope drops or admits.
+      await waitFor(() => syncFn.mock.calls.length > 0);
+      expect(syncFn.mock.calls.length).toBe(1);
+      expect(syncFn.mock.calls[0]![0]).toBeUndefined();
+      expect(watcher.getPendingFiles()).toEqual([]);
+      await new Promise((r) => setTimeout(r, 50)); // let runSync settle
+
+      // An edit inside the newly excluded tree is dropped by the LIVE matcher:
+      // not pending, and no sync scheduled for it.
+      __emitWatchEventForTests(testDir, 'skipme/b.ts');
+      expect(watcher.getPendingFiles().map((p) => p.path)).not.toContain('skipme/b.ts');
+      await new Promise((r) => setTimeout(r, 300)); // > debounce
+      expect(syncFn.mock.calls.length).toBe(1);
+
+      // In-scope edits still sync, scoped to the edited path as before.
+      __emitWatchEventForTests(testDir, 'src/index.ts');
+      await waitFor(() => syncFn.mock.calls.length > 1);
+      expect(syncFn.mock.calls[1]![0]).toEqual(['src/index.ts']);
+
+      watcher.stop();
+    });
+
+    it('a root .gitignore edit is a scope change too', async () => {
+      const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
+      const watcher = newWatcher(syncFn, { debounceMs: 100 });
+      watcher.start();
+      await watcher.waitUntilReady();
+
+      fs.mkdirSync(path.join(testDir, 'gen'));
+      fs.writeFileSync(path.join(testDir, 'gen', 'out.ts'), 'export const g = 1;\n');
+      fs.writeFileSync(path.join(testDir, '.gitignore'), 'gen/\n');
+      __emitWatchEventForTests(testDir, '.gitignore');
+
+      await waitFor(() => syncFn.mock.calls.length > 0);
+      expect(syncFn.mock.calls[0]![0]).toBeUndefined();
+      await new Promise((r) => setTimeout(r, 50));
+
+      __emitWatchEventForTests(testDir, 'gen/out.ts');
+      expect(watcher.getPendingFiles().map((p) => p.path)).not.toContain('gen/out.ts');
+      await new Promise((r) => setTimeout(r, 300));
+      expect(syncFn.mock.calls.length).toBe(1);
+
+      watcher.stop();
+    });
+
+    it('a nested .gitignore inside the scope forces a full sync', async () => {
+      const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
+      const watcher = newWatcher(syncFn, { debounceMs: 100 });
+      watcher.start();
+      await watcher.waitUntilReady();
+
+      fs.mkdirSync(path.join(testDir, 'sub'));
+      fs.writeFileSync(path.join(testDir, 'sub', '.gitignore'), 'build/\n');
+      __emitWatchEventForTests(testDir, 'sub/.gitignore');
+
+      await waitFor(() => syncFn.mock.calls.length > 0);
+      expect(syncFn.mock.calls[0]![0]).toBeUndefined();
+
+      watcher.stop();
+    });
+
+    it('a .gitignore under an ignored tree (npm install churn) schedules nothing', async () => {
+      const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
+      const watcher = newWatcher(syncFn, { debounceMs: 100 });
+      watcher.start();
+      await watcher.waitUntilReady();
+
+      fs.mkdirSync(path.join(testDir, 'node_modules', 'pkg'), { recursive: true });
+      fs.writeFileSync(path.join(testDir, 'node_modules', 'pkg', '.gitignore'), 'lib/\n');
+      __emitWatchEventForTests(testDir, 'node_modules/pkg/.gitignore');
+
+      await new Promise((r) => setTimeout(r, 300));
+      expect(syncFn).not.toHaveBeenCalled();
+
+      watcher.stop();
+    });
+
+    it('removing the exclude again readmits the tree', async () => {
+      const syncFn = vi.fn().mockResolvedValue({ filesChanged: 0, durationMs: 0 });
+      const watcher = newWatcher(syncFn, { debounceMs: 100 });
+      watcher.start();
+      await watcher.waitUntilReady();
+
+      fs.mkdirSync(path.join(testDir, 'skipme'));
+      fs.writeFileSync(path.join(testDir, 'skipme', 'b.ts'), 'export const b = 1;\n');
+      const cfg = path.join(testDir, 'codegraph.json');
+      fs.writeFileSync(cfg, JSON.stringify({ exclude: ['skipme/'] }));
+      __emitWatchEventForTests(testDir, 'codegraph.json');
+      await waitFor(() => syncFn.mock.calls.length > 0);
+      await new Promise((r) => setTimeout(r, 50));
+      __emitWatchEventForTests(testDir, 'skipme/b.ts');
+      expect(watcher.getPendingFiles().map((p) => p.path)).not.toContain('skipme/b.ts');
+
+      // Drop the exclude. The loader is mtime-keyed, so make sure the second
+      // write carries a distinct mtime even on a coarse-timestamp filesystem.
+      fs.writeFileSync(cfg, JSON.stringify({}));
+      const later = new Date(Date.now() + 5000);
+      fs.utimesSync(cfg, later, later);
+      __emitWatchEventForTests(testDir, 'codegraph.json');
+      await waitFor(() => syncFn.mock.calls.length > 1);
+      expect(syncFn.mock.calls[1]![0]).toBeUndefined();
+      await new Promise((r) => setTimeout(r, 50));
+
+      __emitWatchEventForTests(testDir, 'skipme/b.ts');
+      expect(watcher.getPendingFiles().map((p) => p.path)).toContain('skipme/b.ts');
+
+      watcher.stop();
+    });
+  });
+
   describe('pending file tracking (#403)', () => {
     it('should expose edited paths via getPendingFiles before sync fires', async () => {
       // Slow debounce — pending entries are visible until the debounce fires.

+ 1 - 0
codegraph-kernel/Cargo.lock

@@ -47,6 +47,7 @@ name = "codegraph-kernel"
 version = "0.1.0"
 dependencies = [
  "cc",
+ "libc",
  "napi",
  "napi-build",
  "napi-derive",

+ 6 - 0
codegraph-kernel/Cargo.toml

@@ -62,6 +62,12 @@ tree-sitter-luau = "=1.2.0"
 # kotlin grammar C (see build.rs — no kotlin crate dep is possible).
 tree-sitter-language = "0.1"
 
+# Stack-bounds queries for the walker stack guard (src/stack.rs, #1581):
+# pthread_getattr_np / pthread_get_stackaddr_np. Already in the lock file
+# transitively; Windows uses a hand-declared kernel32 extern instead.
+[target.'cfg(unix)'.dependencies]
+libc = "0.2"
+
 [build-dependencies]
 napi-build = "2"
 cc = "1"

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

@@ -776,6 +776,7 @@ impl<'t> Walker<'t> {
     // --- visitNode -----------------------------------------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -852,6 +853,7 @@ impl<'t> Walker<'t> {
     // --- extractors ----------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // 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);
@@ -892,6 +894,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let receiver_type = if self.variant == Variant::Cpp { self.receiver_type_of(node) } else { None };
 
         if !self.inside_class_like() && receiver_type.is_none() {
@@ -956,6 +959,7 @@ impl<'t> Walker<'t> {
 
     /// extractClass for cpp class_specifier (skipBodilessClass, #1093).
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -976,6 +980,7 @@ impl<'t> Walker<'t> {
 
     /// Extract a struct-like declaration while preserving its semantic kind.
     fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -995,6 +1000,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -1028,6 +1034,7 @@ impl<'t> Walker<'t> {
     /// 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 {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             return false;
@@ -1502,10 +1509,12 @@ impl<'t> Walker<'t> {
     // --- function bodies -----------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -1591,6 +1600,7 @@ impl<'t> Walker<'t> {
     /// 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) {
+        stack_guard!();
         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 };
@@ -1703,6 +1713,7 @@ impl<'t> Walker<'t> {
     /// 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) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1749,6 +1760,7 @@ impl<'t> Walker<'t> {
     /// 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) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 13 - 0
codegraph-kernel/src/csharp.rs

@@ -500,6 +500,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, C#-relevant branches) -----------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -571,10 +572,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody ------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -626,6 +629,7 @@ impl<'t> Walker<'t> {
     // --- extractors --------------------------------------------------------------
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         // skipBodilessClass unset: a bodiless `record Empty;` still mints a node.
         let name = self.extract_name(node);
         let extra = Extra {
@@ -656,6 +660,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_struct(&mut self, node: Node<'t>) {
+        stack_guard!();
         // Body gate — EXCEPT C# positional records (`record struct M(…);`,
         // node type record_declaration), complete definitions with no body.
         // A bodiless `struct Fwd;` mints NO node. (#831)
@@ -685,6 +690,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -703,6 +709,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -890,6 +897,7 @@ impl<'t> Walker<'t> {
     /// extractMethod (1737) — method_declaration + constructor_declaration.
     /// Signature is ALWAYS undefined (no getSignature hook); isAsync is real.
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         if !self.inside_class_like() {
             // Unreachable on non-erroring C# (top-level `void M(){}` parses as
             // local_function_statement; erroring files defer) — mirror the TS
@@ -924,6 +932,7 @@ impl<'t> Walker<'t> {
     /// extractFunction — only reachable for a method outside any class
     /// (unreachable on non-erroring C#; kept faithful to the generic tail).
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -1087,6 +1096,7 @@ impl<'t> Walker<'t> {
     /// (object initializers are initializer_expression), so this is
     /// unreachable — mirrored from the shared TS path like java.rs.
     fn extract_anonymous_class(&mut self, node: Node<'t>, body: Node<'t>) {
+        stack_guard!();
         let type_node = node
             .child_by_field_name("constructor")
             .or_else(|| node.child_by_field_name("type"))
@@ -1243,6 +1253,7 @@ impl<'t> Walker<'t> {
 
     /// walkCsharpTypePosition (5955).
     fn walk_type_position(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         match node.kind() {
             "predefined_type" => {}
             "identifier" => {
@@ -1362,6 +1373,7 @@ impl<'t> Walker<'t> {
     /// normalizeValue (function-ref.ts:525) for CSHARP_SPEC: bare identifiers,
     /// the transparent `argument` layer, and the `this.Member` special.
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1412,6 +1424,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 8 - 0
codegraph-kernel/src/dart.rs

@@ -596,6 +596,7 @@ impl<'t> Walker<'t> {
     // --- the main walk (visitNode, tree-sitter.ts:936-1303) ---------------
 
     fn visit(&mut self, node: Node<'t>) {
+        stack_guard!();
         // The visitNode hook (dart.ts:144-157) — the constants branch.
         if node.kind() == "static_final_declaration" {
             let mut cursor = node.walk();
@@ -669,6 +670,7 @@ impl<'t> Walker<'t> {
     // --- extractFunction / extractMethod (:1517 / :1737) ------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // No receiver hook. Name first (resolveName inside extract_name).
         let name = self.extract_name(node);
         if name == "<anonymous>" {
@@ -770,6 +772,7 @@ impl<'t> Walker<'t> {
     // --- extractClass (:1679) — classes, mixins, extensions ---------------
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let resolved_body = self.resolve_body(node);
         // No skipBodilessClass. Anonymous `extension on String` → the name
         // fallback finds the ON type's type_identifier — a class named after
@@ -800,6 +803,7 @@ impl<'t> Walker<'t> {
     // --- extractEnum (:1914) ----------------------------------------------
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let body = match self.resolve_body(node) {
             Some(b) => b,
             None => return,
@@ -1221,6 +1225,7 @@ impl<'t> Walker<'t> {
     }
 
     fn type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let name = self.text(node);
             if !name.is_empty() && !is_builtin_type(name) {
@@ -1239,6 +1244,7 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody (:5129-5286) — dart rows -----------------------
 
     fn visit_body(&mut self, node: Node<'t>) {
+        stack_guard!();
         self.maybe_capture_fn_refs(node);
 
         let kind = node.kind();
@@ -1348,6 +1354,7 @@ impl<'t> Walker<'t> {
     /// normalizeValue with DART_SPEC's one layer (`argument` → fan out).
     /// Named arguments are NOT captured (named_argument is not a layer).
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1378,6 +1385,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 9 - 0
codegraph-kernel/src/go.rs

@@ -380,6 +380,7 @@ impl<'t> Walker<'t> {
     // --- visitNode ------------------------------------------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -417,10 +418,12 @@ impl<'t> Walker<'t> {
     }
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -448,6 +451,7 @@ impl<'t> Walker<'t> {
     // --- extractors --------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // (getReceiverType only matches method_declaration's receiver field —
         // function_declaration has none, so no reroute happens here)
         let name = self.extract_name(node);
@@ -524,6 +528,7 @@ impl<'t> Walker<'t> {
 
     /// extractTypeAlias for Go: type_spec → struct / interface / plain alias.
     fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             return false;
@@ -842,6 +847,7 @@ impl<'t> Walker<'t> {
     /// (constraint_elem) and struct embedding (field_declaration without a
     /// field_identifier), plus the field_declaration_list recursion.
     fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        stack_guard!();
         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 };
@@ -894,6 +900,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let type_name = self.text(node).to_string();
             if !type_name.is_empty() && !is_builtin_type(&type_name) {
@@ -980,6 +987,7 @@ impl<'t> Walker<'t> {
     /// normalizeValue with GO_SPEC's transparent layers (literal_element,
     /// expression_list — both fan out to named children).
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1010,6 +1018,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 11 - 0
codegraph-kernel/src/java.rs

@@ -485,6 +485,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, Java-relevant branches) -----------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -534,10 +535,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody ----------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -577,6 +580,7 @@ impl<'t> Walker<'t> {
     // --- extractors --------------------------------------------------------------
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -600,6 +604,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         if !self.inside_class_like() {
             // (object-literal parents don't exist in Java; a stray top-level
             // method extracts as a function, mirroring extractMethod's tail)
@@ -627,6 +632,7 @@ impl<'t> Walker<'t> {
 
     /// extractFunction — only reachable for a method outside any class.
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -653,6 +659,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -671,6 +678,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -902,6 +910,7 @@ impl<'t> Walker<'t> {
 
     /// extractAnonymousClass — `new T() { ... }`.
     fn extract_anonymous_class(&mut self, node: Node<'t>, body: Node<'t>) {
+        stack_guard!();
         let type_node = node
             .child_by_field_name("constructor")
             .or_else(|| node.child_by_field_name("type"))
@@ -1101,6 +1110,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let type_name = self.text(node).to_string();
             if !type_name.is_empty() && !is_builtin_type(&type_name) {
@@ -1193,6 +1203,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 10 - 0
codegraph-kernel/src/kotlin.rs

@@ -648,6 +648,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, Kotlin-relevant branches) -----------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         if self.try_visit_hook(node) {
             self.scan_fn_ref_subtree(node, 0);
             return;
@@ -719,10 +720,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody ----------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -777,6 +780,7 @@ impl<'t> Walker<'t> {
     // --- extractors ------------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // getReceiverType short-circuit (1522) — extension fns at any scope.
         if self.receiver_type_of(node).is_some() {
             self.extract_method(node);
@@ -810,6 +814,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let receiver = self.receiver_type_of(node);
         let name = self.extract_name(node);
         let qualified_override = receiver.as_ref().map(|r| format!("{r}::{name}"));
@@ -861,6 +866,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let resolved_body = self.resolve_body(node);
         let name = self.extract_name(node);
         let extra = Extra {
@@ -887,6 +893,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -905,6 +912,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = self.resolve_body(node) else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -1283,6 +1291,7 @@ impl<'t> Walker<'t> {
     }
 
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1362,6 +1371,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 37 - 17
codegraph-kernel/src/lib.rs

@@ -16,6 +16,20 @@
 
 #![deny(clippy::all)]
 
+/// First statement of every recursive walker function (see stack.rs, #1581):
+/// once the stack pointer is inside the red zone, stop descending — the
+/// latched flag makes `stack::run_guarded` discard the walk and defer the
+/// file to wasm. `Default::default()` covers every walker return type in use
+/// (`()`, `bool`, `Option<_>`, `String`); a hook returning `false` just sends
+/// its caller down the generic child walk, whose own guard returns at once.
+macro_rules! stack_guard {
+    () => {
+        if $crate::stack::exhausted() {
+            return ::core::default::Default::default();
+        }
+    };
+}
+
 mod buffers;
 mod ccpp;
 mod cfnptr;
@@ -33,6 +47,7 @@ mod rlang;
 mod ruby;
 mod rustlang;
 mod scala;
+mod stack;
 mod swift;
 mod textutil;
 mod python;
@@ -216,23 +231,28 @@ pub fn cfnptr_strip_c(text: String) -> String {
 
 #[napi]
 pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
-    let out = match language.as_str() {
-        "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)?,
-        "rust" => rustlang::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "csharp" => csharp::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "ruby" => ruby::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "php" => php::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "swift" => swift::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "kotlin" => kotlin::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "r" => rlang::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "lua" | "luau" => lua::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
-        "scala" => scala::extract(&file_path, &content).map_err(Error::from_reason)?,
-        "dart" => dart::extract(&file_path, &content).map_err(Error::from_reason)?,
-        _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
-    };
+    // The whole walk runs under the stack guard (stack.rs, #1581): a file
+    // nested deeply enough to overflow this thread's stack comes back as a
+    // `defer:` error — the TS side's routine "take the wasm path" signal —
+    // instead of a SIGSEGV that kills the entire indexer process.
+    let out = stack::run_guarded(|| match language.as_str() {
+        "java" => java::extract(&file_path, &content),
+        "python" => python::extract(&file_path, &content),
+        "go" => go::extract(&file_path, &content),
+        "c" | "cpp" => ccpp::extract(&file_path, &content, &language),
+        "rust" => rustlang::extract(&file_path, &content),
+        "csharp" => csharp::extract(&file_path, &content),
+        "ruby" => ruby::extract(&file_path, &content),
+        "php" => php::extract(&file_path, &content),
+        "swift" => swift::extract(&file_path, &content),
+        "kotlin" => kotlin::extract(&file_path, &content),
+        "r" => rlang::extract(&file_path, &content),
+        "lua" | "luau" => lua::extract(&file_path, &content, &language),
+        "scala" => scala::extract(&file_path, &content),
+        "dart" => dart::extract(&file_path, &content),
+        _ => tsjs::extract(&file_path, &content, &language),
+    })
+    .map_err(Error::from_reason)?;
     Ok(ExtractBuffers {
         meta: out.meta.into(),
         nodes: out.nodes.into(),

+ 6 - 0
codegraph-kernel/src/lua.rs

@@ -418,6 +418,7 @@ impl<'t> Walker<'t> {
     // --- the main walk (visitNode, tree-sitter.ts:936-1303) ---------------
 
     fn visit(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
 
         // The visitNode hook (lua.ts:105-151) runs FIRST.
@@ -487,6 +488,7 @@ impl<'t> Walker<'t> {
     // --- extractFunction / extractMethod (1517 / 1737) --------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // :1522 receiver short-circuit IS the method routing.
         if let Some(receiver) = self.receiver_type(node) {
             let receiver = receiver.to_string();
@@ -522,6 +524,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>, receiver: String) {
+        stack_guard!();
         let name = self.extract_name(node);
         let docstring = preceding_docstring(node, self.src);
         let signature = self.signature_of(node);
@@ -654,6 +657,7 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody (5129-5286) — the hook-free body walk ----------
 
     fn visit_body(&mut self, node: Node<'t>) {
+        stack_guard!();
         // maybeCaptureFnRefs (5137) fires in the body walker too.
         self.maybe_capture_fn_refs(node);
 
@@ -750,6 +754,7 @@ impl<'t> Walker<'t> {
     /// normalizeValue with LUA_SPEC's one transparent layer (expression_list
     /// fans out to named children).
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -780,6 +785,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 12 - 0
codegraph-kernel/src/php.rs

@@ -530,6 +530,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, PHP-relevant branches) ------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         if self.try_visit_hook(node) {
             self.scan_fn_ref_subtree(node, 0);
             return;
@@ -609,10 +610,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody --------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -671,6 +674,7 @@ impl<'t> Walker<'t> {
     // --- extractors ----------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -696,6 +700,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -715,6 +720,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_class(&mut self, node: Node<'t>, kind: &'static str) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -736,6 +742,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -754,6 +761,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -1080,6 +1088,7 @@ impl<'t> Walker<'t> {
     /// nests inside `anonymous_class`, so findAnonymousClassBody finds no
     /// DIRECT child) — mirrored from the shared TS path for shape.
     fn extract_anonymous_class(&mut self, node: Node<'t>, body: Node<'t>) {
+        stack_guard!();
         let type_node = node
             .child_by_field_name("constructor")
             .or_else(|| node.child_by_field_name("type"))
@@ -1206,6 +1215,7 @@ impl<'t> Walker<'t> {
     }
 
     fn walk_php_type_position(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         match node.kind() {
             "primitive_type" => {}
             "name" => {
@@ -1249,6 +1259,7 @@ impl<'t> Walker<'t> {
     }
 
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1337,6 +1348,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 7 - 0
codegraph-kernel/src/python.rs

@@ -320,6 +320,7 @@ impl<'t> Walker<'t> {
     // --- visitNode ------------------------------------------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -356,10 +357,12 @@ impl<'t> Walker<'t> {
     }
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -390,6 +393,7 @@ impl<'t> Walker<'t> {
     // --- extractors --------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -414,6 +418,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -431,6 +436,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -789,6 +795,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 6 - 0
codegraph-kernel/src/rlang.rs

@@ -298,6 +298,7 @@ impl<'t> Walker<'t> {
     // else plain recursion over namedChildren in order.
 
     fn visit(&mut self, node: Node<'t>) {
+        stack_guard!();
         if self.hook(node) {
             return;
         }
@@ -313,6 +314,7 @@ impl<'t> Walker<'t> {
 
     /// The visitNode hook (r.ts:180-309). Returns true when consumed.
     fn hook(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         match node.kind() {
             "call" => self.hook_call(node),
             "binary_operator" => self.hook_binary_operator(node),
@@ -321,6 +323,7 @@ impl<'t> Walker<'t> {
     }
 
     fn hook_call(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         let fname = match self.callee_name(node) {
             Some(f) => f,
             None => return false,
@@ -405,6 +408,7 @@ impl<'t> Walker<'t> {
     }
 
     fn hook_binary_operator(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         let op = match node.child_by_field_name("operator") {
             Some(o) => self.text(o),
             None => return false,
@@ -477,6 +481,7 @@ impl<'t> Walker<'t> {
     /// `list(…)` entries become methods. Non-method argument subtrees are
     /// NEVER visited (`representation(…)`, `signature(…)` invisible).
     fn extract_class_members(&mut self, class_call: Node<'t>, class_row: u32) {
+        stack_guard!();
         let args = match class_call.child_by_field_name("arguments") {
             Some(a) => a,
             None => return,
@@ -543,6 +548,7 @@ impl<'t> Walker<'t> {
     /// `method` node positioned at the ARGUMENT node, signature from the raw
     /// parameters text, body walked hook-aware inside the method scope.
     fn emit_method_arg(&mut self, entry: Node<'t>) {
+        stack_guard!();
         let entry_name = match entry.child_by_field_name("name") {
             Some(n) => n,
             None => return,

+ 9 - 0
codegraph-kernel/src/ruby.rs

@@ -383,6 +383,7 @@ impl<'t> Walker<'t> {
     /// the source of the module multiply-capture quirk (scan runs with the
     /// module already POPPED, so candidates re-attribute to the outer scope).
     fn try_visit_hook(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         let kind = node.kind();
         if kind == "call" && node.child_by_field_name("receiver").is_none() {
             if let Some(method) = node.child_by_field_name("method") {
@@ -451,6 +452,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, Ruby-relevant branches) ----------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         // Language hook FIRST (tree-sitter.ts:943) — a handled subtree is
         // scanned for fn-ref candidates and never reaches the ladder (or the
         // maybeCaptureFnRefs call below).
@@ -522,10 +524,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody ------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -596,6 +600,7 @@ impl<'t> Walker<'t> {
     // --- extractors --------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -619,6 +624,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -634,6 +640,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -872,6 +879,7 @@ impl<'t> Walker<'t> {
     /// qualify); `block_argument` is a transparent layer; specials are the
     /// `method(:sym)` call form and hook-DSL `simple_symbol`s.
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -938,6 +946,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 80 - 60
codegraph-kernel/src/rustlang.rs

@@ -11,10 +11,11 @@
 //! - impl blocks push NO scope: members re-dispatch at file scope, so an impl
 //!   associated `const` becomes a FILE-level `variable`, and the method↔owner
 //!   `contains` edge is a source-order name scan (an impl ABOVE its struct
-//!   gets no edge). `impl Trait for Generic<T>`'s receiver resolves to the
-//!   TRAIT (the only direct type_identifier), and methods get QN
-//!   `Trait::method` — preserve, never "fix" via the grammar's trait:/type:
-//!   fields.
+//!   gets no edge). The receiver (method QN prefix, `contains` owner,
+//!   `implements` source) is the impl_item's `type` field via
+//!   impl_type_name — both sides moved to the grammar's trait:/type: fields
+//!   together in #1588 (the earlier positional scan qualified every
+//!   parameterized impl's methods by the TRAIT).
 //! - `const_item`/`static_item` ride the generic extractVariable fallback:
 //!   kind is always `variable`, no signature, and EVERY direct `identifier`
 //!   child mints a node (`const MAX: u32 = OTHER;` → two nodes, `MAX` + the
@@ -22,10 +23,12 @@
 //! - Unit structs (`struct Unit;`, no body field) mint NO node; `mod_item`
 //!   mints no module node and adds no QN prefix.
 //! - Chained-call re-encode is scoped_identifier-gated (`Foo::new().bar()` →
-//!   `Foo::new().bar`); instance chains, parens, `.await`, 2-hop fields, and
-//!   `self` receivers all collapse to the bare method name (`self` is node
-//!   kind `self`, not `identifier`, so it dodges SKIP_RECEIVERS by falling
-//!   through). Turbofish callees keep the raw `helper::<T>` text.
+//!   `Foo::new().bar`); a call through a field of the enclosing type keeps
+//!   the owner-field shape (`self.inner.run()` → `self.inner.run`, #1585);
+//!   instance chains, parens, `.await`, deeper/non-self field chains, and
+//!   bare `self` receivers all collapse to the bare method name (`self` is
+//!   node kind `self`, not `identifier`, so it dodges SKIP_RECEIVERS by
+//!   falling through). Turbofish callees keep the raw `helper::<T>` text.
 //! - `use` emits an import node named by the ROOT module (`crate`/`self`/…),
 //!   one root `imports` ref, then one FULL-path `imports` ref per binding;
 //!   `use x::*` (use_wildcard) emits nothing at all.
@@ -399,33 +402,35 @@ impl<'t> Walker<'t> {
         Some(if last == "Self" { "self".to_string() } else { last.to_string() })
     }
 
-    /// rustExtractor.getReceiverType: parent-walk to the nearest impl_item;
-    /// LAST direct type_identifier child wins (for `impl Trait for Generic<T>`
-    /// that's the TRAIT — bug preserved); else the first generic_type's inner
-    /// type_identifier.
+    /// rustImplTypeName (languages/rust.ts) — the implementing type's simple
+    /// name for an impl block, from the grammar's `type` field (#1588):
+    /// `impl<T> Tr for G<T>` / `impl<'a> Iterator for Parents<'a>` /
+    /// `impl Tr for &Foo` / `impl Tr for m::Foo` → `G` / `Parents` / `Foo` /
+    /// `Foo`. Shapes naming no single type (tuple, `dyn Tr`, pointer,
+    /// primitive, fn type…) → None. Mirrored byte-for-byte — change both.
+    fn impl_type_name(&self, ty: Option<Node>) -> Option<String> {
+        let ty = ty?;
+        match ty.kind() {
+            "type_identifier" | "identifier" => Some(self.text(ty).to_string()),
+            "generic_type" => self.impl_type_name(ty.child_by_field_name("type")),
+            "scoped_type_identifier" | "scoped_identifier" => {
+                self.impl_type_name(ty.child_by_field_name("name"))
+            }
+            "reference_type" => self.impl_type_name(ty.child_by_field_name("type")),
+            _ => None,
+        }
+    }
+
+    /// rustExtractor.getReceiverType: parent-walk to the nearest impl_item and
+    /// read its `type` field (impl_type_name). The pre-#1588 rule took the
+    /// LAST direct type_identifier child, which for `impl Trait for Generic<T>`
+    /// was the TRAIT — so every parameterized impl's methods were qualified by
+    /// the trait.
     fn receiver_type_of(&self, node: Node) -> Option<String> {
         let mut parent = node.parent();
         while let Some(p) = parent {
             if p.kind() == "impl_item" {
-                let type_idents: Vec<Node> = (0..p.named_child_count())
-                    .filter_map(|i| p.named_child(i))
-                    .filter(|c| c.kind() == "type_identifier")
-                    .collect();
-                if let Some(last) = type_idents.last() {
-                    return Some(self.text(*last).to_string());
-                }
-                let generic = (0..p.named_child_count())
-                    .filter_map(|i| p.named_child(i))
-                    .find(|c| c.kind() == "generic_type");
-                if let Some(g) = generic {
-                    let inner = (0..g.named_child_count())
-                        .filter_map(|i| g.named_child(i))
-                        .find(|c| c.kind() == "type_identifier");
-                    if let Some(inner) = inner {
-                        return Some(self.text(inner).to_string());
-                    }
-                }
-                return None;
+                return self.impl_type_name(p.child_by_field_name("type"));
             }
             parent = p.parent();
         }
@@ -435,6 +440,7 @@ impl<'t> Walker<'t> {
     // --- visitNode ------------------------------------------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -498,6 +504,7 @@ impl<'t> Walker<'t> {
     /// impl method's body, whose parent walk passes through the outer fn) or
     /// the stack top is class-like (trait members).
     fn extract_fn_or_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let receiver = self.receiver_type_of(node);
         let as_method = receiver.is_some() || self.inside_class_like();
 
@@ -564,6 +571,7 @@ impl<'t> Walker<'t> {
     /// extractInterface — kind `trait` (interfaceKind), inheritance from
     /// trait_bounds, body children visited with the trait pushed.
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -584,6 +592,7 @@ impl<'t> Walker<'t> {
 
     /// Extract a Rust struct or union with a body; unit structs remain skipped.
     fn extract_aggregate(&mut self, node: Node<'t>, kind: &'static str) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -606,6 +615,7 @@ impl<'t> Walker<'t> {
     /// extractEnum — body required; enum_variant children → enum_member nodes
     /// (name field only, payloads never walked); other children re-dispatched.
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -700,6 +710,7 @@ impl<'t> Walker<'t> {
 
     /// getRootModule (languages/rust.ts:124).
     fn root_module(&self, n: Node) -> String {
+        stack_guard!();
         let Some(first) = n.named_child(0) else {
             return self.text(n).to_string();
         };
@@ -719,6 +730,7 @@ impl<'t> Walker<'t> {
             if prefix.is_empty() { seg.to_string() } else { format!("{prefix}::{seg}") }
         }
         fn collect<'t>(w: &Walker<'t>, n: Node<'t>, prefix: &str, paths: &mut Vec<(String, Node<'t>)>) {
+            stack_guard!();
             match n.kind() {
                 "identifier" => paths.push((join(prefix, w.text(n)), n)),
                 "scoped_identifier" => {
@@ -835,9 +847,29 @@ impl<'t> Walker<'t> {
                                     callee_name = method_name.to_string();
                                 }
                             }
+                            "field_expression" => {
+                                // `self.<field>.<method>()` — a call through a
+                                // field of the enclosing type (#1585): keep the
+                                // `self.` prefix so the resolver can type the
+                                // field from the owner struct's declaration
+                                // (or leave it unresolved). Any other
+                                // field_expression receiver — a deeper chain,
+                                // a non-self base — keeps the bare name.
+                                let base = r.child_by_field_name("value");
+                                let field = r.child_by_field_name("field");
+                                match (base, field) {
+                                    (Some(b), Some(f))
+                                        if b.kind() == "self" && f.kind() == "field_identifier" =>
+                                    {
+                                        let field_name = self.text(f);
+                                        callee_name = format!("self.{field_name}.{method_name}");
+                                    }
+                                    _ => callee_name = method_name.to_string(),
+                                }
+                            }
                             _ => {
-                                // field_expression 2-hop, parenthesized,
-                                // await_expression, `self` — bare method name.
+                                // parenthesized, await_expression, `self` —
+                                // bare method name.
                                 callee_name = method_name.to_string();
                             }
                         }
@@ -966,6 +998,7 @@ impl<'t> Walker<'t> {
     /// every field has a field_identifier), and the field_declaration_list
     /// recursion that reaches it.
     fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        stack_guard!();
         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 };
@@ -1024,36 +1057,19 @@ impl<'t> Walker<'t> {
         }
     }
 
-    /// extractRustImplItem — `impl Trait for Type` back-reference: positional
-    /// type-node filter (NEVER the grammar's trait:/type: fields), ≥2 needed,
-    /// target found by FIRST earlier node of kind struct/enum/class (never
-    /// trait); ref FROM the type's node, named by the trait's full text.
+    /// extractRustImplItem — `impl Trait for Type` back-reference from the
+    /// grammar's `trait` / `type` fields (#1588; an inherent impl has no
+    /// `trait` field and emits nothing). Target = FIRST earlier node of kind
+    /// struct/union/enum/class (never trait) named by impl_type_name; ref FROM
+    /// the type's node, named by the trait's full text (scoped path / generic
+    /// args kept), at the trait node's position.
     fn extract_rust_impl_item(&mut self, node: Node<'t>) {
-        let has_for = (0..node.child_count())
-            .filter_map(|i| node.child(i))
-            .any(|c| c.kind() == "for" && !c.is_named());
-        if !has_for {
+        let Some(trait_node) = node.child_by_field_name("trait") else {
             return;
-        }
-        let type_idents: Vec<Node> = (0..node.named_child_count())
-            .filter_map(|i| node.named_child(i))
-            .filter(|c| matches!(c.kind(), "type_identifier" | "generic_type" | "scoped_type_identifier"))
-            .collect();
-        if type_idents.len() < 2 {
-            return;
-        }
-        let trait_node = type_idents[0];
-        let type_node = type_idents[type_idents.len() - 1];
-
+        };
         let trait_name = self.text(trait_node).to_string();
-        let type_name = if type_node.kind() == "generic_type" {
-            (0..type_node.named_child_count())
-                .filter_map(|i| type_node.named_child(i))
-                .find(|c| c.kind() == "type_identifier")
-                .map(|c| self.text(c).to_string())
-                .unwrap_or_else(|| self.text(type_node).to_string())
-        } else {
-            self.text(type_node).to_string()
+        let Some(type_name) = self.impl_type_name(node.child_by_field_name("type")) else {
+            return;
         };
 
         let target_row = self
@@ -1086,6 +1102,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let type_name = self.text(node).to_string();
             if !type_name.is_empty() && !is_builtin_type(&type_name) {
@@ -1103,10 +1120,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody -----------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -1262,6 +1281,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 12 - 0
codegraph-kernel/src/scala.rs

@@ -471,6 +471,7 @@ impl<'t> Walker<'t> {
 
     /// scalaBaseTypeName (tree-sitter.ts:201-224).
     fn scala_base_type_name(&self, node: Option<Node<'t>>) -> Option<String> {
+        stack_guard!();
         let node = node?;
         match node.kind() {
             "type_identifier" | "identifier" => Some(self.text(node).to_string()),
@@ -495,6 +496,7 @@ impl<'t> Walker<'t> {
 
     /// emitScalaTypeRefs (scala.ts:27-45) — the hook's own builtin set.
     fn emit_scala_type_refs(&mut self, type_node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if type_node.kind() == "type_identifier" {
             let name = self.text(type_node);
             if !name.is_empty() && !is_scala_builtin(name) {
@@ -529,6 +531,7 @@ impl<'t> Walker<'t> {
     // --- the main walk (visitNode, tree-sitter.ts:936-1303) ---------------
 
     fn visit(&mut self, node: Node<'t>) {
+        stack_guard!();
         // The visitNode hook (scala.ts:131-198) runs FIRST.
         if self.hook(node) {
             self.scan_fn_ref_subtree(node, 0);
@@ -591,6 +594,7 @@ impl<'t> Walker<'t> {
 
     /// The visitNode hook (scala.ts:131-198). Returns true when consumed.
     fn hook(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         match node.kind() {
             "val_definition" | "var_definition" => {
                 let is_val = node.kind() == "val_definition";
@@ -691,6 +695,7 @@ impl<'t> Walker<'t> {
     // --- extractMethod → extractFunction routing (:1737 / :1517) ----------
 
     fn extract_method_or_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         // No receiver hook, no methodsAreTopLevel: inside class-like → method,
         // else → function (the object/object_expression parent check never
         // matches scala node kinds).
@@ -735,6 +740,7 @@ impl<'t> Walker<'t> {
     // --- extractClass (:1679) — classes, objects, traits ------------------
 
     fn extract_class(&mut self, node: Node<'t>, kind: &'static str) {
+        stack_guard!();
         let resolved_body = node.child_by_field_name("body"); // template_body
         // No skipBodilessClass — bodiless mints (scala-complete).
         let name = self.extract_name(node);
@@ -765,6 +771,7 @@ impl<'t> Walker<'t> {
     // --- extractEnum (:1914) ----------------------------------------------
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let body = match node.child_by_field_name("body") {
             Some(b) => b,
             None => return, // bodiless enum mints nothing
@@ -812,6 +819,7 @@ impl<'t> Walker<'t> {
     // --- extractImport (:3170-3236) ---------------------------------------
 
     fn extract_import(&mut self, node: Node<'t>) {
+        stack_guard!();
         let import_text = self.text(node).trim();
         // extractImport hook (scala.ts:200-211): `path` field is FIRST-MATCH-
         // WINS → the FIRST dotted segment names the import.
@@ -1133,6 +1141,7 @@ impl<'t> Walker<'t> {
     }
 
     fn type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let name = self.text(node);
             if !name.is_empty() && !is_builtin_type(name) {
@@ -1151,6 +1160,7 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody (:5129-5286) — scala rows ----------------------
 
     fn visit_body(&mut self, node: Node<'t>) {
+        stack_guard!();
         self.maybe_capture_fn_refs(node);
 
         let kind = node.kind();
@@ -1266,6 +1276,7 @@ impl<'t> Walker<'t> {
     /// normalizeValue with SCALA_SPEC's unwrap (postfix_expression → first
     /// named child — eta-expansion `handler _`). No layers.
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1294,6 +1305,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 273 - 0
codegraph-kernel/src/stack.rs

@@ -0,0 +1,273 @@
+//! Stack-budget guard for the recursive walkers (#1581).
+//!
+//! Every language walker recurses per AST level (`visit_node` →
+//! `visit_for_calls_and_structure` → …). tree-sitter's own parser is
+//! iterative, so a pathologically nested file — clang's
+//! `parser_overflow.c` nests 16,384 `{`, fuzzer corpora go deeper — parses
+//! fine and then overflows the WALKER's native stack. A native overflow is
+//! uncatchable: the parse worker is a thread of the `codegraph` process, so
+//! the SIGSEGV takes the whole indexer down with no message, no partial
+//! index and no per-file fallback. Worker threads get Node's 4 MiB default
+//! stack; the main thread's 8 MiB only moves the cliff (100k levels still
+//! kill it).
+//!
+//! The guard turns "about to overflow" into the kernel's existing `defer:`
+//! routing signal: `exhausted()` is checked at the top of every recursive
+//! walker function (the `stack_guard!` macro in lib.rs), returns `true` once
+//! the stack pointer is within `RED_ZONE` of the thread's stack limit, and
+//! latches a per-thread flag. `run_guarded` wraps a whole extraction: when
+//! the flag is set afterwards the result is discarded and replaced by a
+//! `defer:` error, which the TS side (`src/extraction/kernel/index.ts`)
+//! already treats as "this file takes the wasm path" — and the wasm walker
+//! catches its own JS `RangeError` per file, so the file lands as a partial
+//! result with a recorded parse error instead of a dead process.
+//!
+//! The per-thread stack bounds come from the OS (glibc/musl
+//! `pthread_getattr_np`, macOS `pthread_get_stackaddr_np`, Win32
+//! `GetCurrentThreadStackLimits`), computed once per thread and cached, so
+//! the guard is exact on the 4 MiB worker, the 8 MiB main thread and any
+//! `resourceLimits.stackSizeMb` alike. Where the bounds are unavailable the
+//! guard falls back to a fixed descent budget measured from the entry stack
+//! pointer. Hot path: one thread-local load and one compare.
+
+use std::cell::Cell;
+
+/// Headroom kept free below the deepest walker frame: the napi return path,
+/// tree-sitter's node accessors and the error formatting all still need to
+/// run after the guard trips, and the frames BETWEEN two guard checks (an
+/// `extract_class` between two `visit_node`s) are never more than a few KiB.
+const RED_ZONE: usize = 256 * 1024;
+
+/// Descent budget when the OS can't report the thread's stack bounds — safe
+/// on anything from Node's 4 MiB worker default upwards.
+const FALLBACK_BUDGET: usize = 1024 * 1024;
+
+thread_local! {
+    /// Lowest stack-pointer value the walker may reach before the guard
+    /// trips. `0` = not computed yet for this thread.
+    static THRESHOLD: Cell<usize> = const { Cell::new(0) };
+    /// `true` when the thread's threshold came from real OS bounds (fixed
+    /// for the thread's lifetime) rather than the per-call fallback budget.
+    static THRESHOLD_IS_OS: Cell<bool> = const { Cell::new(false) };
+    /// Latched by `exhausted()`; read by `run_guarded` after the walk.
+    static OVERFLOWED: Cell<bool> = const { Cell::new(false) };
+}
+
+/// Approximate current stack pointer: the address of a local. Stacks grow
+/// downward on every target the kernel ships for (x86_64 / aarch64).
+#[inline(always)]
+fn current_sp() -> usize {
+    let marker = 0u8;
+    std::hint::black_box(&marker) as *const u8 as usize
+}
+
+/// Low (deepest) address of the calling thread's stack, from the OS.
+#[cfg(target_os = "linux")]
+fn os_stack_low() -> Option<usize> {
+    // SAFETY: plain pthread queries on the calling thread; `attr` is
+    // initialised by pthread_getattr_np and destroyed before returning.
+    unsafe {
+        let mut attr: libc::pthread_attr_t = std::mem::zeroed();
+        if libc::pthread_getattr_np(libc::pthread_self(), &mut attr) != 0 {
+            return None;
+        }
+        let mut addr: *mut libc::c_void = std::ptr::null_mut();
+        let mut size: libc::size_t = 0;
+        let rc = libc::pthread_attr_getstack(&attr, &mut addr, &mut size);
+        libc::pthread_attr_destroy(&mut attr);
+        if rc != 0 || addr.is_null() || size == 0 {
+            return None;
+        }
+        Some(addr as usize)
+    }
+}
+
+#[cfg(target_os = "macos")]
+fn os_stack_low() -> Option<usize> {
+    // SAFETY: plain pthread queries on the calling thread.
+    unsafe {
+        let me = libc::pthread_self();
+        // pthread_get_stackaddr_np returns the HIGH end (the stack base).
+        let high = libc::pthread_get_stackaddr_np(me) as usize;
+        let size = libc::pthread_get_stacksize_np(me);
+        if high == 0 || size == 0 || size > high {
+            return None;
+        }
+        Some(high - size)
+    }
+}
+
+#[cfg(windows)]
+fn os_stack_low() -> Option<usize> {
+    #[link(name = "kernel32")]
+    extern "system" {
+        // Win8+ (the bundled Node runtime needs Win10 anyway). Reports the
+        // full RESERVED range; Windows commits pages on demand down to it.
+        fn GetCurrentThreadStackLimits(low_limit: *mut usize, high_limit: *mut usize);
+    }
+    let mut low: usize = 0;
+    let mut high: usize = 0;
+    // SAFETY: both out-pointers are valid for the duration of the call.
+    unsafe { GetCurrentThreadStackLimits(&mut low, &mut high) };
+    if low == 0 || high <= low {
+        return None;
+    }
+    Some(low)
+}
+
+#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
+fn os_stack_low() -> Option<usize> {
+    None
+}
+
+/// Arm the guard for one extraction on the calling thread: clear the latch
+/// and (re)compute the threshold. OS bounds are computed once per thread;
+/// the fallback budget is re-anchored at every call's entry stack pointer.
+pub fn begin() {
+    OVERFLOWED.with(|o| o.set(false));
+    let cached = THRESHOLD.with(|t| t.get());
+    if cached != 0 && THRESHOLD_IS_OS.with(|f| f.get()) {
+        return;
+    }
+    match os_stack_low() {
+        Some(low) => {
+            THRESHOLD.with(|t| t.set(low.saturating_add(RED_ZONE)));
+            THRESHOLD_IS_OS.with(|f| f.set(true));
+        }
+        None => {
+            THRESHOLD.with(|t| t.set(current_sp().saturating_sub(FALLBACK_BUDGET).max(1)));
+            THRESHOLD_IS_OS.with(|f| f.set(false));
+        }
+    }
+}
+
+/// `true` once the walker has descended to within `RED_ZONE` of the stack
+/// limit. Latches `OVERFLOWED` so `run_guarded` can discard the result. A
+/// thread that never called `begin()` (a direct unit-test call) arms itself
+/// lazily from the current position.
+#[inline(always)]
+pub fn exhausted() -> bool {
+    let threshold = THRESHOLD.with(|t| t.get());
+    if threshold == 0 {
+        begin();
+        return exhausted();
+    }
+    if current_sp() < threshold {
+        OVERFLOWED.with(|o| o.set(true));
+        true
+    } else {
+        false
+    }
+}
+
+/// Whether the guard tripped since the last `begin()`.
+pub fn overflowed() -> bool {
+    OVERFLOWED.with(|o| o.get())
+}
+
+/// Run one extraction under the guard. A walk that tripped the guard returns
+/// a `defer:` error — the TS side's routine "take the wasm path" signal —
+/// regardless of what the truncated walk produced.
+pub fn run_guarded<T>(f: impl FnOnce() -> Result<T, String>) -> Result<T, String> {
+    begin();
+    let out = f();
+    if overflowed() {
+        return Err(
+            "defer: nesting too deep for the native walker — wasm recovery handles it".to_string(),
+        );
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// 1 MiB is a quarter of Node's worker default; a guard that holds here
+    /// holds on every real thread. Without the guard these walks SIGSEGV the
+    /// test process instead of failing an assertion.
+    const SMALL_STACK: usize = 1 << 20;
+    const DEPTH: usize = 30_000;
+
+    fn on_small_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
+        std::thread::Builder::new()
+            .stack_size(SMALL_STACK)
+            .spawn(f)
+            .expect("spawn")
+            .join()
+            .expect("walker thread panicked")
+    }
+
+    fn nested_parens(prefix: &str, suffix: &str) -> String {
+        format!("{prefix}{}1{}{suffix}", "(".repeat(DEPTH), ")".repeat(DEPTH))
+    }
+
+    #[test]
+    fn os_bounds_are_sane_on_this_platform() {
+        // Every shipped target has an OS implementation; the fallback budget
+        // is only for platforms the kernel is not built for.
+        let low = os_stack_low().expect("OS stack bounds available");
+        let sp = current_sp();
+        assert!(low < sp, "stack low {low:#x} must be below the current sp {sp:#x}");
+        assert!(sp - low < 1 << 31, "implausible stack size {}", sp - low);
+    }
+
+    #[test]
+    fn small_stack_reports_its_own_bounds() {
+        on_small_stack(|| {
+            let low = os_stack_low().expect("OS stack bounds available");
+            let used = current_sp() - low;
+            // std/the OS round the requested size up a little (macOS reports
+            // 1,060,864 for a 1 MiB request); the point is that the bounds
+            // describe THIS thread's small stack, not the main thread's.
+            assert!(
+                used <= SMALL_STACK + 128 * 1024,
+                "used {used} is not within the {SMALL_STACK}-byte stack"
+            );
+        });
+    }
+
+    #[test]
+    fn deep_braces_c_defer_instead_of_crashing() {
+        let src = format!("void foo(void) {{\n{}{}\n}}\n", "{".repeat(DEPTH), "}".repeat(DEPTH));
+        let r = on_small_stack(move || run_guarded(|| crate::ccpp::extract("deep.c", &src, "c")));
+        let err = r.err().expect("deep nesting must defer");
+        assert!(err.starts_with("defer:"), "unexpected error: {err}");
+    }
+
+    type Extract = fn(&str) -> Result<crate::buffers::EmitOut, String>;
+
+    #[test]
+    fn deep_parens_cpp_rust_ts_python_defer_instead_of_crashing() {
+        let cases: [(&str, Extract, String); 4] = [
+            ("deep.cpp", |s| crate::ccpp::extract("deep.cpp", s, "cpp"), nested_parens("int f() { return ", "; }\n")),
+            ("deep.rs", |s| crate::rustlang::extract("deep.rs", s), nested_parens("fn f() -> i32 { ", " }\n")),
+            ("deep.ts", |s| crate::tsjs::extract("deep.ts", s, "typescript"), nested_parens("function f() { return ", "; }\n")),
+            ("deep.py", |s| crate::python::extract("deep.py", s), nested_parens("def f():\n    return ", "\n")),
+        ];
+        for (name, extract, src) in cases {
+            let r = on_small_stack(move || run_guarded(|| extract(&src)));
+            let err = r.err().unwrap_or_else(|| panic!("{name}: deep nesting must defer"));
+            assert!(err.starts_with("defer:"), "{name}: unexpected error: {err}");
+        }
+    }
+
+    #[test]
+    fn normal_files_are_untouched_by_the_guard() {
+        let src = "int add(int a, int b) { return a + b; }\nint main(void) { return add(1, 2); }\n";
+        let r = on_small_stack(move || run_guarded(|| crate::ccpp::extract("ok.c", src, "c")));
+        assert!(r.is_ok(), "a shallow file must not defer: {:?}", r.err());
+        assert!(!overflowed());
+    }
+
+    #[test]
+    fn latch_resets_between_runs() {
+        let deep = format!("void foo(void) {{\n{}{}\n}}\n", "{".repeat(DEPTH), "}".repeat(DEPTH));
+        on_small_stack(move || {
+            assert!(run_guarded(|| crate::ccpp::extract("deep.c", &deep, "c")).is_err());
+            // The latch from the deep file must not poison the next, shallow one.
+            let ok = run_guarded(|| crate::ccpp::extract("ok.c", "int x;\n", "c"));
+            assert!(ok.is_ok(), "latch leaked into the next run: {:?}", ok.err());
+        });
+    }
+}

+ 15 - 0
codegraph-kernel/src/swift.rs

@@ -264,6 +264,7 @@ fn first_simple_identifier<'t>(node: Option<Node<'t>>) -> Option<Node<'t>> {
 /// lastNamedOfType (function-ref.ts:600): rightmost matching DESCENDANT in
 /// document order (deeper matches override).
 fn last_simple_identifier<'t>(node: Node<'t>) -> Option<Node<'t>> {
+    stack_guard!();
     let mut found: Option<Node<'t>> = None;
     for i in 0..node.named_child_count() {
         let Some(child) = node.named_child(i) else { continue };
@@ -560,6 +561,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode, Swift-relevant branches) -----------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -632,6 +634,7 @@ impl<'t> Walker<'t> {
     /// THE DEDICATED PROPERTY BRANCH (tree-sitter.ts:1113-1193, #1020).
     /// Returns skipChildren.
     fn dedicated_property_branch(&mut self, node: Node<'t>) -> bool {
+        stack_guard!();
         let owner_row = self.top_row();
         let info = self.swift_property_info(node);
         let mut computed_prop: Option<(u32, String)> = None;
@@ -707,6 +710,7 @@ impl<'t> Walker<'t> {
     }
 
     fn walk_attr_args(&mut self, n: Node<'t>) {
+        stack_guard!();
         self.extract_static_member_ref(n);
         for i in 0..n.named_child_count() {
             if let Some(c) = n.named_child(i) {
@@ -718,10 +722,12 @@ impl<'t> Walker<'t> {
     // --- visitFunctionBody ---------------------------------------------------------
 
     fn visit_function_body(&mut self, body: Node<'t>) {
+        stack_guard!();
         self.visit_for_calls_and_structure(body);
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 
@@ -775,6 +781,7 @@ impl<'t> Walker<'t> {
     // --- extractors -----------------------------------------------------------------
 
     fn extract_function(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         if name == "<anonymous>" {
             if let Some(body) = node.child_by_field_name("body") {
@@ -802,6 +809,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_method(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -823,6 +831,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_class(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -845,6 +854,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_struct(&mut self, node: Node<'t>) {
+        stack_guard!();
         // Body gate (:1876) — bodiless mints nothing (record exemption is C#).
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
@@ -866,6 +876,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_enum(&mut self, node: Node<'t>) {
+        stack_guard!();
         let Some(body) = node.child_by_field_name("body") else { return };
         let name = self.extract_name(node);
         let extra = Extra {
@@ -900,6 +911,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_interface(&mut self, node: Node<'t>) {
+        stack_guard!();
         let name = self.extract_name(node);
         let extra = Extra {
             docstring: preceding_docstring(node, self.src),
@@ -1151,6 +1163,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let type_name = self.text(node).to_string();
             if !type_name.is_empty() && !is_builtin_type(&type_name) {
@@ -1317,6 +1330,7 @@ impl<'t> Walker<'t> {
     }
 
     fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        stack_guard!();
         if depth > 4 {
             return;
         }
@@ -1381,6 +1395,7 @@ impl<'t> Walker<'t> {
     }
 
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }

+ 5 - 0
codegraph-kernel/src/tsjs/extractors.rs

@@ -478,6 +478,7 @@ impl<'t> Walker<'t> {
     }
 
     fn find_initializer_returned_object(&self, call: Node<'t>, depth: u32) -> Option<Node<'t>> {
+        stack_guard!();
         if depth > 4 {
             return None;
         }
@@ -499,6 +500,7 @@ impl<'t> Walker<'t> {
 
     fn function_returned_object(&self, fn_node: Node<'t>) -> Option<Node<'t>> {
         fn as_object<'t>(n: Node<'t>) -> Option<Node<'t>> {
+            stack_guard!();
             match n.kind() {
                 "object" | "object_expression" => Some(n),
                 "parenthesized_expression" => {
@@ -873,6 +875,7 @@ impl<'t> Walker<'t> {
     fn extract_ts_tuple_contract_names(&mut self, value: Node<'t>, alias_row: u32, alias_name: &str) {
         let mut tuples: Vec<Node> = Vec::new();
         fn collect<'t>(n: Node<'t>, depth: u32, out: &mut Vec<Node<'t>>) {
+            stack_guard!();
             if depth > 6 {
                 return;
             }
@@ -1230,6 +1233,7 @@ impl<'t> Walker<'t> {
     // --- extractInheritance (TS/JS clauses) ---------------------------------------------------
 
     pub(super) fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        stack_guard!();
         let extends_kind = edge_kind_index("extends").unwrap();
         let implements_kind = edge_kind_index("implements").unwrap();
         for i in 0..node.named_child_count() {
@@ -1298,6 +1302,7 @@ impl<'t> Walker<'t> {
     }
 
     fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        stack_guard!();
         if node.kind() == "type_identifier" {
             let type_name = self.text(node).to_string();
             if !type_name.is_empty() && !is_builtin_type(&type_name) {

+ 3 - 0
codegraph-kernel/src/tsjs/mod.rs

@@ -550,6 +550,7 @@ impl<'t> Walker<'t> {
 
     /// scanFnRefSubtree: capture-only walk of subtrees the main walkers skip.
     fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        stack_guard!();
         if depth > 12 {
             return;
         }
@@ -607,6 +608,7 @@ impl<'t> Walker<'t> {
     // --- the dispatcher (visitNode) --------------------------------------------
 
     fn visit_node(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         let mut skip_children = false;
 
@@ -686,6 +688,7 @@ impl<'t> Walker<'t> {
     }
 
     fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        stack_guard!();
         let kind = node.kind();
         self.maybe_capture_fn_refs(node);
 

+ 13 - 3
docs/design/rust-kernel-migration-plan.md

@@ -148,9 +148,11 @@ them are the ORIGINAL plan and carry expectations that measurement later correct
       tokio node sections IDENTICAL, small precision-positive edge churn only,
       full suite green), walker `codegraph-kernel/src/rustlang.rs` (survey
       artifact: rust-lang-kernel-port-checklist.md — isAsync dead-code,
-      impl-pushes-no-scope, trait-receiver bug on `impl Trait for Generic<T>`,
-      phantom const identifiers, use-binding triple emission, all preserved
-      bug-for-bug). Gates: parity sweeps **0 diffs** on ripgrep (101/101,
+      impl-pushes-no-scope, trait-receiver bug on `impl Trait for Generic<T>`
+      (fixed on both sides together in #1588 — receiver now comes from the
+      impl_item's `type` field), phantom const identifiers, use-binding
+      triple emission, all preserved bug-for-bug). Gates: parity sweeps
+      **0 diffs** on ripgrep (101/101,
       0 deferred) / tokio (790/790, 0 deferred) / rust-analyzer (1217/1488,
       0 diffs; 271 deferrals are token-macro-table sources — `T![~]`, `[$]` —
       that error on BOTH arms, grammar-inherent like fmt's C++ 42%); full-init
@@ -410,6 +412,14 @@ in a different emission order would shift rowids and change resolution — and
    file whose tree `has_error()` to the wasm extractor** (`defer:` signal, silent,
    per-file) — parity by construction on erroring files, 99.6%+ keep the fast path,
    and the harness fails if deferrals exceed 10% (a broken kernel can't hide).
+   The same signal carries a second, rarer case (#1581): the walkers recurse per
+   AST level, and a pathologically nested file (clang's 16,384-brace
+   `parser_overflow.c`, fuzzer corpora) overflowed the native stack — a SIGSEGV
+   that killed the whole indexer, uncatchable from JS. `src/stack.rs` now
+   checks the thread's real stack bounds at every recursive entry
+   (`stack_guard!`) and `run_guarded` turns a tripped walk into `defer:`, so
+   the file lands on the wasm path (which catches its own `RangeError` per
+   file) while the process lives. Pinned by `__tests__/kernel-deep-nesting.test.ts`.
 3. **Retrieval invariants:** kernel-indexed excalidraw — `mutateElement →
    renderStaticScene` connects end-to-end via explore (callback + react-render +
    jsx hops shown); synthesized-edge families present (408 jsx-render / 46

+ 25 - 17
docs/design/rust-lang-kernel-port-checklist.md

@@ -89,18 +89,21 @@ Hooks PRESENT (port each exactly):
 - **getVisibility (rust.ts:74)** — direct child of type `visibility_modifier`:
   text `.includes('pub')` → `'public'` else `'private'`; no modifier →
   `'private'` (so `pub(crate)`/`pub(super)` are all `'public'`).
-- **getReceiverType (rust.ts:83)** — walk PARENT chain to the nearest
-  `impl_item`; there: filter DIRECT namedChildren of type `type_identifier`;
-  if ≥1, return the LAST one's source text (`source.substring(startIndex,
-  endIndex)` — UTF-16 units). If none, find the first `generic_type` child and
-  return its inner `type_identifier` text; else undefined. Never an impl parent
-  → undefined. QUIRK/BUG, PRESERVE: for `impl Trait for Generic<T>` the only
-  direct type_identifier is the TRAIT (probe: `impl Render for Container<T>` →
-  typeIdents=[`Render`] → receiver = **`Render`**, the trait name — methods get
-  qualifiedName `Render::render` and a contains edge from the trait node if one
-  exists in-file). `impl fmt::Display for Fields` is fine
-  (scoped_type_identifier isn't type_identifier → [Fields]). `impl<T>
-  Container<T>` → no direct type_identifiers → generic branch → `Container`.
+- **getReceiverType (rust.ts)** — walk PARENT chain to the nearest
+  `impl_item`; there, read the grammar's `type` field through
+  `rustImplTypeName` (kernel: `impl_type_name`): `type_identifier`/`identifier`
+  → text; `generic_type` → its `type` field (bare name, never the args);
+  `scoped_type_identifier`/`scoped_identifier` → its `name` field (last
+  segment); `reference_type` → its `type` field; anything else (tuple, `dyn`,
+  pointer, primitive, fn type) → undefined. Never an impl parent → undefined.
+  **Changed in #1588 on both sides together**: the original rule took the LAST
+  direct `type_identifier` child, so for `impl Trait for Generic<T>` /
+  `Parents<'a>` / `&Foo` the only bare identifier was the TRAIT's (probe:
+  `impl Render for Container<T>` → receiver **`Render`** → methods
+  `Render::render`, colliding with the trait declaration and feeding the
+  interface-impl synthesizer a phantom declaration). Now `Container`.
+  `impl fmt::Display for Fields` → `Fields`; `impl<T> Container<T>` →
+  `Container`; `impl Tr for m::Foo` → `Foo` (was: no receiver).
   Note `<T>` type_parameters is its own child, its inner T is NOT a direct
   impl child.
 - **extractImport (rust.ts:120)** — signature = trimmed full `use …;` text.
@@ -187,8 +190,9 @@ undefined; **no isConst means `const_item`/`static_item` extract as kind
   present AND not class-like — finds the FIRST node in `this.nodes` with
   `name === receiverType && filePath === this.filePath && kind ∈
   {struct,class,enum,trait}`. Source-order dependent: an impl ABOVE its struct
-  gets no contains edge. `impl Trait for Generic<T>` (receiver=trait bug) links
-  to the TRAIT node if it's in-file.** Then type annotations, decorators
+  gets no contains edge. Since #1588 `impl Trait for Generic<T>` links to the
+  implementing TYPE's node (it used to link to the TRAIT node, the receiver
+  bug).** Then type annotations, decorators
   (no-op), body walk with the method pushed.
 - **Nested `fn` inside an impl-method's body**: visitFunctionBody:5245 →
   named → extractFunction → getReceiverType walks parents THROUGH the outer fn
@@ -222,9 +226,13 @@ Generic else-branch (4312+), `func = childForFieldName('function') ?? namedChild
      (4455) → `Foo::new().bar()` → ref `Foo::new().bar`; an instance chain
      `x.foo().bar()` (innerFn field_expression) → bare `bar`. When not
      re-encoding, calleeName = bare methodName.
-   - receiver anything else (`field_expression` 2-hop `v.field.method()`,
-     `parenthesized_expression`, `await_expression`, `self`) → bare
-     methodName (probed all four).
+   - receiver `field_expression` whose `value` is `self` and whose `field` is
+     a `field_identifier` (`self.inner.run()`) → `self.inner.run` — the
+     owner-field shape the resolver types from the struct declaration
+     (#1585, both sides together).
+   - receiver anything else (`field_expression` with a non-self base
+     `v.field.method()` / deeper `self.a.b.m()`, `parenthesized_expression`,
+     `await_expression`, `self`) → bare methodName (probed all four).
 2. `func.type === 'scoped_identifier'` (4499) → calleeName = FULL text
    (`Foo::new`, `m::helper2`, `std::mem::swap` — whatever the source spells,
    whitespace included).

+ 110 - 79
src/bin/codegraph.ts

@@ -607,94 +607,111 @@ async function recordIndexTelemetry(
 // =============================================================================
 
 /**
- * codegraph init [path]
+ * The `init` flow — shared by `codegraph init` and `codegraph install --init`
+ * (#1578): refuse an unsafe root, create `.codegraph/`, build the initial
+ * index under supervision, then the post-index offers. `yes` makes every
+ * offer non-interactive (defaults only), so a container / CI bootstrap never
+ * blocks on a prompt. An unsafe root sets `process.exitCode = 1` and returns
+ * (no `--force` is implied by any caller); an index failure exits 1.
  */
-program
-  .command('init [path]')
-  .description('Initialize CodeGraph in a project directory and build the initial index')
-  .option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
-  .option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
-  .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
-  .action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean }) => {
-    const projectPath = path.resolve(pathArg || process.cwd());
-    const clack = await importESM('@clack/prompts');
-
-    clack.intro('Initializing CodeGraph');
-
-    try {
-      // Refuse to index your home directory / a filesystem root — it pulls in
-      // caches, other projects, and your whole tree (a multi-GB index + watcher
-      // churn, and on pre-1.0 macOS a machine-crashing fd blowup, #845).
-      const unsafe = unsafeIndexRootReason(projectPath);
-      if (unsafe && !options.force) {
-        clack.log.error(`Refusing to initialize in ${projectPath} — it looks like ${unsafe}.`);
-        clack.log.info('Run this inside a specific project directory, or pass --force if you really mean to index everything under it.');
-        clack.outro('');
-        process.exitCode = 1;
-        return;
-      }
-
-      if (isInitialized(projectPath)) {
-        clack.log.warn(`Already initialized in ${projectPath}`);
-        clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
-        try {
-          const { offerWatchFallback } = await import('../installer');
-          await offerWatchFallback(clack, projectPath);
-        } catch { /* non-fatal */ }
-        clack.outro('');
-        return;
-      }
+async function runInit(
+  projectPath: string,
+  options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean },
+): Promise<void> {
+  const clack = await importESM('@clack/prompts');
 
-      const { default: CodeGraph, getDatabasePath } = await loadCodeGraph();
-      const cg = await CodeGraph.init(projectPath, { index: false });
-      clack.log.success(`Initialized in ${projectPath}`);
+  clack.intro('Initializing CodeGraph');
 
-      // Indexing runs by default now. The legacy -i/--index flag is still
-      // accepted (so existing muscle memory and scripts don't break) but is a
-      // no-op — initializing always builds the initial index.
-      // Supervise the index: self-terminate if orphaned or wedged (#999).
-      // The DB + WAL paths let the liveness watchdog tell a slow store on
-      // degraded storage from a true wedge (#1231).
-      // A closure so we can re-run the exact same supervised, progress-rendered
-      // index if the user opts gitignored child repos in below (#1156).
-      const dbPath = getDatabasePath(projectPath);
-      const runIndex = async (): Promise<IndexResult> => {
-        const supervision = installCommandSupervision('init', { progressPaths: [dbPath, `${dbPath}-wal`] });
-        try {
-          if (options.verbose) {
-            return await cg.indexAll({ onProgress: createVerboseProgress(), verbose: true });
-          }
-          process.stdout.write(`${colors.dim}${getGlyphs().rail}${colors.reset}\n`);
-          const progress = createShimmerProgress();
-          const r = await cg.indexAll({ onProgress: progress.onProgress });
-          await progress.stop();
-          return r;
-        } finally {
-          supervision.stop();
-        }
-      };
-      const result = await runIndex();
-      printIndexResult(clack, result, projectPath);
-      await recordIndexTelemetry(cg, result);
-
-      // An empty graph at a git super-repo usually means `.gitignore` excludes
-      // the child repos that hold the code — surface them and offer to opt in
-      // rather than leaving the user with a silent 0-node "Done". (#1156)
-      if (result.nodesCreated === 0) {
-        await offerIndexIgnoredRepos(clack, projectPath, runIndex, { interactive: true });
-      }
+  try {
+    // Refuse to index your home directory / a filesystem root — it pulls in
+    // caches, other projects, and your whole tree (a multi-GB index + watcher
+    // churn, and on pre-1.0 macOS a machine-crashing fd blowup, #845).
+    const unsafe = unsafeIndexRootReason(projectPath);
+    if (unsafe && !options.force) {
+      clack.log.error(`Refusing to initialize in ${projectPath} — it looks like ${unsafe}.`);
+      clack.log.info('Run this inside a specific project directory, or pass --force if you really mean to index everything under it.');
+      clack.outro('');
+      process.exitCode = 1;
+      return;
+    }
 
+    if (isInitialized(projectPath)) {
+      clack.log.warn(`Already initialized in ${projectPath}`);
+      clack.log.info('Use "codegraph index" to re-index or "codegraph sync" to update');
       try {
         const { offerWatchFallback } = await import('../installer');
-        await offerWatchFallback(clack, projectPath);
+        await offerWatchFallback(clack, projectPath, { yes: options.yes });
       } catch { /* non-fatal */ }
+      clack.outro('');
+      return;
+    }
 
-      clack.outro('Done');
-      cg.destroy();
-    } catch (err) {
-      clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
-      process.exit(1);
+    const { default: CodeGraph, getDatabasePath } = await loadCodeGraph();
+    const cg = await CodeGraph.init(projectPath, { index: false });
+    clack.log.success(`Initialized in ${projectPath}`);
+
+    // Indexing runs by default now. The legacy -i/--index flag is still
+    // accepted (so existing muscle memory and scripts don't break) but is a
+    // no-op — initializing always builds the initial index.
+    // Supervise the index: self-terminate if orphaned or wedged (#999).
+    // The DB + WAL paths let the liveness watchdog tell a slow store on
+    // degraded storage from a true wedge (#1231).
+    // A closure so we can re-run the exact same supervised, progress-rendered
+    // index if the user opts gitignored child repos in below (#1156).
+    const dbPath = getDatabasePath(projectPath);
+    const runIndex = async (): Promise<IndexResult> => {
+      const supervision = installCommandSupervision('init', { progressPaths: [dbPath, `${dbPath}-wal`] });
+      try {
+        if (options.verbose) {
+          return await cg.indexAll({ onProgress: createVerboseProgress(), verbose: true });
+        }
+        process.stdout.write(`${colors.dim}${getGlyphs().rail}${colors.reset}\n`);
+        const progress = createShimmerProgress();
+        const r = await cg.indexAll({ onProgress: progress.onProgress });
+        await progress.stop();
+        return r;
+      } finally {
+        supervision.stop();
+      }
+    };
+    const result = await runIndex();
+    printIndexResult(clack, result, projectPath);
+    await recordIndexTelemetry(cg, result);
+
+    // An empty graph at a git super-repo usually means `.gitignore` excludes
+    // the child repos that hold the code — surface them and offer to opt in
+    // rather than leaving the user with a silent 0-node "Done". (#1156)
+    // Under --yes the offer prints its one-line opt-in snippet instead of
+    // prompting (same as a non-TTY run).
+    if (result.nodesCreated === 0) {
+      await offerIndexIgnoredRepos(clack, projectPath, runIndex, { interactive: !options.yes });
     }
+
+    try {
+      const { offerWatchFallback } = await import('../installer');
+      await offerWatchFallback(clack, projectPath, { yes: options.yes });
+    } catch { /* non-fatal */ }
+
+    clack.outro('Done');
+    cg.destroy();
+  } catch (err) {
+    clack.log.error(`Failed: ${err instanceof Error ? err.message : String(err)}`);
+    process.exit(1);
+  }
+}
+
+/**
+ * codegraph init [path]
+ */
+program
+  .command('init [path]')
+  .description('Initialize CodeGraph in a project directory and build the initial index')
+  .option('-i, --index', 'Deprecated: indexing now runs by default; flag accepted for backward compatibility')
+  .option('-f, --force', 'Initialize even if the path looks like your home directory or a filesystem root')
+  .option('-v, --verbose', 'Show detailed worker lifecycle and memory info')
+  .option('-y, --yes', 'Non-interactive: skip every prompt and take the defaults (for scripts / CI / container bootstraps)')
+  .action(async (pathArg: string | undefined, options: { index?: boolean; force?: boolean; verbose?: boolean; yes?: boolean }) => {
+    await runInit(path.resolve(pathArg || process.cwd()), options);
   });
 
 /**
@@ -2268,6 +2285,7 @@ program
   .option('-t, --target <ids>', 'Target agent(s): comma-separated ids, or "auto"|"all"|"none". Default: prompt')
   .option('-l, --location <where>', 'Install location: "global" or "local". Default: prompt')
   .option('-y, --yes', 'Non-interactive: defaults to --location=global --target=auto, auto-allow on')
+  .option('-i, --init', 'After wiring agents, also run `codegraph init` in the current directory — builds this project’s index, so install + index is one command (combine with --yes for an unattended bootstrap)')
   .option('--no-permissions', 'Skip writing the auto-allow permissions list (Claude Code only)')
   .option('--print-config <id>', 'Print MCP config snippet for the named agent and exit (no file writes)')
   .option('--refresh', 'Rewrite what previous installs configured, for already-configured agents only (never adds new ones). Run automatically by `codegraph upgrade`')
@@ -2275,6 +2293,7 @@ program
     target?: string;
     location?: string;
     yes?: boolean;
+    init?: boolean;
     permissions?: boolean;
     printConfig?: string;
     refresh?: boolean;
@@ -2352,6 +2371,18 @@ program
       error(err instanceof Error ? err.message : String(err));
       process.exit(1);
     }
+
+    // --init: the one-shot "wire agents AND build this project's index"
+    // bootstrap (#1578). The installer itself never indexes implicitly (a
+    // surprise index of $HOME is the thing we refuse) — an explicit flag is
+    // the user choosing. Runs after a successful install, including the
+    // `--target none` / nothing-detected case (the installer returns normally
+    // there), and shares every guard with `codegraph init`: an unsafe root
+    // is refused (exit 1, no implied --force), an already-initialized
+    // project just says so. `--yes` flows through so no offer prompts.
+    if (opts.init) {
+      await runInit(process.cwd(), { yes: opts.yes });
+    }
   });
 
 /**

+ 54 - 2
src/extraction/index.ts

@@ -27,7 +27,7 @@ import { StoreWriter, StoreBundle, finalizeStoreBundle } from './store-writer';
 import { materializeKernelResult } from './kernel';
 import { detectGeneratedFile } from './generated-detection';
 import { detectLanguage, isSourceFile, isLanguageSupported, isFileLevelOnlyLanguage, initGrammars, loadGrammarsForLanguages, readGrammarWasmBytes } from './grammars';
-import { loadExtensionOverrides, loadIncludeIgnoredPatterns, loadExcludePatterns, loadIncludePatterns } from '../project-config';
+import { loadExtensionOverrides, loadIncludeIgnoredPatterns, loadExcludePatterns, loadIncludePatterns, PROJECT_CONFIG_FILENAME } from '../project-config';
 import { isCodeGraphDataDir } from '../directory';
 import { logDebug, logWarn } from '../errors';
 import { validatePathWithinRoot, normalizePath } from '../utils';
@@ -1448,12 +1448,48 @@ export class ExtractionOrchestrator {
    * hasn't run yet so single-file re-index paths can detect on the spot.
    */
   private detectedFrameworkNames: string[] | null = null;
+  /**
+   * Scope matcher for SCOPED syncs, memoized on the mtimes of the two root
+   * files it is derived from (`codegraph.json`, `.gitignore`). See
+   * {@link scopedSyncMatcher}.
+   */
+  private scopedMatcher: { key: string; matcher: ScopeIgnore } | null = null;
 
   constructor(rootDir: string, queries: QueryBuilder) {
     this.rootDir = rootDir;
     this.queries = queries;
   }
 
+  /**
+   * The scope matcher a scoped sync applies to the paths it was handed — the
+   * same `buildScopeIgnore` the full scan uses, so an explicitly-passed path
+   * that is OUT of scope (a user `exclude` in `codegraph.json`, a `.gitignore`
+   * rule, a built-in default) is treated exactly as the full walk would treat
+   * it: absent, hence removed if tracked, never parsed (#1590).
+   *
+   * Memoized on the root config + root `.gitignore` mtimes: building the
+   * matcher runs embedded-repo discovery (`git ls-files`), which would defeat
+   * the scoped path's whole point (skipping O(repo) work) if paid per sync.
+   * Two `stat`s per sync while nothing changed. An embedded repo created
+   * between config edits joins the scoped matcher on the next full sync, the
+   * same lifecycle the watcher's own matcher already has.
+   */
+  private scopedSyncMatcher(): ScopeIgnore {
+    const key = [PROJECT_CONFIG_FILENAME, '.gitignore']
+      .map((name) => {
+        try {
+          return String(fs.statSync(path.join(this.rootDir, name)).mtimeMs);
+        } catch {
+          return '-';
+        }
+      })
+      .join('|');
+    if (this.scopedMatcher && this.scopedMatcher.key === key) return this.scopedMatcher.matcher;
+    const matcher = buildScopeIgnore(this.rootDir);
+    this.scopedMatcher = { key, matcher };
+    return matcher;
+  }
+
   /**
    * Build a filesystem-backed ResolutionContext sufficient for framework
    * detection. Graph-query methods (getNodesByName etc.) return empty because
@@ -2700,7 +2736,23 @@ export class ExtractionOrchestrator {
       // reads `filesChecked === 0 && durationMs === 0` as the
       // lock-unavailable signature (#449).
       const unique = [...new Set(scopedPaths)];
-      currentFiles = unique.filter((p) => fs.existsSync(path.join(this.rootDir, p)));
+      // A scoped path is "present" only if it exists AND is in scope — the
+      // same two gates the full walk applies (source extension, scope
+      // matcher). Without the scope gate a caller's stale view of scope
+      // leaked straight into the index: the watcher re-parsed a file the
+      // user had just excluded in `codegraph.json` while `codegraph sync`
+      // removed it (#1590). Out-of-scope paths fall out of `currentFiles`,
+      // so a tracked one takes the removal branch below, exactly as a full
+      // sync would treat it. (`include`-forced paths pass: ScopeIgnore
+      // applies the include precedence itself.)
+      const scope = this.scopedSyncMatcher();
+      const overrides = loadExtensionOverrides(this.rootDir);
+      currentFiles = unique.filter(
+        (p) =>
+          isSourceFile(p, overrides) &&
+          !scope.ignores(p) &&
+          fs.existsSync(path.join(this.rootDir, p))
+      );
       trackedFiles = [];
       for (const p of unique) {
         const rec = this.queries.getFileByPath(p);

+ 43 - 26
src/extraction/languages/rust.ts

@@ -32,6 +32,45 @@ function extractRustReturnType(node: SyntaxNode, source: string): string | undef
   return last === 'Self' ? 'self' : last;
 }
 
+/**
+ * The implementing type's simple name for an `impl` block, read from the
+ * grammar's `type` field (#1588). Mirrored byte-for-byte by the native
+ * kernel's `impl_type_name` (codegraph-kernel/src/rustlang.rs) — change both.
+ *
+ * `impl<T> Source for BufSource<T>`, `impl<'a> Iterator for Parents<'a>`,
+ * `impl Trait for &Foo`, `impl Trait for m::Foo` all yield the implementing
+ * TYPE (`BufSource`, `Parents`, `Foo`, `Foo`). The previous rule took the last
+ * bare `type_identifier` child of the `impl_item`; once the implementing type
+ * carries parameters it parses as a `generic_type`, so the only bare
+ * identifier left was the TRAIT's — every parameterized impl's methods were
+ * qualified by the trait (`Source::read`), unaddressable by their type and
+ * colliding with the trait's own declaration.
+ *
+ * Shapes that name no single type (tuples, `dyn Trait`, pointers, primitives,
+ * function types…) yield undefined: no receiver, and the fn is extracted
+ * exactly as before.
+ */
+export function rustImplTypeName(typeNode: SyntaxNode | null, source: string): string | undefined {
+  if (!typeNode) return undefined;
+  switch (typeNode.type) {
+    case 'type_identifier':
+    case 'identifier':
+      return getNodeText(typeNode, source);
+    // `Foo<T>` — the `type` field is the bare (or scoped) name, never the args.
+    case 'generic_type':
+      return rustImplTypeName(getChildByField(typeNode, 'type'), source);
+    // `m::Foo` — the last segment is the type's name.
+    case 'scoped_type_identifier':
+    case 'scoped_identifier':
+      return rustImplTypeName(getChildByField(typeNode, 'name'), source);
+    // `&Foo` / `&'a mut Foo` — the referenced type.
+    case 'reference_type':
+      return rustImplTypeName(getChildByField(typeNode, 'type'), source);
+    default:
+      return undefined;
+  }
+}
+
 export const rustExtractor: LanguageExtractor = {
   // `function_signature_item` is a trait method DECLARATION (`fn render(&self);`,
   // no body). Extracting it makes a trait's method set first-class, which
@@ -88,32 +127,10 @@ export const rustExtractor: LanguageExtractor = {
     let parent = node.parent;
     while (parent) {
       if (parent.type === 'impl_item') {
-        // For `impl Type { ... }` — the type is a direct type_identifier child
-        // For `impl Trait for Type { ... }` — the type is the LAST type_identifier
-        // (the first is part of the trait path)
-        const children = parent.namedChildren;
-        // Find all direct type_identifier children (not nested in scoped paths)
-        const typeIdents = children.filter(
-          (c: SyntaxNode) => c.type === 'type_identifier'
-        );
-        if (typeIdents.length > 0) {
-          // Last type_identifier is always the implementing type
-          const typeNode = typeIdents[typeIdents.length - 1]!;
-          return source.substring(typeNode.startIndex, typeNode.endIndex);
-        }
-        // Handle generic types: impl<T> MyStruct<T> { ... }
-        const genericType = children.find(
-          (c: SyntaxNode) => c.type === 'generic_type'
-        );
-        if (genericType) {
-          const innerType = genericType.namedChildren.find(
-            (c: SyntaxNode) => c.type === 'type_identifier'
-          );
-          if (innerType) {
-            return source.substring(innerType.startIndex, innerType.endIndex);
-          }
-        }
-        return undefined;
+        // The grammar names the implementing type directly (the `type` field)
+        // for both `impl Type { … }` and `impl Trait for Type { … }` — see
+        // rustImplTypeName for why the old positional scan was wrong (#1588).
+        return rustImplTypeName(getChildByField(parent, 'type'), source);
       }
       parent = parent.parent;
     }

+ 6 - 0
src/extraction/parse-pool.ts

@@ -214,6 +214,12 @@ export class ParseWorkerPool {
       this.createWorker = opts.createWorker;
     } else if (opts.workerScriptPath) {
       const scriptPath = opts.workerScriptPath;
+      // Deliberately no `resourceLimits.stackSizeMb`: a bigger worker stack
+      // only moves the cliff a deeply nested file falls off (#1581 — the
+      // 8 MiB main thread still dies at 100k levels). The native kernel
+      // guards its own recursion against THIS thread's real stack bounds
+      // (codegraph-kernel/src/stack.rs) and defers such a file to the wasm
+      // path, which catches its JS RangeError per file.
       this.createWorker = () => new Worker(scriptPath);
     } else {
       throw new Error('ParseWorkerPool requires workerScriptPath or createWorker');

+ 35 - 32
src/extraction/tree-sitter.ts

@@ -22,6 +22,7 @@ import { isGeneratedFile } from './generated-detection';
 import type { LanguageExtractor, ExtractorContext } from './tree-sitter-types';
 import { EXTRACTORS } from './languages';
 import { stripCppTemplateArgs } from './languages/c-cpp';
+import { rustImplTypeName } from './languages/rust';
 import { LiquidExtractor } from './liquid-extractor';
 import { RazorExtractor } from './razor-extractor';
 import { SvelteExtractor } from './svelte-extractor';
@@ -4430,6 +4431,26 @@ export class TreeSitterExtractor {
               } else {
                 calleeName = methodName;
               }
+            } else if (
+              this.language === 'rust' &&
+              receiver &&
+              receiver.type === 'field_expression' &&
+              getChildByField(receiver, 'value')?.type === 'self' &&
+              getChildByField(receiver, 'field')?.type === 'field_identifier'
+            ) {
+              // Rust `self.<field>.<method>()` — a call through a field of the
+              // enclosing type (#1585). Keep the `self.` prefix: the resolver
+              // recognizes the shape, reads the field's declared type off the
+              // owner struct's declaration, and resolves the method on THAT
+              // type — or leaves the ref unresolved when the type is external
+              // or unknown. Previously this collapsed to the bare method name,
+              // which exact-matched whichever same-named method was nearest —
+              // often the calling method itself, a self-edge not in the source.
+              // Deeper chains (`self.a.b.m()`), `self.f().m()` and parenthesized
+              // receivers keep the bare name. Mirrored in the kernel's
+              // extract_call (rustlang.rs).
+              const fieldName = getNodeText(getChildByField(receiver, 'field')!, this.source);
+              calleeName = `self.${fieldName}.${methodName}`;
             } else if (
               (this.language === 'cpp' ||
                 this.language === 'c' ||
@@ -5717,38 +5738,20 @@ export class TreeSitterExtractor {
    * For plain `impl Type { ... }` (no trait), no inheritance edge is needed.
    */
   private extractRustImplItem(node: SyntaxNode): void {
-    // Check if this is `impl Trait for Type` by looking for a `for` keyword
-    const hasFor = node.children.some(
-      (c: SyntaxNode) => c.type === 'for' && !c.isNamed
-    );
-    if (!hasFor) return;
-
-    // In `impl Trait for Type`, the type_identifiers are:
-    // first = Trait name, last = implementing Type name
-    // Also handle generic types like `impl<T> Trait for MyStruct<T>`
-    const typeIdents = node.namedChildren.filter(
-      (c: SyntaxNode) => c.type === 'type_identifier' || c.type === 'generic_type' || c.type === 'scoped_type_identifier'
-    );
-    if (typeIdents.length < 2) return;
-
-    const traitNode = typeIdents[0]!;
-    const typeNode = typeIdents[typeIdents.length - 1]!;
-
-    // Get the trait name (handle scoped paths like std::fmt::Display)
-    const traitName = traitNode.type === 'scoped_type_identifier'
-      ? this.source.substring(traitNode.startIndex, traitNode.endIndex)
-      : getNodeText(traitNode, this.source);
-
-    // Get the implementing type name (extract inner type_identifier for generics)
-    let typeName: string;
-    if (typeNode.type === 'generic_type') {
-      const inner = typeNode.namedChildren.find(
-        (c: SyntaxNode) => c.type === 'type_identifier'
-      );
-      typeName = inner ? getNodeText(inner, this.source) : getNodeText(typeNode, this.source);
-    } else {
-      typeName = getNodeText(typeNode, this.source);
-    }
+    // `impl Trait for Type` carries the trait in the grammar's `trait` field;
+    // an inherent `impl Type { … }` has none and needs no inheritance edge.
+    const traitNode = getChildByField(node, 'trait');
+    if (!traitNode) return;
+
+    // Full text, so a scoped path (`std::fmt::Display`) and a generic trait
+    // (`From<u32>`) keep their spelling.
+    const traitName = getNodeText(traitNode, this.source);
+
+    // The implementing type from the `type` field (#1588). The old positional
+    // scan took the LAST type-shaped child, which for a parameterized
+    // implementing type (`BufSource<T>`, `Parents<'a>`, `&Foo`) was the trait.
+    const typeName = rustImplTypeName(getChildByField(node, 'type'), this.source);
+    if (!typeName) return;
 
     // Find the struct/type node for the implementing type
     const typeNodeId = this.findNodeByName(typeName);

+ 3 - 2
src/installer/index.ts

@@ -285,9 +285,10 @@ export async function runInstallerWithOptions(opts: RunInstallerOptions): Promis
   // index a surprise directory (e.g. a shell sitting in $HOME). Same next step
   // regardless of global/local scope.
   clack.note(
-    location === 'local'
+    (location === 'local'
       ? 'codegraph init        # build this project’s graph (one time; auto-syncs after)'
-      : 'cd <your-project>\ncodegraph init        # build a project’s graph (one time; auto-syncs after)',
+      : 'cd <your-project>\ncodegraph init        # build a project’s graph (one time; auto-syncs after)') +
+      '\n# (codegraph install --init does both steps in one command)',
     'Next: index a project',
   );
 

+ 15 - 0
src/resolution/import-resolver.ts

@@ -12,6 +12,7 @@ import { applyAliases } from './path-aliases';
 import { resolveWorkspaceImport } from './workspace-packages';
 import {
   resolveMethodOnType,
+  resolveObjectLiteralMember,
   localReceiverTypePatterns,
   normalizeInferredTypeName,
 } from './name-matcher';
@@ -1545,6 +1546,20 @@ export function resolveViaImport(
                 resolvedBy: 'import',
               };
             }
+            // An imported object literal used as a namespace (#1573):
+            // `api.call()` after `import { api } from './api'` where `api` is
+            // `export const api = { call() {…} }`. Its members have bare
+            // qualified names inside the constant's extent, so the
+            // `Container::member` lookup above can't see them and the edge
+            // landed on the constant — every cross-file caller of the method
+            // went missing. Resolve the member by containment instead.
+            if (targetNode.kind === 'constant' || targetNode.kind === 'variable') {
+              const member = ref.referenceName.slice(imp.localName.length + 1).split('.')[0];
+              if (member) {
+                const literalMember = resolveObjectLiteralMember(targetNode, member, ref, context, 0.9, 'import');
+                if (literalMember) return literalMember;
+              }
+            }
             // An imported VALUE (singleton constant / shared instance) called
             // through a member: `reproStore.notifyJoinGuildStatus()` after
             // `import { reproStore } from './store'`. findExportedSymbol

+ 224 - 0
src/resolution/name-matcher.ts

@@ -543,6 +543,93 @@ export function preferCallSiteFile(nodes: Node[], callSiteFile: string): Node[]
   return same.length ? [...same, ...other] : nodes;
 }
 
+/**
+ * Languages whose object literals declare callable members — `export const
+ * api = { call() {…}, get: () => {…} }` used as a namespace (#1573).
+ */
+const OBJECT_LITERAL_LANGUAGES = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
+
+/** True when `inner`'s source range lies within `outer`'s (lines, then columns on a shared line). */
+function rangeWithin(inner: Node, outer: Node): boolean {
+  const innerEnd = inner.endLine ?? inner.startLine;
+  const outerEnd = outer.endLine ?? outer.startLine;
+  if (inner.startLine < outer.startLine || innerEnd > outerEnd) return false;
+  if (inner.startLine === outer.startLine && inner.startColumn < outer.startColumn) return false;
+  if (innerEnd === outerEnd && inner.endColumn > outer.endColumn) return false;
+  return true;
+}
+
+function sameRange(a: Node, b: Node): boolean {
+  return (
+    a.startLine === b.startLine &&
+    a.startColumn === b.startColumn &&
+    (a.endLine ?? a.startLine) === (b.endLine ?? b.startLine) &&
+    a.endColumn === b.endColumn
+  );
+}
+
+/**
+ * Resolve `container.member` where `container` is a VALUE holding an object
+ * literal — `export const api = { call() {…}, get: () => {…} }` used as the
+ * module's namespace (#1573). The members are extracted as plain functions
+ * with BARE qualified names inside the constant's source extent (there is no
+ * `api::call`), so neither the `Container::member` lookup the class-shaped
+ * kinds use (#825) nor the declared-type inference for singleton instances
+ * (#1292) can reach them, and every such call resolved to nothing — or, via
+ * an import, to the constant itself. This looks the member up by CONTAINMENT:
+ * a node named `member` whose range lies inside the container's, in the
+ * container's own file. A helper declared inside a member's body is not a
+ * member and is skipped; nothing else in the file can donate a match. Calls
+ * take callable kinds only; other references accept value members too.
+ */
+export function resolveObjectLiteralMember(
+  container: Node,
+  member: string,
+  ref: UnresolvedRef,
+  context: ResolutionContext,
+  confidence: number,
+  resolvedBy: ResolvedRef['resolvedBy'],
+): ResolvedRef | null {
+  if (container.kind !== 'constant' && container.kind !== 'variable') return null;
+  if (!OBJECT_LITERAL_LANGUAGES.has(container.language)) return null;
+  if (!sameLanguageFamily(container.language, ref.language)) return null;
+
+  const inFile = context.getNodesInFile(container.filePath);
+  const callable = (n: Node) => n.kind === 'function' || n.kind === 'method';
+  const valueMember = (n: Node) =>
+    callable(n) || n.kind === 'property' || n.kind === 'variable' || n.kind === 'constant';
+  const accepts = ref.referenceKind === 'calls' ? callable : valueMember;
+
+  const inside = inFile.filter((n) => n.id !== container.id && rangeWithin(n, container));
+  let candidates = inside.filter((n) => n.name === member && accepts(n));
+  if (candidates.length === 0) return null;
+
+  // Drop a candidate nested inside ANOTHER callable's body within the literal
+  // (`{ run() { const call = () => {}; } }` — `call` is `run`'s local, not a
+  // member). Strict containment: an identically-ranged sibling node for the
+  // same member (a property node over an arrow function) is not a body.
+  const bodies = inside.filter(callable);
+  candidates = candidates.filter(
+    (c) => !bodies.some((b) => b.id !== c.id && !sameRange(b, c) && rangeWithin(c, b))
+  );
+  if (candidates.length === 0) return null;
+
+  // Several survivors (a property AND a function for one arrow member, say):
+  // a callable first, then the earliest in source order.
+  candidates.sort((a, b) => {
+    const ca = callable(a) ? 0 : 1;
+    const cb = callable(b) ? 0 : 1;
+    if (ca !== cb) return ca - cb;
+    return a.startLine - b.startLine || a.startColumn - b.startColumn;
+  });
+  return {
+    original: ref,
+    targetNodeId: candidates[0]!.id,
+    confidence,
+    resolvedBy,
+  };
+}
+
 // Exported for the precedence unit tests (#1079): they assert the
 // preferredFqn → same-file → matches[0] ordering directly.
 export function resolveMethodOnType(
@@ -1731,6 +1818,18 @@ export function matchMethodCall(
     return matchGoFieldChainCall(objectOrClass!, methodName!, ref, context);
   }
 
+  // Rust call through a field of the enclosing type — `self.inner.run()`,
+  // emitted as `self.inner.run` (#1585). Same discipline as the Go branch
+  // above, and EXCLUSIVE for the same reason: validated field-type inference
+  // or nothing. Letting this shape reach the bare-name strategies below is
+  // how `self.inner.run()` resolved to a same-named method on an unrelated
+  // type — or to the calling method itself, a self-edge the source doesn't
+  // contain — whenever the field's type was external or merely shared a
+  // method name with something nearby.
+  if (ref.language === 'rust' && dotMatch && objectOrClass!.startsWith('self.')) {
+    return matchRustSelfFieldCall(objectOrClass!.slice('self.'.length), methodName!, ref, context);
+  }
+
   // Java/Kotlin: receiver may be a field whose name doesn't match the type by
   // Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
   // the field in the enclosing class to get its declared type, then resolve
@@ -1759,6 +1858,27 @@ export function matchMethodCall(
     }
   }
 
+  // Object-literal namespace receiver (#1573): `api.call()` where `api` is a
+  // same-file `const api = { call() {…}, get: () => {…} }`. Its members are
+  // plain functions with bare names inside the constant's extent — no
+  // `Container::member` qualified name — so none of the class-shaped
+  // strategies below can see them (Strategy 3 only considers `method`
+  // kinds) and the call resolved to nothing at all. Same file only: a
+  // cross-file use reaches the same helper through the import path.
+  if (dotMatch && !objectOrClass!.includes('.') && OBJECT_LITERAL_LANGUAGES.has(ref.language)) {
+    const literalMatch = nmTimedT('mc-literal', ref, (): ResolvedRef | null => {
+      const holders = preferCallSiteFile(context.getNodesByName(objectOrClass!), ref.filePath).filter(
+        (n) => (n.kind === 'constant' || n.kind === 'variable') && n.filePath === ref.filePath
+      );
+      for (const holder of holders) {
+        const hit = resolveObjectLiteralMember(holder, methodName!, ref, context, 0.85, 'instance-method');
+        if (hit) return hit;
+      }
+      return null;
+    });
+    if (literalMatch) return literalMatch;
+  }
+
   // Strategy 1: Direct class name match (existing logic). When the receiver
   // names a class that exists in several files (`Logger.log()` / `Logger::log()`
   // with a `Logger` in both `a/` and `b/`), try the class in the call site's
@@ -1992,6 +2112,110 @@ function matchGoFieldChainCall(
   return null;
 }
 
+// Rust primitives and the prelude's own types: a field of one of these never
+// names a project type, so a `self.<field>.<method>()` on it stays unresolved.
+const RUST_NON_PROJECT_FIELD_TYPES = new Set([
+  'bool', 'char', 'str', 'String',
+  'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
+  'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
+  'f32', 'f64',
+  'Self', 'self',
+]);
+
+/**
+ * Reduce a Rust field's declared type text to the simple name of the type a
+ * method call on that field auto-derefs to, or null when there is none we can
+ * name. Only the layers Rust's method-call auto-deref looks through are
+ * unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
+ * (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
+ * `Inner::run`. Containers that do NOT auto-deref to their parameter
+ * (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
+ * own name and, having no project node, resolve to nothing — `self.items.push()`
+ * must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
+ * the trait, whose method node the interface-impl synthesizer fans out. A
+ * generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
+ * type, or a non-identifier yields null.
+ */
+export function rustFieldTypeName(raw: string): string | null {
+  let t = raw.trim();
+  for (;;) {
+    const before = t;
+    t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
+    t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
+    t = t.replace(/^(?:dyn|impl)\s+/, '');
+    if (t === before) break;
+  }
+  // Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
+  // bounds (`dyn Source + Send`); keep the last path segment.
+  t = t.replace(/[<>+].*$/, '').trim();
+  const seg = t.split('::').filter(Boolean).pop();
+  if (!seg || !/^[A-Za-z_]\w*$/.test(seg)) return null;
+  if (RUST_NON_PROJECT_FIELD_TYPES.has(seg)) return null;
+  if (/^[A-Z]$/.test(seg)) return null; // bare single-letter generic parameter
+  return seg;
+}
+
+/**
+ * Resolve a Rust call through a field of the enclosing type —
+ * `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
+ * Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
+ * method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
+ * type comes from the owner struct's OWN declaration lines, and the method is
+ * resolved AND VALIDATED on that type by resolveMethodOnType. The caller
+ * treats this branch as exclusive for `self.<field>` receivers: a field whose
+ * type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
+ * parameter, or not declared where we can see it yields null and the ref stays
+ * unresolved. Rust struct fields are not graph nodes, so the declaration text
+ * is the only place the type lives.
+ */
+function matchRustSelfFieldCall(
+  field: string,
+  methodName: string,
+  ref: UnresolvedRef,
+  context: ResolutionContext,
+): ResolvedRef | null {
+  // The extractor only ever emits a single field hop; anything else is not ours.
+  if (!field || field.includes('.')) return null;
+  const caller = context.getNodeById?.(ref.fromNodeId);
+  if (!caller) return null;
+  const sep = caller.qualifiedName.lastIndexOf('::');
+  if (sep <= 0) return null; // a free fn has no `self`
+  const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
+  if (!owner) return null;
+
+  const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter(
+    (n) =>
+      (n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
+      n.language === 'rust'
+  );
+  const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+  // `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
+  // the type text runs to the field separator. A comma inside generic args
+  // (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
+  // reduces to the container's own name — exactly the non-deref case it
+  // refuses anyway.
+  const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
+  for (const s of owners) {
+    const source = context.readFile(s.filePath);
+    if (!source) continue;
+    // Only the struct's own declaration lines, comment-stripped line by line —
+    // same discipline as the Go helper: prose or a same-named identifier
+    // elsewhere in the file can never donate a type.
+    const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
+    for (const rawLine of declLines) {
+      const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
+      const m = line.match(fieldRe);
+      if (!m || !m[1]) continue;
+      const fieldType = rustFieldTypeName(m[1]);
+      // The field is declared here; whether or not its type names a project
+      // symbol, this owner is the answer — no other same-named struct applies.
+      if (!fieldType) return null;
+      return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
+    }
+  }
+  return null;
+}
+
 /**
  * Split a camelCase or PascalCase string into words.
  */

+ 53 - 6
src/sync/watcher.ts

@@ -34,7 +34,7 @@
 import * as fs from 'fs';
 import * as path from 'path';
 import { isSourceFile, buildScopeIgnore, type ScopeIgnore } from '../extraction';
-import { loadExtensionOverrides } from '../project-config';
+import { loadExtensionOverrides, PROJECT_CONFIG_FILENAME } from '../project-config';
 import { logDebug, logWarn } from '../errors';
 import { normalizePath } from '../utils';
 import { isCodeGraphDataDir } from '../directory';
@@ -328,11 +328,13 @@ export class FileWatcher {
    * deterministically gate on watcher readiness.
    */
   private readyWaiters: Array<() => void> = [];
-  // The shared scope matcher (built-in defaults + project .gitignore, with
-  // embedded child repos matched by their OWN rules — #514), built once at
-  // start(). Same source of truth the indexer uses, so watcher scope can
-  // never diverge from index scope. An embedded repo created after start()
-  // joins the scope on the next watcher restart / re-index.
+  // The shared scope matcher (built-in defaults + project .gitignore + the
+  // `codegraph.json` exclude/include rules, with embedded child repos matched
+  // by their OWN rules — #514), built at start() and REBUILT whenever one of
+  // the files it is derived from changes (see `refreshScope`, #1590). Same
+  // source of truth the indexer uses, so watcher scope can never diverge from
+  // index scope. An embedded repo created after start() joins the scope on
+  // the next scope refresh / watcher restart / re-index.
   private ignoreMatcher: ScopeIgnore | null = null;
 
   private readonly projectRoot: string;
@@ -573,7 +575,24 @@ export class FileWatcher {
   private handleChange(rel: string): void {
     if (!rel || rel === '.' || rel.startsWith('..')) return;
     if (this.isAlwaysIgnored(rel)) return;
+    // The two root files the scope matcher is derived from are handled BEFORE
+    // the matcher is consulted: a user `exclude` pattern that happens to cover
+    // them (`*.json`, `.*`) must not be able to hide their own edits (#1590).
+    if (rel === PROJECT_CONFIG_FILENAME || rel === '.gitignore') {
+      this.refreshScope(rel);
+      return;
+    }
     if (this.ignoreMatcher && this.ignoreMatcher.ignores(rel)) return;
+    // A nested `.gitignore` (an embedded child repo's own rules, #514, or a
+    // subdirectory rule the git-backed full scan honors) is only a scope
+    // change when it sits INSIDE the current scope — checked after the matcher
+    // on purpose, so the thousands of package-local `.gitignore`s an
+    // `npm install` writes under an ignored `node_modules/` never trigger a
+    // rebuild storm.
+    if (rel.endsWith('/.gitignore')) {
+      this.refreshScope(rel);
+      return;
+    }
     if (!isSourceFile(rel, loadExtensionOverrides(this.projectRoot))) {
       this.maybeScheduleForRemovedDir(rel);
       return;
@@ -591,6 +610,34 @@ export class FileWatcher {
     this.scheduleSync();
   }
 
+  /**
+   * A scope-defining file changed (`codegraph.json`, a `.gitignore`): rebuild
+   * the ignore matcher and make the next sync a FULL reconcile (#1590).
+   *
+   * The matcher used to be built once in `start()` and kept for the watcher's
+   * lifetime — in a long-lived MCP daemon that meant a `codegraph.json`
+   * created or edited after startup was invisible to the live watcher, while
+   * `codegraph sync` (a fresh process) honoured it immediately: the CLI
+   * removed a newly excluded file and the watcher re-added it seconds later.
+   * `loadExtensionOverrides()` on the same filter line was already read live
+   * (mtime-cached), so two fields of the same config file disagreed.
+   *
+   * Rebuilding costs one `git ls-files` pass (embedded-repo discovery), which
+   * is fine per config edit — never per event. Replacing the field is enough
+   * for both strategies: the recursive handler and the per-directory
+   * `shouldIgnoreDir` walk read `this.ignoreMatcher` on every call. The full
+   * scan is required because a scope change has no per-file events: newly
+   * excluded files must be REMOVED from the index and newly included ones
+   * added, and only the scan-diff (which builds its own fresh matcher) knows
+   * which those are.
+   */
+  private refreshScope(rel: string): void {
+    logDebug('Scope config changed; rebuilding watcher scope', { file: rel });
+    this.ignoreMatcher = buildScopeIgnore(this.projectRoot);
+    this.needsFullScan = true;
+    this.scheduleSync();
+  }
+
   /**
    * A deleted DIRECTORY arrives as one event on the directory's own path —
    * no source extension, so the source-file filter drops it, and the files