diff --git a/docs/HANDOVER_MODELLING.md b/docs/HANDOVER_MODELLING.md index c0faba36..f82af3eb 100644 --- a/docs/HANDOVER_MODELLING.md +++ b/docs/HANDOVER_MODELLING.md @@ -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