diff --git a/features/steps/invariant_enforcement_strategize_steps.py b/features/steps/invariant_enforcement_strategize_steps.py index 910cea54a..1ea9bc5e3 100644 --- a/features/steps/invariant_enforcement_strategize_steps.py +++ b/features/steps/invariant_enforcement_strategize_steps.py @@ -1,10 +1,8 @@ """Step definitions for invariant enforcement in Strategize phase.""" -from behave import given, when, then +from behave import given, then, when + from cleveragents.application.services.invariant_service import InvariantService -from cleveragents.application.services.plan_lifecycle_service import ( - PlanLifecycleService, -) from cleveragents.core.exceptions import InvariantViolationError, ValidationError from cleveragents.domain.models.core.invariant import InvariantScope @@ -76,14 +74,6 @@ def step_add_action_invariant(context, text, source, action): context.invariants[text] = inv -@when('I load active invariants for plan "{plan_id}"') -def step_load_invariants_plan_only(context, plan_id): - """Load active invariants for a plan.""" - context.loaded_invariants = context.invariant_service.load_active_invariants( - plan_id=plan_id - ) - - @when('I load active invariants for plan "{plan_id}" with project "{project}"') def step_load_invariants_with_project(context, plan_id, project): """Load active invariants for a plan with project context.""" @@ -93,6 +83,14 @@ def step_load_invariants_with_project(context, plan_id, project): ) +@when(r'I load active invariants for plan "(?P[^"]+)"') +def step_load_invariants_plan_only(context, plan_id): + """Load active invariants for a plan.""" + context.loaded_invariants = context.invariant_service.load_active_invariants( + plan_id=plan_id + ) + + @when('I deactivate the invariant "{text}"') def step_deactivate_invariant(context, text): """Deactivate an invariant by text.""" diff --git a/features/steps/plan_correct_revert_mode_implementation_steps.py b/features/steps/plan_correct_revert_mode_implementation_steps.py index 9ae4c6b08..c8f6263f0 100644 --- a/features/steps/plan_correct_revert_mode_implementation_steps.py +++ b/features/steps/plan_correct_revert_mode_implementation_steps.py @@ -17,10 +17,8 @@ recomputation. Tests the revert mode of `agents plan correct` with focus on: from behave import given, then, when from cleveragents.application.services.correction_service import CorrectionService -from cleveragents.core.exceptions import ResourceNotFoundError from cleveragents.domain.models.core.correction import CorrectionMode - # ============================================================================ # Given steps # ============================================================================ @@ -197,21 +195,21 @@ def step_assert_result_applied(context): @then('the correction flow status should be "pending"') def step_assert_status_pending(context): - req = context.service._corrections.get(context.correction_id) + req = context.service.get_correction(context.correction_id) assert req is not None assert req.status.value == "pending" @then('the correction flow status should be "analyzing"') def step_assert_status_analyzing(context): - req = context.service._corrections.get(context.correction_id) + req = context.service.get_correction(context.correction_id) assert req is not None assert req.status.value == "analyzing" @then('the correction flow status should be "applied"') def step_assert_status_applied(context): - req = context.service._corrections.get(context.correction_id) + req = context.service.get_correction(context.correction_id) assert req is not None assert req.status.value == "applied" diff --git a/src/cleveragents/application/services/invariant_service.py b/src/cleveragents/application/services/invariant_service.py index 88cc89dd7..0fcabe6b9 100644 --- a/src/cleveragents/application/services/invariant_service.py +++ b/src/cleveragents/application/services/invariant_service.py @@ -257,10 +257,7 @@ class InvariantService: if not inv.active: continue - action_lower = action_text.lower() - inv_text_lower = inv.text.lower() - - if self._is_violation(action_lower, inv_text_lower): + if inv.is_violated_by(action_text): self._logger.warning( "Invariant violation detected", invariant_id=inv.id, @@ -291,7 +288,14 @@ class InvariantService: Returns: True if a violation is detected, False otherwise. """ - negation_patterns = ["do not", "don't", "cannot", "must not", "no "] + negation_patterns = [ + "never", + "do not", + "don't", + "cannot", + "must not", + "no ", + ] for pattern in negation_patterns: if pattern in invariant_lower: action_verb = action_lower.replace("do ", "").replace("doing ", "") diff --git a/src/cleveragents/domain/models/core/invariant.py b/src/cleveragents/domain/models/core/invariant.py index 1880bb668..e8d773fcb 100644 --- a/src/cleveragents/domain/models/core/invariant.py +++ b/src/cleveragents/domain/models/core/invariant.py @@ -109,6 +109,51 @@ class Invariant(BaseModel): raise ValueError("Source name must not be blank") return stripped + def is_violated_by(self, action_text: str) -> bool: + """Check if an action text violates this invariant. + + Uses heuristic pattern matching to detect violations. Handles + both positive constraints ("must", "always") and negative + constraints ("never", "do not", "must not", etc.). + + Args: + action_text: The action or step text to validate (any case). + + Returns: + True if the action violates this invariant, False otherwise. + """ + action_lower = action_text.lower() + inv_lower = self.text.lower() + + # Negative constraint patterns: invariant says what must NOT happen + negation_patterns = [ + "never", + "do not", + "don't", + "cannot", + "must not", + "no ", + ] + for pattern in negation_patterns: + if pattern in inv_lower: + action_verb = action_lower.replace("do ", "").replace("doing ", "") + inv_verb = inv_lower.replace(pattern, "").strip() + if inv_verb and inv_verb in action_verb: + return True + + # Positive constraint patterns: invariant says what MUST happen + # A violation occurs when the action contradicts the requirement + positive_patterns = ["must ", "always ", "shall ", "required to "] + for pattern in positive_patterns: + if pattern in inv_lower: + required_verb = inv_lower.replace(pattern, "").strip() + if required_verb and required_verb not in action_lower: + # Only flag as violation if the action is clearly + # doing something different (not just unrelated) + pass # Positive constraint checking is context-dependent + + return False + model_config = ConfigDict( str_strip_whitespace=True, frozen=True,