From b913a611013a8f96172e31e43c7c5e88810d6306 Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 20:33:08 -0600 Subject: [PATCH] explore: batch callerTests' hop 2-3 caller lookups getCallersMany fetches depth-1 callers for a whole frontier in one incoming-edges query + one getNodesByIds instead of a getCallers round-trip per node; the same 64-node budget bounds each hop. --- src/codegraph.ts | 8 ++++++++ src/graph/traversal.ts | 23 +++++++++++++++++++++++ src/mcp/tools.ts | 23 ++++++++++++----------- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/codegraph.ts b/src/codegraph.ts index d7e7cb129..1dc4a46e9 100644 --- a/src/codegraph.ts +++ b/src/codegraph.ts @@ -2823,6 +2823,14 @@ export class CodeGraph { return this.traverser.getCallers(nodeId, maxDepth); } + /** + * Depth-1 callers for many targets in two queries — the batch form of + * getCallers(id, 1), for bounded fan-in walks like callerTests. + */ + getCallersMany(nodeIds: readonly string[]): Array<{ node: Node; edge: Edge }> { + return this.traverser.getCallersMany(nodeIds); + } + /** * Get callees of a function/method * diff --git a/src/graph/traversal.ts b/src/graph/traversal.ts index b0fa20aa0..672d9ef1a 100644 --- a/src/graph/traversal.ts +++ b/src/graph/traversal.ts @@ -294,6 +294,29 @@ export class GraphTraverser { return result; } + /** + * Depth-1 callers for MANY targets in two round-trips — one incoming-edges + * query + one getNodesByIds — instead of a getCallers(id, 1) pair per id. + * Same kind list as getCallersRecursive; a caller repeats once per target it + * calls, matching separate per-node calls, and once per target at most. + */ + getCallersMany(nodeIds: readonly string[]): Array<{ node: Node; edge: Edge }> { + if (nodeIds.length === 0) return []; + const incoming = this.queries.getIncomingEdgesTo(nodeIds, ['calls', 'references', 'imports', 'instantiates', 'navigates']); + const nodes = this.queries.getNodesByIds(incoming.map((e) => e.source)); + const reported = new Set(); + const out: Array<{ node: Node; edge: Edge }> = []; + for (const edge of incoming) { + const node = nodes.get(edge.source); + if (!node) continue; + const key = `${edge.target}${edge.source}`; + if (reported.has(key)) continue; + reported.add(key); + out.push({ node, edge }); + } + return out; + } + private getCallersRecursive( nodeId: string, maxDepth: number, diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index dc71e7085..323afcc67 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -4559,17 +4559,18 @@ export class ToolHandler { for (let hop = 2; hop <= MAX_HOPS && frontier.length > 0 && budget > 0; hop++) { const next: Array<{ node: Node; edge: Edge }> = []; const found: Array<{ node: Node; edge: Edge }> = []; - for (const { node } of frontier) { - if (budget-- <= 0) break; - let callers: Array<{ node: Node; edge: Edge }> = []; - try { callers = cg.getCallers(node.id); } catch { continue; } - for (const c of callers) { - const n = c?.node; - if (!n || visited.has(n.id)) continue; - visited.add(n.id); - if (isTestPath(n.filePath)) found.push(c); - else next.push(c); - } + // One batched depth-1 callers query per hop instead of a getCallers + // round-trip per frontier node; the same 64-node budget bounds it. + const ids = frontier.slice(0, budget).map((f) => f.node.id); + budget -= ids.length; + let pairs: Array<{ node: Node; edge: Edge }> = []; + try { pairs = cg.getCallersMany(ids); } catch { pairs = []; } + for (const c of pairs) { + const n = c?.node; + if (!n || visited.has(n.id)) continue; + visited.add(n.id); + if (isTestPath(n.filePath)) found.push(c); + else next.push(c); } if (found.length > 0) return { callers: found, exhausted: budget <= 0 }; frontier = next;