mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Record ADR-0047 roof sentinel eligibility + lodgement-sentinel glossary entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c1169b2d39
commit
9e1a194b27
2 changed files with 113 additions and 0 deletions
|
|
@ -105,6 +105,10 @@ _Avoid_: patches (deprecated), corrections, manual EPC, edits
|
|||
Resolving a **Landlord Description** (unbounded free-text a landlord supplies for one component — "CWI" / "Cav filled" / "cavity insulated" all name one thing) onto a **Recognised Internal Description** via an LLM classifier, persisted in the `landlord_*_overrides` table (`source=classifier`) as a reviewed cache. Four vocabularies are kept **distinct** and must not be conflated: a **Landlord Description** (unbounded input); a **Recognised Internal Description** (the closed target taxonomy — e.g. a `MainHeatingSystemType` archetype — each binding to a Simulation Overlay); a **Lodged Description** (the gov-EPC `main_heating[].description` rendering, e.g. "Room heaters, electric" — only an example of which system *types* occur, never a map key); and the **SAP main heating code** (Table 4a/4b, what the calculator consumes). The classifier maps Landlord → Recognised Internal → SAP code. When it cannot confidently place the text it emits **`None`** (no overlay → the lodged EPC stands, surfaced to the user as "no suitable match"), **never the nearest wrong archetype** — the target taxonomy must be complete enough that a real system always has a correct home, so the classifier never overflows into a garbage-drawer archetype (ADR-0041).
|
||||
_Avoid_: "the LLM mapper is unreliable" (the failure mode is a too-small target taxonomy, not LLM language ability); conflating the landlord input vocabulary with the gov-EPC lodged rendering or the RdSAP entry-tool catalogue; treating a deterministic dict as a *replacement* for the LLM rather than a reviewed cache of its output
|
||||
|
||||
**Lodgement Sentinels (ND / NI / AB)**:
|
||||
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: A–D uninsulated, E–F 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)
|
||||
|
||||
### Modelling
|
||||
|
||||
**Effective EPC**:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
# Roof Eligibility Resolves Sentinel Thickness by Age Band (vaulted joins sloping; ND ≠ NI)
|
||||
|
||||
Property 742265 (uprn 10033526327, cert 2780-3922-3202-6042-9200) lodges a
|
||||
`Pitched (vaulted ceiling)` roof (gov-API `roof_construction` 5) with
|
||||
`roof_insulation_thickness = "ND"` and got **no roof Recommendation**, leaving
|
||||
its plan short of the scenario's goal band on an unlimited budget. Two
|
||||
generator gaps compound (ADR-0021's dispatch table has no vaulted row, and its
|
||||
uninsulated trigger `!= 0` compares the `"ND"` *string* against int 0, so it
|
||||
never fires), while the SAP10 Calculation already handles both: it treats
|
||||
vaulted as a slope-following ceiling and resolves the `"ND"` sentinel via the
|
||||
roof description / age-band defaults (`heat_transmission.py` — the
|
||||
`is_sloping_ceiling` disjunction and the raw-sentinel inspection). The
|
||||
generator diverged from the calculator — the exact split ADR-0021 set out to
|
||||
prevent.
|
||||
|
||||
## Decision
|
||||
|
||||
**Amends ADR-0021.** The roof Recommendation Generator changes in two ways;
|
||||
the mapper and the calculator change in none.
|
||||
|
||||
**1. Vaulted is a sloping ceiling.** The dispatch matches the same substring
|
||||
disjunction the calculator uses (`"sloping ceiling" in roof_type or "vaulted"
|
||||
in roof_type`): a vaulted ceiling has no loft void, so its one applicable
|
||||
Measure is `sloping_ceiling_insulation` (rafters, 100 mm) — never the loft
|
||||
fallback. ADR-0021's dispatch table gains the row:
|
||||
|
||||
| Roof type (`roof_construction_type` substring) | Measure | Uninsulated trigger | Overlay |
|
||||
|---|---|---|---|
|
||||
| `sloping ceiling` **or `vaulted`** | `sloping_ceiling_insulation` | resolved thickness 0 (below) | → 100 mm |
|
||||
|
||||
**2. Sentinel-aware uninsulated trigger, resolved by age band — codes, not
|
||||
descriptions.** `roof_insulation_thickness` is `Optional[Union[str, int]]`; a
|
||||
lodged string sentinel is data, not noise. The generator resolves eligibility
|
||||
for the two pitched branches (sloping/vaulted and loft) as:
|
||||
|
||||
| Lodged value | Meaning (RdSAP 10) | Eligibility |
|
||||
|---|---|---|
|
||||
| int `0` | explicitly uninsulated | eligible (unchanged) |
|
||||
| int `> 0` | measured thickness | not eligible (unchanged) |
|
||||
| `"NI"` | insulation **present**, thickness unknown (§5.11.4: assume 50 mm) | **not eligible** |
|
||||
| `"ND"` / `None` | not defined — resolve to the age band's **as-built** state | eligible iff as-built uninsulated (bands **A–D**) |
|
||||
|
||||
Age-band as-built states (construction age code): **A–D uninsulated · E–F
|
||||
limited insulation · G+ insulated.** Only A–D resolves to eligible. This
|
||||
extends the codebase's existing precedent (`_PRE_1950_AGE_CODES = {A,B,C,D}`
|
||||
in the mapper's code-8 rule; pinned by cert 001479 Ext2 at U=2.30) from the
|
||||
scoring path to eligibility.
|
||||
|
||||
**Coherence rule (load-bearing):** sentinel resolution must never make an
|
||||
undefined cert *more* eligible than an explicitly-lodged one. An explicit
|
||||
50 mm lodgement is not eligible today, so `"ND"` on a band-E/F ("limited
|
||||
insulation") dwelling must also resolve to not-eligible — recommending a
|
||||
top-up to the dwelling whose insulation we *don't* know while withholding it
|
||||
from the identical dwelling whose thin insulation we *do* know would be
|
||||
incoherent. Whether limited-insulation **top-up** should be a Measure at all
|
||||
is a separate product question that applies equally to explicit thin
|
||||
lodgements; it is out of scope here (see Consequences).
|
||||
|
||||
**The mapper stays untouched — the `"ND"` passthrough is load-bearing.** The
|
||||
calculator inspects the *raw* lodgement to distinguish explicit `int(0)`
|
||||
(drop the description; uninsulated) from `"NI"` (keep the description; §5.11.4
|
||||
retrofit-50 mm) from `"ND"` (defer to description / age default). Normalising
|
||||
sentinels at the mapper boundary would change scoring inputs for every ND-roof
|
||||
cert in the corpus for zero eligibility benefit.
|
||||
|
||||
**Two homes for sentinel resolution, deliberately.** Scoring asks "what
|
||||
thickness should the U-cascade see" (`NI` → 50 mm); eligibility asks "is this
|
||||
roof a candidate" (`NI` → no). Same sentinel, different questions, different
|
||||
answers — a single shared resolver would just relocate the split into its call
|
||||
sites. Each home carries a regression test pinning the *disagreement* as
|
||||
intentional. Consolidate only if a third consumer appears (rule of three).
|
||||
|
||||
## Considered options
|
||||
|
||||
- **Normalise `ND` → 0 in the mapper** (extend the code-8 rule to code 5 and
|
||||
to the `"ND"` string). Rejected: changes calculator inputs corpus-wide —
|
||||
`roof_thickness_explicitly_zero` would fire and drop the roof description
|
||||
the §5.11 resolution depends on — risking the pinned Elmhurst accuracy
|
||||
corpus to fix an eligibility bug.
|
||||
- **Defer to `roofs[].description`** ("no insulation (assumed)") for ND
|
||||
resolution, mirroring the calculator exactly. Rejected: eligibility should
|
||||
key on codes, not free-text description parsing; descriptions drift by
|
||||
lodgement software and the age-band code carries the same as-built signal
|
||||
deterministically.
|
||||
- **Gate eligibility on the calculator's effective roof U-value.** Rejected:
|
||||
no other generator gates on scored output (walls/floors gate on lodged
|
||||
attributes), and it re-couples eligibility to scoring against ADR-0016's
|
||||
separation.
|
||||
- **Treat `"ND"` alone as uninsulated** (sentinel-literal). Rejected: ND means
|
||||
*not defined*, not *none* — a band-G ND roof is almost certainly insulated
|
||||
as built and would get a junk Recommendation.
|
||||
|
||||
## Consequences
|
||||
|
||||
- 742265 (band B, vaulted, ND) becomes eligible: `sloping_ceiling_insulation`
|
||||
over 55.82 m² (~£2.2k, verified by local re-model). Any pitched-loft cert
|
||||
lodging `"ND"` with age band A–D is likewise un-withheld.
|
||||
- The **flat branch is deliberately untouched**: its inverted convention
|
||||
(`None` = uninsulated, per the Elmhurst mapping) means an `"ND"` flat roof
|
||||
is *not eligible* today and stays so — a separately-verifiable follow-up,
|
||||
not silently folded into this change.
|
||||
- **Open follow-up (product):** should limited-insulation dwellings (explicit
|
||||
thin lodgements *and* ND + bands E–F alike) be offered an insulation
|
||||
top-up? Today neither is; changing that is a trigger redesign, not a
|
||||
sentinel question.
|
||||
- ADR-0021's "one dispatching generator, one Measure per roof" and its
|
||||
dispatch order (sloping → flat → no-access → loft) are unchanged; the
|
||||
sloping test simply matches one more substring, ahead of the fallbacks as
|
||||
before.
|
||||
Loading…
Add table
Reference in a new issue