UAT: InvariantService.get_effective_invariants ignores action-scoped invariants (missing 4th tier) #6790

Open
opened 2026-04-10 02:06:16 +00:00 by HAL9000 · 0 comments
Owner

What Was Tested

Code analysis of InvariantService.get_effective_invariants() in src/cleveragents/application/services/invariant_service.py and the merge_invariants() function in src/cleveragents/domain/models/core/invariant.py.

Expected Behavior (From Spec)

Per docs/specification.md §Invariant (Glossary, line 92):

A natural-language constraint on plan execution scoped to global, project, action, or plan level. The runtime precedence chain is four-tier: plan > action > project > global. Exception: global invariants marked non_overridable always win regardless of scope.

The effective invariant set must incorporate all four tiers: plan, action, project, global — with action between plan and project in precedence.

Actual Behavior

InvariantService.get_effective_invariants() only collects invariants from three tiers, completely omitting the action scope:

# src/cleveragents/application/services/invariant_service.py lines 186-202
def get_effective_invariants(self, plan_id=None, project_name=None) -> list[Invariant]:
    # ...
    plan_invs = [inv for inv in active if inv.scope == InvariantScope.PLAN ...]
    project_invs = [inv for inv in active if inv.scope == InvariantScope.PROJECT ...]
    global_invs = [inv for inv in active if inv.scope == InvariantScope.GLOBAL]
    # ACTION invariants are NEVER collected!
    return merge_invariants(plan_invs, project_invs, global_invs)

The method signature also lacks an action_name parameter, so there is no way to pass the action context.

Similarly, the merge_invariants() standalone function in invariant.py only accepts three positional lists (plan, project, global) with no action parameter:

def merge_invariants(
    plan_invariants: list[Invariant],
    project_invariants: list[Invariant],
    global_invariants: list[Invariant],   # <-- action tier is entirely absent
) -> list[Invariant]:

Note: The InvariantReconciliationActor in src/cleveragents/actor/reconciliation.py does correctly handle all four tiers, suggesting get_effective_invariants and merge_invariants were not updated when the action scope was added to the actor.

Steps to Reproduce

from cleveragents.application.services.invariant_service import InvariantService
from cleveragents.domain.models.core.invariant import InvariantScope

svc = InvariantService()
# Add an action-scoped invariant
svc.add_invariant("No TCP calls", InvariantScope.ACTION, "local/test-action")

# get_effective_invariants will NOT return it even though it should
effective = svc.get_effective_invariants(plan_id=None, project_name=None)
assert any(inv.text == "No TCP calls" for inv in effective)  # FAILS - action invariant is dropped

Also visible via CLI:

agents invariant add --action local/test-action "No TCP calls"
agents invariant list --effective  # Does NOT show the action-scoped invariant

Impact

Action-scoped invariants are silently dropped from the effective invariant set when using get_effective_invariants() directly. This breaks the precedence guarantee the spec defines, and means agents invariant list --effective returns incomplete results for any plan that has action-scoped invariants.


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

## What Was Tested Code analysis of `InvariantService.get_effective_invariants()` in `src/cleveragents/application/services/invariant_service.py` and the `merge_invariants()` function in `src/cleveragents/domain/models/core/invariant.py`. ## Expected Behavior (From Spec) Per `docs/specification.md` §Invariant (Glossary, line 92): > A natural-language constraint on plan execution scoped to global, project, action, or plan level. The runtime precedence chain is **four-tier: plan > action > project > global**. Exception: global invariants marked `non_overridable` always win regardless of scope. The effective invariant set must incorporate all **four** tiers: plan, action, project, global — with action between plan and project in precedence. ## Actual Behavior `InvariantService.get_effective_invariants()` only collects invariants from **three** tiers, completely omitting the action scope: ```python # src/cleveragents/application/services/invariant_service.py lines 186-202 def get_effective_invariants(self, plan_id=None, project_name=None) -> list[Invariant]: # ... plan_invs = [inv for inv in active if inv.scope == InvariantScope.PLAN ...] project_invs = [inv for inv in active if inv.scope == InvariantScope.PROJECT ...] global_invs = [inv for inv in active if inv.scope == InvariantScope.GLOBAL] # ACTION invariants are NEVER collected! return merge_invariants(plan_invs, project_invs, global_invs) ``` The method signature also lacks an `action_name` parameter, so there is no way to pass the action context. Similarly, the `merge_invariants()` standalone function in `invariant.py` only accepts three positional lists (plan, project, global) with no action parameter: ```python def merge_invariants( plan_invariants: list[Invariant], project_invariants: list[Invariant], global_invariants: list[Invariant], # <-- action tier is entirely absent ) -> list[Invariant]: ``` **Note:** The `InvariantReconciliationActor` in `src/cleveragents/actor/reconciliation.py` *does* correctly handle all four tiers, suggesting `get_effective_invariants` and `merge_invariants` were not updated when the action scope was added to the actor. ## Steps to Reproduce ```python from cleveragents.application.services.invariant_service import InvariantService from cleveragents.domain.models.core.invariant import InvariantScope svc = InvariantService() # Add an action-scoped invariant svc.add_invariant("No TCP calls", InvariantScope.ACTION, "local/test-action") # get_effective_invariants will NOT return it even though it should effective = svc.get_effective_invariants(plan_id=None, project_name=None) assert any(inv.text == "No TCP calls" for inv in effective) # FAILS - action invariant is dropped ``` Also visible via CLI: ```bash agents invariant add --action local/test-action "No TCP calls" agents invariant list --effective # Does NOT show the action-scoped invariant ``` ## Impact Action-scoped invariants are silently dropped from the effective invariant set when using `get_effective_invariants()` directly. This breaks the precedence guarantee the spec defines, and means `agents invariant list --effective` returns incomplete results for any plan that has action-scoped invariants. --- **Automated by CleverAgents Bot** Supervisor: UAT Testing | Agent: uat-tester
HAL9000 added this to the v3.4.0 milestone 2026-04-10 02:06:22 +00:00
HAL9000 self-assigned this 2026-04-10 06:07:01 +00:00
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#6790
No description provided.