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
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 06:25:02 +02:00
commit 4dcdd8017a
18 changed files with 1477 additions and 6 deletions

View file

@ -152,6 +152,24 @@ description, never from its code)
outcomes — a budget stop notifies too) and `hitl.py` (read-only preserved) share the same
opt-in-gated CLI seam, refusing a webhook-without-opt-in *before* any spend. The payload
shape is stack-local (no shared notification spec across the siblings).
- `valuereport.py` — what the loop actually delivered (**offline, read-only**): a
deterministic projection of the outbox, the inbox and the ledger into three distinct
columns — *modelled* (what the system claimed), *expert-corrected* (what the §4.2 verdict
makes of that claim), *realized* (what passed the expert gate into the book) — plus goal
progress, a quantified learning effect and cost against value. No model call, no clock, no
new state. The honesty rule (§1) sets its shape: a figure the layers do not carry is
reported `UNMARKED`, never back-filled from the stage before it. An `approved_with_adjustment`
verdict changes the amount but carries none in the §4.2 shape, so its corrected value is
unquantified — and a project nobody has judged has no realized figure at all, rather than a
zero that reads as a judgment. Learning is measured, not asserted: settled proposals split
by `run_id` order into an earlier and a later cohort, and a rising approval share is only
reported alongside the modelled→corrected gap that shrank behind it. Cost (USD, itself a K6
upper bound) and value (NOK) are printed side by side and never divided — this repo carries
no sourced exchange rate, and a ratio would invent one. Available standalone
(`uv run python -m portfolio_optimiser_claude.valuereport --outbox <dir> --inbox <dir>
[--ledger <file>]`) and as an opt-in side product of a run (`run.py --value-report <file>`,
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 own exit code).
### Load-bearing tests (§11)
@ -180,8 +198,13 @@ and hitl never writes any layer, proven by a before/after byte snapshot),
per-run opt-in flag refuses fail-fast and its transport never fires — red the moment the gate
is detached — the canned transport receives the structured payload, an AST grep-guard proves
no network path lives outside the one injectable seam function, and the run/hitl entrances
emit on their outcomes while hitl stays read-only), and
`test_sdk_isolation.py` (local config cannot capture the checker).
emit on their outcomes while hitl stays read-only),
`test_valuereport_loadbearing.py` and `test_valuereport_seam_loadbearing.py` (an unjudged
project's realized value stays unmarked and never mirrors the modelled claim — red the moment
that boundary is detached — a rising approval share is not reported as learning without the
gap arithmetic behind it, the projection writes no byte into the three layers it reads, and
the run entrance produces the report on both outcomes while leaving the run's verdict alone),
and `test_sdk_isolation.py` (local config cannot capture the checker).
## The ingest layer — CSV and SQL, in front of the loop