Document pitched sloping-ceiling roof-type family (ADR-0066 + glossary)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-23 13:23:36 +00:00
parent f1e190293b
commit fc738fa843
2 changed files with 143 additions and 0 deletions

View file

@ -155,6 +155,10 @@ _Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target
The RdSAP string sentinels a cert may lodge in place of a numeric value (e.g. `roof_insulation_thickness`). They are data, not noise, and are **not interchangeable**: **`ND`** (Not Defined) — no data; resolve to the construction age band's **as-built** state (roof as-built: AD uninsulated, EF limited insulation, G+ insulated). **`NI`** — insulation **present** but thickness unknown (RdSAP 10 §5.11.4: score as 50 mm); never "no insulation". **`AB`** — As Built; resolve by age band like ND. The mapper deliberately passes roof sentinels through raw — the calculator's resolution depends on distinguishing them (ADR-0047). Scoring and eligibility may legitimately resolve the same sentinel differently (`NI`: 50 mm to the U-cascade, not-eligible to the roof generator).
_Avoid_: reading `ND` as "no insulation"; reading `NI` as "no insulation" (it means the opposite); normalising sentinels at the mapper boundary; comparing a sentinel string against an int (`"ND" != 0` is always True)
**Pitched-Roof Insulation Geometry (loft / at-rafters / sloping ceiling)**:
Three *distinct* RdSAP constructions a pitched roof can carry, separated by `roof_insulation_location` and scored on different U-tables — **never interchangeable**. **Loft (joists)**`roof_insulation_location = 2`, insulation lying on the ceiling joists over a loft void; scores on Table 16 **col (1)** by thickness, Table 18 **col (1)** (`_ROOF_BY_AGE`, floored at 0.40) by age band; the `Pitched, N mm loft insulation` **RoofType** family. **At-rafters**`roof_insulation_location = 1`, insulation on the sloping underside of the rafters over a loft; a *shallower* cavity, so Table 16 **col (2)** (200 mm → 0.29 vs the joists' 0.21) and the Table 18 rafters column; the `Pitched, insulated at rafters` member (gov-API/Summary `roof_construction` still pitched, not code 8). **Sloping ceiling** — gov-API `roof_construction = 8` (`roof_insulation_location = 7`), the ceiling *follows the slope* so there is **no loft void**: by depth it takes Table 17 **col (1a)** "insulated slope" (byte-identical to col (1) — 150 mm → 0.30 for both, Elmhurst-confirmed), and **as built / unknown** it takes Table 18 **col (3)** (`_FLAT_ROOF_BY_AGE`: 2.30 AD, 1.50 E, 0.68 F — *not* the loft col (1) 0.40 floor); the `Pitched, sloping ceiling, …` **RoofType** family (ADR-0066). The calculator keys the sloping path on the substring `"sloping ceiling"` (or `"vaulted"`) in `roof_construction_type` (`is_pitched_sloping_ceiling`), and both the SAP10 U-cascade and `recommend_roof_insulation` read that one string — so an overlay that means "sloping ceiling" **must** set the construction string, not just a thickness. The insulation Measure differs too: loft/at-rafters → loft top-up (joists, 300 mm) or `sloping_ceiling_insulation` respectively; a sloping ceiling → `sloping_ceiling_insulation` (rafters, 100 mm).
_Avoid_: "pitched with sloping ceiling is a room-in-roof" (it is a slope-following ceiling, `roof_construction` 8 — a habitable **Roof room(s)** is a separate construction); collapsing a sloping ceiling onto the `Pitched, N mm loft insulation` family (drops the slope shape → col (1) 0.40 floor at old bands + a loft-top-up recommendation); conflating **at-rafters** (col 2) with **sloping ceiling** (col 3 / col 1a); setting `roof_insulation_thickness` without `roof_construction_type` when the geometry is not a plain loft
### Modelling
**Effective EPC**:

View file

@ -0,0 +1,139 @@
# Pitched Sloping-Ceiling Gets Its Own RoofType Family, Modelled at the Slope Line (distinct from loft and from at-rafters)
A `PitchedWithSlopingCeiling` source roof (gov-API `roof_construction` code 8 —
its ceiling follows the slope, so it has no loft void) was being classified by
the **Landlord-Description Classification** onto the `Pitched, N mm loft
insulation` family. That family's Simulation Overlay sets only
`roof_insulation_thickness` and leaves `roof_construction_type` untouched, so
the **Effective EPC**'s roof reads as a horizontal loft. Two consequences, both
wrong for a slope-following ceiling:
- **Scoring** — with no `"sloping ceiling"` in `roof_construction_type`, the
SAP10 Calculation's `is_pitched_sloping_ceiling` flag stays False, so an
as-built / unknown roof takes the Table 18 **column (1)** age-band default
(`_ROOF_BY_AGE`, floored at 0.40 for bands AG) instead of the **column (3)**
default (`_FLAT_ROOF_BY_AGE`: 2.30 AD, 1.50 E, 0.68 F) that a slope-following
ceiling requires — a 35.75× understatement of roof heat loss at old bands.
It also loses the RdSAP 10 §3.8 inclined-area correction (÷cos 30°).
- **Recommendation**`recommend_roof_insulation` dispatches on the same
`roof_construction_type` substring, so a roof mis-labelled as loft is offered
a loft top-up (joists, 300 mm) rather than `sloping_ceiling_insulation`
(rafters, 100 mm) — a measure that can't physically be installed the way the
overlay implies.
The app-side enum + Drizzle migration (assessment-model#449, migration 0278)
already ships the canonical values; this repo is the lock-step source of truth
(ADR-0002) and must add them byte-for-byte.
## Decision
**Add a dedicated `Pitched, sloping ceiling, …` family to `RoofType`** — an
`as built` member plus a `12 mm … 400+ mm insulation` depth ladder (17 members
total), matching assessment-model#449 byte-for-byte. **Extends ADR-0032/0033;
the calculator changes in none** — it already models sloping ceilings; the fix
is to make the classifier and overlay *reach* that path.
**1. The overlay sets the sloping-ceiling construction string; the calculator
does the rest.** `roof_overlay_for` resolves the new family to a
`BuildingPartOverlay(roof_construction_type="Pitched, sloping ceiling", …)`,
which folds onto the Effective EPC's building part and drives both the scorer
and the recommendation generator off one authoritative string:
| Member | Overlay | SAP10 U-value resolution |
|---|---|---|
| `Pitched, sloping ceiling, as built` | construction `"Pitched, sloping ceiling"`, thickness `None` | Table 18 **col (3)** age-band default via `is_pitched_sloping_ceiling` (`_FLAT_ROOF_BY_AGE`) |
| `Pitched, sloping ceiling, N mm insulation` | construction `"Pitched, sloping ceiling"` + `roof_insulation_thickness = N` | Table 16 col (1) / Table 17 **col (1a)** "insulated slope" thickness ladder (`_ROOF_BY_THICKNESS`); `400+` → 400 |
**The mm ladder scoring on col (1a) is correct, not a loft fallback.** RdSAP 10
Table 17 col (1a) ("insulated slope sloping ceiling, mineral wool/EPS") is
byte-identical to the Table 16 col (1) loft ladder — 100 mm → 0.40, 150 mm →
0.30, 200 mm → 0.21 — confirmed against accredited Elmhurst (age B + 150 mm reads
0.30 for both geometries). The slope-vs-loft distinction bites **only at the
age-band default** (unknown thickness), where the family diverges correctly to
col (3). Setting the explicit construction string guarantees the resolution is a
*deliberate* col-(1a) match, never an *accidental* loft-ladder fallthrough. This
reproduces exactly how a lodged code-8 cert already scores
(`_api_resolve_sloping_ceiling_thickness`), so a landlord-override sloping
ceiling scores identically to a lodged one of the same geometry and depth.
**2. `as built` is sufficient at the no-depth end — no separate `Unknown` /
`no insulation` member.** `Unknown` depth folds to `as built` (mirrors #449).
This is both forced (a distinct member breaks byte-for-byte lock-step) and
correct: `as built` defers to the construction age band, which is itself
uninsulated (2.30) at pre-1950 bands AD, 1.50 at E, 0.68 at F — so "as built"
already collapses to "uninsulated" precisely where an old slope-following
ceiling is. Mirrors the `…, as built` wall proposal (issue #1676 B7): the age
band drives the insulation assumption rather than a hard-coded label.
**3. `Pitched, insulated at rafters` is kept distinct — this family does not
subsume it.** RdSAP separates the two by `roof_insulation_location`: **loc 1 =
insulation at rafters** (Table 16 **col (2)**: 200 mm → 0.29) versus **loc 7 =
sloping ceiling** (col (3) / Table 17 col (1a): 200 mm → 0.21). Different
constructions, different U-tables — folding them together would mis-score one to
the other. (That `Pitched, insulated at rafters` currently produces no overlay
is a pre-existing gap, out of scope here.)
**4. A deterministic classifier guard, not the LLM alone, routes the family.**
`PitchedWithSlopingCeiling: <As Built | Unknown | N mm>` resolves via a
`roof_sloping_ceiling_guard` (mirroring the #1376 `roof_party_ceiling_guard`),
composed into the roof `GuardedColumnClassifier` ahead of the
`ChatGptColumnClassifier` fallback. `As Built` / `Unknown` / empty → `as built`;
`N mm` → the ladder member (nearest tabulated depth ≤ N; ≥ 400 → `400+`). This
is the same defect class the party-ceiling guard was created to kill (#1376):
the LLM resolved *identical* roof inputs inconsistently, and adding 17
near-duplicate `sloping ceiling` members beside the loft ladder is exactly the
ambiguity that produces that split. The guard makes the knowable, costly-if-wrong
mapping deterministic and unbilled; its output still lands in the
`source=classifier` reviewed cache, so the human keeps the final say.
## Considered options
- **Route the mm ladder to the Table 16 col (2) rafters ladder** (200 mm →
0.29, the steeper "insulation at the slope" reading). Rejected: it requires a
calculator change (the `use_rafters` gate deliberately excludes sloping
ceilings) and a new overlay field, and it would make a landlord-override
sloping ceiling score *differently* from an identical lodged code-8 cert
(which routes to col (1a) today) — an override-vs-lodged divergence for zero
accuracy gain, since Elmhurst confirms col (1a) is the accredited value.
- **Fold `PitchedWithSlopingCeiling` into `Pitched, insulated at rafters` and
give that member a depth ladder.** Rejected: conflates RdSAP loc 7 with loc 1
(col (3)/(1a) with col (2)), and #449 already shipped a *separate* named
family — reusing the rafters member would break lock-step.
- **Add a distinct `Unknown` / `no insulation` sloping-ceiling member.**
Rejected: breaks byte-for-byte parity with #449, and the age band already
resolves the no-depth end to the correct (uninsulated-at-old-bands) U.
- **Add the enum members and rely on the LLM classifier alone** (no guard).
Rejected: does not satisfy the issue's explicit "stop routing these onto
`Pitched, N mm loft insulation`" — the near-duplicate loft members are the
exact ambiguity #1376 fixed with a guard; a probabilistic route across 294
costly as-built/Unknown rows is not equivalent to a deterministic one.
- **Guard + roof-classifier `extra_instructions` + a loft-direction eval.**
Rejected as over-scoped: the loft-direction risk (LLM over-picking a sloping
category for a loft description) is a review-surface concern, and CONTEXT.md
already frames `source=classifier` as a human-reviewed cache — the backstop
exists. Revisit if review flags real loft→sloping drift.
## Consequences
- The 311 portfolio-796 rows (`As Built` 202, `Unknown` 92, `150 mm` 16,
`100 mm` 1) currently on `Pitched, … loft insulation` are backfilled onto the
new family **after** this lands and #449 is live (same dry-run → commit flow
as the earlier 796 corrections). The 294 as-built/Unknown rows change U-value
(col (1) → col (3) at old bands); the 17 mm-ladder rows keep their U (col (1a)
≡ col (1)) but gain the correct sloping-ceiling shape (recommendation +
inclined area).
- The roof override becomes **authoritative for both scoring and
recommendation**: an EPC-lodged loft overridden to sloping now dispatches
`sloping_ceiling_insulation`, not a loft top-up.
- A `Pitched, sloping ceiling, 12 mm insulation` override yields **no** roof
Recommendation (the ADR-0021 sloping branch only fires when uninsulated —
as-built AD or explicit 0). This is unchanged ADR-0021 behaviour, accepted
consciously; whether thin-but-present slopes warrant a top-up is the same
open product question ADR-0047 left for the loft branch.
- **Lock-step obligation (ADR-0002):** the 17 member values are the contract
with assessment-model#449's `RoofTypeValues` / migration 0278 — any edit here
must be mirrored there and vice versa.
- The onboarder parity path (`EpcRoofDescriptions`,
`backend/onboarders/mappings/parity/`) already segregates sloping ceilings via
`map_sloping_ceiling_roof` (age band) and is **untouched** — this ADR governs
the Landlord-Override `RoofType` path only.