Files
cleveragents-core/robot/helper_influence_dag_traversal.py
T
freemo 8e6642e8c9
CI / lint (pull_request) Successful in 13s
CI / typecheck (pull_request) Successful in 31s
CI / security (pull_request) Successful in 32s
CI / quality (pull_request) Successful in 15s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 1m17s
CI / integration_tests (pull_request) Successful in 2m50s
CI / coverage (pull_request) Successful in 4m16s
CI / unit_tests (pull_request) Successful in 6m28s
CI / docker (pull_request) Successful in 39s
CI / lint (push) Successful in 12s
CI / typecheck (push) Successful in 31s
CI / security (push) Successful in 29s
CI / quality (push) Successful in 15s
CI / build (push) Successful in 21s
CI / unit_tests (push) Successful in 2m11s
CI / benchmark-regression (push) Has been skipped
CI / docker (push) Successful in 40s
CI / integration_tests (push) Successful in 2m58s
CI / coverage (push) Successful in 6m20s
CI / benchmark-publish (push) Successful in 14m5s
CI / benchmark-regression (pull_request) Successful in 33m18s
feat(decision): implement influence DAG traversal in correction affected subtree computation
Extended _compute_affected_subtree() to BFS over both the structural tree
(parent-child plan relationships) and decision_dependencies edges (influence
DAG). The algorithm performs a single O(V+E) BFS pass that unions neighbors
from both edge sources, using a visited set for cycle detection to guard
against data corruption.

Decision creation now supports dependency_decision_ids parameter in
record_decision() which populates the in-memory influence DAG store.
get_influence_edges() returns the adjacency list format consumed by
CorrectionService.

All public CorrectionService methods (analyze_impact, execute_revert,
execute_correction, generate_dry_run_report) accept an optional
influence_edges parameter while remaining backward-compatible (defaults
to None, preserving structural-only traversal when not provided).

Key design decisions:
- Single BFS pass over union of structural + influence edges rather than
  separate traversals, ensuring O(V+E) complexity and consistent visit order
- Cycle detection via visited set with warning log (not an error) since
  cycles indicate data corruption, not a programming error
- Influence edge logging: traversal emits count of influence edges processed
  for observability
- Backward-compatible API: existing callers that only pass decision_tree
  continue to work identically

ISSUES CLOSED: #542
2026-03-04 15:36:34 +00:00

159 lines
5.5 KiB
Python

"""Helper script for influence DAG traversal Robot Framework tests.
Exercises CorrectionService with both structural tree and influence
DAG edges, plus DecisionService influence dependency recording.
"""
from __future__ import annotations
import sys
from cleveragents.application.services.correction_service import CorrectionService
from cleveragents.application.services.decision_service import DecisionService
from cleveragents.domain.models.core.correction import CorrectionMode
# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------
_PLAN_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV"
def _structural_only() -> None:
"""Revert with only structural children."""
svc = CorrectionService()
req = svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
tree = {"D1": ["D2", "D3"], "D2": ["D4"]}
result = svc.execute_revert(req.correction_id, tree)
assert result.status == "applied"
assert set(result.reverted_decisions) == {"D1", "D2", "D3", "D4"}
print("structural-only-ok")
def _influence_only() -> None:
"""Revert with only influence DAG edges."""
svc = CorrectionService()
req = svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
tree: dict[str, list[str]] = {}
influence = {"D1": ["D5", "D6"], "D5": ["D7"]}
result = svc.execute_revert(req.correction_id, tree, influence)
assert result.status == "applied"
assert set(result.reverted_decisions) == {"D1", "D5", "D6", "D7"}
print("influence-only-ok")
def _combined_revert() -> None:
"""Revert traverses union of structural + influence edges."""
svc = CorrectionService()
req = svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
tree = {"D1": ["D2"]}
influence = {"D1": ["D3"], "D2": ["D4"]}
result = svc.execute_revert(req.correction_id, tree, influence)
assert result.status == "applied"
assert set(result.reverted_decisions) == {"D1", "D2", "D3", "D4"}
print("combined-revert-ok")
def _cycle_detection() -> None:
"""Corrupt cycle in influence DAG handled gracefully."""
svc = CorrectionService()
req = svc.request_correction(_PLAN_ID, "D1", CorrectionMode.REVERT)
tree: dict[str, list[str]] = {}
influence = {"D1": ["D2"], "D2": ["D3"], "D3": ["D1"]}
result = svc.execute_revert(req.correction_id, tree, influence)
assert result.status == "applied"
reverted = set(result.reverted_decisions)
assert reverted == {"D1", "D2", "D3"}, f"Got {reverted}"
# No duplicates
assert len(result.reverted_decisions) == len(set(result.reverted_decisions))
print("cycle-detection-ok")
def _record_deps() -> None:
"""record_decision with dependency_decision_ids populates DAG."""
svc = DecisionService()
root = svc.record_decision(
plan_id=_PLAN_ID,
decision_type="strategy_choice",
question="Strategy?",
chosen_option="Approach A",
)
child = svc.record_decision(
plan_id=_PLAN_ID,
decision_type="implementation_choice",
question="Impl?",
chosen_option="Option B",
parent_decision_id=root.decision_id,
dependency_decision_ids=[root.decision_id],
)
edges = svc.get_influence_edges(_PLAN_ID)
assert root.decision_id in edges
assert child.decision_id in edges[root.decision_id]
print("record-deps-ok")
def _e2e_cascade() -> None:
"""Full flow: record decisions with deps, then revert cascades."""
# 1. Record decisions with influence dependencies
dsvc = DecisionService()
d1 = dsvc.record_decision(
plan_id=_PLAN_ID,
decision_type="strategy_choice",
question="Strategy?",
chosen_option="Approach A",
)
d2 = dsvc.record_decision(
plan_id=_PLAN_ID,
decision_type="resource_selection",
question="Resource?",
chosen_option="File X",
parent_decision_id=d1.decision_id,
)
d3 = dsvc.record_decision(
plan_id=_PLAN_ID,
decision_type="implementation_choice",
question="Impl?",
chosen_option="Option B",
parent_decision_id=d2.decision_id,
dependency_decision_ids=[d1.decision_id, d2.decision_id],
)
# 2. Build structural tree
tree = {
d1.decision_id: [d2.decision_id],
d2.decision_id: [d3.decision_id],
}
# 3. Get influence edges
influence = dsvc.get_influence_edges(_PLAN_ID)
# 4. Revert from d1 → should cascade to d2 and d3
csvc = CorrectionService()
req = csvc.request_correction(_PLAN_ID, d1.decision_id, CorrectionMode.REVERT)
result = csvc.execute_revert(req.correction_id, tree, influence)
assert result.status == "applied"
reverted = set(result.reverted_decisions)
assert d1.decision_id in reverted
assert d2.decision_id in reverted
assert d3.decision_id in reverted
print("e2e-cascade-ok")
# ---------------------------------------------------------------------------
# Dispatch
# ---------------------------------------------------------------------------
_COMMANDS = {
"structural-only": _structural_only,
"influence-only": _influence_only,
"combined-revert": _combined_revert,
"cycle-detection": _cycle_detection,
"record-deps": _record_deps,
"e2e-cascade": _e2e_cascade,
}
if __name__ == "__main__":
if len(sys.argv) < 2 or sys.argv[1] not in _COMMANDS:
print(f"Usage: {sys.argv[0]} <{'|'.join(_COMMANDS)}>", file=sys.stderr)
sys.exit(1)
_COMMANDS[sys.argv[1]]()