feat(mcp): codegraph_explore as the sole primary tool + store coverage + overload disambiguation (#647)
## Summary
Completes the explore-overhaul arc: `codegraph_explore` becomes the single primary tool an agent reaches for, and its coverage + output shape are tuned so flow/architecture questions resolve with near-zero Read/Grep.
### What changed
- **explore is the sole primary tool** — removed `codegraph_context` (the fuzzy-input Read-trigger) and `codegraph_trace` (under-picked by agents); explore already surfaces the call flow among the symbols you name. A plain natural-language question now works as the query.
- **Store/handler coverage** — functions defined inside object literals (Zustand `create((set, get) => ({ … }))`, Redux/Pinia/MobX, exported handler/route maps) are indexed as real symbols, including calls through `useStore.getState().fn()` and destructured `const { fn } = useStore.getState()`. A general AST rule, not a per-lib hack.
- **Overload disambiguation** — explore leads with the *right* definition when a method name is overloaded across types (a PascalCase type token in the query biases to that type's own def); `codegraph_node` returns *every* overload's body in one call, with an optional `file`/`line` selector to pin one.
- **Method-atomic render** — explore never returns half a method; at the size budget it drops whole methods/files (and lists what it dropped) instead of truncating a body mid-method.
- **Native-read-shaped output** — per-call output is capped to ~24K with a 25K hard ceiling and concentrated into ~150–250-line flow windows, mirroring how the agent natively reads; repo size scales the *call* budget, not the per-call size (a larger response just gets externalized to a file the host Reads back).
- **Blast radius** folded into explore (dependents + covering tests, locations only).
### Benchmark (refreshed on this build)
Re-validated the 7-repo A/B on 2026-06-02 (Opus 4.8, effort=high, median of 4). WITH arm re-measured on this build, WITHOUT reused:
**~16% cheaper · 47% fewer tokens · 22% faster · 58% fewer tool calls** — 0 file reads on 6 of 7 repos (Gin ~1).
The arc trades larger, cache-heavy explore responses for guaranteed near-zero reads, so cost/token margins soften vs the prior build (Excalidraw and Tokio land at cost break-even) while time and tool-calls stay clear wins everywhere — consistent with the project's stated optimization target (latency + tool-calls, not token cost).
### Validation
- Full suite green: **1112 passed, 2 skipped**.
- 28/28 plain WITH runs across the 7 README repos completed clean; reads median 0 on 6/7.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
+129
-18
@@ -1104,6 +1104,102 @@ export class TreeSitterExtractor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract function-valued properties of an object literal as named function
|
||||
* nodes (named by their property key). Shared by the two object-of-functions
|
||||
* shapes in extractVariable: the object as a direct const value, and the
|
||||
* object returned by a store-initializer call. Handles both `key: () => {}` /
|
||||
* `key: function() {}` pairs and method shorthand `key() {}`.
|
||||
*/
|
||||
private extractObjectLiteralFunctions(obj: SyntaxNode): void {
|
||||
for (let i = 0; i < obj.namedChildCount; i++) {
|
||||
const member = obj.namedChild(i);
|
||||
if (!member) continue;
|
||||
if (member.type === 'pair') {
|
||||
const key = getChildByField(member, 'key');
|
||||
const value = getChildByField(member, 'value');
|
||||
if (key && value && (value.type === 'arrow_function' || value.type === 'function_expression')) {
|
||||
this.extractFunction(value, this.objectKeyName(key));
|
||||
}
|
||||
} else if (member.type === 'method_definition') {
|
||||
// Method shorthand: `{ fetchUser() {...} }`. extractMethod deliberately
|
||||
// skips object-literal methods, so route through extractFunction with an
|
||||
// explicit name (method_definition exposes a `body` field, so resolveBody
|
||||
// falls through to it and the node spans the full method).
|
||||
const key = getChildByField(member, 'name');
|
||||
if (key) this.extractFunction(member, this.objectKeyName(key));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Property-key text with surrounding quotes stripped (`'foo'` → `foo`). */
|
||||
private objectKeyName(key: SyntaxNode): string {
|
||||
return getNodeText(key, this.source).replace(/^['"`]|['"`]$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `call_expression` initializer (`create((set, get) => ({...}))`),
|
||||
* find the object literal RETURNED by a function argument — descending through
|
||||
* nested call_expression arguments so middleware wrappers are unwrapped
|
||||
* (`create(persist((set, get) => ({...}), {...}))`, devtools, immer,
|
||||
* subscribeWithSelector). Returns null when no such object is found — the
|
||||
* common case for ordinary call initializers — so this stays cheap and silent
|
||||
* rather than guessing. Keyed purely on AST shape; no library names.
|
||||
*/
|
||||
private findInitializerReturnedObject(callNode: SyntaxNode, depth = 0): SyntaxNode | null {
|
||||
if (depth > 4) return null;
|
||||
const args = getChildByField(callNode, 'arguments');
|
||||
if (!args) return null;
|
||||
for (let i = 0; i < args.namedChildCount; i++) {
|
||||
const arg = args.namedChild(i);
|
||||
if (!arg) continue;
|
||||
if (arg.type === 'arrow_function' || arg.type === 'function_expression') {
|
||||
const obj = this.functionReturnedObject(arg);
|
||||
if (obj) return obj;
|
||||
} else if (arg.type === 'call_expression') {
|
||||
const obj = this.findInitializerReturnedObject(arg, depth + 1);
|
||||
if (obj) return obj;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The object literal a function expression returns — either the `=> ({...})`
|
||||
* arrow form (a parenthesized_expression wrapping an object) or a
|
||||
* `=> { return {...} }` block. Returns null for any other body shape.
|
||||
*/
|
||||
private functionReturnedObject(fnNode: SyntaxNode): SyntaxNode | null {
|
||||
const body = getChildByField(fnNode, 'body');
|
||||
if (!body) return null;
|
||||
const asObject = (n: SyntaxNode | null): SyntaxNode | null => {
|
||||
if (!n) return null;
|
||||
if (n.type === 'object' || n.type === 'object_expression') return n;
|
||||
if (n.type === 'parenthesized_expression') {
|
||||
for (let i = 0; i < n.namedChildCount; i++) {
|
||||
const inner = asObject(n.namedChild(i));
|
||||
if (inner) return inner;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
// `(set, get) => ({...})` — body is the (parenthesized) object directly.
|
||||
const direct = asObject(body);
|
||||
if (direct) return direct;
|
||||
// `(set, get) => { return {...} }` — scan top-level return statements.
|
||||
if (body.type === 'statement_block') {
|
||||
for (let i = 0; i < body.namedChildCount; i++) {
|
||||
const stmt = body.namedChild(i);
|
||||
if (stmt?.type !== 'return_statement') continue;
|
||||
for (let j = 0; j < stmt.namedChildCount; j++) {
|
||||
const obj = asObject(stmt.namedChild(j));
|
||||
if (obj) return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a variable declaration (const, let, var, etc.)
|
||||
*
|
||||
@@ -1162,29 +1258,44 @@ export class TreeSitterExtractor {
|
||||
this.extractVariableTypeAnnotation(child, varNode.id);
|
||||
}
|
||||
|
||||
// Exported const object-of-functions — extract each function-valued
|
||||
// property as a function named by its key + walk its body so its
|
||||
// calls are captured. Two shapes, both keyed on AST shape (not on any
|
||||
// library name):
|
||||
// `export const actions = { default: async () => {} }` — object is
|
||||
// the DIRECT value (SvelteKit form actions / handler maps / route
|
||||
// tables).
|
||||
// `export const useStore = create((set, get) => ({ fetchUser:
|
||||
// async () => {} }))` — object is RETURNED by an initializer call,
|
||||
// possibly through middleware wrappers (persist/devtools/immer).
|
||||
// Covers Zustand/Redux/Pinia/MobX stores generically. Without
|
||||
// this, store actions exist only as object-literal properties —
|
||||
// never nodes — so `node`/`callers` on `fetchUser` return "not
|
||||
// found" and the agent Reads the store to reconstruct the flow.
|
||||
// Scoped to EXPORTED consts to exclude inline-object noise
|
||||
// (`ctx.set({...})`) the object-method skip deliberately avoids.
|
||||
const objectOfFns =
|
||||
valueNode && (valueNode.type === 'object' || valueNode.type === 'object_expression')
|
||||
? valueNode
|
||||
: valueNode?.type === 'call_expression'
|
||||
? this.findInitializerReturnedObject(valueNode)
|
||||
: null;
|
||||
const extractObjectMethods = isExported && !!objectOfFns;
|
||||
|
||||
// Visit the initializer body for calls — EXCEPT object literals (their
|
||||
// function-valued properties are extracted below) and the store-factory
|
||||
// call whose returned object we extract method-by-method below (walking
|
||||
// the whole call would re-visit those method arrows and mis-attribute
|
||||
// their inner calls to the file/module scope).
|
||||
if (valueNode &&
|
||||
valueNode.type !== 'object' &&
|
||||
valueNode.type !== 'object_expression') {
|
||||
valueNode.type !== 'object_expression' &&
|
||||
!(extractObjectMethods && valueNode.type === 'call_expression')) {
|
||||
this.visitFunctionBody(valueNode, '');
|
||||
}
|
||||
|
||||
// Exported const object-of-functions: `export const actions =
|
||||
// { default: async () => {} }` (SvelteKit form actions / handler maps
|
||||
// / route tables). Extract each function-valued property as a function
|
||||
// named by its key + walk its body so its calls (e.g. api.post) are
|
||||
// captured. Scoped to EXPORTED consts to exclude the inline-object
|
||||
// noise (`ctx.set({...})`) the object-method skip deliberately avoids.
|
||||
if (isExported && valueNode &&
|
||||
(valueNode.type === 'object' || valueNode.type === 'object_expression')) {
|
||||
for (let j = 0; j < valueNode.namedChildCount; j++) {
|
||||
const pair = valueNode.namedChild(j);
|
||||
if (pair?.type !== 'pair') continue;
|
||||
const v = getChildByField(pair, 'value');
|
||||
const k = getChildByField(pair, 'key');
|
||||
if (k && v && (v.type === 'arrow_function' || v.type === 'function_expression')) {
|
||||
this.extractFunction(v, getNodeText(k, this.source).replace(/^['"`]|['"`]$/g, ''));
|
||||
}
|
||||
}
|
||||
if (extractObjectMethods && objectOfFns) {
|
||||
this.extractObjectLiteralFunctions(objectOfFns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user