| 286 | |
| 287 | /** Validate a graph is acyclic via Kahn's algorithm; returns topo order or null. */ |
| 288 | export function topoSort(graph: TaskGraph): TaskId[] | null { |
| 289 | const indeg = new Map<TaskId, number>(); |
| 290 | const adj = new Map<TaskId, TaskId[]>(); |
| 291 | for (const id of graph.nodes.keys()) { indeg.set(id, 0); adj.set(id, []); } |
| 292 | for (const node of graph.nodes.values()) { |
| 293 | for (const dep of node.dependsOn) { |
| 294 | if (!graph.nodes.has(dep)) continue; |
| 295 | adj.get(dep)!.push(node.id); |
| 296 | indeg.set(node.id, (indeg.get(node.id) ?? 0) + 1); |
| 297 | } |
| 298 | } |
| 299 | const queue: TaskId[] = []; |
| 300 | for (const [id, d] of indeg) if (d === 0) queue.push(id); |
| 301 | const order: TaskId[] = []; |
| 302 | while (queue.length) { |
| 303 | const id = queue.shift()!; |
| 304 | order.push(id); |
| 305 | for (const next of adj.get(id) ?? []) { |
| 306 | indeg.set(next, indeg.get(next)! - 1); |
| 307 | if (indeg.get(next) === 0) queue.push(next); |
| 308 | } |
| 309 | } |
| 310 | return order.length === graph.nodes.size ? order : null; |
| 311 | } |