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
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
237 lines
12 KiB
Gherkin
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
|