* 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)
| 2366 | * qualify so a leaf-only exploration stays clean. |
| 2367 | */ |
| 2368 | private buildBlastRadiusSection(cg: CodeGraph, subgraph: Subgraph): string { |
| 2369 | const ROOT_CAP = 5; // only the symbols the query actually targeted |
| 2370 | const FILE_CAP = 4; // caller files listed per symbol before "+N more" |
| 2371 | const MEANINGFUL = new Set<string>([ |
| 2372 | 'function', 'method', 'class', 'interface', 'struct', 'trait', 'protocol', |
| 2373 | 'enum', 'type_alias', 'component', 'constant', 'variable', 'property', 'field', |
| 2374 | ]); |
| 2375 | const rel = (p: string) => p.replace(/\\/g, '/'); |
| 2376 | |
| 2377 | const roots = subgraph.roots |
| 2378 | .map((id) => subgraph.nodes.get(id)) |
| 2379 | .filter((n): n is Node => !!n && MEANINGFUL.has(n.kind)) |
| 2380 | .slice(0, ROOT_CAP); |
| 2381 | if (roots.length === 0) return ''; |
| 2382 | |
| 2383 | const entries: string[] = []; |
| 2384 | for (const root of roots) { |
| 2385 | let callers: Array<{ node: Node }> = []; |
| 2386 | try { callers = cg.getCallers(root.id) as Array<{ node: Node }>; } catch { /* skip this root */ } |
| 2387 | |
| 2388 | const seen = new Set<string>(); |
| 2389 | const uniq: Node[] = []; |
| 2390 | for (const c of callers) { |
| 2391 | if (c?.node && !seen.has(c.node.id)) { seen.add(c.node.id); uniq.push(c.node); } |
| 2392 | } |
| 2393 | if (uniq.length === 0) continue; // no blast radius → nothing to flag |
| 2394 | |
| 2395 | const callerFiles = [...new Set(uniq.map((n) => rel(n.filePath)))]; |
| 2396 | const testFiles = callerFiles.filter((f) => isTestFile(f)); |
| 2397 | const nonTest = callerFiles.filter((f) => !isTestFile(f)); |
| 2398 | |
| 2399 | const shown = nonTest.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', '); |
| 2400 | const more = nonTest.length > FILE_CAP ? ` +${nonTest.length - FILE_CAP} more` : ''; |
| 2401 | const where = nonTest.length > 0 ? ` in ${shown}${more}` : ''; |
| 2402 | const tests = testFiles.length > 0 |
| 2403 | ? `; tests: ${testFiles.slice(0, FILE_CAP).map((f) => `\`${f}\``).join(', ')}${testFiles.length > FILE_CAP ? ` +${testFiles.length - FILE_CAP}` : ''}` |
| 2404 | : '; ⚠️ no covering tests found'; |
| 2405 | |
| 2406 | entries.push( |
| 2407 | `- \`${root.name}\` (${rel(root.filePath)}:${root.startLine}) — ${uniq.length} caller${uniq.length === 1 ? '' : 's'}${where}${tests}`, |
| 2408 | ); |
| 2409 | } |
| 2410 | if (entries.length === 0) return ''; |
| 2411 | |
| 2412 | return [ |
| 2413 | '**Blast radius — what depends on these (update/verify before editing)**', |
| 2414 | '', |
| 2415 | ...entries, |
| 2416 | '', |
| 2417 | ].join('\n'); |
| 2418 | } |
| 2419 | |
| 2420 | /** |
| 2421 | * Graph-connectivity relevance via Random-Walk-with-Restart (personalized |
no test coverage detected