Model/docs/adr/0041-flat-roofs-scored-by-insulation-thickness.md
Khalim Conn-Kowlessar cba536eba8 Record flat-roof-thickness and glazing-reconcile decisions (ADR 0041/0042)
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>
2026-07-01 09:22:13 +00:00

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 RoofType taxonomy — 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_thickness for a flat roof with a known depth (reusing the existing mm regex), alongside roof_construction_type='Flat', so u_roof reaches Table 16 col (1) instead of the age-band default.
  • Reclassify the existing flat: Nmm rows 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 RoofType value column is a Drizzle-owned Postgres enum (see main-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-cache value writes; the property_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, --apply in a transaction, idempotent), and surfaces any members the live enum does not yet carry as deferred, exactly as Class A did.