This document describes how Infigraph parses source code, extracts symbols and relationships, builds a graph, and enables search. The core logic lives in crates/infigraph-core/ with language definitions in crates/infigraph-languages/.
- Architecture Overview
- Entry Points
- Language Detection and Registry
- File Discovery
- Tree-Sitter Parsing
- Grammar Plugin System (ANTLR)
- Symbol Model
- Edge Extraction
- Graph Storage (Kùzu)
- Incremental Indexing
- Embeddings
- Search
- Watch Mode
- Route and Contract Extraction
- Multi-Repo Groups
File Discovery ──► Language Detection ──► Tree-Sitter Parse ──► AST
│
┌────────────┴────────────┐
entities.scm relations.scm
(symbols) (edges)
│ │
▼ ▼
SymbolKind::* CALLS, IMPORTS,
Function, Class, INHERITS, READS,
Method, Struct... WRITES, TESTED_BY
│ │
└─────────┬───────────────┘
▼
Kùzu Graph Database
(.infigraph/graph.kuzu)
│
┌───────────────┼───────────────┐
Embeddings BM25 Index HNSW Index
(potion-base-8M) (.bm25_cache) (.usearch)
│ │ │
└───────────────┴───────────────┘
│
Hybrid Search
(BM25 + vector + regex grep)
The pipeline runs in this order:
- Walk the directory tree, detect language per file
- Hash each file (SHA-256), skip unchanged files (incremental)
- Parse source via tree-sitter (or ANTLR grammar plugin for unsupported languages)
- Run
entities.scmquery to extract symbols (functions, classes, methods, etc.) - Run
relations.scmquery to extract edges (calls, imports, inherits, etc.) - Extract statements, routes, and additional analysis edges
- Bulk-load symbols and edges into Kùzu via Parquet COPY
- Generate embeddings for new/changed symbols
- Prune stale symbols from deleted files
Infigraph is the top-level driver (analogous to DocIndex for documents). Key methods:
| Method | What it does |
|---|---|
open(root, lang_registry) |
Opens or creates .infigraph/ dir, sets up graph DB path |
init() |
Initializes Kùzu schema, opens the graph store |
index() |
Main incremental indexing routine |
reindex() |
Full wipe and rebuild |
| Tool | Purpose |
|---|---|
index_project |
Index a project (prefers CLI subprocess, falls back to in-process) |
search |
Unified hybrid search (BM25 + vector + grep) |
get_doc_context |
Source + callers + callees for a symbol |
find_all_references |
All usages of a symbol across the codebase |
trace_callers / trace_callees |
Call chain traversal |
transitive_impact |
Blast radius analysis |
watch_project |
Start background file watcher |
The infigraph binary (crates/infigraph-cli/) exposes index, reindex, search, watch, and analysis subcommands.
LanguageRegistry (crates/infigraph-core/src/lang/registry.rs:9-16) maps file extensions to language packs.
Each language is defined as a LanguagePack (crates/infigraph-core/src/lang/mod.rs:42-47):
struct LanguagePack {
name: &'static str,
extensions: &'static [&'static str],
backend: ParserBackend,
custom_edges: Option<...>,
}ParserBackend enum (crates/infigraph-core/src/lang/mod.rs:32-39):
enum ParserBackend {
TreeSitter {
grammar: Language, // tree-sitter grammar
entity_query: Query, // entities.scm compiled
relation_query: Query, // relations.scm compiled
},
Custom(Box<dyn CustomExtractor>),
}bundled_registry() (crates/infigraph-languages/src/lib.rs:182-425) registers all 62 supported languages. Each language has:
- A
tree-sitter-<lang>crate providing the grammar entities.scm— tree-sitter query for symbol extractionrelations.scm— tree-sitter query for relationship extractionlang.toml— language metadata
All query files live under crates/infigraph-languages/languages/<lang>/.
Rust, Python, JavaScript, TypeScript, Java, Go, C, C++, C#, Ruby, PHP, Swift, Kotlin, Scala, Dart, Elixir, Haskell, Lua, R, Julia, Perl, Bash, PowerShell, Zig, Nim, OCaml, Erlang, Clojure, F#, Groovy, MATLAB, Fortran, COBOL, Ada, Pascal, Verilog, SystemVerilog, VHDL, Assembly, Makefile, CMake, Dockerfile, HCL/Terraform, Nix, YAML, TOML, JSON, XML, HTML, CSS, SCSS, SQL, GraphQL, Protobuf, Thrift, Svelte, Vue, JSX, TSX, Markdown, LaTeX, Typst.
File discovery walks the project directory, applying:
Same as document indexing: .git, node_modules, __pycache__, .venv, target, build, dist, plus directories starting with .
A file is indexed if:
- Its extension matches a registered
LanguagePackin the registry - It's not a binary file
- It's under configurable size limits
Files are processed in parallel using rayon's par_iter for extraction and hashing.
The core parsing function for a single file:
- Set the tree-sitter
Languageon a thread-local parser (TS_PARSER) - Parse source code into an AST tree
- Run
extract_entities— executesentities.scmquery against the AST - Run
extract_relations— executesrelations.scmquery against the AST - Run
extract_statements_for_symbols— extracts statement-level detail - Generate route → handler CALLS edges via
generate_route_handler_edges - Compute SHA-256
content_hashof the file
Each language's entities.scm is a tree-sitter query that captures symbol definitions. Example pattern:
;; Capture function definitions
(function_definition
name: (identifier) @name) @definition.function
;; Capture class definitions
(class_definition
name: (identifier) @name) @definition.classCapture names like @definition.function, @definition.class, @definition.method map to SymbolKind variants. The extraction layer reads start/end positions, visibility modifiers, parameters, return types, and docstrings from the AST context.
Each language's relations.scm captures relationships:
;; Capture function calls
(call_expression
function: (identifier) @reference.call)
;; Capture imports
(import_statement
module_name: (dotted_name) @reference.import)
;; Capture inheritance
(class_definition
superclasses: (argument_list
(identifier) @reference.inherits))Capture names like @reference.call, @reference.import, @reference.inherits map to edge types (CALLS, IMPORTS, INHERITS).
For languages without tree-sitter grammars, Infigraph supports ANTLR-based grammar plugins.
Each plugin provides:
plugin.toml— metadata (language name, extensions, grammar files).g4files — ANTLR grammar definitions (externally owned, never modified)
Rust host ──stdin/stdout JSON──► JVM subprocess (GrammarDriver.java)
│
ANTLR Parser
│
Symbol/Edge JSON
- Rust host:
crates/infigraph-grammar-plugin/src/driver.rs:150-173—parse()method sends source code to the JVM process and reads back extracted symbols/edges as JSON - JVM driver:
driver/src/main/java/com/infigraph/driver/GrammarDriver.java— loads ANTLR grammars, parses source, walks the parse tree, and emits structured JSON - Plugin registration:
register_grammar_plugins()(crates/infigraph-grammar-plugin/src/lib.rs:64-144) scans forplugin.tomlfiles and registers each as aCustombackend in the language registry
| Aspect | Tree-Sitter | Grammar Plugin |
|---|---|---|
| Grammar source | Rust crate (tree-sitter-<lang>) |
.g4 files + JVM |
| Query language | .scm (S-expression) |
Java visitor pattern |
| Performance | In-process, fast | Subprocess, slower |
| Setup | Zero (bundled) | Requires JVM |
SymbolKind enum (crates/infigraph-core/src/model/mod.rs:6-21):
pub enum SymbolKind {
Function,
Method,
Class,
Struct,
Interface,
Trait,
Enum,
Module,
Variable,
Constant,
Test,
Section,
Route,
Field,
}The Symbol node table stores:
| Field | Type | Description |
|---|---|---|
id |
STRING (PK) | Unique symbol identifier (typically file::name or qualified path) |
name |
STRING | Symbol name |
kind |
STRING | One of the SymbolKind variants |
file |
STRING | Source file path (relative to project root) |
start_line |
INT64 | First line of the symbol definition |
end_line |
INT64 | Last line of the symbol definition |
signature_hash |
STRING | Hash of the symbol's signature (for change detection) |
language |
STRING | Programming language |
visibility |
STRING | public, private, protected, etc. |
parent |
STRING | Parent symbol ID (for nested definitions) |
docstring |
STRING | Documentation string / comment |
complexity |
INT64 | Cyclomatic complexity |
parameters |
STRING | Parameter list (serialized) |
return_type |
STRING | Return type annotation |
category |
STRING | Semantic category |
All edge types are defined in CREATE_SCHEMA (crates/infigraph-core/src/graph/schema.rs:16-106):
| Edge | From → To | Properties | How extracted |
|---|---|---|---|
CALLS |
Symbol → Symbol | — | relations.scm @reference.call captures |
IMPORTS |
Module → Module | — | relations.scm @reference.import captures |
CONTAINS |
Module → Symbol | — | File-level containment |
INHERITS |
Symbol → Symbol | — | relations.scm @reference.inherits captures |
TESTED_BY |
Symbol → Symbol | — | Test function → tested function mapping |
READS |
Symbol → Symbol | — | Variable read access |
WRITES |
Symbol → Symbol | — | Variable write access |
DEFINES |
File → Symbol | — | File-level symbol ownership |
CALLS_SERVICE |
Symbol → Symbol | method, path, target_service |
Cross-service call detection |
SIMILAR_TO |
Symbol → Symbol | score |
Embedding similarity (clone detection) |
BRIDGE_TO |
Symbol → Symbol | bridge_kind, detail |
Bridge/adapter pattern detection |
HAS_STATEMENT |
Symbol → Statement | — | Statement-level detail extraction |
HAS_CONCERN |
Symbol → Concern | — | Cross-cutting concern detection |
HAS_CONFIG |
Symbol → ConfigBinding | — | Configuration binding detection |
RESOLVES_TO |
Symbol → Symbol | mechanism, config_source |
Dynamic resolution (DI, reflection) |
TAINT_FLOW |
Symbol → Symbol | source_kind, sink_kind, path |
Taint analysis flow |
DEPENDS_ON |
Module → Dependency | is_dev |
Package dependency |
MEMBER_OF |
Symbol → Cluster | — | Code cluster membership |
CONTAINS_FILE |
Folder → File | — | Directory structure |
CONTAINS_FOLDER |
Folder → Folder | — | Directory nesting |
| Node | Fields | Purpose |
|---|---|---|
Module |
id, name, file, content_hash, language |
File-level module (one per source file) |
File |
id, path, language, size |
Physical file |
Folder |
id, path |
Directory |
Dependency |
id, name, version, ecosystem |
External package dependency |
Cluster |
id, label |
Code cluster (from clustering analysis) |
Statement |
id, kind, text, line |
Statement-level nodes |
Concern |
id, name, kind |
Cross-cutting concerns |
ConfigBinding |
id, key, value, source |
Configuration bindings |
Edges from relations.scm use name-based resolution: the query captures a called/imported/inherited name, and the extraction layer resolves it to a symbol ID by searching the current file's symbols and imported modules. Cross-file resolution uses the Module → IMPORTS → Module edges to follow import chains.
Beyond symbols and edges, Infigraph extracts control-flow statements from every function, method, and test. This powers complexity analysis, test context generation, and fine-grained understanding of function internals.
extract_statements_for_symbols (crates/infigraph-core/src/extract/mod.rs:74-99):
- Filters symbols to
Function,Method, andTestkinds - Walks the AST via
collect_fn_nodes(extract/mod.rs:101-123) — matches AST nodes to symbols by line range - For each matched function node, calls
extract_statements(crates/infigraph-core/src/analysis/mod.rs:16-34) to extract control-flow nodes
Extracted statement kinds include:
| Kind | Example |
|---|---|
If |
if condition { ... } |
ElseIf |
else if condition { ... } |
Else |
else { ... } |
For |
for item in collection { ... } |
While |
while condition { ... } |
DoWhile |
do { ... } while (condition) |
Loop |
loop { ... } (Rust) |
Match |
match value { ... } / switch |
Case |
Individual match/switch arms |
Try |
try { ... } |
Catch |
catch (e) { ... } / except |
Ternary |
condition ? a : b |
Guard |
Early return / guard clause |
Statements are stored as Statement nodes in Kùzu, linked to their parent symbol via HAS_STATEMENT edges. Each Statement has: id, kind, text, line.
- Complexity analysis: statement count and nesting depth contribute to cyclomatic complexity
- Test context generation:
generate_test_contextuses statements to understand what a function does internally - Code understanding:
get_doc_contextincludes statement breakdown for richer context
Infigraph automatically detects test functions and links them to the code they test via TESTED_BY edges.
During entity extraction, functions matching test patterns are assigned SymbolKind::Test:
| Language | Pattern |
|---|---|
| Rust | #[test], #[rstest], functions in mod tests |
| Python | def test_*, functions in test_*.py |
| JavaScript/TypeScript | it(), test(), describe() blocks |
| Java | @Test annotation |
| Go | func Test*(t *testing.T) |
| And more per language... |
store.derive_tested_by_edges() (called post-indexing):
- Finds all symbols with
kind = 'Test' - Traces each test's
CALLSedges to find which non-test functions it invokes - Creates
TESTED_BYedges from the tested function → the test function - Idempotent — safe to call multiple times (deletes and recreates)
The get_test_coverage MCP tool uses TESTED_BY edges to report:
- Untested symbols — functions/methods with no TESTED_BY edges, ranked by caller count (more callers = higher priority to test)
- Test-to-code ratio — percentage of functions covered by at least one test
- Source code of untested functions for immediate context
.infigraph/graph.kuzu — a Kùzu embedded graph database directory.
CREATE_SCHEMA (crates/infigraph-core/src/graph/schema.rs:16-106) — all tables created with IF NOT EXISTS for idempotency.
Like the document store, code indexing uses Parquet COPY for bulk loads:
- Export symbols/edges as Arrow RecordBatches
- Write to temporary Parquet files
COPY <table> FROM '<parquet_path>'into Kùzu
Old data for changed files is deleted via DETACH DELETE before re-inserting.
crates/infigraph-core/src/graph/queries.rs — provides typed query methods:
get_callers(symbol_id),get_callees(symbol_id)— direct call relationshipstrace_callers(symbol_id, depth),trace_callees(symbol_id, depth)— transitive traversalfind_references(symbol_id)— all usages across the graphget_symbols_in_file(file)— all symbols defined in a filetransitive_impact(symbol_id)— downstream blast radius
Content hashes are stored on Module nodes via the content_hash column. Retrieved by get_file_hashes() (graph/cozo_store.rs:1405-1412, graph/store.rs:163-175) for incremental indexing.
- Walk the directory tree, collect all source files
- Hash each file with SHA-256
- Load previously stored hashes via
get_file_hashes() - Skip files whose hash hasn't changed
- Re-parse and re-extract only changed/new files
- Delete old symbols/edges for changed files via
DETACH DELETE - Insert new symbols/edges via Parquet COPY
- Delete symbols for files that no longer exist on disk (stale pruning)
detect_changes MCP tool / CLI command uses the diff module (crates/infigraph-core/src/diff/compute.rs) to compare the current file state against the indexed graph, reporting which files have changed, been added, or been removed.
potion-base-8M (Model2Vec) — 256-dimensional, bundled locally at models/potion-base-8M/. No network calls or API keys required.
pub trait EmbedProvider: Send + Sync {
fn dimension(&self) -> usize;
fn embed_batch(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>>;
fn embed(&self, text: &str) -> Result<Vec<f32>> { /* default: batch of 1 */ }
}| Implementation | Dimensions | Usage |
|---|---|---|
Model2VecEmbedder |
256 | Primary (used when model files are available) |
TrigramEmbedder |
configurable | Fallback (character trigram hashing, no model files needed) |
Two separate embedder singletons:
CODE_EMBEDDER/code_embedder()— for code symbolsDOC_EMBEDDER/doc_embedder()— for document chunks
Both initialized via init_embedder / best_embedder (embed/mod.rs:311-340).
Local mode:
- Embeddings:
.infigraph/embeddings.bin(code),.infigraph/docs_embeddings.bin(docs) - HNSW index:
.infigraph/hnsw_index.usearch+.meta
Remote mode (--features remote, INFIGRAPH_BACKEND=neo4j):
- Embeddings: Postgres + pgvector (
embeddingstable,kindcolumn separatessymbolvsdoc_chunk) - HNSW index: not used — brute-force scoring via materialized vectors from
all_embeddings(kind)
HNSW index is built only when the symbol/chunk count exceeds a threshold (100K+ for code, 200K+ for docs) or when an HNSW index already exists. Below the threshold, brute-force linear scan is used (fast enough for smaller codebases). Remote mode always uses brute-force.
The search MCP tool runs three strategies in one call:
- BM25 — keyword/term frequency matching
- Vector — semantic similarity via embeddings
- Regex grep — exact text pattern matching
Results are merged, deduplicated, and ranked.
struct BM25Index {
docs: Vec<(String, String)>, // symbol_id → text
inverted: HashMap<String, Vec<(usize, f32)>>, // term → (doc_index, tf)
avg_doc_len: f32,
}Standard Okapi BM25 scoring with K1 and B parameters. Score computation in compute_raw_scores() (search/mod.rs:200-274).
BM25 index is persisted as .infigraph/bm25_cache.bin to avoid rebuilding across CLI/MCP sessions.
Combined score: score = (1 - alpha) * bm25_normalized + alpha * vector_normalized
Default alpha = 0.3 for code search (0.5 for doc search). Scores are normalized by their respective maximum values before combining.
Uses the notify crate's RecommendedWatcher in recursive mode.
- Monitors the project directory for file changes
- Debounces changes (configurable
debounce_ms) - On change: triggers incremental reindex for affected files
- Emits
WatchEventvia callback for MCP-side tracking - Auto-started after
index_project,scip_import, orgroup_index
- Tracked in
WatcherEntry/get_watchers/init_watchers(crates/infigraph-mcp/src/tools/watch.rs) get_watch_statussurfaces pending files needing full reindex (e.g., cross-file call-edge changes)stop_watchsignals the watcher to stop via channel
Single-file changes can be incrementally re-parsed, but changes affecting cross-file edges (e.g., renaming a function called from other files) may require a broader reindex. The watch status tool reports when this is the case.
detect_routes() (crates/infigraph-core/src/routes/mod.rs:51-75):
- Query all
Function/Methodsymbols from the graph - For each, call
detect_route_from_symbol— heuristic matching on name, decorator, and docstring patterns - Per-language detectors in
routes/{python,go,java,js_ts,rust,ruby,php,csharp,elixir,generic}.rs - Framework sniffers (
detect_python_framework,detect_java_framework,detect_rust_frameworkinroutes/helpers.rs:93-195) identify which web framework is in use
Detected patterns include:
- Python: Flask
@app.route, Djangourlpatterns, FastAPI decorators - Java: Spring
@RequestMapping, JAX-RS@Path - Go:
http.HandleFunc, Gin/Echo/Chi router calls - Rust: Actix
#[get]/#[post], Axum router - JavaScript/TypeScript: Express
.get()/.post(), Next.js file-based routes - And more per language
The newer, more rigorous mechanism for multi-repo use:
sync_group_contracts → extract_contracts (crates/infigraph-core/src/multi/mod.rs):
- Extracts one structured route fact per real endpoint at index time
- Consumed uniformly by the cross-service linker
- Distinct from the heuristic
detect_routes— producesContractstructs withkind,service,method,path,symbol_id,file
crates/infigraph-core/src/multi/cross_service.rs:
scan_source_for_urls— finds URL patterns in source codeextract_api_paths— extracts API path patternslink_cross_service_calls(line 491-700) — matches caller URLs against contracts from other repos, createsCALLS_SERVICEedges
Groups allow indexing and querying across multiple repositories as a unified graph.
group_create("my-group")
→ group_add("my-group", "repo-a", path)
→ group_add("my-group", "repo-b", path)
→ group_build("my-group")
Step 1: Index all repos
Step 2: Sync contracts
Step 3: Link cross-service calls (CALLS_SERVICE edges)
Step 4: Build combined graph
Step 5: Index docs + cross-repo doc linking
build_combined_graph() (crates/infigraph-core/src/multi/combined.rs:22-245):
- Creates a fresh combined Kùzu DB at a group-specific path
- For each repo: exports Symbol, Module, File nodes and all edge tables to Parquet
- Prefixes every ID with
[{repo_name}]::to avoid collisions across repos COPY FROMall Parquet files into the combined store- Runs
resolve_cross_repo()to create cross-repo edges using contracts
Registry stores repo entries (name, path, symbol count) and group definitions (repos, contracts). Persisted to disk and loaded by all group operations.
group_search (combined.rs) runs hybrid BM25+vector search across the combined graph. group_query executes arbitrary Cypher against it.