New ledger.py: typed SavingsLedger; realize is fail-closed on an APPROVED FeedbackContract + a named expert + an explicit timestamp (the §6 determinism rule — no wall-clock default). The sum key is DIMENSION-FREE (the dimension label is annotation only and never participates in the mint), so the same realized saving surfaced via two dimensions lands in one first-write-wins slot and is never double-counted. Deterministic JSON persistence (sort_keys, indent 2, LF, trailing newline), schema-validated on load. New goals.py: GoalContract (absolute target, hard/soft, fail-fast §10). A hard goal reached raises GoalReached, a structured stop event carrying target + observed — never a silent stop; soft flags without stopping. The percent-goal baseline is D-E-gated: the field is reserved and construction refuses with an explicit NotImplementedError. Semantics are marked STACK-LOCAL in the docstrings — mirrored from the MAF plan's capability description, never from MAF code; format shareability stays a proposed decision point in the brief. Two detach proofs delivered (decision gate removed -> red; dimension into the key mint -> the double-counting test red). 400 -> 426 tests; README synced (test count + a Value layer module block). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.5 KiB
Python
63 lines
2.5 KiB
Python
"""Goal contract (parity row 2): absolute target, hard/soft, fail-fast §10.
|
|
|
|
Hard goal reached -> a STRUCTURED stop signal (typed event, never silent);
|
|
soft goal reached -> a flag without stopping. The percent-goal baseline is
|
|
D-E-gated: attempting to construct one is an explicit ``NotImplementedError``
|
|
refusal, never silent semantics.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from portfolio_optimiser_claude.goals import GoalContract, GoalReached
|
|
|
|
|
|
class TestFailFastConstruction:
|
|
"""§10: a malformed goal never constructs."""
|
|
|
|
@pytest.mark.parametrize("target", [0.0, -100000.0, math.inf, math.nan])
|
|
def test_non_positive_or_non_finite_target_is_refused(self, target: float) -> None:
|
|
with pytest.raises(ValidationError):
|
|
GoalContract(target_nok=target, mode="hard")
|
|
|
|
def test_unknown_mode_is_refused(self) -> None:
|
|
with pytest.raises(ValidationError):
|
|
GoalContract(target_nok=100000.0, mode="maybe") # type: ignore[arg-type]
|
|
|
|
def test_percent_goal_is_an_explicit_gated_refusal(self) -> None:
|
|
# D-E-gated: the field is reserved, the semantics are NOT implemented —
|
|
# construction refuses loudly instead of guessing a baseline.
|
|
with pytest.raises(NotImplementedError, match="D-E"):
|
|
GoalContract(target_nok=100000.0, mode="hard", target_percent=10.0)
|
|
|
|
|
|
class TestHardGoal:
|
|
"""Hard goal reached -> typed stop event carrying target + observed."""
|
|
|
|
def test_reaching_the_target_raises_a_structured_stop(self) -> None:
|
|
contract = GoalContract(target_nok=100000.0, mode="hard")
|
|
with pytest.raises(GoalReached) as excinfo:
|
|
contract.check(125000.0)
|
|
assert excinfo.value.target_nok == 100000.0
|
|
assert excinfo.value.observed_nok == 125000.0
|
|
|
|
def test_exactly_at_the_target_counts_as_reached(self) -> None:
|
|
with pytest.raises(GoalReached):
|
|
GoalContract(target_nok=100000.0, mode="hard").check(100000.0)
|
|
|
|
def test_under_the_target_returns_false_without_raising(self) -> None:
|
|
assert GoalContract(target_nok=100000.0, mode="hard").check(99999.0) is False
|
|
|
|
|
|
class TestSoftGoal:
|
|
"""Soft goal reached -> a flag, never a stop."""
|
|
|
|
def test_reaching_the_target_flags_without_stopping(self) -> None:
|
|
assert GoalContract(target_nok=100000.0, mode="soft").check(125000.0) is True
|
|
|
|
def test_under_the_target_returns_false(self) -> None:
|
|
assert GoalContract(target_nok=100000.0, mode="soft").check(50000.0) is False
|