"""S5.4 — verdirapport (målbilde §7): read-only savings roll-up over SavingsLedger. MAF-bound via ``ledger`` (which imports the MAF-bound ``verdicts``), so this module CANNOT live in the framework-neutral ``shared/`` subtree and is deliberately NOT registered in ``tests/test_okf.py``'s ``_MAF_FREE_MODULES``. It is a pure OFFLINE read: it turns the ledger's already-deduped integer-øre totals into a legible statement of value (per-project + portfolio totals, flagged cross-dimension overlaps, per-entry provenance). Zero model calls, zero writes. Correctness rests on CALLING the ledger's roll-up accessors (``per_project_total`` / ``portfolio_total`` / ``overlaps``) — never re-summing ``.entries``, which would double-count a saving realized under two dimensions (the exact bug the ledger's dimension-free dedup prevents). """ from __future__ import annotations from pydantic import BaseModel from portfolio_optimiser.ledger import SavingsLedger class ProvenanceLine(BaseModel): """One realized ledger entry surfaced at the report's ENTRY level: verbatim ``verdict_id`` + ``provenance`` string kept in association, so provenance is legible per-entry in the JSON output. Reads ONLY the entry's own fields — never joins the MAF verdict store (Non-Goal).""" project_id: str dimension: str candidate_identity: str verdict_id: str amount_ore: int provenance: str class ValueReport(BaseModel): """The read-only value roll-up over a ``SavingsLedger``: per-project + portfolio totals (integer øre, dimension-free-deduped by the ledger accessors), the flagged cross-dimension ``overlaps``, and one ``ProvenanceLine`` per ledger entry (deterministically ordered).""" per_project: dict[str, int] portfolio_total_ore: int overlaps: list[tuple[str, str]] provenance: list[ProvenanceLine] def build_value_report(ledger: SavingsLedger) -> ValueReport: """Build the value report by CALLING the ledger's deduped roll-up accessors — never summing ``.entries`` directly (double-count risk on cross-dimension overlaps). The provenance sort key is TOTAL: the leading ``(project_id, dimension, candidate_identity)`` triple is the ledger's ``_storage_key``, unique for in-process-built ledgers but NOT for a ``load()``-ed / hand-authored file (load does no dedup), so two rows sharing the triple would tie and their order would follow file insertion order (byte-instability). Appending ``verdict_id, provenance, amount_ore`` makes the order total and byte-stable (SC6). """ project_ids = sorted({e.project_id for e in ledger.entries}) per_project = {pid: ledger.per_project_total(pid) for pid in project_ids} ordered_entries = sorted( ledger.entries, key=lambda e: ( e.project_id, e.dimension, e.candidate_identity, e.verdict_id, e.provenance, e.amount_ore, ), ) provenance = [ ProvenanceLine( project_id=e.project_id, dimension=e.dimension, candidate_identity=e.candidate_identity, verdict_id=e.verdict_id, amount_ore=e.amount_ore, provenance=e.provenance, ) for e in ordered_entries ] return ValueReport( per_project=per_project, portfolio_total_ore=ledger.portfolio_total(), overlaps=ledger.overlaps(), provenance=provenance, )