All checks were successful
Deploy Development / deploy (push) Successful in 50s
Test Suite / pytest-backend (push) Successful in 2m18s
Test Suite / lint-backend (push) Successful in 3s
Test Suite / compose-smoke (push) Has been skipped
Test Suite / k6 /api/health Baseline (push) Successful in 18s
Test Suite / playwright-smoke (push) Successful in 13s
Co-authored-by: Cursor <cursoragent@cursor.com>
108 lines
3.4 KiB
Python
108 lines
3.4 KiB
Python
"""Join- und Branch-Topologie — AP1.15c.
|
|
|
|
AND-Join: mehrere `requires` auf dasselbe Gate (weiterhin in roadmap_engine).
|
|
`optional_branch`: blockiert nicht — wird aus blocked_by entfernt (defensiv).
|
|
`parallel_group`: Metadaten für parallele Stränge (group_key); keine Blockade.
|
|
OR-Alternativpfade: bewusst nicht implementiert (PO-Deferral).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from typing import Any
|
|
|
|
DEFERRED_TOPOLOGY_EDGE_KINDS = frozenset({"parallel_group", "optional_branch"})
|
|
PLANNED_TOPOLOGY_PACKAGES = ("AP1.15c",)
|
|
|
|
|
|
def _dep_type(dep: dict[str, Any]) -> str:
|
|
return dep.get("dependency_type") or dep.get("edge_kind") or "requires"
|
|
|
|
|
|
def build_topology_metadata(
|
|
*,
|
|
dependencies: list[dict[str, Any]],
|
|
item_by_id: dict[str, dict[str, Any]],
|
|
) -> dict[str, Any]:
|
|
"""Read-only Topologie-Hinweise für UI/Steuerung."""
|
|
parallel_groups: dict[str, list[str]] = defaultdict(list)
|
|
optional_edges: list[dict[str, str]] = []
|
|
|
|
for dep in dependencies:
|
|
dep_type = _dep_type(dep)
|
|
from_id = str(dep["from_item_id"])
|
|
to_id = str(dep["to_item_id"])
|
|
if from_id not in item_by_id or to_id not in item_by_id:
|
|
continue
|
|
|
|
if dep_type == "optional_branch":
|
|
optional_edges.append({"from_item_id": from_id, "to_item_id": to_id})
|
|
elif dep_type == "parallel_group":
|
|
group_key = (dep.get("group_key") or "default").strip() or "default"
|
|
for item_id in (from_id, to_id):
|
|
if item_id not in parallel_groups[group_key]:
|
|
parallel_groups[group_key].append(item_id)
|
|
|
|
return {
|
|
"parallel_groups": dict(parallel_groups),
|
|
"optional_branches": optional_edges,
|
|
}
|
|
|
|
|
|
def _optional_prerequisite_pairs(
|
|
dependencies: list[dict[str, Any]],
|
|
item_by_id: dict[str, dict[str, Any]],
|
|
) -> set[tuple[str, str]]:
|
|
pairs: set[tuple[str, str]] = set()
|
|
for dep in dependencies:
|
|
if _dep_type(dep) != "optional_branch":
|
|
continue
|
|
from_id = str(dep["from_item_id"])
|
|
to_id = str(dep["to_item_id"])
|
|
if from_id in item_by_id and to_id in item_by_id:
|
|
pairs.add((from_id, to_id))
|
|
return pairs
|
|
|
|
|
|
def apply_deferred_topology(
|
|
*,
|
|
dependencies: list[dict[str, Any]],
|
|
item_by_id: dict[str, dict[str, Any]],
|
|
blocked_by_map: dict[str, list[str]],
|
|
) -> dict[str, list[str]]:
|
|
"""
|
|
Entfernt optionale Voraussetzungen aus blocked_by.
|
|
|
|
`parallel_group` ändert blocked_by nicht — Join bleibt über `requires`-Kanten.
|
|
"""
|
|
optional_pairs = _optional_prerequisite_pairs(dependencies, item_by_id)
|
|
if not optional_pairs:
|
|
return blocked_by_map
|
|
|
|
adjusted: dict[str, list[str]] = {}
|
|
for item_id, blockers in blocked_by_map.items():
|
|
filtered = [b for b in blockers if (item_id, b) not in optional_pairs]
|
|
adjusted[item_id] = filtered
|
|
return adjusted
|
|
|
|
|
|
def topology_hints_for_item(
|
|
*,
|
|
item_id: str,
|
|
topology: dict[str, Any],
|
|
) -> dict[str, Any]:
|
|
parallel_keys = [
|
|
key
|
|
for key, members in topology.get("parallel_groups", {}).items()
|
|
if item_id in members
|
|
]
|
|
optional_waiting_on = [
|
|
edge["to_item_id"]
|
|
for edge in topology.get("optional_branches", [])
|
|
if edge["from_item_id"] == item_id
|
|
]
|
|
return {
|
|
"parallel_group_keys": parallel_keys,
|
|
"optional_prerequisites": optional_waiting_on,
|
|
}
|