from __future__ import annotations from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Literal, Optional, TYPE_CHECKING from datatypes.epc.domain.epc_property_data import EpcPropertyData from domain.property_baseline.performance import Performance if TYPE_CHECKING: from domain.sap10_calculator.calculator import SapResult RebaselineReason = Literal["none", "pre_sap10", "physical_state_changed", "both"] # The SAP spec version below which a cert's recorded scores reflect a superseded # methodology and must be rebaselined to the calculator's output (CONTEXT.md: # Rebaselining). _SAP10_FLOOR = 10.0 class RebaselineNotImplemented(Exception): """A Property needs Rebaselining, but the ML adapter is not wired yet. Raised rather than silently recording ``reason="none"`` for a property that genuinely needs rebaselining — a plausible-but-wrong baseline is expensive to discover downstream. Surfaces how much of a First Run cohort the pipeline can handle today (#1135). """ @dataclass(frozen=True) class RebaselineResult: """The outcome of Rebaselining a Property: its Effective Performance, why it differs from Lodged, and the calculator `SapResult` it was scored from. ``sap_result`` is the scored picture (ADR-0013 amendment) — a first-class part of the result because Bill Derivation prices the *same* scoring (ADR-0014). It is ``None`` only for a Rebaseliner that ran no calculator (the test ``StubRebaseliner``); the load-bearing ``CalculatorRebaseliner`` always sets it. """ effective: Performance reason: RebaselineReason sap_result: Optional["SapResult"] class Rebaseliner(ABC): """Produces a Property's Effective Performance by Rebaselining its Effective EPC. Rebaselining (CONTEXT.md) assembles the Effective EPC picture and scores it through SAP10 Calculation, replacing the recorded scores when the EPC was lodged pre-SAP10 or its physical state diverged from the lodged EPC; otherwise Effective Performance equals Lodged. Injected into the PropertyBaselineOrchestrator (ADR-0011) so the implementation can swap without touching the orchestrator, and so the single-property re-score-on-override flow reuses the same port. """ @abstractmethod def rebaseline( self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance ) -> RebaselineResult: ... class StubRebaseliner(Rebaseliner): """A no-calculator stub for tests that don't want the real calculator. SAP10 certs pass through untouched — Effective Performance equals Lodged, reason ``"none"``, and ``sap_result`` is ``None`` (no calculator ran). A pre-SAP10 cert genuinely needs rebaselining, which this stub does not do, so it raises rather than fabricating a "none". Production uses ``CalculatorRebaseliner`` (the calculator is load-bearing — ADR-0013 amendment); this stub stays for orchestrator/repo unit tests that don't exercise the bill. """ def rebaseline( self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance ) -> RebaselineResult: sap_version = effective_epc.sap_version if sap_version is not None and sap_version < _SAP10_FLOOR: raise RebaselineNotImplemented( f"Property needs rebaselining (pre-SAP10 cert, sap_version=" f"{sap_version}); this stub does not run the calculator" ) return RebaselineResult(effective=lodged, reason="none", sap_result=None)