M2-B - cross-repo symbol emission (ingester) Plan¶
REQUIRED SUB-SKILL: superpowers:subagent-driven-development.
Goal: Emit cross-repo provides/requires symbol rows from the Go, Python, and JavaScript analyzers and push them in GraphPush.Symbols.
Spec: docs/superpowers/specs/2026-06-06-code-graph-full-scope-design.md (M2 keying rules + M2-B).
CGo required (CGO_ENABLED=1). Read the existing internal/analyze/{golang,python,javascript}.go, internal/contract/contract.go, internal/config/config.go, cmd/tatara-ingest/run.go before editing.
Task 1: Contract mirror¶
Files: internal/contract/contract.go, internal/contract/contract_shape_test.go.
- Add (mirroring tatara-memory exactly):
- Add
Symbols []SymbolRowjson:"symbols,omitempty"` toGraphPush`. - Extend
contract_shape_test.goto assert aSymbolRowmarshals to keyssymbol,lang,kind,role,entity_id,src_fileand thatGraphPushwith no symbols omits thesymbolskey. - Commit
feat(contract): cross-repo SymbolRow + GraphPush.Symbols.
Task 2: Result.Symbols + config¶
Files: internal/analyze/analyzer.go, internal/config/config.go, internal/config/config_test.go.
- Add
Symbols []contract.SymbolRowtoanalyze.Result. - Add
CrossRepoPrefix stringtoconfig.Config, loaded from keycross-repo-prefix, defaultgithub.com/szymonrychu/. Add a config test. - Commit
feat(analyze,config): Result.Symbols and crossRepoPrefix.
Task 3: Go analyzer provides/requires¶
Files: internal/analyze/golang.go, internal/analyze/golang_test.go.
The Go analyzer needs the module path (from go.mod) to decide intra- vs cross-module, and the org prefix to filter requires. Pass the prefix into the analyzer: change NewGo() to NewGo(crossRepoPrefix string) (update registry.go and any callers to pass it; Default() will need the prefix - see Task 6 wiring, or default it in Default() to github.com/szymonrychu/). Simplest: NewGo(prefix string); Default() keeps a no-arg shape by reading a package default - BUT to avoid hidden globals, give Default(cfg) a prefix param. Decide and keep consistent across Task 6.
- Behavior (add to
golang_test.go, expand the fixture module): for a fixture whose module isexample.com/sample, an exported funcpkg.Fproduces aprovidesSymbolRow{Symbol:"example.com/sample/pkg.F", Lang:"go", Kind:"func", Role:"provides", EntityID:"go:func:example.com/sample/pkg.F", SrcFile:"pkg/pkg.go"}. A reference to an external package under the configured prefix (e.g. add a fixture importinggithub.com/szymonrychu/other/thing.Dois not buildable in a fixture) - so test requires via a UNIT on the helper instead: provide a small fixture that imports another in-fixture module path under the prefix is impractical. Simpler testable rule: make the prefix configurable in the test toexample.com/so that references toexample.com/sample/...from a DIFFERENT module are "external". Since the fixture is one module, testrequiresby setting prefix to a value that matches the fixture's own deps if any, OR unit-test the classifier function directly: extractfunc classifyRef(objPkgPath, modulePath, prefix string) (emit bool, symbol string)and unit-test it: in-module -> no requires; external under prefix -> requires with symbol=objPkgPath+"."+name; external not under prefix (stdlib) -> no requires. - Implement: after building entities, for each exported package-level decl emit a
providesSymbolRow (Kind from the entity type: func/type/method). For requires, walkTypesInfo.Uses; for each used object whoseobj.Pkg() != nil, pkg path is outside the current module, and starts withcrossRepoPrefix, emit arequiresSymbolRow{Symbol: objPkgPath+"."+obj.Name(), Lang:"go", Kind: kindOf(obj), Role:"requires", EntityID: <the using entity's ID>, SrcFile: <the using file>}(dedup by (symbol, entity_id)). SrcFile must be in scope. - Run
CGO_ENABLED=1 go test ./internal/analyze/... -run TestGogreen; commitfeat(analyze): Go provides/requires cross-repo symbols.
Task 4: Python provides/requires¶
Files: internal/analyze/python.go, internal/analyze/python_test.go.
- provides: each top-level (module-level) func/class -> SymbolRow
{Symbol:"<dotted.module>.<name>", Lang:"python", Kind:"func"|"class", Role:"provides", EntityID: <py entity id>, SrcFile}. - requires: for each import that does NOT resolve to an in-repo module (the existing unresolved-import detection), emit a
requiresSymbolRow{Symbol:"<imported dotted name>", Lang:"python", Kind:"module", Role:"requires", EntityID: <importing module entity id>, SrcFile}. - Test (extend python_test.go): assert a provides row for a top-level def and a requires row for an unresolved external import.
- Commit
feat(analyze): Python provides/requires cross-repo symbols.
Task 5: JavaScript provides/requires¶
Files: internal/analyze/javascript.go, internal/analyze/javascript_test.go.
- provides: exported top-level funcs/classes -> SymbolRow
{Symbol:"<module-rel-path>::<name>", Lang:"javascript", Kind:..., Role:"provides", EntityID, SrcFile}. - requires: imports that do not resolve to an in-repo module ->
requiresSymbolRow{Symbol:"<import specifier>", Lang:"javascript", Kind:"module", Role:"requires", EntityID:<importing module id>, SrcFile}. - Test: provides for an exported func; requires for a bare external import (e.g.
import x from 'react'). - Commit
feat(analyze): JavaScript provides/requires cross-repo symbols.
Task 6: Wire symbols through run + registry¶
Files: cmd/tatara-ingest/run.go, internal/analyze/registry.go, cmd/tatara-ingest/main.go (prefix plumb).
-
Default(crossRepoPrefix string)passes the prefix toNewGo. Update all callers (run.gobuilds the registry withcfg.CrossRepoPrefix; pass it throughoptions). Keep Python/JS constructors unchanged (they don't need the prefix; their requires are "unresolved in-repo" which is prefix-independent). - In
run.go, aggregateres.Symbolsintoagg.Symbolsand setGraphPush.Symbols = agg.Symbols. Add the symbol count to the completion slog line. - Update
cmd/tatara-ingest/run_test.goif the registry/Default signature changed. - Run full
CGO_ENABLED=1 go test ./...,golangci-lint run ./...clean. Commitfeat(cmd): push cross-repo symbols.
Task 7: Verify¶
-
CGO_ENABLED=1 go build ./...; fullCGO_ENABLED=1 go test ./... -race -count=1green; lint clean. All emitted SymbolRow.SrcFile values are repo-relative and within the analyzer'sfilesarg (the scope rule still holds for symbols).