Docs

How Graph Indexer works

How Graph Indexer builds a code graph: tree-sitter WASM parsing, reference binding, a local SQLite index, re-sync before each answer, no embeddings.

Updated Graph Indexer 3.0.0 View as Markdown

Graph Indexer parses your repository with tree-sitter compiled to WebAssembly, extracts definitions, references, imports and type evidence, binds each reference to the definition it actually points at, and stores everything in a local SQLite index at .graph-indexer/index.db. Before every answer it re-syncs with the files on disk, then ranks results with lexical search and graph centrality. No embeddings, model, network or native build is involved.

What is the Graph Indexer pipeline?

Graph Indexer runs five stages. Each one is deterministic and runs locally:

  1. Parse each file with a tree-sitter grammar (16 grammars bundled as WebAssembly).
  2. Extract definitions, references, imports and type evidence with one query per language.
  3. Bind every reference to a definition through scopes, imports and inferred receiver types.
  4. Store the facts in SQLite (node:sqlite) under .graph-indexer/.
  5. Query with lexical search, graph traversal and PageRank, after re-syncing the index.

How does Graph Indexer parse code?

Graph Indexer parses code with web-tree-sitter 0.25 and 16 tree-sitter grammars shipped as WebAssembly inside the npm package: JavaScript, TypeScript, TSX, Python, Go, Java, Kotlin, Scala, C#, Rust, C, C++, Ruby, PHP, Bash and CSS (including SCSS and Less). Grammar versions are pinned and checked by SHA-256 in a manifest, so nothing is installed or downloaded at run time and there is no native build step. See supported languages.

For each language, one tree-sitter query extracts definitions (functions, methods, classes, interfaces, fields and so on), references (calls, instantiations, type uses, inheritance, decorators, values and reads), imports, and the type evidence later used for binding.

How does Graph Indexer bind references to definitions?

Graph Indexer binds a reference by resolving it against scopes, imports and the inferred type of the receiver, so a call to UserService.find is kept apart from every other method named find. This binding step is what separates it from a name search, which cannot tell the two apart.

Receiver type inference

For repo.find(id), the type of repo decides which find is meant. Graph Indexer infers it from annotations, initialisers, new expressions and casts, declared and inferred return types, fields and constructor-injected dependencies, collections (xs[i], for x of xs, forEach, list.get(0)), maps (Map<K,V>, dict, HashMap, Go map[K]V), optional and wrapper types (Foo | undefined, Optional[Foo], Promise<Foo>, Arc<Mutex<Foo>>), closures and access chains. Members of primitives and collections are never guessed, and conflicting bindings produce no type: the design favours precision over recall.

The resolve ladder

Each reference walks a ladder from the most local meaning to the most global one, and the result carries a confidence label:

  1. lexical scope
  2. the enclosing type and its base classes (with C3 linearisation for method resolution order)
  3. the same file
  4. an explicit import, including barrel files, tsconfig paths per nearest tsconfig, workspaces, Go modules, Python packages, Rust crates and C includes
  5. the same package
  6. global names

References bound at the top of the ladder are labelled exact (confidence 0.9 and above) or high (0.7 and above). A match on the name alone is labelled likely (0.4 and above) or possible. Tools print these labels, and footers report same-name calls that could not be bound, so an agent knows when to double-check with text search.

Where does Graph Indexer store the index?

Graph Indexer stores the index in a SQLite database at <repo>/.graph-indexer/index.db, using the node:sqlite module built into Node.js 22.5 and newer. The database runs in WAL mode, with FTS5 full-text tables over symbols and files. Facts are owned per file, so editing one file replaces only that file’s facts, and symbol ids stay stable across edits. When the extractor changes between releases, a fingerprint triggers a rebuild automatically.

The folder is git-ignored by npx graph-indexer init. It contains excerpts of your code, so treat it like the repository itself; deleting it is safe because it is rebuilt on the next start. Only the working tree is indexed.

How does Graph Indexer stay up to date?

Graph Indexer re-syncs with the files on disk before every answer, including edits an agent made a second ago. A file watcher (fs.watch) plus periodic stat sweeps detect changes (a 30-second sweep with the watcher; a stat sweep of at most 3 seconds without it), an ensureFresh() step runs before every tool call, and files are re-validated before line numbers are rendered. The MCP server builds the first index in the background when it starts.

The MCP server keeps the index open while the agent runs. With hooks and no server, a resident process starts on demand and exits after 30 idle minutes (set GRAPH_INDEXER_RESIDENT=0 to disable it). It listens on a Unix socket (a named pipe on Windows) with mode 0600 inside .graph-indexer/, never on TCP.

How does Graph Indexer rank search results?

Graph Indexer ranks search results with lexical channels and graph signals, and uses no embeddings. search_code combines four channels: symbol names, BM25F over an FTS5 index with field weights (name 10, qualified name 6, signature 3, documentation 2, path 2.5, body 1), a concept thesaurus, and file-level BM25. Interpretable priors and PageRank centrality over the reference graph are added, and results are diversified to at most three per file. Weights were tuned on a tuning split only.

outline uses the same PageRank, personalised to a focus text, to build a ranked map of a directory or repository within a token budget. Change impact and edit checks start from symbols, files or git diff, follow the graph for dependents, find tests through the graph and naming, and add git co-change history.

On 377 symbol-search queries over 9 repositories in 8 languages, the first result was right 70.3% of the time (mean reciprocal rank 0.760). All numbers and methods are in the benchmarks.

How fast is Graph Indexer?

Graph Indexer indexes the nestjs repository (1,641 files, 96,000 lines) in 3.7 seconds with a 12.3 MB database. The Twenty CRM monorepo (28,512 files, 2.33 million lines, about 170,000 symbols and 555,000 references) takes 51 seconds and 304 MB. Re-syncing an unchanged repository takes 0.2 seconds on nestjs and 1.8 seconds on Twenty.

Once the index is warm, graph tools answer in 1–4 ms, search_code in about 30 ms and outline in about 18 ms. Through the resident process, hook calls drop from 280–570 ms to 70–120 ms and CLI calls from about 370 ms to 110–210 ms. A tool reply is typically 400 to 1,500 tokens, and Graph Indexer adds about 1,200 tokens to each Claude Code session. Source: BENCHMARKS.md.

What are the known limits of Graph Indexer?

Graph Indexer is not a type checker and says so in its replies. Its published limits:

  • Contextually typed object literals, generic instantiation, reflection, any and string-token dependency injection are only partly visible.
  • Only the working tree is indexed.
  • Reference accuracy was verified against the TypeScript, Go and Java compilers or type checkers. Java is the weakest language measured (recall 86.1% to 98.9% across three repositories).
  • The share of references bound to a definition varies by repository, from 43% to 94%.
  • In real issue-fixing tasks, the agentic benchmark showed no measurable saving, and the agent samples are small.

Graph Indexer itself makes no network calls and sends no telemetry. The agent still sends what it reads to its model provider. The tool reference is on the tools page.

Frequently asked questions

Does Graph Indexer use embeddings or a language model?

No. Graph Indexer search is lexical plus graph: BM25 over names, signatures, docs and bodies, a small concept thesaurus, and PageRank centrality over the reference graph. It needs no model, no download and no network.

Is Graph Indexer a type checker?

No. It infers receiver types from annotations, initialisers, returns and dependency injection to bind references, but it does not replace a compiler. Generics, reflection, any and string-token injection are only partly visible.

Can the index be out of date?

Not at answer time. Graph Indexer re-syncs with the files on disk before every tool call, and re-validates files before rendering lines, so edits an agent made a second ago are included.

Try it on your repo.

npx graph-indexer init
Using Claude Code? Install the plugin