"""Savings ledger — LOAD-BEARING (parity rows 2-3; §6-style determinism, §11). The seam this file keeps alive: realized savings enter the book ONLY through the fail-closed expert gate (an explicit approved verdict + expert identity + explicit timestamp), and the sum key is DIMENSION-FREE — the same realized candidate surfaced via two dimensions lands in ONE slot, never double-counted. RED when an unapproved verdict's numbers enter the book (detach point 1: the decision gate in ``realize``), or when the mint key starts carrying the dimension (detach point 2: dimension exclusion in the sum key). Ledger semantics are a STACK-LOCAL contract mirrored from the MAF plan's capability description (typed store, fail-closed realize, dimension-free sum key) — never from MAF code. """ from __future__ import annotations import math from pathlib import Path import pytest from pydantic import ValidationError from portfolio_optimiser_claude.contracts import FeedbackContract from portfolio_optimiser_claude.ledger import LedgerGateError, SavingsLedger APPROVED = FeedbackContract(decision="approved", rationale="Verified on-site by expert.") REJECTED = FeedbackContract(decision="rejected", rationale="Numbers did not hold up.") TIMESTAMP = "2026-07-17T03:00:00Z" EXPERT = "persona:expert-reviewer" def _realize( ledger: SavingsLedger, *, project: str = "bygg-energi-mikro", measure_type: str = "led-retrofit", affected_codes: frozenset[str] = frozenset({"E01"}), amount_nok: float = 25000.0, verdict: FeedbackContract = APPROVED, expert: str = EXPERT, timestamp: str = TIMESTAMP, dimension: str | None = None, ) -> object: return ledger.realize( project=project, measure_type=measure_type, affected_codes=affected_codes, amount_nok=amount_nok, verdict=verdict, expert=expert, timestamp=timestamp, dimension=dimension, ) class TestExpertGate: """LOAD-BEARING (§11): only expert-APPROVED savings are realized — fail-closed.""" def test_rejected_verdict_is_refused_entering_nothing(self) -> None: ledger = SavingsLedger() with pytest.raises(LedgerGateError): _realize(ledger, verdict=REJECTED) assert len(ledger) == 0 assert ledger.total_realized_nok() == 0.0 def test_blank_expert_identity_is_refused(self) -> None: ledger = SavingsLedger() with pytest.raises(LedgerGateError): _realize(ledger, expert=" ") assert len(ledger) == 0 def test_expert_is_an_explicit_required_argument(self) -> None: # No implicit expert — realization without a named expert is a call error. with pytest.raises(TypeError): SavingsLedger().realize( # type: ignore[call-arg] project="p", measure_type="m", affected_codes=frozenset({"E01"}), amount_nok=1.0, verdict=APPROVED, timestamp=TIMESTAMP, ) def test_timestamp_is_an_explicit_required_argument(self) -> None: # No wall-clock default — realization is deterministic and reproducible (§6-style). with pytest.raises(TypeError): SavingsLedger().realize( # type: ignore[call-arg] project="p", measure_type="m", affected_codes=frozenset({"E01"}), amount_nok=1.0, verdict=APPROVED, expert=EXPERT, ) @pytest.mark.parametrize("amount", [0.0, -25000.0, math.inf, math.nan]) def test_non_positive_or_non_finite_amounts_are_refused(self, amount: float) -> None: ledger = SavingsLedger() with pytest.raises(ValidationError): _realize(ledger, amount_nok=amount) assert len(ledger) == 0 class TestDimensionFreeSumKey: """LOAD-BEARING (§11): the sum key excludes the dimension — no double counting.""" def test_same_candidate_under_two_dimensions_is_one_slot(self) -> None: # Key assumption (K1 plan): the same realized saving surfaced via two # dimensions in the same project must NOT be counted twice. ledger = SavingsLedger() first = _realize(ledger, dimension="energi") second = _realize(ledger, dimension="vedlikehold") assert len(ledger) == 1 assert ledger.total_realized_nok() == 25000.0 assert second == first # first-write-wins, §4.2-style idempotence def test_distinct_candidates_in_the_same_project_both_count(self) -> None: ledger = SavingsLedger() _realize(ledger, measure_type="led-retrofit", amount_nok=25000.0) _realize(ledger, measure_type="heat-recovery", amount_nok=40000.0) assert len(ledger) == 2 assert ledger.total_realized_nok() == 65000.0 def test_same_candidate_across_projects_both_count(self) -> None: ledger = SavingsLedger() _realize(ledger, project="prosjekt-a") _realize(ledger, project="prosjekt-b") assert len(ledger) == 2 assert ledger.total_realized_nok() == 50000.0 class TestDeterministicPersistence: """Deterministic JSON persistence: sort_keys, indent 2, LF, trailing newline.""" def _populated(self) -> SavingsLedger: ledger = SavingsLedger() _realize(ledger, project="prosjekt-b", amount_nok=40000.0) _realize(ledger, project="prosjekt-a", amount_nok=25000.0) return ledger def test_identical_sequences_persist_byte_identically(self, tmp_path: Path) -> None: path_a = tmp_path / "a.json" path_b = tmp_path / "b.json" self._populated().save(path_a) self._populated().save(path_b) assert path_a.read_bytes() == path_b.read_bytes() def test_file_is_lf_only_with_trailing_newline(self, tmp_path: Path) -> None: path = tmp_path / "ledger.json" self._populated().save(path) data = path.read_bytes() assert b"\r" not in data assert data.endswith(b"\n") def test_round_trip_is_byte_identical_and_sum_preserving(self, tmp_path: Path) -> None: original = tmp_path / "original.json" rewritten = tmp_path / "rewritten.json" ledger = self._populated() ledger.save(original) loaded = SavingsLedger.load(original) loaded.save(rewritten) assert rewritten.read_bytes() == original.read_bytes() assert loaded.total_realized_nok() == ledger.total_realized_nok() def test_load_fails_fast_on_a_malformed_entry(self, tmp_path: Path) -> None: path = tmp_path / "ledger.json" text = self._populated().to_json().replace("25000.0", "-25000.0") path.write_text(text, encoding="utf-8") with pytest.raises(ValidationError): SavingsLedger.load(path)