feat: rebaseliner adopts calc output on physical-state change 🟩

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) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-26 19:09:55 +00:00
parent 1e019ea3b3
commit 2f7cfbf446
2 changed files with 54 additions and 7 deletions

View file

@ -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(

View file

@ -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)