ADR-0041: flat roofs ARE scored by insulation thickness via RdSAP Table 16
col (1) ('joists at ceiling level and flat roof'); the RoofType taxonomy lacks
flat-thickness members so the depth is discarded — fix needs new FLAT_*MM members
(FE-owned pgEnum). ADR-0042: a landlord glazing override reconciles against the
cert's per-window sap_windows composition rather than flattening every window to
one type; mixed-and-close defers to the cert, mixed-and-far is flagged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
4.1 KiB
Flat roofs are scored by insulation thickness (RdSAP Table 16 col 1), not the age-band default
Context
A Landlord Override naming a flat roof with a known insulation depth (flat: 50mm / 100mm / 150mm) is classified to the canonical RoofType value "Flat, insulated (assumed)", which the roof Simulation Overlay resolves to roof_construction_type='Flat' with no thickness. The calculator then takes the U-value from the flat age-band default (_FLAT_ROOF_BY_AGE, RdSAP Table 18 column (3)). The stated depth is discarded — ~102 property_overrides rows.
The overlay's own comment asserted "No flat RoofType value carries an explicit mm depth". That is a taxonomy artefact, not a truth about the data: the RoofType enum has flat members only for insulation state (FLAT_INSULATED / _ASSUMED / LIMITED / NO_INSULATION) and no flat-thickness members — whereas pitched has the full PITCHED_LOFT_12MM … 400MM ladder. So a flat depth has nowhere to land and is lost at classification, before the overlay ever runs.
Reviewing the RdSAP 10 Specification (10-06-2025) during grilling (issue #1361 override audit / #1376) settled the domain fact: Table 16 ("Roof U-values when loft insulation thickness is known"), column (1), is headed "Insulation at joists at ceiling level and flat roof". A flat roof is scored by thickness — 50 mm → 0.68, 100 mm → 0.40, 150 mm → 0.30. Table 18 (the age-band default) is explicitly only the fallback "used when thickness of insulation cannot be determined". The earlier assumption that flat roofs are age-band-only was wrong.
The calculator already implements this correctly: u_roof routes a flat roof with a thickness through _ROOF_BY_THICKNESS (which is Table 16 col (1)). So a flat roof given its depth scores right today — the only defect is that the depth never reaches the calculator, because the taxonomy can't carry it.
This is the opposite of the wall / flat "as built (assumed)" cases (ADR-0033), where no real datum exists and the age band is the correct lever. Here a real measurement exists and RdSAP scores it.
Decision
Make a flat roof's known insulation thickness first-class, so it reaches Table 16 col (1):
- Add flat-thickness members to the
RoofTypetaxonomy —FLAT_12MM … FLAT_400_PLUS_MM(values like"Flat, 150 mm insulation"— not "loft"; a flat roof has no loft). Minimum set for today's data: 50, 100, 150; the full ladder mirrors the pitched members for symmetry. - The roof Simulation Overlay emits
roof_insulation_thicknessfor a flat roof with a known depth (reusing the existing mm regex), alongsideroof_construction_type='Flat', sou_roofreaches Table 16 col (1) instead of the age-band default. - Reclassify the existing
flat: Nmmrows off"Flat, insulated (assumed)"onto the new members. - Correct the misleading overlay comment.
The flat/pitched distinction is retained even though Table 16 col (1) gives the same U at a given thickness for both — it stays load-bearing for non-U concerns (measure eligibility, shape), so flat depths are not collapsed onto the pitched members.
Consequences
- ~102 flat-roof properties re-score from the age-band default to their true thickness-based U-value — both directions (a newer-band flat roof with 150 mm improves; an old-band flat with 50 mm may worsen relative to its default).
- Cross-repo (FE-owned pgEnum, Dan). The
RoofTypevaluecolumn is a Drizzle-owned Postgres enum (seemain-heating-system-pgenum-is-fe-owned; PR #1361 Class A/B). The new members must be added to the pgEnum by the FE owner before the classifier-cachevaluewrites; theproperty_overrides(TEXT) reclassify is immediate. So this is an enum-dependent slice, grouped with the other new-archetype slices of #1376 — not the no-enum roof/glazing resolver slice. - The reclassify follows the established one-time-script shape (dry-run default,
--applyin a transaction, idempotent), and surfaces any members the live enum does not yet carry as deferred, exactly as Class A did.