diff --git a/docs/HANDOVER_MODELLING.md b/docs/HANDOVER_MODELLING.md index a58f56c3..c0faba36 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:** `641c1bd7`. +**Branch:** `feature/bill-derivation` (worktree `/workspaces/home/hestia-worktrees/model-assemble-new-backend`). **HEAD:** `198122d1`. **PRD:** GitHub `Hestia-Homes/Model#1152`, sliced into #1153–#1161. **All slices #1153–#1161 closed.** ## Issue status @@ -90,8 +90,22 @@ A `/grill-with-docs` pass found the rebuild had the **wrong optimiser objective* Decisions locked (in the ADR amendment): target predicate `sap_continuous ≥ band_floor` (e.g. ≥ 69 for C — conservative, no legacy `allow_slack`); **budget is a hard envelope** — a wall whose ventilation would bust the budget is **dropped, not forced over** (reverses the earlier "forced regardless of budget" call; presence still guaranteed for any *selected* wall); warm-start-on-signal + re-score + repair kept (not exhaustive re-score) for scalability; "recommend slightly more than land short" is satisfied by the conservative floor + repair, not by spending budget for headroom. +## Bill-Derivation: plan-level post-retrofit bills (`75ba5dd7`→`198122d1`) + +A `/grill-with-docs` pass designed the Modelling Bill-Derivation slice (ADR-0014 amended). Plan-level columns done across 4 slices; per-measure is the next slice. + +- **`ced6287b`** — relocated `Bill` / `EnergyBreakdown` / `BillDerivation` / `sap_fuel` (+ tests) from `domain/property_baseline/` to a neutral **`domain/billing/`** (cross-stage concern; both Baseline and Modelling consume it). Pure move, ~10 files. +- **`2bbc401f`** — `Score` gains `sap_result: Optional[SapResult]`, populated by `PackageScorer`. Lets Modelling bill the scored end-state reusing a `SapResult` the optimiser/orchestrator already computed — **no second `calculate`**. Optimiser ignores it (stays `Score`-only; stubs unaffected). +- **`26de28aa`** — `Plan` carries optional `baseline_bill` / `post_bill` and derives `post_energy_bill` / `energy_bill_savings` / `post_energy_consumption` / `energy_consumption_savings` (None until billed → NULL). +- **`198122d1`** — `ModellingOrchestrator` gains a constructor-injected `FuelRatesRepository` (mirrors Baseline — `get_current()` once, one `BillDerivation` per batch); `_plan_for` bills the baseline (`scorer.score(epc, [])`) and post-package (`package.score`) `SapResult`s at the same snapshot, savings = baseline − post. `PlanRow` mirror + `from_domain` persist the four columns (they already exist on the live `plan` table — no FE migration). Pipeline/handler wired. + +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). + ## 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. ## Key references