mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
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>
111 lines
4.5 KiB
Python
111 lines
4.5 KiB
Python
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,
|
|
*,
|
|
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):
|
|
"""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,
|
|
*,
|
|
physical_state_changed: bool = False,
|
|
) -> 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"
|
|
)
|
|
# 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)
|