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.""" """Return deterministic strongly connected components that represent cycles."""
adjacency: dict[str, list[str]] = {node_id: [] for node_id in node_ids} 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: for edge in edges:
adjacency[edge.source_id].append(edge.target_id) 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() targets.sort()
index = 0 visited: set[str] = set()
indexes: dict[str, int] = {} finished: list[str] = []
lowlinks: dict[str, int] = {} for node_id in node_ids:
stack: list[str] = [] if node_id in visited:
on_stack: set[str] = set() continue
components: list[list[str]] = [] 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: assigned: set[str] = set()
nonlocal index components: list[list[str]] = []
indexes[node_id] = index for node_id in reversed(finished):
lowlinks[node_id] = index if node_id in assigned:
index += 1 continue
stack.append(node_id) assigned.add(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
component: list[str] = [] component: list[str] = []
while stack: component_stack = [node_id]
member = stack.pop() while component_stack:
on_stack.remove(member) current = component_stack.pop()
component.append(member) component.append(current)
if member == node_id: for target in reversed(reverse_adjacency[current]):
break if target not in assigned:
assigned.add(target)
component_stack.append(target)
component.sort() component.sort()
if len(component) > 1 or component[0] in adjacency[component[0]]: if len(component) > 1 or component[0] in adjacency[component[0]]:
components.append(component) components.append(component)
for node_id in node_ids:
if node_id not in indexes:
visit(node_id)
return sorted(components) return sorted(components)

View file

@ -14,6 +14,7 @@ from unittest import mock
from docforge.errors import DocForgeError from docforge.errors import DocForgeError
from docforge.manual_projection import ( from docforge.manual_projection import (
_cycles,
build_manual_projection_package, build_manual_projection_package,
build_manual_render_plan, build_manual_render_plan,
) )
@ -390,6 +391,21 @@ class ProjectionContractTests(unittest.TestCase):
workflow["cross_references"], 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: def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None:
renderer = GenericHtmlRenderer() renderer = GenericHtmlRenderer()
self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version) self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version)