UAT: merge_invariants() and InvariantSet.merge() implement 3-tier precedence (plan > project > global), missing ACTION scope from spec's 4-tier chain #6254

Open
opened 2026-04-09 18:17:48 +00:00 by HAL9000 · 0 comments
Owner

Background and Context

The specification (§92, §19081, §19092) defines the invariant precedence chain as a 4-tier system:

plan > action > project > global

This means action-scoped invariants should take precedence over project-scoped invariants when computing the effective invariant set for a plan.

Current Behavior

The merge_invariants() function and InvariantSet.merge() class method in src/cleveragents/domain/models/core/invariant.py implement only a 3-tier precedence chain (plan > project > global), completely omitting the ACTION scope:

def merge_invariants(
    plan_invariants: list[Invariant],
    project_invariants: list[Invariant],
    global_invariants: list[Invariant],  # ACTION scope missing!
) -> list[Invariant]:
    ...
    for inv_list in (plan_invariants, project_invariants, global_invariants):
        ...

The module docstring for invariant.py also incorrectly documents the merge precedence:

## Merge Precedence
When computing the effective set of invariants for a plan, the merge
order is **plan > project > global**.  ...

Additionally, InvariantService.get_effective_invariants() (src/cleveragents/application/services/invariant_service.py) also only handles 3 tiers:

def get_effective_invariants(
    self,
    plan_id: str | None = None,
    project_name: str | None = None,
) -> list[Invariant]:
    ...
    return merge_invariants(plan_invs, project_invs, global_invs)
    # action_invs never collected or merged!

Note: The InvariantReconciliationActor (src/cleveragents/actor/reconciliation.py) correctly implements all 4 scopes and the correct plan > action > project > global precedence. However, the domain model's merge_invariants() / InvariantSet.merge() utility functions and the InvariantService.get_effective_invariants() method — used by the agents invariant list --effective CLI path — are incorrect.

Expected Behavior

merge_invariants() should accept action-scoped invariants and apply the correct 4-tier precedence:

def merge_invariants(
    plan_invariants: list[Invariant],
    action_invariants: list[Invariant],   # Added
    project_invariants: list[Invariant],
    global_invariants: list[Invariant],
) -> list[Invariant]:
    ...
    for inv_list in (plan_invariants, action_invariants, project_invariants, global_invariants):
        ...

InvariantService.get_effective_invariants() should accept an action_name parameter and collect + merge action-scoped invariants.

InvariantSet.merge() should similarly be updated to accept 4 tiers.

The module docstring should be corrected to document plan > action > project > global.

Impact

  • agents invariant list --effective --plan <ID> returns incorrect results: action-scoped invariants that should override project-scoped invariants may be shadowed or shown with wrong precedence
  • Plans that rely on action-scoped invariants to restrict project-scoped ones will silently use wrong effective invariant sets
  • The inconsistency between InvariantReconciliationActor (correct 4-tier) and merge_invariants (incorrect 3-tier) creates a confusing dual-truth situation in the codebase

Acceptance Criteria

  • merge_invariants() accepts action_invariants as the second parameter (between plan and project)
  • InvariantSet.merge() accepts action_invariants parameter
  • InvariantService.get_effective_invariants() accepts action_name and collects action-scoped invariants
  • Module docstring for invariant.py updated to document 4-tier precedence
  • agents invariant list --effective uses the corrected 4-tier merge
  • All existing callers of merge_invariants() and InvariantSet.merge() updated
  • Unit tests (Behave) cover the 4-tier precedence behavior
  • nox (all default sessions) passes

Supporting Information

  • Spec reference: docs/specification.md line 92: plan > action > project > global; §19081, §19092 confirm 4-tier chain
  • Broken file: src/cleveragents/domain/models/core/invariant.pymerge_invariants() and InvariantSet.merge()
  • Broken file: src/cleveragents/application/services/invariant_service.pyInvariantService.get_effective_invariants()
  • Correct implementation (reference): src/cleveragents/actor/reconciliation.pyInvariantReconciliationActor.collect_invariants() correctly collects all 4 scopes

Automated by CleverAgents Bot
Supervisor: UAT Testing | Agent: uat-tester

## Background and Context The specification (§92, §19081, §19092) defines the invariant precedence chain as a **4-tier** system: > `plan > action > project > global` This means action-scoped invariants should take precedence over project-scoped invariants when computing the effective invariant set for a plan. ## Current Behavior The `merge_invariants()` function and `InvariantSet.merge()` class method in `src/cleveragents/domain/models/core/invariant.py` implement only a **3-tier** precedence chain (`plan > project > global`), completely omitting the `ACTION` scope: ```python def merge_invariants( plan_invariants: list[Invariant], project_invariants: list[Invariant], global_invariants: list[Invariant], # ACTION scope missing! ) -> list[Invariant]: ... for inv_list in (plan_invariants, project_invariants, global_invariants): ... ``` The module docstring for `invariant.py` also incorrectly documents the merge precedence: ``` ## Merge Precedence When computing the effective set of invariants for a plan, the merge order is **plan > project > global**. ... ``` Additionally, `InvariantService.get_effective_invariants()` (`src/cleveragents/application/services/invariant_service.py`) also only handles 3 tiers: ```python def get_effective_invariants( self, plan_id: str | None = None, project_name: str | None = None, ) -> list[Invariant]: ... return merge_invariants(plan_invs, project_invs, global_invs) # action_invs never collected or merged! ``` **Note:** The `InvariantReconciliationActor` (`src/cleveragents/actor/reconciliation.py`) correctly implements all 4 scopes and the correct `plan > action > project > global` precedence. However, the domain model's `merge_invariants()` / `InvariantSet.merge()` utility functions and the `InvariantService.get_effective_invariants()` method — used by the `agents invariant list --effective` CLI path — are incorrect. ## Expected Behavior `merge_invariants()` should accept action-scoped invariants and apply the correct 4-tier precedence: ```python def merge_invariants( plan_invariants: list[Invariant], action_invariants: list[Invariant], # Added project_invariants: list[Invariant], global_invariants: list[Invariant], ) -> list[Invariant]: ... for inv_list in (plan_invariants, action_invariants, project_invariants, global_invariants): ... ``` `InvariantService.get_effective_invariants()` should accept an `action_name` parameter and collect + merge action-scoped invariants. `InvariantSet.merge()` should similarly be updated to accept 4 tiers. The module docstring should be corrected to document `plan > action > project > global`. ## Impact - `agents invariant list --effective --plan <ID>` returns incorrect results: action-scoped invariants that should override project-scoped invariants may be shadowed or shown with wrong precedence - Plans that rely on action-scoped invariants to restrict project-scoped ones will silently use wrong effective invariant sets - The inconsistency between `InvariantReconciliationActor` (correct 4-tier) and `merge_invariants` (incorrect 3-tier) creates a confusing dual-truth situation in the codebase ## Acceptance Criteria - [ ] `merge_invariants()` accepts `action_invariants` as the second parameter (between plan and project) - [ ] `InvariantSet.merge()` accepts `action_invariants` parameter - [ ] `InvariantService.get_effective_invariants()` accepts `action_name` and collects action-scoped invariants - [ ] Module docstring for `invariant.py` updated to document 4-tier precedence - [ ] `agents invariant list --effective` uses the corrected 4-tier merge - [ ] All existing callers of `merge_invariants()` and `InvariantSet.merge()` updated - [ ] Unit tests (Behave) cover the 4-tier precedence behavior - [ ] `nox` (all default sessions) passes ## Supporting Information - **Spec reference**: `docs/specification.md` line 92: `plan > action > project > global`; §19081, §19092 confirm 4-tier chain - **Broken file**: `src/cleveragents/domain/models/core/invariant.py` — `merge_invariants()` and `InvariantSet.merge()` - **Broken file**: `src/cleveragents/application/services/invariant_service.py` — `InvariantService.get_effective_invariants()` - **Correct implementation** (reference): `src/cleveragents/actor/reconciliation.py` — `InvariantReconciliationActor.collect_invariants()` correctly collects all 4 scopes --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: uat-tester
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
cleveragents/cleveragents-core#6254
No description provided.