234 lines
8.6 KiB
Python
234 lines
8.6 KiB
Python
"""Load-bearing seams for the savings ledger (Fase 1 — SC4/SC5/SC6/SC8).
|
|
|
|
Step 5 (SC4) — the dimension-free dedup gate: the same underlying candidate (same codes+measure+
|
|
amount) realized under two dimensions counts ONCE in the portfolio sum — double-counting across
|
|
dimensions is the exact error SC4 warns against — while the overlap is FLAGGED (never silently
|
|
merged). RED if the dimension-free sum-key dedup is detached (the sum doubles). Inverse control:
|
|
two DISTINCT realizations with the same codes+measure but a different amount stay SEPARATE (a
|
|
magnitude-free identity would collide them).
|
|
|
|
(Step 6 extends this file with the fail-closed ``realize`` gate + øre-boundary tests; Step 8 adds
|
|
the goal-stop detach.)
|
|
|
|
Pattern: ``tests/test_step8_promotion_loadbearing.py:77`` (fail-closed gate trio).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from portfolio_optimiser.contracts import GoalConfig, GoalContract
|
|
from portfolio_optimiser.ledger import (
|
|
LedgerEntry,
|
|
RealizationRefused,
|
|
SavingsLedger,
|
|
_candidate_identity,
|
|
realize,
|
|
stamp,
|
|
)
|
|
from portfolio_optimiser.run import run_portfolio
|
|
from portfolio_optimiser.verdicts import ProposalFeatures, Verdict
|
|
|
|
_TS = "2026-07-06T00:00:00Z"
|
|
_CODES = frozenset({"ENERGI-TOTAL-EL"})
|
|
_MEASURE = "energy_efficiency"
|
|
|
|
|
|
def _identity(amount_ore: int) -> str:
|
|
return _candidate_identity(affected_codes=_CODES, measure_type=_MEASURE, amount_ore=amount_ore)
|
|
|
|
|
|
def _entry(dimension: str, *, amount_ore: int) -> LedgerEntry:
|
|
return LedgerEntry(
|
|
project_id="P1",
|
|
dimension=dimension,
|
|
candidate_identity=_identity(amount_ore),
|
|
amount_ore=amount_ore,
|
|
verdict_id="v1",
|
|
provenance=stamp(approver="ekspert", experiment="sim", timestamp=_TS),
|
|
)
|
|
|
|
|
|
def test_same_candidate_two_dimensions_counts_once_and_flags_overlap() -> None:
|
|
"""LOAD-BEARING (SC4): the same underlying candidate realized under two dimensions contributes
|
|
ONCE to the portfolio sum, and the overlap is flagged. RED if the dimension-free sum-key dedup
|
|
is detached in ``_dedup_amount`` (the sum then doubles to 10000)."""
|
|
ledger = SavingsLedger()
|
|
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is True
|
|
# Same candidate identity (same codes+measure+amount), a DIFFERENT dimension -> distinct full
|
|
# key -> stored, so the overlap is visible; the sum must still count it once.
|
|
assert ledger.add_realized(_entry("asfalt", amount_ore=5000)) is True
|
|
|
|
assert ledger.portfolio_total() == 5000, (
|
|
"double-counted the same candidate across dimensions (SC4)"
|
|
)
|
|
assert ledger.per_project_total("P1") == 5000
|
|
assert ledger.overlaps() == [("P1", _identity(5000))]
|
|
|
|
|
|
def test_distinct_amounts_same_codes_measure_are_not_collided() -> None:
|
|
"""INVERSE CONTROL (fixes the magnitude-free collision): two DISTINCT realizations with the same
|
|
codes+measure but a different amount are separate candidates -> both counted, no overlap."""
|
|
ledger = SavingsLedger()
|
|
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is True
|
|
assert ledger.add_realized(_entry("energi", amount_ore=7000)) is True # distinct identity
|
|
|
|
assert ledger.portfolio_total() == 12000 # both counted (no collision)
|
|
assert ledger.overlaps() == [] # single dimension -> no cross-dimension overlap
|
|
|
|
|
|
def test_exact_full_key_duplicate_is_not_stored_twice() -> None:
|
|
"""An exact (project, dimension, candidate) re-add is a no-op — storage keys on the full key."""
|
|
ledger = SavingsLedger()
|
|
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is True
|
|
assert ledger.add_realized(_entry("energi", amount_ore=5000)) is False # exact duplicate
|
|
assert ledger.portfolio_total() == 5000
|
|
|
|
|
|
# --- Step 6: fail-closed realize gate + deterministic øre conversion (SC5/SC8) --------------------
|
|
|
|
|
|
def _features(nok: float = 5000.0) -> ProposalFeatures:
|
|
return ProposalFeatures(affected_codes=_CODES, measure_type=_MEASURE, claimed_saving_nok=nok)
|
|
|
|
|
|
def _verdict(decision: str) -> Verdict:
|
|
return Verdict(
|
|
id="v-abc", proposal_features=_features(), decision=decision, rationale="expert prose"
|
|
)
|
|
|
|
|
|
def test_realize_fail_closed_refuses_non_approved() -> None:
|
|
"""LOAD-BEARING (SC5): a non-approved verdict — OTHERWISE fully valid (correct amount, valid
|
|
provenance, so detaching the gate WOULD add it) — raises ``RealizationRefused`` and the ledger is
|
|
unchanged. RED the moment the ``raise`` detaches: the raw entry then appears (self-contamination)."""
|
|
ledger = SavingsLedger()
|
|
with pytest.raises(RealizationRefused):
|
|
realize(
|
|
ledger,
|
|
_features(),
|
|
_verdict("rejected"),
|
|
project_id="P1",
|
|
dimension="energi",
|
|
approver="ekspert",
|
|
experiment="sim",
|
|
timestamp=_TS,
|
|
)
|
|
assert ledger.entries == [] # nothing written
|
|
assert ledger.portfolio_total() == 0
|
|
|
|
|
|
def test_realize_approved_reaches_ledger() -> None:
|
|
"""CAUSALITY: a HITL-approved verdict reaches the ledger (5000 NOK -> 500000 øre)."""
|
|
ledger = SavingsLedger()
|
|
entry = realize(
|
|
ledger,
|
|
_features(),
|
|
_verdict("approved"),
|
|
project_id="P1",
|
|
dimension="energi",
|
|
approver="ekspert",
|
|
experiment="sim",
|
|
timestamp=_TS,
|
|
)
|
|
assert entry in ledger.entries
|
|
assert entry.verdict_id == "v-abc"
|
|
assert ledger.portfolio_total() == 500000
|
|
|
|
|
|
def test_realize_accepts_approved_with_adjustment() -> None:
|
|
"""The gate uses the PROMOTION set: ``approved_with_adjustment`` is admitted (not the binary
|
|
run-path FeedbackContract — H6)."""
|
|
ledger = SavingsLedger()
|
|
realize(
|
|
ledger,
|
|
_features(),
|
|
_verdict("approved_with_adjustment"),
|
|
project_id="P1",
|
|
dimension="energi",
|
|
approver="ekspert",
|
|
experiment="sim",
|
|
timestamp=_TS,
|
|
)
|
|
assert ledger.portfolio_total() == 500000
|
|
|
|
|
|
def test_realize_requires_timestamp_keyword() -> None:
|
|
"""SC8: ``timestamp`` is a required keyword — no wall-clock default. Omitting it raises TypeError."""
|
|
ledger = SavingsLedger()
|
|
with pytest.raises(TypeError):
|
|
realize(
|
|
ledger,
|
|
_features(),
|
|
_verdict("approved"),
|
|
project_id="P1",
|
|
dimension="energi",
|
|
approver="ekspert",
|
|
experiment="sim",
|
|
) # no timestamp
|
|
|
|
|
|
def test_realize_ore_conversion_is_exact() -> None:
|
|
"""øre boundary: 12345.67 NOK -> 1234567 øre exactly, no binary-float ...66.9999 drift."""
|
|
ledger = SavingsLedger()
|
|
entry = realize(
|
|
ledger,
|
|
_features(12345.67),
|
|
_verdict("approved"),
|
|
project_id="P1",
|
|
dimension="energi",
|
|
approver="ekspert",
|
|
experiment="sim",
|
|
timestamp=_TS,
|
|
)
|
|
assert entry.amount_ore == 1234567
|
|
|
|
|
|
# --- Step 8: goal-stop is load-bearing (SC6) -----------------------------------------------------
|
|
|
|
_PORTFOLIO_IDS = ["FV42-GSV-E1", "RV13-RAS-TP", "BRU-LAKS-REHAB"]
|
|
|
|
|
|
def _portfolio_ledger(amount_ore: int) -> SavingsLedger:
|
|
led = SavingsLedger()
|
|
led.add_realized(
|
|
LedgerEntry(
|
|
project_id="FV42-GSV-E1",
|
|
dimension="energi",
|
|
candidate_identity="prior-hitl",
|
|
amount_ore=amount_ore,
|
|
verdict_id="v-prior",
|
|
provenance=stamp(approver="ekspert", experiment="earlier", timestamp=_TS),
|
|
)
|
|
)
|
|
return led
|
|
|
|
|
|
async def test_goal_stop_is_load_bearing(make_portfolio_client_factory) -> None:
|
|
"""LOAD-BEARING (SC6): a reached HARD portfolio goal stops the pass (no project runs); a
|
|
below-goal ledger runs the FULL pass. RED if the goal-stop check is detached in run_portfolio
|
|
(a reached goal then runs past the target). The control (below goal -> full pass) proves the
|
|
stop is CAUSED by the goal being reached, not by the fixture."""
|
|
goals = GoalConfig(portfolio=GoalContract(absolute_ore=1000))
|
|
|
|
stopped = await run_portfolio(
|
|
_PORTFOLIO_IDS,
|
|
"local",
|
|
ledger=_portfolio_ledger(1000), # == goal (>=)
|
|
goals=goals,
|
|
client_factory=make_portfolio_client_factory({}),
|
|
max_rounds=1,
|
|
)
|
|
assert stopped.stopped_early is True
|
|
assert stopped.runs == () # detach -> this becomes 3 (runs past the reached goal)
|
|
|
|
below = await run_portfolio(
|
|
_PORTFOLIO_IDS,
|
|
"local",
|
|
ledger=_portfolio_ledger(999), # below the goal
|
|
goals=goals,
|
|
client_factory=make_portfolio_client_factory({}),
|
|
max_rounds=1,
|
|
)
|
|
assert below.stopped_early is False # control: goal not reached -> full pass
|
|
assert len(below.runs) == 3
|