1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Make projection cycle planning scale-safe

This commit is contained in:
Andraxion 2026-07-29 12:41:17 -04:00
parent f1fabaf0ca
commit 5b43c44dd7
2 changed files with 52 additions and 32 deletions

View file

@ -24,47 +24,51 @@ def _cycles(node_ids: tuple[str, ...], edges: tuple[Edge, ...]) -> list[list[str
"""Return deterministic strongly connected components that represent cycles."""
adjacency: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
reverse_adjacency: dict[str, list[str]] = {node_id: [] for node_id in node_ids}
for edge in edges:
adjacency[edge.source_id].append(edge.target_id)
for targets in adjacency.values():
reverse_adjacency[edge.target_id].append(edge.source_id)
for targets in (*adjacency.values(), *reverse_adjacency.values()):
targets.sort()
index = 0
indexes: dict[str, int] = {}
lowlinks: dict[str, int] = {}
stack: list[str] = []
on_stack: set[str] = set()
components: list[list[str]] = []
visited: set[str] = set()
finished: list[str] = []
for node_id in node_ids:
if node_id in visited:
continue
visited.add(node_id)
traversal: list[tuple[str, int]] = [(node_id, 0)]
while traversal:
current, position = traversal[-1]
targets = adjacency[current]
if position < len(targets):
target = targets[position]
traversal[-1] = (current, position + 1)
if target not in visited:
visited.add(target)
traversal.append((target, 0))
continue
finished.append(current)
traversal.pop()
def visit(node_id: str) -> None:
nonlocal index
indexes[node_id] = index
lowlinks[node_id] = index
index += 1
stack.append(node_id)
on_stack.add(node_id)
for target_id in adjacency[node_id]:
if target_id not in indexes:
visit(target_id)
lowlinks[node_id] = min(lowlinks[node_id], lowlinks[target_id])
elif target_id in on_stack:
lowlinks[node_id] = min(lowlinks[node_id], indexes[target_id])
if lowlinks[node_id] != indexes[node_id]:
return
assigned: set[str] = set()
components: list[list[str]] = []
for node_id in reversed(finished):
if node_id in assigned:
continue
assigned.add(node_id)
component: list[str] = []
while stack:
member = stack.pop()
on_stack.remove(member)
component.append(member)
if member == node_id:
break
component_stack = [node_id]
while component_stack:
current = component_stack.pop()
component.append(current)
for target in reversed(reverse_adjacency[current]):
if target not in assigned:
assigned.add(target)
component_stack.append(target)
component.sort()
if len(component) > 1 or component[0] in adjacency[component[0]]:
components.append(component)
for node_id in node_ids:
if node_id not in indexes:
visit(node_id)
return sorted(components)

View file

@ -14,6 +14,7 @@ from unittest import mock
from docforge.errors import DocForgeError
from docforge.manual_projection import (
_cycles,
build_manual_projection_package,
build_manual_render_plan,
)
@ -390,6 +391,21 @@ class ProjectionContractTests(unittest.TestCase):
workflow["cross_references"],
)
def test_cycle_detection_handles_the_maximum_deep_graph_iteratively(self) -> None:
node_ids = tuple(f"node.{index:05d}" for index in range(10_000))
chain = tuple(
Edge(node_ids[index], "depends_on", node_ids[index + 1])
for index in range(len(node_ids) - 1)
)
self.assertEqual([], _cycles(node_ids, chain))
self.assertEqual(
[list(node_ids)],
_cycles(
node_ids,
(*chain, Edge(node_ids[-1], "depends_on", node_ids[0])),
),
)
def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None:
renderer = GenericHtmlRenderer()
self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version)