Преглед изворни кода

feat(resolution): gin middleware-chain synthesizer + Opus 4.8 benchmark refresh (#547)

* fix(agent-eval): detect idle by content-stability, not spinner absence

Opus 4.8's extended-thinking TUI shows no spinner / interrupt hint / timer while it streams its final answer — those appear only during the thinking and tool-use phases. The old detector treated ~5s of not-busy + prompt-present as done, so it killed interactive runs mid-answer, silently truncating both arms of the tmux A/B (low tool counts; the final assistant message left as a mid-investigation preamble). Now a run is done only when the captured pane stops changing for ~8s; while streaming, the pane changes every poll so stability never accrues. BUSY_RE stays as the immediate busy-reset for the thinking/tool/live-timer phase. Content-stability is model-agnostic — it survives future spinner re-wordings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): refresh VS Code benchmark on v0.9.7 + Opus 4.8

Re-ran the VS Code A/B (headless median-of-4) on the current build and model. Cost savings held at 26% ($0.66->$0.89), but token/time/tool-call savings narrowed (78->63%, 52->20%, 85->69%) because Opus 4.8's without-CodeGraph arm explores far more efficiently than 4.7's did (16 tool calls vs 55, no Explore-subagent fan-out); the WITH arm is unchanged at 5 calls / 0 reads. Recomputed the average row and noted that the VS Code row is now a different model/version epoch than the other six.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(resolution): synthesize gin middleware-chain edges (Next -> registered handlers)

Gin runs its entire handler chain through one dynamic line in (*Context).Next -- c.handlers[c.index](c), a slice-index dispatch tree-sitter can't resolve. So callees(Next) dead-ended at the len() helper and the flow ServeHTTP -> handleHTTPRequest -> Next stopped at the exact symbol a 'how does the middleware chain work' question is about, sending the agent to re-query and Read/grep (a measured gin WITH-arm rabbit-hole: 2/4 headless runs spiraled to ~5min, one mis-firing the opt-in Workflow orchestration tool). Find the chain dispatcher (a Go method invoking a handlers slice by index) and link it -> every HandlerFunc registered via .Use/.GET/.../.Handle, so callees(Next) and trace(ServeHTTP, handler) connect end-to-end. Gated on the dispatcher existing (inert on non-gin Go repos), named handlers only (inline closures skipped), capped; provenance heuristic / synthesizedBy gin-middleware-chain, registeredAt = the registration site. Validated: gin callees(Next) now surfaces Logger/Recovery/ErrorLogger + handlers (node count stable at 2,544; 5 precise edges); agent A/B (headless median-of-4, Opus 4.8) flipped gin from -58% cost / -129% time to +7% cost / +35% tokens / +8% time / 38% tool calls, all 4 WITH runs clean (0 Read/Grep/Bash). 167/167 unit tests pass incl. the new gin-middleware-chain test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(readme): publish uniform Opus 4.8 benchmark + per-repo breakdown accordion

Refresh all 7 benchmark rows to the v0.9.7 / Opus 4.8 headless median-of-4 (was a mix of the 4.8 VS Code row + six 4.7 rows). New average 18% cheaper / 51% fewer tokens / 16% faster / 57% fewer tool calls; headline + methodology note updated 4.7->4.8. The gap is smaller than the prior 4.7 numbers because Opus 4.8's native grep/read is more efficient (the without-arm no longer fans out into large Explore-subagent sweeps) -- not a codegraph regression; CodeGraph still cuts tool calls and tokens on all 7 repos, with cost marginal/negative only on django + okhttp. Adds a top-level 'Per-repo breakdown' accordion (per-metric Time/Reads/Grep-Bash/Tool calls/Tokens/Cost, WITH vs WITHOUT, per repo) directly below the condensed summary; methodology/queries/why-wins move to a second accordion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note Gin middleware-chain synthesizer under [Unreleased]

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Colby Mchenry пре 3 недеља
родитељ
комит
f58de8a391

+ 1 - 0
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
 

+ 88 - 24
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.
+
+<details>
+<summary><strong>Per-repo breakdown — WITH vs WITHOUT (median of 4)</strong></summary>
+
+**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 |
+
+</details>
 
 <details>
 <summary><strong>Full benchmark details</strong></summary>
 
-**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.
 
 </details>
 

+ 113 - 0
__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+/);
+  });
+});

Разлика између датотеке није приказан због своје велике величине
+ 1 - 1
docs/design/dynamic-dispatch-coverage-playbook.md


+ 24 - 11
scripts/agent-eval/itrun.sh

@@ -81,25 +81,38 @@ for _ in $(seq 1 120); do
 done
 [ "$started" = 1 ] || { echo "agent never started working"; cap; tmux kill-session -t "$SESSION" 2>/dev/null; exit 1; }
 
-# Poll for idle: not busy AND ❯ present, for 10 consecutive polls (~5s) to ride
-# out mid-conversation thinking gaps that briefly drop the spinner. Up to ~15min.
-consec=0
-for _ in $(seq 1 1800); do
-  pane=$(cap)
-  if echo "$pane" | grep -qE "$BUSY_RE"; then
-    consec=0
-  elif echo "$pane" | grep -q "❯"; then
-    consec=$((consec+1)); [ "$consec" -ge 10 ] && break
+# Poll for idle. CRITICAL: Opus 4.8 (extended thinking) renders NO spinner /
+# "esc to interrupt" / timer while it STREAMS its final answer — those appear
+# only during the thinking + tool-use phases ("✻ Marinating… (32s · ↓ 1.3k
+# tokens · thinking with max effort)"). So BUSY_RE reads "not busy" for the whole
+# 10-30s answer stream, and any short not-busy threshold kills the run mid-answer
+# (the truncation bug). We therefore detect "done" by CONTENT STABILITY, not by a
+# spinner string: while the agent streams, the captured pane changes every poll,
+# so stability never accrues; it accrues only once the agent has finished and the
+# static "✻ Brewed for 1m 9s" summary is all that is left. BUSY_RE still hard-
+# resets stability (covers thinking/tool-use/live-timer, where text can briefly
+# sit still). Need STABLE_NEEDED polls (~8s) of zero pane change + ❯ present.
+# Content-stability is model-agnostic — it survives future spinner re-wordings.
+STABLE_NEEDED=16
+prev=""; stable=0
+for _ in $(seq 1 2400); do            # up to ~20 min
+  pane="$(cap)"
+  sig="$(printf '%s' "$pane" | tr -s '[:space:]' ' ')"
+  if printf '%s' "$pane" | grep -qE "$BUSY_RE"; then
+    stable=0                          # thinking / tool use / live timer → busy
+  elif [ -n "$sig" ] && [ "$sig" = "$prev" ] && printf '%s' "$pane" | grep -q "❯"; then
+    stable=$((stable+1)); [ "$stable" -ge "$STABLE_NEEDED" ] && break
   else
-    consec=0
+    stable=0                          # answer still streaming → pane changing
   fi
+  prev="$sig"
   sleep 0.5
 done
 sleep 1
 
 tmux capture-pane -p -t "$SESSION" -S - > "$OUT"
 echo "captured $(wc -l < "$OUT") lines -> $OUT"
-grep -oE "Done \([^)]*\)" "$OUT" | tail -1
+grep -oE "Done \([^)]*\)|[A-Z][a-z]+ for ([0-9]+m )?[0-9]+s" "$OUT" | tail -1
 grep -oE "[0-9.]+k?/[0-9.]+M" "$OUT" | tail -1 | sed 's/^/Context /'
 tmux kill-session -t "$SESSION" 2>/dev/null
 

+ 123 - 2
src/resolution/callback-synthesizer.ts

@@ -25,6 +25,7 @@ import type { Edge, Node } from '../types';
 import type { QueryBuilder } from '../db/queries';
 import type { ResolutionContext } from './types';
 import { isGeneratedFile } from '../extraction/generated-detection';
+import { stripCommentsForRegex } from './strip-comments';
 
 const REGISTRAR_NAME = /^(on[A-Z]\w*|subscribe|addListener|addEventListener|register|watch|listen|addCallback)$/;
 const DISPATCHER_NAME = /(emit|trigger|notify|dispatch|fire|publish|flush)/i;
@@ -966,11 +967,129 @@ function mybatisJavaXmlEdges(queries: QueryBuilder): Edge[] {
   return edges;
 }
 
+/**
+ * Gin middleware chain. Gin runs its entire handler chain through one dynamic
+ * line in `(*Context).Next`:
+ *     for c.index < len(c.handlers) { c.handlers[c.index](c); c.index++ }
+ * `c.handlers` is a `HandlersChain` (`[]HandlerFunc`) assembled at registration
+ * time by `combineHandlers` from the funcs passed to `r.Use(...)` /
+ * `r.GET("/path", h...)` / `r.Handle(...)`. Because the call is a computed index
+ * into a runtime-built slice, tree-sitter resolves `c.handlers[c.index](c)` to
+ * NOTHING — so `callees(Next)` is just the `len()` helper and the flow
+ * `ServeHTTP → handleHTTPRequest → Next` dead-ends at the exact symbol the
+ * "how do requests flow through the middleware chain" question is about. The
+ * agent then re-queries Next and falls back to Read/grep (validated: the gin
+ * WITH-arm rabbit-holed on precisely this dead-end).
+ *
+ * Bridge it: find the chain DISPATCHER (a Go method whose body invokes a
+ * `handlers` slice by index) and link it → every HandlerFunc registered via a
+ * gin registration call, so `callees(Next)` and `trace(ServeHTTP, <handler>)`
+ * connect end-to-end. Named handlers only (`gin.Logger()` → `Logger`,
+ * `authMiddleware`); inline closures are anonymous and skipped. Like
+ * react-render / interface-impl this is a deliberate over-approximation —
+ * reachability-correct (any registered handler CAN run for some route), capped,
+ * and gated on the dispatcher existing so it never runs on non-gin Go repos.
+ * Provenance `heuristic`, `synthesizedBy:'gin-middleware-chain'`; `registeredAt`
+ * is the `.Use`/`.GET` site an agent would otherwise grep for.
+ */
+const GIN_DISPATCH_RE = /\.handlers\s*\[[^\]]*\]\s*\(/;                 // c.handlers[c.index](c)
+const GIN_REG_RE = /\.(?:Use|GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\s*\(/g;
+
+/** Balanced `(...)` body starting at the '(' index; null if unbalanced. */
+function goBalancedArgs(s: string, openIdx: number): string | null {
+  let depth = 0;
+  for (let i = openIdx; i < s.length; i++) {
+    const c = s[i];
+    if (c === '(') depth++;
+    else if (c === ')') { depth--; if (depth === 0) return s.slice(openIdx + 1, i); }
+  }
+  return null;
+}
+/** Split a top-level comma list, respecting nested () [] {}. */
+function goSplitArgs(args: string): string[] {
+  const out: string[] = [];
+  let depth = 0, cur = '';
+  for (const c of args) {
+    if (c === '(' || c === '[' || c === '{') { depth++; cur += c; }
+    else if (c === ')' || c === ']' || c === '}') { depth--; cur += c; }
+    else if (c === ',' && depth === 0) { out.push(cur); cur = ''; }
+    else cur += c;
+  }
+  if (cur.trim()) out.push(cur);
+  return out;
+}
+/** Tail ident of a handler arg: `gin.Logger()`→`Logger`, `mw`→`mw`; null for string paths / closures. */
+function goHandlerIdent(expr: string): string | null {
+  const cleaned = expr.trim().replace(/\(\s*\)$/, '');                  // drop a trailing call ()
+  if (!cleaned || cleaned.startsWith('"') || cleaned.startsWith('`') || cleaned.startsWith('func')) return null;
+  const m = cleaned.match(/(?:\.|^)([A-Za-z_]\w*)$/);
+  return m ? m[1]! : null;
+}
+
+function ginMiddlewareChainEdges(queries: QueryBuilder, ctx: ResolutionContext): Edge[] {
+  // 1. Find the chain dispatcher(s): a Go method that invokes a `handlers` slice by index.
+  const dispatchers = queries.getNodesByKind('method').filter((n) => {
+    if (n.language !== 'go') return false;
+    const content = ctx.readFile(n.filePath);
+    const src = content && sliceLines(content, n.startLine, n.endLine);
+    return !!src && GIN_DISPATCH_RE.test(src);
+  });
+  if (dispatchers.length === 0) return [];                              // not a gin repo — bail
+
+  // 2. Collect handler identifiers registered via gin registration calls
+  //    (.Use / .GET / … / .Handle). String args (paths/methods) and inline
+  //    closures are dropped by goHandlerIdent; the rest are HandlerFuncs.
+  const registered = new Map<string, string>();                         // name → registeredAt (file:line)
+  for (const file of ctx.getAllFiles()) {
+    if (!file.endsWith('.go')) continue;
+    const content = ctx.readFile(file);
+    if (!content || (!content.includes('.Use(') && !/\.(?:GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD|Any|Handle)\(/.test(content))) continue;
+    const safe = stripCommentsForRegex(content, 'go');
+    GIN_REG_RE.lastIndex = 0;
+    let m: RegExpExecArray | null;
+    while ((m = GIN_REG_RE.exec(safe))) {
+      const parenIdx = m.index + m[0].length - 1;
+      const argStr = goBalancedArgs(safe, parenIdx);
+      if (!argStr) continue;
+      const line = safe.slice(0, m.index).split('\n').length;
+      for (const arg of goSplitArgs(argStr)) {
+        const name = goHandlerIdent(arg);
+        if (name && !registered.has(name)) registered.set(name, `${file}:${line}`);
+      }
+    }
+  }
+  if (registered.size === 0) return [];
+
+  // 3. Link each dispatcher → each registered handler node (dedup, capped).
+  const edges: Edge[] = [];
+  const seen = new Set<string>();
+  for (const disp of dispatchers) {
+    let added = 0;
+    for (const [name, registeredAt] of registered) {
+      if (added >= MAX_CALLBACKS_PER_CHANNEL) break;
+      const handler = ctx.getNodesByName(name).find(
+        (n) => (n.kind === 'function' || n.kind === 'method') && n.language === 'go'
+      );
+      if (!handler || handler.id === disp.id) continue;
+      const key = `${disp.id}>${handler.id}`;
+      if (seen.has(key)) continue;
+      seen.add(key);
+      edges.push({
+        source: disp.id, target: handler.id, kind: 'calls', line: disp.startLine,
+        provenance: 'heuristic',
+        metadata: { synthesizedBy: 'gin-middleware-chain', via: name, registeredAt },
+      });
+      added++;
+    }
+  }
+  return edges;
+}
+
 /**
  * Synthesize dispatcher→callback edges (field observers + EventEmitters +
  * React re-render + JSX children + Vue templates + RN event channel +
- * Fabric native-impl + MyBatis Java↔XML). Returns the count added. Never
- * throws into indexing — callers wrap in try/catch.
+ * Fabric native-impl + MyBatis Java↔XML + Gin middleware chain). Returns the
+ * count added. Never throws into indexing — callers wrap in try/catch.
  */
 export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionContext): number {
   const fieldEdges = fieldChannelEdges(queries, ctx);
@@ -985,6 +1104,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
   const rnEventEdgesList = rnEventEdges(ctx);
   const fabricNativeEdges = fabricNativeImplEdges(ctx);
   const mybatisEdges = mybatisJavaXmlEdges(queries);
+  const ginEdges = ginMiddlewareChainEdges(queries, ctx);
 
   const merged: Edge[] = [];
   const seen = new Set<string>();
@@ -1001,6 +1121,7 @@ export function synthesizeCallbackEdges(queries: QueryBuilder, ctx: ResolutionCo
     ...rnEventEdgesList,
     ...fabricNativeEdges,
     ...mybatisEdges,
+    ...ginEdges,
   ]) {
     const key = `${e.source}>${e.target}`;
     if (seen.has(key)) continue;

Неке датотеке нису приказане због велике количине промена