48 lines
1.3 KiB
Python
48 lines
1.3 KiB
Python
"""Cycle detection for governance DAG using DFS."""
|
|
|
|
from __future__ import annotations
|
|
|
|
WHITE, GRAY, BLACK = 0, 1, 2
|
|
|
|
|
|
def detect_cycles(adj: dict[str, list[str]]) -> list[list[str]]:
|
|
"""Detect all cycles in a directed graph.
|
|
|
|
Args:
|
|
adj: Adjacency list mapping node → list of neighbors (dependencies).
|
|
|
|
Returns:
|
|
List of cycles, each cycle is a list of node IDs forming the loop.
|
|
"""
|
|
color: dict[str, int] = {node: WHITE for node in adj}
|
|
parent: dict[str, str | None] = {node: None for node in adj}
|
|
cycles: list[list[str]] = []
|
|
|
|
def dfs(u: str) -> None:
|
|
color[u] = GRAY
|
|
for v in adj.get(u, []):
|
|
if v not in color:
|
|
# Node referenced but not in graph — skip
|
|
continue
|
|
if color[v] == GRAY:
|
|
# Back edge → cycle found
|
|
cycle = [v, u]
|
|
node = u
|
|
while node != v:
|
|
node = parent[node]
|
|
if node is None:
|
|
break
|
|
cycle.append(node)
|
|
cycle.reverse()
|
|
cycles.append(cycle)
|
|
elif color[v] == WHITE:
|
|
parent[v] = u
|
|
dfs(v)
|
|
color[u] = BLACK
|
|
|
|
for node in adj:
|
|
if color[node] == WHITE:
|
|
dfs(node)
|
|
|
|
return cycles
|