The codebase graph
Tree-sitter parses a linked repository locally into modules, symbols, and edges, so a coding agent finds a definition, walks its neighborhood, and reads its source in four typed calls instead of a round of file globbing.
Verified 2026-07-09 @ dfd671a3
What this is
The codebase graph is a local, CPU-only projection of a linked repository: every module gets a row, every declared symbol gets a row, every import and call gets a typed edge, all built by parsing real ASTs with tree-sitter — no model call in the ingest path. On top of that structural layer sits an optional second layer: agent-submitted module summaries, produced by the code-mining pipeline described on Mining, searchable once they exist.
Why it exists
A coding agent dropped into an unfamiliar repo pays the same tax every session: glob the file tree, open a handful of anchor files, follow an import by hand, grep for a caller, and only then risk an edit. None of that work survives past the session — a fresh Claude Code or Codex process on the same unchanged repo pays it again. The codebase graph exists to make that orientation pass a typed database read instead of a re-read of the raw files, using extraction that runs once at ingest time (parsing is deterministic and idempotent on content hash, so re-running it costs nothing when nothing changed) rather than once per session.
The point is context economics as much as speed: a grep cascade fills the agent’s context window with exploratory noise, and a long noisy context degrades reasoning for the rest of the session. Graph-first localization keeps working context dense with decision-relevant facts. The win condition is narrower reads, not zero reads — an agent about to edit or assert should still open the real source; it should just arrive there in three targeted files instead of thirty exploratory ones.
Parsing with tree-sitter instead of routing code through the same LLM extraction path as prose documents is a deliberate split, not an oversight: the ingest module’s own header comment states the reasoning plainly — an LLM extractor is out-of-distribution on CamelCase identifiers, can’t reliably recover cross-file edges, and the claim-graph shape (subject-predicate-object) doesn’t fit “function A calls function B.” A parser that reads the actual grammar gets that structure right, deterministically, for $0 per file.
How it works
Three tables carry the structure, one row per real thing in the code:
subscriber_code_modules (one row per file — language, content hash, ingest
timestamps), subscriber_code_symbols (one row per declared class, function,
interface, method, type, or const, each with a line range and an exported
flag), and subscriber_code_edges (typed imports/calls/extends/
implements relationships between symbols and modules, deduplicated on
(edge_kind, from, to) so a re-ingest updates rather than duplicates). Six languages
are wired into the walker registry today — TypeScript
(covering .ts/.tsx/.mts/.cts plus .jsx), Python, Rust, Go, Java, and
C# — each contributing a small, focused walker module behind a shared LanguageModule
interface; the ingest driver itself is language-agnostic.
A background pass re-scans linked repositories on a five-minute cycle, checks a staleness tracker for changed or missing paths, and re-parses only those — idempotent on content hash, so a scan that finds nothing changed is a cheap no-op write, not a full re-ingest.
Two read shapes sit on top of the three tables. A skeleton
(sophia.get_module_skeleton) is a module’s imports plus every symbol’s
signature, JSDoc, and a short body preview — paged at 100 symbols per call,
lazy-filled on first request, sized to stay well under typical context limits
even for large files. A summary is different in kind: a three-section,
agent-written account (what_it_does / api_surface / dependencies)
produced by the mining pipeline, not the parser — sophia.search_code_summaries
runs hybrid BM25-plus-dense retrieval (reciprocal-rank fusion) over whichever
modules have one.
flowchart LR
R["Repo on disk"] -->|"5-min staleness scan"| P["tree-sitter parse
(6 languages, CPU only)"]
P --> M[("code_modules")]
P --> S[("code_symbols")]
P --> E[("code_edges")]
M --> NAV["find_modules_by_symbol
find_modules_by_import
module_neighborhood
query_codebase"]
S --> NAV
E --> NAV
NAV --> A["Your agent"]
MINE["Mining (separate, agent-paid)"] -.->|"Tier-2 summary"| SUM[("code_module_summaries")]
SUM --> NAV A live read against this dogfood repo, captured this session:
const overview = await sophia.query_codebase({
entity_id: 'bb3ad5a2-3336-4b4d-a220-99a72bbc5207', // "Ouroboros Repo"
kind: 'overview',
});
// → {
// module_count: 1407,
// symbol_count_by_kind: { function: 4759, const: 2034, interface: 1352,
// type: 432, method: 516, class: 33 },
// language_breakdown: { typescript: 1119, tsx: 285, python: 3 },
// edge_count: 22488,
// freshness: { commits_behind: 0, status: 'fresh' },
// } The language breakdown reflects this one repository’s actual file mix, not the walker registry’s ceiling — this codebase happens to be TypeScript-heavy with a few Python scripts; a Rust or Go repo linked the same way would show up under its own language keys using the same six languages.
What your agent does with it
The navigation family is four tools that chain in one direction: find a symbol, walk its neighborhood, read its shape, then its source — each step narrowing from “where” to “what,” and each one a database read, not a re-parse.
// 1. Where is this defined? (find_modules_by_symbol)
const hit = await sophia.find_modules_by_symbol({
entity_id: repoId, symbol_name: 'resolveModuleSkeleton',
});
// → { items: [{ rel_path: 'proxy/src/mcp/tools/moduleHelpers.ts',
// qualified_name: 'moduleHelpers.resolveModuleSkeleton',
// start_line: 178, end_line: 255, exported: true }], total: 1 }
// 2. What's around it? One round trip instead of five chained queries.
const nbhd = await sophia.module_neighborhood({
entity_id: repoId, rel_path: 'proxy/src/mcp/tools/moduleHelpers.ts',
});
// → { module: { deep_analyze_intent: 'done' },
// summary: { what_it_does: 'Provides resolver helpers for code-module
// graph reads, including source slices, skeleton retrieval, ...' },
// symbols: [ /* 4 exported functions */ ], callers: [], callees: [],
// imports: [{ target_module_path: 'proxy/src/mcp/tools/types.ts' }],
// imported_by: [{ source_module_path: 'proxy/src/mcp/tools.ts' },
// { source_module_path: 'proxy/src/mcp/tools/codebaseTools.ts' }] }
// 3. Full shape before spending a source read.
const skeleton = await sophia.get_module_skeleton({
entity_id: repoId, rel_path: 'proxy/src/mcp/tools/moduleHelpers.ts',
});
// → { symbols: [{ short_name: 'resolveFindModulesBySymbol',
// signature: 'export function resolveFindModulesBySymbol(\n db: ...',
// jsdoc: '// Backs sophia.find_modules_by_symbol...' }, ... ] }
// 4. Only now, the actual body — scoped to one symbol, not the whole file.
const source = await sophia.get_module_source({
entity_id: repoId, rel_path: 'proxy/src/mcp/tools/moduleHelpers.ts',
symbol: 'resolveFindModulesBySymbol',
});
// → { start_line: 263, end_line: 303, source_text: 'export function ...' } find_modules_by_import runs the same shape in reverse — given
proxy/src/mcp/tools/types.ts, it returned the four modules that import it
(tools.ts, helpers.ts, knowledgeQuery.ts, moduleHelpers.ts) in this
session’s live check — answering “who depends on this?” without a repo-wide
grep. query_codebase covers the remaining shapes one tool at a time
(modules, symbols, callers, callees, imports) when a single filtered
list, rather than a full neighborhood, is what’s needed.
Boundaries
Call-edge resolution is name-only by design, not a bug that’s slipped through:
the ingest module’s own scope-cut notes say a call site is matched against a
symbol by its short name, with no type-level resolution to confirm it’s the
same query or add or poll across files. On a codebase with many
generically-named helpers this shows up concretely — a caller list for a
common-word symbol can include unrelated modules that happen to declare a
function with the same short name. Treat a caller/callee list as a strong
lead, not a proof, until scope-aware resolution ships; module_neighborhood
and query_codebase({ kind: 'callers' | 'callees' }) both inherit this limit,
since both read the same edge table.
The graph also only knows what tree-sitter can see in the text — it has no
runtime trace, no test-coverage mapping, and no cross-language edges (a
TypeScript module calling into a Python script via subprocess shows up as
nothing in code_edges, because there’s no import statement tree-sitter can
follow). What the graph’s rows mean once a fact points at one is
Truth; how a symbol’s structure gets covered in the wave the
mining pipeline processes is Mining; how the resulting
summaries get ranked once you search past a single repo is
Search & Retrieval; the full tool catalog these
calls are drawn from is MCP Surface; and the daemon
that stores all three tables locally is The State Layer.