portfolio-optimiser/src/portfolio_optimiser
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
..
data feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
__init__.py feat(s53): load_dimension fail-fast loader (MAF-free, mirrors load_goal_config) 2026-07-23 21:31:32 +02:00
backends.py fix(s41): pass required credential in AzureFoundryBackend.create_chat_client 2026-07-15 11:18:40 +02:00
budget.py feat(budget): enforce a global portfolio token cap before the call, not after it (S3.4/F10) 2026-07-31 21:34:48 +02:00
contracts.py feat(fase1): GoalContract + GoalConfig standalone fail-fast loader (F1) 2026-07-07 08:01:53 +02:00
costsim.py docs(s54): --report CLI surface + truthful costsim kost_mot_verdi seam note 2026-07-24 01:38:10 +02:00
datasource.py feat(fase2b): OKF-navigated bundle context replaces chunk-stuffing 2026-06-30 06:42:19 +02:00
dimension.py feat(s53): load_dimension fail-fast loader (MAF-free, mirrors load_goal_config) 2026-07-23 21:31:32 +02:00
generate.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
hitl.py fix(s51): close 2 review MINORs — non-UTF-8 skip + affected_codes parity 2026-07-15 20:59:01 +02:00
ingest.py feat(ingest): bound the default http transport in time, in front of the pinned library (S2.4) 2026-08-03 14:54:04 +02:00
ingest_mcp.py fix(ingest): run the MCP stdio transport against a real server, and repair its error contract (kø-x) 2026-08-03 17:56:19 +02:00
ir.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
ledger.py fix(money): quantize NOK to øre in one order, from one source (kø-p) 2026-08-03 20:08:59 +02:00
notify.py fix(s52): resolve ConsoleNotifier default stream at call time 2026-07-17 03:18:00 +02:00
okf.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
outbox.py feat(s42): byte-deterministic write_run_config outbox writer 2026-07-15 12:39:42 +02:00
persona.py feat(shared-root): S3 — configurable shared-root resolver with load-bearing override test 2026-07-03 01:10:05 +02:00
preflight.py fix(s41): close review WARN — OSError refusal + scheme/profile branch coverage 2026-07-15 11:58:10 +02:00
provenance.py feat(fase2): first-class Pydantic provenance stamp 2026-06-24 13:26:55 +02:00
reference_domain.py feat(fase2a): optional Project.bundle_dir + loader (backward-compat, S2.0) 2026-07-15 07:03:15 +02:00
retrieval.py fix(retrieval): fail-closed on uncanonicalisable paths (embedded null byte) 2026-07-21 08:00:11 +02:00
run.py fix(money): quantize NOK to øre in one order, from one source (kø-p) 2026-08-03 20:08:59 +02:00
semretrieval.py fix(s31): close 1 review MAJOR — bound the blend weight so the hybrid preserves structural order 2026-07-25 20:03:37 +02:00
shared_root.py test(okf): lock link_in_index success-path byte/order preservation + honest shared_root claim 2026-07-21 09:38:56 +02:00
simulation.py feat(s42): live_dry_run cut in run_project + DryRunReport 2026-07-15 18:05:16 +02:00
validator.py feat(validator): anchor the deterministic gate to the project's real cost baseline (S4.0) 2026-08-03 17:19:31 +02:00
value_report.py feat(s54): value_report text + deterministic JSON formatters 2026-07-24 01:29:31 +02:00
verdicts.py feat(verdicts): key each verdict on its own candidate, not the bundle's one IR projection (S3.2) 2026-08-03 16:44:58 +02:00
workflow.py feat(fase3): make the maker-checker checker actually gate the reasoning 2026-06-30 07:24:30 +02:00