diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1480ea4..cd89e72 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### New Features
- `codegraph init` now builds the initial index by default — you no longer need the `-i`/`--index` flag (it's still accepted, so existing commands and scripts keep working). (#483)
+- Go: Gin middleware chains now connect end-to-end in `codegraph_trace` and `codegraph_explore` — following a request reaches the middleware and route handlers registered via `.Use()` / `.GET()` instead of dead-ending where the framework dispatches the chain dynamically.
### Fixes
diff --git a/README.md b/README.md
index 6b55f2d..96a3f42 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
### Supercharge Claude Code, Cursor, Codex, OpenCode, Hermes Agent, Gemini, Antigravity, and Kiro with Semantic Code Intelligence
-**~35% cheaper · ~70% fewer tool calls · 100% local**
+**~18% cheaper · ~57% fewer tool calls · 100% local**
### [Documentation & Website →](https://colbymchenry.github.io/codegraph/)
@@ -83,26 +83,101 @@ When Claude Code explores a codebase, it spawns **Explore agents** that scan fil
### Benchmark Results
-Tested across **7 real-world open-source codebases** spanning 7 languages, comparing an agent (Claude Code, headless) answering one architecture question **with** and **without** CodeGraph. Each cell is the savings at the **median of 4 runs per arm**. _Re-validated on **v0.9.4** (2026-05-24)._
+Tested across **7 real-world open-source codebases** spanning 7 languages, comparing an agent (Claude Code, headless) answering one architecture question **with** and **without** CodeGraph. Each cell is the savings at the **median of 4 runs per arm**. _Re-validated on **v0.9.7** + Opus 4.8 (2026-05-28)._
-> **Average: 35% cheaper · 57% fewer tokens · 46% faster · 71% fewer tool calls**
+> **Average: 18% cheaper · 51% fewer tokens · 16% faster · 57% fewer tool calls**
| Codebase | Language | Cost | Tokens | Time | Tool calls |
|----------|----------|------|--------|------|------------|
-| **VS Code** | TypeScript · ~10k files | 26% cheaper | 78% fewer | 52% faster | 85% fewer |
-| **Excalidraw** | TypeScript · ~640 | 52% cheaper | 90% fewer | 73% faster | 96% fewer |
-| **Django** | Python · ~3k | 12% cheaper | 36% fewer | 19% faster | 53% fewer |
-| **Tokio** | Rust · ~790 | 82% cheaper | 86% fewer | 71% faster | 92% fewer |
-| **OkHttp** | Java · ~645 | 2% cheaper | 13% fewer | 31% faster | 45% fewer |
-| **Gin** | Go · ~110 | 21% cheaper | 34% fewer | 27% faster | 40% fewer |
-| **Alamofire** | Swift · ~110 | 47% cheaper | 64% fewer | 48% faster | 83% fewer |
+| **VS Code** | TypeScript · ~10k files | 26% cheaper | 63% fewer | 20% faster | 69% fewer |
+| **Excalidraw** | TypeScript · ~640 | 40% cheaper | 71% fewer | 41% faster | 82% fewer |
+| **Django** | Python · ~3k | 10% costlier | 45% fewer | 3% slower | 64% fewer |
+| **Tokio** | Rust · ~790 | 30% cheaper | 69% fewer | 22% faster | 71% fewer |
+| **OkHttp** | Java · ~645 | 3% costlier | 32% fewer | 15% faster | 60% fewer |
+| **Gin** | Go · ~110 | 7% cheaper | 35% fewer | 8% faster | 38% fewer |
+| **Alamofire** | Swift · ~110 | 38% cheaper | 45% fewer | 6% faster | 8% fewer |
-The gains scale with codebase size: on large repos the agent answers from the index in a handful of calls with **zero file reads**, while the no-CodeGraph agent fans out across grep/find/Read (and the sub-agents it spawns). On a small repo like Gin (~150 files) native search is already cheap, so the margin narrows.
+CodeGraph cuts **tool calls and total tokens on every repo** and answers large repos with **zero file reads**, while the no-CodeGraph agent spends its budget on grep/find/Read discovery. The **cost** margin is narrower — and occasionally negative on smaller repos (Django, OkHttp) — because a modern model's native search is already cheap and CodeGraph's richer responses cost real input tokens; the consistent wins are fewer tool calls and faster answers.
+
+
+Per-repo breakdown — WITH vs WITHOUT (median of 4)
+
+**VS Code** · ~10k files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 1m 49s | 2m 16s | 20% faster |
+| File Reads | 0 | 7 | −7 |
+| Grep/Bash | 0 | 9 | −9 |
+| Tool calls | 5 | 16 | 71% fewer |
+| Total tokens | 672k | 1.81M | 63% fewer |
+| Cost | $0.66 | $0.89 | 26% cheaper |
+
+**Excalidraw** · ~640 files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 1m 41s | 2m 51s | 41% faster |
+| File Reads | 0 | 11 | −11 |
+| Grep/Bash | 0 | 11 | −11 |
+| Tool calls | 4 | 22 | 82% fewer |
+| Total tokens | 692k | 2.39M | 71% fewer |
+| Cost | $0.63 | $1.04 | 40% cheaper |
+
+**Django** · ~3k files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 2m 2s | 1m 58s | 4% slower |
+| File Reads | 2 | 10 | −8 |
+| Grep/Bash | 0 | 5 | −5 |
+| Tool calls | 5 | 14 | 64% fewer |
+| Total tokens | 720k | 1.30M | 45% fewer |
+| Cost | $0.70 | $0.64 | 10% costlier |
+
+**Tokio** · ~790 files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 1m 54s | 2m 26s | 22% faster |
+| File Reads | 0 | 11 | −11 |
+| Grep/Bash | 0 | 6 | −6 |
+| Tool calls | 5 | 17 | 73% fewer |
+| Total tokens | 657k | 2.10M | 69% fewer |
+| Cost | $0.61 | $0.86 | 30% cheaper |
+
+**OkHttp** · ~645 files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 1m 18s | 1m 32s | 15% faster |
+| File Reads | 1 | 5 | −4 |
+| Grep/Bash | 0 | 6 | −6 |
+| Tool calls | 4 | 10 | 58% fewer |
+| Total tokens | 713k | 1.05M | 32% fewer |
+| Cost | $0.59 | $0.57 | 3% costlier |
+
+**Gin** · ~110 files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 1m 12s | 1m 18s | 9% faster |
+| File Reads | 0 | 4 | −4 |
+| Grep/Bash | 0 | 4 | −4 |
+| Tool calls | 5 | 8 | 40% fewer |
+| Total tokens | 533k | 815k | 35% fewer |
+| Cost | $0.44 | $0.47 | 7% cheaper |
+
+**Alamofire** · ~110 files
+| Metric | WITH cg | WITHOUT cg | Δ |
+|---|---|---|---|
+| Time | 2m 0s | 2m 7s | 6% faster |
+| File Reads | 6 | 8 | −2 |
+| Grep/Bash | 2 | 4 | −2 |
+| Tool calls | 11 | 12 | 9% fewer |
+| Total tokens | 1.09M | 1.98M | 45% fewer |
+| Cost | $0.63 | $1.01 | 38% cheaper |
+
+
Full benchmark details
-**Methodology.** Each arm is `claude -p` (Claude Opus 4.7) run headlessly against the repo with `--strict-mcp-config`: **WITH** = CodeGraph's MCP server enabled, **WITHOUT** = an empty MCP config. Built-in Read/Grep/Bash stay available to both. Same question per repo, **4 runs per arm, median reported**. Cost = the run's `total_cost_usd`; Tokens = total tokens processed (input incl. cached + output); Time = wall-clock; Tool calls = every tool invocation, including those inside any sub-agents the model spawns. Repos cloned at `--depth 1` and indexed by the same CodeGraph build that served them. Re-validated on codegraph **v0.9.4** (2026-05-24); per-repo numbers move run-to-run with how hard the without-arm thrashes (the median-of-4 smooths it, but tails remain — e.g. Tokio's without-arm hit $2.41/3m one batch).
+**Methodology.** Each arm is `claude -p` (Claude Opus 4.8) run headlessly against the repo with `--strict-mcp-config`: **WITH** = CodeGraph's MCP server enabled, **WITHOUT** = an empty MCP config. Built-in Read/Grep/Bash stay available to both. Same question per repo, **4 runs per arm, median reported**. Cost = the run's `total_cost_usd`; Tokens = total tokens processed (input incl. cached + output); Time = wall-clock; Tool calls = every tool invocation, including those inside any sub-agents the model spawns. Repos cloned at `--depth 1` and indexed by the same CodeGraph build that served them. Re-validated on codegraph **v0.9.7** (2026-05-28). These numbers are lower than the prior Opus 4.7 validation — not a CodeGraph regression but a stronger native baseline: Opus 4.8 greps/reads efficiently on the main thread instead of fanning out into large Explore-subagent sweeps, so the no-CodeGraph arm is leaner than it used to be. Per-repo numbers move run-to-run with how hard the without-arm thrashes (the median-of-4 smooths it, but tails remain — e.g. Django's without-arm hit $2.71/14m one batch).
**Queries:**
| Codebase | Query |
@@ -115,18 +190,7 @@ The gains scale with codebase size: on large repos the agent answers from the in
| Gin | "How does gin route requests through its middleware chain?" |
| Alamofire | "How does Alamofire build, send, and validate a request?" |
-**Raw medians — WITH → WITHOUT:**
-| Codebase | Cost | Tokens | Time | Tool calls |
-|----------|------|--------|------|------------|
-| VS Code | $0.60 → $0.80 | 601k → 2.8M | 1m 10s → 2m 26s | 8 → 55 |
-| Excalidraw | $0.43 → $0.90 | 344k → 3.5M | 48s → 2m 58s | 3 → 79 |
-| Django | $0.59 → $0.67 | 739k → 1.2M | 1m 19s → 1m 38s | 9 → 19 |
-| Tokio | $0.42 → $2.41 | 379k → 2.6M | 53s → 3m 2s | 4 → 53 |
-| OkHttp | $0.47 → $0.47 | 636k → 730k | 42s → 1m 1s | 6 → 11 |
-| Gin | $0.37 → $0.47 | 444k → 675k | 44s → 1m 0s | 6 → 10 |
-| Alamofire | $0.61 → $1.14 | 1.0M → 2.8M | 1m 17s → 2m 27s | 12 → 69 |
-
-**Why CodeGraph wins:** with the index available, the agent answers directly — `codegraph_context` to map the area, then one `codegraph_explore` for the relevant source — and stops, usually with zero file reads. Without it, the agent (and the Explore sub-agents it spawns) spends most of its budget on discovery (find/ls/grep) before reading the right code. CodeGraph only helps when queried *directly*, so its instructions steer agents to answer directly rather than delegate exploration to file-reading sub-agents — otherwise a sub-agent reads files regardless and CodeGraph becomes overhead.
+**Why CodeGraph wins:** with the index available, the agent answers directly — `codegraph_context` to map the area, then one `codegraph_explore` for the relevant source — and stops, usually with zero file reads. Without it, the agent spends most of its budget on discovery (find/ls/grep) before reading the right code. CodeGraph only helps when queried *directly*, so its instructions steer agents to answer directly rather than delegate exploration to file-reading sub-agents — otherwise a sub-agent reads files regardless and CodeGraph becomes overhead.
diff --git a/__tests__/gin-middleware-chain.test.ts b/__tests__/gin-middleware-chain.test.ts
new file mode 100644
index 0000000..b3f0ae0
--- /dev/null
+++ b/__tests__/gin-middleware-chain.test.ts
@@ -0,0 +1,113 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import * as fs from 'node:fs';
+import * as path from 'node:path';
+import * as os from 'node:os';
+import { CodeGraph } from '../src';
+
+/**
+ * End-to-end synthesizer test for the gin middleware chain.
+ *
+ * `(*Context).Next` runs the handler chain by slice index
+ * (`c.handlers[c.index](c)`) — a computed dispatch tree-sitter can't resolve, so
+ * `callees(Next)` would otherwise dead-end at the `len()` helper. Handlers are
+ * registered via `.Use(...)` / `.GET("/path", h)`. Verify the synthesizer links
+ * `Next` → each registered NAMED HandlerFunc, captures the wiring site, and
+ * skips inline (anonymous) closures.
+ */
+describe('gin middleware-chain synthesizer', () => {
+ let dir: string;
+
+ beforeEach(() => {
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gin-chain-fixture-'));
+ });
+
+ afterEach(() => {
+ fs.rmSync(dir, { recursive: true, force: true });
+ });
+
+ it('links Context.Next to handlers registered via Use/GET and skips inline closures', async () => {
+ fs.writeFileSync(path.join(dir, 'go.mod'), 'module ginapp\n\ngo 1.21\n');
+
+ // gin-core shape: the dynamic-dispatch chain driver + registration surface.
+ fs.writeFileSync(
+ path.join(dir, 'gin.go'),
+ `package gin
+
+type HandlerFunc func(*Context)
+type HandlersChain []HandlerFunc
+
+type Context struct {
+ handlers HandlersChain
+ index int8
+}
+
+func (c *Context) Next() {
+ c.index++
+ for c.index < int8(len(c.handlers)) {
+ c.handlers[c.index](c)
+ c.index++
+ }
+}
+
+type Engine struct {
+ Handlers HandlersChain
+}
+
+func (e *Engine) Use(middleware ...HandlerFunc) {
+ e.Handlers = append(e.Handlers, middleware...)
+}
+
+func (e *Engine) GET(path string, handlers ...HandlerFunc) {}
+`
+ );
+
+ // registration site: named middleware + named route handler + an inline closure.
+ fs.writeFileSync(
+ path.join(dir, 'app.go'),
+ `package gin
+
+func Logger(c *Context) {}
+func Recovery(c *Context) {}
+func getUser(c *Context) {}
+
+func setup() {
+ e := &Engine{}
+ e.Use(Logger, Recovery)
+ e.GET("/users", getUser)
+ e.GET("/inline", func(c *Context) {})
+}
+`
+ );
+
+ const cg = await CodeGraph.init(dir, { silent: true });
+ await cg.indexAll();
+
+ const db = (cg as any).db.db;
+ const rows = db
+ .prepare(
+ `SELECT s.name source_name, s.kind source_kind, t.name target_name,
+ json_extract(e.metadata,'$.via') via,
+ json_extract(e.metadata,'$.registeredAt') registeredAt
+ FROM edges e
+ JOIN nodes s ON s.id = e.source
+ JOIN nodes t ON t.id = e.target
+ WHERE json_extract(e.metadata,'$.synthesizedBy') = 'gin-middleware-chain'`
+ )
+ .all();
+ cg.close?.();
+
+ // Every edge originates from the chain dispatcher Context.Next.
+ expect(rows.length).toBeGreaterThan(0);
+ expect(rows.every((r: any) => r.source_name === 'Next' && r.source_kind === 'method')).toBe(true);
+
+ // Exactly the three NAMED handlers are linked — the inline closure (4th
+ // registration) is anonymous and must be skipped.
+ const targets = new Set(rows.map((r: any) => r.target_name));
+ expect(targets).toEqual(new Set(['Logger', 'Recovery', 'getUser']));
+
+ // The wiring site (`.Use`/`.GET` call) is surfaced for the agent.
+ const logger = rows.find((r: any) => r.target_name === 'Logger');
+ expect(logger.via).toBe('Logger');
+ expect(logger.registeredAt).toMatch(/app\.go:\d+/);
+ });
+});
diff --git a/docs/design/dynamic-dispatch-coverage-playbook.md b/docs/design/dynamic-dispatch-coverage-playbook.md
index cb7fdc8..1124e9f 100644
--- a/docs/design/dynamic-dispatch-coverage-playbook.md
+++ b/docs/design/dynamic-dispatch-coverage-playbook.md
@@ -182,7 +182,7 @@ Status legend: ✅ done+validated · 🔬 hole identified · ⬜ not started.
| Python | Django ORM | QuerySet → SQL compiler | R | ✅ |
| Python | Django / DRF (views) | url → view → model | R + X | ✅ url→view (`path`/`url`/`as_view`) + **DRF `router.register`→ViewSet** (realworld S / wagtail M / saleor L); ORM QuerySet→SQL (prior work). 🔬 signals (`post_save`→receiver), DRF viewset CRUD actions (inherited), saleor GraphQL resolvers |
| Python | Flask / FastAPI | request → route → handler → dependency | R + X | ✅ **Flask: handler resolved across intervening decorators (`@login_required`) + stacked `@x.route` lines** (microblog S 6→27, redash L decorator routes 6/6); **FastAPI: empty-path router-root routes `@router.get("")` incl. multi-line** (realworld S 12→20 / Netflix dispatch L **290/290 100%**) + **bare-name builtin guard** — a handler named after a Python builtin method (`index`/`get`/`update`/`count`…) was filtered as a builtin and lost its route→handler edge. + **Flask-RESTful `add_resource(Resource,'/x')` → Resource class** (redash 6→**77**) + **tuple `methods=('GET',)`** (was mislabeled GET) + **broadened detection** (requirements/Pipfile/setup + subdir app-factory entrypoints — flask-realworld 0→**19**). 🔬 FastAPI `Depends()` dependency edges (light validation) |
-| Go | Gin / chi / gorilla/mux / net-http | request → route → handler → service | X | ✅ **routes on ANY group var** (`v1.GET`, `PublicGroup.GET`) not just `r/router` (gin-vue-admin S→M 4→259 / realworld S / gitness L) — was missing all group-routed apps; named handlers resolve precisely. **gorilla/mux confirmed covered** by the any-receiver `HandleFunc`/`Handle` handling (subrouter-var `s.HandleFunc(...)` + namespaced handlers; `.Methods()` chain ignored). 🔬 inline `func(c){}` handlers (anonymous, body lost); subrouter/`PathPrefix` path-prefix not prepended (label only); gitness chi custom (26/321) |
+| Go | Gin / chi / gorilla/mux / net-http | request → route → handler → service; middleware chain (`Use`→`Next`) | S + X | ✅ **routes on ANY group var** (`v1.GET`, `PublicGroup.GET`) not just `r/router` (gin-vue-admin S→M 4→259 / realworld S / gitness L) — was missing all group-routed apps; named handlers resolve precisely. **gorilla/mux confirmed covered** by the any-receiver `HandleFunc`/`Handle` handling (subrouter-var `s.HandleFunc(...)` + namespaced handlers; `.Methods()` chain ignored). + **gin middleware-chain synthesizer** (`ginMiddlewareChainEdges`): gin runs its entire chain through one dynamic line — `(*Context).Next` does `c.handlers[c.index](c)`, a slice-index dispatch tree-sitter can't resolve, so `callees(Next)` dead-ended at the `len()` helper (`safeInt8`) and the agent rabbit-holed re-querying it. Find the dispatcher (a Go method invoking a `handlers` slice by index) and link it → every HandlerFunc registered via `.Use`/`.GET`/…/`.Handle`; gated on the dispatcher existing (inert on non-gin Go repos), named handlers only (closures skipped), capped. gin L: `callees(Next)` now surfaces `Logger`/`Recovery`/`ErrorLogger`+handlers (node count stable 2,544; 5 precise edges with `registeredAt` wiring sites). **Agent A/B (headless median-of-4, Opus 4.8): gin flipped from codegraph −58% cost / −129% time (the rabbit-hole, incl. a stray `Workflow` mis-fire on 2/4 WITH runs) → +7% cost / +35% tokens / +8% time / 38% tool calls, all 4 WITH runs clean (0 Read/Grep/Bash, no Workflow, no duplicate calls).** 🔬 inline `func(c){}` handlers (anonymous, body lost); subrouter/`PathPrefix` path-prefix not prepended (label only); gitness chi custom (26/321) |
| Rust | Axum / actix / Rocket | request → route → handler | R + X | ✅ **Axum chained methods + namespaced handlers** — `.route("/x", get(h1).post(h2))` emitted only the first method+handler, and `get(mod::handler)` captured the module not the fn (realworld-axum S **12→19, 19/19**); balanced-paren scan + per-method nodes + last-`::`-segment handler. **Rocket attribute macros 550/556 (99%)** (Rocket repo L) — already strong. crates.io named axum routes resolve (6/8; rest are closures/var handlers; its API is mostly the utoipa `routes!` macro = frontier). Cargo-workspace module resolution (prior work). **actix builder API** `web::resource("/x").route(web::get().to(h))` / `.to(h)` / App `.route("/x", web::get().to(h))` (actix-examples **51→128 routes, 35→112 resolved**) — was the dominant actix style and fully missed (the handler is in `.to(h)`, not `get(h)`). 🔬 actix `web::scope("/api")` prefix (not prepended to nested resource paths) + anonymous `.to` closure handlers |
| Java | Spring | request → @RestController → @Autowired service → repo | R + X | ✅ **bare `@GetMapping`/`@PostMapping` + class `@RequestMapping` prefix join → route→method** (realworld S / mall M / halo L) — was missing all path-less method mappings; DI controller→service resolves (name + dir) + **interface→impl dispatch synthesizer** (`interfaceOverrideEdges`: a class's `implements`/`extends` → link each interface/base method → its same-name override; JVM-gated, capped, **overload-aware**; mall **310** / halo **734** synth edges, node count unchanged) so trace follows controller→service-**interface**→**impl** instead of dead-ending at the abstract method — `trace("PmsProductController.getList","PmsProductServiceImpl.list")` connects in **3 hops** (probe-validated). + **field-injected concrete-bean trace** (#389): `this..method()` strips the `this.` receiver at extraction, and the resolver looks up the receiver name in the enclosing class's field declarations to get the declared type, then resolves the method on it — closes the controller→bean hop when the field-name doesn't capitalize to the type (`@Resource(name="userBO") UserBO userbo` → `userbo.toLogin2()` reaches `UserBO.toLogin2`). + **`@Value("${k}")` / `@ConfigurationProperties(prefix="X")` → application.{yml,yaml,properties}** binding with Spring's relaxed binding (kebab↔camel↔snake), incl. `${k:default}`. mall-tiny S: 11/11 `@Value` resolved. ⚠️ **agent A/B null** (n=2: the agent went context→explore→Read and never invoked `trace`, so the synth edges weren't exercised — adoption-gated, the recurring wall; see `docs/benchmarks/call-sequence-analysis.md`). The fix is correct + improves trace/callees/impact/context connectivity regardless; agent-visible read reduction needs trace adoption. 🔬 Spring Data JPA derived queries (`findByEmail`) — metaprogramming frontier; `@PropertySource` external files; Spring Cloud Config; mapper-class simple-name collisions across packages (dropped to avoid mis-resolution) |
| Java | MyBatis (XML mappers) | DAO interface method → `