docs(modelling): handover — per-measure bill savings landed (telescoping cascade)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 18:02:18 +00:00
parent b976c3abd2
commit ae5bbd0646

View file

@ -1,6 +1,6 @@
# HANDOVER — Modelling stage rebuild
**Branch:** `feature/bill-derivation` (worktree `/workspaces/home/hestia-worktrees/model-assemble-new-backend`). **HEAD:** `198122d1`.
**Branch:** `feature/bill-derivation` (worktree `/workspaces/home/hestia-worktrees/model-assemble-new-backend`). **HEAD:** `b976c3ab`.
**PRD:** GitHub `Hestia-Homes/Model#1152`, sliced into #1153#1161. **All slices #1153#1161 closed.**
## Issue status
@ -101,12 +101,19 @@ A `/grill-with-docs` pass designed the Modelling Bill-Derivation slice (ADR-0014
Key properties: **fuel-switch is handled for free** — we bill the fully-overlaid post-package `SapResult`, so a future oil→ASHP measure prices at the new fuel via `sap_code_to_fuel` (no per-measure fuel bookkeeping). Baseline and post are priced at one `FuelRates` snapshot, so the delta is rate-consistent. Carries ADR-0014's **appliances+cooking-stubbed-at-0** limitation (shared with Baseline, so savings stay consistent).
## Bill-Derivation: per-measure bill savings (`e79ffabf``b976c3ab`) — DONE
Filled `recommendation.kwh_savings` + `energy_cost_savings` via the **telescoping bill cascade** over the role-3 best-practice order. 3 slices, all green + pyright-strict-clean:
- **`e79ffabf`** — enabling refactor: pulled the cumulative-prefix scoring out of `marginal_impacts` into a reusable `scoring.cascade_scores(scorer, baseline, overlays) -> list[Score]` (index 0 = baseline, one `calculate` per prefix) + a pure `marginals_from_scores`. Each Score carries its `SapResult`, so the bill cascade re-bills the same prefixes the role-3 attribution scores — **no extra `calculate`**. `marginal_impacts` now delegates (behaviour unchanged).
- **`7e79c30a`** — `PlanMeasure` grows optional `kwh_savings` (delivered energy) + `energy_cost_savings` (£), signed so positive = saving, `None` until billed. `RecommendationRow` declares the live `recommendation.kwh_savings`/`energy_cost_savings` columns + maps them (None→NULL). Vestigial `recommendation.energy_savings` stays **undeclared** (legacy = 0). No FE migration (columns already live).
- **`b976c3ab`** — `_plan_for` scores baseline + every prefix once via `cascade_scores`, bills each at one Fuel Rates snapshot, and takes **consecutive Bill deltas** as each measure's marginal delivered-kWh + £ saving. The Plan's `baseline_bill`/`post_bill` are now the **same cascade endpoints** (`bills[0]`/`bills[-1]`), so per-measure savings telescope **exactly** to the headline savings — pinned on the real calculator (Σ per-measure == plan totals, abs ≤ 1e-6). Ventilation's saving is **negative** and still telescopes. Added `Bill.total_consumption_kwh` (shared by Plan + orchestrator); dropped the redundant standalone baseline `calculate`.
Key property: `MeasureImpact.energy_savings_kwh_per_yr` is *primary* energy and does **not** feed `kwh_savings``kwh_savings` is **delivered** energy from the Bill section kWh. Carries ADR-0014's appliances+cooking-stubbed-at-0 limitation.
## What's left
**Per-measure bill savings (next slice — designed, not built):** fill `recommendation.kwh_savings` + `energy_cost_savings` via a **telescoping bill cascade** over the role-3 best-practice order (fabric → heating → renewables): re-bill each cumulative prefix (reusing the per-prefix `sap_result`s from the role-3 cascade — no extra calls) and diff, telescoping exactly to the plan totals. Per-measure savings can be **negative** (ventilation increases energy) and still telescope. `recommendation.energy_savings` is **vestigial** (legacy = 0) — leave NULL. Note: `MeasureImpact.energy_savings_kwh_per_yr` is *primary* energy, not delivered — it does **not** feed `kwh_savings`.
**Deferred fronts** (open, post-#1161): exclusion-filtering of the candidate pool (deferred from #1160); a **Bill-Derivation slice** that re-runs bills on the post-package EPC to fill the deferred energy/bill columns (`plan.post_energy_consumption`/`post_energy_bill`, `recommendation.kwh_savings`/`energy_cost_savings`); persist **unselected alternatives** (`default=False` rows linked via `plan_id`) for the swap-in UX — open ADR-0016 question: what impact figure they carry; promote `ProductRepository` to the DB+file composite; non-EPC goal objectives (Energy Savings, Reducing CO2) in the optimiser. Possible extension of the ventilation trigger set to roof insulation (now a one-line data edit in `MEASURES_NEEDING_VENTILATION`); and making the dependency builder lazy (thunk) so the Product is only fetched when a trigger is actually selected.
**Deferred fronts** (open, post-#1161): exclusion-filtering of the candidate pool (deferred from #1160); persist **unselected alternatives** (`default=False` rows linked via `plan_id`) for the swap-in UX — open ADR-0016 question: what impact figure they carry; promote `ProductRepository` to the DB+file composite; non-EPC goal objectives (Energy Savings, Reducing CO2) in the optimiser. Possible extension of the ventilation trigger set to roof insulation (now a one-line data edit in `MEASURES_NEEDING_VENTILATION`); and making the dependency builder lazy (thunk) so the Product is only fetched when a trigger is actually selected.
## Key references