"""The deterministic validator (method-spec §3 Step 4, frozen by the golden suite §7.2). The one endpoint-free judge that anchors the loop against swarm self-confirmation — mandatory, blocking, never an optional plugin. Implements the spec's reference procedure: the feasibility bound is the closed form ``0.30 × Σ quantity·unit_cost``; the risk simulation is a Mersenne-Twister Monte Carlo (seed 20260624, 512 samples, uniform draws from each item's assumptions band, fixed cost when no band) whose ``p10``/``p50``/``p90`` are the 1st/5th/9th cut points of the 10-quantiles (inclusive method). ``shared/examples/bygg-energi-mikro/golden.json`` is the ONLY ground truth (§7); ``test_bygg_energi_mikro.py`` freezes every decided field. """ from __future__ import annotations import random import statistics from pydantic import BaseModel from portfolio_optimiser_claude.ir import SavingsProposal # Policy cap (§3 Step 4): max feasible saving as a fraction of the affected total. _FEASIBLE_FRACTION = 0.30 # Frozen by the golden suite (§7.2) — changing either detaches from the fasit. _MC_SEED = 20260624 _MC_SAMPLES = 512 class ValidatedProposal(BaseModel): """The validated outcome: the claim sits within the feasible range (§7.2).""" validates: bool claimed_saving_nok: float nominal_feasible: float p10: float p50: float p90: float class Rejection(BaseModel): """A structural block — a DISTINCT type from ``ValidatedProposal`` (§3 Step 4). Carries the claimed and feasible figures in its ``reason`` and NO percentiles, so it can never be consumed as validated. """ reason: str def validate_proposal(proposal: SavingsProposal) -> ValidatedProposal | Rejection: """Gate the numbers deterministically (§3 Step 4): validated outcome or rejection.""" affected_total = sum(item.quantity * item.unit_cost for item in proposal.affected_items) nominal_feasible = _FEASIBLE_FRACTION * affected_total rng = random.Random(_MC_SEED) feasible_samples: list[float] = [] for _ in range(_MC_SAMPLES): sampled_total = 0.0 for item in proposal.affected_items: band = proposal.assumptions.get(item.code) unit_cost = item.unit_cost if band is None else rng.uniform(band[0], band[1]) sampled_total += item.quantity * unit_cost feasible_samples.append(_FEASIBLE_FRACTION * sampled_total) cut_points = statistics.quantiles(feasible_samples, n=10, method="inclusive") p10, p50, p90 = cut_points[0], cut_points[4], cut_points[8] if proposal.claimed_saving_nok > p90: return Rejection( reason=( f"claimed saving {proposal.claimed_saving_nok:.2f} NOK exceeds the " f"optimistic feasible bound {p90:.2f} NOK " f"(nominal feasible {nominal_feasible:.2f} NOK)" ) ) return ValidatedProposal( validates=True, claimed_saving_nok=proposal.claimed_saving_nok, nominal_feasible=nominal_feasible, p10=p10, p50=p50, p90=p90, )