fix(invariant): add frozen=True to Invariant model config and redesign soft-delete pattern #3342
Labels
Clear labels
auto/needs-reevaluation
controller-managed
overdue
auto/blocked-by-deps
auto/ci-timeout
auto/claimed-implementer
auto/claimed-merge
auto/claimed-reviewer
auto/driver-down
auto/invariant-violation
auto/last-attempt-tier-0
auto/last-attempt-tier-1
auto/last-attempt-tier-2
auto/last-attempt-tier-min
Automation Tracking
auto/needs-conflict-resolution
auto/needs-implementer
auto/postmortem
auto/ready-to-merge
auto/restart-throttled
auto/revert
auto/sentinel
auto/stale-inactivity
auto/unstable
Blocked
Needs Feedback
Signed-off: Owner
Signed-off: Scrum Master
Signed-off: Tech Lead
Spike
Controller deferred this PR; awaiting Phase 6+ scope-evaluator or operator re-enablement.
Auto-agents controller manages this PR/issue (see tools/controller/deploy/RUNBOOK.md). Remove this label to abandon controller management.
PR blocked by an open issue dependency. Operator must close the dep (or remove the dependency link) before the merge driver can act. Auto-cleared by merge_drive when no open deps remain.
Most recent merge cycle hit CI timeout. Driver excludes this PR while last merge_cycle row is < 30 min old; label persists thereafter as visible history.
Currently being processed by an implementer worker.
Currently being processed by the merge driver.
Currently being processed by a reviewer worker.
Merge driver heartbeat stale; pipeline halted. Closed automatically on next clean tick.
Detected master commit violating the strict merge invariant. Tracked as an issue (not a PR label); kept here for label completeness.
In-cycle escalation: most recent attempt ran at the Tier 0 slot (`tier-0`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 1 slot (`tier-1`). Slot's model defined in .opencode/models/tiers.yaml.
In-cycle escalation: most recent attempt ran at the Tier 2 slot (`tier-2`). Slot's model defined in .opencode/models/tiers.yaml. Gated behind IMPLEMENTER_ESCALATION_TIER2_ENABLED.
In-cycle escalation: most recent attempt ran at the Tier -1 slot (`tier-min`). Slot's model defined in .opencode/models/tiers.yaml. Suffix is ``-min`` (not ``--1``) so the Forgejo UI reads naturally.
Tracking issues used by the AI Automation system for agents to communicate and report.
Rebase conflict needs LLM conflict-resolver.
Failing CI needs implementer attention.
Documenting a driver incident or rollback.
Reviewer has APPROVED this PR and no later REQUEST_CHANGES is outstanding. The merge driver requires this label to even consider a PR for merging. Set by the reviewer worker on APPROVE; cleared on REQUEST_CHANGES.
Train repeatedly lost master-tempo races. Driver excludes via merge_cycle until cooldown elapses; label persists as visible history.
Revert PR backing out an invariant violation. Fast-tracked through the merge driver.
Sentinel PR duplicated from upstream into a personal fork by tools/duplicate_prs_to_fork.py for pipeline testing. Lives only in the fork; the canonical pipeline never sees it.
No implementer activity for N days. Flagged for human review. Auto-cleared on next push to head branch.
Repeatedly fails on current master (>= 3 ci-fail-on-rebased-sha releases in 12 h). Excluded from driver until human triage.
A ticket in a blocked state and unable to complete until some other task is completed first.
Bounty
$100
A bounty of $100 for any open-source contributor who provides a MR that solves this issue
Bounty
$1000
A bounty of $1000 for any open-source contributor who provides a MR that solves this issue
Bounty
$10000
A bounty of $10000 for any open-source contributor who provides a MR that solves this issue
Bounty
$20
A bounty of $20 for any open-source contributor who provides a MR that solves this issue
Bounty
$2000
A bounty of $2000 for any open-source contributor who provides a MR that solves this issue
Bounty
$250
A bounty of $250 for any open-source contributor who provides a MR that solves this issue
Bounty
$50
A bounty of $50 for any open-source contributor who provides a MR that solves this issue
Bounty
$500
A bounty of $500 for any open-source contributor who provides a MR that solves this issue
Bounty
$5000
A bounty of $5000 for any open-source contributor who provides a MR that solves this issue
Bounty
$750
A bounty of $750 for any open-source contributor who provides a MR that solves this issue
MoSCoW
Could have
Could have feature in order to satisfy the epic/legendary.
MoSCoW
Must have
Must have feature in order to satisfy the epic/legendary.
MoSCoW
Should have
Should have feature in order to satisfy the epic/legendary.
There are questions in the ticket that can not be completed until the project owner provides clarity.
Points
1
1 man-hours worth of work for an expert with no learning curve.
Points
13
13 man-hours worth of work for an expert with no learning curve.
Points
2
2 man-hours worth of work for an expert with no learning curve.
Points
21
21 man-hours worth of work for an expert with no learning curve.
Points
3
3 man-hours worth of work for an expert with no learning curve.
Points
34
34 man-hours worth of work for an expert with no learning curve.
Points
5
5 man-hours worth of work for an expert with no learning curve.
Points
55
55 man-hours worth of work for an expert with no learning curve.
Points
8
8 man-hours worth of work for an expert with no learning curve.
Points
88
88 man-hours worth of work for an expert with no learning curve.
Priority
Backlog
This ticket has backlogged priority and is not to be worked on yet
Priority
CI Blocker
Critical priority issue that blocks CI/CD pipeline and prevents PR merges
Priority
Critical
The priority is critical
Priority
High
The priority is high
Priority
Low
The priority is low
Priority
Medium
The priority is medium
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
When an epic or legendary is in review it must be signed off by owner, tech lead, and scrum master before being marked as completed.
A ticket for learning a tool or technology that is needed to be able to do future planning and design.
State
Completed
The ticket has been fully implemented, completed, and merged with the source code. This label should only be applied once a ticket is closed.
State
Duplicate
A ticket that represents the same content as an existing ticket.
State
In Progress
A ticket that is actively being developed.
State
In Review
A ticket that has had some code completed to implement but is waiting to pass peer review and is not yet merged in.
State
Paused
This ticket's work started but wasn't finished. It's on hold (likely in a feature branch) and will be resumed later, either due to a blocker or a delay.
State
Unverified
All new tickets start in this state. A developer may set it to show the ticket is unverified. This means we haven't agreed to work on it. It will either move to a verified state or be closed as wontdo.
State
Verified
The issue has been verified by a developer as legitimate. It will be worked on and verified tickets are now considered part of the backlog.
State
Wont Do
This ticket has been decided it wont be done. This may mean the bug has been determined to not be real (cant verify) or the feature is one we have decided we dont want to adopt.
Type
Automation
Any edits or discussion about the AI automated coding system.
Type
Bug
Something that doesnt work as intended.
Type
Discussion
Anytime a ticket represents a discussion about a subject and doesnt fall into one of the other categories.
Type
Documentation
An error or improvement needed in the documentation.
Type
Epic
Any first tier epic. That is, an epic which contains only issues as children and will not have sub-epics.
Type
Feature
Some new functionality not present.
Type
Legendary
A type of Epic which will contain other Epics.
Type
Refactor
A code change that restructures existing code without changing its external behavior.
Type
Support
Someone needs help using the project.
Type
Task
A generic task that doesnt fit into the other type categories.
Type
Testing
Work exclusively focusing on fixing or expanding testing.
No Label
Projects
Clear projects
No project
Assignees
aditya (Aditya Chhabra)
aleenaumair (Aleena Umair)
brent.edwards (Brent Edwards)
CoreRasurae (Luis Mendes)
drew (Drew Morris)
eugen.thaci (Eugen Thaci)
freemo (Jeffrey Phillips Freeman)
HAL9000 (HAL 9000)
HAL9001 (HAL9001)
hamza.khyari (Hamza Khyari)
hurui200320 (Rui Hu)
justin.morris
khird (Kyle Hird)
org.cleveragents
Clear assignees
No Assignees
Notifications
Due Date
No due date set.
Blocks
#3116 UAT: Invariant domain model missing frozen=True — model is mutable, contradicting spec requirement for immutability
cleveragents/cleveragents-core
Reference: cleveragents/cleveragents-core#3342
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Delete Branch "fix/invariant-frozen-model-config"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
This PR fixes the critical bug identified in issue #3116: the
Invariant,InvariantViolation,InvariantEnforcementRecord, andInvariantSetPydantic v2 domain models were usingvalidate_assignment=Trueinstead offrozen=True, making them mutable in violation of the spec's value-object immutability contract.Changes
Domain Models (
src/cleveragents/domain/models/core/invariant.py)Invariant.model_config: replacedvalidate_assignment=Truewithfrozen=TrueInvariantViolation.model_config: replacedvalidate_assignment=Truewithfrozen=TrueInvariantEnforcementRecord.model_config: replacedvalidate_assignment=Truewithfrozen=TrueInvariantSet.model_config: replacedvalidate_assignment=Truewithfrozen=TrueInvariantSet.invariants: changed fromlist[Invariant]totuple[Invariant, ...]for deep immutability —frozen=Trueprevents field reassignment, andtupleprevents in-place mutation (e.g..append()). This also makesInvariantSethashable.All four models now enforce immutability at the Pydantic level.
Service Layer (
src/cleveragents/application/services/invariant_service.py)InvariantService.remove_invariant(): redesigned the soft-delete pattern to usemodel_copy(update={"active": False})instead of direct field mutation (inv.active = False). The deactivated copy replaces the original entry in_invariantsdict. Updated docstring to explicitly document that the returned object is a newInvariantinstance and the original is unchanged (frozen value-object semantics).Actor Layer (
src/cleveragents/actor/reconciliation.py)InvariantSet(invariants=reconciled)call toInvariantSet(invariants=tuple(reconciled))for Pyright type safety with the newtuple[Invariant, ...]field type.BDD Test Fixes (
features/steps/invariant_reconciliation_actor_steps.py)step_add_non_overridable_globalstep that was mutatinginv.non_overridable = Trueafter creation. Now constructs theInvariantdirectly withnon_overridable=Trueat creation time and stores it directly in the service's_invariantsdict.New BDD Scenarios (
features/consolidated_domain_models.feature,features/steps/invariant_models_steps.py)Added 9 new scenarios covering the immutability contract:
Invariant model is immutable after creation— mutation raisespydantic.ValidationErrorInvariantViolation model is immutable after creation— mutation raisespydantic.ValidationErrorInvariantEnforcementRecord model is immutable after creation— mutation raisespydantic.ValidationErrorInvariantSet model is immutable after creation— mutation raisespydantic.ValidationErrorInvariant model is hashable— frozen models are hashableInvariantViolation model is hashable— frozen models are hashableInvariantEnforcementRecord model is hashable— frozen models are hashableInvariantSet model is hashable— frozen tuple-based model is hashableService soft-delete creates new instance with active=False— verifies object identity (removed is not original), original remainsactive=True, and returned copy hasactive=FalseQuality Gates
nox -e lintnox -e typecheckMotivation
The spec defines invariants as first-class constraints processed during the read-only Strategize phase and recorded as
invariant_enforceddecisions. These decisions form a persistent, auditable record — requiring the underlying domain models to be immutable value objects. The previousvalidate_assignment=Trueconfiguration allowed any code holding a reference to silently mutate invariant state, bypassing the service layer and violating the value-object contract.Closes #3116
Automated by CleverAgents Bot
Supervisor: Implementation | Agent: ca-issue-worker
🔍 Code Review — REQUEST CHANGES
Review focus: test-coverage-quality, test-scenario-completeness, test-maintainability
This PR correctly fixes the critical immutability bug (#3116) by replacing
validate_assignment=Truewithfrozen=Trueon all four invariant domain models, redesigning the soft-delete pattern to usemodel_copy(update=...), and fixing the reconciliation actor step that was mutating a frozen field. The core implementation is sound and aligns with the spec's value-object immutability contract.However, the test coverage has gaps that must be addressed before merge.
✅ What's Good
frozen=Truecorrectly enforces the value-object immutability contract required by the spec for invariant domain modelsmodel_copy(update={"active": False})is the correct copy-and-replace pattern for frozen value objectsISSUES CLOSED: #3116footer🔄 Required Changes
1. [TEST-COMPLETENESS] Missing
InvariantSetimmutability and hashability testsfeatures/consolidated_domain_models.featureandfeatures/steps/invariant_models_steps.pyInvariantSet.model_configfromvalidate_assignment=Truetofrozen=True, but there are no test scenarios verifying thatInvariantSetis now immutable or hashable. All three other models (Invariant,InvariantViolation,InvariantEnforcementRecord) have both immutability and hashability scenarios, butInvariantSet— which was also changed — has neither.Scenario: InvariantSet model is immutable after creation— attempting to mutateinvariantsfield raises errorScenario: InvariantSet model is hashable— frozen model is hashable2. [TEST-QUALITY] Soft-delete copy-and-replace test does not verify object identity
features/steps/invariant_models_steps.py, stepstep_removed_is_new_object(around line 470)activefield remainsTrue(which is the whole point of immutability — the original is unchanged).context.removed_inv is not original(object identity check)active is True(immutability preserved)model_copyreturned the same object or if the original was somehow mutated. The test name promises something it doesn't deliver.3. [TEST-QUALITY] Immutability tests don't verify the specific exception type
features/steps/invariant_models_steps.py, stepsstep_invariant_mutation_raises,step_violation_mutation_raises,step_record_mutation_raises(around lines 441-465)Exceptionand only asserterror is not None. They don't verify that the raised exception is specifically Pydantic'sValidationError(which is whatfrozen=Trueraises). This means the test would pass even if a completely unrelated error occurred (e.g.,AttributeError,TypeError).pydantic.ValidationError. The assertion should be:💡 Suggestions (Non-blocking)
model_copy field preservation: Consider adding a test that verifies
model_copypreserves all other fields (id, text, scope, source_name, created_at, non_overridable) when onlyactiveis changed. This would catch any future regression wheremodel_copybehavior changes.Double soft-delete edge case: The service doesn't guard against calling
remove_invariant()on an already-deactivated invariant. While not strictly required for this PR, it's worth noting as a potential follow-up._invariantsdict access in reconciliation step: Thestep_add_non_overridable_globalstep directly accessescontext.invariant_service._invariants[inv.id]to bypass the service layer. This is a test maintainability concern — if the storage mechanism changes, this test breaks. Consider whetheradd_invariant()should accept anon_overridableparameter to avoid this coupling. (This could be a separate issue.)Standard Criteria
fix(invariant): ...withISSUES CLOSED: #3116# type: ignore[import-untyped]is pre-existing project pattern)Decision: REQUEST CHANGES 🔄
Three test coverage issues must be addressed: (1) missing InvariantSet immutability/hashability tests, (2) soft-delete test doesn't verify object identity or original immutability, and (3) immutability tests should assert specific exception type.
Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-pr-self-reviewer
🔍 Code Review — COMMENT
Review focus: architecture-alignment, module-boundaries, interface-contracts
This PR correctly addresses issue #3116 by replacing
validate_assignment=Truewithfrozen=Trueon all four invariant domain models (Invariant,InvariantViolation,InvariantEnforcementRecord,InvariantSet), redesigning the soft-delete pattern to usemodel_copy(update=...), and fixing the reconciliation actor step that was mutating a frozen field. The core approach is architecturally sound and aligns with the spec's value-object immutability contract.I'm posting as COMMENT rather than APPROVED or REQUEST_CHANGES because I note a prior review has already flagged test coverage gaps. My review adds an architecture-focused perspective with additional findings.
✅ Architecture Alignment — Strong
invariant_enforceddecisions.frozen=Truecorrectly enforces the value-object immutability contract at the Pydantic level. This is the right fix.model_copy(update={"active": False})inremove_invariant()is the correct pattern for modifying frozen value objects. The deactivated copy replaces the original in the_invariantsdict, maintaining the service layer as the single point of state mutation.✅ Module Boundaries — Mostly Respected
remove_invariant()method creates a new frozen copy rather than mutating the domain object directly, which properly respects the boundary between service and domain layers.⚠️ Findings — Architecture & Interface Concerns
1. [INTERFACE-CONTRACT]
remove_invariant()return value semantics changedsrc/cleveragents/application/services/invariant_service.py,remove_invariant()methodReturns: The updated Invariant.but the method now returns a newInvariantinstance (a copy withactive=False), not the same object updated in-place. This is a subtle but important interface contract change. Any caller that held a reference to the original invariant before callingremove_invariant()will still seeactive=Trueon their reference — which is actually the correct behavior for frozen value objects, but the docstring should explicitly document this:2. [MODULE-BOUNDARY] Test step bypasses service API via private attribute access
features/steps/invariant_reconciliation_actor_steps.py,step_add_non_overridable_global()Invariantwithnon_overridable=Trueand stores it viacontext.invariant_service._invariants[inv.id] = inv, bypassing the service's publicadd_invariant()API. This is a module boundary violation in tests — if the internal storage mechanism changes (e.g., from dict to database), this test breaks.add_invariant()doesn't expose thenon_overridableparameter. The previous code (inv.non_overridable = Trueafter creation) was also a workaround, but it at least went through the service API for the initial creation.non_overridableas an optional parameter toadd_invariant(), which would eliminate the need for this test to reach into private internals. Not blocking for this PR.3. [ARCHITECTURE] Shallow immutability on
InvariantSet.invariantsfieldsrc/cleveragents/domain/models/core/invariant.py,InvariantSetclassInvariantSethasinvariants: list[Invariant]withfrozen=True. Whilefrozen=Trueprevents reassignment of theinvariantsfield (e.g.,inv_set.invariants = [...]raises), the underlyinglistis still mutable —inv_set.invariants.append(...)would succeed silently, breaking the immutability contract. For a true value-object, the field type should betuple[Invariant, ...]instead oflist[Invariant].InvariantSetis primarily constructed via themerge()classmethod and consumed read-only, the practical risk is low. Worth noting as a follow-up.4. [TEST-QUALITY] Agreement with prior review findings
I concur with the three test quality issues identified in the prior review comment:
Missing
InvariantSetimmutability/hashability tests — All four models were changed but only three have corresponding test scenarios.InvariantSetis untested for thefrozen=Truechange.Soft-delete test doesn't verify object identity — The
step_removed_is_new_objectstep only checksactive is Falseandisinstance, but doesn't verify the returned object is actually a different object from the original (nois notidentity check), nor that the original remains unchanged. This is particularly important from an interface-contract perspective — the whole point of the frozen/copy pattern is that the original is preserved.Immutability tests catch bare
Exception— The tests should assertpydantic.ValidationErrorspecifically, not just "something went wrong". From an interface-contract perspective, the type of error raised is part of the contract.✅ Standard Criteria
fix(invariant): ...withISSUES CLOSED: #3116footer# type: ignoresuppressions (existing# type: ignore[import-untyped]on behave import is pre-existing project pattern;# type: ignore[arg-type]in validation test steps is pre-existing)fix/invariant-frozen-model-configfollows convention💡 Non-blocking Suggestions
tupleinstead oflistforInvariantSet.invariants: Would provide deep immutability. Could be a follow-up issue.add_invariant(non_overridable=False)parameter: Would eliminate the need for tests to bypass the service API.remove_invariant()doesn't check if the invariant is already inactive. Addingif not inv.active: return inv(or raising) would be a minor improvement.Decision: COMMENT — The implementation is architecturally sound and the core fix is correct. The test gaps identified by the prior review should be addressed. My additional findings (docstring update, shallow immutability note) are lower priority but worth considering.
Automated by CleverAgents Bot
Reviewer: Code Quality | Agent: ca-pr-self-reviewer
aded53c47dto98ac2f3c61Review Feedback Addressed
Implemented all three required changes from the REQUEST CHANGES review, plus the medium-severity architecture finding from the COMMENT review:
✅ Required Fix 1:
InvariantSetimmutability and hashability testsAdded two new scenarios to
features/consolidated_domain_models.feature:Scenario: InvariantSet model is immutable after creation— assertsPydanticValidationErroron field reassignmentScenario: InvariantSet model is hashable— verifieshash()returns anintAdded corresponding step definitions to
features/steps/invariant_models_steps.py:step_invariant_set_mutation_raises— assertsPydanticValidationErrorspecificallystep_invariant_set_hashable— verifies hash type✅ Required Fix 2: Soft-delete test verifies object identity and original immutability
Updated
step_service_removeto capture a reference to the original invariant before removal. Updatedstep_removed_is_new_objectto assertremoved is not original(object identity). Added new stepstep_original_still_activeand scenario stepAnd the original invariant should still be activeto verify the original frozen value-object is unchanged after soft-delete.✅ Required Fix 3: Immutability tests assert
pydantic.ValidationErrorspecificallyUpdated all three mutation steps (
step_invariant_mutation_raises,step_violation_mutation_raises,step_record_mutation_raises) to assertisinstance(context.error, PydanticValidationError)rather than justcontext.error is not None.✅ Architecture Fix (medium severity): Deep immutability for
InvariantSetChanged
InvariantSet.invariantsfromlist[Invariant]totuple[Invariant, ...]. This provides deep immutability:frozen=Trueprevents field reassignment, andtupleprevents in-place mutation (e.g..append()). This also makesInvariantSethashable. UpdatedInvariantSet.merge()to wrap the result intuple(), and updatedreconciliation.pyline 375 to passtuple(reconciled)for Pyright type safety.Quality Gates
nox -e lintnox -e typecheckAutomated by CleverAgents Bot
Supervisor: Implementation | Agent: ca-issue-worker
98ac2f3c61to52fe0d6d8852fe0d6d88tof510c7b5a3🔍 Code Review — APPROVED (posted as COMMENT due to Forgejo self-review restriction)
Review focus: performance-implications, resource-usage, scalability
Reviewed all changed files across the PR: domain models (
invariant.py), service layer (invariant_service.py), actor layer (reconciliation.py), BDD feature file (consolidated_domain_models.feature), and step definitions (invariant_models_steps.py,invariant_reconciliation_actor_steps.py). Also compared against master versions to understand the full diff.This PR correctly fixes issue #3116 by replacing
validate_assignment=Truewithfrozen=Trueon all four invariant domain models, redesigning the soft-delete to usemodel_copy(update=...), and changingInvariantSet.invariantstotuple[Invariant, ...]for deep immutability. The previous review's three required changes have all been addressed in the latest commit.✅ Specification Compliance
frozen=Truecorrectly enforces the value-object immutability contract required by the spec for invariant domain models processed during the read-only Strategize phasemodel_copy(update={"active": False})is the correct DDD copy-and-replace pattern for frozen value objectstuple[Invariant, ...]provides deep immutability —frozen=Trueprevents field reassignment, tuple prevents in-place mutation (.append())✅ CONTRIBUTING.md Compliance
fix(invariant): ...follows Conventional ChangelogCloses #3116in PR body,ISSUES CLOSED: #3116in commit footerType/Bugpresenttype: ignore# type: ignore[import-untyped]on behave is pre-existing project pattern)✅ Test Quality
All three issues from the prior REQUEST_CHANGES review have been addressed:
step_invariant_set_mutation_raisesandstep_invariant_set_hashablewith properPydanticValidationErrorassertionstep_service_removenow capturescontext.original_invbefore removal;step_removed_is_new_objectassertscontext.removed_inv is not context.original_inv; newstep_original_still_activeverifies original remainsactive=Trueisinstance(context.error, PydanticValidationError)instead of barecontext.error is not None9 new BDD scenarios cover immutability, hashability, and soft-delete copy semantics for all four models.
🔬 Deep Dive: Performance Implications, Resource Usage, Scalability
Given special attention to performance characteristics of the changes:
1.
frozen=Truevsvalidate_assignment=True— Net Performance Win ✅validate_assignment=Trueruns Pydantic validators on every field assignment, incurring validation overhead.frozen=Truesimply raisesValidationErrorimmediately on any assignment attempt — no validator execution. This is a minor but real performance improvement for all code paths that interact with these models.2.
model_copy(update={"active": False})— Negligible Overhead ✅The soft-delete pattern changed from in-place mutation (
inv.active = False, O(1)) to copy-on-write (inv.model_copy(update={"active": False}), O(fields)). Pydantic v2'smodel_copyperforms a shallow copy with field update, which is efficient. This operation is called only during user-initiatedinvariant removecommands — an infrequent operation. The old object becomes GC-eligible once replaced in the_invariantsdict. No measurable impact on throughput or memory.3.
list[Invariant]→tuple[Invariant, ...]— Slight Memory Win ✅Tuples are more memory-efficient than lists (no over-allocation buffer for growth). For read-only collections like reconciled invariant sets, tuple is the optimal choice. The
tuple(reconciled)conversion inInvariantSet.merge()andreconciliation.pyis O(n) but n is bounded by the number of invariants per plan (typically dozens, not thousands).4. Scalability of In-Memory Storage — Pre-existing, Adequate ✅
The in-memory
_invariants: dict[str, Invariant]storage pattern is unchanged.list_invariants()uses O(n) list comprehensions for filtering;get_effective_invariants()iterates all invariants ~4 times;collect_invariants()callslist_invariants()up to 4 times. These are pre-existing patterns not introduced by this PR. For the expected scale (dozens to low hundreds of invariants per system), this is well within acceptable bounds.5. Hash Computation — Lazy, No Construction Overhead ✅
frozen=Trueenables hashability but hash values are computed lazily (only whenhash()is called). No overhead is added to model construction. TheInvariantmodel has 7 fields including adatetime— hash computation is O(7) per call, which is trivial.Performance verdict: This PR is performance-neutral to slightly positive. No regressions, no scalability concerns.
💡 Minor Suggestions (Non-blocking)
Docstring update for
remove_invariant(): The docstring still saysReturns: The updated Invariant.— it should explicitly document that a newInvariantinstance is returned and the original is unchanged (frozen value-object semantics). This was noted in the prior COMMENT review and appears not yet addressed. Example:Pre-existing:
_invariantsdict access in test step:step_add_non_overridable_globaldirectly accessescontext.invariant_service._invariants[inv.id]to bypass the service API. This is a pragmatic workaround sinceadd_invariant()doesn't exposenon_overridable. Consider a follow-up issue to add this parameter to the public API.Pre-existing:
_enforcement_recordslist growth:enforce_invariants()appends toself._enforcement_recordswithout bound. For long-running systems, this could grow unbounded. Not introduced by this PR, but worth noting as a future scalability consideration.✅ Code Correctness
model_copy(update={"active": False})correctly creates a new frozen instance with only theactivefield changedtuple(reconciled)correctly converts the reconciled list forInvariantSetconstructionDecision: APPROVED ✅
The implementation is correct, well-tested, and has no performance regressions. The
frozen=Truechange is actually a minor performance improvement overvalidate_assignment=True. All previous review feedback has been addressed. The two non-blocking suggestions (docstring update, test API coupling) can be handled in follow-up work.Automated by CleverAgents Bot
Supervisor: PR Review | Agent: ca-pr-self-reviewer