test(valuereport): bind _SHARE_DIGITS to its MEASURED band, not to itself

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
This commit is contained in:
Kjell Tore Guttormsen 2026-07-26 15:28:20 +02:00
commit 8a141370f3
2 changed files with 138 additions and 1 deletions

View file

@ -58,7 +58,13 @@ _REJECTED = "rejected"
_ADJUSTED = "approved_with_adjustment"
_UNMARKED = "UNMARKED"
_SHARE_DIGITS = 6 # shares are rounded so the JSON bytes never carry float noise
# 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)

View file

@ -45,12 +45,18 @@ from __future__ import annotations
import json
import shutil
from dataclasses import dataclass
from decimal import Decimal
from pathlib import Path
from typing import Sequence
import pytest
from portfolio_optimiser_claude.experience import CandidateFeatures
from portfolio_optimiser_claude.goals import GoalContract
from portfolio_optimiser_claude.inbox import VerdictDocument, write_verdict
from portfolio_optimiser_claude.ir import AffectedItem, SavingsProposal
from portfolio_optimiser_claude.valuereport import (
_SHARE_DIGITS,
ProjectValue,
ValueReport,
build_value_report,
@ -228,6 +234,131 @@ def test_learning_needs_two_cohorts_before_it_claims_anything(layers: Layers) ->
assert learning.gap_shrinking is None
# --- what _SHARE_DIGITS is worth: the MEASURED band, not a detach proof -----------------------
#
# The constant was detach-proof but value-unproven: mutating 6 → 4 left all 628
# tests green, so nothing bound the figure to the claim it carries. These two
# tests bind the band that was actually MEASURED, and it is narrower than the
# comment alone suggested:
#
# * d >= 17 → the 1-ULP float tail of a cohort subtraction survives into 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] → every consumer this system has behaves identically.
#
# So 6 → 4 CANNOT be made red without inventing a resolution requirement no
# layer states — and §1 forbids asserting more than the implementation carries.
# The honest binding is the band; the choice of 6 inside it is convention.
_COHORT_PROJECT = "FV42-P1"
@dataclass(frozen=True)
class _Settled:
"""One judged proposal for a synthetic two-cohort history."""
run_id: str
claimed_nok: float
decision: str
# Cohort split is the midpoint of the settled proposals, so the first two runs
# are the earlier cohort and the last two the later one. A rejected verdict
# corrects to 0 and an approved one leaves the claim standing, which is what
# makes each cohort's gap_share the ratio of rejected NOK to quantified NOK.
#
# gap_share 30000/100000 = 0.3 → 10000/100000 = 0.1. The float subtraction
# 0.1 - 0.3 is -0.19999999999999998: a tail the rounding exists to absorb.
_NOISE_HISTORY = (
_Settled("run-101", 30_000.0, "rejected"),
_Settled("run-102", 70_000.0, "approved"),
_Settled("run-103", 10_000.0, "rejected"),
_Settled("run-104", 90_000.0, "approved"),
)
# gap_share 20000/70000 = 2/7 → 30000/70000 = 3/7: repeating decimals, so the
# rendered percent is sensitive to how much of them survives the rounding.
_REPEATING_HISTORY = (
_Settled("run-201", 20_000.0, "rejected"),
_Settled("run-202", 50_000.0, "approved"),
_Settled("run-203", 30_000.0, "rejected"),
_Settled("run-204", 40_000.0, "approved"),
)
def _history_layers(root: Path, history: Sequence[_Settled]) -> tuple[Path, Path]:
"""Write an outbox pair + inbox verdict per proposal, joined by a REAL minted id.
Ids are minted through ``VerdictDocument.from_candidate`` rather than typed
by hand, so the join under test is the production one (K5/§4.2).
"""
outbox, inbox = root / "outbox", root / "inbox"
outbox.mkdir(parents=True)
for item in history:
proposal = SavingsProposal(
project_id=_COHORT_PROJECT,
measure=f"measure-{item.run_id}",
affected_items=[AffectedItem(code="VT-07", quantity=1.0, unit_cost=item.claimed_nok)],
claimed_saving_nok=item.claimed_nok,
)
verdict = VerdictDocument.from_candidate(
CandidateFeatures.from_proposal(proposal),
decision=item.decision,
rationale=f"cohort fixture for {item.run_id}",
description=f"{proposal.measure} in {_COHORT_PROJECT}",
)
write_verdict(inbox, verdict)
(outbox / f"{item.run_id}-proposal.json").write_text(
json.dumps(proposal.model_dump(), sort_keys=True, indent=2), encoding="utf-8"
)
(outbox / f"{item.run_id}-outcome.json").write_text(
json.dumps({"run_id": item.run_id, "verdict_id": verdict.id}, sort_keys=True, indent=2),
encoding="utf-8",
)
return outbox, inbox
def test_share_rounding_keeps_the_float_tail_out_of_the_json_bytes(tmp_path: Path) -> None:
"""VALUE proof (upper bound): raise _SHARE_DIGITS to 17 and this goes RED.
The claim on the constant is that the JSON bytes never carry float noise.
Stated without reference to the constant's own value: the PUBLISHED delta is
the exact decimal difference of the two PUBLISHED cohort shares. Comparing
against the constant-derived literal would only bind the number to itself.
"""
outbox, inbox = _history_layers(tmp_path / "noise", _NOISE_HISTORY)
learning = json.loads(report_to_json(build_value_report(outbox_dir=outbox, inbox_dir=inbox)))[
"learning"
]
earlier, later = learning["earlier_gap_share"], learning["later_gap_share"]
# Guard the fixture itself: this history is only evidence while the raw
# subtraction actually has a tail to absorb.
assert repr(later - earlier) == "-0.19999999999999998"
assert Decimal(str(learning["gap_share_delta"])) == Decimal(str(later)) - Decimal(str(earlier))
def test_share_rounding_survives_into_the_rendered_percent(tmp_path: Path) -> None:
"""VALUE proof (lower bound): drop _SHARE_DIGITS to 2 and this goes RED.
``_pct`` renders a share as a percent with one decimal. The expectation is
computed from the RAW NOK figures, so it holds the rounding to the ratio the
layers carry rather than to whatever the constant currently happens to be.
"""
outbox, inbox = _history_layers(tmp_path / "repeating", _REPEATING_HISTORY)
rendered = render_report(build_value_report(outbox_dir=outbox, inbox_dir=inbox))
assert f"{20_000 / 70_000 * 100:.1f}%" in rendered # earlier cohort gap, 28.6%
assert f"{30_000 / 70_000 * 100:.1f}%" in rendered # later cohort gap, 42.9%
def test_share_digits_sits_inside_the_band_its_two_value_proofs_bound() -> None:
"""The band the two proofs above measure, asserted on the constant itself."""
assert 3 <= _SHARE_DIGITS <= 16
# --- goal progress + cost against value ------------------------------------------------------