portfolio-optimiser-claude/tests/test_valuereport_loadbearing.py
Kjell Tore Guttormsen 4dcdd8017a feat(portfolio): K11 — per-run value report, pure projection over the three layers (parity row 25) [skip-docs]
The S5.4 analog: every run/portfolio pass can produce a deterministic value
report — modelled → expert-corrected → realized, goal progress, a quantified
learning effect, cost against value — with no model call, no clock and no new
state. It is a PURE PROJECTION over what is already persisted (K5 outbox pairs,
§4.2 inbox verdicts, K1 ledger), joined on the verdict_id K5 mints.

The honesty rule (§1) sets the shape, not the layout:
- approved      -> the claim stands        (quantified)
- rejected      -> the claim is void, 0    (quantified — an earned zero)
- approved_with_adjustment -> §4.2 carries NO adjusted amount, so the corrected
  value is UNQUANTIFIED, never back-filled with the claim
- no verdict    -> realized is UNMARKED, never zero-that-reads-as-judged and
  never the modelled figure
Partial quantification is counted in the output (2 of 4 …, 2 UNMARKED) rather
than summed into a full-looking total. Learning is measured, not asserted: a
rising approval share is reported only alongside the modelled→corrected gap that
shrank behind it, over cohorts split by run_id order. Cost (USD, a K6 upper
bound) and value (NOK) sit side by side and are never divided — no sourced FX
rate exists here, and a ratio would invent one.

Surfaces: standalone CLI (valuereport) and an opt-in --value-report on run.py,
which requires --outbox and is refused BEFORE any spend without one; the report
is written on both run outcomes and never rewrites the run's exit code (a budget
stop stays a budget stop).

Six seams detach-proven RED: honesty boundary (corrected + realized), gap
arithmetic, projection purity, run-seam wiring, pre-spend fail-fast, both-outcome
reporting. Fixtures are COMMITTED and generated with the real primitives, so the
id-join under test is genuine.

Note on the purity test: it was green-but-dead in its first form. Snapshotting
the committed fixture tree in place let an earlier test's stray write pre-seed
the 'before' snapshot, so the detached write reproduced it byte-for-byte. Every
test now projects from a per-test copy, and a pinned file-set test guards the
committed tree. Found by running the detach proof — which is what §11 is for.

portfolio.py is deliberately NOT wired: run_portfolio persists nothing, so there
is nothing for a projection to read. Its docstring now says that instead of
promising the wiring it did not get.

562 -> 584 tests green; ruff + mypy --strict clean over 27 src files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
2026-07-25 06:25:02 +02:00

376 lines
14 KiB
Python

"""Per-run value report — LOAD-BEARING (S5.4-analog; §1, §11; paritetsrad 25; K11).
The seam this file keeps alive: every run/portfolio pass can produce a
deterministic value report — modelled → expert-corrected → realized, goal
progress, a QUANTIFIED learning effect, and cost-against-value — as a PURE
PROJECTION over state that already exists (the outbox pairs from K5, the §4.2
inbox verdicts, the K1 ledger) with not one model call and not one byte written.
The honesty rule (§1) is the spine of this module, so it is the spine of this
file: a figure the layers do not carry is reported as UNMARKED, never filled in
from the stage before it. An expert who has not judged is not an expert who
approved; an ``approved_with_adjustment`` verdict carries no adjusted amount in
the §4.2 shape, so the corrected value is unquantified — NOT the modelled claim.
A report that silently promoted modelled figures into the realized column would
be the single most flattering lie this system could tell about itself.
Three detached seams proven RED here:
* Detach proof (the honesty boundary): let the corrected/realized figure fall
back to the modelled claim for a project with no verdict →
``test_project_without_verdict_has_unmarked_realized`` and
``test_unquantified_adjustment_is_never_the_claim`` go red.
* Detach proof (the gap computation): drop the gap arithmetic from the cohort
roll-up so ``gap_share`` stays flat/None → ``test_learning_effect_rises_with_
approval_share`` goes red (a rising approval share alone must NOT be reported
as learning without the shrinking gap that backs it).
* Detach proof (the projection purity): let any build path write a byte into the
three read layers → ``test_report_never_writes_the_read_layers`` goes red.
EVERY test projects from a per-test COPY of the committed fixtures, never the
committed tree in place. That is not tidiness — the purity test was green-but-
dead when tests read the tree directly: an earlier test's stray write landed in
the committed tree, so it was already present in the "before" snapshot and the
detached write reproduced it byte-for-byte. Reading only copies removes the
channel through which one test can pre-seed another's evidence.
Key assumption (tested, not asserted in prose): the outbox + inbox + ledger carry
everything the report needs — it is a pure projection with NO new state. Proven
by building the whole report from the COMMITTED fixtures under
``tests/data/valuereport/`` without executing a single run.
"""
from __future__ import annotations
import json
import shutil
from dataclasses import dataclass
from pathlib import Path
import pytest
from portfolio_optimiser_claude.goals import GoalContract
from portfolio_optimiser_claude.valuereport import (
ProjectValue,
ValueReport,
build_value_report,
main,
render_report,
report_to_json,
)
FIXTURES = Path(__file__).parent / "data" / "valuereport"
# The committed fixture scenario, stated once so every expectation below is
# readable against it (generated with the REAL primitives, so the verdict_id
# join is genuine — never hand-typed ids):
#
# run-001 FV42-P1 LED-retrofit 100 000 -> verdict: rejected
# run-002 FV42-P1 VFD-pumpe 80 000 -> verdict: approved -> ledger
# run-003 FV42-P2 SD-anlegg 50 000 -> NO verdict (pending)
# run-004 FV42-P3 Etterisolering 60 000 -> verdict: approved_with_adjustment
#
# Ledger total: 80 000 (only the plain approval passed the expert gate).
MODELLED_TOTAL = 290_000.0
REALIZED_TOTAL = 80_000.0
# The committed tree, pinned: a stray file written by a run under test would
# otherwise be committed as if it belonged here.
EXPECTED_FIXTURE_FILES = {
"inbox/82d6e4e60ae87e14.json",
"inbox/94a98883515d1267.json",
"inbox/fe354d69ef8a47b1.json",
"ledger.json",
"outbox/run-001-outcome.json",
"outbox/run-001-proposal.json",
"outbox/run-002-outcome.json",
"outbox/run-002-proposal.json",
"outbox/run-003-outcome.json",
"outbox/run-003-proposal.json",
"outbox/run-004-outcome.json",
"outbox/run-004-proposal.json",
}
@dataclass(frozen=True)
class Layers:
"""The three read layers, as a private copy of the committed fixtures."""
root: Path
outbox: Path
inbox: Path
ledger: Path
@pytest.fixture
def layers(tmp_path: Path) -> Layers:
"""A fresh copy of the committed fixtures — the tree under test is never the real one."""
root = tmp_path / "layers"
shutil.copytree(FIXTURES, root)
return Layers(
root=root, outbox=root / "outbox", inbox=root / "inbox", ledger=root / "ledger.json"
)
def _build(layers: Layers, **kwargs: object) -> ValueReport:
return build_value_report(
outbox_dir=layers.outbox,
inbox_dir=layers.inbox,
ledger_path=layers.ledger,
**kwargs, # type: ignore[arg-type]
)
def _project(report: ValueReport, project_id: str) -> ProjectValue:
return next(project for project in report.projects if project.project_id == project_id)
# --- the three stages: modelled -> expert-corrected -> realized ------------------------------
def test_committed_fixture_tree_is_exactly_what_it_claims() -> None:
"""No stray file has crept into the committed layers (a run under test writing home)."""
present = {
str(path.relative_to(FIXTURES))
for path in FIXTURES.rglob("*")
if path.is_file() and "__pycache__" not in path.parts
}
assert present == EXPECTED_FIXTURE_FILES
def test_report_is_a_pure_projection_of_the_committed_fixtures(layers: Layers) -> None:
"""The key assumption: outbox + inbox + ledger carry everything (no run needed)."""
report = _build(layers)
assert report.modelled_nok == MODELLED_TOTAL
assert report.realized_nok == REALIZED_TOTAL
assert report.n_proposals == 4
assert report.n_settled == 3
assert report.n_pending == 1
def test_three_stages_are_distinct_columns_per_project(layers: Layers) -> None:
"""Each stage is measured from its OWN layer — never copied from the one before."""
p1 = _project(_build(layers), "FV42-P1") # one rejected (->0), one approved (->80k)
assert p1.modelled_nok == 180_000.0
assert p1.expert_corrected_nok == 80_000.0
assert p1.realized_nok == 80_000.0
def test_project_without_verdict_has_unmarked_realized(layers: Layers) -> None:
"""§1 honesty: no verdict → realized is UNMARKED, and NEVER equal to modelled.
Detach the honesty boundary (the figure falls back to the modelled claim) and
this test goes red — which is the whole point of it existing.
"""
p2 = _project(_build(layers), "FV42-P2") # run-003, no verdict at all
assert p2.realized_nok is None
assert p2.expert_corrected_nok is None
assert p2.modelled_nok == 50_000.0
assert p2.realized_nok != p2.modelled_nok
def test_unquantified_adjustment_is_never_the_claim(layers: Layers) -> None:
"""``approved_with_adjustment`` carries NO amount in §4.2 → unquantified, not the claim."""
p3 = _project(_build(layers), "FV42-P3") # run-004, adjusted without a figure
assert p3.expert_corrected_nok is None
assert p3.realized_nok is None
assert p3.modelled_nok == 60_000.0
def test_partial_quantification_is_counted_not_hidden(layers: Layers) -> None:
"""A partly-quantified portfolio says so in numbers — never a silent full-looking sum."""
report = _build(layers)
assert report.expert_corrected_nok == 80_000.0
assert report.quantified_proposals == 2
assert report.unquantified_proposals == 2
# --- quantified learning effect (the plan's first RED) ---------------------------------------
def test_learning_effect_rises_with_approval_share(layers: Layers) -> None:
"""Two cohorts of runs with a rising approval share → the metric rises AND the gap shrinks.
Detach the gap computation (cohort gap_share left flat/None) and this test
goes red: a rising approval share on its own is NOT evidence of learning, so
the report must never claim it without the gap that backs it.
"""
learning = _build(layers).learning
assert learning.comparable is True
# earlier cohort = run-001 (rejected), later = run-002 + run-004 (both accepted)
assert learning.earlier_approval_share == 0.0
assert learning.later_approval_share == 1.0
assert learning.approval_share_delta == 1.0
# ...and the modelled→corrected gap shrank from "the whole claim" to nothing.
assert learning.earlier_gap_share == 1.0
assert learning.later_gap_share == 0.0
assert learning.gap_share_delta == -1.0
assert learning.gap_shrinking is True
def test_learning_needs_two_cohorts_before_it_claims_anything(layers: Layers) -> None:
"""One settled run cannot evidence a trend — the report says so instead of inventing one."""
for verdict_file in layers.inbox.glob("*.json"):
verdict_file.unlink() # no verdicts at all → nothing settled to compare
learning = _build(layers).learning
assert learning.comparable is False
assert learning.approval_share_delta is None
assert learning.gap_share_delta is None
assert learning.gap_shrinking is None
# --- goal progress + cost against value ------------------------------------------------------
def test_goal_progress_measures_realized_against_the_target(layers: Layers) -> None:
"""Goal progress is measured on REALIZED value — the only figure that survived the gate."""
report = _build(layers, goal=GoalContract(target_nok=200_000.0, mode="soft"))
assert report.goal is not None
assert report.goal.target_nok == 200_000.0
assert report.goal.realized_nok == REALIZED_TOTAL
assert report.goal.share == 0.4
assert report.goal.reached is False
def test_cost_against_value_never_invents_an_exchange_rate(layers: Layers) -> None:
"""USD cost and NOK value sit side by side — no ratio, because no sourced FX rate exists."""
report = _build(layers, estimated_cost_usd=0.127514)
assert report.cost is not None
assert report.cost.estimated_cost_usd == 0.127514
assert report.cost.realized_value_nok == REALIZED_TOTAL
assert "ESTIMAT" in report.cost.note
# The honest omission: the two currencies are NOT divided into a fake ratio.
assert not hasattr(report.cost, "ratio")
# --- byte determinism (the plan's third RED) --------------------------------------------------
def test_report_bytes_are_deterministic(layers: Layers, tmp_path: Path) -> None:
"""Same ledger + outbox + inbox → byte-identical report (the ``diff`` the plan asks for)."""
first = tmp_path / "first.json"
second = tmp_path / "second.json"
first.write_text(report_to_json(_build(layers)), encoding="utf-8", newline="\n")
second.write_text(report_to_json(_build(layers)), encoding="utf-8", newline="\n")
assert first.read_bytes() == second.read_bytes()
def test_report_json_is_house_style_bytes(layers: Layers) -> None:
"""Sorted keys, 2-space indent, LF only, trailing newline — the house JSON contract."""
payload = report_to_json(_build(layers))
assert payload.endswith("\n")
assert "\r" not in payload
parsed = json.loads(payload)
assert payload == json.dumps(parsed, sort_keys=True, indent=2, ensure_ascii=False) + "\n"
def test_markdown_render_marks_every_unmarked_figure(layers: Layers) -> None:
"""The rendered report never shows a blank where a number is missing — it says so."""
rendered = render_report(_build(layers))
assert "FV42-P2" in rendered
assert "UNMARKED" in rendered # the pending/unquantified projects are labelled
assert "ESTIMAT" not in rendered # no cost estimate was passed in → no cost section
# --- projection purity: the report writes NOTHING ---------------------------------------------
def _snapshot(root: Path) -> dict[str, bytes]:
return {
str(p.relative_to(root)): p.read_bytes() for p in sorted(root.rglob("*")) if p.is_file()
}
def test_report_never_writes_the_read_layers(layers: Layers) -> None:
"""Read-only over all three layers (§3 Step 7) — byte snapshot before/after.
Honest limit: a write that reproduces a file's exact existing bytes is
invisible to a content snapshot. The failure this guards is state leaking
into the read layers, which changes bytes or adds files.
"""
before = _snapshot(layers.root)
_build(
layers,
goal=GoalContract(target_nok=200_000.0, mode="soft"),
estimated_cost_usd=0.5,
)
assert _snapshot(layers.root) == before
# --- the CLI seam -----------------------------------------------------------------------------
def test_cli_writes_the_report_and_exits_zero(
layers: Layers, tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""The thin CLI: project the three layers, print the render, write the JSON."""
out = tmp_path / "value-report.json"
code = main(
[
"--outbox",
str(layers.outbox),
"--inbox",
str(layers.inbox),
"--ledger",
str(layers.ledger),
"--json",
str(out),
]
)
assert code == 0
assert out.read_text(encoding="utf-8") == report_to_json(_build(layers))
assert "VERDIRAPPORT" in capsys.readouterr().out
def test_cli_refuses_a_malformed_ledger_fail_fast(layers: Layers) -> None:
"""§10 (SC5): a valid-JSON-but-wrong-shape ledger is refused, never read as an empty book."""
layers.ledger.write_text('{"entries": {}}', encoding="utf-8")
code = main(
[
"--outbox",
str(layers.outbox),
"--inbox",
str(layers.inbox),
"--ledger",
str(layers.ledger),
]
)
assert code == 1
def test_cli_refuses_a_non_object_ledger_fail_fast(layers: Layers) -> None:
"""A JSON array is not a book either — the `**` unpack must not surface as a crash."""
layers.ledger.write_text("[]", encoding="utf-8")
code = main(
[
"--outbox",
str(layers.outbox),
"--inbox",
str(layers.inbox),
"--ledger",
str(layers.ledger),
]
)
assert code == 1