Why we model codebases as graphs, not file trees
A file tree tells you where code lives. It says nothing about what calls what, which test covers which function, or which requirement a symbol satisfies — exactly where agents waste their time.
Open any repository and the first thing your editor shows you is a tree: folders inside folders, files inside those. It's a familiar, comfortable shape. It's also the wrong one for an AI agent trying to make a change.
A tree answers exactly one question — where does this file sit on disk? But almost every question that matters during a real task is a question about relationships: what calls validateSession, which test exercises it, what route exposes it, and which requirement says it has to reject expired tokens. None of those live in the folder structure. They live in the edges between things.
The cost of re-deriving structure
When an agent only has a file tree and a search tool, it rebuilds structure the expensive way: it greps, it reads whole files, it follows imports by hand, and it holds the resulting mental model in its context window. Then the session ends, the context evaporates, and next time it does the whole thing again.
The single most common action in a typical agent session isn't writing code — it's re-reading files the agent already read in a previous session, to reconstruct relationships it had already worked out once.
That's the tax: structure gets re-derived from raw text, over and over, priced in input tokens every time. The fix isn't a bigger context window. It's storing the structure once and letting the agent query it.
From parse tree to knowledge graph
SpecShip parses every file with tree-sitter, which gives a concrete syntax tree per file. That's the raw material, not the product. We walk those trees to extract typed nodes — functions, classes, methods, routes, components, and specs — and then resolve the edges between them: calls, imports, extends, implements, references, and "tested-by".
The result is a single typed graph stored in a local SQLite database. The node families carry a color that stays meaningful everywhere in the product:
- Code — functions, classes, methods. The bulk of the graph.
- Spec — requirements written in RFC-2119 prose, linked to the symbols that satisfy them.
- Test — test cases, edged back to the code they cover.
- Route — HTTP/RPC entry points the frameworks expose, the doors into the system.
// one node per declaration, edges resolved in a second pass for (const decl of walk(tree.rootNode)) { const node = graph.upsert({ kind: decl.kind, // 'function' | 'class' | 'route' | 'component'… name: decl.name, file: rel, span: [decl.startIndex, decl.endIndex], }); for (const call of decl.calls) { graph.link(node, call.target, 'calls'); } }
Some edges can't be read straight off the syntax — a route handler wired up through a framework, a callback invoked dynamically, a React component re-rendering a child. Those we synthesize: inferred edges, drawn dashed in the UI and tagged provenance: 'heuristic' in the data so you always know what was extracted versus what was guessed.
What the graph unlocks
Once structure is a queryable object instead of something you reconstruct from text, a lot falls out of it almost for free.
Bounded, structural retrieval
Instead of "read this 1,400-line file," an agent asks for validateSession plus its callers, two hops deep. It gets back a handful of qualified symbols and their source — not every line that happened to match a string. In practice that's the difference between tens of thousands of tokens and a few hundred.
Drift you can actually see
Because specs are nodes with edges to the code that satisfies them, a refactor that moves or renames a symbol breaks an edge. That broken edge is drift — and it lands in a review queue instead of silently shipping.
A file tree is a map of the building. A knowledge graph is a map of who talks to whom inside it.
— from the SpecShip design notesFlows across dynamic dispatch
Asking "how does X reach Y" usually means following a chain that crosses callbacks, event emitters, and framework routes — exactly the hops a text search can't follow. Because those hops are synthesized into the graph, one query can connect a state change to the on-screen render across several indirection boundaries, each step showing the source and where the edge was wired.
Why local, why SQLite
The whole graph lives on your machine in a single SQLite file. No service to call, nothing leaving your laptop, and queries that return in single-digit milliseconds. Your code and your transcripts are yours; the graph is just an index on top of them.
File trees aren't going away — they're still how we organize bytes on disk. But for the questions an agent actually asks, the tree was never the answer. The graph is.