Sfoglia il codice sorgente

feat(kernel): R5 — Python and Go ports, gates passed, default-on

Python (codegraph-kernel/src/python.rs) and Go (src/go.rs) join the
native kernel, mirroring the wasm extractors bug-for-bug. Python:
decorated_definition docstrings/decorators (decorates refs only for
bare-identifier decorators — the call-kind quirk), function-in-class →
method, module-level assignments always extract as variable, from-import
per-name binding refs, self.x fn-ref candidates as bare names. Go:
receiver methods with Recv::name qualified names + contains edges to the
first earlier struct of that name, type_spec struct/interface
classification with embedding→extends and interface method nodes,
composite-literal instantiates keeping the package qualifier, top-level
var/const initializer walks attributed to the declared symbol (#693),
2-hop field chains (#1276), New().Method() re-encode (#645/#608), and
the GO_SPEC fn-ref layers.

Grammars: tree-sitter-python 0.23.6 + tree-sitter-go 0.23.4 crates, with
wasm vendored from the same tags (parser.c sha-matched) — both were
2023-era in tree-sitter-wasms.

Gates: extraction sweeps 100% (flask 83/83, django 3,035/3,038 +3
error-file deferrals, gin 99/99, prometheus 978/979 +1); full-init
dump-diffs byte-identical on flask (10,833 rows), gin (17,540), django
(360,794), and prometheus (213,758); torture fixtures enforced in npm
test. DEFAULT_ROUTED now covers typescript/tsx/javascript/jsx/java/
python/go. Full suite: 2,471 tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Colby McHenry 1 mese fa
parent
commit
c2503e2bee

+ 1 - 1
CHANGELOG.md

@@ -11,7 +11,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
 
 ### New Features
 ### New Features
 
 
-- Indexing TypeScript, TSX, JavaScript, JSX, and Java 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- and dubbo-scale codebases (Lombok-generated members included). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
+- Indexing TypeScript, TSX, JavaScript, JSX, Java, Python, and Go projects is faster: parsing and symbol extraction now run in a native engine when a prebuilt binary is available for your platform (release bundles include one), producing exactly the same graph — verified byte-for-byte against the previous engine on real repositories, from small libraries up to vscode-, dubbo-, and django-scale codebases (Lombok-generated members included). The speedup is largest on resource-constrained machines like CI runners. No setup needed: platforms without the native binary, and individual files with syntax errors, automatically use the previous engine, and `CODEGRAPH_KERNEL=0` turns the native path off entirely.
 - 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.
 - 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.
 - 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.
 - 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.

+ 61 - 0
__tests__/fixtures/kernel-parity/torture.go

@@ -0,0 +1,61 @@
+// Go torture fixture — receivers, embedding, interfaces, composite literals.
+package torture
+
+import (
+	"fmt"
+	pkga "example.com/other/pkga"
+)
+
+const MAX_ITEMS = 128
+
+var DefaultRegistry = NewRegistry()
+
+var handlerTable = map[string]func(int){
+	"recv": TargetCb,
+}
+
+type Widget struct {
+	*Base
+	Queryable
+	name string
+}
+
+type Stack[T any] struct {
+	items []T
+}
+
+type Core interface {
+	Reader
+	Marshal(v any) ([]byte, error)
+	Unmarshal(data []byte) error
+}
+
+type Dur int
+
+func NewRegistry() *Registry {
+	w := Widget{name: "w"}
+	q := pkga.Widget{}
+	fmt.Println(w, q, MAX_ITEMS)
+	cfg := loadConfig()
+	cfg.conn.Exec("x")
+	return New().Init()
+}
+
+func (s *Stack[T]) Push(item T) {
+	s.items = append(s.items, item)
+}
+
+func (w Widget) Render() string {
+	return w.name
+}
+
+func TargetCb(n int) {}
+
+func shadowed() {
+	MAX_ITEMS := 5
+	fmt.Println(MAX_ITEMS)
+}
+
+func reads() int {
+	return MAX_ITEMS
+}

+ 49 - 0
__tests__/fixtures/kernel-parity/torture.py

@@ -0,0 +1,49 @@
+"""Python torture fixture — decorators, self fn-refs, imports, shadowing."""
+import os, sys
+import os.path as osp
+from collections import OrderedDict, defaultdict
+from .relative import thing
+from mypkg.handlers import target_cb
+
+RETRY_LIMITS = {"a": 1}
+API_BASE = "https://example.test"
+x = compute(RETRY_LIMITS)
+
+
+class Service(BaseService, mixins.LoggerMixin):
+    """Class docs."""
+
+    def __init__(self, registry):
+        self.registry = registry
+        register(self.on_event)
+        queue(target_cb)
+
+    @staticmethod
+    def helper(arg):
+        return transform(arg)
+
+    async def run(self):
+        cfg = self.registry.lookup("x")
+        limit = RETRY_LIMITS
+        obj.method_chain().deep(cfg)
+        ", ".join(cfg)
+        return await fetch(API_BASE)
+
+    def on_event(self):
+        pass
+
+
+@app.route("/x")
+def view():
+    def inner():
+        return API_BASE
+    return inner()
+
+
+def shadowed():
+    API_BASE = "local"
+    return API_BASE
+
+
+handlers = {"recv": target_cb}
+callbacks = [target_cb, view]

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

+ 4 - 4
__tests__/kernel-scaffold.test.ts

@@ -72,12 +72,12 @@ describe.skipIf(!kernelBuilt)('kernel scaffold', () => {
     expect(info.languages).toContain('javascript');
     expect(info.languages).toContain('javascript');
   });
   });
 
 
-  it('TS/JS family + Java route to the kernel by default; others stay wasm', () => {
-    for (const lang of ['typescript', 'tsx', 'javascript', 'jsx', 'java'] as const) {
+  it('TS/JS family + Java + Python + Go route to the kernel by default; others stay wasm', () => {
+    for (const lang of ['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go'] as const) {
       expect(kernelRoutes(lang), lang).toBe(true);
       expect(kernelRoutes(lang), lang).toBe(true);
     }
     }
-    expect(kernelRoutes('python')).toBe(false);
-    expect(tryKernelExtract('src/a.py', 'def f():\n  pass\n', 'python')).toBeNull();
+    expect(kernelRoutes('ruby')).toBe(false);
+    expect(tryKernelExtract('src/a.rb', 'def f\nend\n', 'ruby')).toBeNull();
     // CODEGRAPH_KERNEL_LANGS REPLACES the default set when present.
     // CODEGRAPH_KERNEL_LANGS REPLACES the default set when present.
     process.env.CODEGRAPH_KERNEL_LANGS = 'tsx';
     process.env.CODEGRAPH_KERNEL_LANGS = 'tsx';
     expect(kernelRoutes('typescript')).toBe(false);
     expect(kernelRoutes('typescript')).toBe(false);

+ 11 - 1
__tests__/kernel-tsjs-parity.test.ts

@@ -60,7 +60,7 @@ let savedEnv: Record<string, string | undefined>;
 describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {
 describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {
   beforeAll(async () => {
   beforeAll(async () => {
     await initGrammars();
     await initGrammars();
-    await loadGrammarsForLanguages(['typescript', 'tsx', 'javascript', 'jsx', 'java']);
+    await loadGrammarsForLanguages(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go']);
   });
   });
 
 
   beforeEach(() => {
   beforeEach(() => {
@@ -110,6 +110,16 @@ describe.skipIf(!kernelBuilt)('kernel TS/JS extraction parity', () => {
     assertParity('fixtures/Torture.java', fs.readFileSync(file, 'utf8'), 'java');
     assertParity('fixtures/Torture.java', fs.readFileSync(file, 'utf8'), 'java');
   });
   });
 
 
+  it('torture fixture (python): decorators, self fn-refs, imports, shadowing', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.py');
+    assertParity('fixtures/torture.py', fs.readFileSync(file, 'utf8'), 'python');
+  });
+
+  it('torture fixture (go): receivers, embedding, interfaces, composite literals', () => {
+    const file = path.join(FIXTURE_DIR, 'torture.go');
+    assertParity('fixtures/torture.go', fs.readFileSync(file, 'utf8'), 'go');
+  });
+
   it.each(REAL_SOURCES)('real source parity: %s', (rel) => {
   it.each(REAL_SOURCES)('real source parity: %s', (rel) => {
     const file = path.join(__dirname, '..', rel);
     const file = path.join(__dirname, '..', rel);
     assertParity(rel, fs.readFileSync(file, 'utf8'), 'typescript');
     assertParity(rel, fs.readFileSync(file, 'utf8'), 'typescript');

+ 22 - 0
codegraph-kernel/Cargo.lock

@@ -52,8 +52,10 @@ dependencies = [
  "regex",
  "regex",
  "sha2",
  "sha2",
  "tree-sitter",
  "tree-sitter",
+ "tree-sitter-go",
  "tree-sitter-java",
  "tree-sitter-java",
  "tree-sitter-javascript",
  "tree-sitter-javascript",
+ "tree-sitter-python",
  "tree-sitter-typescript",
  "tree-sitter-typescript",
 ]
 ]
 
 
@@ -480,6 +482,16 @@ dependencies = [
  "tree-sitter-language",
  "tree-sitter-language",
 ]
 ]
 
 
+[[package]]
+name = "tree-sitter-go"
+version = "0.23.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 [[package]]
 name = "tree-sitter-java"
 name = "tree-sitter-java"
 version = "0.23.5"
 version = "0.23.5"
@@ -506,6 +518,16 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
 checksum = "009994f150cc0cd50ff54917d5bc8bffe8cad10ca10d81c34da2ec421ae61782"
 
 
+[[package]]
+name = "tree-sitter-python"
+version = "0.23.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d065aaa27f3aaceaf60c1f0e0ac09e1cb9eb8ed28e7bcdaa52129cffc7f4b04"
+dependencies = [
+ "cc",
+ "tree-sitter-language",
+]
+
 [[package]]
 [[package]]
 name = "tree-sitter-typescript"
 name = "tree-sitter-typescript"
 version = "0.23.2"
 version = "0.23.2"

+ 2 - 0
codegraph-kernel/Cargo.toml

@@ -23,6 +23,8 @@ regex = "1"
 tree-sitter-typescript = "0.23"
 tree-sitter-typescript = "0.23"
 tree-sitter-javascript = "0.25"
 tree-sitter-javascript = "0.25"
 tree-sitter-java = "0.23"
 tree-sitter-java = "0.23"
+tree-sitter-python = "0.23"
+tree-sitter-go = "0.23"
 
 
 [build-dependencies]
 [build-dependencies]
 napi-build = "2"
 napi-build = "2"

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

@@ -0,0 +1,1223 @@
+//! Go extraction — a faithful Rust port of `TreeSitterExtractor`'s Go paths
+//! (src/extraction/tree-sitter.ts) plus languages/go.ts.
+//!
+//! Go's shape quirks, mirrored exactly: methods are top-level with a receiver
+//! (qualifiedName override `Recv::name` + a contains edge to the FIRST
+//! earlier-in-file struct of that name), structs/interfaces arrive as
+//! `type_spec` and classify via the inner type node (struct embedding →
+//! extends; interface method_elems become method nodes), composite literals
+//! (`pkga.Widget{}`) keep their package qualifier as `instantiates` refs,
+//! top-level var/const specs walk their initializers ATTRIBUTED to the
+//! declared symbol (#693), 2-hop field chains (`t.conn.Exec`) keep the chain
+//! (#1276), and `New().Method()` re-encodes as `New().Method` (#645/#608)
+//! only for bare-identifier factories. Files with parse errors defer to wasm.
+
+use crate::buffers::{
+    build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
+    RefRow, StrRef, Tables, FLAG_IS_EXPORTED, FUNCTION_REF_CODE, NONE, NONE_STR,
+};
+use crate::docstring::preceding_docstring;
+use crate::ids;
+use crate::textutil as util;
+use regex::Regex;
+use std::collections::{HashMap, HashSet};
+use std::sync::OnceLock;
+use tree_sitter::{Node, Parser};
+
+const MAX_VALUE_REF_NODES: usize = 20_000;
+
+fn receiver_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"\(\s*(?:[A-Za-z_]\w*\s+)?\*?\s*([A-Za-z_]\w*)").unwrap())
+}
+fn simple_ident_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_]\w*$").unwrap())
+}
+fn go_two_hop_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"^[A-Za-z_]\w*\.[A-Za-z_]\w*$").unwrap())
+}
+fn generic_angle_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"<[^>]*>").unwrap())
+}
+fn bracket_args_re() -> &'static Regex {
+    static RE: OnceLock<Regex> = OnceLock::new();
+    RE.get_or_init(|| Regex::new(r"\[[^\]]*\]").unwrap())
+}
+
+struct Scope {
+    row: u32,
+    kind: &'static str,
+    name: String,
+}
+
+#[derive(Default)]
+struct Extra {
+    docstring: Option<String>,
+    signature: Option<String>,
+    is_exported: Option<bool>,
+    return_type: Option<String>,
+    qualified_name: 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,
+}
+
+/// Per-node metadata for the receiver-method owner lookup (mirrors the TS
+/// side's scan over `this.nodes` — FIRST match wins, earlier-in-file only).
+struct NodeMeta {
+    kind: &'static str,
+    name: String,
+}
+
+pub struct Walker<'t> {
+    src: &'t str,
+    file_path: &'t str,
+    line_starts: Vec<usize>,
+    arena: Arena,
+    tables: Tables,
+    stack: Vec<Scope>,
+    nodes_meta: Vec<NodeMeta>,
+    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("go").ok_or("no go grammar")?;
+    let t0 = std::time::Instant::now();
+    let mut parser = Parser::new();
+    parser
+        .set_language(&grammar)
+        .map_err(|e| format!("set_language(go) 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(),
+        nodes_meta: 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.nodes_meta.push(NodeMeta { kind: "file", name: base_name.to_string() });
+    w.node_ids.push(ids::file_node_id(file_path));
+    w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
+
+    w.visit_node(tree.root_node());
+    w.flush_fn_ref_candidates();
+    w.flush_value_refs(tree.root_node());
+    w.stack.pop();
+
+    let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
+    let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
+    Ok(EmitOut {
+        meta,
+        nodes: w.tables.nodes,
+        edges: w.tables.edges,
+        refs: w.tables.refs,
+        arena: w.arena.into_vec(),
+    })
+}
+
+impl<'t> Walker<'t> {
+    fn text(&self, node: Node) -> &'t str {
+        &self.src[node.byte_range()]
+    }
+    fn line_of(&self, node: Node) -> u32 {
+        node.start_position().row as u32 + 1
+    }
+    fn col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
+    }
+    fn end_col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
+    }
+    fn top_row(&self) -> u32 {
+        self.stack.last().map(|s| s.row).unwrap_or(0)
+    }
+    fn inside_class_like(&self) -> bool {
+        self.stack
+            .last()
+            .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module"))
+            .unwrap_or(false)
+    }
+
+    fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
+        let name_ref = self.arena.put(name);
+        self.tables.push_ref(&RefRow {
+            from_idx: from_row,
+            kind: kind_code,
+            line: self.line_of(node),
+            column: self.col_of(node),
+            reference_name: name_ref,
+            candidates: NONE_STR,
+            from_id_str: NONE_STR,
+        });
+        if kind_code == edge_kind_index("imports").unwrap() {
+            if util::simple_name().is_match(name) {
+                self.imported_names.insert(name.to_string());
+            } else if let Some(c) = util::qualified_import().captures(name) {
+                self.imported_names.insert(c[1].to_string());
+            }
+        }
+    }
+
+    fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
+        if name.is_empty() {
+            return None;
+        }
+        let start_line = self.line_of(node);
+        let id = ids::node_id(self.file_path, kind, name, start_line);
+        let end_line = node.end_position().row as u32 + 1;
+
+        let qualified = extra.qualified_name.unwrap_or_else(|| {
+            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);
+        }
+        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: 0,
+            flags,
+            start_line,
+            end_line,
+            start_column: self.col_of(node),
+            end_column: self.end_col_of(node),
+            name: name_ref,
+            qualified_name: qn_ref,
+            id: id_ref,
+            docstring: doc_ref,
+            signature: sig_ref,
+            decorators: NONE_STR,
+            type_parameters: NONE_STR,
+            return_type: ret_ref,
+            extra_json: NONE_STR,
+        });
+        self.nodes_meta.push(NodeMeta { kind, name: name.to_string() });
+        self.node_ids.push(id);
+
+        let parent_row = self.top_row();
+        self.tables.push_edge(&EdgeRow {
+            source_idx: parent_row,
+            target_idx: row,
+            kind: edge_kind_index("contains").unwrap(),
+            provenance: 0,
+            line: NONE,
+            column: NONE,
+            metadata_json: NONE_STR,
+            source_id_str: NONE_STR,
+            target_id_str: NONE_STR,
+        });
+
+        if kind == "function" || kind == "method" {
+            self.defined_fn_names.insert(name.to_string());
+        }
+        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)
+    }
+
+    fn extract_name(&self, node: Node) -> 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()
+    }
+
+    /// goExtractor.getSignature: params + ' ' + result.
+    fn signature_of(&self, node: Node) -> Option<String> {
+        let params = node.child_by_field_name("parameters")?;
+        let mut sig = self.text(params).to_string();
+        if let Some(result) = node.child_by_field_name("result") {
+            sig.push(' ');
+            sig.push_str(self.text(result));
+        }
+        Some(sig)
+    }
+
+    /// goExtractor.isExported: uppercase first letter of the name field.
+    fn is_exported(&self, node: Node) -> bool {
+        if let Some(name_node) = node.child_by_field_name("name") {
+            let text = self.text(name_node);
+            return text.as_bytes().first().map(|b| b.is_ascii_uppercase()).unwrap_or(false);
+        }
+        false
+    }
+
+    /// extractGoReturnType (languages/go.ts).
+    fn return_type_of(&self, node: Node) -> Option<String> {
+        let mut result = node.child_by_field_name("result")?;
+        if result.kind() == "parameter_list" {
+            let first = (0..result.named_child_count())
+                .filter_map(|i| result.named_child(i))
+                .find(|c| c.kind() == "parameter_declaration")?;
+            result = first.child_by_field_name("type").unwrap_or(first);
+        }
+        if result.kind() == "pointer_type" {
+            result = (0..result.named_child_count())
+                .filter_map(|i| result.named_child(i))
+                .find(|c| matches!(c.kind(), "type_identifier" | "qualified_type" | "generic_type"))
+                .unwrap_or(result);
+        }
+        let text = self.text(result).trim();
+        let text = text.strip_prefix('*').unwrap_or(text);
+        let text = generic_angle_re().replace_all(text, "");
+        let text = bracket_args_re().replace_all(&text, "");
+        let last = text.rsplit('.').next().unwrap_or("").trim().to_string();
+        if last.is_empty() || !simple_ident_re().is_match(&last) {
+            return None;
+        }
+        Some(last)
+    }
+
+    /// goExtractor.getReceiverType: the regex over the receiver's text.
+    fn receiver_type_of(&self, node: Node) -> Option<String> {
+        let receiver = node.child_by_field_name("receiver")?;
+        let text = self.text(receiver);
+        receiver_re().captures(text).map(|c| c[1].to_string())
+    }
+
+    // --- visitNode ------------------------------------------------------------
+
+    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" {
+            self.extract_function(node);
+            skip_children = true;
+        } else if kind == "method_declaration" {
+            self.extract_method(node);
+            skip_children = true;
+        } else if kind == "type_spec" {
+            skip_children = self.extract_type_alias(node);
+        } else if matches!(kind, "var_declaration" | "short_var_declaration" | "const_declaration")
+            && !self.inside_class_like()
+        {
+            self.extract_variable(node);
+            self.scan_fn_ref_subtree(node, 0);
+            skip_children = true;
+        } else if kind == "import_declaration" {
+            self.extract_import(node);
+        } else if kind == "call_expression" {
+            self.extract_call(node);
+        } else if kind == "composite_literal" {
+            self.extract_instantiation(node);
+        }
+
+        if !skip_children {
+            for i in 0..node.named_child_count() {
+                if let Some(c) = node.named_child(i) {
+                    self.visit_node(c);
+                }
+            }
+        }
+    }
+
+    fn visit_function_body(&mut self, body: Node<'t>) {
+        self.visit_for_calls_and_structure(body);
+    }
+
+    fn visit_for_calls_and_structure(&mut self, node: Node<'t>) {
+        let kind = node.kind();
+        self.maybe_capture_fn_refs(node);
+
+        if kind == "call_expression" {
+            self.extract_call(node);
+        } else if kind == "composite_literal" {
+            self.extract_instantiation(node);
+        }
+
+        if kind == "function_declaration" {
+            let name = self.extract_name(node);
+            if name != "<anonymous>" {
+                self.extract_function(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>) {
+        // (getReceiverType only matches method_declaration's receiver field —
+        // function_declaration has none, so no reroute happens here)
+        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: self.signature_of(node),
+            is_exported: Some(self.is_exported(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.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>) {
+        // methodsAreTopLevel: always a method. Receiver-qualified name +
+        // a contains edge from the FIRST earlier struct/class/enum/trait
+        // node of the receiver's name (mirrors the this.nodes.find scan).
+        let receiver_type = self.receiver_type_of(node);
+        let name = self.extract_name(node);
+        let extra = Extra {
+            docstring: preceding_docstring(node, self.src),
+            signature: self.signature_of(node),
+            return_type: self.return_type_of(node),
+            qualified_name: receiver_type.as_ref().map(|r| format!("{r}::{name}")),
+            ..Extra::default() // extractMethod passes no isExported
+        };
+        let Some(row) = self.create_node("method", &name, node, extra) else { return };
+
+        if let Some(receiver_type) = &receiver_type {
+            if !self.inside_class_like() {
+                let owner_row = self
+                    .nodes_meta
+                    .iter()
+                    .position(|m| {
+                        m.name == *receiver_type
+                            && matches!(m.kind, "struct" | "class" | "enum" | "trait")
+                    })
+                    .map(|i| i as u32);
+                if let Some(owner_row) = owner_row {
+                    self.tables.push_edge(&EdgeRow {
+                        source_idx: owner_row,
+                        target_idx: row,
+                        kind: edge_kind_index("contains").unwrap(),
+                        provenance: 0,
+                        line: NONE,
+                        column: NONE,
+                        metadata_json: NONE_STR,
+                        source_id_str: NONE_STR,
+                        target_id_str: NONE_STR,
+                    });
+                }
+            }
+        }
+
+        self.extract_type_annotations(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();
+    }
+
+    /// extractTypeAlias for Go: type_spec → struct / interface / plain alias.
+    fn extract_type_alias(&mut self, node: Node<'t>) -> bool {
+        let name = self.extract_name(node);
+        if name == "<anonymous>" {
+            return false;
+        }
+        let docstring = preceding_docstring(node, self.src);
+        let is_exported = Some(self.is_exported(node));
+        let type_child = node.child_by_field_name("type");
+        let resolved = type_child.map(|t| t.kind());
+
+        if resolved == Some("struct_type") {
+            let Some(row) = self.create_node(
+                "struct",
+                &name,
+                node,
+                Extra { docstring, is_exported, ..Extra::default() },
+            ) else {
+                return true;
+            };
+            self.stack.push(Scope { row, kind: "struct", name });
+            if let Some(type_child) = type_child {
+                // Struct embedding → extends (field_declaration without a
+                // field_identifier), reached via the inheritance recursion.
+                self.extract_inheritance(type_child, row);
+                let body = type_child.child_by_field_name("body").unwrap_or(type_child);
+                for i in 0..body.named_child_count() {
+                    if let Some(c) = body.named_child(i) {
+                        self.visit_node(c);
+                    }
+                }
+            }
+            self.stack.pop();
+            return true;
+        }
+
+        if resolved == Some("interface_type") {
+            let Some(row) = self.create_node(
+                "interface",
+                &name,
+                node,
+                Extra { docstring, is_exported, ..Extra::default() },
+            ) else {
+                return true;
+            };
+            if let Some(type_child) = type_child {
+                self.extract_inheritance(type_child, row);
+                self.extract_go_interface_methods(type_child, row, &name);
+            }
+            return true;
+        }
+
+        self.create_node(
+            "type_alias",
+            &name,
+            node,
+            Extra { docstring, is_exported, ..Extra::default() },
+        );
+        // (go type_spec has no `value` field — no type-ref walk; TS/tsx member
+        // extraction is TS-family-only)
+        false
+    }
+
+    /// extractGoInterfaceMethods: method_elem/method_spec → method nodes.
+    fn extract_go_interface_methods(&mut self, interface_type: Node<'t>, iface_row: u32, iface_name: &str) {
+        self.stack.push(Scope { row: iface_row, kind: "interface", name: iface_name.to_string() });
+        for i in 0..interface_type.named_child_count() {
+            let Some(m) = interface_type.named_child(i) else { continue };
+            if !matches!(m.kind(), "method_elem" | "method_spec") {
+                continue;
+            }
+            let name_node = m.child_by_field_name("name").or_else(|| m.named_child(0));
+            let Some(name_node) = name_node else { continue };
+            let mname = self.text(name_node).to_string();
+            if !mname.is_empty() {
+                let signature = self.signature_of(m);
+                self.create_node("method", &mname, m, Extra { signature, ..Extra::default() });
+            }
+        }
+        self.stack.pop();
+    }
+
+    /// extractVariable's Go branch: var/const specs + short_var_declaration.
+    fn extract_variable(&mut self, node: Node<'t>) {
+        let docstring = preceding_docstring(node, self.src);
+        let is_const_decl = node.kind() == "const_declaration";
+
+        for i in 0..node.named_child_count() {
+            let Some(spec) = node.named_child(i) else { continue };
+            if !matches!(spec.kind(), "var_spec" | "const_spec") {
+                continue;
+            }
+            let mut var_row: Option<u32> = None;
+            if let Some(name_node) = spec.named_child(0) {
+                if name_node.kind() == "identifier" {
+                    let name = self.text(name_node).to_string();
+                    let value_node = if spec.named_child_count() > 1 {
+                        spec.named_child(spec.named_child_count() - 1)
+                    } else {
+                        None
+                    };
+                    let signature = value_node.map(|v| util::init_signature(self.text(v)));
+                    var_row = self.create_node(
+                        if is_const_decl { "constant" } else { "variable" },
+                        &name,
+                        spec,
+                        Extra { docstring: docstring.clone(), signature, ..Extra::default() },
+                    );
+                }
+            }
+            // Walk the initializer ATTRIBUTED to the declared symbol (#693).
+            if let Some(value_field) = spec.child_by_field_name("value") {
+                if let Some(row) = var_row {
+                    let name = self.nodes_meta[row as usize].name.clone();
+                    self.stack.push(Scope { row, kind: "variable", name });
+                    self.visit_function_body(value_field);
+                    self.stack.pop();
+                } else {
+                    self.visit_function_body(value_field);
+                }
+            }
+        }
+
+        if node.kind() == "short_var_declaration" {
+            let left = node.child_by_field_name("left");
+            let right = node.child_by_field_name("right");
+            if let Some(left) = left {
+                let identifiers: Vec<Node> = if left.kind() == "expression_list" {
+                    (0..left.named_child_count())
+                        .filter_map(|i| left.named_child(i))
+                        .filter(|c| c.kind() == "identifier")
+                        .collect()
+                } else {
+                    vec![left]
+                };
+                for id in identifiers {
+                    let name = self.text(id).to_string();
+                    let signature = right.map(|r| util::init_signature(self.text(r)));
+                    self.create_node(
+                        "variable",
+                        &name,
+                        node,
+                        Extra { docstring: docstring.clone(), signature, ..Extra::default() },
+                    );
+                }
+            }
+        }
+    }
+
+    /// extractImport's Go branch: one import node + ref per import_spec.
+    fn extract_import(&mut self, node: Node<'t>) {
+        let parent = self.top_row();
+        let imports_kind = edge_kind_index("imports").unwrap();
+        let mut handle_spec = |w: &mut Self, spec: Node<'t>| {
+            let lit = (0..spec.named_child_count())
+                .filter_map(|i| spec.named_child(i))
+                .find(|c| c.kind() == "interpreted_string_literal");
+            let Some(lit) = lit else { return };
+            let import_path: String = w
+                .text(lit)
+                .chars()
+                .filter(|c| *c != '\'' && *c != '"')
+                .collect();
+            if import_path.is_empty() {
+                return;
+            }
+            let signature = w.text(spec).trim().to_string();
+            w.create_node(
+                "import",
+                &import_path,
+                spec,
+                Extra { signature: Some(signature), ..Extra::default() },
+            );
+            w.push_ref_at(parent, &import_path, imports_kind, spec);
+        };
+
+        let spec_list = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "import_spec_list");
+        if let Some(list) = spec_list {
+            for i in 0..list.named_child_count() {
+                if let Some(spec) = list.named_child(i) {
+                    if spec.kind() == "import_spec" {
+                        handle_spec(self, spec);
+                    }
+                }
+            }
+        } else {
+            let spec = (0..node.named_child_count())
+                .filter_map(|i| node.named_child(i))
+                .find(|c| c.kind() == "import_spec");
+            if let Some(spec) = spec {
+                handle_spec(self, spec);
+            }
+        }
+    }
+
+    /// extractCall — Go's generic-tail paths (selector_expression callees).
+    fn extract_call(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let func = node
+            .child_by_field_name("function")
+            .or_else(|| node.named_child(0));
+        let mut callee_name = String::new();
+
+        if let Some(func) = func {
+            if func.kind() == "selector_expression" {
+                let property = func
+                    .child_by_field_name("property")
+                    .or_else(|| func.child_by_field_name("field"));
+                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;
+                        }
+                    }
+                    if let Some(r) = receiver {
+                        match r.kind() {
+                            "identifier" | "simple_identifier" | "field_identifier" => {
+                                let receiver_name = self.text(r);
+                                if !matches!(receiver_name, "self" | "this" | "cls" | "super") {
+                                    callee_name = format!("{receiver_name}.{method_name}");
+                                } else {
+                                    callee_name = method_name.to_string();
+                                }
+                            }
+                            "call_expression" => {
+                                // Bare package-level factory chain `New().Method()`
+                                // re-encodes; instance chains keep the bare name.
+                                let inner_fn = r.child_by_field_name("function");
+                                let reencode =
+                                    inner_fn.map(|f| f.kind() == "identifier").unwrap_or(false);
+                                if reencode {
+                                    let inner: String = self
+                                        .text(inner_fn.unwrap())
+                                        .replace("->", ".")
+                                        .chars()
+                                        .filter(|c| !c.is_whitespace())
+                                        .collect();
+                                    callee_name = format!("{inner}().{method_name}");
+                                } else {
+                                    callee_name = method_name.to_string();
+                                }
+                            }
+                            "selector_expression" => {
+                                // 2-hop field chain `t.conn.Exec` (#1276).
+                                let chain: String = self
+                                    .text(r)
+                                    .chars()
+                                    .filter(|c| !c.is_whitespace())
+                                    .collect();
+                                if go_two_hop_re().is_match(&chain) {
+                                    callee_name = format!("{chain}.{method_name}");
+                                } else {
+                                    callee_name = method_name.to_string();
+                                }
+                            }
+                            _ => {
+                                callee_name = method_name.to_string();
+                            }
+                        }
+                    } else {
+                        callee_name = method_name.to_string();
+                    }
+                }
+            } else {
+                callee_name = self.text(func).to_string();
+            }
+        }
+
+        if !callee_name.is_empty() {
+            // `(*T)(x)` conversions normalize to `T`.
+            if let Some(c) = util::paren_conversion().captures(&callee_name) {
+                callee_name = c[1].to_string();
+            }
+            let from = self.top_row();
+            self.push_ref_at(from, &callee_name.clone(), edge_kind_index("calls").unwrap(), node);
+        }
+    }
+
+    /// extractInstantiation's composite_literal branch: named struct types
+    /// only; the package qualifier is KEPT.
+    fn extract_instantiation(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let ctor = node
+            .child_by_field_name("constructor")
+            .or_else(|| node.child_by_field_name("type"))
+            .or_else(|| node.child_by_field_name("name"))
+            .or_else(|| node.named_child(0));
+        let Some(ctor) = ctor else { return };
+        if !matches!(ctor.kind(), "type_identifier" | "qualified_type") {
+            return;
+        }
+        let mut go_type = self.text(ctor).trim().to_string();
+        if let Some(br) = go_type.find('[') {
+            if br > 0 {
+                go_type.truncate(br);
+                go_type = go_type.trim().to_string();
+            }
+        }
+        if !go_type.is_empty() {
+            let from = self.top_row();
+            self.push_ref_at(from, &go_type, edge_kind_index("instantiates").unwrap(), node);
+        }
+    }
+
+    /// extractInheritance — the Go branches: interface embedding
+    /// (constraint_elem) and struct embedding (field_declaration without a
+    /// field_identifier), plus the field_declaration_list recursion.
+    fn extract_inheritance(&mut self, node: Node<'t>, class_row: u32) {
+        let extends_kind = edge_kind_index("extends").unwrap();
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            match child.kind() {
+                "constraint_elem" => {
+                    let type_id = (0..child.named_child_count())
+                        .filter_map(|j| child.named_child(j))
+                        .find(|c| c.kind() == "type_identifier");
+                    if let Some(type_id) = type_id {
+                        let name = self.text(type_id).to_string();
+                        self.push_ref_at(class_row, &name, extends_kind, type_id);
+                    }
+                }
+                "field_declaration" => {
+                    let has_field_identifier = (0..child.named_child_count())
+                        .filter_map(|j| child.named_child(j))
+                        .any(|c| c.kind() == "field_identifier");
+                    if !has_field_identifier {
+                        let type_id = (0..child.named_child_count())
+                            .filter_map(|j| child.named_child(j))
+                            .find(|c| c.kind() == "type_identifier");
+                        if let Some(type_id) = type_id {
+                            let name = self.text(type_id).to_string();
+                            self.push_ref_at(class_row, &name, extends_kind, type_id);
+                        }
+                    }
+                }
+                "field_declaration_list" | "class_heritage" => {
+                    self.extract_inheritance(child, class_row);
+                }
+                _ => {}
+            }
+        }
+    }
+
+    /// extractTypeAnnotations — Go's returnField is `result`.
+    fn extract_type_annotations(&mut self, node: Node<'t>, from_row: u32) {
+        if let Some(params) = node.child_by_field_name("parameters") {
+            self.extract_type_refs_from_subtree(params, from_row);
+        }
+        if let Some(ret) = node.child_by_field_name("result") {
+            self.extract_type_refs_from_subtree(ret, from_row);
+        }
+        let type_annotation = (0..node.named_child_count())
+            .filter_map(|i| node.named_child(i))
+            .find(|c| c.kind() == "type_annotation");
+        if let Some(ta) = type_annotation {
+            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);
+            }
+        }
+    }
+
+    // --- fn refs (GO_SPEC, with the literal_element/expression_list layers) --------
+
+    fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
+        let (mode, field): (&str, &str) = match node.kind() {
+            "argument_list" => ("args", ""),
+            "assignment_statement" => ("rhs", "right"),
+            "short_var_declaration" => ("rhs", "right"),
+            "var_spec" => ("varinit", "value"),
+            "keyed_element" => ("value", ""), // value = LAST named child
+            "literal_value" => ("list", ""),
+            _ => return,
+        };
+        if self.stack.is_empty() {
+            return;
+        }
+        let from = self.top_row();
+
+        let mut values: Vec<Node> = Vec::new();
+        match mode {
+            "args" | "list" => {
+                for i in 0..node.named_child_count() {
+                    if let Some(c) = node.named_child(i) {
+                        values.push(c);
+                    }
+                }
+            }
+            "rhs" => {
+                if let Some(rhs) = node.child_by_field_name(field) {
+                    let lhs_text = node
+                        .child_by_field_name("left")
+                        .map(|l| self.text(l))
+                        .unwrap_or("");
+                    let lhs_last = util::lhs_last_name()
+                        .captures(lhs_text)
+                        .and_then(|c| c.get(1))
+                        .map(|m| m.as_str());
+                    if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
+                        values.push(rhs);
+                    }
+                }
+            }
+            "value" => {
+                let v = node
+                    .child_by_field_name("value")
+                    .or_else(|| {
+                        if node.named_child_count() > 0 {
+                            node.named_child(node.named_child_count() - 1)
+                        } else {
+                            None
+                        }
+                    });
+                if let Some(v) = v {
+                    values.push(v);
+                }
+            }
+            _ => {
+                // varinit — Go var_spec names are plain identifiers (no
+                // destructuring patterns to skip).
+                if let Some(v) = node.child_by_field_name(field) {
+                    values.push(v);
+                }
+            }
+        }
+
+        for v in values {
+            self.normalize_fn_ref_value(v, from, 0);
+        }
+    }
+
+    /// normalizeValue with GO_SPEC's transparent layers (literal_element,
+    /// expression_list — both fan out to named children).
+    fn normalize_fn_ref_value(&mut self, v: Node<'t>, from: u32, depth: u32) {
+        if depth > 4 {
+            return;
+        }
+        match v.kind() {
+            "identifier" => {
+                let name = self.text(v).to_string();
+                if name.is_empty() || is_stoplisted(&name) {
+                    return;
+                }
+                let p = v.start_position();
+                self.fn_ref_cands.push(Cand {
+                    from,
+                    name,
+                    line: p.row as u32 + 1,
+                    column_byte: v.start_byte(),
+                    row: p.row,
+                });
+            }
+            "literal_element" | "expression_list" => {
+                for i in 0..v.named_child_count() {
+                    if let Some(c) = v.named_child(i) {
+                        self.normalize_fn_ref_value(c, from, depth + 1);
+                    }
+                }
+            }
+            _ => {}
+        }
+    }
+
+    fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        if depth > 12 {
+            return;
+        }
+        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 refs -------------------------------------------------------------------
+
+    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 — Go declarator shapes: const_spec/var_spec (name =
+        // first child) and short_var_declaration (left / expression_list).
+        let mut decl_counts: HashMap<&str, u32> = HashMap::new();
+        let mut bump = |decl_counts: &mut HashMap<&'t str, u32>, name_node: Option<Node<'t>>, src: &'t str, targets: &HashMap<String, u32>| {
+            if let Some(n) = name_node {
+                if matches!(n.kind(), "identifier" | "simple_identifier") {
+                    let nm = &src[n.byte_range()];
+                    if targets.contains_key(nm) {
+                        *decl_counts.entry(nm).or_insert(0) += 1;
+                    }
+                }
+            }
+        };
+        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;
+            match n.kind() {
+                "const_spec" | "var_spec" => bump(&mut decl_counts, n.named_child(0), self.src, &targets),
+                "short_var_declaration" => {
+                    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" {
+                            bump(&mut decl_counts, Some(left), self.src, &targets);
+                        } else {
+                            for i in 0..left.named_child_count() {
+                                bump(&mut decl_counts, left.named_child(i), self.src, &targets);
+                            }
+                        }
+                    }
+                }
+                _ => {}
+            }
+            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);
+                    }
+                }
+            }
+        }
+    }
+}
+
+fn is_stoplisted(name: &str) -> bool {
+    matches!(
+        name,
+        "this" | "self" | "super" | "null" | "nil" | "true" | "false" | "undefined" | "new"
+            | "NULL" | "nullptr" | "None"
+    )
+}
+
+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"
+    )
+}
+
+/// BUILTIN_TYPES (shared table).
+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"
+    )
+}
+
+fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
+    match s {
+        Some(s) => arena.put(s),
+        None => NONE_STR,
+    }
+}

+ 4 - 1
codegraph-kernel/src/langs.rs

@@ -15,7 +15,8 @@ use tree_sitter::Language;
 
 
 /// Languages this kernel binary can extract (reported by contractInfo;
 /// Languages this kernel binary can extract (reported by contractInfo;
 /// TS-side routing policy decides what actually routes).
 /// TS-side routing policy decides what actually routes).
-pub const LANGUAGES: [&str; 5] = ["typescript", "tsx", "javascript", "jsx", "java"];
+pub const LANGUAGES: [&str; 7] =
+    ["typescript", "tsx", "javascript", "jsx", "java", "python", "go"];
 
 
 pub fn grammar_for(language: &str) -> Option<Language> {
 pub fn grammar_for(language: &str) -> Option<Language> {
     match language {
     match language {
@@ -23,6 +24,8 @@ pub fn grammar_for(language: &str) -> Option<Language> {
         "tsx" => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
         "tsx" => Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
         "javascript" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
         "javascript" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
         "java" => Some(tree_sitter_java::LANGUAGE.into()),
         "java" => Some(tree_sitter_java::LANGUAGE.into()),
+        "python" => Some(tree_sitter_python::LANGUAGE.into()),
+        "go" => Some(tree_sitter_go::LANGUAGE.into()),
         _ => None,
         _ => None,
     }
     }
 }
 }

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

@@ -19,9 +19,11 @@
 mod buffers;
 mod buffers;
 mod docstring;
 mod docstring;
 mod ids;
 mod ids;
+mod go;
 mod java;
 mod java;
 mod langs;
 mod langs;
 mod textutil;
 mod textutil;
+mod python;
 mod tsjs;
 mod tsjs;
 
 
 use napi::bindgen_prelude::*;
 use napi::bindgen_prelude::*;
@@ -99,6 +101,8 @@ pub fn grammar_info(language: String) -> Option<GrammarInfo> {
 pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
 pub fn extract_file(file_path: String, content: String, language: String) -> Result<ExtractBuffers> {
     let out = match language.as_str() {
     let out = match language.as_str() {
         "java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
         "java" => java::extract(&file_path, &content).map_err(Error::from_reason)?,
+        "python" => python::extract(&file_path, &content).map_err(Error::from_reason)?,
+        "go" => go::extract(&file_path, &content).map_err(Error::from_reason)?,
         _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
         _ => tsjs::extract(&file_path, &content, &language).map_err(Error::from_reason)?,
     };
     };
     Ok(ExtractBuffers {
     Ok(ExtractBuffers {

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

@@ -0,0 +1,978 @@
+//! Python extraction — a faithful Rust port of `TreeSitterExtractor`'s Python
+//! paths (src/extraction/tree-sitter.ts) plus languages/python.ts.
+//!
+//! Same porting contract as tsjs/java: behavior parity, bug-for-bug —
+//! including the quirks: decorates refs only fire for bare-identifier
+//! decorators (`@staticmethod` yes, `@app.route(...)` no — python's `call`
+//! kind isn't `call_expression`), module-level assignments always extract as
+//! `variable` (no isConst hook), and `self.method` fn-ref candidates carry the
+//! BARE attribute name. Python is not a TYPE_ANNOTATION language — no type
+//! refs anywhere. Files with parse errors defer to wasm.
+
+use crate::buffers::{
+    build_meta, edge_kind_index, node_kind_index, Arena, BoolFlags, EdgeRow, EmitOut, NodeRow,
+    RefRow, StrRef, Tables, FLAG_IS_ASYNC, FLAG_IS_STATIC, FUNCTION_REF_CODE, NONE, NONE_STR,
+};
+use crate::docstring::preceding_docstring;
+use crate::ids;
+use crate::textutil as util;
+use std::collections::{HashMap, HashSet};
+use tree_sitter::{Node, Parser};
+
+const MAX_VALUE_REF_NODES: usize = 20_000;
+
+struct Scope {
+    row: u32,
+    kind: &'static str,
+    name: String,
+}
+
+#[derive(Default)]
+struct Extra {
+    docstring: Option<String>,
+    signature: Option<String>,
+    is_async: Option<bool>,
+    is_static: Option<bool>,
+}
+
+struct ValueScope<'t> {
+    row: u32,
+    node: Node<'t>,
+    name: String,
+}
+
+struct Cand {
+    from: u32,
+    name: String,
+    line: u32,
+    column_byte: usize,
+    row: usize,
+}
+
+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("python").ok_or("no python grammar")?;
+    let t0 = std::time::Instant::now();
+    let mut parser = Parser::new();
+    parser
+        .set_language(&grammar)
+        .map_err(|e| format!("set_language(python) 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(crate::buffers::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() });
+
+    w.visit_node(tree.root_node());
+    w.flush_fn_ref_candidates();
+    w.flush_value_refs(tree.root_node());
+    w.stack.pop();
+
+    let duration_ms = t0.elapsed().as_secs_f64() * 1000.0;
+    let meta = build_meta(&w.tables, w.arena.len(), NONE_STR, duration_ms);
+    Ok(EmitOut {
+        meta,
+        nodes: w.tables.nodes,
+        edges: w.tables.edges,
+        refs: w.tables.refs,
+        arena: w.arena.into_vec(),
+    })
+}
+
+impl<'t> Walker<'t> {
+    fn text(&self, node: Node) -> &'t str {
+        &self.src[node.byte_range()]
+    }
+    fn line_of(&self, node: Node) -> u32 {
+        node.start_position().row as u32 + 1
+    }
+    fn col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.start_position().row, node.start_byte())
+    }
+    fn end_col_of(&self, node: Node) -> u32 {
+        util::col16(self.src, &self.line_starts, node.end_position().row, node.end_byte())
+    }
+    fn top_row(&self) -> u32 {
+        self.stack.last().map(|s| s.row).unwrap_or(0)
+    }
+    fn inside_class_like(&self) -> bool {
+        self.stack
+            .last()
+            .map(|s| matches!(s.kind, "class" | "struct" | "interface" | "trait" | "enum" | "module"))
+            .unwrap_or(false)
+    }
+
+    fn push_ref_at(&mut self, from_row: u32, name: &str, kind_code: u8, node: Node) {
+        let name_ref = self.arena.put(name);
+        self.tables.push_ref(&RefRow {
+            from_idx: from_row,
+            kind: kind_code,
+            line: self.line_of(node),
+            column: self.col_of(node),
+            reference_name: name_ref,
+            candidates: NONE_STR,
+            from_id_str: NONE_STR,
+        });
+        if kind_code == edge_kind_index("imports").unwrap() {
+            if util::simple_name().is_match(name) {
+                self.imported_names.insert(name.to_string());
+            } else if let Some(c) = util::qualified_import().captures(name) {
+                self.imported_names.insert(c[1].to_string());
+            }
+        }
+    }
+
+    fn create_node(&mut self, kind: &'static str, name: &str, node: Node<'t>, extra: Extra) -> Option<u32> {
+        if name.is_empty() {
+            return None;
+        }
+        let start_line = self.line_of(node);
+        let id = ids::node_id(self.file_path, kind, name, start_line);
+        let end_line = node.end_position().row as u32 + 1;
+
+        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_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 row = self.tables.push_node(&NodeRow {
+            kind: node_kind_index(kind).unwrap(),
+            visibility: 0,
+            flags,
+            start_line,
+            end_line,
+            start_column: self.col_of(node),
+            end_column: self.end_col_of(node),
+            name: name_ref,
+            qualified_name: qn_ref,
+            id: id_ref,
+            docstring: doc_ref,
+            signature: sig_ref,
+            decorators: NONE_STR,
+            type_parameters: NONE_STR,
+            return_type: NONE_STR,
+            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
+        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)
+    }
+
+    fn extract_name(&self, node: Node) -> 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()
+    }
+
+    /// pythonExtractor.getSignature: params + ` -> returnType`.
+    fn signature_of(&self, node: Node) -> Option<String> {
+        let params = node.child_by_field_name("parameters")?;
+        let mut sig = self.text(params).to_string();
+        if let Some(ret) = node.child_by_field_name("return_type") {
+            sig.push_str(" -> ");
+            sig.push_str(self.text(ret));
+        }
+        Some(sig)
+    }
+
+    /// pythonExtractor.isAsync: the PREVIOUS SIBLING token is `async`.
+    fn is_async(&self, node: Node) -> bool {
+        node.prev_sibling().map(|p| p.kind() == "async").unwrap_or(false)
+    }
+
+    /// pythonExtractor.isStatic: preceding decorator mentioning `staticmethod`.
+    fn is_static(&self, node: Node) -> bool {
+        if let Some(prev) = node.prev_named_sibling() {
+            if prev.kind() == "decorator" {
+                return self.text(prev).contains("staticmethod");
+            }
+        }
+        false
+    }
+
+    // --- visitNode ------------------------------------------------------------
+
+    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_definition" {
+            // functionTypes ∩ methodTypes: inside a class-like ⇒ method.
+            if self.inside_class_like() {
+                self.extract_method(node);
+            } else {
+                self.extract_function(node);
+            }
+            skip_children = true;
+        } else if kind == "class_definition" {
+            self.extract_class(node);
+            skip_children = true;
+        } else if kind == "assignment" && !self.inside_class_like() {
+            self.extract_variable(node);
+            self.scan_fn_ref_subtree(node, 0);
+            skip_children = true;
+        } else if kind == "import_statement" || kind == "import_from_statement" {
+            self.extract_import(node);
+        } else if kind == "call" {
+            self.extract_call(node);
+        }
+
+        if !skip_children {
+            for i in 0..node.named_child_count() {
+                if let Some(c) = node.named_child(i) {
+                    self.visit_node(c);
+                }
+            }
+        }
+    }
+
+    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" {
+            self.extract_call(node);
+        }
+
+        // Nested NAMED functions become their own nodes.
+        if kind == "function_definition" {
+            let name = self.extract_name(node);
+            if name != "<anonymous>" {
+                self.extract_function(node);
+                return;
+            }
+        }
+        if kind == "class_definition" {
+            self.extract_class(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: self.signature_of(node),
+            is_async: Some(self.is_async(node)),
+            is_static: Some(self.is_static(node)),
+        };
+        let Some(row) = self.create_node("function", &name, node, extra) else { return };
+        // (python is not a TYPE_ANNOTATION language — no type refs)
+        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: self.signature_of(node),
+            is_async: Some(self.is_async(node)),
+            is_static: Some(self.is_static(node)),
+        };
+        let Some(row) = self.create_node("method", &name, node, extra) else { return };
+        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),
+            ..Extra::default()
+        };
+        let Some(row) = self.create_node("class", &name, node, extra) else { return };
+
+        // Inheritance: `class Flask(Scaffold, Mixin):` — argument_list children.
+        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() == "argument_list" {
+                for j in 0..child.named_child_count() {
+                    let Some(arg) = child.named_child(j) else { continue };
+                    if matches!(arg.kind(), "identifier" | "attribute") {
+                        let name = self.text(arg).to_string();
+                        self.push_ref_at(row, &name, extends_kind, arg);
+                    }
+                }
+            }
+        }
+        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();
+    }
+
+    /// extractVariable's python branch: `left = right` at module scope.
+    fn extract_variable(&mut self, node: Node<'t>) {
+        let docstring = preceding_docstring(node, self.src);
+        let left = node.child_by_field_name("left").or_else(|| node.named_child(0));
+        let right = node.child_by_field_name("right").or_else(|| node.named_child(1));
+        let Some(left) = left else { return };
+        if !matches!(left.kind(), "identifier" | "constant") {
+            return;
+        }
+        let name = self.text(left).to_string();
+        let signature = right.map(|r| util::init_signature(self.text(r)));
+        // No isConst hook ⇒ always `variable` (UPPER_CASE constants included).
+        self.create_node("variable", &name, node, Extra { docstring, signature, ..Extra::default() });
+    }
+
+    fn extract_import(&mut self, node: Node<'t>) {
+        let import_text = self.text(node).trim().to_string();
+        let imports_kind = edge_kind_index("imports").unwrap();
+
+        if node.kind() == "import_from_statement" {
+            // Hook path: module_name field → import node + module ref, then
+            // per-name binding refs (emitPyFromImportRefs).
+            let Some(module_node) = node.child_by_field_name("module_name") else { return };
+            let module_name = self.text(module_node).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(), imports_kind, node);
+
+            // emitPyFromImportRefs: one `imports` ref per imported name.
+            for i in 0..node.named_child_count() {
+                let Some(child) = node.named_child(i) else { continue };
+                if child.start_byte() == module_node.start_byte()
+                    && child.end_byte() == module_node.end_byte()
+                {
+                    continue;
+                }
+                if child.kind() == "wildcard_import" {
+                    continue;
+                }
+                let name_node = match child.kind() {
+                    "aliased_import" => child
+                        .child_by_field_name("alias")
+                        .or_else(|| child.child_by_field_name("name"))
+                        .or_else(|| child.named_child(0)),
+                    "dotted_name" => Some(child),
+                    _ => None,
+                };
+                let Some(name_node) = name_node else { continue };
+                let raw = self.text(name_node);
+                let local = raw.rsplit('.').next().unwrap_or("");
+                if local.is_empty() {
+                    continue;
+                }
+                self.push_ref_at(parent, &local.to_string(), imports_kind, name_node);
+            }
+            return;
+        }
+
+        // import_statement: `import a.b, x as y` — one import node + module ref
+        // per dotted name (the python multi-import branch).
+        let parent = self.top_row();
+        for i in 0..node.named_child_count() {
+            let Some(child) = node.named_child(i) else { continue };
+            if child.kind() == "dotted_name" {
+                let name = self.text(child).to_string();
+                self.create_node(
+                    "import",
+                    &name,
+                    node,
+                    Extra { signature: Some(import_text.clone()), ..Extra::default() },
+                );
+                self.push_ref_at(parent, &name, imports_kind, child);
+            } else if child.kind() == "aliased_import" {
+                let dotted = (0..child.named_child_count())
+                    .filter_map(|j| child.named_child(j))
+                    .find(|c| c.kind() == "dotted_name");
+                if let Some(dotted) = dotted {
+                    let name = self.text(dotted).to_string();
+                    self.create_node(
+                        "import",
+                        &name,
+                        node,
+                        Extra { signature: Some(import_text.clone()), ..Extra::default() },
+                    );
+                    self.push_ref_at(parent, &name, imports_kind, dotted);
+                }
+            }
+        }
+    }
+
+    /// extractCall — python `call` through the generic tail (attribute callees).
+    fn extract_call(&mut self, node: Node<'t>) {
+        if self.stack.is_empty() {
+            return;
+        }
+        let func = node
+            .child_by_field_name("function")
+            .or_else(|| node.named_child(0));
+        let mut callee_name = String::new();
+
+        if let Some(func) = func {
+            if func.kind() == "attribute" {
+                // `property` and `field` fields don't exist on attribute —
+                // the generic path falls back to namedChild(1) (the attr name).
+                let property = func
+                    .child_by_field_name("property")
+                    .or_else(|| func.child_by_field_name("field"))
+                    .or_else(|| func.named_child(1));
+                if let Some(property) = property {
+                    let method_name = self.text(property);
+                    let receiver = func
+                        .child_by_field_name("object")
+                        .or_else(|| func.child_by_field_name("operand"))
+                        .or_else(|| func.child_by_field_name("argument"))
+                        .or_else(|| func.named_child(0));
+                    if let Some(r) = receiver {
+                        if is_literal_receiver(r.kind()) {
+                            return;
+                        }
+                    }
+                    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 = format!("{receiver_name}.{method_name}");
+                        } else {
+                            callee_name = method_name.to_string();
+                        }
+                    } else {
+                        callee_name = method_name.to_string();
+                    }
+                }
+            } else {
+                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();
+            }
+            let from = self.top_row();
+            self.push_ref_at(from, &callee_name.clone(), edge_kind_index("calls").unwrap(), node);
+        }
+    }
+
+    /// extractDecoratorsFor — python decorators are PRECEDING SIBLINGS inside
+    /// decorated_definition. Only bare-identifier decorators yield a target
+    /// (python's `call` kind isn't `call_expression`, and `attribute` isn't in
+    /// the target-kind list — mirrored exactly).
+    fn extract_decorators_for(&mut self, decl: Node<'t>, decorated_row: u32) {
+        for i in 0..decl.named_child_count() {
+            if let Some(child) = decl.named_child(i) {
+                self.consider_decorator(child, decorated_row);
+            }
+        }
+        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 mut name = self.text(target).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);
+            }
+        }
+        let name = name.trim().to_string();
+        if name.is_empty() {
+            return;
+        }
+        self.push_ref_at(decorated_row, &name, edge_kind_index("decorates").unwrap(), n);
+    }
+
+    // --- fn refs (PYTHON_SPEC) ------------------------------------------------------
+
+    fn maybe_capture_fn_refs(&mut self, node: Node<'t>) {
+        let (mode, field): (&str, &str) = match node.kind() {
+            "argument_list" => ("args", ""),
+            "assignment" => ("rhs", "right"),
+            "keyword_argument" => ("value", "value"),
+            "pair" => ("value", "value"),
+            "list" => ("list", ""),
+            _ => return,
+        };
+        if self.stack.is_empty() {
+            return;
+        }
+        let from = self.top_row();
+
+        let mut values: Vec<Node> = Vec::new();
+        match mode {
+            "args" | "list" => {
+                for i in 0..node.named_child_count() {
+                    if let Some(c) = node.named_child(i) {
+                        values.push(c);
+                    }
+                }
+            }
+            "rhs" => {
+                if let Some(rhs) = node.child_by_field_name(field) {
+                    let lhs_text = node
+                        .child_by_field_name("left")
+                        .map(|l| self.text(l))
+                        .unwrap_or("");
+                    let lhs_last = util::lhs_last_name()
+                        .captures(lhs_text)
+                        .and_then(|c| c.get(1))
+                        .map(|m| m.as_str());
+                    if !(lhs_last.is_some() && lhs_last == Some(self.text(rhs).trim())) {
+                        values.push(rhs);
+                    }
+                }
+            }
+            _ => {
+                if let Some(v) = node.child_by_field_name(field) {
+                    values.push(v);
+                }
+            }
+        }
+
+        for v in values {
+            let (name, anchor) = match v.kind() {
+                "identifier" => (self.text(v).to_string(), v),
+                // `self.handle_click` — object EXACTLY `self`; BARE attr name.
+                "attribute" => {
+                    let obj = v.child_by_field_name("object");
+                    let attr = v.child_by_field_name("attribute");
+                    match (obj, attr) {
+                        (Some(o), Some(a))
+                            if o.kind() == "identifier" && self.text(o) == "self" =>
+                        {
+                            (self.text(a).to_string(), a)
+                        }
+                        _ => continue,
+                    }
+                }
+                _ => continue,
+            };
+            if name.is_empty() || is_stoplisted(&name) {
+                continue;
+            }
+            let p = anchor.start_position();
+            self.fn_ref_cands.push(Cand {
+                from,
+                name,
+                line: p.row as u32 + 1,
+                column_byte: anchor.start_byte(),
+                row: p.row,
+            });
+        }
+    }
+
+    fn scan_fn_ref_subtree(&mut self, node: Node<'t>, depth: u32) {
+        if depth > 12 {
+            return;
+        }
+        // Halts at functionTypes ∪ the fixed arrow/lambda list — python's
+        // `lambda` kind is NOT in that list (mirrored).
+        if depth > 0
+            && matches!(
+                node.kind(),
+                "function_definition" | "arrow_function" | "function_expression" | "lambda_literal"
+                    | "lambda_expression"
+            )
+        {
+            return;
+        }
+        self.maybe_capture_fn_refs(node);
+        for i in 0..node.named_child_count() {
+            if let Some(c) = node.named_child(i) {
+                self.scan_fn_ref_subtree(c, depth + 1);
+            }
+        }
+    }
+
+    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 refs -------------------------------------------------------------------
+
+    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 — python's declarator shape is `assignment`.
+        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() {
+                            if let Some(c) = left.named_child(i) {
+                                if c.kind() == "identifier" {
+                                    let nm = self.text(c);
+                                    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);
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// 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 membership (shared table; python names among them).
+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"
+    )
+}
+
+fn opt_str(arena: &mut Arena, s: Option<&str>) -> StrRef {
+    match s {
+        Some(s) => arena.put(s),
+        None => NONE_STR,
+    }
+}

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

@@ -30,7 +30,7 @@ Work top to bottom; each step has a section below with the detail.
       dispatch), so the Mac headline barely moves (~11.3→11.1s; parse-loop 5.0→4.4s);
       dispatch), so the Mac headline barely moves (~11.3→11.1s; parse-loop 5.0→4.4s);
       the win shows where worker CPU binds (dubbo on 2-CPU: 28→22.5s, ~1.25×). The
       the win shows where worker CPU binds (dubbo on 2-CPU: 28→22.5s, ~1.25×). The
       identified follow-up lever for the Mac number is decode-direct-to-store (§4c).
       identified follow-up lever for the Mac number is decode-direct-to-store (§4c).
-- [ ] **R5. Port Python, Go.** (§4)
+- [x] **R5. Port Python, Go.** (§4) — **ported + gates passed + DEFAULT-ON 2026-07-16, §4e.**
 - [ ] **R6. Kernel-scale re-validation** in the cg1212 container (expect parse 6m → ~2m). (§6)
 - [ ] **R6. Kernel-scale re-validation** in the cg1212 container (expect parse 6m → ~2m). (§6)
 - [ ] **R7. Long-tail languages opportunistically** per the tracker; T3 may stay TS forever. (§4)
 - [ ] **R7. Long-tail languages opportunistically** per the tracker; T3 may stay TS forever. (§4)
 - [ ] **P1. Kernel-scale resolution speed** — the 19.5-min sequential wall at 2M nodes. (§7a)
 - [ ] **P1. Kernel-scale resolution speed** — the 19.5-min sequential wall at 2M nodes. (§7a)
@@ -264,6 +264,29 @@ Default routing: `DEFAULT_ROUTED = {typescript, tsx, javascript, jsx}` in
   Windows VM still deferred (same fallback rationale as §4b).
   Windows VM still deferred (same fallback rationale as §4b).
 - Default routing now includes `java`.
 - Default routing now includes `java`.
 
 
+### 4e. R5 — Python + Go PORTED + gates PASSED + DEFAULT-ON (2026-07-16)
+
+- **Walkers:** `codegraph-kernel/src/python.rs` + `src/go.rs` (the java.rs pattern).
+  Python: decorated_definition docstring/decorator handling (decorates only for
+  bare-identifier decorators — the `call`-kind quirk mirrored), fn-in-class → method,
+  module assignments always `variable` (no isConst hook), from-import binding refs,
+  `self.x` fn-ref candidates as BARE names, attribute callees via the namedChild(1)
+  fallback. Go: receiver methods with `Recv::name` QNs + first-earlier-struct
+  contains edges, type_spec → struct/interface classification (embedding → extends;
+  interface method_elems → method nodes), composite-literal instantiates keeping the
+  package qualifier, top-level var/const initializer walks attributed to the symbol
+  (#693), 2-hop field chains (#1276), `New().Method()` re-encode (#645/#608),
+  GO_SPEC fn-ref layers (literal_element/expression_list fan-out).
+- **Grammars:** crates tree-sitter-python 0.23.6 (bffb65a) + tree-sitter-go 0.23.4
+  (3c3775f); wasm vendored from the same tags, parser.c sha-matched (both were
+  2023-era in tree-sitter-wasms).
+- **Parity:** extraction sweeps 100% — flask 83/83, django 3,035/3,038 (+3 error-file
+  deferrals), gin 99/99, prometheus 978/979 (+1). Full-init dumps byte-identical:
+  flask (10,833 rows), gin (17,540), **django (360,794)**, **prometheus (213,758)**.
+  Torture fixtures in `npm test`. Even Mac-side init already moves where extraction
+  matters: prometheus 5.7→4.5s, django 9.0→8.7s.
+- Default routing now: typescript, tsx, javascript, jsx, java, python, go.
+
 ### 4d. Direct-to-store decode (2026-07-16) — and where the wall ACTUALLY is
 ### 4d. Direct-to-store decode (2026-07-16) — and where the wall ACTUALLY is
 
 
 Kernel-routed files now ship their flat buffers from the parse worker all the way
 Kernel-routed files now ship their flat buffers from the parse worker all the way
@@ -308,8 +331,8 @@ parity before porting the language.
 |---|---|---|---|---|---|
 |---|---|---|---|---|---|
 | typescript, tsx, javascript, jsx | `languages/typescript.ts`, `javascript.ts` + shared branches | T1 | crates.io | First target. Value-reference edges (#895/#897) and component recognition (#841 forwardRef/memo/styled) must survive — they're extraction-side. Largest test surface; gate is strictest here. **PORTED + GATE PASSED + DEFAULT-ON (§4a/§4b); erroring files defer to wasm per-file.** | ✅ |
 | typescript, tsx, javascript, jsx | `languages/typescript.ts`, `javascript.ts` + shared branches | T1 | crates.io | First target. Value-reference edges (#895/#897) and component recognition (#841 forwardRef/memo/styled) must survive — they're extraction-side. Largest test surface; gate is strictest here. **PORTED + GATE PASSED + DEFAULT-ON (§4a/§4b); erroring files defer to wasm per-file.** | ✅ |
 | java | `languages/java.ts` | T1 | crates.io | Second target; unlocks the dubbo-parity claim. Lombok member synthesis (#912) is a NODE synthesizer hook in extraction (`synthesizeMembers`) — port or keep as TS post-pass. **PORTED incl. Lombok + gate passed + DEFAULT-ON (§4c).** | ✅ |
 | java | `languages/java.ts` | T1 | crates.io | Second target; unlocks the dubbo-parity claim. Lombok member synthesis (#912) is a NODE synthesizer hook in extraction (`synthesizeMembers`) — port or keep as TS post-pass. **PORTED incl. Lombok + gate passed + DEFAULT-ON (§4c).** | ✅ |
-| python | `languages/python.ts` | T1 | crates.io | Third. Decorator extraction feeds framework route detection — parity required. | ☐ |
-| go | `languages/go.ts` | T1 | crates.io | Third (tie). Value-reference edges ship here too (#897). | ☐ |
+| python | `languages/python.ts` | T1 | crates.io | Third. Decorator extraction feeds framework route detection — parity required. **PORTED + DEFAULT-ON (§4e).** | ✅ |
+| go | `languages/go.ts` | T1 | crates.io | Third (tie). Value-reference edges ship here too (#897). **PORTED + DEFAULT-ON (§4e).** | ✅ |
 | ruby, php | dedicated files | T1 | crates.io | Straightforward; PHP property-receiver shapes (#1220/#1251) are RESOLUTION-side, unaffected. | ☐ |
 | ruby, php | dedicated files | T1 | crates.io | Straightforward; PHP property-receiver shapes (#1220/#1251) are RESOLUTION-side, unaffected. | ☐ |
 | csharp | `languages/csharp.ts` | T1 | crates.io | Plain. | ☐ |
 | csharp | `languages/csharp.ts` | T1 | crates.io | Plain. | ☐ |
 | rust, dart, scala, lua, luau, r | dedicated files | T1 | crates.io (luau/r/scala: verify crate freshness vs our wasm) | Long-tail T1; port opportunistically after the big five. | ☐ |
 | rust, dart, scala, lua, luau, r | dedicated files | T1 | crates.io (luau/r/scala: verify crate freshness vs our wasm) | Long-tail T1; port opportunistically after the big five. | ☐ |

+ 2 - 2
scripts/kernel-parity.mjs

@@ -39,11 +39,11 @@ if (paths.length === 0) {
   process.exit(2);
   process.exit(2);
 }
 }
 
 
-const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java']);
+const KERNEL_LANGS = new Set(['typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go']);
 const EXTS = new Map([
 const EXTS = new Map([
   ['.ts', 'typescript'], ['.mts', 'typescript'], ['.cts', 'typescript'],
   ['.ts', 'typescript'], ['.mts', 'typescript'], ['.cts', 'typescript'],
   ['.tsx', 'tsx'], ['.js', 'javascript'], ['.mjs', 'javascript'],
   ['.tsx', 'tsx'], ['.js', 'javascript'], ['.mjs', 'javascript'],
-  ['.cjs', 'javascript'], ['.jsx', 'jsx'], ['.java', 'java'],
+  ['.cjs', 'javascript'], ['.jsx', 'jsx'], ['.java', 'java'], ['.py', 'python'], ['.pyw', 'python'], ['.go', 'go'],
 ]);
 ]);
 
 
 /** Collect candidate files. */
 /** Collect candidate files. */

+ 3 - 1
src/extraction/grammars.ts

@@ -280,6 +280,8 @@ export async function initGrammars(): Promise<void> {
  *   - tree-sitter/tree-sitter-typescript v0.23.2 (f975a62) → typescript + tsx
  *   - tree-sitter/tree-sitter-typescript v0.23.2 (f975a62) → typescript + tsx
  *   - tree-sitter/tree-sitter-javascript v0.25.0 (44c892e) → javascript + jsx
  *   - tree-sitter/tree-sitter-javascript v0.25.0 (44c892e) → javascript + jsx
  *   - tree-sitter/tree-sitter-java v0.23.5 (94703d5) → java
  *   - tree-sitter/tree-sitter-java v0.23.5 (94703d5) → java
+ *   - tree-sitter/tree-sitter-python v0.23.6 (bffb65a) → python
+ *   - tree-sitter/tree-sitter-go v0.23.4 (3c3775f) → go
  * Built from each repo's CHECKED-IN parser.c (no `generate`) with
  * Built from each repo's CHECKED-IN parser.c (no `generate`) with
  * tree-sitter-cli 0.25.10 `build --wasm` — the same tables crates.io compiles
  * tree-sitter-cli 0.25.10 `build --wasm` — the same tables crates.io compiles
  * (parser.c sha-matched against the crates.io tarball).
  * (parser.c sha-matched against the crates.io tarball).
@@ -289,7 +291,7 @@ export async function initGrammars(): Promise<void> {
 const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
 const VENDORED_WASM_LANGS: ReadonlySet<GrammarLanguage> = new Set([
   'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
   'pascal', 'scala', 'lua', 'luau', 'csharp', 'r', 'cfml', 'cfscript', 'cfquery',
   'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
   'cobol', 'vbnet', 'erlang', 'terraform', 'arkts', 'nix',
-  'typescript', 'tsx', 'javascript', 'jsx', 'java',
+  'typescript', 'tsx', 'javascript', 'jsx', 'java', 'python', 'go',
 ]);
 ]);
 
 
 /** Absolute path of a language's grammar WASM (vendored or tree-sitter-wasms). */
 /** Absolute path of a language's grammar WASM (vendored or tree-sitter-wasms). */

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

@@ -39,6 +39,8 @@ const DEFAULT_ROUTED: ReadonlySet<Language> = new Set<Language>([
   'javascript',
   'javascript',
   'jsx',
   'jsx',
   'java',
   'java',
+  'python',
+  'go',
 ]);
 ]);
 
 
 /**
 /**

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


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