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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7a75c82dd9
commit
f58de8a391
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user