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:
parent
f41264fbd3
commit
8a141370f3
2 changed files with 138 additions and 1 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ------------------------------------------------------
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue