The constant was detach-proof but value-unproven: mutating 6 -> 4 left all 628
tests green, so nothing held the figure to the claim it carries. Measuring what
actually constrains it gave a narrower answer than the premise assumed — 6 -> 4
CANNOT be made red without inventing a resolution requirement no layer states,
and §1 forbids asserting more than the implementation carries.
Measured band, both ends now load-bearing:
* d >= 17 -> the 1-ULP float tail of a cohort subtraction reaches the JSON
bytes (0.1 - 0.3 publishes as -0.19999999999999998, not -0.2).
* d <= 2 -> the rendered percent moves (2/7 renders 29.0%, not 28.6%).
* d in [3, 16] -> identical to every consumer this system has.
Both proofs are stated WITHOUT reference to the constant's own value — the
exact decimal difference of the two PUBLISHED shares, and a percent computed
from the RAW NOK figures — so they bind the claim rather than the number. A
literal like 0.142857 would only have bound 6 to itself.
The :61 comment justified only the upper end; it now records the measurement
and says plainly that 6 is convention inside the band, not a derived figure.
Mutation-verified: d=2 RED, d=3/4/5/16 GREEN, d=17 RED. Suite 628 -> 631.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MQu2xxwedckjU56byu1aUG
618 lines
24 KiB
Python
618 lines
24 KiB
Python
"""Per-run value report (method-spec §1/§4/§5 analog; S5.4; paritetsrad 25; K11).
|
|
|
|
What did the loop actually deliver? This module answers that as a PURE
|
|
PROJECTION over state that already exists — the K5 outbox pairs, the §4.2 inbox
|
|
verdicts, and the K1 ledger — with no model call, no clock, and no new state.
|
|
Three stages, each measured from its OWN layer and never copied from the one
|
|
before it:
|
|
|
|
* **modelled** — what the system claimed (``claimed_saving_nok`` in the outbox
|
|
proposal). The cheapest figure to produce and the easiest to over-trust.
|
|
* **expert-corrected** — what the expert's §4.2 verdict makes of that claim:
|
|
``approved`` leaves it standing, ``rejected`` writes it to zero. The third
|
|
vocabulary member, ``approved_with_adjustment``, says the amount changes but
|
|
the §4.2 shape carries NO adjusted figure — so the corrected value is
|
|
UNQUANTIFIED, reported as such and never silently back-filled with the claim.
|
|
* **realized** — what passed the expert gate into the K1 book. A project with no
|
|
ledger entry and no settled verdict is UNMARKED (``None``), never zero-that-
|
|
looks-judged and never the modelled figure (§1: the report may not claim more
|
|
than the layers carry).
|
|
|
|
The learning effect is quantified rather than asserted: settled proposals are
|
|
split by ``run_id`` order into an earlier and a later cohort, and BOTH the
|
|
approval share and the modelled→corrected gap are compared across them. A rising
|
|
approval share alone is not evidence of learning — the report pairs it with the
|
|
gap that shrank, or it reports neither.
|
|
|
|
Cost against value is reported side by side WITHOUT a ratio: the cost estimate is
|
|
USD (K6, itself an upper bound) and the realized value is NOK, and this repo has
|
|
no sourced exchange rate. Dividing them would manufacture a number no source
|
|
backs (§1).
|
|
|
|
Run: uv run python -m portfolio_optimiser_claude.valuereport --outbox <dir> \\
|
|
--inbox <dir> [--ledger <file>] [--goal-nok N] \\
|
|
[--estimated-cost-usd X] [--json <file>]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, Sequence
|
|
|
|
from pydantic import ValidationError
|
|
|
|
from portfolio_optimiser_claude.goals import GoalContract
|
|
from portfolio_optimiser_claude.hitl import load_outbox_proposals
|
|
from portfolio_optimiser_claude.inbox import load_inbox
|
|
from portfolio_optimiser_claude.ledger import SavingsLedger
|
|
|
|
# §4.2 decisions that keep the measure alive (the expert did not throw it out).
|
|
_ACCEPTING = frozenset({"approved", "approved_with_adjustment"})
|
|
# The decision that voids the claim outright — a QUANTIFIED correction to zero.
|
|
_REJECTED = "rejected"
|
|
# The decision that changes the amount without carrying one (§4.2 has no field
|
|
# for an adjusted figure) — quantifying it here would be invention, not reading.
|
|
_ADJUSTED = "approved_with_adjustment"
|
|
|
|
_UNMARKED = "UNMARKED"
|
|
# Shares are rounded so the JSON bytes never carry the 1-ULP float tail a cohort
|
|
# subtraction leaves behind (0.1 - 0.3 is -0.19999999999999998). MEASURED band,
|
|
# bound by the two value proofs in test_valuereport_loadbearing.py: 17 leaks that
|
|
# tail, 2 moves the rendered percent, and everything in [3, 16] is identical to
|
|
# every consumer this system has. 6 is a convention inside the band — NOT a
|
|
# figure any layer derives, and the tests say only what was measured.
|
|
_SHARE_DIGITS = 6
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProposalValue:
|
|
"""One outbox proposal projected through the expert stage.
|
|
|
|
``expert_corrected_nok`` is ``None`` for exactly two reasons, both honest:
|
|
no verdict has arrived, or the verdict adjusted the amount without stating
|
|
it. ``status`` names which.
|
|
"""
|
|
|
|
run_id: str
|
|
verdict_id: str
|
|
project_id: str
|
|
measure: str
|
|
modelled_nok: float
|
|
decision: str | None
|
|
expert_corrected_nok: float | None
|
|
status: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ProjectValue:
|
|
"""One project's three stages, plus how much of it is quantified at all."""
|
|
|
|
project_id: str
|
|
modelled_nok: float
|
|
expert_corrected_nok: float | None
|
|
realized_nok: float | None
|
|
quantified_proposals: int
|
|
unquantified_proposals: int
|
|
proposals: list[ProposalValue]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Cohort:
|
|
"""One half of the run history: how it was judged, and how big its gap was.
|
|
|
|
``gap_share`` is computed over the QUANTIFIED proposals only — an
|
|
unquantified adjustment cannot contribute to a gap measurement — and
|
|
``quantified`` says how many that was, so a thin cohort is visible.
|
|
"""
|
|
|
|
settled: int
|
|
accepted: int
|
|
approval_share: float | None
|
|
quantified: int
|
|
modelled_nok: float
|
|
expert_corrected_nok: float | None
|
|
gap_share: float | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LearningEffect:
|
|
"""The quantified trend across cohorts — approval share AND the shrinking gap.
|
|
|
|
``comparable`` is False whenever there are fewer than two settled proposals
|
|
to split: one judgment is a data point, not a trend, and the report says so
|
|
rather than rendering a confident-looking zero.
|
|
"""
|
|
|
|
comparable: bool
|
|
earlier: Cohort | None
|
|
later: Cohort | None
|
|
earlier_approval_share: float | None
|
|
later_approval_share: float | None
|
|
approval_share_delta: float | None
|
|
earlier_gap_share: float | None
|
|
later_gap_share: float | None
|
|
gap_share_delta: float | None
|
|
gap_shrinking: bool | None
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GoalProgress:
|
|
"""Progress toward the §8-adjacent savings goal, measured on REALIZED value only."""
|
|
|
|
target_nok: float
|
|
realized_nok: float
|
|
share: float
|
|
reached: bool
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CostVersusValue:
|
|
"""Run cost against realized value — two currencies, deliberately NOT divided."""
|
|
|
|
estimated_cost_usd: float
|
|
realized_value_nok: float
|
|
note: str
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ValueReport:
|
|
"""The whole projection: portfolio roll-up, per project, trend, goal, cost."""
|
|
|
|
modelled_nok: float
|
|
expert_corrected_nok: float | None
|
|
realized_nok: float
|
|
gap_nok: float
|
|
gap_share: float | None
|
|
n_proposals: int
|
|
n_settled: int
|
|
n_pending: int
|
|
quantified_proposals: int
|
|
unquantified_proposals: int
|
|
approval_share: float | None
|
|
projects: list[ProjectValue]
|
|
learning: LearningEffect
|
|
goal: GoalProgress | None
|
|
cost: CostVersusValue | None
|
|
|
|
|
|
def _share(numerator: float, denominator: float) -> float | None:
|
|
"""A share, or ``None`` when there is nothing to divide by (never a fake zero)."""
|
|
if denominator == 0:
|
|
return None
|
|
return round(numerator / denominator, _SHARE_DIGITS)
|
|
|
|
|
|
def _corrected(decision: str | None, modelled_nok: float) -> tuple[float | None, str]:
|
|
"""Project one verdict onto the claim — the §1 honesty boundary, in one place.
|
|
|
|
Detaching this (returning ``modelled_nok`` for the pending or adjusted case)
|
|
is precisely the flattering lie the load-bearing tests exist to catch.
|
|
"""
|
|
if decision is None:
|
|
return None, "pending"
|
|
if decision == _REJECTED:
|
|
return 0.0, "rejected"
|
|
if decision == _ADJUSTED:
|
|
return None, "adjusted_unquantified"
|
|
return modelled_nok, "approved"
|
|
|
|
|
|
def _cohort(proposals: Sequence[ProposalValue]) -> Cohort:
|
|
"""Roll one cohort up: approval share over settled, gap share over quantified."""
|
|
settled = [p for p in proposals if p.decision is not None]
|
|
accepted = [p for p in settled if p.decision in _ACCEPTING]
|
|
quantified = [p for p in settled if p.expert_corrected_nok is not None]
|
|
modelled = sum(p.modelled_nok for p in quantified)
|
|
corrected = sum(p.expert_corrected_nok or 0.0 for p in quantified) if quantified else None
|
|
return Cohort(
|
|
settled=len(settled),
|
|
accepted=len(accepted),
|
|
approval_share=_share(len(accepted), len(settled)),
|
|
quantified=len(quantified),
|
|
modelled_nok=modelled,
|
|
expert_corrected_nok=corrected,
|
|
gap_share=None if corrected is None else _share(modelled - corrected, modelled),
|
|
)
|
|
|
|
|
|
def _learning_effect(proposals: Sequence[ProposalValue]) -> LearningEffect:
|
|
"""Split the settled history in two by ``run_id`` order and compare the halves.
|
|
|
|
The split point is the midpoint of the SETTLED proposals (pending ones carry
|
|
no judgment to compare). Fewer than two settled → not comparable.
|
|
"""
|
|
settled = sorted((p for p in proposals if p.decision is not None), key=lambda p: p.run_id)
|
|
if len(settled) < 2:
|
|
return LearningEffect(
|
|
comparable=False,
|
|
earlier=None,
|
|
later=None,
|
|
earlier_approval_share=None,
|
|
later_approval_share=None,
|
|
approval_share_delta=None,
|
|
earlier_gap_share=None,
|
|
later_gap_share=None,
|
|
gap_share_delta=None,
|
|
gap_shrinking=None,
|
|
)
|
|
midpoint = len(settled) // 2
|
|
earlier, later = _cohort(settled[:midpoint]), _cohort(settled[midpoint:])
|
|
approval_delta = (
|
|
None
|
|
if earlier.approval_share is None or later.approval_share is None
|
|
else round(later.approval_share - earlier.approval_share, _SHARE_DIGITS)
|
|
)
|
|
# The gap arithmetic is load-bearing: without it a rising approval share
|
|
# would be reported as "learning" with nothing measured behind it.
|
|
gap_delta = (
|
|
None
|
|
if earlier.gap_share is None or later.gap_share is None
|
|
else round(later.gap_share - earlier.gap_share, _SHARE_DIGITS)
|
|
)
|
|
return LearningEffect(
|
|
comparable=True,
|
|
earlier=earlier,
|
|
later=later,
|
|
earlier_approval_share=earlier.approval_share,
|
|
later_approval_share=later.approval_share,
|
|
approval_share_delta=approval_delta,
|
|
earlier_gap_share=earlier.gap_share,
|
|
later_gap_share=later.gap_share,
|
|
gap_share_delta=gap_delta,
|
|
gap_shrinking=None if gap_delta is None else gap_delta < 0,
|
|
)
|
|
|
|
|
|
def _project_value(
|
|
project_id: str, proposals: Sequence[ProposalValue], realized_by_project: dict[str, float]
|
|
) -> ProjectValue:
|
|
"""Roll one project up — realized stays UNMARKED until something passed the gate.
|
|
|
|
A project earns a realized ZERO only when every one of its proposals came
|
|
back rejected: that is a judged outcome. Anything else without a ledger entry
|
|
is ``None`` — unjudged is not the same as worth nothing, and neither is the
|
|
same as the modelled claim.
|
|
"""
|
|
quantified = [p for p in proposals if p.expert_corrected_nok is not None]
|
|
corrected = sum(p.expert_corrected_nok or 0.0 for p in quantified) if quantified else None
|
|
if project_id in realized_by_project:
|
|
realized: float | None = realized_by_project[project_id]
|
|
elif proposals and all(p.decision == _REJECTED for p in proposals):
|
|
realized = 0.0 # judged, and judged worthless — an earned zero
|
|
else:
|
|
realized = None
|
|
return ProjectValue(
|
|
project_id=project_id,
|
|
modelled_nok=sum(p.modelled_nok for p in proposals),
|
|
expert_corrected_nok=corrected,
|
|
realized_nok=realized,
|
|
quantified_proposals=len(quantified),
|
|
unquantified_proposals=len(proposals) - len(quantified),
|
|
proposals=list(proposals),
|
|
)
|
|
|
|
|
|
def load_ledger(ledger_path: Path | None) -> SavingsLedger:
|
|
"""Load the book fail-fast (§10) — a wrong-SHAPE ledger never masquerades as empty.
|
|
|
|
``None`` means no book was supplied (an empty one); anything else is opened
|
|
by ``SavingsLedger.load``, which refuses malformed bytes, a non-object top
|
|
level and a wrong-shaped object alike as ``ValueError``. Reading any of them
|
|
as an empty book would silently report every realized saving as unmarked.
|
|
This function adds ONLY the None case — the failure-type normalization lives
|
|
at the ledger's own entrance, so every caller gets it, not just this path.
|
|
"""
|
|
if ledger_path is None:
|
|
return SavingsLedger()
|
|
return SavingsLedger.load(ledger_path)
|
|
|
|
|
|
def build_value_report(
|
|
*,
|
|
outbox_dir: Path,
|
|
inbox_dir: Path | None = None,
|
|
ledger_path: Path | None = None,
|
|
goal: GoalContract | None = None,
|
|
estimated_cost_usd: float | None = None,
|
|
) -> ValueReport:
|
|
"""Project the three layers into one deterministic report — reads only, writes nothing.
|
|
|
|
The id-join is K5's: each outbox pair carries the ``verdict_id`` minted over
|
|
the candidate features, and an inbox verdict for that id is the expert stage
|
|
for that proposal. Ordering everywhere is ``run_id`` / ``project_id`` sorted,
|
|
so the same inputs always yield the same report (no clock, no set iteration).
|
|
|
|
``inbox_dir`` and ``ledger_path`` are optional because a run may legitimately
|
|
have neither yet — the result is a report where every expert-stage figure is
|
|
UNMARKED, which is the honest picture of a loop whose experts have not spoken.
|
|
"""
|
|
decisions = (
|
|
{}
|
|
if inbox_dir is None
|
|
else {document.id: document.decision for document in load_inbox(inbox_dir)}
|
|
)
|
|
ledger = load_ledger(ledger_path)
|
|
realized_by_project: dict[str, float] = {}
|
|
for entry in ledger.entries():
|
|
realized_by_project[entry.project] = realized_by_project.get(entry.project, 0.0) + (
|
|
entry.amount_nok
|
|
)
|
|
|
|
proposals: list[ProposalValue] = []
|
|
for outbox_proposal in load_outbox_proposals(outbox_dir):
|
|
decision = decisions.get(outbox_proposal.verdict_id)
|
|
corrected, status = _corrected(decision, outbox_proposal.claimed_saving_nok)
|
|
proposals.append(
|
|
ProposalValue(
|
|
run_id=outbox_proposal.run_id,
|
|
verdict_id=outbox_proposal.verdict_id,
|
|
project_id=outbox_proposal.project_id,
|
|
measure=outbox_proposal.measure,
|
|
modelled_nok=outbox_proposal.claimed_saving_nok,
|
|
decision=decision,
|
|
expert_corrected_nok=corrected,
|
|
status=status,
|
|
)
|
|
)
|
|
|
|
by_project: dict[str, list[ProposalValue]] = {}
|
|
for proposal in proposals:
|
|
by_project.setdefault(proposal.project_id, []).append(proposal)
|
|
projects = [
|
|
_project_value(project_id, by_project[project_id], realized_by_project)
|
|
for project_id in sorted(by_project)
|
|
]
|
|
|
|
modelled = sum(p.modelled_nok for p in proposals)
|
|
realized = ledger.total_realized_nok()
|
|
quantified = [p for p in proposals if p.expert_corrected_nok is not None]
|
|
settled = [p for p in proposals if p.decision is not None]
|
|
accepted = [p for p in settled if p.decision in _ACCEPTING]
|
|
return ValueReport(
|
|
modelled_nok=modelled,
|
|
expert_corrected_nok=(
|
|
sum(p.expert_corrected_nok or 0.0 for p in quantified) if quantified else None
|
|
),
|
|
realized_nok=realized,
|
|
gap_nok=modelled - realized,
|
|
gap_share=_share(modelled - realized, modelled),
|
|
n_proposals=len(proposals),
|
|
n_settled=len(settled),
|
|
n_pending=len(proposals) - len(settled),
|
|
quantified_proposals=len(quantified),
|
|
unquantified_proposals=len(proposals) - len(quantified),
|
|
approval_share=_share(len(accepted), len(settled)),
|
|
projects=projects,
|
|
learning=_learning_effect(proposals),
|
|
goal=(
|
|
None
|
|
if goal is None
|
|
else GoalProgress(
|
|
target_nok=goal.target_nok,
|
|
realized_nok=realized,
|
|
share=round(realized / goal.target_nok, _SHARE_DIGITS),
|
|
reached=realized >= goal.target_nok,
|
|
)
|
|
),
|
|
cost=(
|
|
None
|
|
if estimated_cost_usd is None
|
|
else CostVersusValue(
|
|
estimated_cost_usd=estimated_cost_usd,
|
|
realized_value_nok=realized,
|
|
note=(
|
|
"ESTIMAT (K6 upper bound) in USD against realized value in NOK — "
|
|
"reported side by side, NOT divided: this repo carries no sourced "
|
|
"NOK/USD rate, and a ratio would invent one (§1)."
|
|
),
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
def _cohort_payload(cohort: Cohort | None) -> dict[str, Any] | None:
|
|
if cohort is None:
|
|
return None
|
|
return {
|
|
"accepted": cohort.accepted,
|
|
"approval_share": cohort.approval_share,
|
|
"expert_corrected_nok": cohort.expert_corrected_nok,
|
|
"gap_share": cohort.gap_share,
|
|
"modelled_nok": cohort.modelled_nok,
|
|
"quantified": cohort.quantified,
|
|
"settled": cohort.settled,
|
|
}
|
|
|
|
|
|
def report_payload(report: ValueReport) -> dict[str, Any]:
|
|
"""The report as plain data — the one place the JSON shape is defined."""
|
|
return {
|
|
"approval_share": report.approval_share,
|
|
"cost": (
|
|
None
|
|
if report.cost is None
|
|
else {
|
|
"estimated_cost_usd": report.cost.estimated_cost_usd,
|
|
"note": report.cost.note,
|
|
"realized_value_nok": report.cost.realized_value_nok,
|
|
}
|
|
),
|
|
"expert_corrected_nok": report.expert_corrected_nok,
|
|
"gap_nok": report.gap_nok,
|
|
"gap_share": report.gap_share,
|
|
"goal": (
|
|
None
|
|
if report.goal is None
|
|
else {
|
|
"reached": report.goal.reached,
|
|
"realized_nok": report.goal.realized_nok,
|
|
"share": report.goal.share,
|
|
"target_nok": report.goal.target_nok,
|
|
}
|
|
),
|
|
"learning": {
|
|
"approval_share_delta": report.learning.approval_share_delta,
|
|
"comparable": report.learning.comparable,
|
|
"earlier": _cohort_payload(report.learning.earlier),
|
|
"earlier_approval_share": report.learning.earlier_approval_share,
|
|
"earlier_gap_share": report.learning.earlier_gap_share,
|
|
"gap_share_delta": report.learning.gap_share_delta,
|
|
"gap_shrinking": report.learning.gap_shrinking,
|
|
"later": _cohort_payload(report.learning.later),
|
|
"later_approval_share": report.learning.later_approval_share,
|
|
"later_gap_share": report.learning.later_gap_share,
|
|
},
|
|
"modelled_nok": report.modelled_nok,
|
|
"n_pending": report.n_pending,
|
|
"n_proposals": report.n_proposals,
|
|
"n_settled": report.n_settled,
|
|
"projects": [
|
|
{
|
|
"expert_corrected_nok": project.expert_corrected_nok,
|
|
"modelled_nok": project.modelled_nok,
|
|
"project_id": project.project_id,
|
|
"proposals": [
|
|
{
|
|
"decision": proposal.decision,
|
|
"expert_corrected_nok": proposal.expert_corrected_nok,
|
|
"measure": proposal.measure,
|
|
"modelled_nok": proposal.modelled_nok,
|
|
"run_id": proposal.run_id,
|
|
"status": proposal.status,
|
|
"verdict_id": proposal.verdict_id,
|
|
}
|
|
for proposal in project.proposals
|
|
],
|
|
"quantified_proposals": project.quantified_proposals,
|
|
"realized_nok": project.realized_nok,
|
|
"unquantified_proposals": project.unquantified_proposals,
|
|
}
|
|
for project in report.projects
|
|
],
|
|
"quantified_proposals": report.quantified_proposals,
|
|
"realized_nok": report.realized_nok,
|
|
"unquantified_proposals": report.unquantified_proposals,
|
|
}
|
|
|
|
|
|
def report_to_json(report: ValueReport) -> str:
|
|
"""Deterministic house JSON: sorted keys, 2-space indent, trailing LF."""
|
|
return json.dumps(report_payload(report), sort_keys=True, indent=2, ensure_ascii=False) + "\n"
|
|
|
|
|
|
def _nok(value: float | None) -> str:
|
|
"""Render a figure, or the UNMARKED label — never a blank that reads as zero."""
|
|
return _UNMARKED if value is None else f"{value:,.0f} NOK".replace(",", " ")
|
|
|
|
|
|
def _pct(share: float | None) -> str:
|
|
return _UNMARKED if share is None else f"{share * 100:.1f}%"
|
|
|
|
|
|
def render_report(report: ValueReport) -> str:
|
|
"""Render the report as text — every missing figure is LABELLED, never blank."""
|
|
lines = [
|
|
"VERDIRAPPORT — modellert → ekspert-korrigert → realisert",
|
|
(
|
|
f" modellert {_nok(report.modelled_nok)}\n"
|
|
f" ekspert-korrigert {_nok(report.expert_corrected_nok)} "
|
|
f"({report.quantified_proposals} of {report.n_proposals} proposals quantified, "
|
|
f"{report.unquantified_proposals} {_UNMARKED})\n"
|
|
f" realisert {_nok(report.realized_nok)} "
|
|
f"(gap vs modellert: {_nok(report.gap_nok)}, {_pct(report.gap_share)})"
|
|
),
|
|
(
|
|
f" {report.n_proposals} proposal(s): {report.n_settled} settled, "
|
|
f"{report.n_pending} awaiting a verdict, approval share "
|
|
f"{_pct(report.approval_share)}"
|
|
),
|
|
"",
|
|
"PER PROSJEKT",
|
|
]
|
|
for project in report.projects:
|
|
lines.append(
|
|
f" {project.project_id:<12} modellert {_nok(project.modelled_nok):>15} "
|
|
f"korrigert {_nok(project.expert_corrected_nok):>15} "
|
|
f"realisert {_nok(project.realized_nok):>15}"
|
|
)
|
|
lines.append("")
|
|
lines.append("LÆRINGSEFFEKT (tidligere → senere kjøringer, delt på run_id-rekkefølge)")
|
|
learning = report.learning
|
|
if not learning.comparable:
|
|
lines.append(
|
|
" not comparable — fewer than two settled proposals; one judgment is a "
|
|
"data point, not a trend"
|
|
)
|
|
else:
|
|
lines.append(
|
|
f" godkjenningsandel {_pct(learning.earlier_approval_share)} → "
|
|
f"{_pct(learning.later_approval_share)} "
|
|
f"(delta {_pct(learning.approval_share_delta)})"
|
|
)
|
|
lines.append(
|
|
f" gap modellert→korrigert {_pct(learning.earlier_gap_share)} → "
|
|
f"{_pct(learning.later_gap_share)} "
|
|
f"(delta {_pct(learning.gap_share_delta)}, "
|
|
f"shrinking={learning.gap_shrinking})"
|
|
)
|
|
if report.goal is not None:
|
|
lines.append("")
|
|
lines.append(
|
|
f"MÅLPROGRESJON {_nok(report.goal.realized_nok)} of "
|
|
f"{_nok(report.goal.target_nok)} ({_pct(report.goal.share)}), "
|
|
f"reached={report.goal.reached}"
|
|
)
|
|
if report.cost is not None:
|
|
lines.append("")
|
|
lines.append(
|
|
f"KOST MOT VERDI ~${report.cost.estimated_cost_usd:.6f} (ESTIMAT, K6 upper bound) "
|
|
f"vs realisert {_nok(report.cost.realized_value_nok)}"
|
|
)
|
|
lines.append(f" {report.cost.note}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
"""The thin CLI: load the three layers fail-fast (§10) → project → print → optionally write."""
|
|
parser = argparse.ArgumentParser(
|
|
description=(
|
|
"Project the outbox, the inbox and the ledger into one deterministic "
|
|
"value report — modelled → expert-corrected → realized, goal progress, "
|
|
"learning effect, cost against value. Reads only; no model call."
|
|
)
|
|
)
|
|
parser.add_argument("--outbox", required=True, help="outbox dir (run_id-named pairs, K5)")
|
|
parser.add_argument("--inbox", required=True, help="inbox dir (expert verdict files, §4.2)")
|
|
parser.add_argument("--ledger", default=None, help="ledger file (K1); absent = empty book")
|
|
parser.add_argument("--goal-nok", type=float, default=None, help="absolute savings target")
|
|
parser.add_argument(
|
|
"--estimated-cost-usd", type=float, default=None, help="K6 upper-bound run cost estimate"
|
|
)
|
|
parser.add_argument("--json", default=None, help="also write the report as JSON to this path")
|
|
args = parser.parse_args(argv)
|
|
|
|
try:
|
|
report = build_value_report(
|
|
outbox_dir=Path(args.outbox),
|
|
inbox_dir=Path(args.inbox),
|
|
ledger_path=None if args.ledger is None else Path(args.ledger),
|
|
goal=None
|
|
if args.goal_nok is None
|
|
else GoalContract(target_nok=args.goal_nok, mode="soft"),
|
|
estimated_cost_usd=args.estimated_cost_usd,
|
|
)
|
|
except (OSError, ValueError, ValidationError) as exc:
|
|
print(f"VALUE REPORT FAILED — refusing to project a malformed layer (§10): {exc}")
|
|
return 1
|
|
|
|
print(render_report(report))
|
|
if args.json is not None:
|
|
Path(args.json).write_text(report_to_json(report), encoding="utf-8", newline="\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|