# Invariant Reconciliation Module

**Package:** `cleveragents.actor.reconciliation`
**Introduced:** v3.8.0

The Invariant Reconciliation Actor is a built-in actor that automatically runs at
every plan phase transition to ensure all active invariants are consistent and
correctly prioritised before any strategy or execution work begins.

For the invariant data model and scope hierarchy, see
[`docs/reference/invariants.md`](../reference/invariants.md).
For the `InvariantService` API, see
[`docs/api/core.md`](../api/core.md).

---

## Purpose

Invariants are natural-language constraints on plan execution that can be defined
at four scopes: global, project, action, and plan. When multiple invariants apply
to the same plan, conflicts can arise — for example, a project-level invariant may
contradict a plan-level one. The reconciliation actor resolves these conflicts
deterministically using a precedence chain and records each decision in the audit
trail.

---

## Automatic Invocation

`PlanLifecycleService` automatically invokes the reconciliation actor at the start
of three phase transitions:

| Transition | Method |
|------------|--------|
| Strategize start | `start_strategize()` |
| Execute start | `execute_plan()` |
| Apply start | `apply_plan()` |

If reconciliation fails, the phase transition is **blocked** with
`ReconciliationBlockedError` and an `INVARIANT_VIOLATED` event is emitted on the
event bus. The transition cannot proceed until the invariants are corrected.

Post-correction reconciliation runs automatically via a `CORRECTION_APPLIED` event
subscription (best-effort; does not block correction completion).

---

## Reconciliation Algorithm

The actor follows a six-step algorithm:

1. **Load** — fetch all invariants in scope for the plan (global → project → action → plan)
2. **Deduplicate** — remove exact-text duplicates, keeping the highest-scope copy
3. **Detect conflicts** — identify pairs where one invariant's constraint contradicts another's
4. **Resolve** — apply the precedence chain: `plan > action > project > global`; when scopes are equal, the invariant with the lower `position` index wins
5. **Record** — write each resolution decision to the audit trail with rationale
6. **Emit** — publish `INVARIANT_RECONCILED` event on the event bus

---

## Key Classes

### `InvariantReconciliationActor`

```python
from cleveragents.actor.reconciliation import InvariantReconciliationActor

actor = InvariantReconciliationActor(invariant_service, event_bus, audit_service)
result = await actor.reconcile(plan_id)
```

| Parameter | Type | Description |
|-----------|------|-------------|
| `invariant_service` | `InvariantService` | Provides invariant CRUD and scope queries |
| `event_bus` | `EventBus` | Receives `INVARIANT_RECONCILED` / `INVARIANT_VIOLATED` events |
| `audit_service` | `AuditService` | Records reconciliation decisions |

### `ReconciliationResult`

```python
@dataclass
class ReconciliationResult:
    plan_id: str
    resolved_invariants: list[Invariant]
    conflicts_detected: int
    conflicts_resolved: int
    decisions: list[ReconciliationDecision]
    success: bool
```

### `ReconciliationDecision`

```python
@dataclass
class ReconciliationDecision:
    winner: Invariant
    loser: Invariant
    rationale: str          # e.g. "plan scope > project scope"
    timestamp: datetime
```

---

## Error Handling

| Exception | When raised |
|-----------|-------------|
| `ReconciliationBlockedError` | Unresolvable conflict detected; phase transition blocked |
| `InvariantLoadError` | Database error loading invariants |
| `ReconciliationTimeoutError` | Reconciliation exceeded configured timeout |

When `ReconciliationBlockedError` is raised:

1. The phase transition method re-raises the exception to the caller
2. `PlanLifecycleService` emits `INVARIANT_VIOLATED` on the event bus
3. The plan remains in its current phase (e.g., `PENDING` before Strategize)
4. The operator must correct the conflicting invariants and retry the transition

---

## DI Registration

`InvariantService` is registered as a **Singleton** in the DI container:

```python
# src/cleveragents/application/container.py (excerpt)
class Container(containers.DeclarativeContainer):
    invariant_service = providers.Singleton(
        InvariantService,
        uow_factory=uow_factory,
        event_bus=event_bus,
    )
    invariant_reconciliation_actor = providers.Factory(
        InvariantReconciliationActor,
        invariant_service=invariant_service,
        event_bus=event_bus,
        audit_service=audit_service,
    )
```

---

## Usage Examples

### Triggering Reconciliation Manually

```python
from cleveragents.application.container import Container

container = Container()
actor = container.invariant_reconciliation_actor()
result = await actor.reconcile(plan_id="plan-01JXYZ")

if not result.success:
    print(f"Reconciliation failed: {result.conflicts_detected} unresolvable conflicts")
else:
    print(f"Reconciled {result.conflicts_resolved} conflicts")
    for decision in result.decisions:
        print(f"  {decision.winner.text!r} won over {decision.loser.text!r}: {decision.rationale}")
```

### Listening for Reconciliation Events

```python
from cleveragents.domain.events import INVARIANT_RECONCILED, INVARIANT_VIOLATED

@event_bus.subscribe(INVARIANT_RECONCILED)
async def on_reconciled(event):
    logger.info("Invariants reconciled", plan_id=event.plan_id, decisions=event.decision_count)

@event_bus.subscribe(INVARIANT_VIOLATED)
async def on_violated(event):
    logger.error("Invariant violation blocked phase transition", plan_id=event.plan_id)
    # Notify operator, create alert, etc.
```

### Defining Invariants at Different Scopes

```bash
# Global invariant (applies to all plans)
agents invariant create --scope global --text "Never delete production data without a backup"

# Project-level invariant
agents invariant create --scope project --project my-project \
  --text "All database migrations must be reversible"

# Plan-level invariant (highest precedence)
agents invariant create --scope plan --plan plan-01JXYZ \
  --text "This plan may drop the staging schema"
```

In this example, if the plan-level invariant conflicts with the project-level one,
the plan-level invariant wins (higher precedence).

---

## Related Documentation

- [ADR-016 Invariant System](../adr/ADR-016-invariant-system.md) — design rationale
- [ADR-007 Decision Tree & Correction](../adr/ADR-007-decision-tree-and-correction.md) — correction flow
- [Core API Reference](../api/core.md) — `InvariantService` API
- [Shell Safety Module](shell-safety.md) — another built-in safety actor
