Browse Source

feat(kernel): R7b Swift walker — swift module, tree-sitter-swift 0.7.3 bump, swift default-routed (#1381)

Fifth R7b batch-3 port, checklist-first recipe
(docs/design/swift-kernel-port-checklist.md, 1,056 lines — the largest of the
arc, with a built-extractor-validated emission pin and a childForFieldName
truth table).

Grammar bump first, validated standalone: tree-sitter-wasms ^0.4.0 (ABI 13) →
crate 0.7.3 — with a provenance twist: the wasm is built from the CRATE
TARBALL's src/ (alex-pinkus keeps generated files off main and the
0.7.3-with-generated-files tag ships an older ABI-14 generation that can never
sha-match; grammar.json rules are JSON-equal; the tarball is byte-for-byte
what the kernel's cargo build compiles — table identity by construction).
Older crates evaluated and rejected: clean-parse shapes are byte-identical on
0.7.3 (53-line CST battery diff, all inert), so an older pin buys nothing and
loses the macro-era wins. Delta = error-set membership (63 old-error files
parse clean: swift-testing #expect, #Preview/#GET macros, package access,
typed throws — vapor 23.1%→9.3%; 21 NEW-only regressions in 3 probed
construct classes) + two gate-found categories: docstring boundaries near #if
directives (7 clean files, docstring-field-only — verified mechanically) and
array-literal-callee call refs (2 refs, 1 file). Every hunk classified via
the error-union rule + parked-ref↔edge ripple pairing.

Walker (the arc's biggest) centers on the #1020 DEDICATED property branch:
computed properties → property nodes with the getter walked under the
property (SwiftUI body), static let/var → constant/variable, stored → field,
decorator/type-annotation/@Siblings-attr-arg refs all attached to the
ENCLOSING TYPE, stored initializer calls attributed to the class. Preserved
bug-for-bug: the never-resolving 'parameter' field (zero param type refs,
zero signatures), present-false isAsync, open→internal visibility,
everything-is-extends inheritance (first type_identifier per specifier), no
instantiates refs ever, subscript reads as `calls arr`, `defer` as `calls
defer`, multi-case enum entries minting only the first case, /** */ block
docs ignored AND chain-breaking, init/deinit/subscript minting no nodes with
visitNode-routed bodies (calls → class, static reads → nothing), multi-
segment extension resolveName, sugar extension names, the #selector shapes,
and the value_argument label-forward skip. ONE fix found by the sweep (then
pinned in the fixture + checklist): the shared `assignment` shadow-prune case
is swift-live — declared-then-assigned `let X: T` prunes X as a value-ref
target.

Gates: sweeps 0-diff Alamofire 89/98, vapor 224/247, swift-nio 407/554
(--max-deferral 0.3 — swift error incidence is 9–27% on BOTH arms,
structural; every deferral count matches the survey's table exactly);
full-init dumps byte-identical ×3 (31.9k/20.7k/126.3k lines); the Alamofire
census reproduces property=348 (the #1020 number) on the kernel arm;
kernel-swift-parity suite (206-line torture + CRLF + the #if-between-enum-
cases defer fixture) + swift grammar-parity row; full suite 2,626 green ×2
under CODEGRAPH_KERNEL_EXPECT=1. DEFAULT_ROUTED += swift (14 langs).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Colby Mchenry 1 month ago
parent
commit
09e301bbfa

+ 2 - 1
CHANGELOG.md

@@ -11,7 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 ### New Features
 
-- Indexing TypeScript, TSX, JavaScript, JSX, Java, Python, Go, C, C++, Rust, C#, Ruby, and PHP projects is faster: parsing and symbol extraction now run in a native engine when a prebuilt binary is available for your platform (release bundles include one), producing exactly the same graph — verified byte-for-byte against the previous engine on real repositories, from small libraries up to vscode-, dubbo-, django-, git-, protobuf-, tokio-, rust-analyzer-, jellyfin-, rails-, and symfony-scale codebases (Lombok-generated members, C function-pointer tables, and Unreal-Engine-style macro-heavy headers included; CUDA and Metal sources ride the C++ path). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
+- Indexing TypeScript, TSX, JavaScript, JSX, Java, Python, Go, C, C++, Rust, C#, Ruby, PHP, and Swift projects is faster: parsing and symbol extraction now run in a native engine when a prebuilt binary is available for your platform (release bundles include one), producing exactly the same graph — verified byte-for-byte against the previous engine on real repositories, from small libraries up to vscode-, dubbo-, django-, git-, protobuf-, tokio-, rust-analyzer-, jellyfin-, rails-, symfony-, and swift-nio-scale codebases (Lombok-generated members, C function-pointer tables, and Unreal-Engine-style macro-heavy headers included; CUDA and Metal sources ride the C++ path). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
 - Reference resolution now runs in parallel on large projects. When a project has enough pending references to make it worthwhile (roughly 150k+, typical for big Java/Kotlin/Spring codebases), resolution fans out across worker threads while results are applied in the exact order the single-threaded path would have used — the graph comes out byte-for-byte identical, about twice as fast end-to-end on a 4,000-file Java project in our testing. Small projects keep the single-threaded path automatically (the fan-out costs more than it saves there). Set `CODEGRAPH_NO_PARALLEL_RESOLVE=1` to disable, or `CODEGRAPH_PARALLEL_RESOLVE_MIN=<count>` to tune when it engages.
 - Indexing large projects got another sizeable speedup — about a quarter less wall-clock on the same 4,000-file Java project, with the graph still byte-for-byte identical. Two changes: the database no longer interleaves expensive checkpoint housekeeping into the middle of resolution on a fresh index (it's folded once at the end instead), and while one batch's results are being written out, the worker threads are already resolving the next batch instead of sitting idle.
 - The dynamic-dispatch analysis that runs at the end of indexing (callback, event, and framework wiring) now runs its passes in parallel on large projects, cutting that stage roughly in half there — and a pass that crashes now retries safely instead of failing the whole index, which also makes very large codebases that previously died in this stage more likely to index to completion. Graphs remain byte-for-byte identical.
@@ -31,6 +31,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 - Rust files also parse with an up-to-date grammar now (the previously bundled build dated from 2023), which additionally sharpens method-call attribution: calls through struct fields resolve with receiver context instead of falling back to ambiguous bare-name matching, removing a class of wrong call edges on common names like `len` and `start`.
 - Ruby files also parse with an up-to-date grammar now (the previously bundled build dated from early 2024), which fixes a misparse of safe-navigation operator-method calls (`recv&.!= x`) that had recorded the wrong callee name.
 - PHP files also parse with an up-to-date grammar now (the previously bundled build dated from 2023): files using modern PHP features — enum constants, PHP 8.4 property hooks, parenthesis-free `new X()->method()` chaining — no longer hit parse errors or misparses that dropped or garbled their symbols, so codebases like Symfony and Laravel index substantially more accurately.
+- Swift files also parse with an up-to-date grammar now (the previously bundled build dated from 2023): swift-testing `#expect`, `#Preview`-style macros, the `package` access modifier, and typed `throws` no longer produce parse errors that dropped surrounding symbols — server-side Swift projects like Vapor see the biggest recovery.
 - Searching or exploring by field names now finds the code that defines them. A query made of object keys or API field names (`profileInfo isTrialEligible quotaInfo billingMethod`) used to return unrelated results while the defining files never appeared, because three retrieval steps each dropped multi-word camelCase terms: an internal case-comparison bug, a match step that only considered classes (never functions or methods), and exploration seeding that required exact symbol-name matches. All three are fixed — `codegraph_explore` with a bag of field names now surfaces the controllers and services that assemble those fields. (#1196)
 - `codegraph.json`'s `includeIgnored` works again for the "folder of repos" layout: when one `.gitignore` rule covers a parent directory (`/repos/`) holding several embedded git repositories, opting in the individual repos (`"includeIgnored": ["repos/a/"]` — the exact spelling `codegraph init`'s own hint suggests) previously matched nothing and indexed zero files, looping the same suggestion back at you. Both spellings now work — name the parent directory to opt in everything under it, or name individual repos to opt in just those — and the hint no longer re-suggests repos that are already configured. (#1295)
 - Method calls on literals (`", ".join(...)` in Python, `"x".split(...)` in JavaScript, and the like) no longer produce call edges to unrelated project functions that happen to share the builtin's name — a codebase with a function called `join`, `get`, or `update` could show phantom callers from every string-builtin use. Additionally, a function nested inside another function is now only matched as a call target from inside its container, since it isn't reachable from anywhere else. Blast-radius and affected-test results get cleaner on Python and JavaScript codebases especially. (#1230)

+ 206 - 0
__tests__/fixtures/kernel-parity/torture.swift

@@ -0,0 +1,206 @@
+import Foundation
+import UIKit.UIView
+
+/// Class doc line one
+/// Class doc line two
+public final class Session: NSObject, RequestDelegate {
+  let rootQueue: DispatchQueue = DispatchQueue(label: "root")
+  var mutable = 0
+  static let SHARED_MAX: Int = 5
+  static var counter = 0
+  lazy var expensive: Cache = Cache.build()
+  @Published private var wrapped: Bool = false
+  weak var delegate: SessionDelegate?
+  var observed: Int = 0 {
+    willSet { prepare(newValue) }
+    didSet { react(oldValue) }
+  }
+  var isCloudProxy: Bool { return check(SHARED_MAX) }
+  open class func classFunc() {}
+
+  /// method doc
+  public func request(_ convertible: URLConvertible, method: HTTPMethod) -> DataRequest {
+    let req = DataRequest.make().validate()
+    let m = HTTPMethod.get
+    let g = Session.SHARED_MAX
+    self.own()
+    super.retain()
+    rootQueue.async { self.perform(req) }
+    return req
+  }
+  init(raw: String) {
+    self.raw = raw
+    setupMonitor()
+  }
+  deinit { cleanup() }
+  subscript(index: Int) -> Foo { get { store[index] } set { store[index] = newValue } }
+}
+
+struct HTTPMethod: RawRepresentable, Equatable {
+  let rawValue: String
+  static let get = HTTPMethod(rawValue: "GET")
+}
+
+enum AFError: Error, CustomStringConvertible {
+  case invalidURL(url: URLConvertible)
+  case explicitlyCancelled, sessionDeinitialized
+  indirect case wrapped(AFError)
+  var description: String { renderDescription() }
+  static func make() -> AFError { .explicitlyCancelled }
+}
+
+protocol RequestDelegate: AnyObject {
+  var sessionState: Int { get }
+  func didFinish(_ request: Request)
+  static func build() -> Self
+}
+
+extension Session: EventMonitor {
+  func heard(event: Event) { record(event) }
+}
+
+extension KF.Builder {
+  func done() -> KF.Builder { self }
+}
+
+extension Array where Element: Equatable {
+  func dedup() -> [Element] { self }
+}
+
+actor Counter {
+  var n = 0
+  func bump() { n += 1 }
+}
+
+typealias Handler = (Data) -> Void
+typealias BuilderAlias = KF.Builder
+
+let TOP_LEVEL_MAX = 3
+var topVar: Int = compute()
+
+func freeFn(a: Int, cb: @escaping (Int) -> Void) -> Session? {
+  helper()
+  Foo()
+  obj.method(1)
+  a.b.deep()
+  x?.optCall()
+  y!.forced()
+  Foo.make().draw()
+  foo.bar().baz()
+  "lit".upper()
+  arr.map { $0.name }
+  let tbl = [cbA, cbB]
+  o.cb = handler
+  reg2(onFire)
+  forward(value: value)
+  let sel = #selector(Holder.fire)
+  defer { cleanup() }
+  let d = dict["k"]
+  return nil
+}
+
+func onFire() {}
+func handler() {}
+func cbA() {}
+func cbB() {}
+
+// ---- extension rows (checklist inventory) ----
+// non-ASCII line before a symbol: café ünïcode 😀
+@objc enum Suit: Int {
+  case hearts = 1
+  case spades
+}
+
+@main struct MainApp {
+  var body: some View { VStack { Text(label) } }
+}
+
+/** block doc is IGNORED and breaks the chain */
+func blockDoc() {}
+
+/// kept over attribute
+@objc func attributed() {}
+
+func <+> (lhs: Session, rhs: Session) -> Session { lhs }
+
+public class Visi {
+  public private(set) var setterGated = 1
+  static let A_B: Int = 2
+  let (tup, tup2) = makePair()
+  @Siblings(through: Pivot.self, from: \.$left) var siblings: [Tag]
+}
+
+protocol Inherited: AnyObject, Identifiable {
+  var reqComputed: String { get set }
+  static var reqStatic: Int { get }
+}
+
+extension [ServerTrustEvaluating] {
+  func sugarExt() {}
+}
+
+func voidParamProof(cb: (Void) -> Void) -> Result<Foo, Err> {
+  callMe()
+  return .success(Foo())
+}
+
+func nestedGenericRet() -> Result<Array<Foo>, Err> { fail() }
+func voidRet() -> Void { noop() }
+func tupleRet() -> (Int, Foo) { (1, Foo()) }
+func fnRet() -> (Int) -> Foo { { _ in Foo() } }
+func optRet() -> Session? { nil }
+func builderRet() -> KF.Builder { KF.Builder() }
+
+func callZooExtra(m: [[Int]], arr: [Int], f: () -> Void) {
+  Foo.init(raw: "x")
+  """
+  multi
+  """.trimmed()
+  ["k": 1].lookup()
+  (f)()
+  arr[0]
+  m[1][2]
+  .make()
+  try? thrower()
+  Task { await asyncFn() }
+  let msg = "count \(counter.next())"
+  #warning("torture warning")
+}
+
+func staticReads(u: User) {
+  let c = Color.red
+  let r = Suit.hearts.rawValue
+  let s = UserModel.self
+  let d = Deep.Nested.leaf
+  let i = lowercase.field
+  let k = \Foo.bar
+}
+
+#if os(iOS)
+func insideDirective() { directiveCall() }
+#endif
+
+// declared-then-assigned: the assignment-prune case (kernel-sweep-caught) —
+// laterAssigned MUST be pruned as a value-ref target.
+let laterAssigned: Int
+if TOP_LEVEL_MAX > 2 {
+  laterAssigned = 1
+} else {
+  laterAssigned = 2
+}
+let readsLater = laterAssigned + TOP_LEVEL_MAX
+
+func guardNoPrune() {
+  guard let TOP_LEVEL_MAX = optSource() else { return }
+  use(TOP_LEVEL_MAX)
+}
+
+func fnRefExtras() {
+  reg(cb: onFire)
+  let sel1 = #selector(fire)
+  let sel2 = #selector(onNote(_:))
+  self.x = x
+}
+
+import class Darwin.FILE
+@testable import TortureKit

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

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

+ 127 - 0
__tests__/kernel-swift-parity.test.ts

@@ -0,0 +1,127 @@
+/**
+ * Kernel↔wasm Swift extraction parity (R7b of the kernel migration).
+ *
+ * Asserts the native walker (codegraph-kernel/src/swift.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.swift: the DEDICATED in-class property branch
+ * (#1020 — computed→property with getter walk, static let/var→constant/
+ * variable, stored→field, owner-attributed decorator/type/attr-arg refs,
+ * observed-property field + class-attributed observer calls), extensions
+ * (multi-segment resolveName, sugar `[Proto]` names, where-clauses),
+ * everything-is-extends inheritance, the full call matrix (subscript reads,
+ * `defer`, optional-chaining receivers, #750 re-encode, literal-set
+ * membership quirks, implicit members), positional return types with the
+ * nested-generic failure, present-false isAsync, `open`→internal visibility,
+ * multi-case enum first-only minting, `/** *​/` docs ignored-and-chain-
+ * breaking, value-ref targets incl. the declared-then-assigned
+ * assignment-prune case the swift-nio sweep caught, SWIFT_SPEC fn-refs with
+ * the label-forward skip and #selector shapes) and its CRLF variant (derived
+ * in-memory — #1329).
+ *
+ * The full-repo sweep lives in scripts/kernel-parity.mjs (Alamofire/vapor/
+ * swift-nio, --max-deferral 0.3 — swift error incidence is structurally
+ * 9–27% on BOTH arms); this suite keeps the invariant alive in `npm test`.
+ * Skips when no kernel binary is staged; CODEGRAPH_KERNEL_EXPECT=1 turns
+ * that into a failure (kernel-scaffold.test.ts).
+ */
+
+import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest';
+import * as fs from 'fs';
+import * as path from 'path';
+import { extractFromSource } from '../src/extraction';
+import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars';
+import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel';
+import type { ExtractionResult } from '../src/types';
+
+const KERNEL_PATH = path.join(
+  __dirname,
+  '..',
+  'codegraph-kernel',
+  'prebuilds',
+  `${process.platform}-${process.arch}`,
+  'codegraph-kernel.node'
+);
+const kernelBuilt = fs.existsSync(KERNEL_PATH);
+
+const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'kernel-parity');
+
+function canon(result: ExtractionResult): { nodes: string[]; edges: string[]; refs: string[] } {
+  return {
+    nodes: result.nodes
+      .map(({ updatedAt: _u, ...n }) => JSON.stringify(n, Object.keys(n).sort()))
+      .sort(),
+    edges: result.edges.map((e) => JSON.stringify(e, Object.keys(e).sort())).sort(),
+    refs: result.unresolvedReferences
+      .map((r) => JSON.stringify(r, Object.keys(r).sort()))
+      .sort(),
+  };
+}
+
+const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const;
+let savedEnv: Record<string, string | undefined>;
+
+describe.skipIf(!kernelBuilt)('kernel Swift extraction parity', () => {
+  beforeAll(async () => {
+    await initGrammars();
+    await loadGrammarsForLanguages(['swift']);
+  });
+
+  beforeEach(() => {
+    savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]]));
+    resetKernelForTests();
+  });
+
+  afterEach(() => {
+    for (const k of ENV_KEYS) {
+      if (savedEnv[k] === undefined) delete process.env[k];
+      else process.env[k] = savedEnv[k];
+    }
+    resetKernelForTests();
+  });
+
+  function assertParity(filePath: string, source: string, minNodes = 3): void {
+    process.env.CODEGRAPH_KERNEL_LANGS = 'all';
+    delete process.env.CODEGRAPH_KERNEL;
+    const viaKernel = tryKernelExtract(filePath, source, 'swift');
+    expect(viaKernel, `kernel extraction failed for ${filePath}`).not.toBeNull();
+
+    process.env.CODEGRAPH_KERNEL = '0';
+    const viaWasm = extractFromSource(filePath, source, 'swift');
+    delete process.env.CODEGRAPH_KERNEL;
+
+    const k = canon(viaKernel!);
+    const w = canon(viaWasm);
+    expect(k.nodes, `${filePath}: nodes`).toEqual(w.nodes);
+    expect(k.edges, `${filePath}: edges`).toEqual(w.edges);
+    expect(k.refs, `${filePath}: refs`).toEqual(w.refs);
+    expect(viaWasm.nodes.length).toBeGreaterThanOrEqual(minNodes);
+  }
+
+  it('torture fixture: property branch, extensions, call matrix, value refs, fn-refs', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.swift');
+    assertParity('fixtures/torture.swift', fs.readFileSync(file, 'utf8'), 40);
+  });
+
+  // CRLF variant — the shape every Windows autocrlf checkout has. Derived in
+  // memory so no platform or editor can silently normalize it away; pins the
+  // JS-multiline-^ docstring semantics for `///` runs (#1329).
+  it('torture fixture CRLF parity', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.swift');
+    const crlf = fs.readFileSync(file, 'utf8').replace(/(?<!\r)\n/g, '\r\n');
+    assertParity('fixtures/torture.swift (crlf)', crlf, 40);
+  });
+
+  it('files with parse errors defer to the wasm extractor (recovery is encoding-dependent)', () => {
+    // A NEW-only regression construct (`#if` between enum cases — the swift
+    // checklist's grammar-bump delta 5) — errors on the 0.7.3 grammar.
+    const broken = 'enum E {\n  case a\n#if DEBUG\n  case b\n#endif\n}\n';
+    process.env.CODEGRAPH_KERNEL_LANGS = 'all';
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(tryKernelExtract('src/Broken.swift', broken, 'swift')).toBeNull();
+    process.env.CODEGRAPH_KERNEL = '0';
+    const viaWasm = extractFromSource('src/Broken.swift', broken, 'swift');
+    delete process.env.CODEGRAPH_KERNEL;
+    expect(viaWasm.nodes.some((n) => n.kind === 'file')).toBe(true);
+  });
+});

+ 11 - 0
codegraph-kernel/Cargo.lock

@@ -62,6 +62,7 @@ dependencies = [
  "tree-sitter-python",
  "tree-sitter-ruby",
  "tree-sitter-rust",
+ "tree-sitter-swift",
  "tree-sitter-typescript",
 ]
 
@@ -594,6 +595,16 @@ dependencies = [
  "tree-sitter-language",
 ]
 
+[[package]]
+name = "tree-sitter-swift"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fe36052155b9dd69ca82b3b8f1b4ccfb2d867125ac1a4db1dd7331829242668c"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 name = "tree-sitter-typescript"
 version = "0.23.2"

+ 5 - 0
codegraph-kernel/Cargo.toml

@@ -41,6 +41,11 @@ tree-sitter-ruby = "=0.23.1"
 # php: the walker calls LANGUAGE_PHP (the full HTML-interleaving variant the
 # wasm ships) — NEVER LANGUAGE_PHP_ONLY, which errors on leading HTML.
 tree-sitter-php = "=0.24.2"
+# swift: the vendored wasm is built from THIS crate's tarball src/ (the tag's
+# checked-in parser.c is an older ABI-14 generation that can never sha-match;
+# grammar.json rules are JSON-equal — swift checklist header). parser.c is
+# ~20MB generated — expect slow compiles.
+tree-sitter-swift = "=0.7.3"
 
 [build-dependencies]
 napi-build = "2"

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

@@ -15,9 +15,9 @@ use tree_sitter::Language;
 
 /// Languages this kernel binary can extract (reported by contractInfo;
 /// TS-side routing policy decides what actually routes).
-pub const LANGUAGES: [&str; 13] = [
+pub const LANGUAGES: [&str; 14] = [
     "typescript", "tsx", "javascript", "jsx", "java", "python", "go", "c", "cpp", "rust",
-    "csharp", "ruby", "php",
+    "csharp", "ruby", "php", "swift",
 ];
 
 pub fn grammar_for(language: &str) -> Option<Language> {
@@ -44,6 +44,9 @@ pub fn grammar_for(language: &str) -> Option<Language> {
         // R7b: v0.24.2, the full HTML-interleaving variant — LANGUAGE_PHP,
         // NEVER LANGUAGE_PHP_ONLY (which errors on leading HTML).
         "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()),
+        // R7b: crate 0.7.3 — the vendored wasm is built from this crate's own
+        // tarball src/ (table identity by construction; see grammars.ts).
+        "swift" => Some(tree_sitter_swift::LANGUAGE.into()),
         _ => None,
     }
 }

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

@@ -28,6 +28,7 @@ mod langs;
 mod php;
 mod ruby;
 mod rustlang;
+mod swift;
 mod textutil;
 mod python;
 mod tsjs;
@@ -219,6 +220,7 @@ pub fn extract_file(file_path: String, content: String, language: String) -> Res
         "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)?,
         _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
     };
     Ok(ExtractBuffers {

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

@@ -0,0 +1,1603 @@
+//! Swift extraction — a faithful Rust port of `TreeSitterExtractor`'s Swift
+//! paths (src/extraction/tree-sitter.ts) plus languages/swift.ts.
+//!
+//! Same porting contract as the other walkers: behavior parity, bug-for-bug.
+//! The authoritative quirk list is docs/design/swift-kernel-port-checklist.md.
+//! The port's center of gravity is the DEDICATED in-class property branch
+//! (#1020 — Alamofire's 348 `property` nodes): computed properties become
+//! `property` nodes whose getter walks with the property pushed; stored
+//! `static let/var` → constant/variable, instance stored → field; decorator/
+//! type-annotation/attr-arg refs all attach to the ENCLOSING TYPE; stored
+//! declarations descend so initializer calls attribute to the class. Also
+//! preserved on purpose: `parameter` field never resolves (zero param type
+//! refs, zero signatures), isAsync is present-false (dead hook), `open` →
+//! internal visibility, everything-is-`extends` inheritance (first
+//! type_identifier of each specifier), no instantiates refs ever (`Foo()` is
+//! a plain call), subscript reads as `calls arr`, `defer` as `calls defer`,
+//! multi-case enum entries minting only the first case, `/** */` block docs
+//! ignored AND chain-breaking, init/deinit/subscript minting no nodes with
+//! their bodies routed through visitNode (calls → class, static reads →
+//! nothing). Positions in UTF-16 code units. Files with parse errors defer
+//! to wasm (structurally high incidence, 9–27% — the sweep runs
+//! --max-deferral 0.3 by measured both-arm reality).
+
+use crate::buffers::{
+    build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
+    RefRow, StrRef, Tables, FLAG_IS_ASYNC, FLAG_IS_EXPORTED, FLAG_IS_STATIC, FUNCTION_REF_CODE,
+    NONE, NONE_STR,
+};
+use crate::docstring::preceding_docstring;
+use crate::ids;
+use crate::textutil as util;
+use regex::Regex;
+use std::collections::{HashMap, HashSet, VecDeque};
+use std::sync::OnceLock;
+use tree_sitter::{Node, Parser};
+
+const MAX_VALUE_REF_NODES: usize = 20_000;
+
+/// BUILTIN_TYPES (tree-sitter.ts) — full shared table (`Bool` is NOT in it;
+/// `Int`/`String`/`Double` are, via the Scala rows — checklist nuances).
+fn is_builtin_type(name: &str) -> bool {
+    matches!(
+        name,
+        "string" | "number" | "boolean" | "void" | "null" | "undefined" | "never" | "any"
+            | "unknown" | "object" | "symbol" | "bigint" | "true" | "false"
+            | "str" | "bool" | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
+            | "u8" | "u16" | "u32" | "u64" | "u128" | "usize" | "f32" | "f64" | "char"
+            | "int" | "long" | "short" | "byte" | "float" | "double"
+            | "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64"
+            | "float32" | "float64" | "complex64" | "complex128" | "rune" | "error"
+            | "Int" | "Long" | "Short" | "Byte" | "Float" | "Double" | "Boolean" | "Char"
+            | "Unit" | "String" | "Any" | "AnyRef" | "AnyVal" | "Nothing" | "Null"
+    )
+}
+
+/// NAME_STOPLIST (function-ref.ts).
+fn is_stoplisted(name: &str) -> bool {
+    matches!(
+        name,
+        "this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
+            | "NULL" | "nullptr" | "None"
+    )
+}
+
+/// LITERAL_RECEIVER_TYPES (tree-sitter.ts:373) — full shared set. Note the
+/// swift-relevant membership quirks: `line_string_literal` IS in it,
+/// `multi_line_string_literal` and `dictionary_literal` are NOT.
+fn is_literal_receiver(kind: &str) -> bool {
+    matches!(
+        kind,
+        "string" | "string_literal" | "interpreted_string_literal" | "raw_string_literal"
+            | "template_string" | "concatenated_string" | "formatted_string" | "f_string"
+            | "line_string_literal" | "string_content" | "heredoc_body"
+            | "number" | "number_literal" | "integer" | "integer_literal" | "float"
+            | "float_literal" | "int_literal" | "decimal_integer_literal" | "real_literal"
+            | "char_literal" | "character_literal" | "rune_literal" | "regex" | "regex_literal"
+            | "true" | "false" | "boolean_literal" | "bool_literal" | "none" | "null" | "nil"
+            | "null_literal" | "undefined"
+            | "list" | "list_literal" | "array" | "array_literal" | "array_creation_expression"
+            | "dictionary" | "dict_literal" | "object" | "tuple" | "set"
+    )
+}
+
+/// `/^[A-Za-z_]\w*$/` with JS's ASCII `\w`.
+fn ascii_ident_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_][0-9A-Za-z_]*$").unwrap())
+}
+/// getReturnType's generics strip (`/<[^>]*>/g`) — non-nesting (rust-class quirk).
+fn generic_args_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
+}
+/// extractStaticMemberRef's capitalized-receiver test.
+fn capitalized_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Z][A-Za-z0-9_]*$").unwrap())
+}
+
+/// JS `\s` for the chained-call inner-callee strip (`.replace(/\s+/g, '')`).
+fn is_js_space(c: char) -> bool {
+    matches!(
+        c,
+        '\t' | '\n' | '\x0B' | '\x0C' | '\r' | ' ' | '\u{00A0}' | '\u{1680}'
+            | '\u{2000}'..='\u{200A}' | '\u{2028}' | '\u{2029}' | '\u{202F}' | '\u{205F}'
+            | '\u{3000}' | '\u{FEFF}'
+    )
+}
+fn strip_js_ws(s: &str) -> String {
+    s.chars().filter(|c| !is_js_space(*c)).collect()
+}
+
+struct Scope {
+    row: u32,
+    kind: &'static str,
+    name: String,
+}
+
+#[derive(Default)]
+struct Extra {
+    docstring: Option<String>,
+    signature: Option<String>,
+    visibility: Option<u8>,
+    is_static: Option<bool>,
+    is_async: Option<bool>,
+    is_exported: Option<bool>,
+    return_type: Option<String>,
+}
+
+struct ValueScope<'t> {
+    row: u32,
+    node: Node<'t>,
+    name: String,
+}
+
+struct Cand {
+    from: u32,
+    name: String,
+    line: u32,
+    column_byte: usize,
+    row: usize,
+}
+
+struct SwiftPropInfo<'t> {
+    name_node: Option<Node<'t>>,
+    is_let: bool,
+    is_computed: bool,
+}
+
+pub struct Walker<'t> {
+    src: &'t str,
+    file_path: &'t str,
+    line_starts: Vec<usize>,
+    arena: Arena,
+    tables: Tables,
+    stack: Vec<Scope>,
+    node_ids: Vec<String>,
+    defined_fn_names: HashSet<String>,
+    imported_names: HashSet<String>,
+    fn_ref_cands: Vec<Cand>,
+    fs_values: HashMap<String, u32>,
+    fs_value_counts: HashMap<String, u32>,
+    value_scopes: Vec<ValueScope<'t>>,
+}
+
+pub fn extract(file_path: &str, source: &str) -> Result<EmitOut, String> {
+    let grammar = crate::langs::grammar_for("swift").ok_or("no swift grammar")?;
+    let t0 = std::time::Instant::now();
+    let mut parser = Parser::new();
+    parser
+        .set_language(&grammar)
+        .map_err(|e| format!("set_language(swift) failed: {e}"))?;
+    let tree = parser
+        .parse(source, None)
+        .ok_or_else(|| "parser returned null tree".to_string())?;
+    if tree.root_node().has_error() {
+        return Err("defer: parse tree contains errors — wasm recovery is canonical".to_string());
+    }
+
+    let mut w = Walker {
+        src: source,
+        file_path,
+        line_starts: util::line_starts(source),
+        arena: Arena::default(),
+        tables: Tables::default(),
+        stack: Vec::new(),
+        node_ids: Vec::new(),
+        defined_fn_names: HashSet::new(),
+        imported_names: HashSet::new(),
+        fn_ref_cands: Vec::new(),
+        fs_values: HashMap::new(),
+        fs_value_counts: HashMap::new(),
+        value_scopes: Vec::new(),
+    };
+
+    let line_count = source.bytes().filter(|b| *b == b'\n').count() as u32 + 1;
+    let base_name = file_path.rsplit(['/', '\\']).next().unwrap_or(file_path);
+    let mut flags = BoolFlags::default();
+    flags.set(FLAG_IS_EXPORTED, false);
+    let file_id = w.arena.put(&ids::file_node_id(file_path));
+    let name_ref = w.arena.put(base_name);
+    let qn_ref = w.arena.put(file_path);
+    w.tables.push_node(&NodeRow {
+        kind: node_kind_index("file").unwrap(),
+        visibility: 0,
+        flags,
+        start_line: 1,
+        end_line: line_count,
+        start_column: 0,
+        end_column: 0,
+        name: name_ref,
+        qualified_name: qn_ref,
+        id: file_id,
+        docstring: NONE_STR,
+        signature: NONE_STR,
+        decorators: NONE_STR,
+        type_parameters: NONE_STR,
+        return_type: NONE_STR,
+        extra_json: NONE_STR,
+    });
+    w.node_ids.push(ids::file_node_id(file_path));
+    w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
+
+    // No packageTypes — swift has no namespace node; top-level QNs are bare.
+    w.visit_node(tree.root_node());
+    w.flush_fn_ref_candidates();
+    w.flush_value_refs(tree.root_node());
+    w.stack.pop();
+
+    let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
+    let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
+    Ok(EmitOut {
+        meta,
+        nodes: w.tables.nodes,
+        edges: w.tables.edges,
+        refs: w.tables.refs,
+        arena: w.arena.into_vec(),
+    })
+}
+
+/// firstSimpleIdentifier (tree-sitter.ts:261): BFS (FIFO), at most 40 nodes
+/// popped, first `simple_identifier` wins.
+fn first_simple_identifier<'t>(node: Option<Node<'t>>) -> Option<Node<'t>> {
+    let mut q: VecDeque<Node<'t>> = VecDeque::new();
+    if let Some(n) = node {
+        q.push_back(n);
+    }
+    let mut guard = 0;
+    while guard < 40 {
+        let Some(n) = q.pop_front() else { break };
+        guard += 1;
+        if n.kind() == "simple_identifier" {
+            return Some(n);
+        }
+        for i in 0..n.named_child_count() {
+            if let Some(c) = n.named_child(i) {
+                q.push_back(c);
+            }
+        }
+    }
+    None
+}
+
+/// 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>> {
+    let mut found: Option<Node<'t>> = None;
+    for i in 0..node.named_child_count() {
+        let Some(child) = node.named_child(i) else { continue };
+        if child.kind() == "simple_identifier" {
+            found = Some(child);
+        }
+        if let Some(deeper) = last_simple_identifier(child) {
+            found = Some(deeper);
+        }
+    }
+    found
+}
+
+impl<'t> Walker<'t> {
+    fn text(&self, node: Node) -> &'t str {
+        &self.src[node.byte_range()]
+    }
+    fn line_of(&self, node: Node) -> u32 {
+        node.start_position().row as u32 + 1
+    }
+    fn col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
+    }
+    fn end_col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
+    }
+    fn top_row(&self) -> u32 {
+        self.stack.last().map(|s| s.row).unwrap_or(0)
+    }
+    fn inside_class_like(&self) -> bool {
+        self.stack
+            .last()
+            .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module"))
+            .unwrap_or(false)
+    }
+
+    fn push_ref(&mut self, from_row: u32, name: &str, kind_code: u8, line: u32, column: u32) {
+        let name_ref = self.arena.put(name);
+        self.tables.push_ref(&RefRow {
+            from_idx: from_row,
+            kind: kind_code,
+            line,
+            column,
+            reference_name: name_ref,
+            candidates: NONE_STR,
+            from_id_str: NONE_STR,
+        });
+        if kind_code == edge_kind_index("imports").unwrap() {
+            if util::simple_name().is_match(name) {
+                self.imported_names.insert(name.to_string());
+            } else if let Some(c) = util::qualified_import().captures(name) {
+                self.imported_names.insert(c[1].to_string());
+            }
+        }
+    }
+
+    fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
+        self.push_ref(from_row, name, kind_code, self.line_of(node), self.col_of(node));
+    }
+
+    // --- createNode ------------------------------------------------------------
+
+    fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
+        if name.is_empty() {
+            return None;
+        }
+        let start_line = self.line_of(node);
+        let id = ids::node_id(self.file_path, kind, name, start_line);
+        let end_line = node.end_position().row as u32 + 1; // no resolveBody for swift
+
+        let qualified = {
+            let mut parts: Vec<&str> = Vec::new();
+            for s in &self.stack {
+                if s.kind != "file" {
+                    parts.push(&s.name);
+                }
+            }
+            let mut qn = parts.join("::");
+            if !qn.is_empty() {
+                qn.push_str("::");
+            }
+            qn.push_str(name);
+            qn
+        };
+
+        let mut flags = BoolFlags::default();
+        if let Some(v) = extra.is_exported {
+            flags.set(FLAG_IS_EXPORTED, v);
+        }
+        if let Some(v) = extra.is_async {
+            flags.set(FLAG_IS_ASYNC, v);
+        }
+        if let Some(v) = extra.is_static {
+            flags.set(FLAG_IS_STATIC, v);
+        }
+        let name_ref = self.arena.put(name);
+        let qn_ref = self.arena.put(&qualified);
+        let id_ref = self.arena.put(&id);
+        let doc_ref = opt_str(&mut self.arena, extra.docstring.as_deref());
+        let sig_ref = opt_str(&mut self.arena, extra.signature.as_deref());
+        let ret_ref = opt_str(&mut self.arena, extra.return_type.as_deref());
+        let row = self.tables.push_node(&NodeRow {
+            kind: node_kind_index(kind).unwrap(),
+            visibility: extra.visibility.unwrap_or(0),
+            flags,
+            start_line,
+            end_line,
+            start_column: self.col_of(node),
+            end_column: self.end_col_of(node),
+            name: name_ref,
+            qualified_name: qn_ref,
+            id: id_ref,
+            docstring: doc_ref,
+            signature: sig_ref,
+            decorators: NONE_STR, // extractModifiers absent — never set from modifiers
+            type_parameters: NONE_STR,
+            return_type: ret_ref,
+            extra_json: NONE_STR,
+        });
+        self.node_ids.push(id);
+
+        let parent_row = self.top_row();
+        self.tables.push_edge(&EdgeRow {
+            source_idx: parent_row,
+            target_idx: row,
+            kind: edge_kind_index("contains").unwrap(),
+            provenance: 0,
+            line: NONE,
+            column: NONE,
+            metadata_json: NONE_STR,
+            source_id_str: NONE_STR,
+            target_id_str: NONE_STR,
+        });
+
+        if kind == "function" || kind == "method" {
+            self.defined_fn_names.insert(name.to_string());
+        }
+        // captureValueRefScope — struct:/enum: parents accepted (the swift
+        // static-let-namespacing idiom).
+        let target_kind_ok = kind == "constant" || kind == "variable";
+        if target_kind_ok
+            && util::utf16_len(name) >= 3
+            && util::has_upper_or_underscore().is_match(name)
+        {
+            let parent_ok = self
+                .stack
+                .last()
+                .map(|s| matches!(s.kind, "file" | "class" | "module" | "struct" | "enum"))
+                .unwrap_or(false);
+            if parent_ok {
+                self.fs_values.insert(name.to_string(), row);
+                *self.fs_value_counts.entry(name.to_string()).or_insert(0) += 1;
+            }
+        }
+        if matches!(kind, "function" | "method" | "constant" | "variable") {
+            self.value_scopes.push(ValueScope { row, node, name: name.to_string() });
+        }
+        Some(row)
+    }
+
+    // --- hooks (languages/swift.ts) ----------------------------------------------
+
+    /// extractName incl. the resolveName hook: a multi-segment extension name
+    /// (`extension KF.Builder`) takes the LAST type_identifier's text.
+    fn extract_name(&self, node: Node) -> String {
+        if node.kind() == "class_declaration" {
+            if let Some(name_node) = node.child_by_field_name("name") {
+                if name_node.kind() == "user_type" {
+                    let ids: Vec<Node> = (0..name_node.named_child_count())
+                        .filter_map(|i| name_node.named_child(i))
+                        .filter(|c| c.kind() == "type_identifier")
+                        .collect();
+                    if ids.len() > 1 {
+                        return self.text(ids[ids.len() - 1]).to_string();
+                    }
+                }
+            }
+        }
+        if let Some(name_node) = node.child_by_field_name("name") {
+            return self.text(name_node).to_string();
+        }
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                if matches!(c.kind(), "identifier" | "type_identifier" | "simple_identifier" | "constant") {
+                    return self.text(c).to_string();
+                }
+            }
+        }
+        "<anonymous>".to_string()
+    }
+
+    /// getVisibility: whole-text substring matching over `modifiers` children;
+    /// default INTERNAL. `open` → internal, `fileprivate` → private (via the
+    /// 'private' substring), `public private(set)` → public (first match).
+    fn visibility_of(&self, node: Node) -> u8 {
+        for i in 0..node.child_count() {
+            let Some(child) = node.child(i) else { continue };
+            if child.kind() == "modifiers" {
+                let text = self.text(child);
+                if text.contains("public") {
+                    return 1;
+                }
+                if text.contains("private") {
+                    return 2;
+                }
+                if text.contains("internal") {
+                    return 4;
+                }
+                // 'fileprivate' arm is dead — 'private' already matched.
+            }
+        }
+        4 // Swift defaults to internal
+    }
+
+    /// isStatic: modifiers text contains 'static' OR 'class' (class members
+    /// count — deliberate; substring semantics preserved).
+    fn is_static(&self, node: Node) -> bool {
+        (0..node.child_count())
+            .filter_map(|i| node.child(i))
+            .any(|c| {
+                c.kind() == "modifiers" && {
+                    let t = self.text(c);
+                    t.contains("static") || t.contains("class")
+                }
+            })
+    }
+
+    /// isAsync: dead hook — `async` never sits inside `modifiers` (it's an
+    /// anon child after the params) → effectively always false, but PRESENT.
+    fn is_async(&self, node: Node) -> bool {
+        (0..node.child_count())
+            .filter_map(|i| node.child(i))
+            .any(|c| c.kind() == "modifiers" && self.text(c).contains("async"))
+    }
+
+    /// extractSwiftReturnType — POSITIONAL: first user_type/optional_type after
+    /// the name simple_identifier, before function_body; last dotted segment;
+    /// generics stripped non-nesting; Void → None.
+    fn return_type_of(&self, node: Node) -> Option<String> {
+        let mut seen_name = false;
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            if child.kind() == "simple_identifier" && !seen_name {
+                seen_name = true;
+                continue;
+            }
+            if !seen_name {
+                continue;
+            }
+            if child.kind() == "function_body" {
+                return None;
+            }
+            let type_node = match child.kind() {
+                "user_type" => Some(child),
+                "optional_type" => (0..child.named_child_count())
+                    .filter_map(|j| child.named_child(j))
+                    .find(|c| c.kind() == "user_type"),
+                _ => None,
+            };
+            if child.kind() == "user_type" || child.kind() == "optional_type" {
+                let Some(t) = type_node else { return None };
+                let name = generic_args_re()
+                    .replace_all(self.text(t).trim(), "")
+                    .into_owned();
+                let last = name.rsplit('.').next().unwrap_or("").trim();
+                if last.is_empty() || !ascii_ident_re().is_match(last) || last == "Void" {
+                    return None;
+                }
+                return Some(last.to_string());
+            }
+        }
+        None
+    }
+
+    /// swiftPropertyInfo (tree-sitter.ts:277).
+    fn swift_property_info(&self, node: Node<'t>) -> SwiftPropInfo<'t> {
+        let pattern = node.child_by_field_name("name").or_else(|| {
+            (0..node.named_child_count())
+                .filter_map(|i| node.named_child(i))
+                .find(|c| matches!(c.kind(), "value_binding_pattern" | "pattern"))
+        });
+        let binding = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "value_binding_pattern");
+        let is_let = binding
+            .map(|b| self.text(b).trim_start().starts_with("let"))
+            .unwrap_or(false);
+        let is_computed = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .any(|c| matches!(c.kind(), "computed_property" | "protocol_property_requirements"));
+        SwiftPropInfo { name_node: first_simple_identifier(pattern), is_let, is_computed }
+    }
+
+    // --- the dispatcher (visitNode, Swift-relevant branches) -----------------------
+
+    fn visit_node(&mut self, node: Node<'t>) {
+        let kind = node.kind();
+        let mut skip_children = false;
+
+        self.maybe_capture_fn_refs(node);
+
+        if kind == "function_declaration" {
+            if self.inside_class_like() {
+                self.extract_method(node);
+            } else {
+                self.extract_function(node);
+            }
+            skip_children = true;
+        } else if kind == "class_declaration" {
+            // classifyClassNode: `struct`/`enum` keyword children; actor and
+            // extension fall through to 'class'.
+            let mut classified = "class";
+            for i in 0..node.child_count() {
+                if let Some(c) = node.child(i) {
+                    if c.kind() == "struct" {
+                        classified = "struct";
+                        break;
+                    }
+                    if c.kind() == "enum" {
+                        classified = "enum";
+                        break;
+                    }
+                }
+            }
+            match classified {
+                "struct" => self.extract_struct(node),
+                "enum" => self.extract_enum(node),
+                _ => self.extract_class(node),
+            }
+            skip_children = true;
+        } else if kind == "protocol_declaration" {
+            self.extract_interface(node);
+            skip_children = true;
+        } else if kind == "typealias_declaration" {
+            skip_children = self.extract_type_alias(node);
+        } else if kind == "property_declaration" && !self.inside_class_like() {
+            // Top-level let/var (extractVariable's swift branch). Initializers
+            // are NEVER walked — candidates-only scan.
+            self.extract_variable(node);
+            self.scan_fn_ref_subtree(node, 0);
+            skip_children = true;
+        } else if matches!(kind, "property_declaration" | "protocol_property_declaration")
+            && self.inside_class_like()
+        {
+            skip_children = self.dedicated_property_branch(node);
+        } else if kind == "import_declaration" {
+            self.extract_import(node);
+        } else if kind == "call_expression" {
+            self.extract_call(node);
+        }
+        // init/deinit/subscript declarations, macro_invocation, directive,
+        // diagnostic, operator/precedence declarations, protocol function
+        // requirements, associatedtype: no branch — recursed. Their calls
+        // attribute to the enclosing scope; static-member reads inside them
+        // emit NOTHING (the pass is body-walker-only).
+
+        if !skip_children {
+            for i in 0..node.named_child_count() {
+                if let Some(c) = node.named_child(i) {
+                    self.visit_node(c);
+                }
+            }
+        }
+    }
+
+    /// THE DEDICATED PROPERTY BRANCH (tree-sitter.ts:1113-1193, #1020).
+    /// Returns skipChildren.
+    fn dedicated_property_branch(&mut self, node: Node<'t>) -> bool {
+        let owner_row = self.top_row();
+        let info = self.swift_property_info(node);
+        let mut computed_prop: Option<(u32, String)> = None;
+
+        if let Some(name_node) = info.name_node {
+            let name = self.text(name_node).to_string();
+            if info.is_computed {
+                let row = self.create_node(
+                    "property",
+                    &name,
+                    node,
+                    Extra {
+                        visibility: Some(self.visibility_of(node)),
+                        is_static: Some(self.is_static(node)),
+                        ..Extra::default()
+                    },
+                );
+                if let Some(row) = row {
+                    computed_prop = Some((row, name));
+                }
+            } else {
+                let is_static = self.is_static(node);
+                let kind: &'static str = if is_static {
+                    if info.is_let { "constant" } else { "variable" }
+                } else {
+                    "field"
+                };
+                self.create_node(
+                    kind,
+                    &name,
+                    node,
+                    Extra {
+                        visibility: Some(self.visibility_of(node)),
+                        is_static: Some(is_static),
+                        ..Extra::default()
+                    },
+                );
+            }
+        }
+
+        // All three ref passes attach to the ENCLOSING TYPE (ownerId).
+        self.extract_decorators_for(node, owner_row);
+        // extractVariableTypeAnnotation: the direct type_annotation child.
+        let ta = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "type_annotation");
+        if let Some(ta) = ta {
+            self.extract_type_refs_from_subtree(ta, owner_row);
+        }
+        // walkAttrArgs: extractStaticMemberRef over the whole modifiers subtree
+        // (`@Siblings(through: Pivot.self)` metatype args).
+        let mods = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "modifiers");
+        if let Some(mods) = mods {
+            self.walk_attr_args(mods);
+        }
+
+        if let Some((row, name)) = computed_prop {
+            let getter = (0..node.named_child_count())
+                .filter_map(|i| node.named_child(i))
+                .find(|c| matches!(c.kind(), "computed_property" | "protocol_property_requirements"));
+            if let Some(getter) = getter {
+                self.stack.push(Scope { row, kind: "property", name });
+                self.visit_function_body(getter);
+                self.stack.pop();
+            }
+            return true; // skipChildren — computed only
+        }
+        // Stored: descend generically — initializer calls attribute to the
+        // CLASS; observers' bodies likewise; modifiers re-walk is harmless.
+        false
+    }
+
+    fn walk_attr_args(&mut self, n: Node<'t>) {
+        self.extract_static_member_ref(n);
+        for i in 0..n.named_child_count() {
+            if let Some(c) = n.named_child(i) {
+                self.walk_attr_args(c);
+            }
+        }
+    }
+
+    // --- visitFunctionBody ---------------------------------------------------------
+
+    fn visit_function_body(&mut self, body: Node<'t>) {
+        self.visit_for_calls_and_structure(body);
+    }
+
+    fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        let kind = node.kind();
+        self.maybe_capture_fn_refs(node);
+
+        if kind == "call_expression" {
+            self.extract_call(node);
+        }
+        // (INSTANTIATION_KINDS has no swift types; extractBareCall absent.)
+
+        self.extract_static_member_ref(node);
+
+        if kind == "function_declaration" {
+            let name = self.extract_name(node);
+            if name != "<anonymous>" {
+                self.extract_function(node);
+                return;
+            }
+        }
+        if kind == "class_declaration" {
+            let mut classified = "class";
+            for i in 0..node.child_count() {
+                if let Some(c) = node.child(i) {
+                    if c.kind() == "struct" {
+                        classified = "struct";
+                        break;
+                    }
+                    if c.kind() == "enum" {
+                        classified = "enum";
+                        break;
+                    }
+                }
+            }
+            match classified {
+                "struct" => self.extract_struct(node),
+                "enum" => self.extract_enum(node),
+                _ => self.extract_class(node),
+            }
+            return;
+        }
+        if kind == "protocol_declaration" {
+            self.extract_interface(node);
+            return;
+        }
+
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.visit_for_calls_and_structure(c);
+            }
+        }
+    }
+
+    // --- extractors -----------------------------------------------------------------
+
+    fn extract_function(&mut self, node: Node<'t>) {
+        let name = self.extract_name(node);
+        if name == "<anonymous>" {
+            if let Some(body) = node.child_by_field_name("body") {
+                self.visit_function_body(body);
+            }
+            return;
+        }
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            signature: None, // getSignature reads the never-resolving 'parameter' field
+            visibility: Some(self.visibility_of(node)),
+            is_async: Some(self.is_async(node)), // present-false (dead hook)
+            is_static: Some(self.is_static(node)),
+            return_type: self.return_type_of(node),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("function", &name, node, extra) else { return };
+        self.extract_type_annotations(node, row);
+        self.extract_decorators_for(node, row);
+        self.stack.push(Scope { row, kind: "function", name });
+        if let Some(body) = node.child_by_field_name("body") {
+            self.visit_function_body(body);
+        }
+        self.stack.pop();
+    }
+
+    fn extract_method(&mut self, node: Node<'t>) {
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            signature: None,
+            visibility: Some(self.visibility_of(node)),
+            is_async: Some(self.is_async(node)),
+            is_static: Some(self.is_static(node)),
+            return_type: self.return_type_of(node),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("method", &name, node, extra) else { return };
+        self.extract_type_annotations(node, row);
+        self.extract_decorators_for(node, row);
+        self.stack.push(Scope { row, kind: "method", name });
+        if let Some(body) = node.child_by_field_name("body") {
+            self.visit_function_body(body);
+        }
+        self.stack.pop();
+    }
+
+    fn extract_class(&mut self, node: Node<'t>) {
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: Some(self.visibility_of(node)),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("class", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        // primaryCtor refs: csharp-only (no parameter_list child type).
+        // Classes DO get decorates (`@Observable class`), unlike struct/enum.
+        self.extract_decorators_for(node, row);
+        self.stack.push(Scope { row, kind: "class", name });
+        let body = node.child_by_field_name("body").unwrap_or(node);
+        for i in 0..body.named_child_count() {
+            if let Some(c) = body.named_child(i) {
+                self.visit_node(c);
+            }
+        }
+        self.stack.pop();
+    }
+
+    fn extract_struct(&mut self, node: Node<'t>) {
+        // 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);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: Some(self.visibility_of(node)),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("struct", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        // NO extractDecoratorsFor for structs (`@main struct` emits nothing).
+        self.stack.push(Scope { row, kind: "struct", name });
+        for i in 0..body.named_child_count() {
+            if let Some(c) = body.named_child(i) {
+                self.visit_node(c);
+            }
+        }
+        self.stack.pop();
+    }
+
+    fn extract_enum(&mut self, node: Node<'t>) {
+        let Some(body) = node.child_by_field_name("body") else { return };
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            visibility: Some(self.visibility_of(node)),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("enum", &name, node, extra) else { return };
+        // Raw-value types ride inheritance (`enum Suit: String` → extends
+        // String — extends refs have NO builtin filter). NO decorates.
+        self.extract_inheritance(node, row);
+        self.stack.push(Scope { row, kind: "enum", name });
+        for i in 0..body.named_child_count() {
+            let Some(child) = body.named_child(i) else { continue };
+            if child.kind() == "enum_entry" {
+                self.extract_enum_members(child);
+            } else {
+                self.visit_node(child);
+            }
+        }
+        self.stack.pop();
+    }
+
+    fn extract_enum_members(&mut self, node: Node<'t>) {
+        // `name` field = the FIRST case name only — `case put, delete` mints
+        // ONLY `put` (the identifier-scan fallback is dead, the field always
+        // resolves). Associated/raw values never walked.
+        if let Some(name_node) = node.child_by_field_name("name") {
+            let name = self.text(name_node).to_string();
+            self.create_node("enum_member", &name, node, Extra::default());
+        }
+    }
+
+    fn extract_interface(&mut self, node: Node<'t>) {
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            ..Extra::default() // NO visibility, NO decorates
+        };
+        let Some(row) = self.create_node("interface", &name, node, extra) else { return };
+        self.extract_inheritance(node, row);
+        self.stack.push(Scope { row, kind: "interface", name });
+        let body = node.child_by_field_name("body").unwrap_or(node);
+        for i in 0..body.named_child_count() {
+            if let Some(c) = body.named_child(i) {
+                self.visit_node(c);
+            }
+        }
+        self.stack.pop();
+    }
+
+    /// extractTypeAlias (:2890) — plain type_alias node + value-subtree type
+    /// refs (`typealias Handler = (Data) -> Void` → refs Data + Void…Void is
+    /// builtin-suppressed; `= KF.Builder` → refs KF AND Builder). Returns
+    /// skipChildren=false (children also recursed, harmlessly).
+    fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
+        let name = self.extract_name(node);
+        if name == "<anonymous>" {
+            return false;
+        }
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            ..Extra::default()
+        };
+        let row = self.create_node("type_alias", &name, node, extra);
+        if let Some(row) = row {
+            if let Some(value) = node.child_by_field_name("value") {
+                self.extract_type_refs_from_subtree(value, row);
+            }
+        }
+        false
+    }
+
+    /// extractVariable — the swift top-level branch (:2851): let → constant /
+    /// var → variable via swiftPropertyInfo; computed skipped; position = the
+    /// whole declaration; extras = docstring + isExported literal FALSE.
+    fn extract_variable(&mut self, node: Node<'t>) {
+        let docstring = preceding_docstring(node, self.src);
+        let info = self.swift_property_info(node);
+        let Some(name_node) = info.name_node else { return };
+        if info.is_computed {
+            return;
+        }
+        let kind: &'static str = if info.is_let { "constant" } else { "variable" };
+        let name = self.text(name_node).to_string();
+        self.create_node(
+            kind,
+            &name,
+            node,
+            Extra { docstring, is_exported: Some(false), ..Extra::default() },
+        );
+    }
+
+    fn extract_import(&mut self, node: Node<'t>) {
+        let import_text = self.text(node).trim().to_string();
+        let identifier = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "identifier");
+        let Some(identifier) = identifier else { return }; // hook null → nothing
+        let module_name = self.text(identifier).to_string();
+        if module_name.is_empty() {
+            return;
+        }
+        self.create_node(
+            "import",
+            &module_name,
+            node,
+            Extra { signature: Some(import_text), ..Extra::default() },
+        );
+        let parent = self.top_row();
+        self.push_ref_at(parent, &module_name.clone(), edge_kind_index("imports").unwrap(), node);
+    }
+
+    /// extractCall — swift rides the generic member branch (navigation) and
+    /// the raw-text else; the full matrix is in the checklist.
+    fn extract_call(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let caller = self.top_row();
+        let func = node
+            .child_by_field_name("function")
+            .or_else(|| node.named_child(0));
+        let Some(func) = func else { return };
+        let mut callee_name = String::new();
+
+        if func.kind() == "navigation_expression" {
+            // property = property/field fields (null) → namedChild(1), with
+            // the navigation_suffix simple_identifier unwrap.
+            let property = func
+                .child_by_field_name("property")
+                .or_else(|| func.child_by_field_name("field"))
+                .or_else(|| {
+                    let c1 = func.named_child(1);
+                    match c1 {
+                        Some(c) if c.kind() == "navigation_suffix" => (0..c.named_child_count())
+                            .filter_map(|i| c.named_child(i))
+                            .find(|g| g.kind() == "simple_identifier")
+                            .or(Some(c)),
+                        other => other,
+                    }
+                });
+            if let Some(property) = property {
+                let method_name = self.text(property);
+                let receiver = func
+                    .child_by_field_name("object")
+                    .or_else(|| func.child_by_field_name("operand"))
+                    .or_else(|| func.child_by_field_name("argument"))
+                    .or_else(|| func.named_child(0));
+                if let Some(r) = receiver {
+                    if is_literal_receiver(r.kind()) {
+                        return; // `"lit".upper()` / `5.times()` — nothing
+                    }
+                }
+                let recv_ident = receiver.filter(|r| {
+                    matches!(r.kind(), "identifier" | "simple_identifier" | "field_identifier")
+                });
+                if let Some(r) = recv_ident {
+                    let receiver_name = self.text(r);
+                    if matches!(receiver_name, "self" | "this" | "cls" | "super") {
+                        callee_name = method_name.to_string();
+                    } else {
+                        callee_name = format!("{receiver_name}.{method_name}");
+                    }
+                } else if receiver.map(|r| r.kind() == "call_expression").unwrap_or(false) {
+                    // #750 swift re-encode: innerNav = receiver.namedChild(0),
+                    // ws-stripped; capitalized chains only.
+                    let inner = receiver.unwrap().named_child(0);
+                    let inner_callee =
+                        inner.map(|n| strip_js_ws(self.text(n))).unwrap_or_default();
+                    let reencode = inner_callee
+                        .as_bytes()
+                        .first()
+                        .map(|b| b.is_ascii_uppercase())
+                        .unwrap_or(false);
+                    callee_name = if reencode {
+                        format!("{inner_callee}().{method_name}")
+                    } else {
+                        method_name.to_string()
+                    };
+                } else {
+                    // self_expression / super_expression / inner nav /
+                    // postfix / multi_line_string_literal → bare method name.
+                    callee_name = method_name.to_string();
+                }
+            }
+        } else {
+            // Raw func text: bare `helper`, `Foo` (constructor = plain call),
+            // `arr` (subscript reads!), `m[i]`, `defer`, `.make`, tuple
+            // callees (conv-regex below), array-literal callees (bump delta 8).
+            callee_name = self.text(func).to_string();
+        }
+
+        if !callee_name.is_empty() {
+            if let Some(c) = util::paren_conversion().captures(&callee_name) {
+                callee_name = c[1].to_string();
+            }
+            self.push_ref_at(caller, &callee_name.clone(), edge_kind_index("calls").unwrap(), node);
+        }
+    }
+
+    /// extractStaticMemberRef — swift's navigation_expression value reads,
+    /// body walker + walkAttrArgs only.
+    fn extract_static_member_ref(&mut self, node: Node<'t>) {
+        if node.kind() != "navigation_expression" {
+            return;
+        }
+        if self.stack.is_empty() {
+            return;
+        }
+        let owner = self.top_row();
+        // Skip the callee nav of a call.
+        if let Some(parent) = node.parent() {
+            if parent.kind() == "call_expression" {
+                let callee = parent
+                    .child_by_field_name("function")
+                    .or_else(|| parent.child_by_field_name("method"))
+                    .or_else(|| parent.named_child(0));
+                if let Some(callee) = callee {
+                    if callee.start_byte() == node.start_byte() {
+                        return;
+                    }
+                }
+            }
+        }
+        let recv = node
+            .child_by_field_name("object")
+            .or_else(|| node.child_by_field_name("expression"))
+            .or_else(|| node.child_by_field_name("scope"))
+            .or_else(|| node.named_child(0));
+        let Some(recv) = recv else { return };
+        if matches!(
+            recv.kind(),
+            "identifier" | "type_identifier" | "simple_identifier" | "name" | "scoped_type_identifier"
+        ) {
+            let text = self.text(recv);
+            if capitalized_re().is_match(text) {
+                self.push_ref_at(owner, &text.to_string(), edge_kind_index("references").unwrap(), recv);
+            }
+        }
+    }
+
+    /// extractInheritance — the swift inheritance_specifier case: FIRST
+    /// type_identifier of each specifier's user_type, everything as `extends`
+    /// (conformances included; `: Module.Base` takes `Module`).
+    fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        let extends_kind = edge_kind_index("extends").unwrap();
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            if child.kind() != "inheritance_specifier" {
+                continue;
+            }
+            let user_type = (0..child.named_child_count())
+                .filter_map(|j| child.named_child(j))
+                .find(|c| c.kind() == "user_type");
+            let Some(user_type) = user_type else { continue };
+            let type_id = (0..user_type.named_child_count())
+                .filter_map(|j| user_type.named_child(j))
+                .find(|c| c.kind() == "type_identifier");
+            let Some(type_id) = type_id else { continue };
+            let name = self.text(type_id).to_string();
+            self.push_ref_at(class_row, &name, extends_kind, type_id);
+        }
+    }
+
+    /// extractTypeAnnotations — generic path: the 'parameter' field NEVER
+    /// resolves (zero param refs), 'return_type' DOES; the direct
+    /// type_annotation find is null for functions.
+    fn extract_type_annotations(&mut self, node: Node<'t>, from_row: u32) {
+        if let Some(params) = node.child_by_field_name("parameter") {
+            // Unreachable (field never resolves) — mirrored for shape.
+            self.extract_type_refs_from_subtree(params, from_row);
+        }
+        if let Some(ret) = node.child_by_field_name("return_type") {
+            self.extract_type_refs_from_subtree(ret, from_row);
+        }
+        let ta = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "type_annotation");
+        if let Some(ta) = ta {
+            self.extract_type_refs_from_subtree(ta, from_row);
+        }
+    }
+
+    fn extract_type_refs_from_subtree(&mut self, node: Node<'t>, from_row: u32) {
+        if node.kind() == "type_identifier" {
+            let type_name = self.text(node).to_string();
+            if !type_name.is_empty() && !is_builtin_type(&type_name) {
+                self.push_ref_at(from_row, &type_name, edge_kind_index("references").unwrap(), node);
+            }
+            return;
+        }
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.extract_type_refs_from_subtree(c, from_row);
+            }
+        }
+    }
+
+    /// extractDecoratorsFor — swift `attribute` nodes inside `modifiers`.
+    /// Coverage: functions/methods/classes/dedicated-branch properties only.
+    fn extract_decorators_for(&mut self, decl: Node<'t>, decorated_row: u32) {
+        for i in 0..decl.named_child_count() {
+            let Some(child) = decl.named_child(i) else { continue };
+            self.consider_decorator(child, decorated_row);
+            if child.kind() == "modifiers" {
+                for j in 0..child.named_child_count() {
+                    if let Some(m) = child.named_child(j) {
+                        self.consider_decorator(m, decorated_row);
+                    }
+                }
+            }
+        }
+        // Preceding-sibling scan — swift attributes are never siblings; the
+        // scan stops at the first non-decorator sibling immediately.
+        let Some(parent) = decl.parent() else { return };
+        let decl_start = decl.start_byte();
+        let mut decl_idx: isize = -1;
+        for i in 0..parent.named_child_count() {
+            if let Some(sib) = parent.named_child(i) {
+                if sib.start_byte() == decl_start {
+                    decl_idx = i as isize;
+                    break;
+                }
+            }
+        }
+        if decl_idx > 0 {
+            let mut j = decl_idx - 1;
+            while j >= 0 {
+                let Some(sib) = parent.named_child(j as usize) else {
+                    j -= 1;
+                    continue;
+                };
+                if !matches!(sib.kind(), "decorator" | "annotation" | "marker_annotation") {
+                    break;
+                }
+                self.consider_decorator(sib, decorated_row);
+                j -= 1;
+            }
+        }
+    }
+
+    fn consider_decorator(&mut self, n: Node<'t>, decorated_row: u32) {
+        if !matches!(n.kind(), "decorator" | "annotation" | "marker_annotation" | "attribute") {
+            return;
+        }
+        let mut target: Option<Node> = None;
+        for i in 0..n.named_child_count() {
+            let Some(child) = n.named_child(i) else { continue };
+            if child.kind() == "call_expression" {
+                target = child.child_by_field_name("function").or_else(|| child.named_child(0));
+                if target.is_some() {
+                    break;
+                }
+            }
+            if matches!(
+                child.kind(),
+                "identifier" | "member_expression" | "scoped_identifier" | "navigation_expression"
+                    | "user_type" | "type_identifier"
+            ) {
+                target = Some(child);
+                break;
+            }
+        }
+        let Some(target) = target else { return };
+        let name = strip_generic_and_qualifier(self.text(target));
+        if name.is_empty() {
+            return;
+        }
+        self.push_ref_at(decorated_row, &name, edge_kind_index("decorates").unwrap(), n);
+    }
+
+    // --- function-as-value refs (SWIFT_SPEC, function-ref.ts:288) -------------------
+
+    fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
+        enum Mode {
+            Args,
+            Rhs,
+            List,
+            Varinit,
+        }
+        let mode = match node.kind() {
+            "value_arguments" => Mode::Args,
+            "assignment" => Mode::Rhs, // field 'result'
+            "array_literal" => Mode::List,
+            "property_declaration" => Mode::Varinit, // field 'value'
+            _ => return,
+        };
+        if self.stack.is_empty() {
+            return;
+        }
+        let from = self.top_row();
+
+        let mut values: Vec<Node> = Vec::new();
+        match mode {
+            Mode::Args | Mode::List => {
+                for i in 0..node.named_child_count() {
+                    if let Some(c) = node.named_child(i) {
+                        values.push(c);
+                    }
+                }
+            }
+            Mode::Rhs => {
+                if let Some(rhs) = node.child_by_field_name("result") {
+                    // Param-storage skip — swift's LHS field is `target`.
+                    let lhs = node
+                        .child_by_field_name("left")
+                        .or_else(|| node.child_by_field_name("lhs"))
+                        .or_else(|| node.child_by_field_name("target"))
+                        .or_else(|| {
+                            if node.named_child_count() >= 2 { node.named_child(0) } else { None }
+                        });
+                    let lhs_text = lhs.map(|l| self.text(l)).unwrap_or("");
+                    let lhs_last = util::lhs_last_name()
+                        .captures(lhs_text)
+                        .and_then(|c| c.get(1))
+                        .map(|m| m.as_str());
+                    let rhs_text = self.text(rhs).trim();
+                    if !(lhs_last.is_some() && lhs_last == Some(rhs_text)) {
+                        values.push(rhs);
+                    }
+                }
+            }
+            Mode::Varinit => {
+                // Destructuring gate: swift's name field is a `pattern` node —
+                // never in the pattern-kind set → never skipped.
+                let name_child = node
+                    .child_by_field_name("name")
+                    .or_else(|| node.child_by_field_name("pattern"));
+                let is_destructuring = name_child
+                    .map(|nc| {
+                        matches!(
+                            nc.kind(),
+                            "object_pattern" | "array_pattern" | "tuple_pattern" | "struct_pattern"
+                        )
+                    })
+                    .unwrap_or(false);
+                if !is_destructuring {
+                    if let Some(v) = node.child_by_field_name("value") {
+                        values.push(v);
+                    }
+                }
+            }
+        }
+
+        for v in values {
+            self.normalize_fn_ref_value(v, from, 0);
+        }
+    }
+
+    fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        if depth > 4 {
+            return;
+        }
+        match v.kind() {
+            "simple_identifier" => {
+                let name = self.text(v);
+                self.push_fn_ref_cand(from, name, v);
+            }
+            "value_argument" => {
+                // Layer with field 'value' + the label-forward skip (the
+                // Alamofire A/B finding): label text == value text → dropped.
+                let label = v.child_by_field_name("name");
+                let value = v.child_by_field_name("value").or_else(|| {
+                    if v.named_child_count() > 0 {
+                        v.named_child(v.named_child_count() - 1)
+                    } else {
+                        None
+                    }
+                });
+                if let (Some(l), Some(val)) = (label, value) {
+                    if self.text(l).trim() == self.text(val).trim() {
+                        return;
+                    }
+                }
+                if let Some(inner) = v.child_by_field_name("value") {
+                    self.normalize_fn_ref_value(inner, from, depth + 1);
+                }
+            }
+            "selector_expression" => {
+                // `#selector(fire)` → fire; dotted → rightmost
+                // simple_identifier (incl. the `_` quirk); else trimmed text.
+                let Some(inner) = v.named_child(0) else { return };
+                if matches!(inner.kind(), "identifier" | "simple_identifier") {
+                    let name = self.text(inner);
+                    self.push_fn_ref_cand(from, name, inner);
+                    return;
+                }
+                if let Some(last) = last_simple_identifier(v) {
+                    let name = self.text(last);
+                    self.push_fn_ref_cand(from, name, last);
+                    return;
+                }
+                let name = self.text(inner).trim().to_string();
+                self.push_fn_ref_cand(from, &name, inner);
+            }
+            _ => {}
+        }
+    }
+
+    fn push_fn_ref_cand(&mut self, from: u32, name: &str, node: Node) {
+        if name.is_empty() || is_stoplisted(name) {
+            return;
+        }
+        let p = node.start_position();
+        self.fn_ref_cands.push(Cand {
+            from,
+            name: name.to_string(),
+            line: p.row as u32 + 1,
+            column_byte: node.start_byte(),
+            row: p.row,
+        });
+    }
+
+    fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        if depth > 12 {
+            return;
+        }
+        // Halts at functionTypes (function_declaration) + the fixed list —
+        // lambda_literal IS in it (closures halt the scan).
+        if depth > 0
+            && matches!(
+                node.kind(),
+                "function_declaration" | "arrow_function" | "function_expression" | "lambda_literal"
+                    | "lambda_expression"
+            )
+        {
+            return;
+        }
+        self.maybe_capture_fn_refs(node);
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.scan_fn_ref_subtree(c, depth + 1);
+            }
+        }
+    }
+
+    fn flush_fn_ref_candidates(&mut self) {
+        let cands = std::mem::take(&mut self.fn_ref_cands);
+        if cands.is_empty() || util::is_generated_file(self.file_path) {
+            return;
+        }
+        let mut seen: HashSet<(String, String)> = HashSet::new();
+        for c in cands {
+            if !c.name.starts_with("this.")
+                && !c.name.contains("::")
+                && !self.defined_fn_names.contains(&c.name)
+                && !self.imported_names.contains(&c.name)
+            {
+                continue;
+            }
+            if !seen.insert((self.node_ids[c.from as usize].clone(), c.name.clone())) {
+                continue;
+            }
+            let column = util::col16(self.src, &self.line_starts, c.row, c.column_byte);
+            let name_ref = self.arena.put(&c.name);
+            self.tables.push_ref(&RefRow {
+                from_idx: c.from,
+                kind: FUNCTION_REF_CODE,
+                line: c.line,
+                column,
+                reference_name: name_ref,
+                candidates: NONE_STR,
+                from_id_str: NONE_STR,
+            });
+        }
+    }
+
+    // --- value references -------------------------------------------------------------
+
+    fn flush_value_refs(&mut self, root: Node<'t>) {
+        let scopes = std::mem::take(&mut self.value_scopes);
+        let mut targets = std::mem::take(&mut self.fs_values);
+        let counts = std::mem::take(&mut self.fs_value_counts);
+        if std::env::var("CODEGRAPH_VALUE_REFS").as_deref() == Ok("0") {
+            return;
+        }
+        if targets.is_empty() || scopes.is_empty() || util::is_generated_file(self.file_path) {
+            return;
+        }
+
+        // Shadow prune — TWO cases resolve for swift: property_declaration
+        // (firstSimpleIdentifier over the name/binding pattern; guard-let/
+        // if-let bindings have no property_declaration → never prune) AND the
+        // shared `assignment` case — a declared-then-assigned `let X: T`
+        // followed by `X = …` branches counts one bump per assignment (the
+        // directly_assignable_expression's simple_identifier child), pruning
+        // X exactly as the wasm arm does (caught by the swift-nio sweep).
+        let mut decl_counts: HashMap<&str, u32> = HashMap::new();
+        let mut dstack: Vec<Node> = vec![root];
+        let mut dvisited = 0usize;
+        while let Some(n) = dstack.pop() {
+            if dvisited >= MAX_VALUE_REF_NODES {
+                break;
+            }
+            dvisited += 1;
+            if n.kind() == "assignment" {
+                let left = n
+                    .child_by_field_name("left")
+                    .or_else(|| n.child_by_field_name("pattern"))
+                    .or_else(|| n.named_child(0));
+                if let Some(left) = left {
+                    if left.kind() == "identifier" {
+                        let nm = self.text(left);
+                        if targets.contains_key(nm) {
+                            *decl_counts.entry(nm).or_insert(0) += 1;
+                        }
+                    } else {
+                        for i in 0..left.named_child_count() {
+                            let Some(c) = left.named_child(i) else { continue };
+                            if matches!(c.kind(), "identifier" | "simple_identifier") {
+                                let nm = self.text(c);
+                                if targets.contains_key(nm) {
+                                    *decl_counts.entry(nm).or_insert(0) += 1;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+            if n.kind() == "property_declaration" {
+                let vd = (0..n.named_child_count())
+                    .filter_map(|i| n.named_child(i))
+                    .find(|c| c.kind() == "variable_declaration"); // kotlin shape — None for swift
+                let id = match vd {
+                    Some(vd) => (0..vd.named_child_count())
+                        .filter_map(|i| vd.named_child(i))
+                        .find(|c| c.kind() == "simple_identifier"),
+                    None => first_simple_identifier(n.child_by_field_name("name").or_else(|| {
+                        (0..n.named_child_count())
+                            .filter_map(|i| n.named_child(i))
+                            .find(|c| matches!(c.kind(), "value_binding_pattern" | "pattern"))
+                    })),
+                };
+                if let Some(id) = id {
+                    if matches!(id.kind(), "identifier" | "simple_identifier") {
+                        let nm = self.text(id);
+                        if targets.contains_key(nm) {
+                            *decl_counts.entry(nm).or_insert(0) += 1;
+                        }
+                    }
+                }
+            }
+            for i in 0..n.named_child_count() {
+                if let Some(c) = n.named_child(i) {
+                    dstack.push(c);
+                }
+            }
+        }
+        let shadowed: Vec<String> = decl_counts
+            .iter()
+            .filter(|(nm, c)| **c > counts.get(**nm).copied().unwrap_or(1))
+            .map(|(nm, _)| nm.to_string())
+            .collect();
+        for nm in shadowed {
+            targets.remove(&nm);
+        }
+        if targets.is_empty() {
+            return;
+        }
+
+        let refs_kind = edge_kind_index("references").unwrap();
+        for scope in &scopes {
+            let mut seen: HashSet<&str> = HashSet::new();
+            let mut stack: Vec<Node> = vec![scope.node];
+            let mut visited = 0usize;
+            while let Some(n) = stack.pop() {
+                if visited >= MAX_VALUE_REF_NODES {
+                    break;
+                }
+                visited += 1;
+                if matches!(n.kind(), "identifier" | "constant" | "name" | "simple_identifier") {
+                    let ref_name = self.text(n);
+                    if let Some(&target_row) = targets.get(ref_name) {
+                        let target_id = self.node_ids[target_row as usize].as_str();
+                        if target_id != self.node_ids[scope.row as usize]
+                            && ref_name != scope.name
+                            && !seen.contains(&target_id)
+                        {
+                            seen.insert(target_id);
+                            let meta = self.arena.put(r#"{"valueRef":true}"#);
+                            self.tables.push_edge(&EdgeRow {
+                                source_idx: scope.row,
+                                target_idx: target_row,
+                                kind: refs_kind,
+                                provenance: 0,
+                                line: NONE,
+                                column: NONE,
+                                metadata_json: meta,
+                                source_id_str: NONE_STR,
+                                target_id_str: NONE_STR,
+                            });
+                        }
+                    }
+                }
+                for i in 0..n.named_child_count() {
+                    if let Some(c) = n.named_child(i) {
+                        stack.push(c);
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// The shared decorator-name normalization: strip `<...` from the first `<`
+/// (index > 0), keep the segment after the last `.`/`::`, strip ONE leading
+/// `:` or `.`, trim.
+fn strip_generic_and_qualifier(raw: &str) -> String {
+    let mut name = raw.to_string();
+    if let Some(lt) = name.find('<') {
+        if lt > 0 {
+            name.truncate(lt);
+        }
+    }
+    let last_dot = name
+        .rfind('.')
+        .map(|i| i as isize)
+        .unwrap_or(-1)
+        .max(name.rfind("::").map(|i| i as isize).unwrap_or(-1));
+    if last_dot >= 0 {
+        name = name[(last_dot as usize + 1)..].to_string();
+        if name.starts_with(':') || name.starts_with('.') {
+            name.remove(0);
+        }
+    }
+    name.trim().to_string()
+}
+
+fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
+    match s {
+        Some(s) => arena.put(s),
+        None => NONE_STR,
+    }
+}

+ 1121 - 0
docs/design/kotlin-kernel-port-checklist.md

@@ -0,0 +1,1121 @@
+# Kotlin kernel port (R7b) — the bug-for-bug checklist
+
+**Status: SURVEY COMPLETE (2026-07-20) — PORT NOT STARTED.** Survey basis: every
+TS-side branch a `.kt`/`.kts` file exercises, with file:line anchors as of
+**`a6c62d7`** (HEAD at survey time, clean main). Every grammar-shape claim below
+was **probed against both the production tree-sitter-wasms build and a fresh
+fwcd 0.3.8 tag build** (probe scripts + dumps in the session scratchpad
+`svy-kotlin/` — see §Probe artifacts), and every extraction-behavior claim was
+**pinned against the real `dist/` extractor** (`extract-*.txt` ground-truth
+dumps), not derived from code reading alone. Read WITH
+`docs/design/rust-kernel-migration-plan.md` (§0a recipe, §2 boundary, §4
+tracker row "kotlin", §5 gates) and the format precedents
+(`rust-lang-kernel-port-checklist.md`, `ruby-kernel-port-checklist.md`,
+`php-kernel-port-checklist.md`, `csharp-kernel-port-checklist.md`).
+
+**Blocking findings: none — but two eyes-open items, one of them a NOVEL
+mechanism.** (1) The grammar bump is **behavior-neutral** (rust-style gate:
+byte-identical CSTs on all 1,984 gate-repo files, 0 error disagreements) —
+**but the crates.io crate `tree-sitter-kotlin = 0.3.8` is UNUSABLE by the
+kernel** (it pins `tree-sitter >= 0.21, < 0.23`; the kernel links 0.25), and
+the successor crate `tree-sitter-kotlin-ng` is a **different grammar** (8
+fields vs 0, 289 vs 357 symbols, renamed kinds — would break every kotlin.ts
+branch). The port must take the **vendored-grammar-C route**: compile the
+sha-matched 0.3.8 `parser.c`+`scanner.c` inside `codegraph-kernel` via
+build.rs — the FIRST language to exercise the mechanism the §4 tracker
+prescribes for vendored grammars (§Grammar prep). (2) **Both-arm parse-error
+incidence is 4.7–8.5%** on the gate repos (fun-interface misparses, phantom
+single-line-class-body errors, soft-keyword identifiers, `call().prop = x`
+LHS shapes — all grammar-inherent, all identical across arms). The default
+`--max-deferral 0.1` HOLDS but with only ~1.2–2× headroom (okhttp 8.45%) —
+expect double-digit deferral COUNTS on kotlin sweeps and don't misread them
+as walker bugs (§Architecture decisions #6).
+
+## Grammar prep (behavior-neutral re-vendor + the vendored-C kernel build)
+
+kotlin is NOT in `VENDORED_WASM_LANGS` (grammars.ts:291-317) — production
+loads `node_modules/tree-sitter-wasms/out/tree-sitter-kotlin.wasm`
+(mapping `kotlin: 'tree-sitter-kotlin.wasm'` at grammars.ts:35;
+tree-sitter-wasms 0.1.13 builds it from npm `tree-sitter-kotlin` **^0.3.1**,
+the fwcd lineage; production wasm sha256 `b5cb00c8…`, 4,052,705 bytes, ABI 14).
+
+- **Lineage decision (investigated, not assumed):** two crates exist.
+  - `tree-sitter-kotlin` **0.3.8** (crates.io max_stable, published
+    2024-08-03; repo fwcd/tree-sitter-kotlin — OUR wasm's lineage; repo still
+    active but no crate release since). Tag `0.3.8` (annotated tag `9e7e624` →
+    commit `e1a2d5ad1f61f5740677183cd4125bb071cd2f30`); sha256-verified
+    crate-tarball ↔ tag, BOTH generated artifacts (kotlin HAS an external
+    scanner):
+    - `src/parser.c` `54104a7ef1555c265b746c790e0f8bb953cc17806e9df0c3af82f7f62c06a70a`
+    - `src/scanner.c` `27f73337ec357fc341fa57538f34c14277b0346980c3405dc30beab6202ec6d0`
+  - `tree-sitter-kotlin-ng` 1.1.0 (tree-sitter-grammars org, 2025-01) —
+    **REJECTED**: a different grammar, not a re-publish (STATE_COUNT 11432 vs
+    10155, SYMBOL_COUNT 289 vs 357, **FIELD_COUNT 8 vs 0**, kinds renamed —
+    `additive_expression`→`binary_expression`, `call_suffix` gone,
+    `binding_pattern_kind` gone…). Adopting it is an extractor REWRITE, not a
+    port. Do not revisit until/unless the TS side migrates grammars.
+- **The 0.3.8 build is behavior-IDENTICAL to the production wasm** (this bump
+  is a reproducibility re-vendor, csharp-flavored, not a version change):
+  kind/field tables identical (360 node types, 134 named kinds, 0 fields,
+  ABI 14 both — `table-compare.cjs`); the 258-line torture file's full CST dump
+  is byte-identical OLD↔NEW; and the gate-repo sweep (`error-sweep.cjs
+  <repo> --sexp`) found **0 error disagreements and 0 s-expression mismatches
+  on every clean file across all three repos**. Expect the standalone bump
+  gate's old-vs-new full-init dump diff to be **byte-identical on all three**
+  (rust-style "expect zero", NOT php's enumerate+classify).
+- **ABI note:** the 0.3.8 tag's checked-in parser.c declares
+  `LANGUAGE_VERSION 14` — content parity, ABI stays 14 (ruby precedent).
+  kernel-grammar-parity must assert same-revision, not an ABI change.
+- **FIELD_COUNT 0 is load-bearing for the whole port**: every
+  `childForFieldName`/`getChildByField` lookup in the kotlin path returns
+  null, which is what makes several TS hooks dead code (§Extractor config).
+  The walker must reproduce the null-field world exactly — do NOT "helpfully"
+  use -ng-style fields that don't exist here.
+- **Wasm build (from the tag's CHECKED-IN parser.c — never `tree-sitter
+  generate`):**
+  ```
+  git clone --depth 1 --branch 0.3.8 https://github.com/fwcd/tree-sitter-kotlin
+  cd tree-sitter-kotlin
+  # the 0.3.8 tag predates tree-sitter.json, which cli 0.25.10 requires —
+  # add the METADATA-ONLY shim (grammar name/scope; nothing regenerated):
+  #   {"grammars":[{"name":"kotlin","scope":"source.kotlin","path":".",
+  #     "file-types":["kt","kts"]}],"metadata":{"version":"0.3.8","license":"MIT"}}
+  npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-kotlin.wasm .
+  ```
+  (brew emcc present; survey artifact sha256
+  `c80c88867a589a1a0959bcea89de84b7e9684b3693b2cdb2944812458e62ff48`,
+  4,052,313 bytes, at scratchpad `svy-kotlin/tree-sitter-kotlin-NEW.wasm`.
+  Do NOT use tree-sitter-cli 0.24 — it drops `\p{...}` classes, the #1164
+  vbnet lesson, and this grammar's identifiers use them.)
+- **Kernel side — the NOVEL part (crate pin impossible):** the 0.3.8 crate's
+  `[dependencies.tree-sitter] version = ">= 0.21, < 0.23"` + old-style
+  `pub fn language() -> tree_sitter::Language` bindings cannot link against
+  the kernel's `tree-sitter = "0.25"`. Instead of a crate dep, **vendor the
+  grammar C into the kernel** (the §4 tracker's prescription for
+  vendored-grammar languages — kotlin is the first to need it):
+  - copy the tag's `src/parser.c`, `src/scanner.c`, and `src/tree_sitter/*.h`
+    to `codegraph-kernel/grammars/kotlin/` (shas above, recorded in a comment);
+  - `codegraph-kernel/build.rs`: `cc::Build` compiling both C files with the
+    crate's own flag set (`-Wno-unused-parameter`,
+    `-Wno-unused-but-set-variable`, `-Wno-trigraphs`; msvc `-utf-8` — crib
+    the tarball's `bindings/rust/build.rs`);
+  - Cargo: add `tree-sitter-language = "0.1"` (the version-agnostic
+    `LanguageFn` shim every modern grammar crate uses) + `cc` as a
+    build-dependency (if not already present);
+  - `langs.rs`:
+    ```rust
+    extern "C" { fn tree_sitter_kotlin() -> *const (); }
+    // …
+    "kotlin" => Some(unsafe { tree_sitter_language::LanguageFn::from_raw(tree_sitter_kotlin) }.into()),
+    ```
+    plus `LANGUAGES` += `"kotlin"` (14 entries).
+  - `__tests__/kernel-grammar-parity.test.ts:39` `GRAMMAR_LANGUAGES += 'kotlin'`
+    — the id-by-id ABI/kind/field-table compare against the vendored wasm is
+    the proof the C build and the wasm build are the same revision.
+- **Staging plan (bump PR, before any walker exists):** vendor the wasm to
+  `src/extraction/wasm/tree-sitter-kotlin.wasm`; `VENDORED_WASM_LANGS +=
+  'kotlin'` (grammars.ts:291) with an R7b comment (tag + sha-matched note +
+  "crate unusable — kernel compiles vendored C, see codegraph-kernel/grammars/
+  kotlin"); the kernel C vendor + build.rs + langs.rs + grammar-parity row can
+  land WITH the bump (they're inert until a walker exists) or with the walker —
+  but wasm + C must be same-tag from day one. `copy-assets` already globs
+  `src/extraction/wasm/*.wasm`. MIT license (fwcd), same family as the rest.
+- **Error incidence (both arms, all `.kt`/`.kts` ≤1MiB, `error-sweep.cjs`):**
+
+  | Repo | files | OLD hasError | NEW hasError | disagreements | sexp mismatches (clean files) |
+  |---|---|---|---|---|---|
+  | okio | 322 | 23 (7.14%) | 23 (7.14%) | 0 | 0 |
+  | okhttp | 580 | 49 (8.45%) | 49 (8.45%) | 0 | 0 |
+  | kotlinx.coroutines | 1,082 | 51 (4.71%) | 51 (4.71%) | 0 | 0 |
+
+  Error classes (sampled + probed): **(a) `fun interface`** — unsupported by
+  the grammar, ALWAYS errors (okhttp 10/49, kotlinx 6/51; okhttp3's core
+  `Call.kt`/`Authenticator.kt`/`Dns.kt` are in this class); **(b) phantom
+  single-line class bodies** — `class X { fun f() {} }` sets `hasError=true`
+  with ZERO ERROR/missing nodes and a COMPLETE, correct CST (probed both
+  arms; okio 1, okhttp 7, kotlinx 6); **(c) soft-keyword identifiers**
+  (`var final = false; final = true` errors — `final` is reserved by the
+  grammar); **(d) `call("x").prop = value`** navigation-off-call assignment
+  LHS (errors; plain `obj.prop = x` is fine); **(e)** assorted expect-header
+  and gradle-kts DSL shapes. `class Foo private constructor(x)` and
+  `@Inject constructor` parse CLEAN (probed — don't blame ctor visibility).
+  All classes error on BOTH arms → defer-to-wasm keeps parity; only the
+  speedup is lost on those files.
+- Probe scripts + outputs live in the survey scratchpad (`…/scratchpad/
+  svy-kotlin/`): `table-compare.cjs`, `shape-probe-kotlin.cjs` +
+  `torture-{OLD,NEW}.txt` (byte-identical), `mini-probes.cjs` +
+  `mini-probes.out` (17 targeted shapes, all OLD==NEW), `error-sweep.cjs` +
+  `errors-<repo>.txt`, `extract-probe.cjs` (runs the REAL dist extractor —
+  its `extract-{torture,vref,vref-nopkg,docs,bodiless,crlf,funiface,lfpkg,kts}.txt`
+  dumps are the pinned ground truth cited throughout and double as walker
+  test expectations), `torture.kt` + the small fixtures, the 0.3.8 tag clone +
+  crate tarball + ng tarball with matching shas. Scratch is throwaway —
+  re-derive from this doc if gone.
+
+## Architecture decisions
+
+1. **No preParse.** `kotlinExtractor` has no `preParse` hook (languages/
+   kotlin.ts — whole file, no such key), so `preParsedSource`
+   (kernel/index.ts:96) is a no-op — both arms parse raw bytes. Nothing to
+   hoist. No `POST_PASSES` entry either (kernel/index.ts:81) →
+   `tryKernelExtractRaw` stays eligible.
+2. **Three framework resolvers can force the DECODED path for kotlin; none of
+   the gate repos trips any of them (verified).** parse-worker.ts:93-100
+   forces any language with an applicable framework `extract()` onto the
+   decoded `extractFromSource` path. Kotlin appears in:
+   - `springResolver` (frameworks/java.ts:13, `languages: ['java','kotlin',
+     'yaml','properties']`; extract() at :197 regexes `@GetMapping` etc. over
+     raw `.kt` source) — detect (:23) = pom.xml/build.gradle/build.gradle.kts
+     containing `spring-boot`/`springframework`, or Spring annotations in any
+     `.java` file. okio/okhttp/kotlinx.coroutines: none match (grepped).
+   - `expoModulesResolver` (frameworks/expo-modules.ts:154, `languages:
+     ['swift','kotlin']`) — detect = package.json `expo-modules-core` or an
+     Expo `Module` DSL source scan. Not present.
+   - `fabricViewResolver` (frameworks/fabric.ts:366, kotlin in languages) —
+     detect needs `codegenNativeComponent`. Not present.
+   So all three parity repos exercise the raw buffers-to-store transport;
+   a Spring-Boot Kotlin app or an Expo/RN app is the decoded-path smoke check.
+3. **The framework extractors themselves need NO port** — regex over raw
+   source, run in extractFromSource:6736-6758 after either arm. §Frameworks
+   pins their input contracts.
+4. **One walker module** (suggest `codegraph-kernel/src/kotlin.rs`; no crate
+   collision since there is no kotlin crate dep), registered in langs.rs;
+   per-file `has_error()` → `defer:` like every walker. **java.rs is the
+   closest crib** (JVM package→namespace node via `extractFilePackage`,
+   class-like scope stack, methods-in-class-like, dotted imports,
+   STATIC_MEMBER + TYPE_ANNOTATION + VALUE_REF membership, annotation
+   decorators, `node_ids` vec). Kotlin diverges from it in nine places, each
+   detailed below: (a) a `visitNode` hook whose PROPERTY branch is the only
+   live-in-kernel part (fun-interface recovery is defer-shielded); (b)
+   `getReceiverType` — extension functions → receiver-qualified method QNs +
+   the owner-contains fallback (NO ported walker has this surface yet); (c)
+   `extractModifiers` — expect/actual → node DECORATORS (also a first); (d)
+   `resolveBody` by TYPE (zero-field grammar); (e) `extraClassNodeTypes`
+   (`object_declaration`); (f) classifyClassNode keyword sniffing
+   (interface/enum reuse `class_declaration`); (g) the #750 kotlin re-encode
+   in extractCall (namedChild(0), NOT a function field); (h) a fn-ref spec
+   with EMPTY idTypes + `callable_reference`/`navigation_expression`
+   specials; (i) dead-field lookups everywhere (signatures, type
+   annotations) that must stay dead.
+5. **`.kt`/`.kts` → `kotlin`** at detectLanguage (grammars.ts:106-107), no
+   content sniffing, no dialect. `.kts` scripts are ordinary kotlin files
+   whose top-level statements attribute calls to the FILE node (pinned:
+   `extract-kts.txt` — `calls println from=file`, top-level `val` →
+   `constant`). MAX_FILE_SIZE (1 MiB, extraction/index.ts:132) and
+   generated-file skips are orchestrator/TS-side and shared.
+6. **Deferral expectations:** okio 23/322 = 7.14%, okhttp 49/580 = 8.45%,
+   kotlinx.coroutines 51/1,082 = 4.71% — grammar-inherent, both-arm (§Grammar
+   prep table). Keep the sweep default `--max-deferral 0.1` (it holds on all
+   three) but EXPECT these counts; a kotlin sweep at ~8% deferral is normal,
+   one at >10% means a walker bug. No c/cpp 0.5 exemption.
+7. **REF_FLAG_FILE_PATH (wire v2 slot) is NOT needed for kotlin.** No kotlin
+   extraction path emits refs carrying `filePath` (the visitNode hook only
+   creates nodes; verified across every ground-truth dump — zero refs printed
+   a filePath). The ruby/php trait-mixin bit stays unused here.
+
+## Extractor config (languages/kotlin.ts — 353 lines, read it whole)
+
+Types: functionTypes=[`function_declaration`]; classTypes=[`class_declaration`]
+(covers class/interface/enum via classifyClassNode); methodTypes=
+[`function_declaration`] (same list — the 994/995 gate routes in-class-like
+functions to extractMethod); interfaceTypes=[] ; structTypes=[]; enumTypes=[];
+enumMemberTypes=[`enum_entry`]; typeAliasTypes=[`type_alias`];
+importTypes=[`import_header`]; callTypes=[`call_expression`];
+variableTypes=[`property_declaration`]; fieldTypes=[`property_declaration`]
+(both lists — the hook consumes nearly all of them first);
+extraClassNodeTypes=[`object_declaration`]. nameField=`simple_identifier`,
+bodyField=`function_body`, paramsField=`function_value_parameters`,
+returnField=`type`.
+
+**FIELD_COUNT 0 consequences (the dead-field cluster — reproduce the deadness):**
+
+- `nameField`/`bodyField`/`paramsField`/`returnField` are node-TYPE names used
+  as FIELD names — every `getChildByField` on them returns null. Names resolve
+  via extractName's FALLBACK (first direct namedChild of type
+  `identifier`|`type_identifier`|`simple_identifier`|`constant`,
+  tree-sitter.ts:178-189); bodies resolve via the `resolveBody` hook (by
+  type); params/return field walks are DEAD (§Type-annotation refs).
+- **getSignature (kotlin.ts:277) is DEAD CODE — always undefined.** It reads
+  `getChildByField(node, 'function_value_parameters')` → null → early return.
+  Ground truth: every function/method in `extract-torture.txt` has
+  `sig=undefined`. The walker must emit NO signature for functions/methods.
+- **The property hook's `typeNode = node.childForFieldName('type')`
+  (kotlin.ts:125) is DEAD** → property `signature` is always undefined too
+  (`val topVal: Int = 3` → sig undefined — pinned).
+
+Hooks PRESENT (port each exactly):
+
+- **visitNode (kotlin.ts:87-215)** — runs for EVERY node the main walker
+  visits (tree-sitter.ts:943-953; NOT in visitFunctionBody). Three branches:
+  1. **`property_declaration` (:98-131) — the LIVE branch.** varDecl = first
+     namedChild of type `variable_declaration`; nameNode = ITS first
+     `simple_identifier`; either missing (destructuring's
+     `multi_variable_declaration`) → return false (fall to the ladder). Then
+     the SCOPE WALK up the parent chain, first match wins:
+     `function_body|function_declaration|lambda_literal|
+     anonymous_initializer|control_structure_body|getter|setter` → 'local' →
+     **return true, extract nothing** (this is how init-block/getter-body/
+     top-level-control-flow locals reached via visitNode recursion are
+     skipped); `companion_object|object_declaration` → 'const';
+     `class_declaration` → 'instance'; nothing matches (top level) → 'const'
+     (the initial value). Kind: instance → **`field`**; else `val` (a
+     `binding_pattern_kind` child with text exactly `val`) → **`constant`**,
+     `var` → **`variable`** (`const val` is just a val; a delegated
+     `by lazy {}` property has no `=` but still a binding → same rule).
+     `ctx.createNode(kind, name, node, { signature: undefined })` — extra
+     carries ONLY the (always-undefined) signature: **no docstring, no
+     visibility, no isStatic, no returnType on kotlin property nodes** —
+     but createNode's extractModifiers merge still runs, so `expect val` /
+     `actual val` DO get decorators. Return true → the dispatcher runs
+     `scanFnRefSubtree(node, 0)` (capture-only, halts at nested
+     function/lambda types) and NEVER descends → **property initializers
+     emit NO calls/instantiates refs anywhere** (`val SHARED = WidgetK(0)`
+     → nothing; `by lazy { compute() }` → nothing, the scan halts at the
+     lambda_literal). Consequences pinned in `extract-torture.txt`.
+  2. **`lambda_literal` after a fun-interface ERROR (:139-143)** and
+  3. **fun-interface misparse recovery (:145-214)** (ERROR/
+     function_declaration shapes; `isFunInterfaceNode` :46; Pattern 1 walks
+     the sibling lambda's `statements` with a synthesized `interface` node
+     pushed — ground truth `extract-funiface.txt`: interface node at the
+     ERROR's extent + `transform` as its method) — **both branches are
+     DEFER-SHIELDED in the kernel**: every `fun interface` (either pattern,
+     probed) makes the tree `hasError=true`, so the kernel defers the whole
+     FILE to wasm before the walker would run. **Do NOT port branches 2-3.**
+     Walker rule: port branch 1 only; a `defer:` on has_error covers the
+     rest. (The parity suite still needs a fun-interface fixture asserting
+     the kernel defers and the wasm arm serves the pinned output.)
+- **resolveBody (kotlin.ts:219-241)** — find by TYPE among namedChildren:
+  first `ERROR` child whose child(0) is `{` (the fun-interface parent-body
+  case — unreachable on non-erroring files, keep for wasm-parity of the TS
+  side only), else first `function_body` | `class_body` | `enum_class_body`.
+  Used by extractFunction/Method/Class/Enum body resolution AND by
+  createNode's endLine extension (tree-sitter.ts:1329-1333, function/method
+  kinds only). Single-expression bodies (`fun f() = expr`) are a
+  `function_body` starting with `=` — resolved and walked like any body.
+- **classifyClassNode (kotlin.ts:242-255)** — scan ALL children (anon
+  included): child.type `interface` → 'interface'; `enum` → 'enum'; else
+  'class'. `annotation class` / `data class` / `sealed class` → 'class'
+  (their `class_modifier` children don't match); `sealed interface` →
+  'interface'.
+- **getReceiverType (kotlin.ts:256-276) — LIVE, the extension-function
+  surface.** Walk ALL children in order: remember the last `user_type`; on a
+  `.` (anon) child WITH a remembered user_type → return that user_type's
+  FIRST `type_identifier` child's text (else the whole user_type text); on
+  `simple_identifier` or `function_value_parameters` → break (past the name;
+  no receiver). Probed shapes:
+  - `fun WidgetK.extend()` → `WidgetK`.
+  - `fun <T> List<T>.genericExt()` → `List` (type_parameters is skipped;
+    generic args live in a `type_arguments` child of the user_type, and the
+    FIRST type_identifier is the base).
+  - **`fun com.example.Qualified.qext()` → `com`** — a qualified receiver's
+    user_type holds MULTIPLE type_identifiers (`com`,`example`,`Qualified`)
+    and the find takes the FIRST segment. QN becomes `com::qext`. BUG,
+    PRESERVE.
+  - `infix fun Int.pow()` → `Int`; `operator fun WidgetK.plus()` → `WidgetK`
+    (modifiers don't disturb the walk).
+- **getVisibility (kotlin.ts:288-301)** — for each child of type `modifiers`:
+  TEXT `.includes('public'|'private'|'protected'|'internal')` in that order;
+  no modifiers/no match → **'public'**. QUIRKS, PRESERVE: (a) kotlin emits a
+  visibility value no other language does — **'internal'**; (b) the probe
+  file's `private internal fun` (invalid kotlin, parses fine) → 'private'
+  (order); (c) TEXT-includes false positives — annotations live inside
+  `modifiers`, so `@publicize`-style lowercase annotation text containing a
+  keyword flips visibility (e.g. a lone `@internalApi` annotation → 'internal'
+  instead of 'public'). Match the includes-on-raw-text semantics exactly.
+- **isStatic (kotlin.ts:302-305)** — always **false** (not undefined): every
+  function/method node carries `isStatic: false`.
+- **isAsync (kotlin.ts:306-315)** — any `modifiers` child whose TEXT
+  `.includes('suspend')` → true, else false. The real shape is
+  `modifiers > function_modifier > suspend`. TEXT-includes false positive,
+  PRESERVE (probed, `mini-probes.out` suspendFalsePos): `@suspendMarker fun
+  g()` → **isAsync true** (the annotation text contains lowercase 'suspend').
+- **extractModifiers (kotlin.ts:316-338) — expect/actual, the KMP surface.**
+  Scan children for `modifiers` → their `platform_modifier` children → their
+  children of NODE TYPE `expect`/`actual` (anon keyword nodes; matched by
+  type, not text) → collect in order; empty → undefined. Runs inside
+  createNode (tree-sitter.ts:1355-1358) for **EVERY node kind** — merged
+  `newNode.decorators = [...(existing ?? []), ...mods]`. Ground truth:
+  `expect fun`/`expect class` → dec=["expect"]; `actual fun/class/val` →
+  ["actual"]; **`actual typealias PlatformClock` → type_alias node with
+  dec=["actual"]** (the synthesizer's KMP_TYPE_KINDS depends on this);
+  members of an `expect class` are NOT marked (no platform_modifier of their
+  own) but an `actual fun` inside an `actual class` IS. `decorators` on
+  kotlin nodes come ONLY from this hook — the annotation channel is
+  `decorates` REFS, never the node list (§Decorators).
+- **extractImport (kotlin.ts:339-346)** — signature = trimmed
+  `source.substring(node.startIndex, node.endIndex)` (UTF-16); moduleName =
+  the first namedChild of type `identifier`'s substring (the dotted path).
+  No identifier → null (doesn't occur; even `import a.b.*` has the
+  identifier). No handledRefs → the generic imports ref also fires
+  (§Imports).
+- **packageTypes=[`package_header`] + extractPackage (kotlin.ts:347-352)** —
+  first namedChild of type `identifier` → trimmed substring
+  (`com.example.torture`); none → null. §Namespace capture.
+
+Hooks ABSENT (the walker must NOT do these): `preParse`, `resolveName`,
+`recoverMangledName`, `isMisparsedFunction`, `isConst`, `isExported`
+(**undefined on every node except the file node's literal `false`**),
+`classifyMethodNode`, `extractPropertyName`, `propertyTypes`,
+`interfaceKind` (→ kind `interface`), `extractBareCall`, `synthesizeMembers`,
+`skipBodilessClass` (**a bodiless `class Foo` still mints a node** — the
+1685 comment names Kotlin as the deliberate case), `methodsAreTopLevel`,
+`resolveTypeAliasKind`.
+
+## tree-sitter.ts branches (anchors as of `a6c62d7`)
+
+### visitNode dispatch — what each kotlin node hits (ladder at 936-1303)
+
+| Node | Branch | Behavior |
+|---|---|---|
+| every node | visitNode hook first (943) | property_declarations (non-destructuring) consumed there; handled → `scanFnRefSubtree` + STOP |
+| every node | maybeCaptureFnRefs (990) | fires for `value_arguments`/`assignment` (the KOTLIN_SPEC keys) in visitNode context too — how top-level/class-scope callable refs in call args are captured |
+| `function_declaration` | functionTypes:994 | inside class-like AND ∈ methodTypes → extractMethod:1737; else extractFunction:1517 (which itself diverts to extractMethod when getReceiverType fires — extension fns at any scope). skipChildren |
+| `class_declaration` | classTypes:1005 → classify | 'interface' → extractInterface:1834; 'enum' → extractEnum:1914; else extractClass:1679 |
+| `object_declaration` | extraClassNodeTypes:1022 | extractClass(node) → kind **`class`** (objects and sealed-class `object` members are class nodes; extractInheritance runs → their delegation_specifiers emit extends) |
+| `companion_object` | **no branch** | recursed → its class_body children visited with the OUTER CLASS still on top: properties → hook ('const' scope → constant/variable under the class, class: parent ⇒ value-ref targets), functions → extractMethod of the outer class. A NAMED companion (`companion object Named`) is identical — the name mints nothing |
+| `property_declaration` (hook-declined = destructuring) | fieldTypes:1084 (in class-like) else variableTypes:1098 | extractField / extractVariable — both emit **NOTHING** for kotlin destructuring (no `variable_declarator`/`variable_declaration`/`identifier` direct children; extractVariable's generic fallback :2863-2881 finds no `identifier`-typed child — kotlin names are `simple_identifier`). skipChildren + scanFnRefSubtree → the RHS call is invisible too. `isClassScopeConstantAssignment` (1508) needs node.type `assignment` → always false for kotlin |
+| `type_alias` | typeAliasTypes:1071 → extractTypeAlias:2890 | plain `type_alias` node (no resolveTypeAliasKind). QUIRK: the alias-value ref walk reads `getChildByField(node,'value')` → null (no fields) → **NO reference to the aliased type**; returns false → the alias's children ARE re-visited (harmless — user_type/modifiers match nothing) |
+| `import_header` | importTypes:1209 → extractImport:3170 | §Imports (the `import_list` wrapper has no branch and recurses into each header) |
+| `package_header` | consumed by extractFilePackage BEFORE the walk (1397) | during the walk it's recursed, nothing matches |
+| `call_expression` (top level / class body / object body / .kts statements) | callTypes:1248 → extractCall:3684 | attributes to the nodeStack top (file/namespace/class). Note class-BODY calls only occur via init blocks etc. (below) |
+| `anonymous_initializer` (`init { }`) | no branch | recursed → its statements' calls → **`calls` refs FROM THE CLASS node**; its `val` locals → hook 'local' → nothing (pinned: `calls "register" from=class:WidgetK`) |
+| `secondary_constructor` | no branch | **NO constructor node**; recursed → body calls attribute to the CLASS (`calls "log" from=class:WidgetK`); the `constructor_delegation_call`'s value_arguments still feed fn-ref capture |
+| `getter`/`setter` as SIBLINGS (accessor on its own line) | no branch | recursed → accessor-body calls attribute to the CLASS (or file). See §Properties for the sibling/child split |
+| `object_literal` (`object : T { … }` initializer) | no branch anywhere | never a node; see §Body walker for the method-leak quirk |
+| `file_annotation` (`@file:JvmName("x")`) | no branch | recursed; its value_arguments feed fn-ref capture (string args → nothing). No decorates ref |
+| INSTANTIATION_KINDS (354-361) | **no kotlin member** | extractInstantiation:4610 is **UNREACHABLE** for kotlin — constructor calls `Foo()` are call_expressions → plain `calls` refs named `Foo` (capitalized). Kotlin emits **zero `instantiates` refs**, ever |
+| `impl_item`:1274 / property_signature:1282 / export_statement / swift property:1121 | never | not kotlin node kinds (the swift `property_declaration` branch at 1121-1193 is gated `language === 'swift'` — kotlin property_declarations never enter it) |
+
+### Node creation, IDs, qualified names
+
+- createNode (1308): id = `generateNodeId(filePath, kind, name, startRow+1)`
+  = `` `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}` ``
+  (tree-sitter-helpers.ts:18-30). FILE node id = literal `file:${filePath}`
+  (509), name = basename, qualifiedName = filePath, endLine =
+  `source.split('\n').length`, isExported false. Dedupe/self-checks compare
+  ID STRINGS (`node_ids` vec pattern).
+- endLine extension via resolveBody (1329) is LIVE for kotlin
+  function/method nodes (body found by type; in-range for this grammar, so
+  in practice a no-op extension — but CALL the hook, the ERROR-body branch
+  is part of the contract).
+- contains edge from nodeStack top for every created node (1363);
+  extractModifiers merge (1355-1358); captureValueRefScope (1374).
+- **Namespace capture** — extractFilePackage (1397): scan the ROOT's direct
+  namedChildren for the first `package_header` (a leading `file_annotation`
+  or KDoc is skipped by the type filter); extractPackage → dotted text →
+  `createNode('namespace', 'com.example.torture', pkgNode)` = **node #2
+  after the file node**, pushed for the WHOLE walk. Every top-level symbol's
+  qualifiedName = `com.example.torture::Name` (buildQualifiedName:1447 joins
+  stack names with `::`; namespacePrefix always empty outside C/C++). No
+  package header (scripts) → no namespace node, bare QNs, file: parents.
+- **Receiver-QN override**: extension methods get `extraProps.qualifiedName =
+  composeReceiverQualifiedName(receiverType, name)` (1790-1792) =
+  `` `${receiverType}::${name}` `` verbatim (1435-1436; prefix empty → pass
+  through) — **NO package prefix**: `fun WidgetK.extend` in package
+  com.example.torture has QN `WidgetK::extend` (pinned). This is the first
+  ported walker with the receiver-QN surface — get the two QN builders'
+  divergence exactly right.
+- isInsideClassLikeNode (1486): stack-top node kind ∈ {class, struct,
+  interface, trait, enum, module} — **`namespace` does NOT count** (top-level
+  fns under the package namespace stay functions).
+
+### extractFunction / extractMethod (1517 / 1737)
+
+- extractFunction: **line 1522 — getReceiverType short-circuit is LIVE**: any
+  function_declaration with a receiver (top-level extension fns, and nested
+  ones inside bodies) diverts to extractMethod. Name via extractName fallback
+  (first simple_identifier — backtick names keep their backticks:
+  `` function "`weird name`" ``). `<anonymous>` unreachable (grammar requires
+  the name). Extras: docstring (§Docstrings), signature undefined (dead
+  hook), visibility (hook), isExported undefined, isAsync (hook), isStatic
+  false, returnType (hook — §below). extractTypeAnnotations → **emits
+  NOTHING** (§Type-annotation refs); extractDecoratorsFor → §Decorators.
+  Push, body via resolveBody, visitFunctionBody, pop.
+- extractMethod (in-class-like functions + receiver-diverted extension fns):
+  receiverType recomputed (1742); gate 1747 passes via class-like OR
+  receiver. Same extras. **Receiver path (extension fns): QN override
+  (1790) + the owner-contains fallback (1799-1813)** — receiver present AND
+  not class-like → find the FIRST node in `this.nodes` with `name ===
+  receiverType && filePath === this.filePath && kind ∈ {struct, class, enum,
+  trait}` → contains edge owner→method. QUIRKS, PRESERVE: **`interface` is
+  NOT in the kind set** — `fun Drawable.ext()` never gets an owner edge even
+  with Drawable in-file; source-order dependent (extension above its class →
+  no edge); the qualified-receiver bug (`com::qext`) looks up a node named
+  `com` (never found). Extension fns keep their normal contains edge from
+  the nodeStack top (namespace/file) REGARDLESS — the owner edge is
+  additive.
+- **`expect fun` has no body** → resolveBody null → no body walk; node still
+  minted with dec=["expect"]. Interface bodiless methods likewise.
+- Nested named `fun` inside a body → visitFunctionBody:5245 →
+  extractFunction → a `function` node contained by the enclosing
+  function/method (QN `…::caller::localFn`), receiver check applies (a
+  nested extension fn becomes a method with owner-contains).
+
+### getReturnType = extractKotlinReturnType (kotlin.ts:17-43)
+
+Positional (no fields): iterate namedChildren; before
+`function_value_parameters` → skip; after it, the FIRST `user_type` |
+`nullable_type` wins; hitting `function_body` or `type_constraints` first →
+undefined. nullable_type unwraps to its inner user_type (`?? child`). Name =
+the user_type's first `type_identifier`'s text (`?? the whole user_type`),
+trimmed; must match `/^[A-Za-z_]\w*$/`; `Unit`/`Nothing`
+(KOTLIN_NON_CLASS_RETURN kotlin.ts:6) → undefined. Pinned: `: WidgetK` →
+WidgetK; `: WidgetK?` → WidgetK; `: Unit` → undefined; inferred `= expr` →
+undefined; `: (Int) -> Unit` (function_type) → undefined; **`: T` (generic
+param) → `T`** (leaks as a returnType — preserve); extension receiver types
+never mistaken (they sit BEFORE the params). Methods and functions both.
+
+### extractClass (1679) — and the bodiless-header asymmetry
+
+resolvedBody = resolveBody (class_body by type; null for bodiless). NO
+skipBodilessClass → bodiless classes mint nodes. Extras: docstring,
+visibility (hook), isExported undefined; decorators via createNode's
+extractModifiers (expect/actual classes). Then extractInheritance (§below) —
+**BEFORE the body walk**, so extends refs precede member emissions. Then
+extractCsharpPrimaryCtorParamRefs (no-op — needs language csharp… actually
+gated at :5939 by language, cheap early-out) and extractDecoratorsFor
+(§Decorators — the `@MyMarker class` ref). Push, walk, pop.
+
+- **Bodied class: body = class_body → ONLY class_body children are visited.**
+  The `primary_constructor` is a DIRECT child of class_declaration, NOT of
+  class_body → **constructor properties (`class Foo(val a: Int)`) mint NO
+  field nodes, ctor default-value calls emit NOTHING, and `data class`
+  components are invisible** (pinned: DataK has zero members).
+- **Bodiless class: body = the class node itself (1714) → the HEADER children
+  are visited**: the primary_constructor recursion reaches default-value
+  `call_expression`s → **`calls` refs from the CLASS node**, and
+  delegation_specifier recursion reaches super-ctor argument calls too.
+  Ground truth (`extract-bodiless.txt`): `class Bodiless(val b: Int =
+  initB()) : Base(readCfg2())` → extends `Base` + calls `initB` + calls
+  `readCfg2`, all from class:Bodiless; the IDENTICAL bodied class emits
+  ONLY extends `Base`. Reproduce the asymmetry exactly; also note the
+  header's value_arguments feed fn-ref capture in the bodiless case only.
+- Class-body members: hook properties (fields/constants), function_
+  declarations → extractMethod, nested class/object/enum → their branches,
+  `getter`/`setter` siblings + `anonymous_initializer` + `secondary_
+  constructor` → plain recursion (calls attribute to the class).
+- extractInterface (1834): kind `interface`; docstring, isExported undefined —
+  **NO visibility** (extractInterface never calls getVisibility; pinned
+  vis=undefined). extractInheritance runs; body walk with the interface
+  pushed (bodiless member funs still mint method nodes; a `val prop` with
+  same-line getter → hook → `field`). Bodiless interface (`sealed interface
+  SealedIface`) → body ?? node fallback (1856) → header children re-visited
+  (nothing emits — but keep the traversal).
+- extractEnum (1914): body REQUIRED (resolveBody finds `enum_class_body`) —
+  a bodiless enum would mint nothing (doesn't occur). docstring, visibility,
+  isExported undefined. extractInheritance (enum delegation_specifiers).
+  Body loop (1941-1950): `enum_entry` ∈ enumMemberTypes →
+  extractEnumMembers(entry); everything else (function_declaration after the
+  `;`, companion_object, secondary constructors) → visitNode with the enum
+  pushed → methods of the enum, companion constants under it.
+- **extractEnumMembers (1958)**: `getChildByField(node,'name')` → null (no
+  fields) → the identifier-children scan (1967-1974): one `enum_member` node
+  per direct `simple_identifier` child, **positioned at the IDENTIFIER node**
+  (`createNode('enum_member', text, child)`) — one per entry in practice.
+  QUIRKS, PRESERVE: an entry's `value_arguments` (`OK(200)`) and **an entry's
+  `class_body` (`OK(200) { override fun label() … }`) are NEVER visited — the
+  override methods and any calls inside them are COMPLETELY INVISIBLE**
+  (pinned: Http has enum_members OK/ERR + methods label/common/of only).
+
+### Properties — the hook rules + the getter-position split (probed)
+
+- Same-line accessor (`val a: Int get() = compute()`) → the `getter` is a
+  CHILD of property_declaration → the hook consumes everything → **the
+  getter body is never walked** (no calls refs).
+- Next-line accessor (`val b: Int\n    get() = compute()`) → the `getter` is
+  a **SIBLING** (child of class_body / source_file) → after the hook handles
+  the property, the walker visits the getter separately → its body's calls
+  attribute to the CLASS (or file/namespace at top level). Pin BOTH variants.
+- Top-level properties: scope 'const' → `val`→constant / `var`→variable,
+  contained by the namespace (or file). Class body → `field`. companion/
+  object body → constant/variable under the CLASS node (stack top). Interface
+  body → `field` (class_declaration parent matches 'instance').
+- Body-context locals NEVER reach the hook (visitFunctionBody doesn't run
+  it) — instead they're plain-recursed: **a local `val fn = { … }` lambda
+  initializer IS walked, so its inner calls attribute to the enclosing
+  function** (pinned: `println` from caller), unlike hook-consumed
+  properties. Local `val x: T = …` type annotations emit nothing
+  (§Type-annotation refs).
+- `lateinit var svc: Service` in a class → field `svc` (modifiers don't
+  matter to the hook).
+
+### Imports (3170-3236) — and the comment-gluing trap
+
+Hook returns {moduleName: dotted path, signature: trimmed full text}: import
+node (name = `com.example.other.OtherClass`; QN = namespace-prefixed) + the
+generic `imports` ref (3183-3194): {fromNodeId: nodeStack top — **the
+namespace node when a package exists, else the file node**, referenceName:
+the dotted path, line: import_header startRow+1, column: startColumn}. NO
+kotlin-specific emit pass (the 3197-3234 rust/php/ruby/python emitters are
+all gated off). Shapes (probed):
+
+- `import a.b.C` → identifier text `a.b.C`.
+- **wildcard `import com.example.wild.*`** → the `identifier` covers only
+  `com.example.wild` (the `.*` is a sibling `wildcard_import`) → a normal
+  import node/ref named `com.example.wild` (NOT null, unlike rust's
+  wildcard).
+- **alias `import a.b.LongName as Short`** → identifier = `a.b.LongName`
+  (the `import_alias` child is ignored) — the SOURCE path, the alias binds
+  nothing.
+- **Comment-gluing (grammar quirk, LF and CRLF alike): comments FOLLOWING an
+  import (or the package header) attach INSIDE the import_header /
+  package_header node** → the node's extent extends over them and the
+  hook's `signature` (trimmed full text) INCLUDES the comment lines
+  verbatim (pinned: sig `"import com.example.alias.LongName as Short\n\n//
+  line comment run 1\n// line comment run 2"`, node L7-10); the namespace
+  node similarly spans to the last glued comment (`extract-lfpkg.txt`:
+  namespace L1-4). Ref line/column stay at the header START. Downstream:
+  those comments are NOT siblings of the next declaration → its docstring is
+  LOST (§Docstrings).
+- flushFnRefCandidates' QUALIFIED_IMPORT (665) matches dotted paths → kotlin
+  imports contribute their LAST segment to importedNames (`OtherClass`,
+  `helper`, `wild`, `LongName`) — the fn-ref gate is "defined in this file ∪
+  imported simple names" (unlike rust/ruby).
+
+### extractCall (3684) — the kotlin paths
+
+Entry: not vbnet/erlang/ruby/arkts. `func = getChildByField(node,'function')
+?? node.namedChild(0)` (4313) → always namedChild(0) for kotlin (no fields).
+The cpp operator recovery (4324) is language-gated off.
+
+**Member branch (4364)** — func.type === `navigation_expression`:
+
+1. property (4369-4378): field lookups null → `child1 = func.namedChild(1)`;
+   `navigation_suffix` → its first `simple_identifier` (?? the suffix itself
+   — unreachable in valid code: suffixes carry the identifier; `::class`
+   suffixes never appear under a call's function position). methodName =
+   its text. Safe-call suffixes (`?.`) carry the same simple_identifier —
+   **`x?.render()` emits exactly like `x.render()`**.
+2. receiver = object/operand/argument fields (null) ?? `func.namedChild(0)`
+   (4385-4389).
+3. **LITERAL_RECEIVER_TYPES (4397, set at 373-388)**: kotlin members that
+   occur: `string_literal` (`"literal".uppercase()`), `integer_literal`
+   (`5.toString()`) → **emit NOTHING** (pinned). Kotlin's other literal kinds
+   (`boolean_literal`, `character_literal`, `null`) also appear in the set —
+   port the WHOLE set verbatim.
+4. receiver `simple_identifier` (4401; kotlin's name kind IS in the check
+   list) not in SKIP_RECEIVERS {self,this,cls,super} (by TEXT — kotlin
+   receivers are never those texts as simple_identifiers) →
+   `` `${recv}.${method}` `` (`w.render`, `Registry.register`,
+   `Short.static`, `instances.add`, `it.render`, `anon.draw`).
+5. receiver `this_expression`/`super_expression` → none of the branches →
+   fall to the ELSE → **bare methodName** (`this.toString()` → `toString`;
+   `super.hashCode()` → `hashCode`). Same net effect as SKIP, different
+   path.
+6. **receiver `call_expression` + kotlin in the gate list (4408-4418) — the
+   #750 re-encode (4429-4442):** `innerNav = receiver.namedChild(0)` (NOT a
+   function field!) → its text with `/\s+/g` stripped; **re-encode ONLY when
+   `/^[A-Z]/`** → `` `${innerCallee}().${methodName}` ``. Pinned:
+   `WidgetK.create().render()` → `WidgetK.create().render` (+ the inner
+   `WidgetK.create` from recursion); `Foo.getInstance().bar()` →
+   `Foo.getInstance().bar` + `Foo.getInstance`; `Foo().bar()` would →
+   `Foo().bar`; lowercase chains fall to bare: `lowerFactory().chain()` →
+   `chain` + `lowerFactory`; `w.chainInner().render()` → `render` +
+   `w.chainInner`; `listOf(1).forEach {…}` → `forEach` + `listOf`.
+7. receiver `navigation_expression` (2-hop `a.b.method()`), `postfix_
+   expression` (`x!!.draw()`), parenthesized, etc. → bare methodName.
+
+**Else branch (4518-4520)** — calleeName = RAW func text: bare
+`helper`/`run`/`WidgetK` (constructor calls are plain capitalized `calls`
+refs — kotlin emits NO instantiates, §dispatch table); backticked
+`` `weird name` `` verbatim; generic calls `generic<Int>(1)` → `generic`
+(type_arguments live in the call_suffix, not the callee). QUIRKS, PRESERVE
+(all pinned in `extract-torture.txt`):
+
+- **Paren-then-lambda `trailing() { it * 3 }`** parses as
+  call(call(trailing,()), annotated_lambda) → TWO refs: the outer's callee is
+  the inner call's RAW TEXT **`trailing()`** (garbage, unresolvable) + the
+  inner `trailing`. A no-paren trailing call (`trailing { … }`,
+  `run { … }`) is ONE call → one clean ref.
+- **Newline-glued invoke chains**: `fn(3)` followed by a line starting `(`
+  continues the expression (kotlin grammar) → `fn(3)\n(fn)(4)` is one
+  3-deep call chain emitting callees `fn(3)\n    (fn)` (raw text with
+  embedded newline), `fn(3)`, and `fn`. Deterministic garbage — reproduce
+  byte-for-byte.
+- The parenthesized-conversion regex (4530,
+  `/^\(\s*\*?\s*([A-Za-z_][\w.]*)\s*\)$/`) applies to kotlin calleeNames —
+  a clean single-line `(handler)(4)` (as the FIRST statement of a body,
+  un-glued) has func text `(handler)` → rewritten to `handler`. Port the
+  regex.
+- Template strip (4542) + cpp fn-ptr fan-out (4556) are c/cpp-gated — no.
+- Final ref: {callerId = stack top, name, line = call startRow+1, column =
+  call startColumn (UTF-16)}. Inner calls of every chain are ALSO visited
+  (the body walker recurses after extractCall — no consumption).
+- Calls inside string-template interpolations EMIT (in bodies):
+  `"… ${w.render()} …"` → calls `w.render` at the inner call's position
+  (interpolated_expression recursion). `$topVal` (interpolated_identifier)
+  emits nothing anywhere.
+
+### Static-member / value-read refs (4750-4808) — kotlin IS in STATIC_MEMBER_LANGS (345-347)
+
+Called ONLY from the body walker (5218) — top-level/class-scope reads emit
+nothing (hook-consumed property initializers doubly so).
+`navigation_expression` ∈ MEMBER_ACCESS_TYPES (326). Mechanics:
+
+- callee-of-call skip (4772-4778): parent ∈ callTypes AND parent.namedChild(0)
+  starts at this node → skip (`Registry.register(w)`'s nav emits no
+  references ref).
+- recv = object/expression/scope fields (null) ?? namedChild(0); accepted
+  types include `simple_identifier` (4792); text must match
+  `/^[A-Z][A-Za-z0-9_]*$/` → `references` ref **at the RECEIVER's position**
+  (pushStaticMemberRef 4800).
+- Pinned: `Registry.count` (statement) → references `Registry`; `Color.RED`
+  → references `Color`; **`com.example.Fq.CONST_READ` → NOTHING** (nested
+  navs — the outer recv is a navigation_expression, not accepted; the
+  innermost recv `com` is lowercase); `listOf(1).size` → nothing (recv is a
+  call). **Assignment WRITES emit nothing**: `Registry.count = 5` /
+  `+= 1` parse as `assignment > directly_assignable_expression` — that node
+  type is NOT in MEMBER_ACCESS_TYPES (pinned: assignRefs() emits zero refs).
+- A `Foo.Bar` nav nested inside a bigger expression is visited on its own
+  as the walker recurses — each nav node is evaluated once.
+
+### Decorators — kotlin annotations DO emit `decorates` (unlike csharp/php), asymmetrically
+
+extractDecoratorsFor (4897) runs for functions/methods/classes (NOT
+hook-created properties/fields/constants — the hook never calls it; pinned:
+`@field:JvmField val fielded` → NO ref). Kotlin annotations live at
+`modifiers > annotation > …` — scan #1 (4976-4987) descends into `modifiers`
+children (the comment at 4979 names Kotlin) and consider() accepts node type
+`annotation` (4928):
+
+- **`@JvmStatic` / `@MyMarker` (no args)** → annotation > `user_type` —
+  user_type IS in the target list (4950) → name = its text (`<`-strip +
+  last-`.`/`::`-segment normalization at 4959-4962 apply) → **`decorates`
+  ref** {from: the decorated node, name, line/col of the ANNOTATION node}.
+  Pinned: decorates `MyMarker` from class Annotated, `JvmStatic` from method
+  jvmStatic.
+- **`@Deprecated("gone", ReplaceWith("new"))` (with args)** → annotation >
+  `constructor_invocation` — NOT `call_expression`, NOT in the identifier
+  list → **NO ref at all** (and the argument expressions are never visited
+  by anything — no calls refs either). Pinned: method `old` has zero
+  decorator refs.
+- **Use-site targets `@field:JvmField`** on a FUNCTION/CLASS would emit (the
+  `use_site_target` child is skipped, the user_type matches) — but on
+  properties (their usual home) the hook path never runs the extractor, so
+  in practice they're silent.
+- `platform_modifier` children of modifiers (expect/actual) → not accepted
+  types → no decorates refs (they ride node.decorators instead).
+- The backward-sibling scan (5013-5022) is inert — kotlin annotations are
+  inside the declaration's `modifiers`, never preceding siblings
+  (file_annotation has no branch that reaches consider()).
+
+### Inheritance — delegation_specifier (5595-5615)
+
+extractInheritance's child loop runs over the CLASS NODE's direct
+namedChildren — kotlin's `delegation_specifier`s are direct children
+(probed; no wrapper node). Per specifier:
+
+- userType = find direct `user_type`; ctorInv = find direct
+  `constructor_invocation`; target = userType ?? ctorInv; none → skip.
+- typeId: user_type → its FIRST `type_identifier` (?? itself);
+  constructor_invocation → its user_type's FIRST type_identifier (?? the
+  user_type ?? the invocation).
+- ONE **`extends`** ref per specifier {name: typeId text, line/col: the
+  typeId node}. Kotlin NEVER emits `implements` — interfaces ride extends
+  too (pinned: SubK → extends OpenBase + Drawable + Comparable).
+- QUIRKS, PRESERVE: **qualified supertype `: com.example.deep.RemoteBase()`
+  → ref named `com`** (first type_identifier of the multi-segment
+  user_type — pinned); generic supertype `Comparable<SubK>` → `Comparable`
+  (type_arguments' identifiers aren't direct children); **`by`-delegation
+  (`: Drawable by d`) emits NOTHING** — the specifier's only child is
+  `explicit_delegation` and the direct-child finds miss (pinned:
+  DelegatedImpl has zero extends).
+- Runs for classes, objects (extraClassNodeTypes → extractClass), interfaces,
+  enums. `object Add : SealedOp()` → extends SealedOp ✓. Anonymous
+  `object_literal`s never reach it (no class node).
+
+### Type-annotation references — kotlin ∈ TYPE_ANNOTATION_LANGUAGES (5753) but the machinery is DEAD
+
+extractTypeAnnotations (5788) for kotlin takes the GENERIC path: params =
+`getChildByField(node, 'function_value_parameters')` (5844) → **null** (zero
+fields); returnType = `getChildByField(node, 'type')` (5851) → **null**; the
+`type_annotation` direct-child search (5873) → no such node kind in this
+grammar. extractVariableTypeAnnotation (6074) needs a `type_annotation`
+child → dead; the body-walker variable_declarator branch (5230) needs node
+kind `variable_declarator` → kotlin has none. property_signature/
+method_signature (1282) are TS-only kinds. **Net: kotlin emits ZERO
+type-annotation `references` refs — no param types, no return types, no
+property types, no local types** (pinned: torture has no such refs).
+BUILTIN_TYPES is never consulted for kotlin. The walker needs cheap
+early-outs that preserve exactly this nothing.
+
+### Docstrings (tree-sitter-helpers.ts:95-127) — KDoc is DROPPED
+
+Kotlin comment node kinds: `line_comment` (`//`) and `multiline_comment`
+(`/* */` AND KDoc `/** */`). getPrecedingDocstring accepts only
+{comment, line_comment, block_comment, documentation_comment} —
+**`multiline_comment` is NOT in the set → KDoc NEVER becomes a docstring,
+and a KDoc sitting between a `//` run and the declaration BREAKS the chain**
+(it's a non-comment named sibling to the scan). Pinned
+(`extract-docs.txt`): `/** KDoc */` + `// line one` + `// line two` + fun →
+doc = "line one\nline two"; KDoc-only → doc undefined; `/** kdoc */` then
+`// trailing line` then fun → "trailing line" only. DOCSTRING_WRAPPER_TYPES
+(55-62) contains no kotlin kinds → no anchor climbing. cleanCommentMarkers
+(77-90): only the `^\/\/[/!]?\s?` per-line strip fires for kotlin line
+comments — all `gm` strips ride `js_multiline_strip` in docstring.rs (#1329
+CRLF semantics) — **call the shared docstring.rs, port nothing**. Properties
+(hook-created) never get docstrings at all. The import/package
+comment-gluing (§Imports) eats the docstring of the first declaration after
+the import block — pinned: `topLevel` has doc=undefined despite two `//`
+lines directly above it.
+
+### Value-reference edges (398-931) — kotlin IS in VALUE_REF_LANGS (401)
+
+Port the full machinery (crib java.rs/go.rs): `CODEGRAPH_VALUE_REFS=0` kill;
+MAX_VALUE_REF_NODES = 20,000 caps the prune DFS and each reader scan;
+isGeneratedFile skip.
+
+- **Targets** (captureValueRefScope:735): kind constant|variable, name
+  length ≥3 AND `/[A-Z_]/` (`file_table` qualifies via `_`; `count` does
+  not), parent id prefix ∈ {file:, class:, module:, struct:, enum:}.
+  **QUIRK (the php-namespace analogue, pinned): in ANY file with a `package`
+  header, top-level properties' parent is the `namespace:` node → NOT
+  accepted → top-level kotlin constants are NEVER value-ref targets.** Only
+  un-packaged files (scripts, `.kts`) keep file-level targets
+  (`extract-vref-nopkg.txt`), and class/object/companion-scope constants
+  (class: parent) are the working population (`extract-vref.txt`:
+  readTop → TOP_LIMIT, readBoth → SHARED_TABLE).
+- **Reader scopes**: every function/method/constant/variable node (764) —
+  fields are NOT readers (a class `val`'s initializer reads nothing;
+  a hook-'const' object property IS a reader — its whole
+  property_declaration subtree incl. `by lazy { }` lambda contents is
+  DFS'd; the reader DFS has NO lambda halt, unlike the fn-ref scan).
+- **Shadow prune** (803-878): the kotlin declarator case is
+  **`property_declaration` (856-869)** — vd = find direct
+  `variable_declaration` → its first `simple_identifier` → bump (the
+  Swift half of that case — name field / value_binding_pattern — is null
+  path for kotlin); destructuring (multi_variable_declaration) bumps
+  NOTHING (a destructured local shadow never prunes — quirk, preserve).
+  bump() counts `identifier`/`simple_identifier` nodes (807 — the comment
+  names Kotlin). Every `val`/`var` ANYWHERE in the tree bumps its name:
+  the target's own declarator + any body-local re-declaration → declCount
+  > fileScopeCount → target deleted. Pinned: companion `RETRY_MAX` +
+  a method-local `val RETRY_MAX = 9` → RETRY_MAX pruned (readBoth emits
+  only SHARED_TABLE).
+- **Emission** (880-930): per reader scope DFS (stack-based, namedChildren
+  pushed in order and POPPED — reverse-source-order visitation, ruby
+  precedent; edge ORDER follows); reader node type `simple_identifier`
+  (906-909 — the comment names Kotlin; `identifier`/`constant`/`name` never
+  occur in kotlin trees). Any textual occurrence whose text maps to a
+  target: nav members (`X.SHARED_TABLE`'s member half), `${TARGET}`
+  interpolations (interpolated_expression > simple_identifier) — but NOT
+  `$TARGET` (node kind `interpolated_identifier`, not accepted — pinned).
+  Skip self-id, same-name, dedupe per (scope,target) → EDGE
+  {kind:'references', metadata:{valueRef:true}}, appended AFTER the walk
+  (flush order below). The Dart/Pascal sibling pull (891) is inert — a
+  kotlin property's next sibling can be a `getter`, which is neither
+  `function_body` nor `block`.
+
+### Function-as-value capture (#756) — KOTLIN_SPEC (function-ref.ts:240-248)
+
+idTypes = **EMPTY** (bare simple_identifiers are NEVER candidates;
+explicitRef always true — irrelevant, no addressOfOnly). dispatch:
+`value_arguments` → args; `assignment` → rhs with **NO field** (RHS = LAST
+named child; the lhs for the param-storage skip comes from
+namedChild(0) — the `directly_assignable_expression`; the skip
+(408:430-443) compares the LHS's trailing identifier to the FULL rhs text —
+callable refs start `::`, so it effectively never fires for kotlin, but port
+the comparison). layers: `value_argument` → null (fan out namedChildren).
+special: {`callable_reference`, `navigation_expression`}. No
+unwrap/ungatedModes/addressOfOnly.
+
+- The `value_argument` **label-forward skip (547-557) is DEAD for kotlin** —
+  it reads `getChildByField(node,'name')` → null (zero fields), so a named
+  argument `f(cb = cb)` is NOT skipped; the fan-out visits both the label
+  and value identifiers (bare ids → nothing anyway, idTypes empty). Only
+  Swift exercises the skip. Reproduce the fan-out.
+- **`callable_reference` special (649-665):** scan namedChildren — receiver =
+  last `type_identifier` child, member = last `simple_identifier` child. No
+  member → [] (**`String::class` — the `class` is an anon keyword → member
+  null → nothing**). No receiver → bare member (`::topLevel` → `topLevel`,
+  gated). Receiver present → `/^[A-Z]/` on its text: `OtherClass::handle` →
+  candidate **`OtherClass::handle`** (the `::` rule at flush:709 —
+  ALWAYS-flush); **`w::render` → [] — the grammar parses even a lowercase
+  variable receiver as `type_identifier`, and the CASE regex (not the node
+  type) is what drops it** (pinned: no ref).
+- **`navigation_expression` special (671-681):** only when the WHOLE node's
+  text starts `this::` → the navigation_suffix starting `::` → its LAST
+  named child → candidate **`this.<member>`** (ALWAYS-flush). `this::caller`
+  pinned. Ordinary `a.b` navs in args → [].
+- Capture points: visitNode:990 (top-level/class-scope call args),
+  visitFunctionBody:5137, scanFnRefSubtree (hook-consumed property
+  subtrees — `val x = register(::f)` captures via the inner
+  value_arguments; **the scan halts at `lambda_literal` (610), so refs
+  inside `by lazy { }`/trailing lambdas under a hook-consumed property are
+  NOT captured**). **NOT captured anywhere: property/local initializer
+  callable refs (`val m = ::caller`, `val bound = w::render`) — kotlin's
+  dispatch has NO property_declaration/varinit key** (unlike SWIFT_SPEC —
+  do not borrow it). Pinned: torture emits exactly three function_refs —
+  `topLevel` (definedHere), `OtherClass::handle`, `this.caller`.
+- Flush gate (639-728): generated-file skip; `this.`-prefixed +
+  `::`-containing candidates always flush; bare names need definedHere
+  (same-file function/method names) ∪ importedNames (dotted-import last
+  segments — §Imports). Dedupe `${fromNodeId}|${name}` →
+  {referenceKind:'function_ref'} (FUNCTION_REF_CODE 200 on the wire).
+
+### visitFunctionBody (5129-5286) — kotlin rows
+
+- maybeCaptureFnRefs (5137) per node; `macro_invocation` branch rust-gated.
+- `call_expression` → extractCall (5143), NO return → children recursed
+  (chains/args re-visited).
+- INSTANTIATION_KINDS (5145) — never for kotlin. extractBareCall — absent.
+- extractStaticMemberRef (5218) — every body node (§Static-member).
+- variable_declarator type-annotation branch (5230) — dead (no such kind).
+- **Nested `function_declaration` (5245)** → named → extractFunction (→
+  receiver check → possibly extractMethod). Local funs become `function`
+  nodes (QN `…::caller::localFn`).
+- classTypes (5255): a body-local `class LocalClass { … }` → full
+  extractClass (kind via classify), contained by the enclosing function —
+  its methods extract normally (pinned: `caller::LocalClass::lm`).
+- **`object_declaration` in a body is NOT dispatched** (extraClassNodeTypes
+  is not checked in visitForCallsAndStructure) → recursed → its class_body's
+  `fun`s hit 5245 → **extractFunction: a local object's methods become
+  FUNCTIONS contained by the enclosing function** (pinned: `caller::om`),
+  its properties mint nothing (no hook here). Same for **`object_literal`
+  (anonymous objects)**: `val anon = object : Drawable { override fun
+  draw() … }` in a body → NO class node, NO extends ref, `draw` leaks out as
+  a function under the enclosing fn with its body calls attributed to it
+  (pinned: `caller::draw`, `calls helper from=function:draw`). At
+  TOP-LEVEL/class scope the same object_literal sits inside a hook-consumed
+  property → **completely invisible** (methods and all — scan halts at
+  nothing relevant but extraction never runs). Pin the asymmetry.
+- Bodies recursed transparently through when/if/for/try
+  (control_structure_body), elvis, postfix `!!`, labels (`label@`), lambdas
+  (annotated_lambda/lambda_literal — enclosing-fn attribution), string
+  templates (interpolated_expression emits calls; interpolated_identifier
+  is inert).
+
+### Misc shared paths
+
+- Positions: `line = startPosition.row + 1`, `column = startPosition.column`
+  — **UTF-16 code units** (textutil::col16), as are startIndex/endIndex
+  substrings (getNodeText everywhere) and the import-signature `.trim()`.
+- Refs carry NO filePath/language (store denormalizes; §arch-7 — no
+  REF_FLAG_FILE_PATH use). `function_ref` = wire code 200.
+- `extract()` wrap: file node → namespace node (if package) → walk order →
+  `flushFnRefCandidates` then `flushValueRefs` (538-539, both while the
+  namespace is still pushed) → pops. Table order: nodes in creation order;
+  contains edges interleaved with creation, value-ref EDGES appended LAST;
+  walk-order refs, then function_ref refs at flush. Store/harness are
+  rowid-order-sensitive — reproduce exactly.
+- **CRLF hazards inventory for the kotlin path**: kotlin.ts has NO regexes
+  over multi-line source (getReturnType's `/^[A-Za-z_]\w*$/` and the
+  visibility `.includes` are single-token); the shared paths' regexes that
+  fire for kotlin are extractCall's parenthesized-conversion (4530,
+  single-name, `\s*` can eat `\r` — port as-is with `\s` semantics),
+  the #750 `\s+` strip (JS `\s` ⊇ `\r` — Rust regex `\s` matches `\r` too,
+  parity holds), decorator name normalization (4959-4962), and
+  cleanCommentMarkers' `gm` strips → **`js_multiline_strip` in docstring.rs
+  (#1329), call it**. Grammar-level CRLF probed clean (identical shapes,
+  no errors, `extract-crlf.txt` byte-sane; the comment-gluing reproduces on
+  CRLF identically).
+- Defer policy: per-file `has_error()` → `defer:`; expected incidence
+  4.7–8.5% (§arch-6) including the PHANTOM class (hasError with no
+  ERROR/missing node — trust the flag, not node presence). wasm recovery is
+  canonical; fun-interface files always land here.
+- MAX_FILE_SIZE / generated-file skips: shared, nothing kotlin-specific.
+
+## Frameworks & synthesis consumers (stay TS-side — pin the walker's output contract)
+
+- **kotlinExpectActualEdges (resolution/callback-synthesizer.ts:987-1026;
+  doc block :955-985)** — the tracker's "expect/actual pairing is
+  synthesis-side". Reads `queries.iterateNodesByLanguageWithDecorator(
+  'kotlin','actual')` (db/queries.ts:1082 — a LIKE pre-filter over the
+  node DECORATORS column) then exact `decorators.includes('actual')`,
+  `getNodesByQualifiedNameExact(act.qualifiedName)`, kind compatibility via
+  KMP_TYPE_KINDS {class, interface, struct, enum, **type_alias**} (:982 —
+  `actual typealias` fulfillment), different file, counterpart NOT marked
+  actual → synthesized `calls` edge decl→actual. **Walker obligations:
+  decorators content+order from extractModifiers; EXACT qualifiedNames
+  (package-prefixed — `com.example::PlatformFile`); kinds; filePath;
+  startLine.** Validate on kotlinx.coroutines (the KMP gate repo: 30
+  expect-files / 95 actual-files at survey time) — spot-check
+  `synthesizedBy: 'kotlin-expect-actual'` edge counts are IDENTICAL under
+  kernel and wasm arms after a full init of each.
+- **Closure-collection pass (callback-synthesizer.ts:252-326, CC_LANGUAGES
+  :77 = {swift, kotlin} — the #1235 gate)** — synthesis-side, no port, but
+  it consumes extraction artifacts: for every method/function node with
+  `language === 'kotlin'` it re-reads source and slices
+  `sliceLines(content, m.startLine, m.endLine)`, regexing
+  `.forEach { it( ` dispatchers and `.append/.add/.push/.insert(`
+  registrars. **Walker obligations: method/function node startLine/endLine
+  spans (incl. the resolveBody endLine extension) and the language tag** —
+  a truncated endLine silently drops dispatch edges.
+- **rnCrossPlatformEdges (callback-synthesizer.ts:1645+)** — kotlin ∈ NATIVE
+  set (:1649); pairs native method/function node NAMES across
+  java/kotlin/objc/cpp with JS callers. Standard node-table obligation only.
+- **springResolver / expoModulesResolver / fabricViewResolver** (§arch-2) —
+  regex over raw `.kt` source in extractFromSource:6736-6758; their route
+  nodes carry literal ids (`route:${filePath}:${line}:…`) and their refs
+  carry filePath+language (framework refs, unlike extraction refs). No
+  walker dependency beyond method/class node names for their handler-ref
+  resolution.
+
+## Parity mechanics (all have bitten before)
+
+- **Emission order** per §Misc: file → namespace → source-order walk (per
+  construct: node + contains edge → extends refs BEFORE body members →
+  extractor-order refs) → function_ref refs → value-ref EDGES last.
+- **generateNodeId inputs**: (filePath, kind, name, startRow+1) — name keeps
+  backticks; import nodes are named the dotted path; enum_member line = the
+  IDENTIFIER's line (not the enum_entry's — same line in practice, but the
+  COLUMN and the node's position row both come from the identifier child);
+  namespace line = the package_header start; the glued-comment extents
+  affect endLine/endColumn (and import signatures), never the id line.
+- **Receiver-QN methods**: id hashes the NAME only — the qualifiedName
+  override (`WidgetK::extend`) does not enter generateNodeId.
+- **UTF-16 columns/slices** (textutil::col16/slice_utf16): every ref/node
+  column, getNodeText substrings, import-signature trim. Kotlin sources are
+  multibyte-heavy (string templates, KDoc) — the torture fixture needs a
+  non-ASCII line before a symbol.
+- **CRLF**: §Misc inventory; CRLF variants of every fixture derived
+  in-memory (kernel-tsjs-parity pattern).
+- **Defer policy**: `has_error()` → `defer:` — INCLUDING phantom errors
+  (complete CSTs; do not "optimize" by checking for ERROR nodes) and every
+  fun-interface file. Sweep with the default `--max-deferral 0.1`; expected
+  deferral counts okio 23 / okhttp 49 / kotlinx.coroutines 51.
+- **node-ID-string dedupe**: `node_ids` vec pattern (same-(kind,name,line)
+  collisions are routine — e.g. one-line `class X { fun x() … }` shapes).
+
+## Gates (per plan §5, no exceptions)
+
+- **Standalone GRAMMAR-BUMP gate first (rust pattern), before any walker:**
+  vendor the wasm + `VENDORED_WASM_LANGS += 'kotlin'` (+ the kernel C
+  vendor/build.rs/langs.rs/grammar-parity row staged with it), then
+  old-vs-new **full-init dump-diffs** (`scripts/dump-graph.mjs`, cmp) on the
+  three gate repos with the kernel OFF both arms. Expected:
+  **byte-identical on all three** (behavior-neutral bump — any hunk at all
+  blocks). Full suite green ×2.
+- **Torture fixtures** per `## Fixtures to build` below, exercised by a new
+  `__tests__/kernel-kotlin-parity.test.ts`.
+- **Parity sweeps** (`scripts/kernel-parity.mjs <dir>`, order-sensitive
+  full-object, default `--max-deferral 0.1`):
+  - `/private/tmp/claude-501/-Users-colby-Development-CodeGraph-codegraph/0c11bda1-0b19-4fec-bcd9-d0cb4b2d6e8a/scratchpad/gate-repos/okio` (small, 322 kt/kts files)
+  - `…/gate-repos/okhttp` (medium, 580)
+  - `…/gate-repos/kotlinx.coroutines` (large, 1,082 — **the KMP/expect-actual gate**)
+  (cloned fresh at survey; re-clone public OSS if gone — agent-eval policy).
+  Expect 0-diff on every NON-deferred file and exactly the §arch-6 deferral
+  counts. Then **full-init dump-diffs byte-identical** (kernel arm vs
+  `CODEGRAPH_KERNEL=0`, `dump-graph.mjs`, cmp) on the same three.
+- **KMP synthesis spot-check** (tracker row requirement): after the
+  kotlinx.coroutines dumps, `select count(*) from edges where
+  json_extract(metadata,'$.synthesizedBy')='kotlin-expect-actual'` equal
+  across arms (the dump gate already implies it; assert it explicitly once).
+- **Suite**: kernel-kotlin-parity torture + CRLF variants + the defer
+  fixture (a `fun interface` file — asserts kernel `defer:` + wasm-served
+  output matches `extract-funiface.txt` shape) + a phantom-error fixture
+  (single-line class body — kernel defers despite the complete CST); full
+  suite ×2 green with `CODEGRAPH_KERNEL_EXPECT=1`.
+- **`DEFAULT_ROUTED += 'kotlin'`** (kernel/index.ts:37) only after ALL of
+  the above; changelog rides the existing kernel entry.
+- Post-route perf sanity: gate repos ride the raw path (§arch-2); a
+  Spring-Boot-Kotlin or Expo repo is the decoded-path smoke check. Deferral
+  costs mean the kotlin speedup lands on ~92-95% of files — measure
+  accordingly.
+
+## Fixtures to build
+
+1. `__tests__/fixtures/kernel-parity/torture.kt` — the survey's
+   `svy-kotlin/torture.kt` is the seed (its `extract-torture.txt` is the
+   expected-output pin). Inventory, by branch: package header + file KDoc +
+   `@file:` annotation; imports: plain, dotted, wildcard (`.*` → package
+   path), alias (`as` — source path), **comment-glued signature** (comments
+   after the last import); top-level fn with params/return (ret from
+   user_type; sig undefined); extension fns: plain (`WidgetK::extend`),
+   generic receiver (`List`), **qualified receiver (`com::qext` bug)**,
+   infix (`Int::pow`), operator; suspend fn (isAsync true) + the
+   **`@suspendMarker` text-includes false positive**; `private internal`
+   (visibility order) + an `internal fun` ('internal'); inferred return /
+   `Unit` / nullable / lambda return / `: T` generic leak; `expect fun`
+   (bodiless + dec) / `actual fun`; tailrec self-call in expression body;
+   top-level `val`/`var`/`const val`/`by lazy {}` (constant/variable kinds,
+   NO initializer refs, NO capture inside the delegate lambda) +
+   **destructuring (`val (a,b)` → nothing, both scopes)** + next-line-getter
+   top-level `val` (getter calls → file/namespace); class with primary ctor
+   (props invisible, defaults not walked), class-body val/var/computed
+   (fields; same-line getter consumed vs **next-line getter → class-attributed
+   calls**), init block + secondary ctor (class-attributed calls), methods,
+   companion (constants under the class + method + **named companion
+   `Named`** minting nothing); bodiless-vs-bodied header asymmetry (`class
+   Bodiless(val b = initB()) : Base(readCfg2())` → extends + 2 class-level
+   calls; the bodied twin → extends only); data class (no members);
+   abstract/open one-liners (phantom-error shapes — but keep the PARITY
+   fixture erroring-free: single-line bodies go in the DEFER fixture
+   instead, since the kernel defers them!); interface (no visibility;
+   bodiless method nodes; default impl; `val prop` w/ same-line getter →
+   field); sealed class + nested `object`/`data class` members (extends
+   SealedOp); sealed interface (bodiless); enum: simple entries (positions =
+   identifier), ctor'd entries, **entry-with-body (overrides invisible)**,
+   post-`;` methods + companion-in-enum; `object` declaration (class kind;
+   members; value-ref targets); `annotation class`; annotated class/method
+   (`@MyMarker`/`@JvmStatic` → decorates; **`@Deprecated("x", …)` → NOTHING**;
+   `@field:`/`@get:` on properties → nothing); typealias ×2 (no value refs)
+   + **`actual typealias` (dec on type_alias)**; `expect class` +
+   `actual class` (+ marked member); call shapes: bare, constructor
+   (`WidgetK` calls ref, NO instantiates), `this.`/`super.` (bare), member,
+   aliased-import receiver, literal receivers (nothing), capitalized-chain
+   re-encode (`WidgetK.create().render` + inner) + lowercase chains (bare +
+   inner), 2-hop nav call, safe-call `x?.render()` (plain encoding), `!!`
+   receiver (bare), elvis-arm ctor call, trailing lambda (`run {}` /
+   `w.let {}` / `listOf(1).forEach {}`), **paren-then-lambda
+   (`trailing() {}` → `trailing()` + `trailing`)**, generic call
+   (`generic<Int>(1)` → `generic`), backtick call, glued newline-invoke
+   chain (raw-text callee — or a note excluding it if the fixture keeps
+   statements separated; pin ONE of the two deliberately), `(handler)(4)`
+   first-in-body (conv-regex rewrite), interpolation call
+   (`"${w.render()}"`) + `$id` (nothing); static-member reads:
+   `Registry.count` / `Color.RED` (references at receiver pos),
+   `com.example.Fq.X` (nothing), `Cls.member` as callee (skip),
+   **assignment LHS writes (nothing)**; delegation specifiers: plain +
+   ctor'd + generic + **qualified (`com` bug)** + **`by` delegation
+   (nothing)**; local declarations in bodies: named local fn, local class
+   (full), **local `object` (methods leak as functions)**, **`object :
+   Iface {}` literal (function leak; and the top-level-property twin —
+   invisible)**; callable refs: `::topLevel` (defined-here gate),
+   `OtherClass::handle` (import gate irrelevant — always-flush),
+   `w::render` (dropped), `this::caller` (`this.` flush),
+   `String::class` (nothing), `val m = ::caller` (NOT captured),
+   assignment `obj.cb = ::handler` (captured), named-arg
+   `f(cb = ::handler)` (fan-out capture); value refs: companion/object
+   constants + readers (incl. a `${CONST}` interpolation read and a
+   `$CONST` non-read), the local-shadow prune, `count`-style
+   non-target names, and the namespace-drop (packaged file: top-level
+   consts are NOT targets — plus the un-packaged `.kts`/no-package twin
+   where they ARE); docstrings: `//` runs (kept), KDoc (dropped), KDoc
+   above a `//` run (run kept), KDoc between run and decl (chain broken),
+   comment-after-imports (lost to gluing); a non-ASCII (UTF-16) line
+   before a symbol; a `when`/`for`/labeled-loop body.
+2. **CRLF variants** of the fixtures derived in-memory (kernel-tsjs-parity
+   pattern) — docstring cleaning + comment-gluing + import signatures under
+   CRLF bytes.
+3. **`.kts` fixture** — top-level statements (calls from the FILE node),
+   top-level val (file-parent value-ref target), no package header.
+4. **Defer fixture #1: `fun interface`** (Pattern 1 shape) — kernel defers
+   (`defer:`), wasm output serves the pinned interface+method recovery
+   (`extract-funiface.txt`).
+5. **Defer fixture #2: phantom error** — `abstract class A { abstract fun
+   i(): Int }` one-liner — kernel defers on has_error() despite a complete,
+   ERROR-node-free CST; wasm output is byte-normal.
+6. **KMP fixture pair** — `expect class` + `fun` in one file, `actual class`
+   + `actual typealias` in another (same QNs, different files) — feeds the
+   expect/actual synthesizer identically under both arms (can fold into the
+   frameworks-integration or synthesizer suites if simpler).
+
+## Probe artifacts (session scratchpad `svy-kotlin/`)
+
+`table-compare.cjs` (kind/field/ABI tables), `shape-probe-kotlin.cjs` +
+`torture-{OLD,NEW}.txt` (full CST dumps, byte-identical) + `torture.diff`
+(empty), `mini-probes.cjs` + `mini-probes.out` (fun-interface ×2,
+destructuring ×2, safe-call/elvis, assignment shapes, file annotation,
+delegates, named-arg refs, getter nesting, KDoc runs, CRLF, suspend
+false-positive, lateinit, top-level object literal — all OLD==NEW),
+`error-sweep.cjs` + `errors-{okio,okhttp,kotlinx.coroutines}.txt`,
+`extract-probe.cjs` + `extract-{torture,vref,vref-nopkg,docs,bodiless,crlf,
+funiface,lfpkg,kts}.txt` (dist-extractor ground truth), fixtures
+(`torture.kt`, `vref.kt`, `vref-nopkg.kt`, `docs.kt`, `bodiless.kt`,
+`crlf.kt`, `funiface.kt`, `lfpkg.kt`, `script.kts`), grammar material
+(`tree-sitter-kotlin/` 0.3.8 tag clone + the tree-sitter.json shim,
+`crate-extract/` = crates.io 0.3.8 tarball, `ng-extract/` = kotlin-ng 1.1.0
+tarball, `tree-sitter-kotlin-NEW.wasm` = the staged-candidate build,
+sha256s in §Grammar prep). Scratch dirs are throwaway — re-derive from this
+doc if gone.

+ 1 - 1
docs/design/rust-kernel-migration-plan.md

@@ -610,7 +610,7 @@ parity before porting the language.
 | rust | `languages/rust.ts` | T1 | crates.io | **DONE (R7b #1, 2026-07-20)** — `rustlang.rs` walker; grammar bumped to v0.24.2 (crate + vendored wasm together). Parity 0-diff on ripgrep/tokio/rust-analyzer + dump byte-identical ×3; rust-analyzer's parser crates defer 18% (token-macro tables, both-arm parse errors — grammar-inherent). Quirk list: docs/design/rust-lang-kernel-port-checklist.md. | ☑ |
 | dart, scala, lua, luau, r | dedicated files | T1 | crates.io (luau/r/scala: verify crate freshness vs our wasm) | Long-tail T1; port opportunistically after the big five. | ☐ |
 | kotlin | `languages/kotlin.ts` | T1½ | crates.io | Expect/actual pairing is synthesis-side (fine); extraction is clean but validate against a KMP repo. | ☐ |
-| swift | shared + dedicated branch | T1½ | crates.io | **Trap:** in-class property extraction lives in `tree-sitter.ts`'s DEDICATED branch, not `swift.ts` (#1020 — Alamofire went 0→348 props). Gate on Alamofire. | ☐ |
+| swift | shared + dedicated branch | T1½ | crates.io | **DONE (R7b #5, 2026-07-20)** — `swift.rs` walker incl. the #1020 dedicated property branch (Alamofire's 348 property nodes reproduced exactly on the kernel arm); grammar bumped to crate 0.7.3 (wasm built from the CRATE TARBALL's src — the tag ships an older ABI-14 generation; delta = error-set membership + 2 gate-found categories, all classified). Parity 0-diff Alamofire/vapor/swift-nio (720 clean files; deferral 9–27% both-arm structural — sweeps use --max-deferral 0.3). One walker fix found by the sweep: the shared `assignment` shadow-prune case is swift-live (declared-then-assigned `let X: T`). Quirk list: docs/design/swift-kernel-port-checklist.md. | ☑ |
 | c, cpp | `languages/c-cpp.ts` | **T2** | crates.io | **DONE (R7a, 2026-07-17)** — `ccpp/` walker; ALL pre-passes stayed TS-side via the route-point preParse hoist (+6 new blanks added during gating — see the checklist doc); content-based `.h` C-vs-C++ detection stays upstream at detectLanguage. Parity 0-diff + dump byte-identical on redis/git/fmt/protobuf/ALS. | ☑ |
 | metal, cuda | dialects over the cpp grammar | **T2** (rides c/cpp) | crates.io (cpp) | **DONE (rides R7a)** — `.metal`/`.cu`/`.cuh` map to 'cpp' and their blanks run in the hoisted preParse (filePath rides along for the extension gates); hoist-parity pinned in kernel-ccpp-parity.test.ts + the metal/cuda suites. | ☑ |
 | objc | `languages/objc.ts` | T2 | crates.io | Rides the c-cpp trap family; RN bridge extraction feeds `rnCrossPlatformEdges` (synthesis-side, fine). | ☐ |

+ 1084 - 0
docs/design/swift-kernel-port-checklist.md

@@ -0,0 +1,1084 @@
+# Swift kernel port (R7b) — the bug-for-bug checklist
+
+**Status: PORT COMPLETE (2026-07-20)** — walker `codegraph-kernel/src/swift.rs`,
+all gates passed (bump validated standalone with the diff classified per the
+error-union rule + ripple pairing + two gate-found categories 7/8 below;
+parity sweeps 0-diff Alamofire 89/98 / vapor 224/247 / swift-nio 407/554 at
+--max-deferral 0.3 with exactly the predicted deferral counts; dump gates
+byte-identical ×3; the Alamofire census reproduced property=348 on the kernel
+arm; kernel-swift-parity suite; DEFAULT_ROUTED += swift). **One survey
+correction found by the swift-nio sweep: §Shadow prune understated the case
+list — the shared `assignment` prune case (tree-sitter.ts:829) is ALSO
+swift-live** (a declared-then-assigned `let X: T` + `X = …` branches bumps
+per assignment via the directly_assignable_expression's simple_identifier,
+pruning X — swift-nio's main.swift files pin it). Survey basis: every
+TS-side branch a `.swift` file exercises, with file:line anchors as of
+**`a6c62d7`** (HEAD at survey time, clean main). Every grammar-shape and
+emission claim below was **probed against both the current production wasm
+(tree-sitter-wasms 0.1.13 build of npm tree-sitter-swift ^0.4.0, ABI 13) and a
+fresh crate-0.7.3 build (ABI 15)** — CST probes for shapes, **plus the built
+`dist/` extractor run end-to-end on a 106-line torture file and on all 98
+Alamofire files** (emissions, order, counts pinned from real output, not
+inference). Probe scripts + raw outputs: session scratchpad `svy-swift/`
+(§Probe artifacts). Read WITH `docs/design/rust-kernel-migration-plan.md`
+(§0a recipe, §2 boundary, §5 gates) and the format precedents
+(`rust-lang-`, `csharp-`, `ruby-`, `php-kernel-port-checklist.md` — php's
+non-neutral-bump "enumerate + classify" gate is the template here).
+
+**Blocking findings: none.** Three eyes-open findings that shape the port:
+(1) the grammar bump is **NOT graph-neutral** — the old→new delta is confined
+to *error-set membership* (65 files across the three gate repos parse
+differently-erroneously per arm; three NEW-only regression construct classes
+enumerated in §Grammar-bump deltas) while every clean-parse extractor-relevant
+shape probed **byte-identical** (53-line CST diff across the whole battery,
+all classified inert); the bump gate is php-style enumerate+classify, not
+expect-zero. (2) **Swift error incidence is structurally high — 9.2–26.5% on
+the NEW arm (both-arm reality, not a bump artifact)** — the parity-sweep
+deferral guard MUST be raised for swift (§Deferral policy); a 0.1 guard fails
+the sweep on all three gate repos. (3) The port's center of gravity is the
+**dedicated in-class property branch in tree-sitter.ts (#1020)** — it is NOT
+in languages/swift.ts, it attributes its refs to the *enclosing type* rather
+than the property node, and Alamofire's 348 computed-property nodes (re-counted
+at survey time: exactly 348) ride it (§Dedicated property branch).
+
+## Grammar prep (NOT staged — land FIRST, before any walker exists)
+
+swift is **not** in `VENDORED_WASM_LANGS` (grammars.ts:291) — production loads
+`require.resolve('tree-sitter-wasms/out/tree-sitter-swift.wasm')` (mapping
+`swift: 'tree-sitter-swift.wasm'` at grammars.ts:34; `.swift` → swift at
+grammars.ts:105), a 2023/24-era **ABI-13** build of npm tree-sitter-swift
+^0.4.0 (sha256 `41c4fdb2…`, 3,147,876 bytes; 540 node types / 45 fields).
+
+- **Version: crate `tree-sitter-swift = "=0.7.3"`** (crates.io max_stable,
+  published 2026-06-01; repo alex-pinkus/tree-sitter-swift).
+- **PROVENANCE TWIST — the crate tarball is the canonical generated-file
+  source, NOT the git tag.** alex-pinkus keeps generated files off `main`;
+  tag `0.7.3-with-generated-files` (commit
+  `31d17fe7e818a2048c808b5c6fdc2dc792f4f5b5`) ships an **ABI-14**
+  parser.c (LANGUAGE_VERSION 14, SYMBOL_COUNT 565 — an *older-generator* run,
+  plus `parser_abi13.c`/`parser_abi14.c` variants), while the crates.io
+  tarball ships an **ABI-15** regeneration (LANGUAGE_VERSION 15, SYMBOL_COUNT
+  558, STATE_COUNT 10321). The two are the SAME grammar — `grammar.json`
+  `rules` and `externals` are JSON-equal between tag and crate (probed) — but
+  the parser.c bytes differ, so a tag-side sha-match is impossible.
+  **Build the wasm from the CRATE TARBALL's `src/`** (that is literally what
+  the kernel's cargo build compiles → table-identity by construction, the
+  csharp-precedent verification posture). Never touch the tag's parser.c and
+  never run `tree-sitter generate`.
+  - crate `src/parser.c` sha256
+    `d3edff6effe31b9a507f496577407987343b101b23eb7bee7a9b050e8ab5d27a` (20,642,243 bytes — expect slow builds)
+  - crate `src/scanner.c` sha256
+    `f3d6271d64f58c39eed544104a70ca2cf9ecbf80c5d900620f1afd38836542cb`
+    (== the tag's scanner.c byte-for-byte). **External scanner: YES** — the
+    crate build compiles it automatically; the wasm build picks it up from `src/`.
+- **Build (survey-verified commands):**
+  ```
+  curl -sL https://crates.io/api/v1/crates/tree-sitter-swift/0.7.3/download -o ts-swift-0.7.3.crate
+  tar xzf ts-swift-0.7.3.crate && cd tree-sitter-swift-0.7.3
+  # crate tarball lacks tree-sitter.json (metadata only — no effect on tables);
+  # copy it from the 0.7.3-with-generated-files tag clone:
+  cp ../tag-clone/tree-sitter.json .
+  npx -y tree-sitter-cli@0.25.10 build --wasm -o tree-sitter-swift.wasm .
+  ```
+  (brew emcc present; one benign scanner warning. Survey artifact: ABI 15,
+  3,726,622 bytes, sha256
+  `cc77a63b8487956270e2f385e29a03ba0773ba532a3c8a8844a26b4c98793843`, 563 node
+  types / 46 fields, at scratchpad `svy-swift/tree-sitter-swift.wasm`.)
+- **Version-choice rationale (evaluated, not defaulted):** the feared
+  alex-pinkus shape churn did NOT materialize for extraction — the full
+  OLD-vs-NEW CST battery (types/funcs/props/calls/imports/statics/docs) diffs
+  in only 53 lines, all inert (§deltas). An older crate (0.5.0/0.6.0) was
+  considered as a closer-match alternative and REJECTED: clean-parse shapes are
+  already identical on 0.7.3, so an older pin buys no shape proximity and
+  loses the error-set wins that dominate the delta — swift-testing `#expect`
+  (Alamofire's whole test suite), `#Preview`/`#GET`-style freestanding macros
+  (vapor 57→23 error files), `package` access, and typed `throws(E)` all parse
+  ONLY on 0.7.x. 0.7.3's three regression construct classes (§deltas) are
+  net-smaller (21 files) than its fixes (63 files) on the gate repos.
+- **Staging plan:** vendor to `src/extraction/wasm/tree-sitter-swift.wasm`;
+  add `'swift'` to `VENDORED_WASM_LANGS` (grammars.ts:291) with an R7b
+  comment noting the crate-tarball provenance (NOT tag-sha-matched — state
+  why); pin `tree-sitter-swift = "=0.7.3"` in codegraph-kernel/Cargo.toml
+  (crate + wasm move TOGETHER); add `'swift'` to `GRAMMAR_LANGUAGES` in
+  `__tests__/kernel-grammar-parity.test.ts:39`; kernel symbol
+  `tree_sitter_swift::LANGUAGE` in langs.rs `grammar_for` + the `LANGUAGES`
+  const (langs.rs:18, `[&str; 13]` → 14). MIT license. `copy-assets` already
+  globs `src/extraction/wasm/*.wasm`.
+- **Bump lands FIRST, standalone, full suite green + the enumerate+classify
+  dump gate (§Gates)** — the diff is expected NON-empty and must be confined
+  to the classified categories.
+
+## Grammar-bump deltas (old ^0.4.0 → 0.7.3), every one classified
+
+**Clean-parse shapes: byte-identical.** The whole probe battery (class-family,
+functions, properties incl. wrappers/observers/tuple-lets, every call shape,
+imports, static reads, docstrings) produced a 53-line CST diff, fully
+decomposed into:
+
+**Inert (verified against every consuming branch):**
+
+1. `#selector` token split: OLD one anon `#selector` token; NEW anon `#` +
+   `selector`. `selector_expression`'s NAMED children are unchanged →
+   `normalizeSpecial` (function-ref.ts:685) and the body walker see identical
+   trees. Inert.
+2. `#warning(…)` internals: OLD = one `diagnostic` leaf swallowing the full
+   text; NEW = `diagnostic` with an anon `#` child. `diagnostic` matches no
+   type list on either arm; no named children either way; no stray sibling
+   call on NEW (probed clean-file). Inert.
+3. ABI/table renumber (540→563 node kinds, 45→46 fields) — kind-id churn
+   only; every consumer keys on type STRINGS.
+
+**Behavior-changing — ALL of it is error-set membership** (the wasm arm never
+defers: production extracts from error-recovery trees, so files whose
+error-status or recovery changes WILL diff in the bump gate's dump):
+
+4. **OLD-error → NEW-clean (the bump working as intended), 63 files across
+   the gate repos.** Construct classes, each probed to a minimal repro:
+   freestanding macros — `#Preview { }` (OLD: ERROR + orphan lambda; NEW:
+   `macro_invocation` — a NEW-only node type, in NO type list → recursed, its
+   lambda's calls attribute to the enclosing scope), swift-testing `#expect(…)`
+   (all 4 OLD-only Alamofire error files are Tests using it), vapor's `#GET`
+   route macros (drives vapor 57→23); `package` access modifier; typed throws
+   `throws(ErrorType)` (swift-nio ByteBuffer-views). Node/edge/ref diffs in
+   these files are expected and accepted.
+5. **NEW-only error regressions, 21 files (3 Alamofire / 1 vapor / 17
+   swift-nio), three construct classes probed to minimal repros:**
+   - `#if`/`#elseif` **between enum cases** (`enum E { case a\n#if DEBUG\n
+     case b\n#endif }`) → NEW ERROR, OLD clean. (`#if` at top level, between
+     CLASS members, around postfix `.modifier(x)` chains, and with `||`
+     conditions all stay clean on both.)
+   - **Parenthesized-compound directive conditions**
+     (`#if (compiler(<6.1) && !os(WASI)) || (compiler(>=6.1) && …)`) → NEW
+     ERROR (swift-nio's lock/NIOLock family).
+   - **Optional-subscript + cast + coalesce chain**
+     (`info?["k"] as? String ?? "d"`) → NEW ERROR (Alamofire HTTPHeaders).
+   - (vapor's one file combines a backtick-escaped `` `default` `` parameter
+     label with other syntax; the label alone parses clean on both.)
+   These files' dumps diff (different recovery trees). Accept as classified;
+   count them; they are the deferral set the kernel will hand to wasm anyway.
+6. **BOTH-arm error files that diff anyway**: two grammars recover
+   differently from the SAME error (e.g. vapor Authenticator's
+   `if let c = try? await …` — errors on both, different trees). Any hunk in
+   a file that is in EITHER arm's error list falls under this category — the
+   gate's mechanical rule is per-file, not per-construct (below).
+7. **(Found at bump-gate time, survey-missed.) Docstring boundaries near
+   `#if` directives on CLEAN files.** The NEW grammar's directive/comment
+   sibling structure changes what getPrecedingDocstring accumulates next to
+   `#if`/`#endif` lines: 7 clean-on-both files (5 Alamofire, 2 swift-nio
+   Mocking.swift) diff in the docstring field ONLY (verified mechanically:
+   every other node field byte-equal). Direction: NEW gains a docstring the
+   OLD chain dropped (`webSocketRequest` gains "Only Apple platforms…") or
+   extends it with directive-adjacent text (`_withWindowsPaths` gains a
+   leading "ENABLE_MOCKING" line). The walker reproduces this automatically
+   (same shared docstring code over the same NEW trees).
+8. **(Found at bump-gate time, survey-missed.) Array-literal-callee call
+   refs.** One clean file (swift-nio IPv4Header.swift) emits 2 NEW-only
+   `calls` refs whose callee text is a whole multi-line array literal —
+   a call_expression whose namedChild(0) is the array literal on NEW where
+   OLD parsed the same bytes as separate expressions. Same garbage-but-
+   deterministic family as `m[i][j]`; the walker reproduces it via the raw
+   func-text rule.
+
+**Bump-gate rule (php precedent, adapted):** run the old-wasm vs new-wasm
+full-init dump diff on all three gate repos; **every diffing file must be in
+the union of the two arms' `hasError` file lists** (compute both lists first —
+`svy-swift/error-incidence.cjs` does exactly this); a diff in a clean-on-both
+file blocks the bump. Expect RESOLUTION ripple beyond the erroring files
+(recovered symbols re-resolve refs in OTHER files — php's category-3
+mechanism); prove ripple hunks mechanically by ref↔edge pairing
+(php checklist's ripple-proof.mjs pattern) instead of re-litigating per file.
+
+### Error incidence (probed, all `.swift` ≤1 MiB, both arms)
+
+| Repo | files | OLD (^0.4.0 ABI-13) | NEW (0.7.3) | OLD-only | NEW-only | both |
+|---|---|---|---|---|---|---|
+| Alamofire | 98 | 10 (10.20%) | 9 (9.18%) | 4 | 3 | 6 |
+| vapor | 247 | 57 (23.08%) | **23 (9.31%)** | 35 | 1 | 22 |
+| swift-nio | 554 | 154 (27.80%) | 147 (26.53%) | 24 | 17 | 130 |
+
+### Deferral policy — swift needs a c/cpp-style exemption
+
+Swift sits FAR outside the ts/java/py/go norm (0–0.42%): heavy `#if`
+platform-conditionalization (swift-nio), `try? await` in conditions, and
+`@storageRestrictions` init-accessors error on BOTH arms. Per-file
+`has_error()` → `defer:` like every walker; **run parity sweeps with
+`--max-deferral 0.3`** (covers swift-nio's 26.5% with headroom; Alamofire/vapor
+sit under 10%). Double-digit deferral on swift is grammar reality, not a
+walker bug — but a deferral-rate JUMP vs the table above is a walker bug.
+Deferred files are served by wasm and byte-match by construction; the sweep's
+compared set is the ~73–91% that parse clean.
+
+## Architecture decisions
+
+1. **No preParse.** `swiftExtractor` has no `preParse` hook (languages/swift.ts
+   — whole file, no such key) → `preParsedSource` (kernel/index.ts:96) is a
+   no-op; both arms parse raw bytes. Swift's `#if` is grammar-native
+   (`directive` nodes, both branches kept as siblings — probed) — nothing to
+   blank, unlike C/C#.
+2. **Framework detection decides decoded vs raw path per repo.** THREE swift
+   resolvers carry `extract()` hooks (parse-worker.ts:93-99 forces any
+   language with an applicable framework `extract()` onto the decoded
+   `extractFromSource` path): `swiftUIResolver` (frameworks/swift.ts:11,
+   detect = any .swift containing `import SwiftUI`, else any file path ending
+   `.xcodeproj`/`.xcworkspace` — note `getAllFiles()` returns indexed files,
+   so the xcodeproj leg rarely fires; the pbxproj inside doesn't end with
+   `.xcodeproj`), `uikitResolver` (:136, detect = any .swift containing
+   `import UIKit`/`UIViewController`/`UIView`), `vaporResolver` (:269, detect
+   = `Package.swift` containing `vapor`, else `import Vapor`). Also
+   `expoModulesResolver` (expo-modules.ts:154, languages swift+kotlin, has
+   extract) and `swiftObjcBridgeResolver` (swift-objc.ts:251 — resolve-only,
+   NO extract, doesn't force decode). Gate-repo reality (verified):
+   **Alamofire → decoded** (Example/ imports UIKit; watchOS example imports
+   SwiftUI), **vapor → decoded** (Package.swift), **swift-nio → RAW buffers
+   path** (no detector fires) — one gate repo per transport, keep all three.
+3. **Framework extractors themselves need NO port** (regex-over-raw-source TS,
+   run in extractFromSource after either arm). Their input contract on the
+   walker: node names/kinds (`struct`/`class`/`component` resolution by name,
+   frameworks/swift.ts:432) and — for the closure-collection pass —
+   function/method node **extents** (§CC pass). Vapor's route refs carry
+   filePath+language but those are FRAMEWORK refs (TS-side, post-walk).
+4. **No extraction ref carries filePath** — swift has no visitNode hook and no
+   framework-independent denormalized refs (probed: every ref in the
+   torture/Alamofire runs has filePath undefined). **The v2 REF_FLAG_FILE_PATH
+   wire slot (buffers.rs:125 / layout.ts:98) stays UNUSED for swift** — flag
+   0 on every ref, like rust/csharp.
+5. **One walker module** (suggest `codegraph-kernel/src/swift.rs`), registered
+   in langs.rs (`grammar_for` → `tree_sitter_swift::LANGUAGE.into()`,
+   `LANGUAGES` += "swift"). Skeleton cribs: **java.rs** for the class-like
+   scope stack + static-member refs + decorators; **ruby.rs/php.rs** for
+   hook-heavy dispatch; **rustlang.rs** for the `node_ids` dedupe + import
+   hooks. The dedicated property branch has no precedent in any walker — it
+   is new logic, transcribe from §Dedicated property branch.
+6. **`.swift` → swift only** (grammars.ts:105); no content sniffing, no
+   dialect. MAX_FILE_SIZE (1 MiB) and generated-file skips are
+   orchestrator/TS-side and shared.
+7. **No POST_PASSES entry** (kernel/index.ts:81 — none for swift) →
+   `tryKernelExtractRaw` stays eligible.
+
+## Extractor config (languages/swift.ts — 138 lines, read it whole)
+
+Types: functionTypes=[`function_declaration`]; classTypes=[`class_declaration`];
+methodTypes=[`function_declaration`] (== functionTypes — the 994 branch handles
+both; the 1027 methodTypes branch NEVER fires for swift);
+interfaceTypes=[`protocol_declaration`]; structTypes=[`struct_declaration`]
+(**DEAD — node type absent from BOTH grammars**, probed against the kind
+tables); enumTypes=[`enum_declaration`] (**DEAD** likewise);
+enumMemberTypes=[`enum_entry`]; typeAliasTypes=[`typealias_declaration`];
+importTypes=[`import_declaration`]; callTypes=[`call_expression`];
+variableTypes=[`property_declaration`, `constant_declaration`]
+(**`constant_declaration` DEAD** — absent from both grammars; swift `let`/`var`
+at every scope is `property_declaration`). nameField=`name`, bodyField=`body`,
+paramsField=`parameter`, returnField=`return_type`.
+
+**THE FIELD-LOOKUP SUBTLETY (do not trust CST dumps here):**
+`fieldNameForChild` labels a function's return type and a typealias's value
+`name:` — but `childForFieldName` resolves MULTIPLE field names on those
+children through hidden rules. Probed truth-table, IDENTICAL on both grammars
+(`svy-swift/field-probe.cjs`):
+
+| lookup | result |
+|---|---|
+| function_declaration `name` | the simple_identifier (function name) |
+| function_declaration `parameter` | **NULL** (parameter children are unfielded) |
+| function_declaration `return_type` | **the return type node** (user_type/optional_type/…) |
+| function_declaration `body` | function_body |
+| typealias_declaration `name` | type_identifier (alias name) |
+| typealias_declaration `value` | **the aliased type node** |
+| property_declaration `name` | the `pattern` node |
+| property_declaration `value` | the initializer expression |
+| property_declaration `computed_value` | computed_property |
+| enum_entry `name` | the FIRST case-name simple_identifier only |
+
+The Rust walker must reproduce these exact resolutions (tree-sitter's native
+`child_by_field_name` has the same semantics — but pin them in the parity
+fixture, this is the branch the whole type-ref story hangs on).
+
+Hooks PRESENT (port each exactly):
+
+- **getReturnType = extractSwiftReturnType (swift.ts:14)** — POSITIONAL scan of
+  namedChildren: skip everything until the first `simple_identifier` (the
+  name; modifiers/type_parameters before it are skipped via the `seenName`
+  latch); after it, `function_body` → undefined (body reached); `user_type` →
+  that node; `optional_type` → its first namedChild of type `user_type`
+  (`?? null`); first such hit wins: text = `getNodeText(typeNode).trim()
+  .replace(/<[^>]*>/g, '')`, last `.`-segment (`KF.Builder` → `Builder` — the
+  comment explains why NOT the first type_identifier), must match
+  `/^[A-Za-z_]\w*$/` and ≠ `'Void'` else undefined. Probed results:
+  `-> Widget` → `Widget`; `-> Foo?` → `Foo`; `-> KF.Builder` → `Builder`;
+  `-> Result<Foo, Err>` → `Result` (single-level generic strip); **nested
+  generics break like rust** — `Result<Vec<Foo>, E>` → strip leaves
+  `Result, E>` → regex fails → undefined; `-> Void`/`-> [Foo]`(array_type)/
+  `-> (Int, Foo)`(tuple_type)/`-> (Int) -> Foo`(function_type) → undefined.
+  Parameters never match (their node type is `parameter`, not user_type — the
+  scan does NOT descend).
+- **resolveName (swift.ts:60)** — class_declaration ONLY, `name` field must be
+  a `user_type` with >1 `type_identifier` children → the LAST one's text
+  (`extension KF.Builder` → `Builder`, so extension members merge with the
+  extended type's simple name, #750). Single-segment (`extension Plain`) and
+  non-user_type names → undefined (default extraction).
+- **getSignature (swift.ts:76)** — `getChildByField(node,'parameter')` is
+  **ALWAYS NULL** (table above) → `if (!params) return undefined` →
+  **signature is undefined on EVERY swift function/method** (probed: sig=null
+  across the torture file and Alamofire). The returnType concatenation is dead
+  code. The walker emits NO signature for swift, ever.
+- **getVisibility (swift.ts:87)** — scan ALL children (anon included) for a
+  child of type `modifiers`; on its WHOLE text: `.includes('public')` →
+  'public', `.includes('private')` → 'private', `.includes('internal')` →
+  'internal', `.includes('fileprivate')` → 'private' (**dead arm** —
+  'fileprivate' contains 'private', the second check already caught it);
+  no modifiers / no match → **'internal'**. QUIRKS, PRESERVE: **`open` →
+  'internal'** (not checked — probed: `open class func classFunc` →
+  vis=internal); `public private(set)` → 'public' (first match);
+  substring-matching runs over ATTRIBUTE text inside modifiers too (an
+  attribute whose lowercase text contains 'private'/'public' would flip
+  visibility — not expressible with real-world wrappers, but the walker must
+  substring-match the same way).
+- **isStatic (swift.ts:101)** — any `modifiers` child whose text includes
+  `'static'` OR `'class'` → true. `class func`/`class var` → static ✓
+  deliberate. Same substring hazard (an `@objc(myclassthing)` attribute text
+  would set isStatic — match bug-for-bug).
+- **classifyClassNode (swift.ts:112)** — scan ALL children for a child of
+  TYPE `struct` → 'struct', TYPE `enum` → 'enum', else 'class'. The grammar
+  puts the keyword in the `declaration_kind:` anon child (`class`/`struct`/
+  `enum`/`actor`/`extension`) → **actor → 'class', extension → 'class'**.
+- **isAsync (swift.ts:121)** — modifiers text includes 'async'. **Effectively
+  always FALSE** (rust-precedent dead code): the grammar puts `async` as an
+  anon child AFTER the parameter list, never inside `modifiers` (probed:
+  `func asyncy() async throws` → isAsync=false). The walker must reproduce
+  false — note extractFunction/extractMethod store `isAsync: false` (hook
+  present, returns false), NOT undefined.
+- **extractImport (swift.ts:130)** — signature = trimmed full node text;
+  identifier = namedChildren `find(type === 'identifier')` → moduleName = its
+  FULL text (`Foundation`, `UIKit.UIView` — dotted kept). Kinded imports
+  (`import class Darwin.FILE` — anon `class` child) and `@testable import`
+  (modifiers first — find skips it) still find the identifier (probed). No
+  identifier → null → because the hook exists, tree-sitter.ts:3350 fires →
+  nothing emitted (no generic fallback; not constructible in valid swift).
+  `handledRefs` never set → generic `imports` ref {from file node, name =
+  moduleName, line/col of the import node} (tree-sitter.ts:3183-3194). None
+  of the language-gated binding emitters (TS/py/rust/php/ruby, :3197-3234)
+  match swift.
+
+Hooks ABSENT (the walker must NOT do these): `preParse`, `isConst` (**but
+kinds still split let/const — via the swift-specific branches, not the
+hook**), `isExported` (undefined on every node except the file node's literal
+`false` and extractVariable's `?? false`), `isStatic` IS present (above),
+`getReceiverType` (**never diverts extractFunction at :1522; methods happen
+ONLY via isInsideClassLikeNode; no receiver QN override, no :1799
+owner-contains fallback**), `resolveBody`, `recoverMangledName`,
+`isMisparsedFunction`, `classifyMethodNode`, `extractPropertyName`,
+`propertyTypes`, `fieldTypes`, `extraClassNodeTypes`, `packageTypes`/
+`extractPackage` (no namespace node — top-level QNs are bare),
+`extractModifiers`, `synthesizeMembers`, `extractBareCall`, `visitNode` hook,
+`skipBodilessClass` (**but extractStruct/extractEnum have their own bodiless
+skip** — §class-family), `methodsAreTopLevel`, `interfaceKind` (**protocols
+are kind `interface`, NOT `protocol`**), `resolveTypeAliasKind`.
+
+Registration: `EXTRACTORS.swift` (languages/index.ts:54), `FN_REF_SPECS.swift`
+(function-ref.ts:392).
+
+## tree-sitter.ts branches (anchors as of `a6c62d7`)
+
+### visitNode dispatch — what each swift node hits
+
+| Node | Branch | Behavior |
+|---|---|---|
+| `function_declaration` (top level) | functionTypes:994 → extractFunction:1517 | no receiver hook → always extractFunction at file scope |
+| `function_declaration` (inside class-like) | :995 → extractMethod:1737 | methodTypes includes it + isInsideClassLikeNode (nodeStack top kind ∈ class/struct/interface/trait/enum/module, :1486) |
+| `class_declaration` (class/struct/enum/actor/extension — ALL of them) | classTypes:1005 → classify → extractClass:1679 / extractStruct:1869 / extractEnum:1914 | §class-family |
+| `protocol_declaration` | interfaceTypes:1054 → extractInterface:1834 | kind `interface`; §class-family |
+| `typealias_declaration` | typeAliasTypes:1071 → extractTypeAlias:2890 | §Type aliases. Returns false → children ALSO recursed (nothing matches — user_type/function_type have no branches) |
+| `property_declaration` (top level) | variableTypes:1098 (`!isInsideClassLikeNode()`) → extractVariable:2538 swift branch :2851 | §extractVariable. skipChildren=true + scanFnRefSubtree — **top-level initializers are NEVER walked: no calls/instantiates from them** (probed: `var topVar = compute()` emits nothing) |
+| `property_declaration` (inside class-like) | **DEDICATED swift branch :1121-1193** | §Dedicated property branch. The :1098 gate fails first (isClassScopeConstantAssignment:1508 needs node.type `assignment` → false) |
+| `protocol_property_declaration` | same dedicated branch (:1123) | protocol pushed = interface = class-like ✓ |
+| `constant_declaration` | variableTypes | DEAD — node type doesn't exist |
+| `import_declaration` | importTypes:1209 → extractImport:3170 | hook (§Extractor config) |
+| `call_expression` (top-level statement / class-body initializer descent) | callTypes:1248 → extractCall:3684 | §extractCall. skipChildren stays false → nested calls recursed |
+| `init_declaration` / `deinit_declaration` / `subscript_declaration` | **NO branch** | recursed by the generic loop :1295. **No node minted.** Their body statements are visited by visitNode (NOT visitFunctionBody) with the CLASS still on the stack → their calls attribute to the CLASS node (probed: `init { setupMonitor() }` → `calls setupMonitor from=class`; deinit's cleanup, subscript accessor bodies same). Consequences of the visitNode route: fn-ref capture fires (:990) but **extractStaticMemberRef does NOT** (body-walker-only, :5218) — a `Color.red` read inside `init` emits NOTHING |
+| `enum_entry` | (via extractEnum's body loop only) | §class-family |
+| `associatedtype_declaration`, `protocol_function_declaration`, `macro_invocation` (NEW), `directive`, `diagnostic`, `operator_declaration`-family, `precedence_group_declaration` | no branch | recursed, nothing extracted. **Protocol method requirements mint NO nodes** (probed — `func didFinish(_:)`/`static func build()` in a protocol produce nothing; only protocol_property_declarations become nodes via the dedicated branch) |
+| `navigation_expression` (top level / class body) | no branch in visitNode | recursed. Static-member refs come ONLY from the body walker + walkAttrArgs — a top-level `Type.self` statement emits nothing |
+| INSTANTIATION_KINDS (:354) | — | **contains NO swift node types** → `Foo()` is a plain `calls Foo` ref via extractCall; NO instantiates refs exist for swift, ever (probed) |
+
+### Node creation, IDs, order
+
+- createNode (:1308): id = `generateNodeId(filePath, kind, name, startRow+1)`
+  = `` `${kind}:${sha256(`${filePath}:${kind}:${name}:${line}`).hex.slice(0,32)}` ``
+  (tree-sitter-helpers.ts:18). File node id = literal `file:${filePath}`
+  (:509), name = basename, qualifiedName = filePath, endLine =
+  `source.split('\n').length`, isExported false. Dedupe/self-checks compare
+  ID STRINGS (`node_ids` vec pattern) — same-(kind,name,line) collisions are
+  legal.
+- endLine extension via resolveBody (:1329) — no hook → no-op.
+- contains edge from nodeStack top for every created node (:1363).
+- qualifiedName = nodeStack names joined `::` (buildQualifiedName:1447;
+  namespacePrefix always empty outside C/C++). No package node → top-level
+  QN = bare name; members `Session::request`. Extension members ride the
+  extension's class node name (`Builder::done` for `extension KF.Builder`).
+- extractModifiers hook absent → no decorators-from-modifiers merge (:1355
+  no-op); the `decorators` node field stays unset (probed dec=null
+  everywhere) — decorator refs are separate unresolved refs.
+- **Emission order** (pinned by the torture probe, `svy-swift/extract-probe.out`):
+  file node → source-order walk (per construct: node + contains edge → its
+  refs in extractor-internal order) → fn-ref refs (flushFnRefCandidates :538)
+  → value-ref EDGES (flushValueRefs :539, appended to edges LAST). Per
+  class: class node → extends refs (one per inheritance_specifier, source
+  order) → (extractCsharpPrimaryCtorParamRefs no-op — needs `parameter_list`
+  child type, absent) → decorates refs of the class → body members in source
+  order. Per dedicated-branch property: property/field/constant/variable
+  node → decorates (owner) → type-annotation refs (owner) → walkAttrArgs
+  static-member refs (owner) → then (stored only) initializer-descent refs.
+  Per method: node → type-annotation (return-type) refs → decorates → body
+  refs.
+
+### The class family — extractClass / extractStruct / extractEnum / extractInterface
+
+classify → 'struct': **extractStruct (:1869)** — body = `getChildByField(node,
+'body')` (`class_body`); **bodiless → NO node** (:1876; record_declaration
+exemption is C#-only; a bodiless struct isn't valid swift anyway). Node kind
+`struct` {docstring, visibility, isExported:undefined}. extractInheritance →
+§below. **NO extractDecoratorsFor** — `@main struct MyApp` emits NO decorates
+ref (probed; the asymmetry: class YES, struct/enum/interface NO). Body
+namedChildren visited with the struct pushed.
+
+classify → 'enum': **extractEnum (:1914)** — body field (`enum_class_body`);
+bodiless → no node. Kind `enum` {docstring, visibility, isExported:undefined}.
+extractInheritance (the raw-value type rides it — `enum Suit: String` →
+`extends String`; extends refs have NO builtin filter, probed `extends Int`).
+NO decorates. Body loop: `enum_entry` ∈ enumMemberTypes → extractEnumMembers
+(:1958): **`name` field path — `getChildByField(node,'name')` returns the
+FIRST case name → ONE `enum_member` node, positioned at the enum_entry, then
+RETURN.** QUIRKS, PRESERVE (probed): multi-case `case put, delete` → ONLY
+`put` (delete DROPPED — the identifier-scan fallback :1967 is dead for swift
+since the field always exists); associated values (`case success(Data)` —
+`data_contents: enum_type_parameters`) and raw values (`= "H"`) are never
+walked → NO refs from them; `indirect case` same. Non-enum_entry children
+(computed properties, methods, nested types) → visitNode with the enum pushed.
+
+classify → 'class' (incl. actor + extension): **extractClass (:1679)** — no
+skipBodilessClass → a bodiless class_declaration would still mint (not valid
+swift). Kind `class` {docstring, visibility, isExported:undefined}.
+extractInheritance; extractCsharpPrimaryCtorParamRefs no-op;
+**extractDecoratorsFor DOES run** (`@Observable class Model` → `decorates
+Observable`). Push, visit body namedChildren via visitNode, no
+synthesizeMembers, pop. **Extensions:** name via resolveName (multi-segment)
+else raw `name`-field text — `extension Point` → class `Point` (a SECOND
+class node named like the original — same name, different line → distinct
+id); `extension [ServerTrustEvaluating]` → class named literally
+**`[ServerTrustEvaluating]`** (raw sugar text, probed); `extension Array
+where Element: Equatable` → class `Array`, and the `type_constraints` where
+clause emits NOTHING (inheritance_constraint is not an inheritance_specifier).
+
+**extractInterface (:1834)** — protocol_declaration → kind `interface` (no
+interfaceKind hook) {docstring, isExported:undefined — **NO visibility, NO
+decorates**}. extractInheritance on the protocol node (protocol inheritance
+`: AnyObject, Identifiable` → extends refs). Body (`protocol_body`)
+namedChildren visited with the interface pushed: protocol_property_declaration
+→ dedicated branch (§below); protocol_function_declaration /
+init_declaration / associatedtype_declaration → recursed, nothing.
+
+### Inheritance — extractInheritance (:5291), the swift case (:5619-5632)
+
+Direct namedChildren scan of the declaration node; ONLY the
+`inheritance_specifier` case matches swift (one specifier per conformance,
+each `inherits_from: user_type`): userType = child.namedChildren
+`find(type==='user_type')` → typeId = ITS namedChildren
+`find(type==='type_identifier')` — the FIRST — → one **`extends`** ref
+{from class/struct/enum/interface node, name = typeId text, line/col of the
+TYPE IDENTIFIER}. QUIRKS, PRESERVE: **everything is `extends`** — protocol
+conformance, protocol inheritance, raw-value types; swift emits NO
+`implements` refs from extraction (resolution's supertype machinery handles
+both kinds); a qualified base `: Module.Base` takes the FIRST segment
+(`Module`) — the outer namespace, not the type. Where-clause constraints and
+generic bounds (`type_parameter` `name:` bounds) emit nothing. None of the
+other clause cases (extends_clause/base_class_clause/delegation_specifier/…)
+match swift node types.
+
+## THE DEDICATED PROPERTY BRANCH (tree-sitter.ts:1113-1193) — #1020, the port's core
+
+Fires for `property_declaration` | `protocol_property_declaration` when
+`language === 'swift'` AND isInsideClassLikeNode(). Position in the else-if
+ladder: AFTER variableTypes (:1098 — which in-class properties fail, see
+dispatch table), BEFORE importTypes. **Alamofire baseline (re-measured at
+survey HEAD on the production path): 98 files → 3,988 nodes = method 1801 /
+field 588 / class 379 / property 348 / enum_member 230 / import 183 / struct
+111 / file 98 / constant 97 / enum 77 / type_alias 39 / interface 27 /
+function 10; 16,426 refs.** The 348 `property` nodes are exactly this
+branch's computed path — the #1020 number reproduced to the digit.
+
+### swiftPropertyInfo (tree-sitter.ts:277-291) — transcribe exactly
+
+```
+pattern  = childForFieldName('name')
+        ?? namedChildren.find(t === 'value_binding_pattern' || t === 'pattern')
+        ?? null
+binding  = namedChildren.find(t === 'value_binding_pattern')      // DIRECT children only
+isLet    = binding != null && text(binding).trimStart().startsWith('let')
+isComputed = namedChildren.some(t === 'computed_property' || t === 'protocol_property_requirements')
+nameNode = firstSimpleIdentifier(pattern)
+```
+
+`firstSimpleIdentifier` (:261-273): BFS (shift from a queue) over
+namedChildren, **guard: at most 40 nodes popped**; first `simple_identifier`
+wins. Tuple pattern `let (a, b)` → `a` only (one node for the whole
+declaration, PRESERVE).
+
+Shape notes (probed, identical both grammars):
+- class/struct/enum property_declaration: `value_binding_pattern`
+  (mutability let/var) and `name: pattern > bound_identifier:
+  simple_identifier` are SIBLING direct children → binding found → isLet real.
+- **protocol_property_declaration NESTS the value_binding_pattern INSIDE the
+  `name: pattern`** → the direct-children `find` MISSES it → binding null →
+  **isLet always false for protocol requirements** (irrelevant today — they
+  are all computed — but transcribe the miss).
+- `computed_value: computed_property` carries the getter; the code matches by
+  TYPE not field.
+- **`willSet`/`didSet` observers are a `willset_didset_block`, NOT a
+  computed_property** → an observed stored property (`var observed: Int = 0
+  { willSet {…} didSet {…} }`) is **isComputed=false → a `field` node**, and
+  (stored-path descent, below) the observer bodies' calls attribute to the
+  CLASS (probed: `calls prepare` / `calls react` from=class).
+
+### The branch body (:1126-1193), step by step
+
+1. `ownerId = nodeStack top` (the class/struct/enum/interface node).
+2. If nameNode:
+   - **isComputed → `createNode('property', name, node, {visibility: hook,
+     isStatic: hook ?? false})`** — computed properties become `property`
+     nodes (the #1020 "var isCloudProxy: Bool" fix; SwiftUI `var body: some
+     View` is the canonical case). Note extras: NO docstring, NO signature,
+     NO returnType — visibility + isStatic ONLY (probed doc=null on all).
+   - else **stored: `isStatic = hook ?? false`; kind = isStatic ? (isLet ?
+     'constant' : 'variable') : 'field'`** — `static let` → `constant`,
+     `static var` → `variable` (`class var` → isStatic via 'class' substring
+     → `variable`), instance stored (let OR var) → `field`. Same extras.
+   - Node position = the property_declaration (multi-line observed props span
+     the block). nameNode null (no simple_identifier in pattern) → NO node,
+     but steps 3-5 still run.
+3. If ownerId: `extractDecoratorsFor(node, ownerId)` — **refs attach to the
+   ENCLOSING TYPE, not the property** (probed: `@Published private var wrapped`
+   → `decorates Published from=class:Session`). §Decorators for mechanics.
+4. If ownerId: `extractVariableTypeAnnotation(node, ownerId)` (:6074) — finds
+   the direct `type_annotation` child → extractTypeRefsFromSubtree (:6090) →
+   one `references` ref per `type_identifier` leaf not in BUILTIN_TYPES,
+   **from the OWNER** (probed: `let rootQueue: DispatchQueue` → `references
+   DispatchQueue from=class` at the leaf's position). BUILTIN_TYPES nuances
+   (:5768): `Int`/`String`/`Double`/`Float`/`Boolean`/`Long`/`Char` are
+   suppressed (Scala rows), **`Bool` is NOT** (→ real `references Bool`
+   noise ref, probed), `Void`/`Character`/`Self` NOT. Wrappers recurse:
+   `[Tag]` → `Tag`; `Session?` → `Session`; `some View` (opaque_type) →
+   `View`.
+5. If ownerId: **walkAttrArgs** (:1165-1175) — find the `modifiers` direct
+   child; if present, recursively run `extractStaticMemberRef(n)` over EVERY
+   node of the modifiers subtree. This is the Vapor `@Siblings(through:
+   PivotModel.self, from: \.$left, …)` mechanism: the attribute-argument
+   `navigation_expression` with target `simple_identifier PivotModel` +
+   suffix `.self` → `references PivotModel` from the OWNER at the RECEIVER's
+   position. Keypath args (`\.$left` — target is `key_path_expression`) and
+   the wrapper's own `user_type` are self-filtered (not MEMBER_ACCESS_TYPES /
+   not an accepted recv type). §Static-member refs for the shared mechanics.
+6. Computed only: getter = namedChildren find(`computed_property` |
+   `protocol_property_requirements`); if found: push property id,
+   `visitFunctionBody(getter, '')`, pop; **skipChildren = true**. The
+   getter's calls/reads attribute to the PROPERTY node (probed: `calls check
+   from=property:isCloudProxy`; SwiftUI body's `VStack`/`Text` subtree
+   becomes the property's callees). protocol_property_requirements contains
+   only getter/setter specifiers → walks emit nothing.
+7. **Stored: skipChildren stays FALSE** → the generic loop (:1295) descends
+   into the property_declaration's children via visitNode: the initializer's
+   call_expressions hit callTypes → **`calls` refs from the CLASS** (`let
+   stored = Session()` → `calls Session from=class`; `lazy var expensive =
+   Cache.build()` → `calls Cache.build from=class`); willset/didset bodies
+   likewise; the modifiers subtree is re-walked harmlessly (attributes have
+   no visitNode branches; walkAttrArgs already emitted the static refs —
+   different ref kinds, no dupes). Value-position navigation reads (`let m =
+   Suit.hearts` as an in-class initializer) emit NOTHING here (no
+   static-member call in visitNode) — contrast bodies.
+8. fn-ref capture: maybeCaptureFnRefs ran at :990 BEFORE the branch —
+   property_declaration ∈ SWIFT_SPEC dispatch (varinit field `value`) → a
+   stored `let cb: Handler = onFire` captures `onFire`; computed properties
+   have no `value` field → nothing.
+
+## extractVariable — the swift top-level branch (tree-sitter.ts:2851-2862)
+
+Top-level property_declaration only (dispatch table). `swiftPropertyInfo`
+again: nameNode && !isComputed → `createNode(isLet ? 'constant' : 'variable',
+name, NODE, {docstring, isExported})` — **position = the whole declaration**,
+extras: docstring (getPrecedingDocstring — top-level consts DO get
+docstrings, unlike the in-class branch), isExported = `?? false` → literal
+**false** (:2549; probed exp=false — the only swift nodes besides the file
+node with a non-undefined isExported), **NO signature** (unlike the TS/Go
+branches — no initializer capture), no visibility/isStatic. Computed
+top-level (`var topComputed: Int { 9 }`) → **skipped entirely** (no node).
+One node per declaration — tuple `let (a, b) = …` → `a` only. The :1098
+dispatch sets skipChildren=true + scanFnRefSubtree (capture-only, halts at
+functionTypes + `lambda_literal` :610) → **top-level initializers emit NO
+calls/instantiates refs** (probed: `var topVar: Int = compute()` → nothing
+but the node; no type-annotation refs either — extractVariableTypeAnnotation
+is NOT called on this path, PRESERVE the asymmetry with the in-class branch).
+
+## extractFunction / extractMethod for swift (:1517 / :1737)
+
+- extractFunction: no getReceiverType → never diverts. Name via extractName
+  (:90): resolveName (class_declaration-gated → undefined for functions) →
+  nameField `name` → the simple_identifier. **Operator functions:** `func <+>
+  (lhs:…)` has `name: custom_operator` → extractName returns the raw operator
+  text → a function node named `<+>` (probed clean-parse on both arms).
+  `<anonymous>` never occurs (grammar requires a name; the arrow/function_
+  expression paths are TS-only).
+- Node extras: docstring (§Docstrings), signature **undefined** (dead hook),
+  visibility (hook), isExported undefined, **isAsync false** (dead hook),
+  isStatic (hook — real for `static`/`class` modifiers), returnType (hook).
+- Then extractTypeAnnotations (:1594) — §Type-annotation refs: **return-type
+  refs ONLY**. Then extractDecoratorsFor (:1599) — attributes inside
+  `modifiers` → real decorates refs (`@objc func attributed` → `decorates
+  objc`, position = the attribute node). Push, walk `body` field
+  (function_body) via visitFunctionBody, pop.
+- extractMethod (function_declaration inside class-like): gate :1747 passes
+  via isInsideClassLikeNode. Same extras (visibility/isAsync-false/isStatic/
+  returnType, signature undefined). receiverType undefined → no QN override
+  (:1790 skipped), no owner-contains fallback (:1799). Bodiless is
+  impossible for function_declaration in valid swift (protocol requirements
+  are a different node type, unhandled).
+- **Nested named function in a body** → visitFunctionBody:5245 →
+  functionTypes + named → extractFunction → `function` node contained by the
+  enclosing method/function (its QN prefixes the enclosing chain via
+  nodeStack). Closures (`lambda_literal`) are NOT functionTypes → no nodes;
+  their calls attribute to the enclosing symbol (probed: `rootQueue.async {
+  self.perform(req) }` → `calls perform from=method:request`).
+- Body-level class/struct/enum/protocol declarations → visitForCallsAndStructure
+  :5255-5275 dispatches classTypes (with classify)/interfaceTypes → full
+  extraction, contained by the enclosing function.
+
+## Type aliases — extractTypeAlias (:2890)
+
+typealias_declaration → name field (alias name). No resolveTypeAliasKind →
+plain `type_alias` node {docstring, isExported:undefined}. swift ∈
+TYPE_ANNOTATION_LANGUAGES → value = `getChildByField(node,'value')` — **the
+multi-field lookup WORKS** (table above) → extractTypeRefsFromSubtree over
+the aliased type → refs per type_identifier leaf (probed: `typealias Handler
+= (Data) -> Void` → `references Data` + `references Void`; `typealias
+BuilderAlias = KF.Builder` → refs `KF` AND `Builder` — one per segment).
+The TS-only member extraction (:2983) is gated away. Returns false →
+children recursed (nothing matches).
+
+## extractCall (:3684) — the swift paths
+
+Swift never hits the vbnet/erlang/objc/php/java branches (no name+object
+fields on call_expression). Generic else :4312: `func =
+childForFieldName('function') ?? namedChild(0)` — swift has NO function field
+→ **namedChild(0) always**. cpp operator recovery (:4324) is cpp-gated. The
+full observed matrix (every row probed via the built extractor):
+
+| Call shape | func node | Path | Emitted ref |
+|---|---|---|---|
+| `helper()` | simple_identifier | else :4518 raw text | `calls helper` |
+| `Foo()` | simple_identifier | same | `calls Foo` (constructor = plain call; NO instantiates, ever) |
+| `Foo.init(raw:)` | navigation_expression | :4364 member branch | `calls Foo.init` |
+| `obj.method(1)` | navigation_expression | member | `calls obj.method` |
+| `self.own()` | nav, target `self_expression` | receiver not identifier-typed → fallthrough | `calls own` (bare — swift `self` is its own node TYPE; SKIP_RECEIVERS:4400 {self,this,cls,super} is reached only for identifier-typed receivers, so the same net effect arrives via a different path than kotlin) |
+| `super.parent()` | nav, target `super_expression` | same | `calls parent` |
+| `a.b.deep()` | nav, target inner nav | fallthrough | `calls deep` (bare — 2-hop receivers drop) |
+| `x?.optCall()` | nav, targets [simple_identifier x, anon `?`] | receiver = namedChild(0) = `x` | `calls x.optCall` (**optional chaining keeps the receiver** — the `?` is a second anon `target:` child, invisible to namedChild) |
+| `y!.forced()` | nav, target postfix_expression | fallthrough | `calls forced` |
+| `Foo.make().draw()` | nav, target call_expression | **#750 re-encode :4408-4442**: swift-gated; innerNav = receiver.namedChild(0) (`Foo.make` nav or bare identifier), text ws-stripped; `/^[A-Z]/` gate | `calls Foo.make().draw` + (recursion) `calls Foo.make` |
+| `foo.bar().baz()` | same | innerCallee `foo.bar` lowercase → reencode false | `calls baz` + `calls foo.bar` |
+| `"lit".upper()` | nav, target line_string_literal | LITERAL_RECEIVER_TYPES :4397 (`line_string_literal` ∈ set :377) | **NOTHING** |
+| `"""…""".trimmed()` | nav, target multi_line_string_literal | **NOT in the literal set** | `calls trimmed` (bare) — PRESERVE |
+| `5.times()` / `[1,2].reduce(…)` | integer_literal / array_literal | literal set | NOTHING |
+| `["k":1].lookup()` | dictionary_literal | **NOT in the set** (`dictionary` is, `dictionary_literal` isn't) | `calls lookup` — PRESERVE |
+| `(freestanding)()` | tuple_expression | else raw text `(freestanding)` → **parenthesized-conversion regex :4530 FIRES** | `calls freestanding` |
+| `closureTaking { }` / multi-trailing | simple_identifier (lambda in call_suffix) | else | `calls closureTaking` |
+| `arr[0]` (subscript READ) | simple_identifier (bracket call_suffix) | else | **`calls arr`** — every subscript access parses as call_expression; huge-volume quirk, PRESERVE |
+| `m[i][j]` | inner call_expression | receiver is call but func = call_expression not nav → else raw text | **`calls m[i]`** (garbage, PRESERVE) |
+| `self.items[k] = 1` | nav self.items | member, self receiver | `calls items` |
+| `defer { cleanup() }` | simple_identifier `defer` | else | **`calls defer`** + inner `calls cleanup` — defer parses as a trailing-closure call (both arms, probed) |
+| `try f()` / `try? f()` / `await f()` | (wrapped in try_expression/await_expression) | recursion reaches the inner call_expression | normal ref, position = the CALL node (after the keyword) |
+| `"count \(counter.next())"` | (interpolated_expression inside the literal) | recursion descends into string literals | `calls counter.next` — interpolation calls DO emit |
+| `.make()` (implicit member call) | prefix_expression | else raw text | **`calls .make`** (leading dot, unresolvable — PRESERVE) |
+| macro `#Preview { ContentView() }` (NEW) | (macro_invocation unhandled) | recursion | inner `calls ContentView` from the enclosing scope |
+
+Post-processing: parenthesized-conversion (:4529) as noted; template-strip
+(:4542) and cpp fn-ptr fan-out (:4556) are c/cpp-gated. Final ref: {callerId
+= nodeStack top, name, line = call startRow+1, column = call startColumn
+(UTF-16)}. Inner nav/call children are ALSO visited after extractCall
+(callTypes doesn't skipChildren; body walker recurses) — chains emit inner
+refs, and the callee-position navigation_expression is exempted from
+static-member refs by the :4771 callee-of-call check.
+
+## Static-member / value-read refs (:4750-4808) — swift IS in STATIC_MEMBER_LANGS (:345)
+
+Called from the body walker ONLY (:5218) plus the property branch's
+walkAttrArgs. `navigation_expression` ∈ MEMBER_ACCESS_TYPES (:326). Mechanics
+for swift: callee-of-call skip (:4771-4779 — parent ∈ callTypes and
+parent.namedChild(0).startIndex === node.startIndex → the callee nav of a
+call emits nothing); recv = object/expression/scope fields (all null for
+swift — nav's fields are `target`/`suffix`) → **namedChild(0)** = the target.
+Accepted recv types (:4791): identifier/type_identifier/simple_identifier/
+name/scoped_type_identifier — swift targets are `simple_identifier` →
+capitalized regex `^[A-Z][A-Za-z0-9_]*$` → `references <target>` from the
+enclosing symbol at the RECEIVER's position. Probed rows: `Color.red` →
+`references Color`; `Suit.hearts.rawValue` → outer nav recv = inner nav →
+nothing, inner nav → `references Suit`; `UserModel.self` → `references
+UserModel`; `Deep.Nested.leaf` → `references Deep` (inner nav only);
+`.implicitMember` → prefix_expression, not a nav → NOTHING; `lowercase.field`
+→ nothing; keypath `\Foo.bar` → recv = key_path_expression → nothing (the
+type_identifier inside the keypath is never read). Remember the visitNode
+gap: class-level initializer reads and init/deinit/subscript bodies emit NO
+static-member refs (dispatch table).
+
+## Type-annotation references — swift ∈ TYPE_ANNOTATION_LANGUAGES (:5753)
+
+For every function/method (extractTypeAnnotations :5788, called at
+:1594/:1816): params = `getChildByField(node, 'parameter')` → **NULL → NO
+parameter type refs, ever** (probed: `@escaping (Int) -> Void` param emits
+nothing — `Void` would have shown). returnType = `getChildByField(node,
+'return_type')` → **WORKS** (field table) → extractTypeRefsFromSubtree over
+the return node → one `references` per type_identifier leaf not in
+BUILTIN_TYPES: `-> DataRequest` → ref; `-> KF.Builder` → refs `KF` AND
+`Builder`; `-> Session?` → `Session`; `-> Result<Foo, Err>` → `Result`,
+`Foo`, `Err` (leaves); builtins suppressed per the §property-branch nuance
+list (`Bool` NOT suppressed). The trailing direct `type_annotation` find
+(:5873) is null for function_declarations. extractVariableTypeAnnotation
+(:6074): the dedicated property branch (owner-attributed) and the body
+walker's `variable_declarator` gate (:5230-5236 — **dead for swift**, no such
+node type; body-local `let x: Foo` property_declarations emit NO type refs —
+PRESERVE). property_signature/method_signature (:1282) — TS-only types.
+
+## Decorators — extractDecoratorsFor (:4897), swift = `attribute` nodes
+
+consider() (:4898) accepts type `attribute`; the target scan (:4937) finds
+the attribute's first namedChild of accepted types — **swift attribute →
+`user_type`** (:4950) → name = user_type text, strip `<…>` generic suffix,
+strip to last `.`/`::` segment, trim → `decorates` ref {from the decorated
+node id (or the OWNER for the property branch), name, line/col of the
+ATTRIBUTE node}. Attributes live INSIDE the declaration's `modifiers` child →
+found via the modifiers descent (:4983); the preceding-sibling scan (:5013)
+finds nothing for swift (attributes are never siblings) and stops at the
+first non-decorator sibling. Coverage matrix (probed): function ✓ (`@objc` →
+`decorates objc`), method ✓, class ✓ (`@Observable`), dedicated-branch
+property ✓ (owner-attributed) — **struct/enum/interface ✗ NO decorates**
+(extractStruct/extractEnum/extractInterface never call it — `@main struct`,
+`@objc enum`, attributed protocols emit nothing, probed). Attribute ARGUMENTS
+are never walked here — only walkAttrArgs (property branch) reads them, and
+only for static-member shapes. Parameter attributes (`@escaping` etc. inside
+`parameter`/`parameter_modifiers`) are never reached by any pass.
+
+## Docstrings (tree-sitter-helpers.ts:95)
+
+`///` doc lines AND `//` plain comments are node type `comment` — accepted by
+the sibling scan; consecutive runs accumulate (unshift → source order) and
+`//` vs `///` are NOT distinguished (a plain comment right above a decl IS
+its docstring). **`/** */` and `/* */` block comments are `multiline_comment`
+— NOT in the accepted set (comment/line_comment/block_comment/
+documentation_comment) → block docs are IGNORED and BREAK the accumulation
+chain** (probed: `/** block doc */ func blockDoc` → doc=null). PRESERVE.
+DOCSTRING_WRAPPER_TYPES: no swift wrappers → no climbing. Attributes do NOT
+break the chain (they're inside the declaration node — `/// doc` +
+`@objc func` keeps its doc, probed). cleanCommentMarkers (:77): swift hits
+`^\/\/[/!]?\s?` (gm) and — only if a multiline_comment ever reached it, which
+it can't — the block strips. **The `gm` per-line strips are the ONLY
+CRLF-sensitive regexes on the swift path (#1329): use `js_multiline_strip`
+in docstring.rs.** Docstrings attach to: functions, methods, classes
+(incl. extensions/actors), structs, enums, interfaces, top-level
+constants/variables (extractVariable), type_aliases. NOT to: dedicated-branch
+properties/fields/constants (extras carry no docstring — probed), enum
+members, import nodes.
+
+## Value-reference edges (:398-931) — swift IS in VALUE_REF_LANGS (:401)
+
+Port the full machinery (crib go.rs/java.rs): `CODEGRAPH_VALUE_REFS=0` kill;
+MAX_VALUE_REF_NODES = 20,000 caps the prune DFS and each reader scan;
+isGeneratedFile skip.
+
+- **Targets** (captureValueRefScope :735): kind constant|variable, name ≥3
+  chars AND `/[A-Z_]/`, parent id prefix ∈ {file:, class:, module:,
+  **struct:, enum:**} — the struct:/enum: rows exist FOR swift's
+  static-let-namespacing idiom (`enum Constants { static let X }`, comment at
+  :748-750). Swift targets: top-level `let`/`var` (under file:) and in-class
+  `static let`/`static var` (constant/variable under class:/struct:/enum:).
+  `interface:` NOT accepted → protocol statics (hypothetical) excluded.
+  Instance `field`s and computed `property`s are NOT targets.
+- **Reader scopes** (:764): function/method/constant/variable nodes —
+  **`property` and `field` are NOT readers** (probed: `isCloudProxy`'s read
+  of SHARED_MAX emits no edge; `method request`'s does).
+- **Shadow prune** (:803-878): the declarator switch's
+  **`property_declaration` case (:856-869) RESOLVES for swift** (unlike
+  php): vd = find `variable_declaration` (Kotlin) — absent for swift → else
+  `firstSimpleIdentifier(childForField('name') ?? find(value_binding_pattern
+  | pattern))` — the swift shape → bump() counts it when the text is a
+  target name (bump accepts identifier|simple_identifier :807). EVERY
+  property_declaration in the tree bumps — targets' own declarations, body
+  locals (`let SHARED_MAX = 5` in a method — the shadow source), and
+  same-named instance properties. `declCount > fileScopeCount` → target
+  deleted. **guard-let/if-let bindings do NOT bump** (guard_statement/
+  if_statement hold bound_identifier directly, no property_declaration —
+  probed) → a `guard let X = …` shadow does NOT prune. PRESERVE both sides.
+- **Emission** (:880-930): per reader scope DFS (bodies are children — the
+  Dart/Pascal sibling pull :891 is inert); match node type
+  `simple_identifier` (:908 — swift's every name reference) whose text maps
+  to a target, target ≠ self, name ≠ scope's own name, dedupe per
+  (scope,target) → EDGE {source: scopeId, target, kind:'references',
+  metadata:{valueRef:true}}, appended AFTER all other edges. Because every
+  `simple_identifier` matches — nav suffixes included — `Session.SHARED_MAX`
+  AND a bare `SHARED_MAX` both emit (probed: the single torture valueRef
+  edge method:request → constant:SHARED_MAX).
+
+## Function-as-value capture (#756) — SWIFT_SPEC (function-ref.ts:288)
+
+```
+idTypes:  { simple_identifier }
+dispatch: value_arguments        → args
+          assignment             → rhs   (field 'result')
+          array_literal          → list
+          property_declaration   → varinit (field 'value')
+layers:   value_argument → 'value'        // FIELD 'value', not null
+special:  { selector_expression }
+```
+No unwrap/ungatedModes/addressOfOnly. Mechanics (function-ref.ts:408-597 +
+:685-696), all probed:
+
+- **args/list**: every namedChild → normalizeValue. Bare `simple_identifier`
+  → candidate (NAME_STOPLIST drops self/true/nil/…). `reg2(onFire)` →
+  `onFire` ✓; `[cbA, cbB]` → both ✓ (the array_literal's OWN dispatch —
+  varinit's normalizeValue over an array_literal yields [], the capture
+  happens when the walker/scan VISITS the array node).
+- **value_argument layer** (:547-557): the **label-forward skip** — label =
+  childForField('name') (a `value_argument_label`), value =
+  childForField('value') ?? last namedChild; label text === value text →
+  DROPPED (`forward(value: value)` → nothing; the Alamofire A/B finding).
+  Else descend the `value` field.
+- **rhs** (assignment, field `result`): param-storage skip — lhs =
+  left/lhs/**target** fields ?? namedChild(0) (swift's field is `target`, a
+  directly_assignable_expression); lhs text's last identifier === rhs text →
+  skip (`self.raw = raw` → nothing, probed). `o.cb = handler` → `handler` ✓.
+- **varinit** (property_declaration, field `value`): destructuring check
+  reads childForField('name') — swift's is type `pattern`, NOT in
+  {object_pattern, array_pattern, tuple_pattern, struct_pattern} → never
+  skipped; tuple-let values are non-normalizable expressions anyway.
+  Computed properties have no `value` field → nothing.
+- **selector_expression special** (:685-696): namedChild(0) is
+  identifier/simple_identifier → its text (`#selector(fire)` → `fire`); else
+  rightmost simple_identifier descendant (`#selector(Holder.fire)` → `fire`;
+  QUIRK: `#selector(onNote(_:))` → the rightmost simple_identifier is the
+  argument label `_` → candidate `_`, dropped by the gate in practice); else
+  trimmed inner text. explicitRef=true (not an idType).
+- Capture fires from visitNode:990 (incl. BEFORE the dedicated property
+  branch and on init/deinit/subscript descents), visitFunctionBody:5137, and
+  scanFnRefSubtree (top-level initializer scan; halt list :606-612 includes
+  `lambda_literal` — closures halt the scan — and functionTypes).
+- **Flush gate** (:639): definedHere (same-file function/method names) ∪
+  importedNames. Swift import refs are module names — `Foundation` (SIMPLE)
+  and `UIKit.UIView` (QUALIFIED_IMPORT → last segment `UIView`) both land in
+  importedNames but almost never match a candidate → **the swift gate is
+  effectively "defined in this file"** (probed: `#selector(Holder.fire)`'s
+  `fire` dropped; `onFire`/`handler`/`cbA`/`cbB` survive as same-file
+  functions). Survivors dedupe on `${fromNodeId}|${name}` →
+  {referenceKind:'function_ref'}, appended after all walk refs.
+
+## Closure-collection pass & other synthesis consumers (no port — pin the contract)
+
+- **swift IS in CC_LANGUAGES** (resolution/callback-synthesizer.ts:77, with
+  kotlin — the #1235 gate). The pass re-reads **function/method nodes'
+  filePath + startLine..endLine source slices** (callback-synthesizer.ts:270-
+  297) and regexes for `.forEach`/`.append(`/`.add(`/`.push(`/`.insert(`
+  dispatch/registration pairs. **Input contract on the walker: node kinds,
+  languages, and LINE EXTENTS byte-match** — a wrong endLine silently changes
+  synthesis. The dump gate covers this; nothing extra to build.
+- swift-objc bridge (`swiftObjcBridgeResolver`) + the `sendEvent` scan
+  (callback-synthesizer.ts:1387-1438) read raw `.swift` file text — file-set
+  contract only.
+- The :811 and :3507 language gates consume node kinds/names — covered by
+  parity.
+
+## Frameworks (stay TS-side — behavior pinned in §Architecture #2)
+
+swiftUIResolver / uikitResolver / vaporResolver (resolution/frameworks/
+swift.ts:11/:136/:269): extract() hooks emit `component`/`class`/`route`
+nodes with LITERAL ids (`view:${filePath}:${name}:${line}`,
+`route:${filePath}:${line}:${METHOD}:${path}` — NOT hashed) + vapor handler
+refs (which DO carry filePath+language — framework refs, not extraction
+refs). resolve() consumes plain `references` names by suffix + kind + dir
+conventions (`getNodesByName` — walker's node names/kinds are the contract).
+Vapor's `@Siblings` metatype refs (§property branch step 5) are the
+extraction-side feed that keeps pivot models un-orphaned on Fluent repos.
+
+## Parity mechanics (all have bitten before)
+
+- **Emission order** per §Node creation — file → source-order walk → fn-refs
+  → value-ref edges. Refs interleave with nodes exactly as the TS call sites
+  do: extends before members; per-property decorates → type-refs → attr-arg
+  refs → initializer-descent refs; per-function/method the return-type refs
+  come FIRST (extractTypeAnnotations :1594/:1816 runs before
+  extractDecoratorsFor :1599/:1819), then decorates, then body refs.
+- **generateNodeId inputs**: (filePath, kind, name, startRow+1) — name is the
+  bare bound identifier for properties (no pattern text), the LAST segment
+  for multi-segment extensions, raw sugar text (`[ServerTrustEvaluating]`)
+  for sugar extensions, `<+>` for operator functions, the full dotted module
+  (`UIKit.UIView`) for import nodes; line = declaration start (= the
+  `modifiers`/attribute start when attributes precede the keyword — the
+  declaration node INCLUDES its modifiers).
+- **UTF-16 columns + slices** (textutil::col16/slice_utf16): every ref/node
+  column, `startIndex/endIndex` substrings (getNodeText — getVisibility/
+  isStatic read `child.text`, same substring), and the `.slice(0,100)`-class
+  truncations (unused on the swift path — no signature/initializer capture).
+  Swift sources are emoji/CJK-heavy in tests — the torture fixture needs a
+  non-ASCII line before a symbol.
+- **CRLF**: the ONLY multiline regexes on the swift path are
+  cleanCommentMarkers' `gm` strips (§Docstrings) → `js_multiline_strip` in
+  docstring.rs. `trimStart()`/`startsWith('let')` in swiftPropertyInfo are
+  whitespace-semantics-identical in Rust (`trim_start`). CRLF fixture
+  variants derived in-memory, per the tsjs pattern.
+- **Defer policy**: per-file `has_error()` → `defer:`; **expected incidence
+  9–27% (§table); sweep with `--max-deferral 0.3`** — the c/cpp-style
+  exemption, justified by both-arm measurement, NOT a walker allowance: a
+  JUMP vs the table is a walker bug.
+- Refs carry NO filePath/language (§Architecture #4 — REF_FLAG_FILE_PATH
+  unused); the wire contract is exactly extractFromSource's return.
+- No POST_PASS; no preParse; `sourceIsPreParsed` never set for swift.
+
+## Gates (per plan §5, no exceptions)
+
+- **Grammar bump lands FIRST, standalone** (php pattern): vendor wasm +
+  `=0.7.3` crate pin + VENDORED_WASM_LANGS + kernel-grammar-parity
+  `GRAMMAR_LANGUAGES += 'swift'` in one change, full suite green, before any
+  walker exists. Old-wasm vs new-wasm **full-init dump diff on all three gate
+  repos** (`scripts/dump-graph.mjs`, cmp): expected NON-empty; **every
+  diffing file must be in the union of the two arms' hasError lists**
+  (regenerate with `svy-swift/error-incidence.cjs`; categories in
+  §Grammar-bump deltas — macro/testing/package/typed-throws fixes, the three
+  NEW-regression classes, both-arm recovery drift); prove residual
+  resolution ripple mechanically by parked-ref↔edge pairing (php's
+  ripple-proof pattern). Any clean-on-both-arms file diffing blocks the bump.
+- **Torture fixtures** per `## Fixtures to build` (+ CRLF variants derived
+  in-memory), exercised by the new parity suite.
+- **Parity sweeps** (`scripts/kernel-parity.mjs <dir>`, order-sensitive
+  full-object, **`--max-deferral 0.3`**):
+  - `…/scratchpad/gate-repos/Alamofire` (small, 98 files — **the mandatory
+    #1020 gate**, decoded path)
+  - `…/scratchpad/gate-repos/vapor` (medium, 247 files, decoded path,
+    Fluent/@Siblings + route shapes)
+  - `…/scratchpad/gate-repos/swift-nio` (large, 554 files, **raw-buffers
+    path** — no framework detects)
+  (all three cloned at survey time; re-clone fresh if gone). Then **full-init
+  dump-diffs byte-identical** (kernel arm vs `CODEGRAPH_KERNEL=0`,
+  dump-graph.mjs, cmp) on the same three.
+- **Alamofire #1020 spot-check** (belt to the dump gate's suspenders): after
+  a kernel-arm index, node-kind census must match the wasm arm EXACTLY —
+  survey baseline at `a6c62d7` + current Alamofire HEAD: **property = 348**
+  (the #1020 number, re-measured), field 588, method 1801, class 379
+  (extensions!), constant 97, struct 111, enum 77, enum_member 230,
+  interface 27, type_alias 39, function 10, import 183 (98 files, 3,988
+  nodes, 16,426 refs — `svy-swift/count-props.cjs`). A missing dedicated
+  branch shows up here as property→0 / field→0 before the dump diff even
+  runs.
+- **Suite**: new `__tests__/kernel-swift-parity.test.ts` — torture + CRLF
+  variants + an intentionally-erroring defer fixture (use a NEW-only
+  regression construct — e.g. `#if DEBUG` between enum cases — asserting the
+  kernel defers and wasm output is served); full suite ×2 green with
+  `CODEGRAPH_KERNEL_EXPECT=1`.
+- **`DEFAULT_ROUTED += 'swift'`** (kernel/index.ts:37) only after ALL of the
+  above; changelog rides the existing kernel entry.
+- Post-route sanity: §Architecture #2 — Alamofire/vapor ride the decoded
+  path, swift-nio the raw transport; measure the parse loop accordingly.
+
+## Fixtures to build
+
+**`torture.swift`** (seed: the survey's `svy-swift/torture.swift`, already
+validated against the built extractor — extend it with the rows below),
+**a CRLF variant derived in-memory**, **one defer fixture** (a NEW-only
+regression construct, above), and keep every line traceable to a branch:
+
+- class family: plain class; `public final class C: Base, Proto1, Proto2`
+  (extends ×3, all 'extends'); struct + bodiless-skip N/A; enum backed
+  (`: String` → extends String) + `case a = "H"` + **multi-case `case put,
+  delete` (ONLY `put` minted)** + associated values (no refs) + indirect +
+  computed property + static method in enum; actor (→ class); `extension
+  Point: Hashable` (second class node, extends); `extension KF.Builder`
+  (resolveName → `Builder`); `extension Array where Element: Equatable`
+  (class `Array`, NO constraint refs); **`extension [Proto]` (class named
+  `[Proto]`)**; protocol with inheritance + var requirement {get set} (→
+  property node, isLet=false via the nested-binding miss) + static var
+  requirement + **func/init/associatedtype requirements (NO nodes)**.
+- the dedicated branch, exhaustively: stored let with type+initializer
+  (field + owner type-ref + owner-attributed initializer call); untyped var;
+  `static let SHARED_MAX` (constant, value-ref target); `static var`
+  (variable); `class var` computed (property, isStatic via 'class');
+  `lazy var x: Cache = Cache.build()` (field + `calls Cache.build`
+  from class); `public private(set)` (→ 'public'); `@Published private var`
+  (decorates from OWNER + vis 'private'); **`@Siblings(through: Pivot.self,
+  from: \.$left) var siblings: [Tag]`** (decorates Siblings + walkAttrArgs
+  `references Pivot` + type-ref `Tag`, all from OWNER); computed
+  `var isCloudProxy: Bool { check() }` (property + `calls check` FROM the
+  property + `references Bool` — Bool not builtin); **SwiftUI-shaped
+  `var body: some View { VStack { Text(label) } }`** (property; VStack/Text
+  calls from the property; `references View`); observed `var x = 0 {
+  willSet{prepare(newValue)} didSet{react(oldValue)} }` (**field**, observer
+  calls from the CLASS); `weak var delegate: SessionDelegate?`; tuple
+  `let (a, b) = makePair()` (ONE node `a`, `calls makePair` from class);
+  `open` visibility (→ 'internal').
+- functions/methods: free func with params+return (`sig=null`, return-type
+  ref, NO param refs — include a `Void`-bearing param type to prove the
+  negative); `-> KF.Builder` (returnType 'Builder' + refs KF AND Builder);
+  `-> Result<Foo, Err>` (returnType 'Result'); `-> Result<Vec<Foo>, E>`-shaped
+  nested generic (returnType undefined); `-> Foo?` / `-> [Foo]` / `-> (A,B)`
+  / `-> (Int) -> Foo` / `-> Void`; `func f() async throws` (isAsync FALSE);
+  `static func` / `open class func` (isStatic true, vis 'internal');
+  operator `func <+> (lhs:…)` (node named `<+>`); nested named func in a
+  body; `@objc func` (decorates objc); `@main struct` + `@objc enum` +
+  attributed protocol (NO decorates).
+- init/deinit/subscript: `init(raw:) { self.raw = raw; setupMonitor() }`
+  (no node; `calls setupMonitor` from class; NO fn-ref for the param-storage
+  assignment); `convenience init?` (`calls init` bare from class); `deinit`;
+  subscript with get/set (`calls store` ×2 from class).
+- calls: every row of the §extractCall matrix, verbatim — bare, `Foo()`,
+  `Foo.init()`, member, self/super (bare), 2-hop (bare), `x?.m()`
+  (receiver kept), `y!.m()` (bare), `Foo.make().draw()` re-encode + inner,
+  lowercase chain (bare + inner), `"lit".upper()` (nothing),
+  `"""m""".t()` (bare t), `5.times()` / `[1,2].reduce` (nothing),
+  `["k":1].lookup()` (bare), `(f)()` (conv-regex → f), trailing + multi-
+  trailing closures, `arr[0]` (`calls arr`), `m[i][j]` (`calls m[i]`),
+  `defer {}` (`calls defer`), try/try?/await wraps, string-interpolation
+  call, implicit-member call `.make()` (`calls .make`), `Task { await f() }`.
+- static-member reads (in a BODY): `Color.red`, `Suit.hearts.rawValue`
+  (`Suit` only), `UserModel.self`, `Deep.Nested.leaf` (`Deep` only),
+  `.implicit` (nothing), `lowercase.f` (nothing), keypath `\Foo.bar`
+  (nothing) — plus the SAME reads at class-initializer scope and inside
+  `init` (nothing — the visitNode gap).
+- value refs: `static let SHARED_MAX` in a class/struct/enum read from a
+  method (edge) and from a computed property (NO edge — property not a
+  reader); a method-local `let SHARED_MAX = 5` in a SECOND fixture variant
+  (prune kills the target); a `guard let SHARED_MAX` variant (NO prune);
+  top-level `let TOP_LEVEL_MAX` + reader.
+- fn-refs: `reg(onFire)`, `reg(cb: onFire)` (label≠value → survives),
+  `forward(value: value)` (label-forward skip), `o.cb = handler`,
+  `self.x = x` (param-storage skip), `let table = [cbA, cbB]`,
+  `#selector(fire)` / `#selector(Holder.fire)` / `#selector(onNote(_:))`
+  (the `_` gate-drop), an undefined-name arg (gate-drop).
+- imports: `import Foundation`, dotted `import UIKit.UIView`, kinded
+  `import class Darwin.FILE`, `@testable import`.
+- typealias: function-type value (refs Data+Void) + member-type value
+  (refs KF+Builder).
+- docstrings: `///` run (joined), `//` plain (IS a docstring), `/** */`
+  (**ignored**), `/* */` between `///` and decl (breaks chain), doc over an
+  attributed decl (kept), doc on top-level const (kept) vs in-class property
+  (dropped).
+- misc: `#if os(iOS)` around members (both branches extracted), `#warning`,
+  a macro_invocation file (NEW arm; on OLD this file is a defer candidate —
+  keep it in the DEFER fixture instead), a non-ASCII (UTF-16) line before a
+  symbol, guard/if-let/for-await bodies with calls.
+
+## Probe artifacts (session scratchpad `svy-swift/`)
+
+`probe-swift.cjs` + `shape-{OLD,NEW}.txt` + `shape.diff` (the 53-line
+classified battery diff), `mini-probes.cjs` + `mini-probes.out` (node-type
+inventory, per-construct error matrix, defer/subscript/#if/async-let/regex
+shapes), `field-probe.cjs` (the childForFieldName truth table),
+`regress-probe.cjs` (NEW-regression minimal repros), `error-incidence.cjs`
+(the §incidence table + per-file lists), `error-diag.cjs` (first-ERROR
+diagnosis used for the delta classification), `extract-probe.cjs` +
+`torture.swift` + `extract-probe.out` (the built-extractor emission pin),
+`sugar-probe.cjs` (extension sugar/decorates asymmetry/implicit-member call),
+`count-props.cjs` (the Alamofire census), `tree-sitter-swift.wasm` (the
+staged-candidate 0.7.3 build), `tree-sitter-swift-0.7.3/` (crate extract) +
+`tag-clone/` (0.7.3-with-generated-files) with the sha evidence.

+ 2 - 1
scripts/kernel-parity.mjs

@@ -48,7 +48,7 @@ if (paths.length === 0) {
   process.exit(2);
 }
 
-const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', 'c', 'cpp', 'rust', 'csharp', 'ruby', 'php']);
+const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go', 'c', 'cpp', 'rust', 'csharp', 'ruby', 'php', 'swift']);
 const EXTS = new Map([
   ['.ts', 'typescript'], ['.mts', 'typescript'], ['.cts', 'typescript'],
   ['.tsx', 'tsx'], ['.js', 'javascript'], ['.mjs', 'javascript'],
@@ -62,6 +62,7 @@ const EXTS = new Map([
   ['.cs', 'csharp'], // R7b
   ['.rb', 'ruby'], ['.rake', 'ruby'], // R7b
   ['.php', 'php'], ['.module', 'php'], ['.install', 'php'], ['.theme', 'php'], ['.inc', 'php'], // R7b
+  ['.swift', 'swift'], // R7b
 ]);
 
 /** Collect candidate files. */

+ 9 - 0
src/extraction/grammars.ts

@@ -313,6 +313,15 @@ const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
   // tarball. Replaces the ^0.22 tree-sitter-wasms build (ABI 14 → 15). NOT
   // graph-neutral — the classified delta list lives in the php checklist doc.
   'php',
+  // R7b (Swift kernel port prep): tree-sitter-swift crate 0.7.3. Built from
+  // the CRATE TARBALL's src/ (NOT a tag sha-match: alex-pinkus keeps
+  // generated files off main and the 0.7.3-with-generated-files tag ships an
+  // older ABI-14 generation; grammar.json rules are JSON-equal, and the crate
+  // tarball is byte-for-byte what the kernel's cargo build compiles — table
+  // identity by construction). Replaces the ^0.4.0 tree-sitter-wasms build
+  // (ABI 13 → 15). NOT graph-neutral — delta is error-set membership only;
+  // classified list in the swift checklist doc.
+  'swift',
 ]);
 
 /** Absolute path of a language's grammar WASM (vendored or tree-sitter-wasms). */

+ 6 - 0
src/extraction/kernel/index.ts

@@ -68,6 +68,12 @@ const DEFAULT_ROUTED: ReadonlySet<Language> = new Set<Language>([
   // symfony (13,950 files byte-parity) + full-init dump-diffs byte-identical
   // ×3. Deferral ≈0–0.1% (genuinely-broken fixtures) — default sweep guard.
   'php',
+  // R7b (2026-07-20): parity swept 0-diff on Alamofire/vapor/swift-nio (720
+  // clean files byte-parity; Alamofire's 348 #1020 property nodes reproduced
+  // exactly) + full-init dump-diffs byte-identical ×3. Swift error incidence
+  // is structurally 9–27% on BOTH arms (heavy #if conditionalization) —
+  // sweeps run --max-deferral 0.3; a deferral-rate JUMP is the bug signal.
+  'swift',
 ]);
 
 /**

BIN
src/extraction/wasm/tree-sitter-swift.wasm