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:
+1
-47
@@ -1073,52 +1073,6 @@ function printFileTree(
|
||||
renderNode(root, '', true, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* codegraph context <task>
|
||||
*/
|
||||
program
|
||||
.command('context <task>')
|
||||
.description('Build context for a task (outputs markdown)')
|
||||
.option('-p, --path <path>', 'Project path')
|
||||
.option('-n, --max-nodes <number>', 'Maximum nodes to include', '50')
|
||||
.option('-c, --max-code <number>', 'Maximum code blocks', '10')
|
||||
.option('--no-code', 'Exclude code blocks')
|
||||
.option('-f, --format <format>', 'Output format (markdown, json)', 'markdown')
|
||||
.action(async (task: string, options: {
|
||||
path?: string;
|
||||
maxNodes?: string;
|
||||
maxCode?: string;
|
||||
code?: boolean;
|
||||
format?: string;
|
||||
}) => {
|
||||
const projectPath = resolveProjectPath(options.path);
|
||||
|
||||
try {
|
||||
if (!isInitialized(projectPath)) {
|
||||
error(`CodeGraph not initialized in ${projectPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const { default: CodeGraph } = await loadCodeGraph();
|
||||
const cg = await CodeGraph.open(projectPath);
|
||||
|
||||
const context = await cg.buildContext(task, {
|
||||
maxNodes: parseInt(options.maxNodes || '50', 10),
|
||||
maxCodeBlocks: parseInt(options.maxCode || '10', 10),
|
||||
includeCode: options.code !== false,
|
||||
format: options.format as 'markdown' | 'json',
|
||||
});
|
||||
|
||||
// Output the context
|
||||
console.log(context);
|
||||
|
||||
cg.destroy();
|
||||
} catch (err) {
|
||||
error(`Failed to build context: ${err instanceof Error ? err.message : String(err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* codegraph serve
|
||||
*/
|
||||
@@ -1161,8 +1115,8 @@ program
|
||||
}
|
||||
`));
|
||||
console.error('Available tools:');
|
||||
console.error(chalk.cyan(' codegraph_explore') + ' - Primary: source of the relevant symbols for any question');
|
||||
console.error(chalk.cyan(' codegraph_search') + ' - Search for code symbols');
|
||||
console.error(chalk.cyan(' codegraph_context') + ' - Build context for a task');
|
||||
console.error(chalk.cyan(' codegraph_callers') + ' - Find callers of a symbol');
|
||||
console.error(chalk.cyan(' codegraph_callees') + ' - Find what a symbol calls');
|
||||
console.error(chalk.cyan(' codegraph_impact') + ' - Analyze impact of changes');
|
||||
|
||||
+95
-6
@@ -25,7 +25,8 @@ import { GraphTraverser } from '../graph';
|
||||
import { formatContextAsMarkdown, formatContextAsJson } from './formatter';
|
||||
import { logDebug } from '../errors';
|
||||
import { validatePathWithinRoot } from '../utils';
|
||||
import { isTestFile, extractSearchTerms, scorePathRelevance, getStemVariants } from '../search/query-utils';
|
||||
import { isTestFile, extractSearchTerms, scorePathRelevance, getStemVariants, isDistinctiveIdentifier } from '../search/query-utils';
|
||||
import { LOW_CONFIDENCE_MARKER } from './markers';
|
||||
|
||||
/**
|
||||
* Extract likely symbol names from a natural language query
|
||||
@@ -172,6 +173,11 @@ const DEFAULT_FIND_OPTIONS: Required<FindRelevantContextOptions> = {
|
||||
nodeKinds: HIGH_VALUE_NODE_KINDS, // Filter out imports/exports by default
|
||||
};
|
||||
|
||||
// Re-export the low-confidence sentinel (defined in a dependency-free leaf so
|
||||
// the MCP layer can import it without pulling this module's deps onto the
|
||||
// cold-start path). Builder code below uses the imported binding directly.
|
||||
export { LOW_CONFIDENCE_MARKER } from './markers';
|
||||
|
||||
/**
|
||||
* Context Builder
|
||||
*
|
||||
@@ -259,7 +265,9 @@ export class ContextBuilder {
|
||||
|
||||
// Return formatted output or raw context
|
||||
if (opts.format === 'markdown') {
|
||||
return formatContextAsMarkdown(context) + this.buildCallPathsSection(subgraph);
|
||||
return formatContextAsMarkdown(context)
|
||||
+ this.buildCallPathsSection(subgraph)
|
||||
+ (subgraph.confidence === 'low' ? this.buildLowConfidenceNote(entryPoints) : '');
|
||||
} else if (opts.format === 'json') {
|
||||
return formatContextAsJson(context);
|
||||
}
|
||||
@@ -267,6 +275,36 @@ export class ContextBuilder {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Honest handoff appended when retrieval confidence is low (the query matched
|
||||
* mostly common words). Instead of the usual "this covers the surface" framing
|
||||
* — which, when wrong, sends the agent off to Read/Grep — it admits the
|
||||
* uncertainty and routes the agent to the precise tools (explore with real
|
||||
* symbol names, search, or files to browse the closest areas we *did* surface).
|
||||
*/
|
||||
private buildLowConfidenceNote(entryPoints: Node[]): string {
|
||||
const dirs: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
for (const n of entryPoints) {
|
||||
const slash = n.filePath.lastIndexOf('/');
|
||||
const dir = slash > 0 ? n.filePath.slice(0, slash) : n.filePath;
|
||||
if (!seen.has(dir)) { seen.add(dir); dirs.push(dir); }
|
||||
if (dirs.length >= 4) break;
|
||||
}
|
||||
const dirLine = dirs.length
|
||||
? `\n- \`codegraph_files\` a likely area: ${dirs.map(d => `\`${d}\``).join(', ')}`
|
||||
: '';
|
||||
return `\n\n${LOW_CONFIDENCE_MARKER}\n\n`
|
||||
+ 'This query matched mostly on common words, so the entry points above may '
|
||||
+ 'be off-target — treat them as a starting point, not a complete answer. '
|
||||
+ 'For a reliable result:\n'
|
||||
+ '- `codegraph_explore` with the **exact symbol names** you are after '
|
||||
+ '(class / function / method names), or\n'
|
||||
+ '- `codegraph_search <name>` for one specific symbol'
|
||||
+ dirLine
|
||||
+ '\n\nDo not assume the list above is comprehensive.';
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface short call-paths among the symbols this context already found,
|
||||
* derived in-memory from the subgraph's `calls` edges (no extra queries).
|
||||
@@ -653,6 +691,21 @@ export class ContextBuilder {
|
||||
// term group is counter-productive.
|
||||
const exactMatchIds = new Set(exactMatches.map(r => r.node.id));
|
||||
|
||||
// ...but only exempt exact matches the user *named as an identifier*
|
||||
// (camelCase/snake_case/acronym). A plain dictionary word that happens to
|
||||
// exact-match an unrelated symbol — query "flat object" → a constant named
|
||||
// FLAT — must NOT be exempt, or the +exact-name bonus floats it to the top
|
||||
// of a prose query with zero corroboration from any other term. Classify by
|
||||
// the QUERY token (what the user typed), not the matched symbol's name.
|
||||
const distinctiveTokens = new Set(
|
||||
symbolsFromQuery.filter(isDistinctiveIdentifier).map(s => s.toLowerCase())
|
||||
);
|
||||
const distinctiveExactMatchIds = new Set(
|
||||
exactMatches
|
||||
.filter(r => distinctiveTokens.has(r.node.name.toLowerCase()))
|
||||
.map(r => r.node.id)
|
||||
);
|
||||
|
||||
for (const result of searchResults) {
|
||||
// Check term matches in name (substring) and path DIRECTORIES (exact).
|
||||
// Directory segments must match exactly — "search" matches directory
|
||||
@@ -672,10 +725,17 @@ export class ContextBuilder {
|
||||
if (matchCount >= 2) {
|
||||
// Multiplicative boost — 2 terms → 2x, 3 terms → 2.5x
|
||||
result.score *= 1 + matchCount * 0.5;
|
||||
} else if (!exactMatchIds.has(result.node.id)) {
|
||||
// Mild dampen for single-term matches — they might be generic
|
||||
} else if (distinctiveExactMatchIds.has(result.node.id)) {
|
||||
// Exact match on a distinctive identifier the user explicitly named —
|
||||
// keep full score (e.g. "LiveEditMode DevServerPreview").
|
||||
} else if (exactMatchIds.has(result.node.id)) {
|
||||
// Exact match on a COMMON word (e.g. "flat" → FLAT): high-scoring noise
|
||||
// inflated by the +exact-name bonus, corroborated by no other query
|
||||
// term. Demote hard so corroborated matches win.
|
||||
result.score *= 0.3;
|
||||
} else {
|
||||
// Mild dampen for generic single-term matches — they might be generic
|
||||
// but could also be the right result (e.g., "Protocol" class for an IPC query).
|
||||
// Exempt exact name matches: they are specific symbols the user queried for.
|
||||
result.score *= 0.6;
|
||||
}
|
||||
}
|
||||
@@ -841,6 +901,35 @@ export class ContextBuilder {
|
||||
filteredResults = filteredResults.slice(0, opts.searchLimit);
|
||||
}
|
||||
|
||||
// Confidence signal for the honest-handoff footer (consumed in buildContext).
|
||||
// A multi-term prose query that resolves only to isolated common-word matches
|
||||
// — no entry point corroborated by 2+ distinct query terms, and none a
|
||||
// distinctive identifier the user explicitly named — is LOW confidence: the
|
||||
// results are best-effort, not a located answer, so the agent should be told
|
||||
// to drill in with explore/trace rather than trust the list as comprehensive.
|
||||
// Single-keyword and symbol-name queries are exempt (their single match IS the
|
||||
// answer), so the handoff never fires on them.
|
||||
let confidence: 'high' | 'low' = 'high';
|
||||
const confTerms = extractSearchTerms(query, { stems: false }).filter(t => t.length >= 3);
|
||||
if (confTerms.length >= 2 && filteredResults.length > 0) {
|
||||
const distinctive = new Set(
|
||||
symbolsFromQuery.filter(isDistinctiveIdentifier).map(s => s.toLowerCase())
|
||||
);
|
||||
const anyStrong = filteredResults.some(r => {
|
||||
if (distinctive.has(r.node.name.toLowerCase())) return true;
|
||||
const nameLower = r.node.name.toLowerCase();
|
||||
const dirSegs = path.dirname(r.node.filePath).toLowerCase().split('/');
|
||||
let hits = 0;
|
||||
for (const t of confTerms) {
|
||||
if (nameLower.includes(t) || dirSegs.includes(t)) {
|
||||
if (++hits >= 2) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (!anyStrong) confidence = 'low';
|
||||
}
|
||||
|
||||
// Add entry points to subgraph
|
||||
for (const result of filteredResults) {
|
||||
nodes.set(result.node.id, result.node);
|
||||
@@ -1048,7 +1137,7 @@ export class ContextBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
return { nodes: finalNodes, edges: finalEdges, roots };
|
||||
return { nodes: finalNodes, edges: finalEdges, roots, confidence };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Stable sentinel strings shared between the context builder (which emits them
|
||||
* into its markdown) and the MCP layer (which detects them to adjust framing).
|
||||
*
|
||||
* Intentionally a dependency-free leaf module: the MCP tool layer imports this
|
||||
* to recognise a low-confidence response, and routing that recognition through
|
||||
* the full context module would drag its dependencies onto the cold-start path.
|
||||
* Keep this file import-free.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Heading that leads the honest low-confidence handoff appended to a context
|
||||
* response when the query resolved only to weak/isolated matches. The MCP layer
|
||||
* checks for it to suppress the contradictory "this is comprehensive, don't call
|
||||
* explore" small-repo footer. Changing the text is a breaking sentinel change —
|
||||
* both the emitter (`ContextBuilder`) and the detector (`src/mcp/tools.ts`)
|
||||
* import this constant, so they stay in sync automatically.
|
||||
*/
|
||||
export const LOW_CONFIDENCE_MARKER = '### ⚠️ Low-confidence match';
|
||||
+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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -681,6 +681,15 @@ export class CodeGraph {
|
||||
return this.queries.getNodesByKind(kind);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ALL nodes with an exact name (direct index lookup, not FTS-ranked/capped).
|
||||
* Used to enumerate every overload of a heavily-overloaded name so the specific
|
||||
* definition the caller wants is never dropped below a search cut.
|
||||
*/
|
||||
getNodesByName(name: string): Node[] {
|
||||
return this.queries.getNodesByName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search nodes by text
|
||||
*/
|
||||
@@ -692,7 +701,7 @@ export class CodeGraph {
|
||||
* Find the project's "primary route file" — the file with the densest
|
||||
* concentration of framework-emitted `route` nodes (≥3 routes, ≥30%
|
||||
* of all non-test routes). Used to inline the routing config in
|
||||
* `codegraph_context` responses on small realworld template repos
|
||||
* `codegraph_explore` responses on small realworld template repos
|
||||
* (rails-realworld, laravel-realworld, drupal-admintoolbar, …) where
|
||||
* Glob+Read of `routes.rb`/`urls.py`/etc. otherwise beats codegraph.
|
||||
*/
|
||||
|
||||
@@ -31,12 +31,13 @@ export function getMcpServerConfig(): { type: string; command: string; args: str
|
||||
*/
|
||||
export function getCodeGraphPermissions(): string[] {
|
||||
return [
|
||||
'mcp__codegraph__codegraph_explore',
|
||||
'mcp__codegraph__codegraph_search',
|
||||
'mcp__codegraph__codegraph_context',
|
||||
'mcp__codegraph__codegraph_node',
|
||||
'mcp__codegraph__codegraph_callers',
|
||||
'mcp__codegraph__codegraph_callees',
|
||||
'mcp__codegraph__codegraph_impact',
|
||||
'mcp__codegraph__codegraph_node',
|
||||
'mcp__codegraph__codegraph_files',
|
||||
'mcp__codegraph__codegraph_status',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -25,32 +25,31 @@ editing code, not during.
|
||||
## Answer directly — don't delegate exploration
|
||||
|
||||
For "how does X work", architecture, trace, or where-is-X questions,
|
||||
answer DIRECTLY using 2-3 codegraph calls: \`codegraph_context\` first,
|
||||
then ONE \`codegraph_explore\` for the source of the symbols it surfaces.
|
||||
Codegraph IS the pre-built search index — so delegating the lookup to a
|
||||
separate file-reading sub-task/agent, or running your own grep + read
|
||||
loop, repeats work codegraph already did and costs more for the same
|
||||
answer. Reach for raw Read/Grep only to confirm a specific detail
|
||||
codegraph didn't cover. A direct codegraph answer is typically a handful
|
||||
of calls; a grep/read exploration is dozens.
|
||||
answer DIRECTLY — usually with ONE \`codegraph_explore\` call.
|
||||
\`codegraph_explore\` takes either a natural-language question or a bag of
|
||||
symbol/file names and returns the verbatim source of the relevant symbols
|
||||
grouped by file, so it is Read-equivalent and most often the ONLY
|
||||
codegraph call you need. Codegraph IS the pre-built search index — so
|
||||
delegating the lookup to a separate file-reading sub-task/agent, or
|
||||
running your own grep + read loop, repeats work codegraph already did and
|
||||
costs more for the same answer. Reach for raw Read/Grep only to confirm a
|
||||
specific detail codegraph didn't cover. A direct codegraph answer is
|
||||
typically one to a few calls; a grep/read exploration is dozens.
|
||||
|
||||
## Tool selection by intent
|
||||
|
||||
- **"What is the symbol named X?"** → \`codegraph_search\`
|
||||
- **"What's the deal with this task / feature / area?"** → \`codegraph_context\` (PRIMARY — composes search + node + callers + callees in one call)
|
||||
- **"How does X reach/become Y? / trace the flow / the path from X to Y"** → \`codegraph_trace\` (ONE call returns the whole call path, including dynamic-dispatch hops — callbacks, React re-render, JSX children — that grep can't follow)
|
||||
- **"What calls this?"** → \`codegraph_callers\`
|
||||
- **"What does this call?"** → \`codegraph_callees\`
|
||||
- **"What would changing this break?"** → \`codegraph_impact\`
|
||||
- **"Show me this symbol's source / signature / docstring."** → \`codegraph_node\`
|
||||
- **"Show me several related symbols' source / survey an area."** → \`codegraph_explore\` (ONE capped call; prefer over many codegraph_node/Read)
|
||||
- **Almost any question — "how does X work", architecture, a bug, "what/where is X", or surveying an area** → \`codegraph_explore\` (PRIMARY — call FIRST; ONE capped call returns the verbatim source of the relevant symbols grouped by file; most often the ONLY call you need)
|
||||
- **"How does X reach/become Y? / the flow / the path from X to Y"** → \`codegraph_explore\`, naming the symbols that span the flow (e.g. \`mutateElement renderScene\`) — it surfaces the call path among them, including dynamic-dispatch hops (callbacks, React re-render, JSX children) grep can't follow
|
||||
- **"What is the symbol named X?" (just its location)** → \`codegraph_search\`
|
||||
- **"What calls this?" / "What does this call?" / "What would changing this break?"** → \`codegraph_callers\` / \`codegraph_callees\` / \`codegraph_impact\`
|
||||
- **One specific symbol's full source (esp. a body \`codegraph_explore\` trimmed), or an OVERLOADED name** → \`codegraph_node\` (with \`includeCode\`): for an ambiguous name it returns EVERY matching definition's body in one call, so you never Read a file to find the right overload
|
||||
- **"What's in directory X?"** → \`codegraph_files\`
|
||||
- **"Is the index ready / what's its size?"** → \`codegraph_status\`
|
||||
|
||||
## Common chains
|
||||
|
||||
- **Flow / "how does X reach Y"**: \`codegraph_trace\` from→to FIRST — one call returns the entire path with dynamic-dispatch hops bridged. Then ONE \`codegraph_explore\` for the hop bodies if you need them. Do NOT reconstruct the path with \`codegraph_search\` + \`codegraph_callers\` — that's exactly what trace does in a single call.
|
||||
- **Onboarding**: \`codegraph_context\` first. If still unclear, \`codegraph_explore\` for breadth, then \`codegraph_node\` on specific symbols.
|
||||
- **Flow / "how does X reach Y"**: ONE \`codegraph_explore\` with the symbol names spanning the flow — it surfaces the call path among them (riding dynamic-dispatch hops) AND returns their source. No need to reconstruct the path with \`codegraph_search\` + \`codegraph_callers\`.
|
||||
- **Onboarding / understanding any area**: ONE \`codegraph_explore\` is usually the whole answer. Only follow up — \`codegraph_node\` for a specific symbol — if something is still unclear.
|
||||
- **Refactor planning**: \`codegraph_search\` → \`codegraph_callers\` → \`codegraph_impact\`. The blast-radius answer comes from impact, not from walking callers manually.
|
||||
- **Debugging a regression**: \`codegraph_callers\` of the suspected symbol; widen with \`codegraph_impact\` if an unexpected call appears.
|
||||
|
||||
@@ -58,7 +57,7 @@ of calls; a grep/read exploration is dozens.
|
||||
|
||||
- **Trust codegraph's results — don't re-verify them with grep.** They come from a full AST parse; re-checking with grep is slower, less accurate, and wastes context.
|
||||
- **Don't grep first** when looking up a symbol by name — \`codegraph_search\` is faster and returns kind + location + signature.
|
||||
- **Don't chain \`codegraph_search\` + \`codegraph_node\`** when you just want context — \`codegraph_context\` is one round-trip.
|
||||
- **Don't chain \`codegraph_search\` + \`codegraph_node\`** to understand an area — ONE \`codegraph_explore\` returns the relevant symbols' source together in a single round-trip.
|
||||
- **Don't loop \`codegraph_node\` over many symbols** — one \`codegraph_explore\` call returns them all grouped by file, while each separate call re-reads the whole context and costs far more. Use \`codegraph_node\` for a single symbol.
|
||||
- **After editing, check the staleness banner.** When a tool response starts with "⚠️ Some files referenced below were edited since the last index sync…", the listed files are pending re-index — Read those specific files for accurate content. Every file NOT in that banner is fresh, so still trust codegraph. \`codegraph_status\` also lists pending files under "Pending sync".
|
||||
|
||||
|
||||
+547
-867
File diff suppressed because it is too large
Load Diff
@@ -340,3 +340,30 @@ export function kindBonus(kind: Node['kind']): number {
|
||||
};
|
||||
return bonuses[kind] ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a query token looks like a code identifier the user deliberately typed
|
||||
* (camelCase / PascalCase-with-internal-caps / snake_case / has a digit) rather
|
||||
* than a plain dictionary word ("flat", "object", "screen").
|
||||
*
|
||||
* Used to decide whether an EXACT name match earns the "the user named this
|
||||
* symbol" exemption from single-term dampening. A common English word that
|
||||
* happens to exact-match an unrelated symbol — the query "flat object" matching
|
||||
* a constant named `FLAT` — must NOT get that exemption, or the +exact-name
|
||||
* bonus floats it to the top of a prose query on its own.
|
||||
*
|
||||
* Classifies the token AS THE USER TYPED IT, not the matched symbol's name:
|
||||
* "flat" (lowercase, descriptive) is non-distinctive even though it matches
|
||||
* `FLAT`. A leading-capital-only word ("Screen", "Zustand") is also treated as
|
||||
* a plain word — sentence-start capitalization and proper nouns aren't reliable
|
||||
* identifier signals.
|
||||
*/
|
||||
export function isDistinctiveIdentifier(token: string): boolean {
|
||||
if (!token) return false;
|
||||
// snake_case / SCREAMING_SNAKE, or an embedded digit → a deliberate identifier.
|
||||
if (/[_0-9]/.test(token)) return true;
|
||||
// An uppercase letter anywhere AFTER the first char → a camelCase/PascalCase
|
||||
// boundary (setLastEmail, OrgUserStore) or an acronym (REST, HTTP).
|
||||
if (/[A-Z]/.test(token.slice(1))) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -311,6 +311,15 @@ export interface Subgraph {
|
||||
|
||||
/** Root node IDs (entry points) */
|
||||
roots: string[];
|
||||
|
||||
/**
|
||||
* Retrieval confidence for context-style queries. `'low'` means the query
|
||||
* resolved only to isolated common-word matches (no entry point corroborated
|
||||
* by 2+ distinct query terms) — callers should surface an honest handoff to
|
||||
* explore/trace rather than present the results as comprehensive. Undefined
|
||||
* for graph traversals that don't run the search-ranking path.
|
||||
*/
|
||||
confidence?: 'high' | 'low';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user