CGraph turns a source tree into a queryable graph by parsing each file with a tree-sitter grammar, then resolving the raw nodes into a connected graph of imports, calls, and relations. Eleven languages go through vendored tree-sitter grammars; a handful more use regex or structured extraction. The whole path is deterministic — the same tree yields the same graph every time.
This post walks the extraction pipeline stage by stage, using CGraph's own indexing pipeline docs as the reference, so you can see exactly where language-specific parsing ends and language-agnostic graph building begins.
The 11 tree-sitter languages
CGraph's extract stage parses these eleven languages with vendored tree-sitter grammars:
C, C++, Java, JavaScript, TypeScript, TSX, Kotlin, Scala, Groovy, Python, and Ruby.
Vendoring the grammars — they live under vendor/ alongside the tree-sitter core, per the architecture docs — means extraction doesn't depend on a language toolchain being installed on the machine. You don't need a JDK to parse Java or a Ruby runtime to parse Ruby. The grammar ships with CGraph and produces a concrete syntax tree that the extractor walks.
Not everything is a tree-sitter language. The rest of the supported inputs — Apex, Delphi, and MSBuild/XML, plus MCP config — go through regex or structured extraction instead. They land in the same graph; they just take a different front door into it.
Why tree-sitter, not regex, for the eleven
A regex knows about characters. A tree-sitter grammar knows about syntax. That difference is the whole reason the eleven core languages get a real parser.
When you grep for a function name, you get every textual match: the definition, the calls, the comment that mentions it, the string literal that happens to contain it. A parsed syntax tree distinguishes a declaration from a call site from a comment — which is exactly the distinction the graph needs, because a node ("this function is defined here") and a link ("this function is called there") are different things. Tree-sitter gives the extractor structured, positioned nodes instead of a pile of line matches.
Tree-sitter is also incremental and error-tolerant by design, which matters for the daemon: when you edit a file, the parser can re-parse the changed region rather than the whole file, and it still produces a usable tree even when the code doesn't compile. That's what lets CGraph's daemon fold edits into the graph within a couple of seconds instead of rebuilding from scratch.
From files to a resolved graph
Extraction alone produces a raw graph — nodes and some obvious links, but not yet a connected model. The connective tissue comes from post-processing, which runs in a fixed order:
The six post-processing steps, in the order CGraph runs them:
| Step | What it does |
|---|---|
| Import resolution | Connect files/modules to what they import |
| Raw call resolution | Connect callers to callees |
| Relation resolution | Resolve remaining relationships |
| Semantic deduplication | Merge duplicate semantic nodes |
| Community detection | Group related nodes into communities |
| Graph analysis | Compute the analytical properties queries rely on |
Order matters here. You can't resolve a call to a callee until you know which modules a file imports, and you can't detect communities until the edges those communities are made of exist. Each step depends on the one before it.
What ends up in the graph
The resolved graph is a directed graph of nodes and links, serialized as node-link JSON. Per the graph model docs, nodes are the entities extracted from the tree — files, symbols, functions, classes, and the references between them — and links are the directed edges resolution produces:
file --imports--> file
fn --calls-----> fn
doc --describes-> symbol
Imports come from import resolution, calls come from raw call resolution (they're the basis of the call-flow export), and relations come from relation resolution plus any semantic enrichment layered on top. The describes edge above is an example of the latter — a document node pointing at the code it explains.
The point of expressing code this way is that the two questions agents care about most become graph traversals rather than text searches. What calls this? walks incoming call edges. What breaks if I change it? walks the transitive blast radius of a node. We go deep on that second one in Impact analysis in ~10ms.
Where static extraction stops
CGraph is honest about its edges, and so is this post. Static resolution is precise for direct calls and imports, but it's approximate where the language is dynamic. Reflection, dynamic dispatch, and runtime wiring can't always be resolved from source alone — a call made through a string-keyed dispatch table has no syntactic caller-to-callee edge to find. CGraph favors a deterministic, reproducible graph over chasing every dynamic edge, which means the graph is a precise map of the static structure and an approximate one of the dynamic behavior.
That trade — precision and determinism over exhaustive dynamic recall — is why the same tree always produces the same graph, and why the daemon can diff and fold in changes instead of rebuilding. Determinism isn't a nice-to-have here; it's the property the whole incremental update model rests on.
Try it
The fastest way to see the extraction is to run the one-shot CLI against a checkout and open the exports:
cgraph --root . --out cgraph-out
That writes graph.json plus the HTML, SVG, Obsidian, Cypher, and call-flow views. The code — extractor, resolver, and vendored grammars — is open source; read it on GitHub. If you want to see how the parsed structure translates into token savings for a coding agent, the CGraph benchmark post measures it against grep and against Graphify.