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.
Every claim on this page checked against the product source at a pinned revision — 2026-07-20 @ 804cbf79.
What this is
The codebase graph is a local projection of a linked repository: every module and declared symbol gets a structural row, and every extracted import or call site gets a resolution attempt. Only a unique match admitted by the resolver becomes an exact edge. The structure comes from real syntax trees rather than a model judgment. On top sits a separate semantic layer: evidence-gated module summaries and code claims produced by the Mining pipeline.
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 observes the source structure deterministically, without paying a model to decide what the syntax says.
How it works
Three core tables carry the observed 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). Resolution
attempts and their admission receipts sit beside those rows, keeping candidate,
ambiguous, stale and exact-current states distinct. 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. Hybrid mode falls back explicitly to full-text search when
dense assets are unavailable; semantic-only mode returns an error rather than
quietly pretending it ran.
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 The homepage carries the dated aggregate capture for Sophia’s dogfood workspace. This page keeps the read examples focused on response shape because the code graph is actively reconciling and a bare count would become stale faster than the explanation around it.
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
The current resolver is deterministic, but it is not yet compiler- or type-aware. The landed legacy-AST adapter resolves relative and project-local imports, symbols in the same module, and exported symbols reached through an already-resolved import. A whole-repository short-name match is only a candidate; it cannot mint a canonical edge. Unsupported or ambiguous sites remain labelled attempts with their receipts, and a strict research answer is directed to current source rather than treating the candidate as topology.
That is a narrower claim than full semantic resolution. Overloaded symbols,
dynamic dispatch, framework wiring and cross-language calls still require
source inspection or a future compiler-aware adapter. Caller and callee reads
can also expose compatibility rows labelled legacy_resolution_unknown;
exact_current is the receipt-backed class. The read surface preserves that
label instead of implying every stored edge cleared the new admission path.
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.