diff --git a/CONTEXT.md b/CONTEXT.md index 2e8c5d00..53ae8f2a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -234,6 +234,14 @@ _Avoid_: selected measures, default measures, optimal solution, recommended bund The catalogue classification of a retrofit measure (e.g. `solar_pv`, `loft_insulation`, `ashp`); one or more Recommendations reference the same Measure Type with property-specific cost and impact. _Avoid_: measure (ambiguous), category +**External Wall Insulation (EWI)** / **Internal Wall Insulation (IWI)**: +The two competing **Measure Options** for insulating a *solid* (non-cavity) main wall — insulation fixed to the outside face (`wall_insulation_type = 1`) or the room side (`wall_insulation_type = 3`), both 100 mm at λ = 0.04 W/m·K; the calculator **derives** the post-insulation U-value from the type + thickness (the lodged cert carries no U-value). IWI additionally lowers the wall's thermal-mass parameter (changing heating demand); EWI does not. +_Avoid_: solid wall insulation (that names the pair, not one Option), cladding, drylining + +**Wall Insulation Eligibility**: +The rule fixing which wall Option(s) the main-wall **Recommendation** offers, by wall construction then planning status. By construction: **cavity** → cavity fill only (never solid insulation); **solid brick** / **system-built** → IWI + EWI; **timber-frame** → IWI only (EWI is not constructable); **cob** / **granite or whinstone** / **sandstone or limestone** → none (breathable fabric — standard insulation risks trapping moisture). Then, as planning gates: a **conservation area** or **flat** removes EWI (external-appearance / whole-block constraints), and a **listed** or **heritage** building removes **both** EWI and IWI (protected fabric). +_Avoid_: restricted measures (legacy collapsed conservation/listed/heritage into one boolean — they now gate different Options, so keep them distinct) + ### Valuation **Property Valuation**: @@ -346,3 +354,4 @@ _Avoid_: API key, auth token, secret - **"phase"** (sequencing measures into ordered steps within a Scenario/Plan) was a speculative, prospective-client feature and is **deferred — out of scope** (see ADR-0005). It is *not* a current domain term: a **Scenario** carries one set of measures, a **Plan** one **Optimised Package**. The only live use of "phase" is cut-over timeline language in the PRD ("Phase 0 — Status quo"), which is project-management vocabulary and does not enter code. - **"valuation"** was used for both a Property's current market value and the increase a retrofit produces — resolved into two distinct terms: **Property Valuation** (current value, a Baseline attribute) and **Valuation Uplift** (the plan-conditional, percentage-primary increase). The bare word "valuation" should be qualified to one of these. - **"stale"** appears in two senses: cache-freshness ("a Repo record is stale and the orchestrator should refetch") — a legitimate operational concept; and as loose shorthand for the EPC's recorded cost fields being unusable. The cost fields are not stale — they are pinned to the inspection-date fuel rates by design. Use "pinned to inspection date" or "pre-SAP10 schema" (whichever applies) instead. +- **"restricted_measures"** (legacy `backend/Property.py`) collapsed `in_conservation_area`, `is_listed_building`, and `is_heritage_building` into one boolean that blocked EWI only. Resolved: the rebuild keeps the three flags **distinct**, because they gate different Options — a **conservation area** blocks EWI but allows IWI, whereas **listed/heritage** block both (see **Wall Insulation Eligibility**). Don't reintroduce a single collapsed flag. diff --git a/docs/adr/0019-wall-insulation-eligibility.md b/docs/adr/0019-wall-insulation-eligibility.md new file mode 100644 index 00000000..b8448770 --- /dev/null +++ b/docs/adr/0019-wall-insulation-eligibility.md @@ -0,0 +1,31 @@ +# Wall Insulation Eligibility (cavity vs IWI vs EWI) + +The solid-wall Recommendation Generator must decide, per Property, which wall-insulation Option(s) to offer. We decided eligibility is fixed first by **wall construction**, then narrowed by **planning status**, and that **External (EWI)** and **Internal (IWI)** wall insulation are two competing **Measure Options** under one "Main wall" **Recommendation** (the Optimiser picks at most one), consistent with ADR-0016 and the "cavity-fill vs EWI" exclusivity already described in CONTEXT.md. + +## Decision + +**By construction (detected from the wall *description* string, not the numeric `wall_construction` code — see Consequences):** + +| Construction | Cavity fill | IWI | EWI | +|---|---|---|---| +| Cavity | ✅ only | ❌ | ❌ | +| Solid brick | — | ✅ | ✅ | +| System built | — | ✅ | ✅ | +| Timber frame | — | ✅ | ❌ (not constructable) | +| Cob / Granite-whinstone / Sandstone-limestone | — | ❌ | ❌ | + +**Planning gates (on top), using three *distinct* flags (see ADR-0020):** +- **Conservation area** or **Flat** → remove **EWI** (external-appearance / whole-block constraints); IWI still offered. +- **Listed** or **Heritage** → remove **both** EWI and IWI (protected fabric). + +A Recommendation is produced only when the wall is genuinely uninsulated (description contains "no insulation"), at a fixed **100 mm** insulation depth. + +## Considered options + +- **Mirror the legacy `is_suitable_for_solid_insulation` / `ewi_valid` rules verbatim.** Rejected in part: legacy collapsed all three planning flags into one `restricted_measures` boolean that blocked EWI only. We keep the flags distinct so listed/heritage can block IWI too — a deliberate strengthening. +- **Offer solid-wall insulation on cob/stone** (the calculator *can* model it — Elmhurst produces valid after-certs). Rejected: recommending standard EWI/IWI on breathable cob/rubble-stone fabric risks trapping moisture; we do not auto-suggest it. + +## Consequences + +- Cob and both stone types get **no** wall-insulation recommendation at all, even though the SAP calculator scores them fine — this is a deliberate building-pathology safeguard, not a gap. +- The conservation/listed/heritage gate depends on Property data not yet ingested (ADR-0020); until that lands the gate is an explicit generator input defaulting to "unrestricted", so the offline generator over-offers EWI in the interim. Not production-exposed. diff --git a/docs/adr/0020-conservation-status-as-property-attributes.md b/docs/adr/0020-conservation-status-as-property-attributes.md new file mode 100644 index 00000000..ba487026 --- /dev/null +++ b/docs/adr/0020-conservation-status-as-property-attributes.md @@ -0,0 +1,19 @@ +# Conservation / Listed / Heritage as distinct Property attributes + +Wall Insulation Eligibility (ADR-0019) — and later Solar-PV and Windows generators — gate on a Property's planning status. That status is **not** on the EPC and **not** in the OS Open-UPRN parquet the geospatial Repo reads today (it only yields coordinates); legacy derives it by spatial-joining separate conservation/listed/heritage layers (`OpenUprnClient.set_spatial_data` → `property_spatial` table). We decided to model it as **three distinct boolean Property attributes** — `in_conservation_area`, `is_listed`, `is_heritage` — resolved through the **geospatial layer** during Ingestion and read off the **Property** (not the EPC), because they describe the building's location/protection, not its energy fabric. + +## Decision + +- Keep the three flags **separate**, not legacy's collapsed `restricted_measures` boolean — they gate different Measure Options (conservation blocks EWI only; listed/heritage block both — ADR-0019). +- Surface them via the geospatial Repo (a `GeospatialRepository` method returning a planning-status record, alongside `coordinates_for`), persist onto the Property in Ingestion, and pass them into the generator as an explicit input. +- Build this as the **final integrating slice** of the solid-wall feature (build order A), after the calculator mechanics and generator are pinned — it also unlocks the PV/Windows gates. + +## Source (decided) + +The conservation/listed/heritage flags are **co-located with longitude/latitude in the same S3 partition** the geospatial Repo already reads — so we **extend the existing `GeospatialS3Repository`** to surface those extra columns alongside the coordinates, rather than porting a separate spatial-join or reading the legacy `property_spatial` table. A further deep-dive into the exact S3 columns/shape precedes slice 3. + +## Consequences + +- Generators that need planning status take it as an input or read it off the Property; it never lands on `EpcPropertyData`. +- Until this slice ships, the ADR-0019 gate defaults to "unrestricted" (offline only). +- Mirrors legacy's conservative stance where missing data implied restriction — the source slice should decide the "unknown" default explicitly (block EWI vs allow) rather than silently allowing.