Files
cleveragents-core/robot/helper_invariant_reconciliation.py
T
freemo abd4c6de49
CI / lint (pull_request) Successful in 14s
CI / typecheck (pull_request) Successful in 32s
CI / security (pull_request) Successful in 30s
CI / quality (pull_request) Successful in 18s
CI / benchmark-publish (pull_request) Has been skipped
CI / build (pull_request) Successful in 16s
CI / unit_tests (pull_request) Successful in 1m59s
CI / integration_tests (pull_request) Successful in 3m47s
CI / docker (pull_request) Successful in 47s
CI / coverage (pull_request) Successful in 5m48s
CI / lint (push) Successful in 13s
CI / typecheck (push) Successful in 35s
CI / security (push) Successful in 34s
CI / quality (push) Successful in 19s
CI / build (push) Successful in 17s
CI / unit_tests (push) Successful in 2m27s
CI / integration_tests (push) Successful in 2m59s
CI / benchmark-regression (push) Has been skipped
CI / coverage (push) Successful in 4m9s
CI / benchmark-publish (push) Successful in 16m43s
CI / docker (push) Successful in 39s
CI / benchmark-regression (pull_request) Successful in 30m2s
feat(actor): implement built-in invariant reconciliation actor
Add InvariantReconciliationActor that runs at the start of the Strategize
phase to reconcile invariants from four scopes (global, project, action,
plan). The actor detects conflicts, resolves them using specificity-based
precedence (plan > action > project > global), honours non_overridable
global invariants, records invariant_enforced decisions, and produces a
reconciled InvariantSet.

Changes:
- New: src/cleveragents/actor/reconciliation.py
  - InvariantReconciliationActor class with collect_invariants() and run()
  - reconcile_invariants() pure function
  - ScopeInvariants, ConflictRecord, ReconciliationResult dataclasses
- Modified: src/cleveragents/domain/models/core/invariant.py
  - Added non_overridable: bool field to Invariant model
- New: features/invariant_reconciliation_actor.feature (26 BDD scenarios)
- New: features/steps/invariant_reconciliation_actor_steps.py
- New: robot/invariant_reconciliation_actor.robot
- New: robot/helper_invariant_reconciliation.py
- New: benchmarks/invariant_reconciliation_bench.py

Closes #549
2026-03-04 20:26:42 +00:00

220 lines
6.1 KiB
Python

"""Robot Framework helper for invariant reconciliation actor integration tests.
Exercises the full reconciliation pipeline: collect → reconcile → enforce,
then verifies the reconciled set is respected by subsequent operations.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
# Ensure source tree is importable
_SRC = str(Path(__file__).resolve().parents[1] / "src")
if _SRC not in sys.path:
sys.path.insert(0, _SRC)
from cleveragents.actor.reconciliation import ( # noqa: E402
InvariantReconciliationActor,
)
from cleveragents.application.services.decision_service import ( # noqa: E402
DecisionService,
)
from cleveragents.application.services.invariant_service import ( # noqa: E402
InvariantService,
)
from cleveragents.domain.models.core.decision import DecisionType # noqa: E402
from cleveragents.domain.models.core.invariant import InvariantScope # noqa: E402
def _run_reconciliation_pipeline() -> dict[str, object]:
"""Run the full reconciliation pipeline and return results as dict."""
inv_svc = InvariantService()
dec_svc = DecisionService()
# Add invariants across scopes
inv_svc.add_invariant(
"Never delete production data",
InvariantScope.GLOBAL,
"system",
)
inv_svc.add_invariant(
"Use ORM for all queries",
InvariantScope.PROJECT,
"local/api-service",
)
inv_svc.add_invariant(
"Separate commits per file",
InvariantScope.ACTION,
"local/code-coverage",
)
inv_svc.add_invariant(
"Mock all network calls",
InvariantScope.PLAN,
"01JQAAAAAAAAAAAAAAAAAAAA01",
)
actor = InvariantReconciliationActor(
invariant_service=inv_svc,
decision_service=dec_svc,
)
result = actor.run(
plan_id="01JQAAAAAAAAAAAAAAAAAAAA01",
project_name="local/api-service",
action_name="local/code-coverage",
)
return {
"invariant_count": len(result.reconciled_set.invariants),
"conflict_count": len(result.conflicts),
"decision_count": len(result.enforced_decision_ids),
"invariant_texts": [inv.text for inv in result.reconciled_set.invariants],
}
def _run_conflict_resolution() -> dict[str, object]:
"""Run reconciliation with conflicts and verify resolution."""
inv_svc = InvariantService()
dec_svc = DecisionService()
inv_svc.add_invariant(
"All tests must pass",
InvariantScope.GLOBAL,
"system",
)
inv_svc.add_invariant(
"All tests must pass",
InvariantScope.PLAN,
"01JQAAAAAAAAAAAAAAAAAAAA01",
)
actor = InvariantReconciliationActor(
invariant_service=inv_svc,
decision_service=dec_svc,
)
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
winner_scope = ""
for inv in result.reconciled_set.invariants:
if inv.text.lower() == "all tests must pass":
winner_scope = inv.scope.value
break
return {
"invariant_count": len(result.reconciled_set.invariants),
"conflict_count": len(result.conflicts),
"winner_scope": winner_scope,
}
def _run_non_overridable_test() -> dict[str, object]:
"""Verify non_overridable global invariants block overrides."""
inv_svc = InvariantService()
dec_svc = DecisionService()
inv = inv_svc.add_invariant(
"Payment processing must be idempotent",
InvariantScope.GLOBAL,
"system",
)
inv.non_overridable = True
inv_svc.add_invariant(
"Payment processing must be idempotent",
InvariantScope.PLAN,
"01JQAAAAAAAAAAAAAAAAAAAA01",
)
actor = InvariantReconciliationActor(
invariant_service=inv_svc,
decision_service=dec_svc,
)
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
winner_scope = ""
for r_inv in result.reconciled_set.invariants:
if "idempotent" in r_inv.text.lower():
winner_scope = r_inv.scope.value
break
return {
"invariant_count": len(result.reconciled_set.invariants),
"conflict_count": len(result.conflicts),
"winner_scope": winner_scope,
"has_non_overridable_reason": any(
"non_overridable" in c.reason for c in result.conflicts
),
}
def _run_decision_verification() -> dict[str, object]:
"""Verify invariant_enforced decisions are properly recorded."""
inv_svc = InvariantService()
dec_svc = DecisionService()
inv_svc.add_invariant(
"Never delete production data",
InvariantScope.GLOBAL,
"system",
)
inv_svc.add_invariant(
"Mock all network calls",
InvariantScope.PLAN,
"01JQAAAAAAAAAAAAAAAAAAAA01",
)
actor = InvariantReconciliationActor(
invariant_service=inv_svc,
decision_service=dec_svc,
)
result = actor.run(plan_id="01JQAAAAAAAAAAAAAAAAAAAA01")
# Verify decisions
all_correct_type = True
all_correct_plan = True
for did in result.enforced_decision_ids:
decision = dec_svc.get_decision(did)
if decision.decision_type != DecisionType.INVARIANT_ENFORCED:
all_correct_type = False
if decision.plan_id != "01JQAAAAAAAAAAAAAAAAAAAA01":
all_correct_plan = False
return {
"decision_count": len(result.enforced_decision_ids),
"all_correct_type": all_correct_type,
"all_correct_plan": all_correct_plan,
}
def main() -> None:
"""Entry point for Robot Framework helper."""
if len(sys.argv) < 2:
print("Usage: helper_invariant_reconciliation.py <command>")
sys.exit(1)
command = sys.argv[1]
handlers = {
"reconcile": _run_reconciliation_pipeline,
"conflict": _run_conflict_resolution,
"non-overridable": _run_non_overridable_test,
"decisions": _run_decision_verification,
}
handler = handlers.get(command)
if handler is None:
print(f"Unknown command: {command}")
sys.exit(1)
result = handler()
print(json.dumps(result))
print("reconciliation-ok")
if __name__ == "__main__":
main()