portfolio-optimiser/tests/test_money_quantization_loadbearing.py
Kjell Tore Guttormsen 756e8f8259 fix(money): quantize NOK to øre in one order, from one source (kø-p)
Two quantization orders existed and met at exactly one comparison.
SavingsLedger quantizes every realized candidate to integer øre and sums the
ints; run.py's goal baselines summed Project.total_cost FLOATS across items and
projects and quantized the total once. _goal_limit_if_reached compared the
former against a threshold derived from the latter — so whether a portfolio pass
stops early was decided by two differently-computed sides.

Measured divergence: three 60000.005 NOK lines are 18000003 øre quantized first
but 18000001 summed first (the float sum drifts to 180000.01499999998).

Decision: quantize per cost line, then sum integers. Each CostItem IS a money
amount — S4.0 made per-line quantity/unit_cost the validator's ground truth — and
integer addition is associative, keeping totals order-independent under the D-D
wave model, which the float fold is not.

ledger.to_ore is now the framework's one NOK->øre conversion; run.py imports it
rather than keeping a private copy (the S4.0 REPLIES precedent).

Measuring the mutations found two further gaps, both now closed: the per-project
baseline is a SECOND call site whose mutation survived the whole suite, and
realize bypassing to_ore with a raw float*100 was caught by nothing.

Load-bearing MEASURED (tests/test_money_quantization_loadbearing.py), five
mutations all red: detach the portfolio baseline · detach the per-project
baseline · reintroduce a private copy in run.py · change the rounding mode · let
realize bypass to_ore. 615 -> 621 tests.

Honesty boundary: sum_claimed_saving_nok (run.py:_aggregate) is deliberately
untouched — a float NOK reporting field that is never quantized and never
compared against the ledger, hence outside the ordering defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WiY53sm8JFqk7NN75g5wRS
2026-08-03 20:08:59 +02:00

216 lines
10 KiB
Python

"""Kø-(p) LOAD-BEARING: money is quantized to øre in ONE order, from ONE source.
Two orders existed. ``ledger`` quantizes EVERY realized candidate to integer øre and sums the
ints (``ledger.py`` documents ``amount_ore`` as "exact, order-independent sums"); ``run.py``'s
goal baselines summed ``Project.total_cost`` FLOATS across items and projects and quantized the
total ONCE. The two met at exactly one place — ``_goal_limit_if_reached``, where a percent goal
compares the ledger's quantize-first ``observed_ore`` against a threshold derived from the
sum-first baseline. A comparison whose two sides are computed in different orders decides whether
a portfolio pass stops early, so the divergence is not cosmetic.
DECISION (Kø-(p)): **quantize each cost line, then sum integers.** Each ``CostItem`` IS a money
amount ("One cost line in a project's estimate"), and S4.0 made per-line ``quantity``/``unit_cost``
the validator's ground truth — a real amount has a real number of øre. Float summation is also
order-dependent, which is the nondeterminism class ``_wave_snapshot`` already eliminates elsewhere.
The fixture makes the two orders disagree by construction: three lines of ``60000.005`` NOK each.
Quantize-first gives ``6000001`` øre per line -> ``18000003``; the float sum drifts to
``180000.01499999998`` and quantize-last gives ``18000001``. A ledger observation of ``18000002``
øre sits STRICTLY BETWEEN the two thresholds, so a 100 % portfolio goal stops the pass under the
old order and does not under the new one — RED in either detach direction.
"""
from __future__ import annotations
from portfolio_optimiser import ledger as ledger_mod
from portfolio_optimiser import run as run_mod
from portfolio_optimiser.contracts import GoalConfig, GoalContract
from portfolio_optimiser.ledger import SavingsLedger, realize
from portfolio_optimiser.reference_domain import CostItem, Project
from portfolio_optimiser.run import run_portfolio
from portfolio_optimiser.verdicts import ProposalFeatures, capture_verdict
# Three lines whose per-line øre value ends in a half øre (60000.005 NOK = 6000000.5 øre).
_HALF_ORE_UNIT_COST = 60000.005
_QUANTIZE_FIRST_ORE = 18_000_003 # 3 x ROUND_HALF_UP(6000000.5) = 3 x 6000001
_SUM_FIRST_ORE = 18_000_001 # ROUND_HALF_UP(100 x 180000.01499999998)
_OBSERVED_ORE = 18_000_002 # strictly between the two -> discriminates the orders
_OBSERVED_NOK = 180_000.02
# Whole-øre control lines: the two orders agree, so the outcome must NOT flip.
_WHOLE_ORE_UNIT_COST = 60_000.00
# A proposal reconciling with the fixture's first cost line (S4.0 baseline gate). Degenerate
# Monte Carlo: P90 = 0.30 x 60000.005 = 18000.0015 >= claimed 15000 -> validates.
_ALIGNED_REPLY = (
'{"measure":"LED-retrofit av kontorbelysning","affected_items":'
'[{"code":"ORE-A","quantity":1.0,"unit_cost":60000.005}],"claimed_saving_nok":15000}'
)
def _make_docs(tmp_path) -> str:
"""A tmp docs folder with citable content, so the road path retrieval is non-empty."""
d = tmp_path / "ore-docs"
d.mkdir()
(d / "cost.txt").write_text(
"Lighting retrofit reduced the electricity cost on the office stretch.", encoding="utf-8"
)
return str(d)
def _project(tmp_path, *, unit_cost: float) -> Project:
return Project(
id="ORE-P",
name="Øre project",
description="a project whose cost lines expose the quantization order",
currency="NOK",
cost_items=tuple(
CostItem(
code=code,
description=f"line {code}",
quantity=1.0,
unit="stk",
unit_cost=unit_cost,
)
for code in ("ORE-A", "ORE-B", "ORE-C")
),
docs_dir=_make_docs(tmp_path),
verdict_input={"decision": "approved", "rationale": "reviewed (sim)"},
bundle_dir=None,
verdict_dir=None,
)
def _ledger_observing(amount_nok: float) -> SavingsLedger:
"""A ledger holding ONE realized entry worth ``amount_nok`` — the quantize-first side of the
comparison, built through the production ``realize`` path rather than a hand-made entry."""
ledger = SavingsLedger(entries=[])
features = ProposalFeatures(
affected_codes=frozenset({"ORE-A"}),
measure_type="lighting retrofit",
claimed_saving_nok=amount_nok,
)
realize(
ledger,
features,
capture_verdict(features, "approved", "realized out of band"),
project_id="ORE-P",
dimension="kostnad",
approver="persona",
experiment="ko-p",
timestamp="2026-08-03T00:00:00Z",
)
return ledger
def test_the_fixture_actually_discriminates_the_two_orders() -> None:
"""The premise itself, MEASURED — not assumed. If these constants ever stop disagreeing the
load-bearing test below would pass vacuously, so the disagreement is asserted first."""
lines = [1.0 * _HALF_ORE_UNIT_COST] * 3
assert sum(ledger_mod.to_ore(x) for x in lines) == _QUANTIZE_FIRST_ORE
assert ledger_mod.to_ore(sum(lines, 0.0)) == _SUM_FIRST_ORE
assert _SUM_FIRST_ORE < _OBSERVED_ORE < _QUANTIZE_FIRST_ORE
assert ledger_mod.to_ore(_OBSERVED_NOK) == _OBSERVED_ORE
def test_nok_to_ore_conversion_has_exactly_one_source() -> None:
"""ONE source, not two copies that drift (the S4.0 ``REPLIES`` precedent). ``run`` must use
``ledger``'s conversion by IDENTITY — a re-implementation that happens to agree today is the
defect class, not the fix. Reintroduce a private copy in ``run.py`` -> RED."""
assert run_mod.to_ore is ledger_mod.to_ore, (
"run.py no longer shares ledger.py's NOK->øre conversion — two copies of a money "
"conversion is exactly the drift Kø-(p) closed"
)
assert not hasattr(run_mod, "_to_ore"), "the private duplicate in run.py must be gone"
async def test_goal_baseline_is_quantized_per_cost_line_not_after_summing(
tmp_path, monkeypatch, make_recording_client_factory
) -> None:
"""LOAD-BEARING: the portfolio goal baseline is quantize-first, so BOTH sides of
``_goal_limit_if_reached`` are computed in the same order.
Observed = 18000002 øre. Quantize-first baseline 18000003 -> a 100 % goal is NOT reached ->
the project runs. Revert the baseline to ``to_ore(sum(total_cost))`` -> baseline 18000001 ->
18000002 >= 18000001 -> hard stop before any project runs -> RED."""
project = _project(tmp_path, unit_cost=_HALF_ORE_UNIT_COST)
monkeypatch.setattr("portfolio_optimiser.run.load_reference_projects", lambda: (project,))
factory, _recorded = make_recording_client_factory(_ALIGNED_REPLY)
result = await run_portfolio(
profile="local",
client_factory=factory,
ledger=_ledger_observing(_OBSERVED_NOK),
goals=GoalConfig(portfolio=GoalContract(percent=100.0, mode="hard")),
)
assert result.stopped_early is False, (
"the pass stopped early: the goal threshold was derived from a sum-first baseline "
f"({_SUM_FIRST_ORE} øre) instead of the quantize-first one ({_QUANTIZE_FIRST_ORE} øre)"
)
assert len(result.runs) == 1, "the single project did not run"
async def test_per_project_goal_baseline_is_also_quantized_per_cost_line(
tmp_path, monkeypatch, make_recording_client_factory
) -> None:
"""LOAD-BEARING (second call site): the PER-PROJECT goal baseline is quantize-first too.
The portfolio and per-project baselines are separate call sites, and a fix applied to only one
leaves the other on the old order — measured: mutating this call site alone survived the whole
suite before this test existed. A hard per-project goal ``continue``s past its own pid rather
than stopping the pass, so the tell is that the project never runs. Revert this call site to
``to_ore(project.total_cost)`` -> baseline 18000001 -> the goal reads as reached -> RED."""
project = _project(tmp_path, unit_cost=_HALF_ORE_UNIT_COST)
monkeypatch.setattr("portfolio_optimiser.run.load_reference_projects", lambda: (project,))
factory, _recorded = make_recording_client_factory(_ALIGNED_REPLY)
result = await run_portfolio(
profile="local",
client_factory=factory,
ledger=_ledger_observing(_OBSERVED_NOK),
goals=GoalConfig(per_project={"ORE-P": GoalContract(percent=100.0, mode="hard")}),
)
assert result.stop_reason is None, (
"the per-project goal read as reached: its threshold came from a sum-first baseline "
f"({_SUM_FIRST_ORE} øre) instead of the quantize-first one ({_QUANTIZE_FIRST_ORE} øre)"
)
assert len(result.runs) == 1, "the project was skipped by a goal that should not have fired"
def test_realize_converts_through_the_exact_decimal_conversion() -> None:
"""``realize`` must go through ``to_ore``, not a raw ``float * 100``. ``1.13`` NOK is ``113``
øre, but ``int(1.13 * 100)`` truncates the binary-float representation to ``112`` — an øre lost
per realized candidate. Measured: bypassing ``to_ore`` in ``realize`` survived the whole suite
before this test existed. Replace the call with ``int(claimed * 100)`` -> RED."""
ledger = _ledger_observing(1.13)
assert ledger.entries[0].amount_ore == 113, (
"realize lost an øre to binary-float drift — its NOK->øre conversion is not going through "
"to_ore's Decimal path"
)
assert ledger.portfolio_total() == 113
async def test_control_whole_ore_lines_do_not_flip_the_outcome(
tmp_path, monkeypatch, make_recording_client_factory
) -> None:
"""CONTROL (causality): with whole-øre cost lines the two orders AGREE, so the same observation
and goal must reach the goal and stop. Proves the flip above comes from the quantization order —
not from the goal machinery merely being present."""
project = _project(tmp_path, unit_cost=_WHOLE_ORE_UNIT_COST)
monkeypatch.setattr("portfolio_optimiser.run.load_reference_projects", lambda: (project,))
factory, _recorded = make_recording_client_factory(_ALIGNED_REPLY)
result = await run_portfolio(
profile="local",
client_factory=factory,
ledger=_ledger_observing(_OBSERVED_NOK),
goals=GoalConfig(portfolio=GoalContract(percent=100.0, mode="hard")),
)
assert result.stopped_early is True, (
"whole-øre lines give baseline 18000000 øre under BOTH orders, and observed 18000002 "
"exceeds it — the goal must be reached regardless of quantization order"
)