Files
cleveragents-core/features/invariant_reconciliation_actor.feature
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

237 lines
12 KiB
Gherkin

Feature: Invariant Reconciliation Actor
As a plan strategizer
I want invariants from all scopes to be reconciled automatically
So that the Strategize phase operates under a clear, conflict-free invariant set
Background:
Given a fresh InvariantService for reconciliation
And a fresh DecisionService for reconciliation
# === Single-scope invariants (no conflicts) ===
@single_scope
Scenario: Single global invariant with no conflicts
Given a global invariant "Never delete production data" from source "system"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the reconciled set should contain "Never delete production data"
And 0 conflicts should be detected
And 1 invariant_enforced decision should be recorded
@single_scope
Scenario: Single plan invariant with no conflicts
Given a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the reconciled set should contain "Mock all network calls"
And 0 conflicts should be detected
@single_scope
Scenario: Multiple global invariants with no conflicts
Given a global invariant "Never delete production data" from source "system"
And a global invariant "All APIs must maintain backward compatibility" from source "system"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 2 invariants
And 0 conflicts should be detected
And 2 invariant_enforced decisions should be recorded
# === Multi-scope compatible invariants ===
@multi_scope
Scenario: Multi-scope invariants that are all compatible
Given a global invariant "Never delete production data" from source "system"
And a project invariant "Use ORM for all queries" from source "local/api-service" for project "local/api-service"
And a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile with-project "local/api-service" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 3 invariants
And the reconciled set should contain "Never delete production data"
And the reconciled set should contain "Use ORM for all queries"
And the reconciled set should contain "Mock all network calls"
And 0 conflicts should be detected
And 3 invariant_enforced decisions should be recorded
@multi_scope
Scenario: Four-scope invariants all compatible
Given a global invariant "Never delete production data" from source "system"
And a project invariant "Use ORM for all queries" from source "local/api-service" for project "local/api-service"
And an action invariant "Separate commits per file" from source "local/code-coverage" for action "local/code-coverage"
And a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile with-project-action "local/api-service" and "local/code-coverage" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 4 invariants
And 0 conflicts should be detected
And 4 invariant_enforced decisions should be recorded
# === Conflict detection and specificity resolution ===
@conflict
Scenario: Plan invariant overrides project invariant on same text
Given a project invariant "Use REST for all APIs" from source "local/api-service" for project "local/api-service"
And a plan invariant "Use REST for all APIs" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile with-project "local/api-service" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "use rest for all apis" should be from "plan" scope
And 1 conflict should be detected
@conflict
Scenario: Plan overrides global on same text
Given a global invariant "All tests must pass" from source "system"
And a plan invariant "All tests must pass" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "all tests must pass" should be from "plan" scope
And 1 conflict should be detected
@conflict
Scenario: Action invariant overrides project invariant
Given a project invariant "Coverage above 80%" from source "local/api-service" for project "local/api-service"
And an action invariant "Coverage above 80%" from source "local/code-coverage" for action "local/code-coverage"
When I reconcile with-project-action "local/api-service" and "local/code-coverage" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "coverage above 80%" should be from "action" scope
And 1 conflict should be detected
@conflict
Scenario: Project invariant overrides global invariant
Given a global invariant "Database timeout 10s" from source "system"
And a project invariant "Database timeout 10s" from source "local/api-service" for project "local/api-service"
When I reconcile with-project "local/api-service" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "database timeout 10s" should be from "project" scope
And 1 conflict should be detected
# === non_overridable global invariant ===
@non_overridable
Scenario: Non-overridable global invariant blocks plan override
Given a non_overridable global invariant "Payment processing must be idempotent" from source "system"
And a plan invariant "Payment processing must be idempotent" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "payment processing must be idempotent" should be from "global" scope
And 1 conflict should be detected
And the conflict reason should mention "non_overridable"
@non_overridable
Scenario: Non-overridable global invariant blocks project override
Given a non_overridable global invariant "Audit logging is mandatory" from source "system"
And a project invariant "Audit logging is mandatory" from source "local/api-service" for project "local/api-service"
When I reconcile with-project "local/api-service" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "audit logging is mandatory" should be from "global" scope
And 1 conflict should be detected
And the conflict reason should mention "non_overridable"
@non_overridable
Scenario: Non-overridable global invariant blocks action override
Given a non_overridable global invariant "Security scan required" from source "system"
And an action invariant "Security scan required" from source "local/deploy" for action "local/deploy"
When I reconcile with-action "local/deploy" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the winning invariant for "security scan required" should be from "global" scope
And 1 conflict should be detected
@non_overridable
Scenario: Regular global invariant can be overridden by plan
Given a global invariant "Use verbose logging" from source "system"
And a plan invariant "Use verbose logging" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the winning invariant for "use verbose logging" should be from "plan" scope
# === invariant_enforced decision recording ===
@decisions
Scenario: Each reconciled invariant gets an invariant_enforced decision
Given a global invariant "Never delete production data" from source "system"
And a project invariant "Use ORM for all queries" from source "local/api-service" for project "local/api-service"
And a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile with-project "local/api-service" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then 3 invariant_enforced decisions should be recorded
And all recorded decisions should be of type "invariant_enforced"
And each decision should reference the plan "01JQAAAAAAAAAAAAAAAAAAAA01"
@decisions
Scenario: Decisions include rationale about scope
Given a global invariant "Never delete production data" from source "system"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the decision rationale should mention "global"
@decisions
Scenario: Decision question includes invariant text
Given a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the decision question should contain "Mock all network calls"
# === Reconciled InvariantSet accessibility ===
@invariant_set
Scenario: Reconciled InvariantSet is returned and accessible
Given a global invariant "Never delete production data" from source "system"
And a plan invariant "Mock all network calls" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the result should contain a reconciled InvariantSet
And the InvariantSet should have 2 invariants
And the InvariantSet invariants should be Invariant model instances
@invariant_set
Scenario: Empty scope produces empty reconciled set
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 0 invariants
And 0 conflicts should be detected
And 0 invariant_enforced decisions should be recorded
# === Validation and edge cases ===
@validation
Scenario: Empty plan_id raises ValueError
Then running reconciliation with empty plan_id should raise ValueError
@validation
Scenario: Inactive invariants are excluded from reconciliation
Given a global invariant "Never delete production data" from source "system"
And an inactive global invariant "Deprecated rule" from source "system"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And the reconciled set should not contain "Deprecated rule"
@validation
Scenario: Case-insensitive conflict detection
Given a global invariant "Use HTTPS only" from source "system"
And a plan invariant "use https only" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I reconcile plan-only for "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the reconciled set should contain 1 invariant
And 1 conflict should be detected
@validation
Scenario: Parent decision ID is passed to recorded decisions
Given a global invariant "Never delete production data" from source "system"
When I reconcile with-parent "01JQBBBBBBBBBBBBBBBBBBBB01" for plan "01JQAAAAAAAAAAAAAAAAAAAA01"
Then the recorded decisions should have parent "01JQBBBBBBBBBBBBBBBBBBBB01"
# === Collect invariants separately ===
@validation
Scenario: Creating actor with None invariant_service raises ValueError
Then creating an actor with None invariant_service should raise ValueError
@validation
Scenario: Creating actor with None decision_service raises ValueError
Then creating an actor with None decision_service should raise ValueError
@validation
Scenario: Reconciliation with no plan_id scope omits plan invariants
Given a global invariant "Global rule" from source "system"
When I reconcile without-plan-id using project "local/api-service"
Then the collected plan invariants should be empty
@collection
Scenario: Collect invariants gathers from all scopes
Given a global invariant "Global rule" from source "system"
And a project invariant "Project rule" from source "local/api-service" for project "local/api-service"
And an action invariant "Action rule" from source "local/deploy" for action "local/deploy"
And a plan invariant "Plan rule" from source "01JQAAAAAAAAAAAAAAAAAAAA01"
When I collect invariants for plan "01JQAAAAAAAAAAAAAAAAAAAA01" with project "local/api-service" and action "local/deploy"
Then the collected scope invariants should have 1 global invariant
And the collected scope invariants should have 1 project invariant
And the collected scope invariants should have 1 action invariant
And the collected scope invariants should have 1 plan invariant