* 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>)
| 2159 | * connected flow never reaches this method. |
| 2160 | */ |
| 2161 | private buildDynamicBoundaries(cg: CodeGraph, scanList: Node[], named: Map<string, Node>): string { |
| 2162 | const MAX_NOTES = 4; // boundary bullets per explore |
| 2163 | const MAX_SCAN = 8; // bodies scanned |
| 2164 | const MAX_TOTAL_CHARS = 200_000; |
| 2165 | let projectRoot: string; |
| 2166 | try { projectRoot = cg.getProjectRoot(); } catch { return ''; } |
| 2167 | const notes: string[] = []; |
| 2168 | const seenNode = new Set<string>(); |
| 2169 | const seenSite = new Set<string>(); |
| 2170 | let scanned = 0, charsScanned = 0; |
| 2171 | for (const node of scanList) { |
| 2172 | if (notes.length >= MAX_NOTES || scanned >= MAX_SCAN || charsScanned > MAX_TOTAL_CHARS) break; |
| 2173 | if (seenNode.has(node.id) || !node.startLine || !node.endLine) continue; |
| 2174 | seenNode.add(node.id); |
| 2175 | const absPath = validatePathWithinRoot(projectRoot, node.filePath); |
| 2176 | if (!absPath || !existsSync(absPath)) continue; |
| 2177 | let content: string; |
| 2178 | try { content = readFileSync(absPath, 'utf-8'); } catch { continue; } |
| 2179 | const body = content.split('\n').slice(node.startLine - 1, node.endLine).join('\n'); |
| 2180 | scanned++; |
| 2181 | charsScanned += body.length; |
| 2182 | for (const m of scanDynamicDispatch(body, node.language || '', node.startLine)) { |
| 2183 | if (notes.length >= MAX_NOTES) break; |
| 2184 | const siteKey = `${node.filePath}:${m.line}:${m.form}`; |
| 2185 | if (seenSite.has(siteKey)) continue; |
| 2186 | seenSite.add(siteKey); |
| 2187 | const more = m.moreSites ? ` (+${m.moreSites} more such site${m.moreSites > 1 ? 's' : ''} in this body)` : ''; |
| 2188 | notes.push(`- \`${node.name}\` (${node.filePath}:${m.line}) — ${m.label}: \`${m.snippet}\`${more}`); |
| 2189 | if (m.key) { |
| 2190 | const cand = this.boundaryCandidates(cg, m.key, !!m.keyIsType, named, node.id); |
| 2191 | if (cand) notes.push(` ${cand}`); |
| 2192 | } |
| 2193 | } |
| 2194 | } |
| 2195 | if (notes.length === 0) return ''; |
| 2196 | return [ |
| 2197 | '**Dynamic boundaries (the static path ends at runtime dispatch)**', |
| 2198 | '', |
| 2199 | ...notes, |
| 2200 | '', |
| 2201 | '> 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.', |
| 2202 | '', |
| 2203 | ].join('\n'); |
| 2204 | } |
| 2205 | |
| 2206 | /** |
| 2207 | * Interface/registry-dispatch announcement — #687 extended to GRAPH-visible |
no test coverage detected