Skip to content
HitBase
Back to research

White paper · Software Engineering

Jun 22, 2026 · 22 min read

View the code
Works with
Gemini CLICodexClaude Code
On this page

CA Code Graph: A Lightweight, Confidence-Labelled Code-Graph Engine for Agents over MCP

A technical white paper

ProjectCA Code Graph (ca-codegraph)
Version0.1.0
StatusBeta: reference implementation
LicenseApache-2.0
Document revision1.1
DateJune 22, 2026
AudienceEngineers 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

  1. Introduction & Motivation
  2. Design Goals, Constraints & Anti-Goals
  3. System Architecture
  4. Graph Schema & Symbol Identity
  5. The Extraction Engine
  6. Tiered Resolution: The Accuracy Core
  7. Framework-Aware Route Extraction
  8. Storage, Determinism & Incrementality
  9. Graph Analytics
  10. The MCP Server & Tool Surface
  11. Performance Evaluation
  12. Correctness & Quality Assurance
  13. Licensing & Supply-Chain Posture
  14. Comparison to Prior Art
  15. Limitations & Future Work
  16. Conclusion
  17. 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:

  1. 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.
  2. 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

ConstraintHow it is met
Original implementationConventions adopted from prior art; all code original.
Permissive licenses onlyRuntime closure verified at build time; CI gate fails on GPL/LGPL/AGPL.
Lightweight, no daemonsSingle package + embedded SQLite; pure-Python PageRank (no numpy/networkx).
Fast / accurate / reliableMeasured budgets (§11); tiered resolution (§6); graceful degradation (§12).
MCP-native, spec-compliantFastMCP; stdio default + Streamable HTTP; SSE intentionally omitted.
No LLM in indexingDeterministic static analysis only.
No network on query hot pathServing 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

ConcernModule(s)
IR, taxonomy, IDs, configmodel.py, ids.py, config.py
Discover / parsediscover.py, parse.py, langs/ (+ queries/*.scm)
Extractextract/{engine,base,python,typescript}.py
Resolveresolve/{resolver,symtab,scip}.py
Routesroutes/{base,python_frameworks,js_frameworks}.py
Graph analytics & searchgraph.py, search.py
Storagestore/sqlite.py
Orchestrationpipeline.py, watch.py
Serving surfaceapi.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

EdgeMeaning
CONTAINSHierarchy backbone (file → class → method).
CALLSFunction/method invocation (including new/construction).
IMPORTSModule/symbol import dependency.
INHERITSClass extends class.
IMPLEMENTSClass implements interface/trait.
REFERENCESGeneric symbol use (type annotation, identifier).
HANDLESRoute → 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 classSuffixExample
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 the display method of class User in models.py. Descriptor-breaking characters in a name (spaces, /, #, ()) are percent-escaped, so a route descriptor for path /users renders as GET:%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/cls receivers resolve to a member of the enclosing class, including inherited members, found by walking the resolved inheritance graph (a pre-pass binds every INHERITS/IMPLEMENTS base to its class id);
  • receiver-type inference: obj.method() resolves when obj’s class is knowable: self.attr (from self.attr = Class(...), an annotation, or a typed __init__/constructor parameter), a local variable (from x = 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__.py that re-imports a name, is transparently chased to the original definition (named and export * / 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 at src/app/x.py importable as app.x); and JS/TS relative specifiers and tsconfig.json path aliases (@/utils/x, custom paths, baseUrl) with extension/index resolution. Path aliases are discovered monorepo-wide: every tsconfig/jsconfig in the tree is loaded (following extends chains) 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 heuristic label. 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:

ReferenceEdgeLabelWhy
class User(Entity)INHERITS User→Entityheuristicsame-file type binding (Tier 1)
make_user builds User()CALLS make_user→UserheuristicUser imported from .models
display calls format_nameCALLS display→format_namesyntacticsame-file unique name (Tier 0)
web.py imports fastapiIMPORTS web→fastapisyntacticexternal module: unresolved, by name
services imports .modelsIMPORTS services→modelsheuristicin-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.

LanguageFrameworks
PythonFastAPI, Flask, APIRouter/Blueprint (decorator routing, including methods=[…] expansion), Django urls.py (path/re_path/url)
JS / TSExpress / 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

ToolPurpose
get_index_infoCounts, languages, enabled tiers, confidence breakdown, staleness.
get_file_outlineNested DocumentSymbol hierarchy for a file.
find_symbolLocate definitions by name/kind (exact or substring).
get_symbolFull detail: fqn, kind, signature, doc, location, container, members.
get_definitionPosition → definition (0-based, LSP-style).
get_referencesAll usages (by id or position), each with resolution.
get_callers / get_calleesCall-graph traversal, retaining unresolved calls by name.
get_type_hierarchySupertypes / subtypes / implementations.
get_imports / get_importersModule dependency edges, in and out.
list_routesHTTP routes → handler symbols, filterable by framework/method.
get_neighborhoodSubgraph around a symbol over chosen edge kinds.
search_symbolsFuzzy, ranked search over fqns (rapidfuzz).
pack_contextToken-budget-bounded context bundle.
get_repo_mapCentrality-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.

filesLOCnodesedges
50069,50024,00057,500

11.3 Throughput & memory

MetricValue
Full cold index6.86 s
Index throughput10.1 kLOC/s
Single-file incremental148.2 ms
Peak Python heap (tracemalloc)119.1 MB
Peak RSS291.3 MB

11.4 Warm query latency

Toolp50 (ms)p95 (ms)
get_file_outline0.8460.914
find_symbol1.9422.134
get_callers0.0300.050
pack_context8.6588.916
get_repo_map125.069131.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.

Corpusrel. edgesresolved (have dst_id)heuristic (Tier 1)syntactic (Tier 0)CALLS resolved
py_app (golden, Python)3211 (34%)10247 / 21 (33%)
ts_app (golden, TS)1810 (55%)996 / 13 (46%)
CA Code Graph src/ (real, Python)2,135870 (41%)4381,697785 / 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:

AreaCoverage
IdsDescriptor suffixes, overload disambiguation, round-trip, escaping.
DiscoveryExtension/shebang detection, ignore dirs/exts, size caps, deterministic order.
ExtractionPython & TS/JS node kinds, signatures, docs, visibility, imports, heritage, constants.
ResolutionTier labels, cross-file/import/self binding, receiver-type inference, inherited members, external references retained unresolved, --no-tier1 purity.
Cross-moduletsconfig 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.
StorageRound-trip, schema version, edge dedupe, recursive traversal, cycle termination, deletion.
RoutesFastAPI/Flask/Django/Express detection + HANDLES edges. (The NestJS/Koa/Fastify extractors exist but are not yet covered by tests.)
GraphPageRank determinism & ranking, repo-map budget, pack-context shape/budget, neighborhood.
APIEvery §10 tool’s shape; position queries; references; callers/callees; hierarchy; imports.
Setup wizardIdempotent MCP-config merge (no re-add), background-service start/restart/stop, JSONC/alias loader.
MCP conformanceLive stdio client lists tools and calls the five acceptance tools.
DeterminismByte-identical exports and stable IDs across runs (Python & TS).
RobustnessSyntax-error files → partial index, no crash; empty/binary/oversize skipped.
IncrementalHash-gated no-op, single-file edit, file add/delete, cross-file re-resolution.
LicensingBuild-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

SystemStrengthTrade-off relative to CA Code Graph
Sourcegraph / SCIPCompiler-precise cross-refs, stable symbol identityHeavy 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-graphsScope-aware navigation at scaleHosting-bound; CA Code Graph implements its own scope/import heuristics locally as Tier 1.
aider repo-mapPageRank over tree-sitter tags for context selectionRepo-map only; CA Code Graph generalizes to a full graph + 16 tools, with an original PageRank.
Language servers (LSP)Rich, precise, interactivePer-language servers, stateful, not agent-native; CA Code Graph emits LSP-shaped output offline over MCP.
Embedding/RAG retrievalCaptures fuzzy semantic similarityCannot 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 precise cross-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.scm and 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

LabelSource
preciseSCIP/compiler-grade, or structural fact (CONTAINS, in-file HANDLES).
heuristicScope/import-map binding (Tier 1).
syntacticName 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/DocumentSymbol conventions 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.

  1. Microsoft. Language Server Protocol Specification (3.17): SymbolKind & DocumentSymbol. https://microsoft.github.io/language-server-protocol/
  2. Sourcegraph. SCIP: SCIP Code Intelligence Protocol. https://github.com/sourcegraph/scip
  3. Brunsfeld, M., et al. Tree-sitter: incremental parsing; code-navigation tag queries (tags.scm). https://tree-sitter.github.io/tree-sitter/
  4. Gauthier, P. aider: repository map (PageRank over tree-sitter tags). https://aider.chat/docs/repomap.html
  5. GitHub. Stack graphs / tree-sitter-stack-graphs (precise code navigation). https://github.com/github/stack-graphs
  6. Meta. Glean: a system for collecting, deriving and querying facts about source code. https://glean.software/
  7. Anthropic. Model Context Protocol: Specification. https://modelcontextprotocol.io/
  8. tree-sitter-language-pack: prebuilt permissively-licensed grammars. https://github.com/Goldziher/tree-sitter-language-pack