fix(invariant): restore ACTION scope in merge_invariants and InvariantSet.merge precedence chain #11143
+2
-1
@@ -186,7 +186,8 @@ Changed `wf10_batch.robot` to be less likely to create files, and
|
||||
were cleaned up in `features/actor_add_update_enforcement.feature` so the
|
||||
tests report correctly now that the underlying bug has been fixed.
|
||||
|
||||
- **Resolved Behave AmbiguousStep collisions in step definitions** (#4186): Renamed
|
||||
- **Fixed `merge_invariants()` missing ACTION scope — 4-tier precedence restored** (#9126): Updated ``merge_invariants()`` and ``InvariantSet.merge()`` to accept a fourth parameter ``action_invariants`` alongside plan, project, and global tiers. The module docstring, ``InvariantScope`` docstring, and ``InvariantService.get_effective_invariants()`` now all reflect the correct precedence chain: ``plan > action > project > global``. Added ``action_name`` parameter to ``get_effective_invariants()`` so action-scoped invariants are collected and passed through the merge pipeline instead of silently dropped. All docstrings across both files were corrected from ``plan > project > global`` to ``plan > action > project > global``. Comprehensive Behave scenarios added covering four-tier merge precedence, action-before-project ordering, action override of project with same text, and effective invariant computation with all four scopes.
|
||||
|
||||
step texts to avoid case-sensitive collisions between different step modules that
|
||||
prevented all Behave tests from loading. Renamed steps in
|
||||
`edge_case_plan_steps.py`, `plan_executor_coverage_boost_steps.py`,
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
Below are some of the specific details of various contributions.
|
||||
|
||||
* Jeffrey Phillips Freeman has acted as Lead Developer, daily contributor, and Project Owner.
|
||||
* Jeffrey Phillips Freeman has contributed an implementation for the invariant propagation fix (PR #10881 / issue #9131): added `_propagate_invariant_decisions()` to `SubplanService` to propagate all `invariant_enforced` decisions from parent plans to child plan decision trees during subplan spawn, satisfying the specification requirement for invariant propagation across hierarchical plan execution.
|
||||
* Jeffrey Phillips Freeman has contributed the invariant merge precedence fix (#9126): restored the missing ACTION scope in ``merge_invariants()`` and ``InvariantSet.merge()``, corrected all module docstrings from ``plan > project > global`` to the spec-compliant ``plan > action > project > global``, and added comprehensive BDD test coverage for four-tier merge precedence.
|
||||
* Brent E. Edwards has contributed quality assurance, test coverage, and CI pipeline improvements.
|
||||
* HAL 9000 has contributed automated implementation, bug fixes, and feature development as part of the CleverAgents automation pool.
|
||||
* HAL 9000 has contributed concurrency safety improvements, including thread-safe context tier management (issue #7547) for parallel plan execution.
|
||||
|
||||
@@ -74,7 +74,7 @@ class MergeSmallSuite:
|
||||
|
||||
def time_merge_small(self) -> None:
|
||||
"""Benchmark merge with ~13 invariants."""
|
||||
merge_invariants(self.plan, self.project, self.global_invs)
|
||||
merge_invariants(self.plan, [], self.project, self.global_invs)
|
||||
|
||||
|
||||
class MergeMediumSuite:
|
||||
@@ -88,7 +88,7 @@ class MergeMediumSuite:
|
||||
|
||||
def time_merge_medium(self) -> None:
|
||||
"""Benchmark merge with ~50 invariants."""
|
||||
merge_invariants(self.plan, self.project, self.global_invs)
|
||||
merge_invariants(self.plan, [], self.project, self.global_invs)
|
||||
|
||||
|
||||
class MergeLargeSuite:
|
||||
@@ -102,7 +102,7 @@ class MergeLargeSuite:
|
||||
|
||||
def time_merge_large(self) -> None:
|
||||
"""Benchmark merge with ~250 invariants."""
|
||||
merge_invariants(self.plan, self.project, self.global_invs)
|
||||
merge_invariants(self.plan, [], self.project, self.global_invs)
|
||||
|
||||
|
||||
class MergeDeduplicationSuite:
|
||||
@@ -137,7 +137,7 @@ class MergeDeduplicationSuite:
|
||||
|
||||
def time_merge_dedup(self) -> None:
|
||||
"""Benchmark merge with 60 invariants, all duplicates."""
|
||||
merge_invariants(self.plan, self.project, self.global_invs)
|
||||
merge_invariants(self.plan, [], self.project, self.global_invs)
|
||||
|
||||
|
||||
class InvariantSetMergeSuite:
|
||||
@@ -151,7 +151,7 @@ class InvariantSetMergeSuite:
|
||||
|
||||
def time_invariant_set_merge(self) -> None:
|
||||
"""Benchmark InvariantSet.merge()."""
|
||||
InvariantSet.merge(self.plan, self.project, self.global_invs)
|
||||
InvariantSet.merge(self.plan, [], self.project, self.global_invs)
|
||||
|
||||
|
||||
class ServiceEffectiveSuite:
|
||||
|
||||
@@ -418,7 +418,7 @@ Feature: Consolidated Domain Models
|
||||
Scenario: InvariantScope has four values
|
||||
Then InvariantScope should have values "global, project, action, plan"
|
||||
|
||||
# === Merge Precedence (plan > project > global) ===
|
||||
# === Merge Precedence (plan > action > project > global) ===
|
||||
|
||||
|
||||
Scenario: Merge with no duplicates preserves all invariants
|
||||
@@ -480,6 +480,75 @@ Feature: Consolidated Domain Models
|
||||
And the merged invariant at index 0 should have text "LOG ALL CHANGES"
|
||||
|
||||
|
||||
Scenario: Merge with all four scopes preserves action tier — Issue #9126
|
||||
Given I have plan invariants
|
||||
| text | source |
|
||||
| Plan rule | plan1 |
|
||||
And I have action invariants
|
||||
| text | source |
|
||||
| Action rule | action1 |
|
||||
And I have project invariants
|
||||
| text | source |
|
||||
| Project rule | proj1 |
|
||||
And I have global invariants
|
||||
| text | source |
|
||||
| Global rule | system |
|
||||
When I merge the invariants
|
||||
Then the merged set should have 4 invariants
|
||||
And plan invariants appear before action invariants in merge
|
||||
And action invariants appear before project invariants in merge
|
||||
And the merged invariant at index 0 should have text "Plan rule"
|
||||
And the merged invariant at index 1 should have text "Action rule"
|
||||
And the merged invariant at index 2 should have text "Project rule"
|
||||
And the merged invariant at index 3 should have text "Global rule"
|
||||
|
||||
|
||||
Scenario: Action invariant overrides project with same text
|
||||
Given I have plan invariants
|
||||
| text | source |
|
||||
And I have action invariants
|
||||
| text | source |
|
||||
| Log all changes | action1 |
|
||||
And I have project invariants
|
||||
| text | source |
|
||||
| Log all changes | proj1 |
|
||||
And I have global invariants
|
||||
| text | source |
|
||||
When I merge the invariants
|
||||
Then the merged set should have 1 invariants
|
||||
And the merged invariant at index 0 should have scope "action"
|
||||
|
||||
|
||||
Scenario: Action invariant is included in de-duplication with plan override
|
||||
Given I have plan invariants
|
||||
| text | source |
|
||||
| Log all changes | plan1 |
|
||||
And I have action invariants
|
||||
| text | source |
|
||||
| Log all changes | action1 |
|
||||
And I have project invariants
|
||||
| text | source |
|
||||
And I have global invariants
|
||||
| text | source |
|
||||
When I merge the invariants
|
||||
Then the merged set should have 1 invariants
|
||||
And the merged invariant at index 0 should have scope "plan"
|
||||
|
||||
|
||||
Scenario: Inactive action invariants are excluded from merge
|
||||
Given I have plan invariants with an inactive entry
|
||||
And I have action invariants
|
||||
| text | source |
|
||||
| Active | action1 |
|
||||
And I have project invariants
|
||||
| text | source |
|
||||
And I have global invariants
|
||||
| text | source |
|
||||
When I merge the invariants
|
||||
Then the merged set should have 1 invariants
|
||||
And the merged invariant at index 0 should have scope "action"
|
||||
|
||||
|
||||
Scenario: Inactive invariants are excluded from merge
|
||||
Given I have plan invariants with an inactive entry
|
||||
And I have project invariants
|
||||
@@ -492,7 +561,7 @@ Feature: Consolidated Domain Models
|
||||
# === InvariantSet merge class method ===
|
||||
|
||||
|
||||
Scenario: InvariantSet.merge produces correct result
|
||||
Scenario: InvariantSet.merge produces correct result
|
||||
Given I have plan invariants
|
||||
| text | source |
|
||||
| Plan rule | plan1 |
|
||||
@@ -505,7 +574,25 @@ Feature: Consolidated Domain Models
|
||||
When I merge using InvariantSet
|
||||
Then the invariant set should have 3 invariants
|
||||
|
||||
# === Service: Add/List/Remove ===
|
||||
|
||||
Scenario: InvariantSet.merge with four-tier precedence — Issue #9126
|
||||
Given I have plan invariants
|
||||
| text | source |
|
||||
| Plan rule | plan1 |
|
||||
And I have action invariants
|
||||
| text | source |
|
||||
| Action rule | action1 |
|
||||
And I have project invariants
|
||||
| text | source |
|
||||
| Project rule | proj1 |
|
||||
And I have global invariants
|
||||
| text | source |
|
||||
| Global rule | system |
|
||||
When I merge using InvariantSet
|
||||
Then the invariant set should have 4 invariants
|
||||
|
||||
|
||||
# === Service: Add/List/Remove ===
|
||||
|
||||
|
||||
Scenario: Service add invariant
|
||||
@@ -574,6 +661,16 @@ Feature: Consolidated Domain Models
|
||||
And the effective set should contain global invariants last
|
||||
|
||||
|
||||
Scenario: Effective invariants include action scope — Issue #9126
|
||||
Given an invariant service with invariants at all four scopes
|
||||
When I get effective invariants for plan "plan1", action "action1", and project "proj1"
|
||||
Then the effective set should contain plan invariants first
|
||||
And the effective set should contain action invariants second
|
||||
And the effective set should contain project invariants after action
|
||||
And the effective set should contain global invariants last
|
||||
And the effective set should have 4 invariants
|
||||
|
||||
|
||||
Scenario: Effective invariants de-duplicate across scopes
|
||||
Given an invariant service with duplicate text across scopes
|
||||
When I get effective invariants for plan "plan1" and project "proj1"
|
||||
|
||||
@@ -135,6 +135,11 @@ def step_plan_invariants(context):
|
||||
context.plan_invariants = _parse_invariant_table(context, InvariantScope.PLAN)
|
||||
|
||||
|
||||
@given("I have action invariants")
|
||||
def step_action_invariants(context):
|
||||
context.action_invariants = _parse_invariant_table(context, InvariantScope.ACTION)
|
||||
|
||||
|
||||
@given("I have project invariants")
|
||||
def step_project_invariants(context):
|
||||
context.project_invariants = _parse_invariant_table(context, InvariantScope.PROJECT)
|
||||
@@ -160,6 +165,7 @@ def step_plan_invariants_inactive(context):
|
||||
def step_merge(context):
|
||||
context.merged = merge_invariants(
|
||||
getattr(context, "plan_invariants", []),
|
||||
getattr(context, "action_invariants", []),
|
||||
getattr(context, "project_invariants", []),
|
||||
getattr(context, "global_invariants", []),
|
||||
)
|
||||
@@ -180,6 +186,43 @@ def step_merged_scope(context, idx, scope):
|
||||
assert context.merged[idx].scope.value == scope
|
||||
|
||||
|
||||
@then("action invariants appear before project invariants in merge")
|
||||
def step_action_before_project(context):
|
||||
"""Verify ACTION tier comes before PROJECT tier in the merged result."""
|
||||
action_invs = [i for i in context.merged if i.scope == InvariantScope.ACTION]
|
||||
project_invs = [i for i in context.merged if i.scope == InvariantScope.PROJECT]
|
||||
assert len(action_invs) > 0, "No action invariants found in merged result"
|
||||
assert len(project_invs) > 0, "No project invariants found in merged result"
|
||||
first_action_idx = context.merged.index(action_invs[0])
|
||||
first_project_idx = context.merged.index(project_invs[0])
|
||||
assert first_action_idx < first_project_idx, (
|
||||
f"Action invariant at index {first_action_idx} "
|
||||
f"should appear before project invariant at index {first_project_idx}"
|
||||
)
|
||||
|
||||
|
||||
@then("plan invariants appear before action invariants in merge")
|
||||
def step_plan_before_action(context):
|
||||
"""Verify PLAN tier comes before ACTION tier in the merged result."""
|
||||
plan_invs = [i for i in context.merged if i.scope == InvariantScope.PLAN]
|
||||
action_invs = [i for i in context.merged if i.scope == InvariantScope.ACTION]
|
||||
assert len(plan_invs) > 0, "No plan invariants found in merged result"
|
||||
assert len(action_invs) > 0, "No action invariants found in merged result"
|
||||
first_plan_idx = context.merged.index(plan_invs[0])
|
||||
first_action_idx = context.merged.index(action_invs[0])
|
||||
assert first_plan_idx < first_action_idx, (
|
||||
f"Plan invariant at index {first_plan_idx} "
|
||||
f"should appear before action invariant at index {first_action_idx}"
|
||||
)
|
||||
|
||||
|
||||
@then("action invariants are preserved in merge result")
|
||||
def step_action_invariants_preserved(context):
|
||||
"""Verify that action-scoped invariants appear in the merged output."""
|
||||
action_invs = [i for i in context.merged if i.scope == InvariantScope.ACTION]
|
||||
assert len(action_invs) > 0, "Expected action-scoped invariants in merge result"
|
||||
|
||||
|
||||
# ================================================================
|
||||
# InvariantSet
|
||||
# ================================================================
|
||||
@@ -189,6 +232,7 @@ def step_merged_scope(context, idx, scope):
|
||||
def step_merge_invariant_set(context):
|
||||
inv_set = InvariantSet.merge(
|
||||
getattr(context, "plan_invariants", []),
|
||||
getattr(context, "action_invariants", []),
|
||||
getattr(context, "project_invariants", []),
|
||||
getattr(context, "global_invariants", []),
|
||||
)
|
||||
@@ -389,6 +433,15 @@ def step_service_all_scopes(context):
|
||||
context.service.add_invariant("Plan rule", InvariantScope.PLAN, "plan1")
|
||||
|
||||
|
||||
@given("an invariant service with invariants at all four scopes")
|
||||
def step_service_all_four_scopes(context):
|
||||
context.service = InvariantService()
|
||||
context.service.add_invariant("Global rule", InvariantScope.GLOBAL, "system")
|
||||
context.service.add_invariant("Project rule", InvariantScope.PROJECT, "proj1")
|
||||
context.service.add_invariant("Action rule", InvariantScope.ACTION, "action1")
|
||||
context.service.add_invariant("Plan rule", InvariantScope.PLAN, "plan1")
|
||||
|
||||
|
||||
@when('I get effective invariants for plan "{plan_id}" and project "{project}"')
|
||||
def step_effective(context, plan_id, project):
|
||||
context.effective = context.service.get_effective_invariants(
|
||||
@@ -396,6 +449,15 @@ def step_effective(context, plan_id, project):
|
||||
)
|
||||
|
||||
|
||||
@when(
|
||||
'I get effective invariants for plan "{plan_id}", action "{action}", and project "{project}"'
|
||||
)
|
||||
def step_effective_with_action(context, plan_id, action, project):
|
||||
context.effective = context.service.get_effective_invariants(
|
||||
plan_id=plan_id, action_name=action, project_name=project
|
||||
)
|
||||
|
||||
|
||||
@then("the effective set should contain plan invariants first")
|
||||
def step_effective_plan_first(context):
|
||||
plan_invs = [i for i in context.effective if i.scope == InvariantScope.PLAN]
|
||||
@@ -404,6 +466,35 @@ def step_effective_plan_first(context):
|
||||
assert first_plan_idx == 0
|
||||
|
||||
|
||||
@then("the effective set should contain action invariants second")
|
||||
def step_effective_action_second(context):
|
||||
"""Verify ACTION tier appears after PLAN and before PROJECT."""
|
||||
plan_invs = [i for i in context.effective if i.scope == InvariantScope.PLAN]
|
||||
action_invs = [i for i in context.effective if i.scope == InvariantScope.ACTION]
|
||||
project_invs = [i for i in context.effective if i.scope == InvariantScope.PROJECT]
|
||||
assert len(plan_invs) > 0, "No plan invariants found"
|
||||
assert len(action_invs) > 0, "No action invariants found — bug #9126 not fixed!"
|
||||
assert len(project_invs) > 0, "No project invariants found"
|
||||
first_action_idx = context.effective.index(action_invs[0])
|
||||
if plan_invs:
|
||||
last_plan_idx = context.effective.index(plan_invs[-1])
|
||||
assert first_action_idx > last_plan_idx
|
||||
first_project_idx = context.effective.index(project_invs[0])
|
||||
assert first_project_idx > first_action_idx
|
||||
|
||||
|
||||
@then("the effective set should contain project invariants after action")
|
||||
def step_effective_project_after_action(context):
|
||||
"""Verify PROJECT tier appears after ACTION tier."""
|
||||
action_invs = [i for i in context.effective if i.scope == InvariantScope.ACTION]
|
||||
project_invs = [i for i in context.effective if i.scope == InvariantScope.PROJECT]
|
||||
assert len(action_invs) > 0, "No action invariants found"
|
||||
assert len(project_invs) > 0, "No project invariants found"
|
||||
first_proj_idx = context.effective.index(project_invs[0])
|
||||
last_action_idx = context.effective.index(action_invs[-1])
|
||||
assert first_proj_idx > last_action_idx
|
||||
|
||||
|
||||
@then("the effective set should contain project invariants second")
|
||||
def step_effective_project_second(context):
|
||||
proj_invs = [i for i in context.effective if i.scope == InvariantScope.PROJECT]
|
||||
@@ -435,6 +526,13 @@ def step_no_duplicates(context):
|
||||
assert len(texts) == len(set(texts)), f"Duplicates found: {texts}"
|
||||
|
||||
|
||||
@then("the effective set should have {count:d} invariants")
|
||||
def step_effective_count(context, count):
|
||||
assert len(context.effective) == count, (
|
||||
f"Expected {count}, got {len(context.effective)}"
|
||||
)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# Enforcement
|
||||
# ================================================================
|
||||
|
||||
@@ -867,6 +867,7 @@ def invariants_enforced_during_strategize() -> None:
|
||||
|
||||
merged = merge_invariants(
|
||||
plan_invariants=[plan_inv],
|
||||
action_invariants=[],
|
||||
project_invariants=[project_inv],
|
||||
global_invariants=[global_inv],
|
||||
)
|
||||
@@ -890,6 +891,7 @@ def invariants_enforced_during_strategize() -> None:
|
||||
|
||||
invariant_set = InvariantSet.merge(
|
||||
plan_invariants=[plan_inv],
|
||||
action_invariants=[],
|
||||
project_invariants=[project_inv],
|
||||
global_invariants=[global_inv],
|
||||
)
|
||||
|
||||
@@ -11,8 +11,8 @@ a dict keyed by invariant ID.
|
||||
|
||||
## Merge Precedence
|
||||
|
||||
Effective invariants are computed using plan > project > global order.
|
||||
See ``merge_invariants`` for de-duplication semantics.
|
||||
Effective invariants are computed using plan > action > project > global
|
||||
order. See ``merge_invariants`` for de-duplication semantics.
|
||||
|
||||
Based on ``docs/specification.md`` and implementation plan Stage M3.5.
|
||||
"""
|
||||
@@ -124,6 +124,7 @@ class InvariantService:
|
||||
if effective:
|
||||
return self.get_effective_invariants(
|
||||
plan_id=source_name if scope == InvariantScope.PLAN else None,
|
||||
action_name=source_name if scope == InvariantScope.ACTION else None,
|
||||
project_name=source_name if scope == InvariantScope.PROJECT else None,
|
||||
)
|
||||
|
||||
@@ -167,16 +168,22 @@ class InvariantService:
|
||||
def get_effective_invariants(
|
||||
self,
|
||||
plan_id: str | None = None,
|
||||
action_name: str | None = None,
|
||||
project_name: str | None = None,
|
||||
) -> list[Invariant]:
|
||||
"""Return the merged precedence chain for a plan/project context.
|
||||
"""Return the merged precedence chain for a plan/action/project context.
|
||||
|
||||
Collects active invariants from each scope tier and merges them
|
||||
using plan > project > global precedence.
|
||||
using plan > action > project > global precedence.
|
||||
|
||||
Args:
|
||||
plan_id: Optional plan identifier to collect plan-scoped
|
||||
invariants.
|
||||
action_name: Optional action name to collect action-scoped
|
||||
invariants (promoted to plan-level during reconciliation).
|
||||
When ``None``, the action tier is omitted for backward
|
||||
compatibility. Pass ``"*"`` to include all action-scoped
|
||||
invariants regardless of source name.
|
||||
project_name: Optional project name to collect project-scoped
|
||||
invariants.
|
||||
|
||||
@@ -191,6 +198,13 @@ class InvariantService:
|
||||
if inv.scope == InvariantScope.PLAN
|
||||
and (plan_id is None or inv.source_name == plan_id)
|
||||
]
|
||||
action_invs = [
|
||||
inv
|
||||
for inv in active
|
||||
if inv.scope == InvariantScope.ACTION
|
||||
and action_name is not None # Only include when explicitly requested
|
||||
and (inv.source_name == action_name or action_name == "*")
|
||||
]
|
||||
project_invs = [
|
||||
inv
|
||||
for inv in active
|
||||
@@ -199,7 +213,7 @@ class InvariantService:
|
||||
]
|
||||
global_invs = [inv for inv in active if inv.scope == InvariantScope.GLOBAL]
|
||||
|
||||
return merge_invariants(plan_invs, project_invs, global_invs)
|
||||
return merge_invariants(plan_invs, action_invs, project_invs, global_invs)
|
||||
|
||||
def enforce_invariants(
|
||||
self,
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
Invariants are natural-language constraints on plan execution, scoped at
|
||||
global, project, action, or plan level. When an action is used, its
|
||||
invariants are promoted to plan-level. The runtime precedence chain is:
|
||||
invariants are promoted to plan-level and participate in the merge.
|
||||
The runtime precedence chain is:
|
||||
|
||||
plan > project > global
|
||||
plan > action > project > global
|
||||
|
||||
They are reconciled by the Invariant Reconciliation Actor at the start
|
||||
of Strategize and recorded as ``invariant_enforced`` decisions.
|
||||
@@ -21,8 +22,8 @@ of Strategize and recorded as ``invariant_enforced`` decisions.
|
||||
## Merge Precedence
|
||||
|
||||
When computing the effective set of invariants for a plan, the merge
|
||||
order is **plan > project > global**. Duplicate texts (case-insensitive)
|
||||
are de-duplicated, keeping the highest-precedence copy.
|
||||
order is **plan > action > project > global**. Duplicate texts
|
||||
(case-insensitive) are de-duplicated, keeping the highest-precedence copy.
|
||||
|
||||
Based on ``docs/specification.md`` and implementation plan Stage M3.5.
|
||||
"""
|
||||
@@ -39,8 +40,10 @@ from ulid import ULID
|
||||
class InvariantScope(StrEnum):
|
||||
"""Scope at which an invariant applies.
|
||||
|
||||
Precedence (highest to lowest): PLAN > PROJECT > GLOBAL.
|
||||
ACTION invariants are promoted to PLAN scope at ``plan use`` time.
|
||||
Precedence (highest to lowest): PLAN > ACTION > PROJECT > GLOBAL.
|
||||
ACTION invariants are promoted to plan-level and participate in the
|
||||
merge during reconciliation, sitting between PLAN and PROJECT in the
|
||||
precedence chain.
|
||||
"""
|
||||
|
||||
GLOBAL = "global"
|
||||
@@ -137,10 +140,11 @@ class InvariantSet(BaseModel):
|
||||
def merge(
|
||||
cls,
|
||||
plan_invariants: list[Invariant],
|
||||
project_invariants: list[Invariant],
|
||||
global_invariants: list[Invariant],
|
||||
action_invariants: list[Invariant] | None = None,
|
||||
project_invariants: list[Invariant] | None = None,
|
||||
global_invariants: list[Invariant] | None = None,
|
||||
) -> InvariantSet:
|
||||
"""Merge invariants respecting plan > project > global precedence.
|
||||
"""Merge invariants respecting plan > action > project > global precedence.
|
||||
|
||||
De-duplicates by text (case-insensitive), keeping the copy from
|
||||
the highest-precedence tier. Within each tier, source ordering
|
||||
@@ -148,6 +152,9 @@ class InvariantSet(BaseModel):
|
||||
|
||||
Args:
|
||||
plan_invariants: Plan-level invariants (highest precedence).
|
||||
action_invariants: Action-scoped invariants (second-highest
|
||||
precedence; promoted to plan-level when an
|
||||
action is used).
|
||||
project_invariants: Project-level invariants.
|
||||
global_invariants: Global-level invariants (lowest precedence).
|
||||
|
||||
@@ -156,7 +163,12 @@ class InvariantSet(BaseModel):
|
||||
"""
|
||||
return cls(
|
||||
invariants=tuple(
|
||||
merge_invariants(plan_invariants, project_invariants, global_invariants)
|
||||
merge_invariants(
|
||||
plan_invariants,
|
||||
action_invariants or [],
|
||||
project_invariants or [],
|
||||
global_invariants or [],
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -165,10 +177,11 @@ class InvariantSet(BaseModel):
|
||||
|
||||
def merge_invariants(
|
||||
plan_invariants: list[Invariant],
|
||||
project_invariants: list[Invariant],
|
||||
global_invariants: list[Invariant],
|
||||
action_invariants: list[Invariant] | None = None,
|
||||
project_invariants: list[Invariant] | None = None,
|
||||
global_invariants: list[Invariant] | None = None,
|
||||
) -> list[Invariant]:
|
||||
"""Merge invariants implementing plan > project > global precedence.
|
||||
"""Merge invariants implementing plan > action > project > global precedence.
|
||||
|
||||
De-duplicates by text (case-insensitive). The first occurrence
|
||||
(from the highest-precedence tier) wins. Within each tier, the
|
||||
@@ -176,8 +189,13 @@ def merge_invariants(
|
||||
|
||||
Args:
|
||||
plan_invariants: Plan-level invariants (highest precedence).
|
||||
project_invariants: Project-level invariants.
|
||||
action_invariants: Action-scoped invariants (second-highest
|
||||
precedence; promoted to plan level when an action is used).
|
||||
Defaults to empty list when ``None``.
|
||||
project_invariants: Project-level invariants. Defaults to empty
|
||||
list when ``None``.
|
||||
global_invariants: Global-level invariants (lowest precedence).
|
||||
Defaults to empty list when ``None``.
|
||||
|
||||
Returns:
|
||||
A de-duplicated list of invariants in precedence order.
|
||||
@@ -185,7 +203,12 @@ def merge_invariants(
|
||||
seen: set[str] = set()
|
||||
result: list[Invariant] = []
|
||||
|
||||
for inv_list in (plan_invariants, project_invariants, global_invariants):
|
||||
for inv_list in (
|
||||
plan_invariants,
|
||||
action_invariants or [],
|
||||
project_invariants or [],
|
||||
global_invariants or [],
|
||||
):
|
||||
for inv in inv_list:
|
||||
if not inv.active:
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user