This page describes the current Rust implementation. It covers ownership and data flow; detailed CLI, MCP, storage, and golden contracts live in their owning references.
The root Cargo.toml workspace list and each crate manifest are authoritative.
The following diagram shows normal (shipped) intra-workspace dependencies; test
and benchmark-only development dependencies are omitted.
graph TD
core[codegraph-core]
extract[codegraph-extract]
store[codegraph-store]
resolve[codegraph-resolve]
graph[codegraph-graph]
mcp[codegraph-mcp]
watch[codegraph-watch]
daemon[codegraph-daemon]
ui[codegraph-ui]
cli[codegraph-rs / codegraph binary]
bench[codegraph-bench]
extract --> core
store --> core
resolve --> core
resolve --> extract
resolve --> store
graph --> core
graph --> store
mcp --> core
mcp --> graph
mcp --> resolve
mcp --> store
watch --> core
watch --> extract
watch --> resolve
watch --> store
daemon --> core
daemon --> mcp
daemon --> watch
ui --> core
ui --> extract
ui --> store
ui --> resolve
ui --> graph
ui --> mcp
ui --> watch
cli --> core
cli --> extract
cli --> store
cli --> resolve
cli --> graph
cli --> mcp
cli --> watch
cli --> daemon
cli --> ui
bench --> core
bench --> extract
| Crate | Responsibility |
|---|---|
codegraph-core |
shared types, node IDs, configuration, classification, logging |
codegraph-extract |
source discovery, language/custom extraction, unresolved references |
codegraph-store |
SQLite/FTS5 schema, migrations, persistence, queries, leases |
codegraph-resolve |
generic import/name matching and framework-aware resolution |
codegraph-graph |
traversal, impact, query parsing, search ranking |
codegraph-mcp |
tool schemas/engine, rmcp stdio and streamable-HTTP transports |
codegraph-watch |
incremental reconciliation and filesystem watcher |
codegraph-daemon |
detached shared process, IPC, and process registries |
codegraph-ui |
browser viewer: loopback JSON API, live channel, embedded ui/ build |
codegraph-rs |
clap CLI, agent installer, orchestration, shipped binary |
codegraph-bench |
equivalence oracle and benchmark harness; not shipped |
The dependency direction keeps extraction independent of persistence and keeps storage independent of query presentation. The CLI is the product assembly root.
graph LR
source[Project files] --> scan[scan policy + language detection]
scan --> extract[tree-sitter / custom extraction]
extract --> facts[nodes + structural edges + unresolved refs]
facts --> store[(SQLite + FTS5)]
store --> resolve[generic + framework resolution]
resolve --> edges[resolved edges]
edges --> store
store --> query[graph traversal + search]
query --> cli[CLI]
query --> mcp[MCP]
watcher[watch events] --> reconcile[incremental reconcile]
reconcile --> extract
reconcile --> resolve
codegraph-extract applies project configuration, ignore rules, extension
overrides, generated-file signals, and deterministic path ordering before
parallel file parsing. A file then follows one of these paths:
- a custom/embedded extractor (Vue, Svelte, Astro, Razor, Liquid, MyBatis XML, DFM/FMX);
- a grammar-backed
LanguageSpecand generic tree-sitter walker; or - file-level handling for formats that intentionally expose no language symbols.
The walker emits file/symbol nodes and structural ownership edges immediately,
and so the value references of a file, which need nothing outside it (see
data-model.md). Calls, imports, type relationships, decorators, and framework evidence that need
global knowledge are stored as unresolved references. Per-file extraction is
fail-closed: an unsafe tree depth or fatal parse condition cannot leave a partial
replacement graph for that file.
File parsing can run in parallel because each input is independent. Persistence and later resolution restore stable order before output; scheduler completion order is never a graph contract.
codegraph-store owns a project-local SQLite database with FTS5. Fresh databases
start at the current schema; older supported databases advance through ordered,
transactional migrations. Foreign keys, WAL behavior, indexes, FTS triggers, and
lease/state gates are configured by the store rather than by callers.
See data-model.md for the current schema version, tables,
indexes, and migration history.
ReferenceResolver reads the complete extracted snapshot (or the bounded
streaming path for very large graphs), then applies deterministic strategies.
Every entry (the full index's batches, the serial pass, a sync's scoped pass)
resolves and persists the wiring references first, imports and extends/
implements supertypes, and the rest after them, so a call on a receiver
typed as a subclass reaches a method its base declares within one pass. A C
or C++ base is resolved only by C++ name lookup (the scopes around the
deriving class, its file's namespace aliases and using lines, a
translation unit's own classes), never by the strategies below:
- language-aware imports, re-exports, packages, and paths (a CommonJS
require('./x')imports its file, and a call through arequirebinding followsmodule.exports,exports.name = …, arequire(…).memberand a.defaultthe way the module exports them;export * as nsexports the namespace alone, a namespace import'sz.core.fn()continues into it, andimport { default as X }is the default import; a Python name imported from a package follows the package's named and star re-exports to its definition, three modules deep); - qualified, scoped, receiver/type, lexical, and exact-name matching (a
receiver written as, or inferred to be, a type the project does not declare
stops the name guesses, except that a C# call still reaches the one
project extension method in scope that takes that type as
thisand fits its arguments; a class-name receiver also reaches the class methods its declared supertypes provide; a receiver-word guess never takes a framework-dispatched handler, an unnamed test double or the caller itself; in a language that overloads by arity, a call then moves to the same-owner overload its argument count fits, or in Swift its argument labels), then a last-resort fuzzy match that ignores case only in PHP, Pascal and CFML, whose identifiers do; - detected
FrameworkResolverimplementations; and - bounded follow-up passes for relationships that require graph conformance.
The selected target then passes fail-closed correctness gates: language/module
visibility, C-family macro and translation-unit scope, Go local bindings,
external-import locality, and target-kind eligibility. A JS-family import is
from outside the repository only when the importing file's package.json, or
an enclosing one, declares the package (a workspace:, file: or link:
dependency is the repository's own, and a Deno import map counts), or it is a
Node built-in, @std/…, a runtime scheme (node:, npm:, jsr:) or a
framework's virtual module ($app/…, astro:…); an alias the resolver does
not follow ($lib/…, ~/…) stays the project's. Calls in Vue, Svelte and
Astro components follow the JS/TS rules and, like ArkTS, see the JS
built-ins, unless the file imports its own binding of the built-in's name.
A result that lands on
the referencing node itself is kept only when nothing rules it out: a call
written through super / base / parent::, on another object's value, or
from a value's initializer is not a self-call, and a type never inherits from
itself (the other type of its name its written qualifier leads to is taken
instead, or none). A name match from non-test code never reaches a symbol
in a test suite (a test source set, a tests/ / spec/ / e2e/ tree, a
FooTest.kt / test_foo.py / foo.test.ts / foo_unittest.cc file): test
support a project ships (testing/, fakes/, a *-test module) stays in
reach, and so does a C or C++ file the reference's translation unit
includes. A type name a declaration around the reference declares as a type
parameter (class Foo<T>, def f[A], <T> T max(, template <typename T>) is
that parameter, never a project type of its name; a Scala def's value
parameter is that parameter, and a bare Scala type name reaches another
type's member only from inside it, a subtype, or a file importing its members
(otherwise the type of that name in scope is taken). A function or type
declared inside a function is reached only from inside it, and a Python name
passed as a value that its function binds (a parameter, an assignment, a
for / as target) is that local, unless a pytest fixture of its name is in
reach (its own module, or a conftest.py above it); a JS/TS name the calling
function binds itself (a parameter, or a var / let / const above the
reference) is that local, never a same-named function elsewhere in its file,
and a name destructured from a call's result (const { login } = useAuth())
is the function the callee returns under that key.
A name match from another file never reaches an ES module's top-level binding
the module does not export (the export keyword, an export { … } list,
export default x and export default { … } export it; classic scripts,
CommonJS and .d.ts files keep their globals), nor a declaration in a Svelte
instance <script> or a Vue <script setup> (a <script module>, a plain
Vue <script> and a type a <script setup> exports stay importable). A name
match from another file never reaches into a minified script (*.min.js, *-min.js, a script
whose text is mostly code on lines of a thousand characters, or a webpack
build), though what an import names still binds and calls inside the script
still resolve; a sync that changes such a script re-resolves the references
naming its symbols, since its verdict can flip with no symbol moving. Pure JS-family alias
bindings may forward to a unique callable; wrappers and ambiguity stop at the
binding. A framework-resolved edge carries its own reachability proof and is not
erased by ordinary source-language visibility (for example Tauri IPC to a
private Rust command).
The registered framework candidates are NestJS, React/Next.js, Vue/Nuxt, Godot,
and Tauri, in a deliberate order. Each activates only when its detection guard
matches, and resolves only references written in its own languages (React
and Vue their scripts', NestJS TypeScript and JavaScript, Tauri the JS-family
invoke sites and Rust; Godot stays universal). Godot also extracts project/scene/resource relationships; Tauri bridges
literal JS-family invoke calls to uniquely registered Rust commands. Ambiguous
or dynamic targets remain unresolved rather than falling through to a global
same-name guess.
The static/runtime boundaries for supported languages and Godot are documented in
languages.md, godot.md, and the upstream-difference
ledger in upstream-sync/KNOWN_DIFFS.md.
codegraph-graph operates on persisted nodes and edges. It owns callers,
callees, impact, paths, hierarchies, cycle detection, FTS candidates, fallback
matching, filters, and deterministic scoring. Generated/ambient/deprioritized
paths can affect ranking without removing graph facts.
The CLI and MCP share the same query engine for search, explore, and node,
so source selection, staleness handling, and relationship rendering do not drift
between interfaces.
The codegraph-rs package builds the codegraph binary. The CLI owns:
- lifecycle commands (
init,sync, full index, status, recovery); - query and analysis commands;
- MCP process launch and HTTP/stdio observability commands;
- agent/IDE configuration and Skill installation;
- shell completion and self-update; and
- human/JSON output contracts.
The CLI directly uses codegraph-watch for one-shot incremental synchronization
and codegraph-daemon for shared MCP service. It keeps logs on stderr so JSON and
MCP stdout remain protocol-clean. The complete command contract is
cli.md.
codegraph-mcp uses rmcp and Tokio for MCP protocol handling over stdio and
streamable HTTP. The source schema defines a known callable tool set and a smaller
default tools/list surface; an allowlist can expose any known tool. The current
source-derived counts are validated by scripts/docs-check.py, not maintained as
an unchecked architecture constant.
Each tool call resolves its project independently. A pinned server uses its
launch project; an unpinned server can use an explicit projectPath, client MCP
roots, or bounded deterministic workspace adoption. Engine caches are reopened
when the database is replaced and do not grant filesystem authority outside a
validated project.
Explicit project routing has a separate, bounded lifecycle layer. The first access to an existing index goes through a session broker (maximum 32 projects), which starts or attaches that project's shared daemon, retains a client connection, and completes one catch-up before query execution. The daemon, not the MCP handler, owns the watcher and long-lived writer lock. This keeps multiple child indexes independent without choosing a default or creating state, while several MCP sessions converge on one watcher owner per project.
Protocol revisions, default methods, tool schemas, project selection, staleness,
and HTTP headers are specified in mcp.md.
serve --mcp chooses direct mode or the per-project shared daemon. A daemon owns
one project rendezvous, accepts multiple client sessions, starts the watcher, and
exits after its last client has been idle for the configured period. IPC uses Unix
local sockets or Windows named pipes. Project lock/state files stay under the
selected index root; long Unix socket paths may use a deterministic temporary
socket location recorded in the rendezvous metadata.
The watcher maintains per-directory watches outside ignored trees, debounces
bursts, reloads project control files, and reconciles additions, modifications,
deletions, and scope changes through codegraph-watch. A burst past 500 paths,
a backend's sign of dropped events, or (on Windows) drift found by an idle
sentinel escalates to one full reconcile, reported as RECOVERING until it
commits. A sync re-resolves the references that name what it changed, a
qualified one (User.create) by its last segment as well (at most 500 per
segment), and one written with :: (an Erlang util::double/1, a MyBatis
include's app.CommonMapper::userColumns) by the whole qualified name of a node
that came, went or changed, however many share its segment, since a node's id
holds no qualified name (a mapper namespace renamed in place keeps every id);
a PHP node that only moved (its id changed with its line, nothing a
reference reads did) changes no name: the edges into its file re-resolve at
their sites, and the references elsewhere to a node of its name keep their
targets unless the order of that name's nodes in its file changed (as upstream
pairs a re-indexed file's old and new nodes and keeps the edges whose target
only moved), while a moved node of another language still touches its name,
since resolution there reads declaration text no node records (a C function's
static); an Erlang function that came, went or changed touches its name with
its arity as well (do_get/3), the name a bare call is written and parked
under, so a call waiting for the module it imports from or a header to declare
the function is retried however many share it; a Python import statement that comes or goes counts the names it
can re-export as changed (what it binds, and for a star import every name the
module offers, as deep as the re-export walk looks), since an importer's name
then moves with no node of it changing; and a changed Python file refreshes the
method values that read through a module global it declares or can write, in
that module and its importers, since the global's type is read from statements
no node records; a changed conftest.py, or a module one pulls in, refreshes
the functions under its reach that take a parameter named like one of its
fixtures, since a fixture's return type types that parameter; a
JavaScript-family file whose module surface moved (its re-exports, export
aliases and lists, CommonJS default, sealed-ness or component scripts, kept as
a digest per file in project_metadata), and every file that came or went,
sends the direct dependents of it and of the barrels that forward it (by the
targets their rows recorded or what their sources resolve to now) down the
whole-file lane and touches the names it declares, since no node records any
of that text; a sync withdraws the surface marker before its first mutation
and restores it last, so one that fails in between leaves the next sync to
reconcile every JavaScript-family file; a changed C or C++ header (.h,
.hpp, .hxx) sends its same-stem sources (.c, .cc, .cpp, .cxx) down
the whole-file lane, since receiver inference reads a member's declared type
from it, and a C or C++ file whose type surface moved (an alias that came,
went or names another type by its recorded declaration, a class-like type or
enum that came or went under an alias's name, a class whose bases changed),
compared with one snapshot of the store taken before the sync first changed a
C or C++ file, sends every unchanged C or C++ file whose text mentions a
touched name down the same lane, the names closed over the aliases whose
declarations mention them and over the classes deriving from a class whose
bases changed, since an alias lookup reads those declarations by name across
the project; a C or C++ file whose namespace row moved (a namespace alias or a
namespace macro definition that came, went or changed, with the names its body
writes, or a changed use of an opener or closer, with the namespaces it opens,
closed over every alias and macro whose body names a seed) sends every
unchanged C or C++ file mentioning a seed down the same lane, together with the
bare names (an operator's or destructor's first identifier) of the
declarations whose effective name, macro prefix plus qualified name, moved in
the changed files that use an opener or closer and in the unchanged files that
use a seed macro, since a qualified name and a call from the global scope read
those namespaces from text; the cpp_type_surface_version marker is withdrawn
and restored with the surface marker, and an index without it, or with an
older version, re-resolves every C and C++ file once and rebuilds the namespace
rows (a base that becomes a namesake in another namespace, and a class gaining
or losing its template <…> head while no alias's text changes, are not
detected); a Java file whose head row moved (a class, interface or enum whose
head names other types, by the two readers resolution uses and in file order,
came, went or now names others; the rows are kept per file in
project_metadata) sends every unchanged file declaring a Java type of a
touched name, or an anonymous class instantiating one, down the whole-file lane,
the names closed over every head that names one, since a bare call's scope walk
and a type name's ancestor walk read those heads by name across the project; a
package the sync makes the project declare, or stop declaring (only Java and
Kotlin files declare one), sends every unchanged Java file with an import from
under it down the same lane, since an import from outside the project owns its
name; the java_type_heads_version marker is withdrawn and restored with the
others, and an index without it re-resolves every Java file once and rebuilds
the head rows (a call through a receiver typed as a subtype of a changed head,
in a file declaring no type of a touched name, is not detected); a Kotlin file
whose surface row moved re-resolves, whole, the unchanged Kotlin files whose
walks can start from a name the changed heads reach (closing over the heads
naming a reached name and Android's platform ancestry in one queue) or whose
lambdas are passed to a function whose receiver moved, and touches, for the name
lane, the members of every type that joined or left the project-wide receiver
types and of every owner a Gradle script reads that came or left as a project
class name, since none of those facts is a node; the kotlin_surface_version
marker is withdrawn and restored with the others; a Scala file whose surface row
moved re-resolves, whole, the unchanged Scala files whose walks start from a
name its changed heads or package objects reach (closing over every head and
package object naming a reached name), and touches, for the name lane, the
functions and vals whose extension or block reading flipped (by node id),
every name of the file when the package its package object opens changed, and
its top-level types when the package holding them changed, sending the unchanged
files with references to those names written with a receiver down the
whole-file lane too, however many there are, since none of those facts is a
node; the scala_surface_version marker is withdrawn and restored with the
others; a C# file whose global usings or global using statics changed
re-resolves every unchanged C# file whole, since every file of a project reads
the first and every file the second, and the project is a .csproj, which is
no source file (a .csproj or .props change is the control-input lane's, below); one
whose surface row's heads moved re-resolves, whole, the unchanged files
declaring a C# type of a name those heads reach (closing over every head
listing a reached name as a base to either reader), since a nested type is
reached from a type deriving from its owner, and a bare member from the types
around it and their bases, through heads read by name; and a C# type
that keeps its node while its owner turns from a type into a namespace of the
same name, or back, has its name touched for the name lane; the
csharp_surface_version marker is withdrawn and restored with the others; a
C#, Java, Kotlin, Swift or Dart file whose member surface row moved (a class's head, type
parameters or member-level lines changed, by a digest, or a class came or went)
re-resolves, whole, the unchanged files declaring a class of a name the changed
classes reach (closing over every head naming a reached name), since a call
through an inherited member reads its type from the classes it inherits by
name; a Dart class that neither came nor went narrows that: when its head,
supertypes and qualified name stayed, the types of its members are read from
its member lines before and after, and when its head changed, every member a
class of its ancestry declares, before the change or after it, counts; an
unchanged file below it then re-resolves whole only when it writes such a
member as a receiver (shape. or shape?.); a method body edit changes no row; the member_surface_version marker is
withdrawn and restored with the others; a Ruby file whose head row moved (a
class or module came or went, or its superclass or mixins changed) re-resolves,
whole, the unchanged files declaring a class or module the changed qualified
names reach (closing over every declaration writing a constant whose last
segment is a reached name's), since a bare call in a class reads the class's
ancestry from its declarations' source; the ruby_heads_version marker is
withdrawn and restored with the others; a .cfc file whose component row moved
(it came or went, declares other classes, or extends something else)
re-resolves, whole, the unchanged components whose extends names a class the
changed files declare (closing over every component extending a reached name),
since a bare call in a component reads its extends chain from the components'
text, and the unchanged CFML files holding a reference written with a receiver
whose last segment names a method a changed component gained or lost (every
method of one that came, went or was renamed) re-resolve whole, however many
there are, since a typed call waits while components of its type's name in
several files declare the method; the cfml_heads_version marker is withdrawn
and restored with the others; a Solidity file whose head row moved (a type
came or went, or its is clause names other bases) retries, at their sites,
the calls in the unchanged Solidity files declaring a type of a moved name or
one below it (closing over every head naming a reached name) that name a
method of a type which entered or left the chain of a type the file declares,
resolved or parked, since a bare call reads its contract's is chain from the
declarations' heads (a file with a chain at the resolver's limit of 60 names
re-resolves whole); the solidity_heads_version marker is withdrawn and
restored with the others; a Shopify theme root that came or went (a
layout/theme.liquid or config/settings_schema.json appeared in or left the
directory holding a theme's folders, read against the shopify_themes row)
hands the theme's JSON templates, on disk and indexed, in its templates/ and
sections/ folders of any case, to the per-path pass, which keeps or drops
each as a scan would, and re-resolves the theme's unchanged Liquid files whole,
since a JSON template is Liquid, and a section or snippet reference links its
file, only inside a theme; the moved roots are recorded before the pass, so a
sync that stops partway leaves them to the next; a marker counts though
ignored, so the watcher hands its events to the sync whether or not it is
source or in scope, watches an ignored config/ or layout/ itself for one,
and counts a marker folder removed or moved away as its marker gone; the
shopify_themes_version marker is withdrawn and restored with the others; a
Swift file whose head row moved (a type
declaration came or went, became or stopped being an extension, moved to
another qualified name, names other supertypes, writes another nested path, or
became or stopped being a @Reducer enum) re-resolves, whole, the unchanged
Swift files holding a reference to a name the changed declarations reach,
resolved or parked, or declaring a type of one (closing over every head naming
a reached name), and touches every name the file declared or declares (a
changed Swift file whose row stayed touches each name it now declares under
other qualified names than before, as when a standalone extension gains or
loses a member), sending the unchanged Swift files with unresolved references
written with a receiver ending in one down the whole-file lane however many
there are, since a type reference, a bare call's hierarchy and a type-path call
read whether a node is an extension, its scope, its heads and the path its
extension writes from the declarations' text; the swift_heads_version marker
is withdrawn and restored with the others; an Objective-C file whose head row
moved (a class came or went or its @interface names another superclass, a
property is declared with another type) touches the names of the members of
every class above a changed class or type, before the change and after it, and
every name declared in a file named after one, sending the unchanged
Objective-C files with unresolved messages written with a receiver ending in
one down the whole-file lane however many there are, since a message to self
or super, an untyped receiver and a typed local read a hierarchy from the
heads' text; the objc_heads_version marker is withdrawn and restored with the
others; a Go file whose head row moved (a type declaration came or went or its
text changed) touches the methods of every type its old and new declarations
mention and of the types those embed, before the change and after it, sending
the unchanged Go files with unresolved calls written with a receiver ending in
one down the whole-file lane however many there are, since a receiver's
embeddings, an alias's target and a field chain's field type are read from the
declarations' text; the go_heads_version marker is withdrawn and restored with the others;
a PHP file whose head row moved (a type came, went or changed kind, or its extends or
trait uses name other types, read through its file's imports) touches the names of the
methods of every type a changed type's ancestry gained or lost and of a type that changed kind,
re-resolves whole every unchanged PHP file declaring a type the ancestry gained or lost or a type
below a changed one whose ancestry starts or stops leaving the repository, and retries at its site
every unresolved call written with a receiver named after a class at or below a changed one,
however many there are, since a $this, self, static or parent call and an untyped receiver
read an ancestry from the heads' text (where an ancestry below a changed type reaches the
resolver's 80-type walk limit, every file declaring a type above or below it re-resolves whole);
the php_heads_version marker is withdrawn and restored with the others;
a touched name also retries every parked PHP, Pascal and CFML reference whose name equals it in
ASCII lowercase and re-resolves at its site every edge from those languages to a node whose
lowercased name equals it, since their names ignore case and a namesake in another case coming
or going changes the one candidate a case-insensitive match keeps; those retries live in the
sync's memory, so the case_folded_names_version marker is withdrawn and restored with the
others, and a sync that finds it missing re-resolves every PHP, Pascal and CFML file;
a Dart file whose head row moved (a type came, went, became or stopped being an extension, names
other supertypes, or sits on another type) touches the names of the members of every type of a
changed type's ancestry, before the change and after it, and of every extension on one of those,
and retries at its site every parked Dart call written with a receiver to a member of such a name,
however many there are, since a bare call's class hierarchy and an extension's owner are read from
the heads' text; a non-extension type whose head moved, came or went has the unchanged Dart files
that reference it or a type below it (an edge to such a type before the change or after it, or a
reference left unresolved, by its name or through it as a receiver), and the files declaring a
type below one such a file declares, retry at their sites the references written with a receiver
to a member of a type whose distance from it changed or of an extension on one, parked or
resolved, since a member read or a call on a receiver typed by it reaches members through its
lineage and the receiver is typed by what its file writes or by a field of a class it extends; a getter, an extension's member or a type's constant that a
changed file newly declares (no declaration of the file had its qualified name, kind and signature
before) on a type that existed before, or on an extension, retries at its site every parked Dart
reference written with a receiver to its name, however many there are, since a member read, a
typed call and a call through a type wait for one by name; the dart_heads_version marker is withdrawn and restored with the others, and a
sync that finds it missing, or a row it cannot read, withdraws it before changing anything and
re-resolves every Dart file;
a Dart file whose library row moved, or a Dart file added or removed, has the sync read the Dart
library model twice, from the rows before the change and after it: an unchanged Dart file whose
scope (its library and the files its imports apply from), imported libraries (an unprefixed import
naming no project library left out) or directive targets moved re-resolves whole, and, library by
library, every name a namespace brings in or lets out before against after (a file joining or
leaving it touches its top-level names and its unnamed extensions' members) is touched for the
name lane, since a receiver-less Dart name reaches what its library and the libraries it imports
export along export chains read from the files' heads; a type whose reach moved for a file that
writes it in an extends or implements touches, too, the names of the members of its ancestry
and of the extensions on one of those types (read from the head rows), and retries at its site
every parked Dart call written with a receiver to such a name, however many there are, since a
call on a subclass reaches them through that edge; an added Dart file retries at its site every
parked Dart import whose URI ends in its name, however many there are; a moved Dart import
node touches no name, since no resolution reads one and its URI, written as a bare file name,
names every file of that name; the dart_libraries_version marker is withdrawn and restored with
the others, and a sync that finds it missing, or a row it cannot read, withdraws it before
changing anything and re-resolves every Dart file; the pubspec.yaml and pubspec_overrides.yaml
files a Dart file's package and path: dependencies are read from are no source files, so every
index and sync also keeps a dart_pubspec: row per folder above a Dart file that holds one, with
both texts, and a sync re-reads them (a CLI sync always, a watcher's when a path it was handed is
named like one, or a Dart file came or went): when they moved, the before and after library models
each read their own state's manifests, and the same comparison re-resolves what a package renamed,
a folder becoming a package or a path: dependency moved, rather than every Dart file;
a changed Dart file moves the later links of the call chains in unchanged Dart files through the
names its declarations changed (a member or top-level declaration, as the member surface rows keep
it, came, went or reads otherwise; a member of an extension whose head changed, came or went; a
name a library's visibility moved; and what a constant or field whose untyped initializer names
one declares, until none is added) and through a class whose head changed, came or went, which
moves every lookup on it and on the types below it: what every declaration whose type names such
a class declares joins it (a member typed by one, a subtype, whose head names it, an untyped
initializer naming one, never a constructor, until none is added), and a chain reading one of
those names moves only when it also reads a name the class's ancestry declares, before the change
or after it, or an extension on a type of it (every extension's, for a class that came or went),
since a chain's typing reads other files only through names its file writes or a declaration it
read on the way names; in each unchanged Dart file writing such names, the parked chain links and
the call edges of the chain links whose typing reads them are retried at their sites, read as the
typing reads them: the chain's head, links and type arguments, not a call's arguments, the type it
is written in, and for a local or a parameter the declaration the typing's own lookup finds (its
declared type, or what its initializer reads in turn), its name read only as a type's; every chain
link is retried when a call method is among the names; the lane rides on the member surface rows,
so a sync that rebuilds them re-resolves every Dart file;
a changed R file touches each name of a base function it binds as a variable or constant, before
the change and after it, since whether such a binding is what a call of its name means is read
from its line, which no node records (c <- list(f = function(x) x), upstream #2239); the
r_base_bindings_version marker is withdrawn and restored with the others, and a sync that
finds it missing re-resolves every R file;
a Lua file whose module row moved touches the name of each declaration that became or stopped
being local, since a local is invisible outside its file, and when the digest of what an
alias through it reads changed (its functions, its variables and their locality, in source
order, each name's first export), or it came or went, re-resolves whole every unchanged Lua
file whose aliases require it and the files requiring those, four modules deep, passing
through the files the sync changed by what they read before the change and after it; an
unchanged Lua file whose aliases or exports read a touched name through a global table
(local snooze = ngx.sleep reads every node named sleep) re-resolves whole and counts as
such a module, and so does a changed one for the files requiring it; the
lua_modules_version marker is withdrawn and restored with the others, and a sync that finds
it missing, or a row it cannot read, withdraws it before changing anything and re-resolves
every Lua file;
a Rust enum variant that came, went or changed touches its enum's name, since a variant path
reaches an enum only while it declares the variant; a Rust type, trait or alias name that came, went or changed re-resolves, in every unchanged
Rust file whose text writes it as a path receiver (Name::, Name.), the references written
through it, since a CamelCase path receiver is an outside type unless the project declares a
type of its name, and the rust_receivers_version marker is withdrawn and restored with the
others; a bare Rust name's scope reads its own file's use items, which re-resolve with it, and the
Cargo.toml files at or above it, which are no source files (a Cargo.toml change is the
control-input lane's, below);
an import names a file, a folder or a namespace rather than a symbol, so the parked
imports an added file or a changed file's namespace can satisfy (by the file's
name and stem, its folder's, or the namespace's whole name), and the parked
references written as a path (a Liquid {% render 'price' %} waits under
price for snippets/price.liquid), send their files
down the whole-file re-resolution lane, which rebuilds an unchanged file's
resolved edges and keeps what its extraction produced (contains edges and
same-file value references). A call through a class name (User.create())
reads the supertypes each class head names, at most five levels up, which no
node records: every index and sync keeps a class_heads: row per file declaring
such a type, with what each declaration's own head writes, and a sync seeds the
types whose entries came, went or changed, re-resolves at their sites the edges
to methods named like those of a seed's old or new supertypes (five levels up),
and retries the calls parked through a seed or a type up to four levels below
it, however many there are; while it resolves, a sync whose rows were current
as it started reads the heads of a file it neither rebuilds nor removes from
that file's row rather than its source; the class_heads_version marker is
withdrawn and restored with the others. Resolution also reads control files, which are no
source files: the JS/TS path-alias configs along their extends chains, the
workspace manifests and the folders a workspace glob lists, the package.json
and Deno manifests above each JavaScript-family file, the C# project files
above each C# file, and the Cargo.toml files above each Rust file; and the
frameworks it detects. Each reader reports what it read or probed in the same
traversal, and every index and sync keeps a control_inputs: row per kind
(js, csharp, rust) holding each input's digest by path (the sha256 of
its text, or of a listed folder's names; null for a missing one; one outside
the project by the absolute path its reader used), and a frameworks_detected
row. A sync re-reads a kind's inputs (a CLI sync always; a watcher's when a
path it was handed is a recorded input, a control file of the kind, or a folder
coming into a workspace glob's listing, or when a file of the kind came, went
or changed language), whole, with no size or time shortcut; a kind whose inputs
moved re-resolves every file of its languages whole, and a framework that came
or went re-extracts every file, since framework nodes are made at extraction.
The control_inputs_version marker is withdrawn and restored with the others,
and frameworks_version only by a sync re-extracting for a framework change.
The watcher hands a control file's event to the sync, ignored or not, and an
event for a recorded input, reading the rows at start and after each of its
syncs; an input in a folder it does not watch (a config an extends chain
reaches in node_modules), or one another process's sync comes to record
before the watcher's next sync, sends no event that reaches a sync: the next
CLI sync, or a daemon's catch-up sync at start, compares it. An edited JavaScript-family module sends down the
same lane each importer whose statement of it holds a binding the import did not
resolve, parked or matched by its name (W of import { Widget as W } while
the module declares no Widget), read before the module's old nodes go: the
edit may give the module what the binding imports under another name, which no
name lookup finds. A dependent a sync refreshes keeps its nodes and has its
references read again: while the resolved_refs_version marker a full index
wrote holds for the current extraction inputs, the ones stored for it (the rows
resolution moved into resolved_refs and the ones still in unresolved_refs),
only at the sites it refreshes unless the file goes whole, and otherwise, and
always for project.godot, whose extraction reads other files, an in-memory
extraction, run in parallel across the dependents. Every dependent's new
resolution state is then written in one transaction. Each mutation acquires the
store's short-lived index.lock writer authority; readers use corroborated state
and shared leases. A separate persistent writer.pid file carries an OS kernel
exclusive lock for the lifetime of the ONE process allowed to run watcher/catch-up
services for that project. Its JSON (pid, mode, start time) is diagnostic only;
the kernel lock is authority and is released automatically on process exit.
Default daemon mode multiplexes every MCP client onto that one writer. On a cold
start the foreground stdio session answers immediately without its own background
services while the detached daemon takes ownership. Once the daemon's socket is
up, the session retains a passive lease on it, the same client connection an
explicit projectPath service holds, so the daemon does not idle-exit while the
session lives. If that lease stops being live (the daemon died, was replaced, or
the index was re-created), the session starts or re-attaches a daemon with
exponential backoff, and never creates an index that is gone. A session that
meets a daemon of an older plain X.Y.Z release asks it to drain over the
project-bound control channel and starts one from its own install; a daemon of
any other version keeps the project. Each daemon also exits once its own
executable is replaced or removed. Explicit direct mode
(CODEGRAPH_NO_DAEMON=1) may own the writer instead; a second direct process fails
fast rather than alternating sync mutations. An unindexed explicit path remains
state-free: only codegraph init creates an index namespace.
Streamable HTTP is different: a server is keyed by bind address and can be pinned to one project or globally accept explicit per-call projects. Detached HTTP and foreground stdio processes use global observability registries. Those registries do not replace project authorization or database leases.
Operational details and environment variables are in cli.md and
mcp.md.
The durable compatibility boundary is the canonical graph, not SQLite file bytes or process scheduling. Tests lock:
- node ID inputs and file-node IDs;
- canonical nodes, edges, unresolved references, files, and schema;
- incremental/full-index convergence;
- deterministic candidate and output ordering;
- structural MCP compatibility; and
- installer/release asset contracts.
See equivalence.md. Performance evidence is a separate
measurement discipline described in benchmark.md.