From 2f7cfbf4460de4c76b2d7931d65901060b0b1e9c Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jun 2026 19:09:55 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20rebaseliner=20adopts=20calc=20output=20?= =?UTF-8?q?on=20physical-state=20change=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CalculatorRebaseliner uses the calculator output as Effective Performance whenever a Rebaselining trigger fired — pre-SAP10 (a) OR overrides/prediction moved the physical state (b)/(c) — tagging pre_sap10 / physical_state_changed / both. Only a pristine lodged >=10.2 cert keeps its accredited figure (the sole case the calculator runs purely to validate). Divergence is logged only in that pristine case. ABC + StubRebaseliner take the new keyword-only flag. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../calculator_rebaseliner.py | 31 ++++++++++++++++--- domain/property_baseline/rebaseliner.py | 30 ++++++++++++++++-- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/domain/property_baseline/calculator_rebaseliner.py b/domain/property_baseline/calculator_rebaseliner.py index 6ed95c4e..04558ab7 100644 --- a/domain/property_baseline/calculator_rebaseliner.py +++ b/domain/property_baseline/calculator_rebaseliner.py @@ -4,7 +4,11 @@ import logging from typing import TYPE_CHECKING, Optional from domain.property_baseline.performance import Performance -from domain.property_baseline.rebaseliner import Rebaseliner, RebaselineResult +from domain.property_baseline.rebaseliner import ( + Rebaseliner, + RebaselineReason, + RebaselineResult, +) if TYPE_CHECKING: from datatypes.epc.domain.epc_property_data import EpcPropertyData @@ -50,7 +54,12 @@ class CalculatorRebaseliner(Rebaseliner): self._calculator = calculator def rebaseline( - self, property_id: int, effective_epc: "EpcPropertyData", lodged: Performance + self, + property_id: int, + effective_epc: "EpcPropertyData", + lodged: Performance, + *, + physical_state_changed: bool = False, ) -> RebaselineResult: # A raise (UnmappedSapCode, etc.) propagates: the calculator is # load-bearing, so the batch aborts and the cert is fixed at once. The @@ -58,10 +67,24 @@ class CalculatorRebaseliner(Rebaseliner): # regardless of whether lodged or calculated figures win (ADR-0013/0014). result: SapResult = self._calculator.calculate(effective_epc) sap_version: Optional[float] = effective_epc.sap_version - if sap_version is not None and sap_version < _MIN_TRUSTED_SAP_VERSION: + pre_sap10 = sap_version is not None and sap_version < _MIN_TRUSTED_SAP_VERSION + # The calculator output IS Effective Performance whenever a Rebaselining + # trigger fired: (a) a superseded methodology (pre-SAP10), or (b)/(c) the + # physical state was changed by Landlord Overrides / Prediction. Only a + # pristine lodged SAP >= 10.2 cert keeps its accredited figure — that is + # the *only* case where the calculator runs purely to validate (and where + # its known divergence from the accredited register would mislead). + if pre_sap10 or physical_state_changed: + reason: RebaselineReason = ( + "both" + if pre_sap10 and physical_state_changed + else "pre_sap10" + if pre_sap10 + else "physical_state_changed" + ) return RebaselineResult( effective=Performance.from_sap_result(result), - reason="pre_sap10", + reason=reason, sap_result=result, ) self._log_divergence( diff --git a/domain/property_baseline/rebaseliner.py b/domain/property_baseline/rebaseliner.py index e5d94d3f..9171b4bf 100644 --- a/domain/property_baseline/rebaseliner.py +++ b/domain/property_baseline/rebaseliner.py @@ -59,8 +59,20 @@ class Rebaseliner(ABC): @abstractmethod def rebaseline( - self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance - ) -> RebaselineResult: ... + self, + property_id: int, + effective_epc: EpcPropertyData, + lodged: Performance, + *, + physical_state_changed: bool = False, + ) -> RebaselineResult: + """Produce Effective Performance. ``physical_state_changed`` is True when + the Effective EPC was assembled from something other than a pristine + lodged cert — Landlord Overrides, Site Notes, or EPC Prediction moved the + physical picture (Rebaselining trigger (b)/(c)) — so the accredited lodged + figure no longer describes the dwelling and the calculator output wins + even at SAP >= 10.2.""" + ... class StubRebaseliner(Rebaseliner): @@ -76,7 +88,12 @@ class StubRebaseliner(Rebaseliner): """ def rebaseline( - self, property_id: int, effective_epc: EpcPropertyData, lodged: Performance + self, + property_id: int, + effective_epc: EpcPropertyData, + lodged: Performance, + *, + physical_state_changed: bool = False, ) -> RebaselineResult: sap_version = effective_epc.sap_version if sap_version is not None and sap_version < _SAP10_FLOOR: @@ -84,4 +101,11 @@ class StubRebaseliner(Rebaseliner): f"Property needs rebaselining (pre-SAP10 cert, sap_version=" f"{sap_version}); this stub does not run the calculator" ) + # A physical-state change needs the calculator this stub does not run; + # raise rather than fabricate a "none" that ignores the overrides. + if physical_state_changed: + raise RebaselineNotImplemented( + "Property needs rebaselining (physical state changed by overrides " + "/ prediction); this stub does not run the calculator" + ) return RebaselineResult(effective=lodged, reason="none", sap_result=None)