fix: Increase explore traversal depth and support qualified symbol lookups
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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0d6f460b15
commit
b986b78fa9
@@ -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** |
|
| **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** | 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** |
|
| **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** |
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary><strong>Full benchmark details</strong></summary>
|
<summary><strong>Full benchmark details</strong></summary>
|
||||||
@@ -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 |
|
| Excalidraw (TypeScript) | 626 | 9,859 | 3 | 57.1k | 29s | 0 |
|
||||||
| Claude Code (Python+Rust) | 115 | 3,080 | 3 | 67.1k | 39s | 0 |
|
| Claude Code (Python+Rust) | 115 | 3,080 | 3 | 67.1k | 39s | 0 |
|
||||||
| Claude Code (Java) | — | — | 1 | 40.8k | 19s | 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:**
|
**Without CodeGraph — the agent uses grep, find, ls, and Read extensively:**
|
||||||
| Codebase | Tool Uses | Tokens | Time | File Reads |
|
| 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 |
|
| Excalidraw (TypeScript) | 47 | 77.9k | 1m 45s | ~20 |
|
||||||
| Claude Code (Python+Rust) | 40 | 69.3k | 1m 8s | ~15 |
|
| Claude Code (Python+Rust) | 40 | 69.3k | 1m 8s | ~15 |
|
||||||
| Claude Code (Java) | 26 | 73.3k | 1m 22s | ~15 |
|
| Claude Code (Java) | 26 | 73.3k | 1m 22s | ~15 |
|
||||||
|
| Alamofire (Swift) | 32 | 52.4k | 1m 39s | ~10 |
|
||||||
|
|
||||||
**Key observations:**
|
**Key observations:**
|
||||||
- With CodeGraph, the agent **never fell back to reading files** — it trusted the codegraph_explore results completely
|
- 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
|
- 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
|
- 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
|
- 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
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
|||||||
+28
-10
@@ -622,7 +622,7 @@ export class ToolHandler {
|
|||||||
// Step 1: Find relevant context with generous parameters
|
// Step 1: Find relevant context with generous parameters
|
||||||
const subgraph = await cg.findRelevantContext(query, {
|
const subgraph = await cg.findRelevantContext(query, {
|
||||||
searchLimit: 8,
|
searchLimit: 8,
|
||||||
traversalDepth: 2,
|
traversalDepth: 3,
|
||||||
maxNodes: 80,
|
maxNodes: 80,
|
||||||
minScore: 0.2,
|
minScore: 0.2,
|
||||||
});
|
});
|
||||||
@@ -1113,15 +1113,38 @@ export class ToolHandler {
|
|||||||
* Find a symbol by name, handling disambiguation when multiple matches exist.
|
* Find a symbol by name, handling disambiguation when multiple matches exist.
|
||||||
* Returns the best match and a note about alternatives if any.
|
* 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 {
|
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]) {
|
if (results.length === 0 || !results[0]) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If only one result, or first is an exact name match, use it directly
|
const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
|
||||||
const exactMatches = results.filter(r => r.node.name === symbol);
|
|
||||||
|
|
||||||
if (exactMatches.length === 1) {
|
if (exactMatches.length === 1) {
|
||||||
return { node: exactMatches[0]!.node, note: '' };
|
return { node: exactMatches[0]!.node, note: '' };
|
||||||
@@ -1152,12 +1175,7 @@ export class ToolHandler {
|
|||||||
return { nodes: [], note: '' };
|
return { nodes: [], note: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match by exact name, OR by file basename without extension
|
const exactMatches = results.filter(r => this.matchesSymbol(r.node, symbol));
|
||||||
// (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)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (exactMatches.length <= 1) {
|
if (exactMatches.length <= 1) {
|
||||||
const node = exactMatches[0]?.node ?? results[0]!.node;
|
const node = exactMatches[0]?.node ?? results[0]!.node;
|
||||||
|
|||||||
Reference in New Issue
Block a user