* Compact "blast radius" for the entry symbols of an explore result: who * depends on each (callers) and which test files cover it — LOCATIONS ONLY, * no source, so the agent knows what to update / re-verify before editing * without reaching for a separate impact call. Always-on, but skips
(cg: CodeGraph, subgraph: Subgraph)
| 2438 | * qualify so a leaf-only exploration stays clean. |
| 2439 | */ |
| 2440 | private buildBlastRadiusSection(cg: CodeGraph, subgraph: Subgraph): string { |
| 2441 | const ROOT_CAP = 5; // only the symbols the query actually targeted |
| 2442 | const FILE_CAP = 4; // caller files listed per symbol before "+N more" |
| 2443 | const MEANINGFUL = new Set<string>([ |
| 2444 | 'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol', |
| 2445 | 'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field', |
| 2446 | ]); |
| 2447 | const rel = (p: string) => p.replace(/\\/g, '/'); |
| 2448 | |
| 2449 | const roots = subgraph.roots |
| 2450 | .map((id) => subgraph.nodes.get(id)) |
| 2451 | .filter((n): n is Node => !!n && MEANINGFUL.has(n.kind)) |
| 2452 | .slice(0, ROOT_CAP); |
| 2453 | if (roots.length === 0) return ''; |
| 2454 | |
| 2455 | const entries: string[] = []; |
| 2456 | for (const root of roots) { |
| 2457 | let callers: Array<{ node: Node }> = []; |
| 2458 | try { callers = cg.getCallers(root.id) as Array<{ node: Node }>; } catch { /* skip this root */ } |
| 2459 | |
| 2460 | const seen = new Set<string>(); |
| 2461 | const uniq: Node[] = []; |
| 2462 | for (const c of callers) { |
| 2463 | if (c?.node && !seen.has(c.node.id)) { seen.add(c.node.id); uniq.push(c.node); } |
| 2464 | } |
| 2465 | if (uniq.length === 0) continue; // no blast radius → nothing to flag |
| 2466 | |
| 2467 | const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))]; |
| 2468 | const testFiles = callerFiles.filter((f) => isTestFile(f)); |
| 2469 | const nonTest = callerFiles.filter((f) => !isTestFile(f)); |
| 2470 | |
| 2471 | const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', '); |
| 2472 | const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : ''; |
| 2473 | const where = nonTest.length > 0 ? ` in ${shown}${more}` : ''; |
| 2474 | const tests = testFiles.length > 0 |
| 2475 | ? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}` |
| 2476 | : this.indirectTestNote(cg, uniq, rel); |
| 2477 | |
| 2478 | entries.push( |
| 2479 | `- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`, |
| 2480 | ); |
| 2481 | } |
| 2482 | if (entries.length === 0) return ''; |
| 2483 | |
| 2484 | return [ |
| 2485 | '**Blast radius — what depends on these (update/verify before editing)**', |
| 2486 | '', |
| 2487 | ...entries, |
| 2488 | '', |
| 2489 | ].join('\n'); |
| 2490 | } |
| 2491 | |
| 2492 | /** |
| 2493 | * Test-coverage note for a blast-radius entry whose DIRECT callers include no |
no test coverage detected