TraceDecay is a code intelligence tool that builds semantic knowledge graphs from source code. It parses source files with tree-sitter, extracts symbols and relationships into a SQLite database, and exposes the graph through a CLI and an MCP (Model Context Protocol) server. The core insight is that AI coding agents waste tokens reading raw files when a pre-built graph can answer most questions instantly.
The system is structured as a pipeline: source files flow through extraction, resolution, and storage, then get queried via the CLI or MCP server.
graph LR
subgraph Indexing
A[Source Files] --> B[Tree-sitter Parsing]
B --> C[Language Extractors]
C --> D[Nodes + Edges + Unresolved Refs]
D --> E[Reference Resolver]
E --> F[SQLite Database]
end
subgraph Querying
F --> G[Graph Traversal]
F --> H[Graph Queries]
F --> I[Context Builder]
G --> J[MCP Server]
H --> J
I --> J
G --> K[CLI]
H --> K
I --> K
end
J --> L[Claude / Gemini / Codex / OpenCode]
K --> M[Terminal]
The binary (src/main.rs) serves as both the CLI frontend and MCP server entry point.
The library (src/lib.rs) exposes all internals so the CLI and server share the same code
paths without duplication.
src/
main.rs CLI entry point, subcommand dispatch
lib.rs Crate root, module declarations, lint config
tracedecay.rs TraceDecay facade -- the main public API
branch.rs Git branch resolution (current branch, default detection, merge-base)
branch_meta.rs Branch metadata persistence (branch-meta.json)
config.rs Per-project config (exclude patterns, limits)
errors.rs Error types (thiserror)
sync.rs Content hashing, stale/new/removed file detection
user_config.rs User-level config (~/.tracedecay/config.toml)
cloud.rs Cloudflare Worker counter, GitHub release checks
global_db.rs Cross-project token tracking (~/.tracedecay/global.db)
extraction/ Tree-sitter based extractors (one per language)
mod.rs Extractor registry, feature-gated language modules
complexity.rs Cyclomatic complexity counting (language-configurable)
rust_extractor.rs ... through qbasic_extractor.rs
tree_sitter/ Vendored tree-sitter grammars
cobol.rs COBOL (no working crate on crates.io)
protobuf.rs Protobuf (version conflict with tree-sitter 0.26)
db/ SQLite persistence
connection.rs Database struct, WAL setup, checkpointing
migrations.rs Sequential schema migrations via PRAGMA user_version
queries.rs All SQL queries as async methods on Database
resolution/ Cross-file symbol resolution
resolver.rs Matches unresolved references to known nodes
graph/ Higher-level graph algorithms
traversal.rs BFS/DFS, callers/callees, impact radius, path finding
queries.rs Dead code detection, circular deps, file dependencies
context/ AI-ready context assembly
builder.rs Builds TaskContext from a natural language query
formatter.rs Markdown and JSON output formatters
vectors/ Embedding storage and brute-force similarity search
search.rs Store/query/delete vectors, cosine similarity
mcp/ Model Context Protocol server
server.rs McpServer: stdio JSON-RPC loop, lifecycle
tools.rs 37 tool definitions and dispatch
transport.rs JSON-RPC request/response/error types
agents/ Agent integration (install/uninstall/doctor)
mod.rs Agent trait, registry, shared helpers, git hooks
claude.rs Claude Code: MCP in ~/.claude.json, hooks, permissions
codex.rs Codex CLI: MCP in ~/.codex/config.toml, AGENTS.md
opencode.rs OpenCode: MCP in ~/.config/opencode/opencode.json, ~/.config/opencode/AGENTS.md
gemini.rs Gemini CLI: MCP in ~/.gemini/settings.json, GEMINI.md
The graph has three primary entities stored in SQLite tables.
Nodes represent code symbols. Each node has:
- A deterministic ID (content-addressed hash of file path + name + kind)
- A
NodeKindenum with 50+ variants spanning all supported languages (Function, Method, Struct, Class, Enum, Trait, Interface, Field, etc.) - Source location (file, start line, end line)
- Metadata: signature, visibility, docstring, body hash, line count
- Complexity metrics: branches, loops, returns, max nesting, unsafe blocks
Edges represent relationships between nodes:
| EdgeKind | Meaning |
|---|---|
| Contains | Parent contains child (file->fn) |
| Calls | Function/method calls another |
| Uses | Symbol references another symbol |
| Implements | Type implements trait/interface |
| TypeOf | Field/param has a type reference |
| Returns | Function returns a type |
| Inherits | Class extends another |
| Overrides | Method overrides a parent method |
| Imports | File imports from another |
| AnnotatedBy | Symbol annotated by annotation |
Files track indexing state per source file:
- Path, content hash (SHA-256), language, size, last-indexed timestamp
- Content hashing enables incremental sync: only re-extract changed files
TraceDecay::index_all walks the project tree, filters by extension (language support)
and config exclude globs. If git_ignore is enabled, it additionally filters through
.gitignore rules via the ignore crate.
Each source file is dispatched to a language-specific extractor based on file extension.
Extractors use tree-sitter to parse the file into a concrete syntax tree, then walk it
to produce an ExtractionResult containing:
- Nodes: every symbol found in the file
- Edges: intra-file relationships (contains, calls, uses, implements, etc.)
- Unresolved references: call sites and type references that name symbols potentially defined in other files
The extractor architecture is stateless: XxxExtractor::extract_source(path, source)
takes a file path and source string, returns nodes and edges. An internal ExtractionState
accumulates results during the tree walk, tracks scope nesting for containment edges,
and collects unresolved references.
Complexity metrics are computed during extraction using a language-configurable walker
(ComplexityConfig) that counts branches, loops, nesting depth, unsafe blocks, and
unchecked calls in each function body.
After all files are extracted, the ReferenceResolver runs a second pass. It loads all
nodes into memory, builds a name-to-node index, and attempts to match each unresolved
reference to a known node. Matched references become typed edges (Calls, Uses, TypeOf,
etc.) that create cross-file connections in the graph.
Nodes, edges, and file records are bulk-inserted into SQLite via Database::insert_nodes
and Database::insert_edges, which use batched prepared statements for performance.
The database runs in WAL mode with busy_timeout = 5000ms to handle concurrent access
from the MCP server and git post-commit hooks.
TraceDecay::sync compares the current file system state against the stored file records
using SHA-256 content hashes. It identifies three sets:
- New files: on disk but not in DB, need full extraction
- Modified files: hash mismatch, re-extract and replace
- Removed files: in DB but not on disk, delete nodes and edges
Only changed files are re-extracted. After re-extraction, reference resolution runs again on the full graph to pick up any new cross-file edges.
The database uses libsql (a SQLite fork by Turso) for async support. The schema
is managed by sequential migrations tracked via PRAGMA user_version.
Key schema features:
- FTS5 full-text search index on node names for fuzzy symbol search
- Covering indexes on edges
(source_id, kind)and(target_id, kind)for fast traversal in both directions - Content-addressed node IDs enable deduplication and stable references
- WAL mode with
NORMALsynchronous for concurrent read/write safety
The queries.rs module (~1600 lines) implements all data access as async methods
on Database. Complex analytical queries (god classes, inheritance depth, coupling)
use CTEs and window functions to avoid pulling large datasets into Rust.
graph/traversal.rs provides BFS and DFS traversal with configurable edge kinds,
max depth, and direction. Built on top of it:
- Callers/callees: follow Calls edges upstream/downstream
- Impact radius: BFS from a node following all edge kinds to find the blast radius
- Call graph: bidirectional expansion from a function
- Type hierarchy: follows Implements and Inherits edges
- Path finding: BFS shortest path between two nodes
graph/queries.rs provides higher-level analyses:
- Dead code detection: nodes with zero incoming Calls/Uses edges (excluding files)
- Circular dependencies: Tarjan's SCC algorithm on file-level import edges
- File dependencies/dependents: which files a file depends on or is depended upon by
The context builder (context/builder.rs) is the key integration point for AI agents.
Given a natural language task description, it:
- Extracts symbol names from the query using heuristics (camelCase splitting, etc.)
- Searches for each extracted symbol via FTS5 full-text search
- Searches for each agent-provided keyword (the
extra_keywordsfield) - Expands relevant nodes by following edges to include callers, callees, and types
- Reads source code snippets for the top-ranked nodes
- Assembles a
TaskContextwith ranked symbols, their code, and relationship summaries
The output can be formatted as markdown (for humans / LLM prompts) or JSON (for programmatic consumption).
The primary search mechanism is FTS5 with BM25 scoring, which matches against
node names, qualified names, signatures, and docstrings. This works well when
query terms appear literally in the code, but fails when concepts don't match
symbol names (e.g. "authentication" won't find login() unless a docstring
mentions it).
Rather than embedding models, tracedecay uses agent-driven keyword expansion.
The tracedecay_context MCP tool accepts a keywords array where the calling
agent provides synonyms:
{
"task": "how does authentication work",
"keywords": ["login", "session", "credential", "token", "jwt"]
}Each keyword runs as an independent FTS5 query, and results are merged with the main query's results (deduplicated by node ID).
Why keywords instead of embeddings:
| Agent keywords | Local embeddings | |
|---|---|---|
| Indexing cost | Zero | ~30s per 1,000 nodes (ONNX inference) |
| Model dependency | None | ~50MB model download |
| Query latency | ~1ms per keyword (FTS5 index hit) | ~200ms (brute-force cosine) |
| Binary size impact | None | +15-20 MB (ONNX runtime) |
| Conceptual match quality | Depends on agent's domain knowledge | Better for truly alien naming |
| Works without an LLM | No (needs an agent to provide keywords) | Yes (standalone) |
The trade-off: if the codebase uses naming conventions the agent can't predict
(e.g. guardianGateway for authentication), keywords miss while embeddings
would catch it via distributional semantics. In practice, the agent is an LLM
that understands programming conventions well enough to supply good synonyms
for the vast majority of cases.
The MCP server (mcp/server.rs) runs over stdio using JSON-RPC 2.0. It implements
the Model Context Protocol lifecycle:
- initialize: returns server capabilities and tool list
- tools/list: returns the 36 available tools with JSON Schema input definitions
- tools/call: dispatches to
handle_tool_callwhich routes by tool name
The server is stateless between calls (each call queries the database independently).
It tracks basic statistics (call counts, tokens saved per tool) for the tracedecay_status
tool.
The 37 MCP tools fall into several categories:
| Category | Tools |
|---|---|
| Search | search, context, node, files, diff_context |
| Navigation | callers, callees, impact, affected |
| Analysis | complexity, dead_code, god_class, circular, coupling |
| Metrics | rank, hotspots, largest, distribution, inheritance_depth |
| Quality | doc_coverage, unused_imports, recursion |
| Refactoring | rename_preview, similar, module_api |
| Git/CI | changelog, commit_context, pr_context |
| Quality Scan | simplify_scan, test_map, type_hierarchy |
| Porting | port_status, port_order |
| Branching | branch_search, branch_diff |
| Status | status |
Each tool is defined in mcp/tools.rs with a JSON Schema for its parameters.
handle_tool_call deserializes the arguments, calls the appropriate TraceDecay
method, and formats the result.
The agents/ module implements the Agent trait, providing install, uninstall,
and healthcheck operations for each supported coding agent. The registry in mod.rs
maps string IDs to agent implementations.
Each agent's install routine:
- Registers the MCP server in the agent's config file
- Sets up tool permissions / auto-approval where supported
- Installs a PreToolUse hook (Claude Code only) to block redundant file reads
- Appends prompt rules to the agent's instructions file
graph TB
subgraph "tracedecay install --agent X"
I[Install] --> MCP[Register MCP Server]
I --> PERM[Set Tool Permissions]
I --> HOOK[Install Hook]
I --> PROMPT[Append Prompt Rules]
end
subgraph "Agent Config Locations"
MCP --> C1["Claude: ~/.claude.json"]
MCP --> C2["Codex: ~/.codex/config.toml"]
MCP --> C3["OpenCode: ~/.config/opencode/opencode.json"]
MCP --> C4["Gemini: ~/.gemini/settings.json"]
PROMPT --> P1["Claude: ~/.claude/CLAUDE.md"]
PROMPT --> P2["Codex: ~/.codex/AGENTS.md"]
PROMPT --> P3["OpenCode: ~/.config/opencode/AGENTS.md"]
PROMPT --> P4["Gemini: ~/.gemini/GEMINI.md"]
end
The doctor command runs healthchecks across all (or a specific) agent,
verifying that the MCP server is registered, permissions are correct, hooks
are installed, and prompt rules are present.
Languages are organized into feature-gated tiers to control binary size and compile time:
| Tier | Languages | Feature Flag |
|---|---|---|
| Lite | Rust, Go, Java, Scala, TypeScript/JS, Python, C, C++, Kotlin, C#, Swift | always on |
| Medium | Dart, Pascal, PHP, Ruby, Bash, Protobuf, PowerShell, Nix, VB.NET | medium |
| Full | Lua, Zig, Obj-C, Perl, Batch, Fortran, COBOL, MSBasic2, GWBasic, QBasic | full (default) |
Two grammars (COBOL and Protobuf) are vendored as C source compiled via build.rs
because their crate counterparts are either missing or depend on incompatible
tree-sitter versions. The FFI shims live in src/tree_sitter/.
TraceDecay tracks how many tokens it saves compared to raw file reads. Each MCP tool call estimates the tokens that would have been consumed by reading the relevant files, subtracts the size of the tool's response, and accumulates the difference in the per-project database.
A global database at ~/.tracedecay/global.db aggregates totals across all projects
(an existing legacy ~/.tracedecay/ directory is still honored as a fallback).
An opt-in worldwide counter (Cloudflare Worker) lets users contribute their totals
anonymously. Upload is best-effort with 2-second timeouts and never blocks the CLI.
The system uses tokio for async I/O but most work is CPU-bound (tree-sitter parsing)
or SQLite-bound. Key concurrency points:
- The MCP server processes one JSON-RPC request at a time (single stdio stream)
- The git post-commit hook runs
tracedecay syncin the background (&).syncrequires an existing database -- it will not create one. This prevents the hook from silently bootstrapping indexes in repos that were never initialized withtracedecay init. - SQLite WAL mode + busy timeout handles concurrent access gracefully
- Version checks and counter uploads run on background threads during
sync
- Cargo:
cargo install tracedecay - Homebrew: custom tap with prebuilt bottles (macOS ARM64, Linux x86_64)
- Scoop: custom bucket with prebuilt Windows x86_64 zip
- GitHub Releases: prebuilt archives for all platforms
The release workflow (release.yml) builds binaries for 4 targets, packages them
as archives and Homebrew bottles, then updates the Homebrew tap and Scoop bucket
repos automatically.