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.

repo on disk → tree-sitter → three tables → navigation tools
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.