Model/docs/adr/0021-roof-insulation-eligibility.md
Khalim Conn-Kowlessar 0c7ce634db docs: roof-insulation eligibility design (ADR-0021 + CONTEXT)
Grill-with-docs outcome for the roof-insulation generator. One dispatching
recommend_roof_insulation, one Measure per roof by type (loft 300mm incl.
thatch / sloping-ceiling 100mm / flat-roof 200mm; no-access → none),
MAIN-only, room-in-roof deferred. Detection keys on the roof_construction_type
string (populated on both paths; the calculator already dispatches on it) with
sloping→flat→no-access→loft ordering; the roof_construction-int cross-mapper
parity is the follow-up Hestia-Homes/Model#1178. Thatch is not excluded — it
takes loft insulation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 20:53:21 +00:00

5.8 KiB

Roof Insulation Eligibility (loft vs sloping-ceiling vs flat-roof vs room-in-roof)

The roof Recommendation Generator must decide, per Property, which roof-insulation Measure to offer. Unlike walls (ADR-0019), where External and Internal are two competing Options under one Recommendation, a roof of a given type takes exactly one applicable Measure — there is no menu to choose between. We fix eligibility by roof type, detected from the roof's construction description.

Decision

One dispatching generator, one Measure per roof. recommend_roof_insulation inspects the MAIN building part's roof and emits a single "Roof" Recommendation carrying the one applicable Measure Option — folding the existing recommend_loft_insulation in as the loft branch. The measures are mutually exclusive by roof type, so the dispatch makes that exclusivity structural (no cross-guards, unlike the legacy is_sloping_ceiling_appropriate / is_loft_insulation_appropriate tangle).

Roof type (roof_construction_type substring) Measure Uninsulated trigger Overlay roof_insulation_thickness
sloping ceiling sloping_ceiling_insulation roof_insulation_thickness 0 100 mm
flat flat_roof_insulation roof_insulation_thickness None 200 mm
loft / thatch loft_insulation (existing) roof_insulation_thickness 0 300 mm
no access none (can't reach the void)
room-in-roof (sap_room_in_roof present) deferred

All three measures write the same overlay field — roof_insulation_thickness (verified against the Elmhurst before→after certs: sloping 0→100, flat None→200, thatch-loft 0→300). They differ only by detected roof type and recommended depth. The uninsulated trigger is therefore roof_insulation_thickness ∈ {0, None} (the Elmhurst mapper resolves "As Built" to 0 for pitched/sloping but None for flat). Note: flat_roof_insulation_thickness is not used on the Elmhurst path (it stays None even after a flat-roof measure) — it is an API-path field; reconciling that is part of the #1178 parity work. roof_insulation_location carries the human label (Sloping ceiling insulation / Flat roof insulation / Joists) as a corroborating signal.

Detection keys on roof_construction_type (the string), not the roof_construction int. This inverts the wall decision (ADR-0019 keys walls on the int because the Elmhurst wall description is empty) for a concrete reason: for roofs it is the int that the Elmhurst path leaves None (it only sets roof_construction_type via _strip_code(roof.roof_type)), while the string is populated on both paths — and the calculator already dispatches roof type the same way ("sloping ceiling" in roof_construction_type.lower(), heat_transmission.py:811). Keying the generator on the same field keeps generator and calculator reading one source. Populating roof_construction on the Elmhurst path for full cross-mapper parity is tracked separately in Hestia-Homes/Model#1178 (incl. the unknown gov code for thatch); the generator is not blocked on it.

Dispatch order matters (substring matching): sloping → flat → no-access → loft/thatch. "no access to loft" contains both "loft" and "access to loft", so the no-access branch must be tested before the loft branch, else a no-access roof would wrongly match loft.

Thatch is not excluded. A Pitched (thatch) roof takes loft (joist) insulation — the before→after Elmhurst cert shows Insulation: None → Joists, 300 mm. The thatch covering doesn't block insulating the loft floor beneath it; a breathability exclusion would only bite if we tried to insulate a thatched roof at the rafters, which is not in scope.

Room-in-roof is a distinct, deferred Measure. An RR (sap_room_in_roof present) is a habitable room in the roof space: its ceiling joists are the room's floor, so loft/joist insulation is physically impossible — it is insulated at its own slopes / stud walls / flat ceiling, and the quantity is the estimated rafter area, not the part's floor area (roof_area). Dispatch checks RR first and, finding one, emits nothing for now. Deferred pending retrofit-specialist before/after examples — soon to close.

MAIN building part only. Like every other rebuild generator (wall / loft / floor), the roof generator operates on the MAIN part. Multi-roof dwellings (MAIN + extensions) are out of scope here and land when extensions are tackled across all generators.

Considered options

  • Separate generators per measure (recommend_sloping_ceiling, recommend_flat_roof, alongside recommend_loft_insulation). Rejected: each would need cross-guards so only one fires on a given roof — reintroducing the interacting-conditions complexity the legacy RoofRecommendations carries. A single dispatcher makes one-measure-per-roof structural.
  • Key detection on the roof_construction int (consistent with walls). Rejected for now: the Elmhurst path leaves the int None, so it can't drive detection for the very fixtures we pin. Fixing that is #1178; until then the string is the only cross-path signal.
  • Exclude thatch (as we exclude cob/stone walls). Rejected: the cert evidence shows thatched roofs take loft insulation; the breathability concern is about rafter-level insulation of the covering, which we don't offer.

Consequences

  • Substring detection is looser than an enum; the match terms and their order are load-bearing and pinned against real fixtures. Closing #1178 lets a later revision switch to the int if desired.
  • A room-in-roof property gets no roof Measure until the deferred RR work lands — a known temporary gap, not a silent drop.
  • Recommended depths differ by Measure (loft 300 mm, flat 200 mm, sloping 100 mm), each pinned to the Elmhurst re-lodged after-cert at 1e-4.