docs(modelling): ADR-0014 amendment — cross-stage billing + Modelling post-package bills

Records the /grill-with-docs design for the Modelling Bill-Derivation slice:
Bill Derivation is cross-stage (relocate Bill/EnergyBreakdown/BillDerivation/
sap_fuel to a neutral domain/billing/); Modelling bills the fully-overlaid
post-package SapResult (so fuel-switch measures price at the new fuel for free),
diffing against the baseline at the same FuelRates snapshot; the post-package
and baseline SapResults are captured from scores the optimiser/orchestrator
already compute (Score.sap_result), so no second calculate; FuelRatesRepository
is constructor-injected into ModellingOrchestrator mirroring Baseline; plan-level
columns this slice, per-measure telescoping bill cascade next (energy_savings is
vestigial, left NULL).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 17:17:03 +00:00
parent 660dc54246
commit 75ba5dd744

View file

@ -136,3 +136,19 @@ the fuel each end use burns come from?* Resolved in a `/grill-with-docs` session
`BillSection` gains `COOLING` (kWh from `SapResult.space_cooling_fuel_kwh_per_yr`, electricity by
construction), so §6's layout gains a `cooling_kwh` + `cooling_cost_gbp` column pair (FE-owned
Drizzle migration).
## Amendment (2026-06-03): Bill Derivation is cross-stage; the Modelling stage prices the post-package end-state
Bill Derivation is no longer Baseline-only — the **Modelling** stage now re-runs it on the **Optimised Package** to produce post-retrofit bills and savings. Decided in a `/grill-with-docs` session.
- **Bill Derivation is a cross-stage domain concern → relocate to `domain/billing/`.** `Bill` / `EnergyBreakdown` / `BillDerivation` / `sap_fuel` were under `domain/property_baseline/` only because Baseline was built first. Two stages now consume them, and a `modelling → property_baseline` import would couple two stages ADR-0011 keeps independent under a name that wrongly implies ownership. They move to a neutral `domain/billing/` (`Fuel`/`FuelRates` already live in the shared `domain/fuel_rates/`). Mechanical move + import rewrite; covered by the existing Baseline tests.
- **Modelling bills the simulated *end-state*, never adjusts the baseline bill.** The post-retrofit bill is `BillDerivation.derive(EnergyBreakdown.from_sap_result(post_package_sap_result))`, where the `SapResult` comes from scoring the fully-overlaid `EpcPropertyData` (all selected Simulation Overlays + injected dependencies). **This is what makes fuel-switch measures correct for free:** a measure that switches heating fuel (e.g. oil → electric ASHP) changes the heating fuel *code* on that `SapResult`, so `sap_code_to_fuel` prices it at the *new* fuel automatically — no per-measure fuel bookkeeping. Savings are `baseline post`, both priced at the **same** `FuelRates` snapshot (read once per run), so the delta is never polluted by a rate change.
- **No second calculator pass.** The post-package `SapResult` is the one the optimiser's whole-package re-score (role 2) already computed; it rides on the `Score` (`Score.sap_result`, populated by `PackageScorer`, ignored by the optimiser — so the optimiser stays `Score`-only and its stub-scorer tests are unaffected). Likewise the baseline `SapResult` is the one the orchestrator already scores for the role-3 cascade and the target gain. Billing reuses both — zero extra `calculate`.
- **`FuelRatesRepository` is constructor-injected into `ModellingOrchestrator`**, mirroring the Baseline orchestrator — `get_current()` once per `run()`, one `BillDerivation` reused across the batch. Not on the `UnitOfWork` (read-once reference data, ADR-0011). The extra per-pipeline read (Baseline + Modelling each resolve rates) is accepted; a shared/injected snapshot is a future optimisation.
- **Plan-level first, per-measure savings next (telescoping cascade).** This slice fills the plan columns (`post_energy_bill`, `post_energy_consumption`, `energy_bill_savings`, `energy_consumption_savings`). Per-measure `recommendation.kwh_savings` / `energy_cost_savings` come from a **bill cascade over the role-3 best-practice order** (fabric → heating → renewables) — re-bill each cumulative prefix and diff, telescoping exactly to the plan totals (mirroring the SAP role-3 attribution; reuses the per-prefix `sap_result`s, no extra calls). Per-measure savings can be **negative** (ventilation increases energy) and still telescope. The legacy `recommendation.energy_savings` column is **vestigial** (legacy set it to `0`; the canonical delivered-energy field is `kwh_savings`) — left NULL.
- **Limitation carried over.** The "Appliances + cooking kWh stubbed at 0" deferral above still applies — Modelling's post-package bill understates by the same unregulated-electricity load until those fields land on `SapResult`. Baseline and Modelling share the gap, so baseline-vs-post savings remain consistent.