From b986b78fa9c0dca8317a766139a7e0e035f81a87 Mon Sep 17 00:00:00 2001 From: Colby McHenry Date: Fri, 3 Apr 2026 19:56:58 -0500 Subject: [PATCH] fix: Increase explore traversal depth and support qualified symbol lookups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two fixes discovered while benchmarking Swift (Alamofire): 1. codegraph_explore traversalDepth 2→3: Deep call chains (e.g., Alamofire's 9-step Session.request()→URLSession flow) couldn't be followed in a single explore call, forcing agents to fall back to file reads. 2. findSymbol/findAllSymbols now support "Parent.child" notation (e.g., "Session.request") by matching against qualified names (::Parent::child). Previously only checked node.name === symbol, which never matched qualified queries since node names are unqualified. Also adds Alamofire Swift benchmark data to README (91% fewer tool calls, 78% faster with CodeGraph). Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 4 ++++ src/mcp/tools.ts | 38 ++++++++++++++++++++++++++++---------- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 717463c..c0d72c2 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ We tested the same exploration queries across 4 real-world codebases in differen | **Excalidraw** | TypeScript | "How does collaborative editing and real-time sync work?" | 3 calls, 29s | 47 calls, 1m 45s | **94% fewer** | **72% faster** | | **Claude Code** | Python + Rust | "How does tool execution work end to end?" | 3 calls, 39s | 40 calls, 1m 8s | **93% fewer** | **43% faster** | | **Claude Code** | Java | "How does tool execution work end to end?" | 1 call, 19s | 26 calls, 1m 22s | **96% fewer** | **77% faster** | +| **Alamofire** | Swift | "Trace how a request flows from Session.request() through to the URLSession layer" | 3 calls, 22s | 32 calls, 1m 39s | **91% fewer** | **78% faster** |
Full benchmark details @@ -57,6 +58,7 @@ All tests used Claude Opus 4.6 (1M context) with Claude Code v2.1.91. Each test | Excalidraw (TypeScript) | 626 | 9,859 | 3 | 57.1k | 29s | 0 | | Claude Code (Python+Rust) | 115 | 3,080 | 3 | 67.1k | 39s | 0 | | Claude Code (Java) | — | — | 1 | 40.8k | 19s | 0 | +| Alamofire (Swift) | 102 | 2,624 | 3 | 57.3k | 22s | 0 | **Without CodeGraph — the agent uses grep, find, ls, and Read extensively:** | Codebase | Tool Uses | Tokens | Time | File Reads | @@ -65,12 +67,14 @@ All tests used Claude Opus 4.6 (1M context) with Claude Code v2.1.91. Each test | Excalidraw (TypeScript) | 47 | 77.9k | 1m 45s | ~20 | | Claude Code (Python+Rust) | 40 | 69.3k | 1m 8s | ~15 | | Claude Code (Java) | 26 | 73.3k | 1m 22s | ~15 | +| Alamofire (Swift) | 32 | 52.4k | 1m 39s | ~10 | **Key observations:** - With CodeGraph, the agent **never fell back to reading files** — it trusted the codegraph_explore results completely - Without CodeGraph, agents spent most of their time on discovery (find, ls, grep) before they could even start reading relevant code - The Java codebase needed only **1 codegraph_explore call** to answer the entire question - Cross-language queries (Python+Rust) worked seamlessly — CodeGraph's graph traversal found connections across language boundaries +- The Swift benchmark (Alamofire) traced a **9-step call chain** from `Session.request()` to `URLSession.dataTask()` — CodeGraph's graph traversal at depth 3 captured the full chain in one explore call
diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index f346f0d..aebfe97 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -622,7 +622,7 @@ export class ToolHandler { // Step 1: Find relevant context with generous parameters const subgraph = await cg.findRelevantContext(query, { searchLimit: 8, - traversalDepth: 2, + traversalDepth: 3, maxNodes: 80, minScore: 0.2, }); @@ -1113,15 +1113,38 @@ export class ToolHandler { * Find a symbol by name, handling disambiguation when multiple matches exist. * Returns the best match and a note about alternatives if any. */ + /** + * Check if a node matches a symbol query, supporting both simple names and + * qualified "Parent.child" notation (e.g., "Session.request" matches a method + * named "request" inside a class named "Session"). + */ + private matchesSymbol(node: Node, symbol: string): boolean { + // Simple name match + if (node.name === symbol) return true; + // File basename match (e.g., "product-card" matches "product-card.liquid") + if (node.kind === 'file' && node.name.replace(/\.[^.]+$/, '') === symbol) return true; + + // Qualified name match: "Parent.child" → look for "::Parent::child" in qualified_name + if (symbol.includes('.')) { + const parts = symbol.split('.'); + const qualifiedSuffix = parts.join('::'); + if (node.qualifiedName.includes(qualifiedSuffix)) return true; + } + + return false; + } + private findSymbol(cg: CodeGraph, symbol: string): { node: Node; note: string } | null { - const results = cg.searchNodes(symbol, { limit: 10 }); + // Use higher limit for qualified lookups (e.g., "Session.request") since the + // target may rank lower in FTS when there are many partial matches + const limit = symbol.includes('.') ? 50 : 10; + const results = cg.searchNodes(symbol, { limit }); if (results.length === 0 || !results[0]) { return null; } - // If only one result, or first is an exact name match, use it directly - const exactMatches = results.filter(r => r.node.name === symbol); + const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol)); if (exactMatches.length === 1) { return { node: exactMatches[0]!.node, note: '' }; @@ -1152,12 +1175,7 @@ export class ToolHandler { return { nodes: [], note: '' }; } - // Match by exact name, OR by file basename without extension - // (e.g., "product-card" matches file node named "product-card.liquid") - const exactMatches = results.filter(r => - r.node.name === symbol || - (r.node.kind === 'file' && r.node.name.replace(/\.[^.]+$/, '') === symbol) - ); + const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol)); if (exactMatches.length <= 1) { const node = exactMatches[0]?.node ?? results[0]!.node;