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:
parent
d0107e7f8c
commit
4dcdd8017a
18 changed files with 1477 additions and 6 deletions
|
|
@ -58,6 +58,7 @@ from portfolio_optimiser_claude.notify import (
|
|||
notify_config_from_args,
|
||||
)
|
||||
from portfolio_optimiser_claude.validator import Rejection
|
||||
from portfolio_optimiser_claude.valuereport import build_value_report, report_to_json
|
||||
|
||||
_PROPOSER_ROLE = "proposer"
|
||||
_CHECKER_ROLE = "checker"
|
||||
|
|
@ -240,6 +241,35 @@ def execute_run(
|
|||
return 0
|
||||
|
||||
|
||||
def write_value_report(
|
||||
*,
|
||||
outbox_dir: Path,
|
||||
inbox_dir: Path | None,
|
||||
ledger_path: Path | None,
|
||||
destination: Path,
|
||||
) -> bool:
|
||||
"""K11 opt-in: project the persisted layers into a value report AFTER the run.
|
||||
|
||||
The report is a projection over what is already on disk — including the pair
|
||||
this run just filed — so it runs after the run, never before, and it can
|
||||
never change what the run itself decided. A malformed layer is reported here
|
||||
and makes the COMMAND non-zero (the operator asked for a report and did not
|
||||
get one), but a budget stop stays a budget stop: reporting never rewrites a
|
||||
run's own verdict. Returns whether the report was written.
|
||||
"""
|
||||
try:
|
||||
report = build_value_report(
|
||||
outbox_dir=outbox_dir, inbox_dir=inbox_dir, ledger_path=ledger_path
|
||||
)
|
||||
except (OSError, TypeError, ValueError) as exc:
|
||||
print(f"VALUE REPORT FAILED — refusing to project a malformed layer (§10): {exc}")
|
||||
return False
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
destination.write_text(report_to_json(report), encoding="utf-8", newline="\n")
|
||||
print(f"value-report: {destination}")
|
||||
return True
|
||||
|
||||
|
||||
def build_dry_run_config(
|
||||
contracts: Contracts,
|
||||
*,
|
||||
|
|
@ -357,6 +387,19 @@ def main(
|
|||
parser.add_argument("--max-debate-rounds", type=int, default=3)
|
||||
parser.add_argument("--max-attempts", type=int, default=3)
|
||||
parser.add_argument("--top-k", type=int, default=3)
|
||||
parser.add_argument(
|
||||
"--value-report",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="after the run, project the outbox (+ --inbox, + --ledger) into a "
|
||||
"deterministic value report written as JSON to this path (K11; no model call).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ledger",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="realized-savings ledger read by --value-report; absent = an empty book.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--live-dry-run",
|
||||
action="store_true",
|
||||
|
|
@ -378,6 +421,11 @@ def main(
|
|||
)
|
||||
elif args.outbox is not None and not (args.run_id or "").strip():
|
||||
parser.error("--outbox requires --run-id (no wall-clock default)")
|
||||
# K11: the value report PROJECTS the outbox — without one there is nothing to
|
||||
# project. Rejected here, before any spend, so a run never completes only to
|
||||
# find it cannot produce the report the operator asked for.
|
||||
if args.value_report is not None and args.outbox is None:
|
||||
parser.error("--value-report requires --outbox (the report projects the outbox pairs)")
|
||||
|
||||
# K10 (§8): build the notify sinks BEFORE any spend — a --notify-webhook
|
||||
# without --allow-webhook-egress is refused fail-fast here, so no run rides
|
||||
|
|
@ -442,7 +490,7 @@ def main(
|
|||
refusals=refusals,
|
||||
)
|
||||
|
||||
return execute_run(
|
||||
code = execute_run(
|
||||
client,
|
||||
composed,
|
||||
contracts=contracts,
|
||||
|
|
@ -453,6 +501,20 @@ def main(
|
|||
run_id=args.run_id,
|
||||
notifiers=notifiers,
|
||||
)
|
||||
# K11: the value report is produced on BOTH outcomes — a budget-stopped run
|
||||
# still has a value picture worth reporting (the accumulated outbox, inbox
|
||||
# and ledger are what it projects, not this one run's success).
|
||||
if args.value_report is not None:
|
||||
assert args.outbox is not None # narrowed by the fail-fast above
|
||||
written = write_value_report(
|
||||
outbox_dir=args.outbox,
|
||||
inbox_dir=args.inbox,
|
||||
ledger_path=args.ledger,
|
||||
destination=args.value_report,
|
||||
)
|
||||
if not written and code == 0:
|
||||
code = 1
|
||||
return code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue