* Handle codegraph_impact
(args: Record<string, unknown>)
| 3241 | * Handle codegraph_impact |
| 3242 | */ |
| 3243 | private async handleImpact(args: Record<string, unknown>): Promise<ToolResult> { |
| 3244 | const symbol = this.validateString(args.symbol, 'symbol'); |
| 3245 | if (typeof symbol !== 'string') return symbol; |
| 3246 | |
| 3247 | const cg = this.getCodeGraph(args.projectPath as string | undefined); |
| 3248 | const depth = clamp((args.depth as number) || 2, 1, 10); |
| 3249 | const fileFilter = typeof args.file === 'string' ? args.file : undefined; |
| 3250 | |
| 3251 | const allMatches = this.findAllSymbols(cg, symbol); |
| 3252 | if (allMatches.nodes.length === 0) { |
| 3253 | return this.textResult(`Symbol "${symbol}" not found in the codebase${allMatches.note}`); |
| 3254 | } |
| 3255 | |
| 3256 | const { groups, filteredOut } = this.groupDefinitions(allMatches.nodes, fileFilter); |
| 3257 | const answerPaths = new Set(groups.flat().map(node => node.filePath)); |
| 3258 | const filterNote = filteredOut |
| 3259 | ? `\n\n> **Note:** no definition of "${symbol}" matches file "${fileFilter}" — showing all definitions instead.` |
| 3260 | : ''; |
| 3261 | |
| 3262 | const impactOf = (defNodes: Node[]) => { |
| 3263 | const mergedNodes = new Map<string, Node>(); |
| 3264 | const mergedEdges: Edge[] = []; |
| 3265 | const seenEdges = new Set<string>(); |
| 3266 | for (const node of defNodes) { |
| 3267 | const impact = cg.getImpactRadius(node.id, depth); |
| 3268 | for (const [id, n] of impact.nodes) { |
| 3269 | mergedNodes.set(id, n); |
| 3270 | answerPaths.add(n.filePath); |
| 3271 | } |
| 3272 | for (const e of impact.edges) { |
| 3273 | const key = `${e.source}->${e.target}:${e.kind}`; |
| 3274 | if (!seenEdges.has(key)) { |
| 3275 | seenEdges.add(key); |
| 3276 | mergedEdges.push(e); |
| 3277 | } |
| 3278 | } |
| 3279 | } |
| 3280 | return { nodes: mergedNodes, edges: mergedEdges, roots: defNodes.map((n) => n.id) }; |
| 3281 | }; |
| 3282 | |
| 3283 | // Single definition (or same-file overloads): the familiar merged report. |
| 3284 | if (groups.length === 1) { |
| 3285 | const formatted = this.formatImpact(symbol, impactOf(groups[0]!)) + (fileFilter && !filteredOut ? "" : allMatches.note) + filterNote; |
| 3286 | return this.answerResult(cg, this.truncateOutput(formatted), answerPaths); |
| 3287 | } |
| 3288 | |
| 3289 | // Multiple DISTINCT definitions (#764): a blast radius PER definition — |
| 3290 | // merging unrelated same-named classes (one UserService per monorepo app) |
| 3291 | // overstated impact and confused agents. Narrow with `file`. |
| 3292 | const sections: string[] = [ |
| 3293 | `**Impact of ${symbol} — ${groups.length} distinct definitions (each with its own blast radius; narrow with \`file\`)**`, |
| 3294 | ]; |
| 3295 | for (const group of groups) { |
| 3296 | const head = group[0]!; |
| 3297 | const line = head.startLine ? `:${head.startLine}` : ''; |
| 3298 | sections.push( |
| 3299 | '', |
| 3300 | this.formatImpact(`${head.qualifiedName} (${head.filePath}${line})`, impactOf(group)) |
no test coverage detected