| 250 | * Groups triggers by their immediate downstream blocks to identify disjoint paths |
| 251 | */ |
| 252 | export function groupTriggersByPath< |
| 253 | T extends { type: string; subBlocks?: Record<string, unknown> }, |
| 254 | >( |
| 255 | candidates: StartBlockCandidate<T>[], |
| 256 | edges: Array<{ source: string; target: string }> |
| 257 | ): Array<StartBlockCandidate<T>[]> { |
| 258 | if (candidates.length <= 1) { |
| 259 | return [candidates] |
| 260 | } |
| 261 | |
| 262 | const groups: Array<StartBlockCandidate<T>[]> = [] |
| 263 | const processed = new Set<string>() |
| 264 | |
| 265 | // Build adjacency map (edges should already be filtered to exclude trigger-to-trigger) |
| 266 | const adjacency = new Map<string, string[]>() |
| 267 | for (const edge of edges) { |
| 268 | if (!adjacency.has(edge.source)) { |
| 269 | adjacency.set(edge.source, []) |
| 270 | } |
| 271 | adjacency.get(edge.source)!.push(edge.target) |
| 272 | } |
| 273 | |
| 274 | // Group triggers that feed into the same immediate blocks |
| 275 | for (const trigger of candidates) { |
| 276 | if (processed.has(trigger.blockId)) continue |
| 277 | |
| 278 | const immediateTargets = adjacency.get(trigger.blockId) || [] |
| 279 | const targetSet = new Set(immediateTargets) |
| 280 | |
| 281 | // Find all triggers with the same immediate targets |
| 282 | const group = candidates.filter((t) => { |
| 283 | if (processed.has(t.blockId)) return false |
| 284 | if (t.blockId === trigger.blockId) return true |
| 285 | |
| 286 | const tTargets = adjacency.get(t.blockId) || [] |
| 287 | |
| 288 | // Different number of targets = different paths |
| 289 | if (immediateTargets.length !== tTargets.length) return false |
| 290 | |
| 291 | // Check if all targets match |
| 292 | return tTargets.every((target) => targetSet.has(target)) |
| 293 | }) |
| 294 | |
| 295 | group.forEach((t) => processed.add(t.blockId)) |
| 296 | groups.push(group) |
| 297 | } |
| 298 | |
| 299 | logger.info('Grouped triggers by path', { |
| 300 | groupCount: groups.length, |
| 301 | groups: groups.map((g) => ({ |
| 302 | count: g.length, |
| 303 | triggers: g.map((t) => ({ id: t.blockId, type: t.block.type })), |
| 304 | })), |
| 305 | }) |
| 306 | |
| 307 | return groups |
| 308 | } |
| 309 | |