docs(modelling): handover — ventilation now a generator + dependency delegates

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 14:09:32 +00:00
parent 02afc04ce2
commit d1f8d516f6

View file

@ -1,6 +1,6 @@
# HANDOVER — Modelling stage rebuild
**Branch:** `feature/bill-derivation` (worktree `/workspaces/home/hestia-worktrees/model-assemble-new-backend`). **HEAD:** `84ec6da0`.
**Branch:** `feature/bill-derivation` (worktree `/workspaces/home/hestia-worktrees/model-assemble-new-backend`). **HEAD:** `02afc04c`.
**PRD:** GitHub `Hestia-Homes/Model#1152`, sliced into #1153#1161. **All slices #1153#1161 closed.**
## Issue status
@ -75,7 +75,9 @@ Forks resolved with the user (AskUserQuestion): **guard now** (skip when already
3. **`1bf5b410`** — `domain/modelling/optimisation/measure_dependency.py`: `MEASURES_NEEDING_VENTILATION` (cavity/internal/external wall, cf. legacy `assumptions.measures_needing_ventilation`) + `ventilation_dependency(epc, products)` → MEV Option (`mechanical_ventilation_kind="EXTRACT_OR_PIV_OUTSIDE"`, decentralised MEV = legacy "mechanical, extract only"), priced at 2 fully-loaded units. Returns **None** when `sap_ventilation.mechanical_ventilation_kind` is already set (= legacy `has_ventilation` — confirmed against `backend/Property.py:1236`). Note: builder fetches the Product up-front, so the catalogue needs a `mechanical_ventilation` row for **every** not-yet-ventilated dwelling, even if no wall is ultimately selected.
4. **`0fec0699`** — orchestrator wiring: `_measure_dependencies` builds the (≤1) dependency; `_BEST_PRACTICE_ORDER` gains `"mechanical_ventilation"` between loft and floors (role-3 cascade walls→roof→**vent**→floor); ventilation persists as a Plan Measure with its real negative marginal + cost. Added `mechanical_ventilation: 0.26` contingency (legacy `Costs.CONTINGENCIES`). On 000490 the real calculator scores MEV at **1.275 SAP**.
Gotchas for the next agent: the ventilation Product/contingency must exist for any not-yet-ventilated dwelling (build-time fetch, not inject-time); the stub scorer in `test_optimiser.py` indexes `building_parts[MAIN]`, so vent-only overlays need the separate `_VentStubScorer`.
**Post-#1161 refactor (`631df921``02afc04c`):** production split from selection-semantics. Detection + pricing moved into a proper generator `generators/ventilation_recommendation.py::recommend_ventilation(epc, products) -> Optional[Recommendation]` (same shape as wall/roof/floor; guard returns None when already mechanically ventilated). `optimisation/measure_dependency.py` now owns only the trigger set + the forced-edge wrapping: `ventilation_dependency` delegates to the generator and wraps the Recommendation (cheapest Option) into the `MeasureDependency`. The orchestrator's `_measure_dependencies` call is unchanged. **Key asymmetry:** `recommend_ventilation` lives in `generators/` but is **not** in `_candidate_recommendations`' generator tuple — it's consumed only by the dependency path, never the free pool. This is the natural home for the multi-option future (MEV-c / MVHR) and the FE swap-in front.
Gotchas for the next agent: the ventilation Product/contingency must exist for any not-yet-ventilated dwelling (the generator fetches the Product at build time, not inject-time); the stub scorer in `test_optimiser.py` indexes `building_parts[MAIN]`, so vent-only overlays need the separate `_VentStubScorer`.
## What's left