* feat(go): generated-file down-rank + gRPC stub-impl bridge + trace-failure inlining
Multi-pronged fix to make codegraph competitive on Go multi-module repos
(cosmos-sdk, etcd) where it previously lost or tied. Driven by an 8-question
agent-eval audit across cobra, gin, prometheus, cosmos-sdk, and etcd: the
baseline had codegraph losing ~60% on cost on cosmos-sdk and mixed on etcd
deep cross-module flows, while winning cleanly on the single-module and
non-protobuf-heavy repos.
Diagnostics ruled OUT `go.work` parsing as the gap (prometheus crushes
without it). The actual failure modes were generated-file noise warping
disambiguation, missing gRPC interface→impl bridge in structural-typing Go,
and trace's failure path triggering 3-5 follow-up tool calls instead of
inlining the material the agent needed.
Changes:
- New `src/extraction/generated-detection.ts` — path-pattern classifier
for `.pb.go`, `.pulsar.go`, `_grpc.pb.go`, `_mock.go`, `_mocks.go`,
`mock_*.go`, `.generated.[jt]sx?`, `_pb2(_grpc)?.py`, `.pb.{cc,h}`,
`.g.dart`, `.freezed.dart`. Applied as a stable sort tiebreaker in
`findSymbol`, `findAllSymbols`, `codegraph_search` (MCP + CLI),
`codegraph_explore` file ranking, and context formatter Entry Points /
Related Symbols / Code blocks. Cosmos's `msgServer.Send` now ranks #3
instead of #9 on a `Send` search.
- New `goGrpcStubImplEdges` synthesizer in `callback-synthesizer.ts` —
detects `UnimplementedXxxServer` structs in generated files, identifies
their RPC methods (excluding `mustEmbed*` / `testEmbeddedByValue` gRPC
markers), and emits `calls` edges to the matching methods on any
non-generated struct whose method-name set is a superset. Closes Go's
structural-typing gap that the existing `interfaceOverrideEdges` (Java /
Kotlin only) couldn't bridge. 467 bridge edges on cosmos-sdk; bank's
`UnimplementedMsgServer::Send` points to `x/bank/keeper/msg_server.go`
only, not to `msgClient` siblings or mock files.
- Trace-failure rewrite (`handleTrace`) — when no static path connects
endpoints, instead of telling the agent to call `codegraph_node` (a
3-4-call fan-out), inline both endpoints' bodies (120 lines / 3600 chars
per endpoint), their callers (≤6), and callees (≤8) in one response.
- Trace endpoint-pairing improvements — scores every `from`×`to`
candidate combo by shared directory prefix and tries the best-paired
pair first (the full candidate set, not just FTS top-5). A
less-canonical-path penalty (`enterprise/`, `contrib/`, `examples/`,
`vendor/`, `third_party/`, `deprecated/`, `legacy/`) ensures the
canonical-module pair wins even when a side-experiment shares more of
its directory prefix. Find-path probe budget capped at 20 pairs.
- Test-file deprioritization in `codegraph_explore` `isLowValue` — adds
suffix patterns (`_test.go`, `_spec.rb`, `.test.ts`, `.spec.tsx`,
`Test.java`, `Spec.kt`) alongside the existing directory-style patterns.
Otherwise etcd's `watchable_store_test.go` consumes 5K chars of explore
budget that should go to the hand-written flow source.
Tests:
- New `__tests__/generated-detection.test.ts` (4 unit tests) pins the
suffix patterns.
- New "Go gRPC stub→impl synthesis" integration test suite in
`frameworks-integration.test.ts` (2 tests): positive bridge from stub
to hand-written impl, AND the precision case (don't bridge to a
generated sibling like `msgClient` in the same .pb.go).
- Full suite: 1076/1076 pass.
Empirical (post-fix, n=2 average per question):
| Repo / Q | WITH | WITHOUT | Reads (W/WO) | Time (W/WO)
|-------------------------|------------|-------------|--------------|------------
| cobra (parse cmds) | $0.27 | $0.27 | 0 / 4 | 39s / 60s
| prometheus (scrape→TSDB)| $0.63 | $0.70 | 0 / 6 | 106s/143s
| cosmos-sdk Q1 (MsgSend) | $0.41 | $0.26 | 1 / 2 | 67s / 64s
| cosmos-sdk Q2 (Delegate)| $0.47 | $0.46 | 0 / 5 | 50s / 73s
| cosmos-sdk Q3 (gov tally)| $0.34 | $0.31 | 1.5 / 3 | 54s / 76s
| etcd Q1 (Put→raft) | $0.65 | $0.78 | 0 / 4 | 98s / 129s
| etcd Q2 (watch) | $0.36 | $0.50 | 0 / 4+ | 58s / 89s
Codegraph wins on reads + time on every question. Cost is mixed: 3 clean
wins, 3 tied (within 10%), 1 stubborn cost loss on the grep-favored Q1.
Compared to baseline, the cosmos-sdk cost-gap collapsed from -60% to -15%
on average, and Q3 went from a 75% loss to a tie. Raw run artifacts in
`/tmp/cg-finalv2-*/` and `/tmp/cg-final-*/`.
Memory written at `project_go_multi_module_audit.md` for the methodology
+ before/after numbers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): auto-inline trace in codegraph_context for flow queries
When a codegraph_context task contains a flow keyword ("trace", "from",
"reach", "flow", "propagat", "how does", "how do") AND at least two
distinct PascalCase / camelCase identifiers, internally invoke trace
between the first two extracted symbols and splice the trace body into
the context response. Conservative trigger by design: false positives
waste one graph query; false negatives just fall back to the agent
calling trace itself (existing path-proximity wiring handles either
case).
Goal: collapse the agent's typical context → trace → explore sequence
into a single context call for clear flow queries, closing the
remaining cost-overhead gap on multi-call patterns. The path-proximity
+ less-canonical-path scoring + the trace-failure-inlined-bodies
behavior already let the inline trace land on the right endpoint pair
and return enough material that no follow-up codegraph_node/Read is
needed.
Doesn't fire on:
- cobra's "How does cobra parse commands and flags?" (no PascalCase
symbols) — verified in regression run, no behavior change ($0.260
WITH vs $0.257 WITHOUT, basically tied)
- queries where the agent doesn't call codegraph_context at all
(cosmos Q1 in the audit went search → trace → node → trace → node)
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): trace failure inlines TO file siblings to displace node fan-out
The cosmos-Q1 audit revealed a static-resolution gap: msgServer.Send's
*real* next hop is `k.Keeper.SendCoins` — an interface-method call on an
embedded field that tree-sitter can't resolve. The static getCallees list
for msgServer.Send is all utility/error functions (StringToBytes, Wrapf,
…). The actual flow (SendCoins → subUnlockedCoins → addCoins →
setBalance) lives entirely inside `x/bank/keeper/send.go`, which is also
where the TO endpoint (setBalance) lives.
When trace fails (no static path), inline the **top 5 functions/methods
in the destination file**, ordered by line-distance from the TO node.
This catches the flow that interface-method calls obscure — the
canonical "k.<Iface>.<Method>" pattern in Go, also relevant to Java
dependency-injection / Rails service-object dispatch / etc. where
interface dispatch hides the real call.
Conservative: only fires on trace FAILURE (no static path); the success
path is unchanged. Per-body cap (40 lines / 1200 chars), top 5 siblings.
Bookkeeps with `inlinedBodies` Set so endpoints already shown above
aren't duplicated.
Result: cosmos-Q1 — historically the most stubborn cost loss (-2.2× to
-39% across the audit) — flipped to a clean WIN: $0.257 WITH vs $0.449
WITHOUT (-43%), 34s vs 79s, 0 Reads vs 2 Reads + 5 Greps, 5 codegraph
calls vs 12. Regression-checked: prometheus, cobra, cosmos-Q2, etcd-Q1
all still WIN; Q3 is high-variance ($0.30-$0.45 range historically) and
fell within that on this run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat: extend coverage to all supported languages, not just Go
PR review feedback: the audit was Go-driven, so the patterns I added
were Go-flavored. Extend each axis to every language CodeGraph
supports per the README, so the same improvements help Java / C# /
Python / TS / Swift / Dart projects too.
**generated-detection.ts** — Added patterns for:
- TS/JS: `.gen.[jt]sx?`, `.pb.[jt]s`, `_pb.[jt]s`, `_grpc_pb.[jt]s`
(ts-proto, gRPC-web, Apollo / GraphQL codegen, Hasura).
- Python: `_pb2.pyi` (mypy stubs from protobuf).
- C#: `.g.cs` (T4 / Razor codegen), `Grpc.cs` (protoc-gen-csharp).
- Java: `OuterClass.java` (protoc-gen-java), `Grpc.java`
(protoc-gen-grpc-java; this is where the `*ImplBase` abstract
class lives — same shape as the Go `Unimplemented*Server` stub).
- Swift: `.pb.swift` (protoc-gen-swift).
- Dart: `.pb.dart`, `.pbgrpc.dart`, `.chopper.dart`.
- Rust: `.generated.rs`.
**test-file deprioritization** (`isLowValue` in `codegraph_explore`)
— Added per-language conventions that the previous regex missed:
- Python: `test_*.py` (pytest discovery) and `*_test.py`.
- Ruby: `*_test.rb` (minitest) — `*_spec.rb` already covered.
- C#: `*Tests.cs`, `*Test.cs`, `*Spec.cs`.
- Swift: `*Tests.swift` (XCTest).
- Dart: `*_test.dart`.
**IFACE_OVERRIDE_LANGS** in `callback-synthesizer.ts`'s
`interfaceOverrideEdges` — extended from `java, kotlin` to
`java, kotlin, csharp, typescript, javascript, swift, scala`. Same
shape across these (nominal `implements`/`extends` on a class to an
interface/abstract base). Also iterates `struct` (Swift value types
conforming to a protocol) in addition to `class`. The existing
matchesSymbol-style logic and `getOutgoingEdges(..., ['implements',
'extends'])` work unchanged.
**CLAUDE.md** — Added a House rule: when the user references issues
or comments, anchor them to a date and version (last release vs.
last main commit vs. current branch tip) BEFORE concluding a fix is
incomplete. Issue #388 comments from May 25-27 were responding to
the released v0.9.5 / merged-PR-469 state — not to this branch's
in-flight work. The new rule walks through the disambiguation:
`grep -m1 '^## \[' CHANGELOG.md` for release version, `git log
--first-parent main -1` for main tip.
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): tiny-repo tool gating + shorter tool descriptions
Two cumulative changes targeting the small-repo cost gap surfaced by
the cross-language audit:
1. **Tool descriptions trimmed** (~2.1KB total saved across 10 tools).
The verbose marketing prose on codegraph_context / codegraph_node /
codegraph_explore / codegraph_trace / etc. wasn't moving the agent
toward better tool choices on top of the actual usage, but it was
adding ~525 tokens of cache-creation overhead to every question.
The trimmed descriptions keep the operational hints (e.g. "Query is
a bag of symbol/file names, not a question" for explore) but drop
the redundant prose.
2. **Dynamic tiny-repo tool gating** in `ToolHandler.getTools()`. On a
project with < 150 indexed files, the MCP server only exposes the
5 core tools (search, context, node, explore, trace) instead of all
10 — the omitted callers/callees/impact/status/files tools' use
cases on a sub-150-file repo reduce to one grep anyway. The MCP
tool-defs overhead is the #1 source of cost loss on tiny repos
(~$0.10-0.15 fixed cache-creation per question); cutting 5 tools
drops that by ~50%.
Effect on ky (~25 files, the worst pre-fix offender):
- Before: $0.59 WITH vs $0.42 WITHOUT (+42% loss, n=1)
- After: $0.32 WITH vs $0.44 WITHOUT (-26%, **flipped to WIN**)
Effect on cobra/sinatra/slim (50-80 files): still cost-loss, but
the gating doesn't regress them — same call-count, same reads.
The structural lower bound on those repos is what the agent's
grep+read path costs in absolute terms (~$0.20-0.30).
Non-breaking for medium+/large repos: all 10 tools remain exposed
when fileCount >= 150.
Tests: 1076/1076 still pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): combined tiny-tier — smaller explore + tool gating (cobra/ky flip to WIN)
Combines the tool gating from the previous commit with a matching
explore-budget cut for projects under 150 files. The two together close
the cost gap that neither closes alone:
- Tool gating alone helped ky (WIN) but didn't move cobra/slim/sinatra
- Explore-budget cut alone helped slim slightly but regressed cobra
- COMBINED: cobra flips to WIN, ky stays a WIN, ky/cobra both clean
`getExploreOutputBudget(fileCount < 150)` returns:
maxOutputChars: 13000 (was 18000)
defaultMaxFiles: 4 (was 5)
gapThreshold: 7 (was 8)
maxSymbolsInFileHeader: 5 (was 6)
maxEdgesPerRelationshipKind: 4 (was 6)
includeRelationships: true (kept ON — cheap structural signal)
maxCharsPerFile: 3800 (unchanged — monotonic invariant w/ next tier)
This survives the cobra-regression-with-trim that the earlier
budget-only attempt suffered: with only 5 tools to choose from, the
agent doesn't fall back to extra codegraph_node calls when explore
returns less — there's no node call available.
Results on the four worst small-repo losses (combined intervention):
| Repo | Files | WITH (combo)| WITHOUT | Verdict (pre → post) |
|--------|-------|-------------|-------------|--------------------------|
| cobra | ~50 | $0.25 | $0.31 | loss → **WIN** (-19%) |
| ky | ~25 | $0.39 | $0.39 | -42% → tied |
| slim | ~80 | $0.31 | $0.24 | LOSS 31% → still LOSS |
| sinatra| ~60 | $0.30 | $0.23 | LOSS 18% → still LOSS |
sinatra/slim remain a cost-loss because their WITHOUT path is
structurally cheap (~$0.20 — fewer than 4 cheap grep+read calls).
Codegraph can't beat that absolute floor with any meaningful response.
Both still WIN on time + reads + tool-call count.
Tests: tier boundary cases updated to cover the new <150 / 150-499 /
500-4999 / 5000-14999 / >=15000 progression. Off-by-one guard updated
to include the new 149↔150 boundary. All 1076 tests pass.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(context): trim maxNodes default to 8 on tiny repos
On a <150-file project the entire repo is grep-able in one turn, so the
20-node default `codegraph_context` was paying for a graph subset that
exceeds the agent's actual question. Cutting the tiny-repo default to 8
(typical 1-3 entry points + their immediate 1-hop neighbors) reduces
the context-tool response body without hitting sufficiency on the flow
shapes small repos actually contain.
Non-breaking: the agent can still pass an explicit `maxNodes` to
override; medium+ repos (>=150 files) keep the 20-node default.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(mcp): pin the empirical 5-tool gating floor for tiny repos
n=2 audit on cobra/ky/sinatra ruled out cutting below 5 tools (search +
context + node + explore + trace) on the tiny-repo tier. The smaller
3-tool gate (search + context + trace) saved ~$0.025 of prompt overhead
but the agent fell back to extra Reads to cover what codegraph_node and
codegraph_explore would have answered — net cost regression on all three
test repos (cobra 17% → 48% loss, sinatra 18% → 96% loss). Documented
inline so future tuners don't re-try this dead-end.
No behavior change beyond the comment: the 5-tool gate remains the
production setting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(mcp): pin empirical lower bound on tool gating after n=2 micro test
Tested the hypothesis that exposing FEWER tools on micro repos (<50
files) would close the cost gap. Results:
- 1-tool gate (codegraph_search only):
- ky: +44% (worse than 5-tool +30%)
- express: +107% (catastrophic — was -43% WIN with all 10)
- cobra: +126% (way worse than 5-tool +17%)
The single-tool gate forces the agent to read everything because it
can't navigate the call graph. The 5 omitted tools (context, node,
explore, trace) were doing real work that grep+Read can't replicate.
Conclusion: 5 tools (search + context + node + explore + trace) is the
empirical lower bound on the tiny-repo tier. Cutting below regresses
EVERY tested repo. The remaining ~$0.04-0.08 of structural cost overhead
on tiny repos is unavoidable without sacrificing the value codegraph
provides at that scale (which would also make WITH = WITHOUT, defeating
the install).
Comment documents the dead-ends so future tuners don't relitigate.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(mcp): iter3/iter4 — raise tool-gate to 500, sufficiency steering in context, hard-exclude low-value files
Three layered changes targeting the sinatra/slim/small-repo cost gap
that iter2's body-shrink failed to close (smaller bodies just pushed
the agent to Read instead):
1. **Tool-gate threshold 150 → 500** (`TINY_REPO_FILE_THRESHOLD`).
Sinatra (~159 files) and slim (~200 files) have the same structural
problem as cobra (
* feat(context): iter7 — core-directory boost to surface dominant-file siblings in search ranking
On projects with a single file holding the dense majority of internal
call edges (e.g. sinatra's `lib/sinatra/base.rb` at ~85% of in-file
edges), text search was favoring small focused extension files over the
core file. A small focused file like `multi_route.rb` wins on verbatim
name match + file-size normalization, burying the 1500-line core file's
longer method names (e.g. `route!` vs `route`).
Fix: detect the "dominant file" — the file whose in-file edge count is
≥3× the next candidate's — then add +25 to all results sharing its
directory prefix. This pulls the core file's siblings above
sibling-package extensions without hardcoding any repo structure.
`getDominantFile()` excludes test/spec files and generated files
(e.g. etcd's `rpc.pb.go` has 4× the in-file edges of `server.go` and
would otherwise hijack the boost toward generated protobuf stubs).
SQL pulls the top 20 candidates; path-pattern filtering handles what
SQLite LIKE can't express.
* feat(mcp): iter10+iter12 — routing manifest inline + probe-sweep harness
On small projects (<500 files) with a routing-shaped query, build a
URL→handler manifest directly from the graph (each `route` node joins to
its handler via `references`/`calls` edges) and inline the top handler
file's source. The agent gets the canonical routing answer in ONE
codegraph_context call — no need to parse framework DSL, Glob for
controllers, or chase down handler files.
The lever is "make the backend smarter so the agent doesn't have to":
- Parsing routes.rb / routes/api.php / urls.py DSL is the agent's job
in the WITHOUT arm. Codegraph already has it parsed as `route` nodes
with edges to handlers — we just project that to a manifest table.
- The handler implementations are right there in the index too; inline
the highest-handler-count file so the agent sees real code, not just
symbol names.
Results on the realworld template repos that were losing badly:
rails-rw +89% LOSS → -15% WIN (agent often answers with 0-1 tool calls)
laravel-rw +29% LOSS → +12% (tight gap)
gin-rw +30% LOSS → +23% (still loss but smaller)
flask-mb +64% LOSS → +25% (smaller gap)
The residual losses are mostly the agent's defensive read behavior on
super-cheap-WITHOUT repos (express-rw still does 4 Reads even with a
19-row manifest + service file inlined). That's an agent-side ceiling
the backend can't reach further without removing tools.
Also lands `scripts/agent-eval/probe-sweep.mjs` — a direct-MCP test
harness that runs context probes across 21 repos in ~600ms (vs ~30min
for a real claude audit). Enables rapid iteration on backend changes:
edit tools.ts / context-builder, npm run build, re-run probe-sweep,
compare signals (manifest fired? handler file inlined? response size?)
before paying for a claude run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(mcp): first tool call awaits catch-up sync (no stale rows for deleted files)
`MCPEngine.catchUpSync()` reconciles the index against the working tree
after open (catching `git pull`/`checkout`/`rebase` and any edits or
deletes made while no server was running). It was fire-and-forget — so a
tool call landing in the first ~50-300ms could race past it and serve
rows for files that no longer exist on disk. The per-file staleness
banner can't help here, because that signal is populated by the file
watcher (not by catch-up).
The fix: `catchUpSync()` now pushes its promise into `ToolHandler` via
`setCatchUpGate(p)`; the first `execute()` call awaits the gate and then
clears it. Subsequent calls pay nothing. Catch-up rejections are logged
by the engine and swallowed by the handler so a transient sync failure
never breaks tools.
Most visible on the "deleted everything between sessions" case, where
MCP previously returned stale rows pointing at non-existent files.
Validated end-to-end on a 10,640-file VS Code index: with the gate, a
codegraph_search for "ExtensionHost" against an empty (but stale-DB)
directory returns "No results found" after the catch-up drains the DB;
without the gate, the same call returns 10 stale hits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(changelog): cover small-repo retrieval tuning + auto-trace + iface-override expansion
Add entries for work that landed on this branch but wasn't yet in
[Unreleased]: tiny-repo tool gating + sufficiency steering + budget
tier, auto-inline trace in codegraph_context, routing manifest inline,
core-directory ranking boost, JVM-only interfaceOverrideEdges extended
to C#/TS/JS/Swift/Scala, and the shorter tool descriptions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
27 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
CodeGraph is a local-first code intelligence library + CLI + MCP server. It parses any supported codebase with tree-sitter, stores symbols/edges/files in SQLite (FTS5), and exposes a knowledge graph to AI agents (Claude Code, Cursor, Codex CLI, opencode) over MCP. Per-project data lives in .codegraph/. Extraction is deterministic — derived from AST, not LLM-summarized.
Distributed as @colbymchenry/codegraph on npm; same binary serves as installer, indexer, and MCP server.
Build, Test, Run
npm run build # tsc + copy schema.sql and *.wasm into dist/; chmods dist/bin/codegraph.js
npm run dev # tsc --watch
npm run clean # rm -rf dist
npm test # vitest run (all)
npm run test:watch
npm run test:eval # only __tests__/evaluation/
npm run eval # build then run __tests__/evaluation/runner.ts via tsx
npm run cli # build then run the local dist binary
# Single test file / pattern
npx vitest run __tests__/installer-targets.test.ts
npx vitest run __tests__/extraction.test.ts -t "TypeScript"
copy-assets (called from build) copies src/db/schema.sql and all src/extraction/wasm/*.wasm files into dist/. Any new SQL or grammar wasm must be copied or it won't ship.
Node engines: >=18.0.0 <25.0.0. There is a hard exit on Node 25.x (see src/bin/node-version-check.ts).
Architecture
Layered pipeline
files → ExtractionOrchestrator (tree-sitter) → DB (nodes/edges/files)
↓
ReferenceResolver (imports, name-matching, framework patterns)
↓
GraphQueryManager / GraphTraverser (callers, callees, impact)
↓
ContextBuilder (markdown/JSON for AI consumption)
The public API surface is src/index.ts — the CodeGraph class wires all the layers and re-exports types. Library users only touch this file; the MCP server and CLI also drive it.
Module layout
src/index.ts—CodeGraphclass:init/open/close,indexAll,sync,searchNodes,getCallers/getCallees,getImpactRadius,buildContext,watch/unwatch.src/db/—DatabaseConnection,QueryBuilder(prepared statements),schema.sql. Backed bybetter-sqlite3(native) when available, transparently falls back tonode-sqlite3-wasm.codegraph statussurfaces which backend is live; wasm is the slow path.src/extraction/—ExtractionOrchestrator, tree-sitter wrappers, per-language extractors underlanguages/(one file per language), plus standalone extractors for non-tree-sitter formats (svelte-extractor.ts,vue-extractor.ts,liquid-extractor.ts,dfm-extractor.tsfor Delphi).parse-worker.tsruns heavy parsing off the main thread.src/resolution/—ReferenceResolverorchestratesimport-resolver.ts(withpath-aliases.tsfor tsconfig path aliases + cargo workspace member globs),name-matcher.ts, andframeworks/(Express, Laravel, Rails, FastAPI, Django, Flask, Spring, Gin, Axum, ASP.NET, Vapor, React Router, SvelteKit, Vue/Nuxt, Cargo workspaces). Frameworks emitroutenodes andreferencesedges.src/graph/—GraphTraverser(BFS/DFS, impact radius, path finding) andGraphQueryManager(high-level queries).src/context/—ContextBuilder+ formatter for markdown/JSON output.src/search/— full-text query parser and helpers for FTS5.src/sync/—FileWatcher(native FSEvents/inotify/RDCW) with debounce + filter, and git-hook helpers.src/mcp/— MCP server (MCPServer,tools.ts,transport.ts).server-instructions.tsis what the server returns in the MCPinitializeresponse — keep it in sync with the user-facing tool guidance.src/installer/— see below.src/bin/codegraph.ts— CLI (commander). Subcommands:install,init,uninit,index,sync,status,query,files,context,affected,serve --mcp.src/ui/— terminal UI (shimmer progress, worker).
NodeKind / EdgeKind
Defined in src/types.ts. Both extractors and resolvers must use these exact strings.
- NodeKind:
file,module,class,struct,interface,trait,protocol,function,method,property,field,variable,constant,enum,enum_member,type_alias,namespace,parameter,import,export,route,component. - EdgeKind:
contains,calls,imports,exports,extends,implements,references,type_of,returns,instantiates,overrides,decorates.
Multi-agent installer
src/installer/ is the entry point for codegraph install (and the bare codegraph/npx @colbymchenry/codegraph invocation). Architecture:
targets/registry.tslists every supported agent.targets/types.tsdefines theAgentTargetinterface — adding a 5th agent (Continue, Zed, Windsurf…) is one new file intargets/+ one entry inregistry.ts. Each target owns its config-file location, MCP-server JSON/TOML/JSONC writing, and instructions-file path.- Current targets:
claude.ts,cursor.ts,codex.ts,opencode.ts. targets/toml.tsis a hand-rolled TOML serializer scoped to[mcp_servers.codegraph](used by Codex). Sibling tables and[[array_of_tables]]are preserved verbatim. No new dependency.- opencode reads
opencode.jsoncby default; the installer prefers existing.jsonc, falls back to.json, and creates.jsoncfor greenfield installs. Edits are surgical viajsonc-parserso user comments and formatting survive install/re-install/uninstall round-trips. instructions-template.tsis the agent-agnostic instructions file written to each target (e.g.CLAUDE.md,.cursor/rules/codegraph.mdc,~/.codex/AGENTS.md,~/.config/opencode/AGENTS.md). It explicitly says "trust codegraph results, don't re-verify with grep" — earlier versions prescribed Claude-specific "spawn an Explore agent" and confused other agents.claude-md-template.tsis the legacy Claude-only template, retained for compatibility paths.- All installer changes need matching coverage in
__tests__/installer-targets.test.ts— there are ~47 parameterized contract tests covering install idempotency, sibling preservation, uninstall reverses install, byte-equal re-runs returningunchanged, and partial-state recovery for Codex.
Cursor MCP working-directory quirk
Cursor launches MCP subprocesses with the wrong cwd and doesn't pass rootUri in initialize. The installer injects --path into Cursor's MCP args — absolute path for local installs, ${workspaceFolder} for global installs. If you touch Cursor wiring, preserve this.
MCP server instructions
src/mcp/server-instructions.ts is sent back to the agent in the MCP initialize response. This is the first thing every agent sees about how to use the tools — treat it as the authoritative tool guidance and keep it in sync with instructions-template.ts and .cursor/rules/codegraph.mdc.
Retrieval performance & dynamic-dispatch coverage (do not regress)
CodeGraph's core value is letting an agent answer structural/flow questions ("how does X reach Y", trace, impact, callers) with a few fast codegraph calls and zero Read/Grep. The optimization target is wall-clock latency + tool-call count — don't optimize for token cost. (Cost is lower, not "flat" as earlier framing claimed: a current-build with-vs-without A/B across the 7 README repos, median of 4, saved on average 35% cost · 57% tokens · 46% time · 71% tool calls — reproducing the published README. The mechanism is far fewer turns over a much smaller accumulated context — NOT cache-ability: the without-arm's huge token volume is mostly cheap cache-reads, which is why token-count savings (57%) look bigger than cost savings (35%). Measure tokens by summing per-turn assistant usage, not result.usage (last-turn only in current Claude Code). See docs/benchmarks/call-sequence-analysis.md.) The mechanism that drives everything here: an agent falls back to Read/Grep the instant a codegraph answer is insufficient. So every change is judged by one question — is codegraph's answer sufficient enough to stop the agent from reading?
Target behavior: a flow question resolves in 1 codegraph call on small repos, scaling to 3–5 on large, with Read/Grep = 0. When reviewing a PR or trying something new, do not regress this.
Adapt the tool to the agent — don't try to change the agent
The lever that decides whether a retrieval change lands. Test before building anything here: does this make a tool the agent already calls do more with the input it already gives? If it instead needs the agent to behave differently — pick a different tool, query differently, learn from examples — it hits the low-salience wall and won't land.
CodeGraph's only channels to influence the agent are low-salience: the MCP initialize instructions (server-instructions.ts) and the tool descriptions. Changing them does not reliably move the agent's tool choice or query style — validated: trace-first steering ported into the server-instructions + tool descriptions (3 wording variants) never reproduced what a CLI --append-system-prompt achieved, and regressed wall-clock vs baseline. New tools fare worse (rarely chosen — the agent under-picks even trace); "better examples" is the same steering. The agent's tool-choice does improve on its own as host models get better at tool use — but that is not ours to force.
What works is meeting the agent where it already is:
- Sufficiency —
codegraph_traceinlines each hop's body + the destination's own callees, so one trace call ends the flow investigation (no follow-up explore/node/Read). - explore-flow —
codegraph_explore's query is a precise bag of symbol names (incl. qualifiedClass.method) spanning the flow the agent is after; explore finds the call path among those named symbols (riding synthesized edges) and leads its output with it — delivering trace-quality flow through the call the agent reliably makes. (buildFlowFromNamedSymbols: segment/co-naming disambiguation; ≤1 unnamed bridge so it never wanders a god-function's fan-out.)
What fails is the inverse — folding a precise answer into a fuzzy-input tool. codegraph_context gets a description, not symbols, so it can't disambiguate a flow's endpoints and surfaces the wrong feature. Precise output needs precise input.
The remaining lever under this axis is coverage: every flow made to connect statically (a new dynamic-dispatch synthesizer) is then surfaced automatically by explore-flow/trace, no agent change needed. Reactive/reconciler runtimes (Halo's ReactiveExtensionClient, MediatR, Vue Proxy) are the frontier — flows there have no static edges, so nothing surfaces (correctly — silent beats wrong). Full investigation + A/B record: docs/benchmarks/call-sequence-analysis.md.
Explore budget — keep BOTH budgets monotonic with repo size
Two functions in src/mcp/tools.ts scale explore with indexed file count. This is the expected resolution (a regression here silently forces agents back to Read):
| Repo | files | explore calls | chars/call | per-file |
|---|---|---|---|---|
| express (small) | 147 | 1 | 18K | 3800 |
| excalidraw/django (medium) | 643–3043 | 2 | 28K | 6500 |
| vscode (large) | 10446 | 3 | 35K | 7000 |
| ~20k / ~40k | — | 4 / 5 | 38K | 7000 |
getExploreBudget(fileCount)→ call budget:<500→1, <5000→2, <15000→3, <25000→4, ≥25000→5(max 5).getExploreOutputBudget(fileCount)→ per-call output (chars / files / per-file). Invariant: a larger tier must never get a smallermaxCharsPerFilethan a smaller tier. (Regression that motivated this doc: the<5000tier's 2500 was below the<500tier's 3800, so on a god-file repo — excalidraw's 415 KBApp.tsx— one explore returned <1% of the file and forced a Read.)- Explore output must never tell the agent to "use Read" — steer to another
codegraph_exploreand "treat returned source as already Read."
Dynamic-dispatch coverage — the flow must EXIST in the graph end-to-end
Static tree-sitter extraction misses computed/indirect calls, so flows break at dynamic dispatch and the agent reads to reconstruct them. Synthesizers/resolvers bridge these so trace/explore connect end-to-end (src/resolution/callback-synthesizer.ts, src/resolution/frameworks/). Channels today: callback/observer, EventEmitter, React re-render (setState→render), JSX child (render→child component), django ORM descriptor. All synthesized edges are provenance:'heuristic' with metadata.synthesizedBy + registeredAt (the wiring site), surfaced inline in trace, the node trail, and context call-paths.
Principle: partial coverage is WORSE than none. Bridging one boundary but not the next reveals a hop the agent then drills + reads to finish. Measured on excalidraw: react-render alone raised reads to 5–7; only completing the flow (adding the jsx-child hop) dropped it to 0–1. Always close the flow end-to-end and re-measure — never ship a half-bridged flow.
Validation methodology (REQUIRED for every new language/framework)
For each language × framework, validate on small, medium, and large real repos with ≥3 different flow prompts each:
- Pick the canonical flow for the framework ("how does X reach Y": state→render, request→handler→view, query→SQL, action→reducer→store…).
- Deterministic probes (
scripts/agent-eval/probe-{trace,node,context,explore}.mjsagainst the builtdist/):trace(from,to)connects end-to-end with no break; no node explosion (select count(*) from nodesstable before/after re-index); synthesized-edge precision spot-check (select … where provenance='heuristic'). - Agent A/B (
scripts/agent-eval/run-all.sh <repo> "<Q>"): with vs without codegraph, ≥2 runs/arm (run-to-run variance is large — never conclude from n=1). Record duration, total tool calls, Read, Grep. Optional forced-Read-0 sufficiency proof via the block-read hook (scripts/agent-eval/hook-settings.json). - Pass bar: a normal flow question reaches ~0 Read/Grep within the repo's explore-call budget, runs faster than without-codegraph, and shows no regression on a control repo. Record the numbers in
docs/design/dynamic-dispatch-coverage-playbook.md(the coverage matrix).
Full playbook + per-mechanism design: docs/design/dynamic-dispatch-coverage-playbook.md and docs/design/callback-edge-synthesis.md.
Worked example — Excalidraw (TS/React, medium, 643 files)
The template to replicate per language/framework. Question: "how does updating an element re-render the canvas on screen?" (the full flow crosses three React boundaries: observer callback, setState→render, and JSX child).
| Stage | duration | Read | Grep | codegraph |
|---|---|---|---|---|
| Without codegraph | 115–139s | 9–10 | 10–11 | 0 |
| Broken (explore-budget regression) | 131–139s | 5–10 | 3–5 | 6–14 |
| Fixed (budget + msgs + synthesis) | 64–112s | 0–2 | 2–4 | 3–10 |
| + trace-first steering | 51–74s | 0–2 | 0–4 | 3–4 |
n=4 unhooked runs/stage, same prompt. After steering flow questions to codegraph_trace first: best run 0 Read / 0 Grep / 3 codegraph / 51s; 2 of 4 fully clean (0 Read, 0 Grep). Steering eliminated the over-drill variance — call count tightened from 3–10 to 3–4, trace adoption went 3/4 → 4/4, and the search+callers path-reconstruction floundering dropped to 0. Run-to-run variance is still real; report the range, never a single run. Residual reads/greps are all the nonce data-flow (canvasNonce — a local prop with no graph edges); that's the def-use/data-flow frontier, left deliberately uncovered (tracking every local would explode the graph). Validated: trace(mutateElement, renderStaticScene) connects in 6 hops across all three boundaries (mutateElement → triggerUpdate → [callback] triggerRender → [react-render] render → [jsx] StaticCanvas → renderStaticScene), each hop showing inline source + the wiring site; node count stable at 9,289; 1 callback + 46 react-render + 280 jsx-render synthesized edges (no explosion, precision-checked).
Tests
Tests live in __tests__/ and mirror the module they cover. Notable ones beyond the obvious:
installer-targets.test.ts— parameterized contract suite across all 4 agent targets (see installer notes above).evaluation/—runner.ts+test-cases.tsexercise codegraph against synthetic projects and score the results; run vianpm run eval(builds first). Not part ofnpm test.sqlite-backend.test.ts— covers native + wasm backend selection and fallback.pr19-improvements.test.ts,frameworks-integration.test.ts— regression coverage for specific past PRs/incidents; don't rename these, the names anchor to git history.
Tests create temp dirs with fs.mkdtempSync and clean up in afterEach. They write real files and exercise real SQLite — there is no DB mocking.
Windows-gated tests
Behavior that differs by platform (path resolution, drive letters, SENSITIVE_PATHS, %APPDATA% config dirs, CRLF) must be gated, not assumed. Use it.runIf(process.platform === 'win32')(...) for Windows-only assertions and it.runIf(process.platform !== 'win32')(...) for POSIX-only ones — e.g. /etc is sensitive on POSIX but resolves to C:\etc (non-existent) on Windows, so an ungated /etc assertion fails on Windows. Validate the Windows side for real (see below); don't merge a Windows-gated test you haven't seen run.
Cross-platform validation
The dev machine — and the default npm test target — is macOS, so local runs cover the macOS path. The other two platforms aren't here; when a change is platform-sensitive (file watching, sockets / named pipes, path & symlink handling, process lifecycle, inotify budget) validate them for real rather than guessing.
Linux (Docker)
When asked to test or validate on Linux, use Docker — there's no Linux box, but Docker runs on the macOS host. Build a throwaway image from the repo and run the suite inside it:
FROM node:22-bookworm;COPYthe repo with a.dockerignoreexcludingnode_modules/dist/.git/.codegraph;RUN npm ci && npm run build. Don't reuse the Macnode_modules—esbuild/rollupship platform-specific binaries.- Run with
docker run --rm --init. The--initis load-bearing for any process-lifecycle test (daemon reaping, the #277 PPID watchdog, idle-timeout): without a zombie-reaping PID 1, a SIGKILL'd/exited process lingers as a zombie andprocess.kill(pid, 0)still reports it alive, so exit-detection assertions false-fail even though the process did exit. - Linux is where the inotify watch budget actually bites: count a process's watches via
/proc/<pid>/fdinfo/*(sum^inotifylines on the fd whosereadlinkisanon_inode:inotify).
Windows (Parallels VM + SSH)
For any Windows-specific PR, bug, or implementation, validate it on the real Windows VM rather than guessing. Connection details live in the gitignored .parallels file at the repo root (VM name, guest IP, SSH user/key). prlctl exec needs Parallels Pro and is unavailable, so SSH is the bridge.
- Connect / run from the Mac host:
ssh <user>@<guest_ip> "...". For multi-line work, pipe PowerShell over stdin and refresh PATH from the registry first (sshd's session has a stale PATH after winget installs):ssh colby@10.211.55.3 "powershell -NoProfile -ExecutionPolicy Bypass -Command -" <<'PS' $env:Path = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Environment]::GetEnvironmentVariable("Path","User") Set-Location C:\dev\codegraph PS - Clone fresh into a Windows-local path (
C:\dev\codegraph) andnpm cithere — never run npm against the shared Mac repo, sinceesbuild/rollupship platform-specific binaries. - Guest toolchain (winget): Node LTS, Git, and the VC++ ARM64 redistributable (required by
@rollup/rollup-win32-arm64-msvc, which vitest pulls in). - Fetch a contributor PR head straight from their fork to dodge
pull/<n>/headlag:git fetch <fork-url> <branch>thengit checkout -f FETCH_HEAD. - Known pre-existing Windows failures (they reproduce on
main, unrelated to your change — confirm againstorigin/mainbefore blaming your PR, and don't let them mask new regressions):security.test.ts > Session marker symlink resistance > does not follow a pre-planted symlink(symlink creation needs privileges on Windows); and themcp-initialize.test.ts/mcp-roots.test.tssuites, which fail inafterEachwithEPERMremoving the temp dir because a spawnedserve --mcp(its--liftoff-onlyre-exec grandchild) still holds the cwd / SQLite file open — a Windows file-locking quirk, not a logic bug.
Releases
Released to npm and mirrored as GitHub Releases. CHANGELOG.md is the source of truth; GitHub Release notes are extracted from it.
Writing changelog entries
Default: write entries under ## [Unreleased] — that's the section reserved for work landing between releases. Don't pre-create a ## [X.Y.Z] block for the next release: the Release workflow's first step is scripts/prepare-release.mjs, which automatically promotes everything under [Unreleased] into a new ## [X.Y.Z] - <YYYY-MM-DD> block at release time (or merges into a pre-existing [X.Y.Z] block if one exists — but you don't need one). Pre-staging is what caused the v0.9.5 sparse-release-notes incident: a sparse [0.9.5] block hand-added before the rest of the work landed got picked by the extractor over the much-larger [Unreleased] section above it. Don't do that.
Formatting rules for any entry (anywhere — [Unreleased] or otherwise):
- Group under
### Added,### Changed,### Fixed,### Removed,### Deprecated,### Security— omit empty sections. The promote step merges matching sub-section headings, so writing under### Addedin[Unreleased]lands under### Addedin[X.Y.Z]. - Write from the user's perspective, not the implementation's. Lead with the observable symptom or capability; mention internals only if a user needs them (e.g., to work around an existing bad install).
- Issue / PR references in entries are by number (
(#403)etc.); the GitHub renderer auto-links them in the published release notes. - Don't add a
[X.Y.Z]: https://...link reference yourself —prepare-release.mjsappends it automatically when it promotes the version (idempotent: a re-run is a no-op if it already exists).
Release flow (the user runs these)
Releases are built and published by the GitHub Actions "Release" workflow
(.github/workflows/release.yml). It runs scripts/prepare-release.mjs to
promote [Unreleased] into [<version>] (and auto-commit + push that
CHANGELOG change back to main so on-disk truth matches the published
notes), then bundles a Node runtime per platform (scripts/build-bundle.sh)
and publishes both the GitHub Release and the npm thin-installer
(scripts/pack-npm.sh: a shim package + per-platform packages).
Publishing manually is wrong now — a plain npm publish ships the root
package (non-bundled), which breaks anyone on Node < 22.5.
Claude does NOT bump the version unless explicitly asked. The maintainer
typically does it themselves — often by editing package.json directly via
the GitHub web UI. Don't proactively commit a version bump as part of
unrelated work, and don't propose one when summarizing a PR.
When the maintainer DOES bump the version, the only edit strictly required is
to package.json — the workflow's "Sync package-lock.json" step detects a
mismatch between package.json and package-lock.json, runs
npm install --package-lock-only --ignore-scripts to rewrite the lock file's
version fields (top-level + packages.""), and auto-commits + pushes the
result back to main with [skip ci]. So a GitHub-web-UI single-file edit to
package.json is enough to kick off a clean release. (If they edit both files
locally, that's fine too — the sync step no-ops.)
Once package.json is at the target version on main, trigger
Actions → Release → Run workflow (on main). The workflow:
- Syncs
package-lock.jsontopackage.json's version if they've drifted; commits + pushes that change. - Runs
prepare-release.mjs <X.Y.Z>→ promotes[Unreleased]→[X.Y.Z] - <today>inCHANGELOG.md, appends the link reference, commits + pushes the move with[skip ci]. - Builds every platform bundle on one runner, generates
SHA256SUMS. - Creates the GitHub Release with notes from the freshly-promoted
[X.Y.Z]block. - Publishes the npm shim + per-platform packages. Requires the
NPM_TOKENrepo secret.
Do not run npm publish, git push, or git tag yourself — these are
publish actions on shared state. Write the files, hand the user the commands.
House rules
- The
0.7.xline is in active multi-agent rollout. Any change tosrc/installer/(especiallytargets/) needs corresponding test coverage and a CHANGELOG entry — installer regressions break every new install silently. - When changing what the MCP tools do or how agents should use them, update all three of
src/mcp/server-instructions.ts,src/installer/instructions-template.ts, and.cursor/rules/codegraph.mdc— they're written to different places but say the same thing. - CodeGraph provides code context, not product requirements. For new features, ask the user about UX, edge cases, and acceptance criteria — the graph won't tell you.
- When the user references issues, PR comments, or external reports, anchor them to a date and version before drawing conclusions. Check the comment's
createdAtagainst:- The last released version —
grep -m1 '^## \[' CHANGELOG.mdshows the top-of-file version (older releases follow). A comment dated before the latest## [X.Y.Z] - YYYY-MM-DDis reacting to released state — work that's only onmainor on an unmerged branch doesn't apply. - The last main commit —
git log --first-parent main -1 --format='%ai %h %s'. A comment after the last release but before a fix on main may already be addressed there but unreleased. - The current branch's tip — your own unmerged work obviously can't be what the comment is reacting to. Always disambiguate "released," "merged-but-unreleased," and "in-progress" before agreeing that a user-reported problem is unfixed (or that a fix is incomplete). A user saying "your fix only covers X" about a recent PR is usually pointing at the released shortcomings — your in-flight branch may already address them but they have no way to know that.
- The last released version —