* Dynamic-boundary surfacing (#687): when the flow among the agent's named * symbols does not fully connect, scan the disconnected symbols' bodies for * dynamic-dispatch sites (computed member calls, getattr, reflection, typed * message buses, runtime-keyed emits) and ANNOUNCE the boundary
(cg: CodeGraph, scanList: Node[], named: Map<string, Node>)
| 2117 | * connected flow never reaches this method. |
| 2118 | */ |
| 2119 | private buildDynamicBoundaries(cg: CodeGraph, scanList: Node[], named: Map<string, Node>): string { |
| 2120 | const MAX_NOTES = 4; // boundary bullets per explore |
| 2121 | const MAX_SCAN = 8; // bodies scanned |
| 2122 | const MAX_TOTAL_CHARS = 200_000; |
| 2123 | let projectRoot: string; |
| 2124 | try { projectRoot = cg.getProjectRoot(); } catch { return ''; } |
| 2125 | const notes: string[] = []; |
| 2126 | const seenNode = new Set<string>(); |
| 2127 | const seenSite = new Set<string>(); |
| 2128 | let scanned = 0, charsScanned = 0; |
| 2129 | for (const node of scanList) { |
| 2130 | if (notes.length >= MAX_NOTES || scanned >= MAX_SCAN || charsScanned > MAX_TOTAL_CHARS) break; |
| 2131 | if (seenNode.has(node.id) || !node.startLine || !node.endLine) continue; |
| 2132 | seenNode.add(node.id); |
| 2133 | const absPath = validatePathWithinRoot(projectRoot, node.filePath); |
| 2134 | if (!absPath || !existsSync(absPath)) continue; |
| 2135 | let content: string; |
| 2136 | try { content = readFileSync(absPath, 'utf-8'); } catch { continue; } |
| 2137 | const body = content.split('\n').slice(node.startLine - 1, node.endLine).join('\n'); |
| 2138 | scanned++; |
| 2139 | charsScanned += body.length; |
| 2140 | for (const m of scanDynamicDispatch(body, node.language || '', node.startLine)) { |
| 2141 | if (notes.length >= MAX_NOTES) break; |
| 2142 | const siteKey = `${node.filePath}:${m.line}:${m.form}`; |
| 2143 | if (seenSite.has(siteKey)) continue; |
| 2144 | seenSite.add(siteKey); |
| 2145 | const more = m.moreSites ? ` (+${m.moreSites} more such site${m.moreSites > 1 ? 's' : ''} in this body)` : ''; |
| 2146 | notes.push(`- \`${node.name}\` (${node.filePath}:${m.line}) — ${m.label}: \`${m.snippet}\`${more}`); |
| 2147 | if (m.key) { |
| 2148 | const cand = this.boundaryCandidates(cg, m.key, !!m.keyIsType, named, node.id); |
| 2149 | if (cand) notes.push(` ${cand}`); |
| 2150 | } |
| 2151 | } |
| 2152 | } |
| 2153 | if (notes.length === 0) return ''; |
| 2154 | return [ |
| 2155 | '**Dynamic boundaries (the static path ends at runtime dispatch)**', |
| 2156 | '', |
| 2157 | ...notes, |
| 2158 | '', |
| 2159 | '> These sites choose their call target at runtime (registry / bus / reflection) — the site shown IS where the flow continues. To follow it, run codegraph_explore or codegraph_node on a candidate; source for the sites above is included below.', |
| 2160 | '', |
| 2161 | ].join('\n'); |
| 2162 | } |
| 2163 | |
| 2164 | /** |
| 2165 | * Interface/registry-dispatch announcement — #687 extended to GRAPH-visible |
no test coverage detected