* Shortlist candidate runtime targets for a dispatch key surfaced by * buildDynamicBoundaries. Exact conventional names first (`save` → * `onSave`/`handleSave`; `CreateCmd` → `CreateCmdHandler`), then FTS, with a * normalized-containment post-filter (FTS camel-splitting is fuzzier t
(cg: CodeGraph, key: string, keyIsType: boolean, named: Map<string, Node>, selfId: string)
| 2371 | * are marked — that's the "you were right, here's the wiring" case. |
| 2372 | */ |
| 2373 | private boundaryCandidates(cg: CodeGraph, key: string, keyIsType: boolean, named: Map<string, Node>, selfId: string): string { |
| 2374 | const CALLABLE = new Set(['method', 'function', 'component', 'constructor', 'class']); |
| 2375 | const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, ''); |
| 2376 | const keyNorm = norm(key); |
| 2377 | if (keyNorm.length < 3) return ''; |
| 2378 | const cands = new Map<string, Node>(); |
| 2379 | const consider = (n: Node | undefined | null) => { |
| 2380 | if (!n || n.id === selfId || !CALLABLE.has(n.kind) || cands.has(n.id)) return; |
| 2381 | const nameNorm = norm(n.name || ''); |
| 2382 | if (nameNorm.length < 3) return; |
| 2383 | if (!nameNorm.includes(keyNorm) && !keyNorm.includes(nameNorm)) return; |
| 2384 | cands.set(n.id, n); |
| 2385 | }; |
| 2386 | const cap = key.charAt(0).toUpperCase() + key.slice(1); |
| 2387 | const probes = keyIsType |
| 2388 | ? [`${key}Handler`, key] |
| 2389 | : [key, `on${cap}`, `handle${cap}`, `${key}Handler`, `handle_${key}`]; |
| 2390 | for (const p of probes) { |
| 2391 | try { for (const n of cg.getNodesByName(p)) consider(n); } catch { /* exact probe miss is fine */ } |
| 2392 | } |
| 2393 | let raw = 0; |
| 2394 | try { |
| 2395 | const results = cg.searchNodes(key, { limit: 12 }); |
| 2396 | raw = results.length; |
| 2397 | for (const r of results) consider(r.node); |
| 2398 | } catch { /* FTS syntax edge — exact probes already ran */ } |
| 2399 | if (cands.size === 0) { |
| 2400 | return raw >= 12 && key.length < 5 ? `key \`${key}\` is too generic to shortlist (${raw}+ matches)` : ''; |
| 2401 | } |
| 2402 | // A constructor candidate duplicates its class: extractors emit ctors as |
| 2403 | // METHOD nodes named like the class (C#/Java `Foo::Foo`) — keep the class. |
| 2404 | const all = [...cands.values()]; |
| 2405 | const classKey = new Set(all.filter((n) => n.kind === 'class').map((n) => `${n.name}|${n.filePath}`)); |
| 2406 | const namedNames = new Set([...named.values()].map((n) => n.name)); |
| 2407 | const isNamed = (n: Node) => named.has(n.id) || namedNames.has(n.name); // the flow's named set holds callables only — transfer the mark to the class |
| 2408 | const list = all |
| 2409 | .filter((n) => !(n.kind !== 'class' && classKey.has(`${n.name}|${n.filePath}`))) |
| 2410 | .sort((a, b) => (isNamed(b) ? 1 : 0) - (isNamed(a) ? 1 : 0)) |
| 2411 | .slice(0, 4) |
| 2412 | .map((n) => { |
| 2413 | // Typed-bus convention: the runtime target is the candidate class's |
| 2414 | // Handle/Execute/Consume method — name the exact node, not just the class. |
| 2415 | let display = n.qualifiedName || n.name; |
| 2416 | let at = `${n.filePath}:${n.startLine}`; |
| 2417 | if (keyIsType && n.kind === 'class') { |
| 2418 | try { |
| 2419 | const HANDLER_METHODS = /^(handle|handleAsync|execute|executeAsync|consume|consumeAsync|run|__invoke)$/i; |
| 2420 | const method = cg.getOutgoingEdges(n.id) |
| 2421 | .filter((e) => e.kind === 'contains') |
| 2422 | .map((e) => { try { return cg.getNode(e.target); } catch { return null; } }) |
| 2423 | .find((c): c is Node => !!c && c.kind === 'method' && HANDLER_METHODS.test(c.name)); |
| 2424 | if (method) { display = `${n.name}.${method.name}`; at = `${method.filePath}:${method.startLine}`; } |
| 2425 | } catch { /* class without resolvable members — show the class itself */ } |
| 2426 | } |
| 2427 | return `\`${display}\` (${at})${isNamed(n) ? ' ← you named this' : ''}`; |
| 2428 | }); |
| 2429 | return `candidates for key \`${key}\`: ${list.join(', ')}`; |
| 2430 | } |
no test coverage detected