CA Code Graph: A Lightweight, Confidence-Labelled Code-Graph Engine for Agents over MCP
A technical white paper
| Project | CA Code Graph (ca-codegraph) |
| Version | 0.1.0 |
| Status | Beta: reference implementation |
| License | Apache-2.0 |
| Document revision | 1.1 |
| Date | June 22, 2026 |
| Audience | Engineers and architects building code-intelligence for AI coding agents |
Abstract
Large language model (LLM) coding agents are bottlenecked not by reasoning but by context: to edit a codebase correctly, an agent must know where symbols are defined, who calls them, what they inherit, and which HTTP routes they back. Retrieval by embedding similarity is lossy and cannot answer relational questions precisely. CA Code Graph is an original, production-oriented engine (version 0.1) that extracts the hierarchical and relational structure of a codebase using deterministic static analysis and serves it to agents over the Model Context Protocol (MCP).
CA Code Graph’s central engineering thesis is honest, tiered resolution: it
combines the breadth of tree-sitter syntactic extraction with scope/import
heuristics, and a designed (not-yet-wired) path to compiler-grade SCIP
precision, in a single graph, and it labels every relationship with an explicit
confidence (precise, heuristic, or syntactic), so an agent (or a human)
is never shown a guess dressed as a fact. The system is lightweight (a single
Python package plus embedded SQLite, no daemons), fast (most warm structural
queries in well under a millisecond, name search in low-single-digit
milliseconds, ~10 kLOC/s cold indexing), deterministic (byte-identical graph
exports across runs), and offline on the query hot path. It exposes 16
agent-native tools (including a
token-budget-bounded pack_context and a PageRank-ranked get_repo_map) and
ships with a full regression suite (125 tests, including live MCP-client
conformance) and a license-compliance gate. This paper describes the
architecture, the resolution model, the data schema, the analytics, and the
empirical evaluation.
Table of Contents
- Introduction & Motivation
- Design Goals, Constraints & Anti-Goals
- System Architecture
- Graph Schema & Symbol Identity
- The Extraction Engine
- Tiered Resolution: The Accuracy Core
- Framework-Aware Route Extraction
- Storage, Determinism & Incrementality
- Graph Analytics
- The MCP Server & Tool Surface
- Performance Evaluation
- Correctness & Quality Assurance
- Licensing & Supply-Chain Posture
- Comparison to Prior Art
- Limitations & Future Work
- Conclusion
- Appendices
1. Introduction & Motivation
1.1 The context problem
A coding agent operating on an unfamiliar repository faces a structural information deficit. Reading files linearly does not scale; grep finds strings but not meaning; and vector retrieval surfaces similar-looking code rather than structurally-related code. The questions an agent actually needs answered are relational and exact:
- Where is this symbol defined, and what is its signature and docstring?
- Who calls this function? What does it call? What does it inherit or implement?
- Which file imports this module? Which HTTP route invokes this handler?
- Given a token budget, what is the most relevant slice of context around this symbol?
These are graph queries over a precisely extracted model of the code, not
similarity searches. The state of the art (Sourcegraph/SCIP, GitHub’s precise
code navigation, tree-sitter-stack-graphs, aider’s repo-map, Meta’s Glean)
demonstrates that such a model is achievable, but the tooling is typically heavy
(language servers, indexers, databases) or tied to a hosting product.
1.2 The opportunity
Two trends make a lightweight, agent-native code-graph engine timely:
- Tree-sitter provides fast, error-tolerant, incremental parsing for 100+
languages behind a uniform interface, with a community convention
(
tags.scm) for extracting symbol definitions and references. - The Model Context Protocol (MCP) standardizes how agents discover and call tools, so a single server can serve Claude Code, Cursor, VS Code Copilot, and others without per-client integration.
CA Code Graph occupies the intersection: it turns any repository into a queryable graph with one command and serves it over MCP, with no external services and no LLM in the indexing loop.
1.3 The core idea: honest, tiered resolution
The original engineering contribution is not any single extraction technique but the unification of breadth and precision under explicit confidence labels in one graph. Tree-sitter gives broad, zero-setup coverage but only syntactic certainty; scope/import analysis adds heuristic cross-file binding; and compiler-grade indexers (SCIP) would add precision at the cost of a toolchain. CA Code Graph composes these as tiers in a single graph, tagging every edge with the tier that produced it:
precise(structural fact today; compiler/SCIP-grade once Tier 2 is wired) >heuristic(scope/import-table binding) >syntactic(name match within scope).
This makes the output trustworthy: an agent can weight a precise structural edge
differently from a syntactic guess, and unresolved references are surfaced by
name rather than silently dropped or wrongly bound. The Tier-2 SCIP upgrade is
designed and detection-wired but does not yet produce edges in this build (§6.1).
2. Design Goals, Constraints & Anti-Goals
2.1 Success criteria
A senior reviewer can: (1) point the tool at any repo and obtain a queryable
graph in one command; (2) run serve and have a real MCP client discover and
call tools; (3) obtain LSP-shaped outlines and relational queries; (4) see explicit
confidence labels on every edge; (5) re-index only changed files incrementally
with measured latency; and (6) read a complete third-party-license accounting.
2.2 Hard constraints
| Constraint | How it is met |
|---|---|
| Original implementation | Conventions adopted from prior art; all code original. |
| Permissive licenses only | Runtime closure verified at build time; CI gate fails on GPL/LGPL/AGPL. |
| Lightweight, no daemons | Single package + embedded SQLite; pure-Python PageRank (no numpy/networkx). |
| Fast / accurate / reliable | Measured budgets (§11); tiered resolution (§6); graceful degradation (§12). |
| MCP-native, spec-compliant | FastMCP; stdio default + Streamable HTTP; SSE intentionally omitted. |
| No LLM in indexing | Deterministic static analysis only. |
| No network on query hot path | Serving is fully local/offline; indexing may shell out to local toolchains. |
2.3 Anti-goals
Embeddings are not the source of truth (fuzzy search is a convenience rank only); no bespoke symbol taxonomy is invented (everything maps to LSP/SCIP); no competitor source is copied or copyleft code vendored; and a confidence label is always surfaced: a heuristic is never presented as a fact.
2.4 Maturity
Version 0.1 has production-ready discovery, parsing, extraction
(Python/TypeScript/TSX/JavaScript), Tier-0 and Tier-1
resolution, the SQLite store, incremental re-index, the full MCP/CLI surface, and
the determinism, robustness, and licensing guarantees. Not yet wired: the Tier-2
precise path performs toolchain detection only: SCIP subprocess invocation
and edge upgrade are designed but unimplemented in this build (see §6.1, §15), so
all precise labels currently derive from structural facts (CONTAINS, in-file
HANDLES), not compiler-grade cross-references.
3. System Architecture
CA Code Graph is a staged pipeline. Each stage is independently testable and communicates through a small internal intermediate representation (IR), so languages, storage backends, and route frameworks are pluggable.
3.1 Module map
| Concern | Module(s) |
|---|---|
| IR, taxonomy, IDs, config | model.py, ids.py, config.py |
| Discover / parse | discover.py, parse.py, langs/ (+ queries/*.scm) |
| Extract | extract/{engine,base,python,typescript}.py |
| Resolve | resolve/{resolver,symtab,scip}.py |
| Routes | routes/{base,python_frameworks,js_frameworks}.py |
| Graph analytics & search | graph.py, search.py |
| Storage | store/sqlite.py |
| Orchestration | pipeline.py, watch.py |
| Serving surface | api.py, mcp_server.py, cli.py |
3.2 Design principle: a thin IR seam
The IR (model.py) defines Node, Edge, and RawReference as lightweight
slots dataclasses (cheap on the hot path). Extraction produces nodes and raw
references; resolution binds references into typed, labelled edges; storage and
serving consume only the IR. Because nothing downstream of extraction touches a
tree-sitter tree, the extraction layer can be swapped per language, and results
are serializable (enabling parallel extraction and a clean storage boundary).
4. Graph Schema & Symbol Identity
4.1 Nodes
Each node carries both an LSP SymbolKind integer (for editor/agent interop
and outline shaping) and a richer node_type string that preserves
distinctions LSP collapses (e.g. trait versus interface, type_alias versus
class) plus the framework-derived route. Minimum attributes:
id, name, fqn, kind (LSP int), node_type, language, path, 0-based
start/end_line, start/end_col, start/end_byte, signature, doc,
visibility, content_hash, container_id, an extra map (name-token position,
route metadata), and a persisted centrality score.
4.2 Edges
| Edge | Meaning |
|---|---|
CONTAINS | Hierarchy backbone (file → class → method). |
CALLS | Function/method invocation (including new/construction). |
IMPORTS | Module/symbol import dependency. |
INHERITS | Class extends class. |
IMPLEMENTS | Class implements interface/trait. |
REFERENCES | Generic symbol use (type annotation, identifier). |
HANDLES | Route → handler symbol. |
DECORATES (adv.) | Decorator applied to a symbol. |
RETURNS_TYPE / PARAM_TYPE (adv.) | Reserved type edges defined in the schema for future type-annotation extraction. |
Every edge has a mandatory resolution label and a path:line:col. An
unresolved edge retains dst_name + scope_hint instead of dst_id, so
“what does this reference, by name” remains answerable without overstating
resolution.
4.3 Symbol identity (SCIP-inspired, deterministic, structural)
Node identifiers are deterministic, structural strings, not position-based, so editing one symbol does not renumber everything below it (a property that keeps IDs stable under incremental edits):
codegraph <language> <relpath>#<descriptor-chain>
The descriptor chain concatenates SCIP-style suffixes from the outermost container down to the symbol:
| Kind class | Suffix | Example |
|---|---|---|
| module / namespace / package | / | pkg/ |
| class / interface / struct / enum / trait / type-alias | # | User# |
| function / method / constructor | (). | display(). |
| property / field / variable / constant / enum-member | . | MAX. |
| route (custom) | ! | GET:%2Fusers! |
Example:
codegraph python models.py#User#display().denotes thedisplaymethod of classUserinmodels.py. Descriptor-breaking characters in a name (spaces,/,#,()) are percent-escaped, so a route descriptor for path/usersrenders asGET:%2Fusers!.
Overloads and same-named siblings receive a deterministic, source-order
disambiguator (f()., f(1).). This scheme is SCIP-inspired, not byte-for-byte
SCIP: real SCIP indexes are consumed separately for the precise tier and mapped
onto these IDs. Two indexing runs of the same commit produce byte-identical
graph exports (verified by test).
4.4 Standards alignment
CA Code Graph deliberately does not invent a taxonomy. SymbolKind uses the LSP
integer values verbatim; outlines are DocumentSymbol-shaped (name, detail,
kind, range, selectionRange, children); positions are 0-based per LSP and
also carry byte offsets. The full mapping is documented in CONVENTIONS.md.
5. The Extraction Engine
5.1 A query-driven backbone with per-language hooks
Extraction has two layers. The language-agnostic backbone (extract/engine.py)
runs a per-language tags.scm tree-sitter query (using standardized capture
names @definition.*, @reference.*, @name), reconstructs the containment
hierarchy from tree ancestry, assigns the deterministic IDs of §4.3, and emits
Nodes, CONTAINS edges, and RawReferences. Per-language hooks
(LanguageExtractor subclasses) then enrich each definition with the
language-specific detail the generic query cannot capture.
This division is what lets any language ship a tags.scm and obtain a Tier-0
graph with zero project setup, while Python and TypeScript/JavaScript get precise
signatures, docstrings/JSDoc, visibility, import maps, and heritage edges. A
registered-but-unqueried language (e.g. Go) degrades gracefully to a file node,
never a crash.
5.2 Why tree ancestry, not flat captures
A flat list of query captures cannot express nesting. CA Code Graph reconstructs the
hierarchy by walking from each captured definition up the concrete syntax tree to
find its enclosing definition, building a container stack in source order. This
yields correct CONTAINS edges, fully qualified names, and a stable disambiguator
for overloaded names. It also reclassifies a function nested in a class as a
method (and __init__/constructor as a Constructor).
5.3 What the hooks extract
For Python: parameter/return signatures (including async), docstrings,
PEP-8 visibility (_protected, __private, dunders public), import/from … import … as maps with alias bindings, base classes (INHERITS), decorators
(DECORATES), and module/class-level constants versus variables. For
TypeScript/TSX/JavaScript: typed signatures, JSDoc/line-comment docs, TS
accessibility modifiers and #private fields, ES-module imports (named, default,
namespace), and extends/implements heritage. Both languages capture call
receivers (self.m(), obj.m()) to inform Tier-1 resolution.
5.4 Robustness
Tree-sitter is error-tolerant: a file with syntax errors still yields a partial tree, and CA Code Graph indexes what it can. A read or parse failure is recorded on the result and the run continues: a single bad file never aborts the index.
6. Tiered Resolution: The Accuracy Core
Resolution turns RawReferences into typed, confidence-labelled edges. For each
reference, the most precise applicable strategy is tried first; the strategy that
succeeds sets the label. This is the precedence rule
(precise > heuristic > syntactic) made operational.
6.1 The three tiers
Tier 0: Syntactic (always on). Bind a reference by name within its file, or
to a globally unique definition. Broad coverage, no toolchain. Label
syntactic. Crucially, Tier 0 only binds same-file or globally unique names;
ambiguous names are left unresolved rather than guessed.
Tier 1: Heuristic (default on). Scope and import-map reasoning:
self/this/clsreceivers resolve to a member of the enclosing class, including inherited members, found by walking the resolved inheritance graph (a pre-pass binds everyINHERITS/IMPLEMENTSbase to its class id);- receiver-type inference:
obj.method()resolves whenobj’s class is knowable:self.attr(fromself.attr = Class(...), an annotation, or a typed__init__/constructor parameter), a local variable (fromx = Class(...)or an annotation), or a typed function parameter (def f(x: Class)). The inferred type name is bound to a class and searched (including inherited members); - bare imported names resolve to the imported symbol, following re-export
barrels: a TS
export … from "./impl"index file, or a Python__init__.pythat re-imports a name, is transparently chased to the original definition (named andexport */import *wildcards, with cycle guards); module.fn()resolves through the module’s import target;- cross-file imports resolve module specifiers to in-repo files. This handles the
patterns real repos actually use: Python dotted and relative modules, plus
src/-layout package roots (a file atsrc/app/x.pyimportable asapp.x); and JS/TS relative specifiers andtsconfig.jsonpath aliases (@/utils/x, custompaths,baseUrl) with extension/indexresolution. Path aliases are discovered monorepo-wide: everytsconfig/jsconfigin the tree is loaded (followingextendschains) and the nearest one is applied per file, so a backend package's@utils/*and a frontend package's@/*resolve independently. Without these, cross-module resolution on a typical TS repo collapses to the syntactic tier: they are essential, not optional; - import-aware fallback: when a name is explicitly imported but its specifier
can't be pinned to a definition (a workspace package, an unconfigured/foreign
alias, an unfollowable barrel) yet exactly one definition of that name exists
in the repo, the call binds to it with a
heuristiclabel. Distinctively named symbols (parseMentions,sendMentionEmail) thus resolve even when their import path is opaque, while truly external names (useState) have no in-repo definition and so never mis-bind.
These bindings are labeled heuristic. Tier 1 overrides Tier 0 for the same
reference. Receivers whose type is genuinely unknowable (e.g. a loop variable
for t in items: t.fire()) are
not upgraded: they stay syntactic (or unresolved), preserving the confidence
contract.
Tier 2: Precise (opt-in subprocess; designed but detection-only in this
build). When a SCIP indexer (scip-typescript, scip-python,
rust-analyzer, …) is present on PATH, invoke it as an external subprocess
(never linked, avoiding any licensing concern), parse the emitted SCIP index,
map its symbols onto CA Code Graph IDs, and upgrade matching edges to precise. In
the current build, only toolchain detection is implemented:
resolve/scip.py locates indexer binaries via shutil.which and reports them,
but it does not spawn a subprocess, ingest a SCIP index, or upgrade any edge
(enriched_edges = 0). Subprocess invocation, SCIP protobuf ingestion, the
symbol-to-id mapping, and time-boxing are future work (§15). The path is
gracefully skipped: the lower tiers remain authoritative and a run never fails
on a missing or unwired optional dependency.
Consequently, every precise label produced today comes from a structural
fact (CONTAINS and in-file HANDLES edges, which are exact by construction),
not from a compiler-grade cross-reference. The §6.2 example reflects this:
its resolved edges are heuristic or syntactic.
6.2 Worked example
Given a small package (models.py, services.py, web.py), CA Code Graph resolves:
| Reference | Edge | Label | Why |
|---|---|---|---|
class User(Entity) | INHERITS User→Entity | heuristic | same-file type binding (Tier 1) |
make_user builds User() | CALLS make_user→User | heuristic | User imported from .models |
display calls format_name | CALLS display→format_name | syntactic | same-file unique name (Tier 0) |
web.py imports fastapi | IMPORTS web→fastapi | syntactic | external module: unresolved, by name |
services imports .models | IMPORTS services→models | heuristic | in-repo module resolved (Tier 1) |
Disabling Tier 1 (--no-tier1) yields a graph with zero heuristic edges:
only Tier-0 syntactic resolution plus the structural precise backbone
(CONTAINS / in-file HANDLES) remains, the mechanism used to attribute Tier 1’s
contribution.
7. Framework-Aware Route Extraction
HTTP routes are high-value agent context that does not fall out of generic AST
traversal. CA Code Graph defines a pluggable RouteExtractor registry; each extractor
inspects a parsed file and yields Route nodes (method, path pattern, framework,
middleware) plus HANDLES edges to the handler symbol.
| Language | Frameworks |
|---|---|
| Python | FastAPI, Flask, APIRouter/Blueprint (decorator routing, including methods=[…] expansion), Django urls.py (path/re_path/url) |
| JS / TS | Express / Koa / Fastify method calls (app.get('/x', handler)), NestJS controller decorators (@Get('/x')) |
The handler is linked to its symbol node when defined in the same file (by byte span or by name); otherwise the route records the handler name without claiming a resolved target. A misbehaving extractor is caught and never aborts indexing, and adding a framework is a small, self-contained plugin.
8. Storage, Determinism & Incrementality
8.1 Why SQLite
CA Code Graph persists the graph in an embedded SQLite database: the best fit for
“lightweight, no daemon.” It provides recursive common table expressions (CTEs)
for graph traversal (callers/callees, type hierarchy), atomic transactions, and a
single-file footprint with zero external services. Indexes cover the hot lookups
(nodes(path, name, fqn, node_type, container), edges(src+type, dst+type, type, path)), and a unique index deduplicates edges. Heavier embedded graph databases
(Kùzu, DuckDB) were considered and rejected as unnecessary at this scale; an
in-memory-only approach was rejected for lacking warm, persisted queries.
A schema_version is stamped into every index; an incompatible version triggers
a clean rebuild, which is always safe because the index is a derived cache.
8.2 Determinism
Determinism is a first-class property. Discovery yields files in sorted order;
IDs are structural (§4.3); read queries impose a deterministic ORDER BY; and the
PageRank power iteration processes nodes in sorted ID order. As a result, two
indexing runs of the same source produce byte-identical graph exports, a
property asserted directly in the test suite.
8.3 Incremental re-index
A full index is content-hashed per file. When a file changes, reindex_changed re-hashes
the working tree, surgically deletes the nodes/edges of changed and deleted files,
re-extracts only the changed files, and re-resolves their references against the
current global symbol table, so cross-file edges (e.g. a new caller in a new
file) bind correctly. PageRank centrality, a ranking heuristic, is refreshed on
full index and intentionally left slightly stale on the incremental path to keep
edits fast; new symbols rank neutral until the next full index. A watchfiles-based
watcher drives this loop, filtering the index directory to avoid feedback.
9. Graph Analytics
9.1 Centrality (original PageRank)
Symbol importance is computed with a weighted PageRank over the resolved-edge
subgraph (calls, imports, inheritance, implements, references, handles), using an
original pure-Python power iteration: no numpy, scipy, or networkx dependency,
keeping the footprint minimal. It is deterministic (sorted node order, uniform
dangling-mass redistribution) and computed once at index time and persisted to
the nodes.centrality column, so warm repo_map/pack_context read it from
storage instead of recomputing.
9.2 get_repo_map: a high-signal overview
Inspired by aider’s repo-map, get_repo_map ranks files and their symbols by
centrality and emits a token-budget-bounded overview (file → top symbols with
signatures and centrality), so an agent can orient in a large repo cheaply.
9.3 pack_context: bounded context assembly
pack_context returns a ranked, token-budget-bounded bundle around a focus
symbol: the symbol’s detail, its container, and its most relevant neighbors
(callers, callees, members, types, imports), each ranked by edge weight ×
centrality and truncated to the budget. Strategies (balanced, callees,
callers) bias the mix. Every neighbor carries the resolution label of the edge
that surfaced it. This directly answers “give me exactly the context I need to
edit this symbol, sized to fit my window.”
10. The MCP Server & Tool Surface
CA Code Graph serves the graph over MCP using FastMCP. The default transport is
stdio; --http enables Streamable HTTP bound to 127.0.0.1 with
DNS-rebinding/Origin protection. SSE is intentionally not implemented because
it is deprecated. The server is read-only and picks up external incremental writes via
SQLite WAL, so a watch process can run alongside it. Tool input schemas are
derived from typed signatures; responses are token-efficient (IDs plus minimal fields
by default), and every edge-bearing response surfaces resolution.
10.1 The 16 tools
| Tool | Purpose |
|---|---|
get_index_info | Counts, languages, enabled tiers, confidence breakdown, staleness. |
get_file_outline | Nested DocumentSymbol hierarchy for a file. |
find_symbol | Locate definitions by name/kind (exact or substring). |
get_symbol | Full detail: fqn, kind, signature, doc, location, container, members. |
get_definition | Position → definition (0-based, LSP-style). |
get_references | All usages (by id or position), each with resolution. |
get_callers / get_callees | Call-graph traversal, retaining unresolved calls by name. |
get_type_hierarchy | Supertypes / subtypes / implementations. |
get_imports / get_importers | Module dependency edges, in and out. |
list_routes | HTTP routes → handler symbols, filterable by framework/method. |
get_neighborhood | Subgraph around a symbol over chosen edge kinds. |
search_symbols | Fuzzy, ranked search over fqns (rapidfuzz). |
pack_context | Token-budget-bounded context bundle. |
get_repo_map | Centrality-ranked repo overview (PageRank). |
A single shared query layer (api.py) backs both the MCP server and the CLI, so
behavior is identical across transports and the command line.
Never silently empty. Relationship tools degrade explicitly: get_callers and
get_references return their statically-resolved results and a
possible_*_by_name list of call sites that reference the symbol’s name but did
not resolve to it (dynamic dispatch, exotic imports, or, until the precise tier
is wired, a cross-reference only SCIP would catch). These are explicitly labelled
unverified, so an empty resolved result becomes "here are N name-matched
candidates to confirm" rather than a confidently wrong "no callers." This directly
addresses the failure mode where a sparse caller list misleads an agent.
10.2 Conformance
A live MCP client connects to the stdio server as a subprocess, runs
initialize/list_tools, and successfully calls get_file_outline,
find_symbol, get_references, list_routes, and pack_context, exercised in
CI (tests/test_mcp.py) and reproducible via examples/mcp_session.py.
11. Performance Evaluation
11.1 Methodology & environment
Benchmarks run against a synthetic repository generated to a chosen size, with
cross-module imports, inheritance, and calls so that resolution and PageRank are
exercised. The cold index measures parse + extract + resolve + store (+ one-time
PageRank). Warm queries run against the persisted SQLite store with no re-parse,
reported as p50/p95 over 50 iterations for the five representative tools
below. The incremental measurement edits a single file and re-resolves only
changed references. Numbers are machine-dependent; the harness
(benchmarks/bench.py) is the deliverable and is fully reproducible.
Test environment. Apple M4 (10 cores), 16 GB RAM, macOS 15.6.1 (arm64), CPython 3.13.3, tree-sitter 0.25.2, tree-sitter-language-pack 1.12.0.
Caveat. The headline cold-index, incremental, and memory figures in §11.3 are single-run point estimates (the committed harness runs once); only the §11.4 warm-query latencies are reported as p50/p95 over repeated iterations. They should be read as order-of-magnitude characterizations, not precise constants.
11.2 Corpus
The throughput/memory corpus is synthetic (uniformly generated modules), so its parse/resolve costs are representative but its resolution rates are best-case (clean imports); a real-world codebase is measured separately in §11.6.
| files | LOC | nodes | edges |
|---|---|---|---|
| 500 | 69,500 | 24,000 | 57,500 |
11.3 Throughput & memory
| Metric | Value |
|---|---|
| Full cold index | 6.86 s |
| Index throughput | 10.1 kLOC/s |
| Single-file incremental | 148.2 ms |
| Peak Python heap (tracemalloc) | 119.1 MB |
| Peak RSS | 291.3 MB |
11.4 Warm query latency
| Tool | p50 (ms) | p95 (ms) |
|---|---|---|
get_file_outline | 0.846 | 0.914 |
find_symbol | 1.942 | 2.134 |
get_callers | 0.030 | 0.050 |
pack_context | 8.658 | 8.916 |
get_repo_map | 125.069 | 131.061 |
11.5 Analysis
Most structural lookups (outline, callers, definition) are sub-millisecond;
fuzzy name search (find_symbol) is low-single-digit milliseconds (≈1.9 ms p50)
because it scores every symbol with rapidfuzz. pack_context is single-digit
milliseconds because centrality is persisted, not recomputed. This was a
deliberate optimization: during development, the pre-caching implementation that
recomputed PageRank on every call measured pack_context ≈ 658 ms and
get_repo_map ≈ 775 ms p50 on this corpus. Persisting centrality therefore yields an
≈75× speedup for pack_context. (Those pre-optimization figures were observed in
development and are not reproduced by the committed single-path harness.)
get_repo_map remains the heaviest warm query (≈125 ms) because it materializes
and ranks all symbols. This is acceptable for an orientation tool whose output is
bounded by the token budget. Moving PageRank to index time raised cold-index time but kept
the incremental path fast (≈148 ms), the latency that matters for an edit-driven
watch loop.
11.6 Resolution coverage & accuracy
Resolution quality matters as much as speed: references must bind correctly, and
the remainder must retain an accurate confidence label. The table below reports,
for the two hand-verified golden fixtures and for a real codebase: CA Code
Graph’s own source (dogfooding), the
share of relationship edges (CALLS/IMPORTS/INHERITS/IMPLEMENTS/
REFERENCES/DECORATES, excluding the always-precise structural backbone)
that bind to an in-repo target, broken down by tier.
| Corpus | rel. edges | resolved (have dst_id) | heuristic (Tier 1) | syntactic (Tier 0) | CALLS resolved |
|---|---|---|---|---|---|
py_app (golden, Python) | 32 | 11 (34%) | 10 | 24 | 7 / 21 (33%) |
ts_app (golden, TS) | 18 | 10 (55%) | 9 | 9 | 6 / 13 (46%) |
CA Code Graph src/ (real, Python) | 2,135 | 870 (41%) | 438 | 1,697 | 785 / 1,896 (41%) |
Interpreting resolution rates. A 34–55% resolution rate is expected and
correct, not a deficiency: the unresolved remainder is overwhelmingly calls to
the standard library, builtins, and third-party packages (len, str.strip,
json.dumps, framework methods) that are genuinely not in the repo graph and
are therefore surfaced by name rather than wrongly bound. Tier 1 contributes a
substantial share of the in-repo bindings (≈440 heuristic edges on the real
source). Receiver-type inference, inherited-member resolution, and the realistic
import-pattern handling (tsconfig aliases, barrels, src/ roots) of §6.1 are what
push that figure up: they convert method calls like self.attr.method() and
inherited self.method() from low-confidence name matches into correctly-bound
heuristic edges; on the real source they roughly doubled the heuristic edge
count versus name-binding alone. The system never fabricates a dst_id to inflate
this rate: that is the entire point of the confidence labels.
Precision of resolved edges. Rather than compute a global precision/recall
against a fully-labelled ground truth, CA Code Graph validates the correctness of
resolved edges through its golden fixtures: tests assert that specific edges
resolve to the specific correct target with the specific correct tier (e.g.
make_user → User is heuristic, display → format_name is syntactic,
web → fastapi is syntactic-unresolved). Tier monotonicity is also asserted:
precise edges are 100% resolved by construction, and heuristic edges resolve
to a target at a strictly higher rate than syntactic ones. Computing per-tier
precision/recall against a hand-labelled corpus is future work (§15).
12. Correctness & Quality Assurance
CA Code Graph ships a 125-test regression suite spanning unit, integration, golden, and conformance levels:
| Area | Coverage |
|---|---|
| Ids | Descriptor suffixes, overload disambiguation, round-trip, escaping. |
| Discovery | Extension/shebang detection, ignore dirs/exts, size caps, deterministic order. |
| Extraction | Python & TS/JS node kinds, signatures, docs, visibility, imports, heritage, constants. |
| Resolution | Tier labels, cross-file/import/self binding, receiver-type inference, inherited members, external references retained unresolved, --no-tier1 purity. |
| Cross-module | tsconfig path aliases (including monorepo multi-config plus extends), named/export * barrel & __init__ re-exports, src/ package roots, the import-aware unique-global fallback, and the name-matched result fallback. |
| Storage | Round-trip, schema version, edge dedupe, recursive traversal, cycle termination, deletion. |
| Routes | FastAPI/Flask/Django/Express detection + HANDLES edges. (The NestJS/Koa/Fastify extractors exist but are not yet covered by tests.) |
| Graph | PageRank determinism & ranking, repo-map budget, pack-context shape/budget, neighborhood. |
| API | Every §10 tool’s shape; position queries; references; callers/callees; hierarchy; imports. |
| Setup wizard | Idempotent MCP-config merge (no re-add), background-service start/restart/stop, JSONC/alias loader. |
| MCP conformance | Live stdio client lists tools and calls the five acceptance tools. |
| Determinism | Byte-identical exports and stable IDs across runs (Python & TS). |
| Robustness | Syntax-error files → partial index, no crash; empty/binary/oversize skipped. |
| Incremental | Hash-gated no-op, single-file edit, file add/delete, cross-file re-resolution. |
| Licensing | Build-time closure scan fails on copyleft (see §13). |
Golden fixtures are small, hand-verified Python and TypeScript repositories with known outlines, edges, and routes; resolution quality is asserted to be monotonic across tiers (precise edges fully resolved; heuristic resolves more often than syntactic).
13. Licensing & Supply-Chain Posture
CA Code Graph is Apache-2.0. Its 30-package runtime dependency closure is
verified at build time (from installed metadata, not from memory) and the
verification is enforced as a regression test that fails the build on any
GPL/LGPL/AGPL dependency. All runtime dependencies are permissive
(MIT/BSD-3-Clause/Apache-2.0/PSF-2.0) except one weak-copyleft transitive
package, certifi (MPL-2.0), pulled by the MCP SDK’s HTTP client. Because
certifi is unmodified, not incorporated into CA Code Graph’s source, and not
exercised on the query hot path (stdio default; the HTTP server uses BSD
starlette/uvicorn, not the httpx client), MPL-2.0’s file-level copyleft
imposes no obligation on CA Code Graph. This exception is documented and allowlisted
so any new copyleft dependency fails the build. Optional SCIP indexers run as
external, permissively-licensed subprocesses (Apache-2.0 for
scip-typescript/scip-python/scip-java/scip-go; MIT-OR-Apache-2.0 for
rust-analyzer), never linked. The full accounting
(versions, SPDX identifiers, grammar licenses, and collected license texts) is in
THIRD_PARTY_NOTICES.md and licenses/.
14. Comparison to Prior Art
| System | Strength | Trade-off relative to CA Code Graph |
|---|---|---|
| Sourcegraph / SCIP | Compiler-precise cross-refs, stable symbol identity | Heavy indexers/hosting; CA Code Graph is designed to consume SCIP as an optional precise tier (detection-only today, §6.1) and runs fully without it. |
| GitHub precise code-nav / stack-graphs | Scope-aware navigation at scale | Hosting-bound; CA Code Graph implements its own scope/import heuristics locally as Tier 1. |
| aider repo-map | PageRank over tree-sitter tags for context selection | Repo-map only; CA Code Graph generalizes to a full graph + 16 tools, with an original PageRank. |
| Language servers (LSP) | Rich, precise, interactive | Per-language servers, stateful, not agent-native; CA Code Graph emits LSP-shaped output offline over MCP. |
| Embedding/RAG retrieval | Captures fuzzy semantic similarity | Cannot answer relational/precise queries; CA Code Graph keeps vectors as an optional convenience, never the source of truth. |
CA Code Graph’s differentiators are the confidence-labelled tiered graph,
zero-daemon lightweight footprint, and agent-native MCP surface
including pack_context and get_repo_map.
15. Limitations & Future Work
- Heuristic ambiguity. Tier-0/1 binding can miss or, for ambiguous names,
decline to bind; the labels make this transparent. Tier 2 (SCIP) is designed
to close the gap where toolchains exist but is detection-only in this build
(§6.1). Future: wire SCIP subprocess invocation, protobuf ingestion, and edge
upgrade so
precisecross-references are actually produced. - Cross-file re-resolution on incremental. Changed-file references re-resolve against the global table, but edges into a changed file from unchanged files are not re-walked; structural IDs keep most targets stable, and a full re-index is always available. Future: reverse-dependency-driven re-resolution.
- Language breadth. First-class extraction covers Python, TypeScript, TSX, and
JavaScript; other registered grammars degrade to file nodes until
tags.scmand hooks are added. Future: Go, Java, Ruby, Rust extractors and route frameworks. - PageRank at very large scale. Pure-Python power iteration adds to cold-index time on very large graphs; it is computed once and excluded from the incremental hot path. Future: sparse-matrix acceleration behind an optional extra.
- Position fidelity. Columns are tree-sitter byte/codepoint offsets within a line (equal to LSP UTF-16 for ASCII/BMP); full UTF-16 remapping is future work.
16. Conclusion
CA Code Graph demonstrates that a lightweight engine can give coding agents
relational code context with explicit confidence labels. It combines tree-sitter
breadth and scope/import heuristics with a designed path to compiler-grade SCIP
precision (§6.1, §15), persists a deterministic graph in embedded SQLite, and
serves 16 agent-native MCP tools, including the token-budgeted pack_context and
centrality-ranked get_repo_map. The result supports one-command indexing, real
MCP-client interoperability, LSP/SCIP-aligned output, measured performance,
deterministic and robust behavior, and a verified permissive-license posture. The
125-test regression suite, including live MCP conformance and a license gate, is
green, and the benchmark harness is reproducible.
17. Appendices
Appendix A: Command-line interface
codegraph # interactive one-stop setup wizard
codegraph setup [--repo R --client C --watch --yes] # the same wizard, scriptable
codegraph setup --stop # stop background services for the repo
codegraph index <repo> # build/refresh the index
codegraph status <repo> # index info as JSON
codegraph query <tool> key=value ... # run any tool, print JSON
codegraph serve <repo> [--http] # MCP server (stdio default)
codegraph watch <repo> # incremental re-index on change
The one-stop setup wizard (wizard.py) indexes the chosen repo, then
writes/merges the client's MCP config (.mcp.json, .cursor/mcp.json, or via the
claude CLI) idempotently: an identical entry is never re-added, and any
existing file is backed up before modification. It then offers to start a
background watcher (and, optionally, a standalone HTTP MCP server) as detached
services managed via PID files under <repo>/.codegraph/, with kill-and-relaunch
semantics so re-running never spawns duplicates; --stop tears them down. Robust
by construction: child-liveness is verified with poll() (zombie-safe), a process
guard prevents killing a recycled PID, and writes are atomic. The full walkthrough,
flag reference, and troubleshooting guide is in docs/SETUP.md.
Appendix B: Confidence labels
| Label | Source |
|---|---|
precise | SCIP/compiler-grade, or structural fact (CONTAINS, in-file HANDLES). |
heuristic | Scope/import-map binding (Tier 1). |
syntactic | Name match within file / globally unique (Tier 0); also retains unresolved references. |
Appendix C: Reproducibility
python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
python -m pytest # 125-test regression suite
python benchmarks/bench.py --files 500 --write # regenerate benchmark results
python examples/mcp_session.py # live MCP client transcript
Appendix D: Glossary
- MCP: Model Context Protocol; the JSON-RPC tool protocol agents use.
- LSP: Language Server Protocol; source of the
SymbolKind/DocumentSymbolconventions adopted here. - SCIP: SCIP Code Intelligence Protocol; the language-agnostic symbol-identity format consumed for the precise tier.
tags.scm: tree-sitter query convention for definitions/references.- Tier: a resolution strategy with a fixed confidence: 0 syntactic, 1 heuristic, 2 precise.
References
All URLs accessed June 2026.
- Microsoft. Language Server Protocol Specification (3.17):
SymbolKind&DocumentSymbol. https://microsoft.github.io/language-server-protocol/ - Sourcegraph. SCIP: SCIP Code Intelligence Protocol. https://github.com/sourcegraph/scip
- Brunsfeld, M., et al. Tree-sitter: incremental parsing; code-navigation
tag queries (
tags.scm). https://tree-sitter.github.io/tree-sitter/ - Gauthier, P. aider: repository map (PageRank over tree-sitter tags). https://aider.chat/docs/repomap.html
- GitHub. Stack graphs /
tree-sitter-stack-graphs(precise code navigation). https://github.com/github/stack-graphs - Meta. Glean: a system for collecting, deriving and querying facts about source code. https://glean.software/
- Anthropic. Model Context Protocol: Specification. https://modelcontextprotocol.io/
- tree-sitter-language-pack: prebuilt permissively-licensed grammars. https://github.com/Goldziher/tree-sitter-language-pack
