From 79bf28d8cb3319c99ab3b577865bbe971b5e25da Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 28 Jul 2026 10:33:06 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Offer=20EWI/IWI=20on=20a=20gov-API=20system?= =?UTF-8?q?-built=20wall=20(code=208)=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gov-EPC API lodges a system-built wall as `wall_construction == 8` per the authoritative GOV.UK RdSAP `WallConstructionCode` enumeration (communitiesuk/epb-data-warehouse `.../SAP-Domains.xsd`: 8 = "system built" in every schema version 17.0-21.0.1; park home is 10). The solid- wall Recommendation Generator instead read code 8 as the Elmhurst park-home label and suppressed the whole cohort's EWI/IWI Recommendation (a 2026 DB sweep finds ~5.1k certs at code 8, ~2.1k uninsulated main walls). Rewrite the mislabelled park-home test to pin the correct behaviour (code 8 → EWI+IWI), and add a genuine park-home test on the gov code 10 so the fix doesn't start over-offering on real park homes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz --- .../modelling/test_wall_recommendation.py | 40 ++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/domain/modelling/test_wall_recommendation.py b/tests/domain/modelling/test_wall_recommendation.py index 4734314c9..577e88ca2 100644 --- a/tests/domain/modelling/test_wall_recommendation.py +++ b/tests/domain/modelling/test_wall_recommendation.py @@ -153,13 +153,43 @@ def test_treatable_old_band_solid_wall_keeps_its_insulation_options() -> None: } -def test_park_home_wall_yields_no_solid_wall_recommendation() -> None: - # Arrange — a park home (wall_construction code 8) with an uninsulated - # as-built wall. Code 8 is NOT system-built (ADR-0019); a park home's - # proprietary panel is never EWI/IWI-suitable, so the generator excludes it. +def test_gov_api_system_built_wall_keeps_its_insulation_options() -> None: + # Arrange — a SYSTEM-BUILT dwelling as ingested from the gov-EPC API, + # where `wall_construction == 8` means "system built" per the + # GOV.UK RdSAP `WallConstructionCode` enumeration (8 = system built in + # EVERY schema version 17.0-21.0.1). A precast/no-fines-concrete system- + # built wall IS solid-wall-insulation-suitable (EWI + IWI), exactly like + # `u_wall`'s `_GOV_API_WALL_CODE_TO_TYPE[8] = WALL_SYSTEM_BUILT` already + # resolves it for the U-value cascade. Age band B, uninsulated: the + # Wall U-Value Gate lets both Options through (0.35 << 2.0). + baseline: EpcPropertyData = build_epc() # age band B + main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN) + main.wall_construction = 8 # gov-API "system built" + main.wall_insulation_type = 4 # as-built / uninsulated — the trigger + + # Act + recommendation: Recommendation | None = recommend_solid_wall( + baseline, _StubProducts() + ) + + # Assert + assert recommendation is not None + assert {option.measure_type for option in recommendation.options} == { + "external_wall_insulation", + "internal_wall_insulation", + } + + +def test_gov_api_park_home_wall_yields_no_solid_wall_recommendation() -> None: + # Arrange — a genuine park home as ingested from the gov-EPC API, where + # `wall_construction == 10` means "park home wall" per the GOV.UK RdSAP + # `WallConstructionCode` enumeration (10 = park home in every schema + # version). A park home's proprietary panel is never EWI/IWI-suitable, + # so the generator must still exclude it — the fix for system-built + # (code 8) must not start over-offering on real park homes. baseline: EpcPropertyData = build_epc() main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN) - main.wall_construction = 8 + main.wall_construction = 10 # gov-API "park home wall" main.wall_insulation_type = 4 # as-built / uninsulated — the trigger # Act From d1f064433cbc37da052e291cec14e4678a403207 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 28 Jul 2026 10:33:16 +0000 Subject: [PATCH 2/3] =?UTF-8?q?Offer=20EWI/IWI=20on=20a=20gov-API=20system?= =?UTF-8?q?-built=20wall=20(code=208)=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Key the solid-wall Recommendation Generator's system-built branch on the gov-EPC API code 8 (`_WALL_SYSTEM_BUILT_GOV_API`) alongside the internal code 6, so a precast/no-fines-concrete system-built wall gets its EWI+IWI Options. This mirrors `u_wall`, which already resolves code 8 as system-built via `rdsap_uvalues._GOV_API_WALL_CODE_TO_TYPE[8] = WALL_SYSTEM_BUILT`. Repoint the explicit park-home exclusion to the gov-canonical park-home code 10. Supersedes ADR-0019's "do not key system-built on 8", which read code 8 as the Elmhurst park-home label. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz --- .../generators/solid_wall_recommendation.py | 40 ++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/domain/modelling/generators/solid_wall_recommendation.py b/domain/modelling/generators/solid_wall_recommendation.py index 32cee3437..b3fbf5051 100644 --- a/domain/modelling/generators/solid_wall_recommendation.py +++ b/domain/modelling/generators/solid_wall_recommendation.py @@ -36,16 +36,35 @@ _INTERNAL_MEASURE_TYPE: Final[MeasureType] = MeasureType.INTERNAL_WALL_INSULATIO # RdSAP `wall_construction` codes (consistent across paths for 1-5). _WALL_SOLID_BRICK: Final[int] = 3 _WALL_TIMBER_FRAME: Final[int] = 5 -# System-built (precast/no-fines concrete): `WALL_SYSTEM_BUILT` in -# rdsap_uvalues. NB this is the Elmhurst code (`SY`); the *basement-wall* signal -# also lodges as 6 today (`BASEMENT_WALL_CONSTRUCTION_CODE`), so system-built is -# disambiguated from basement by `main_wall_is_basement` below — a basement wall -# is never solid-wall-insulation-suitable regardless. +# System-built (precast/no-fines concrete). TWO codes reach this generator for +# the same physical wall, because the `wall_construction` code-space diverges by +# ingestion path: +# - Code 6 (`WALL_SYSTEM_BUILT`) — the Elmhurst `SY System build` label and the +# calculator's internal constant. Code 6 is *overloaded* with the basement- +# wall signal (`BASEMENT_WALL_CONSTRUCTION_CODE`), so system-built is +# disambiguated from basement by `main_wall_is_basement` below — a basement +# wall is never solid-wall-insulation-suitable regardless. +# - Code 8 — what the gov-EPC API actually lodges for a system-built wall. Per +# the authoritative GOV.UK RdSAP `WallConstructionCode` enumeration +# (communitiesuk/epb-data-warehouse `.../SAP-Domains.xsd`), 8 = "system built" +# in EVERY schema version 17.0 through 21.0.1. This is the canonical +# production signal (a 2026 DB sweep finds 5.1k certs at code 8, overwhelmingly +# "System built, ..." wall descriptions; 2.1k are uninsulated main walls). +# `u_wall` already resolves code 8 as system-built for the U-value cascade via +# `rdsap_uvalues._GOV_API_WALL_CODE_TO_TYPE[8] = WALL_SYSTEM_BUILT`; keying both +# codes here brings the recommendation generator into line with that convention. +# (Supersedes ADR-0019's "do not key system-built on 8", which read code 8 as the +# Elmhurst park-home label — see `_WALL_PARK_HOME` below.) _WALL_SYSTEM_BUILT: Final[int] = 6 -# Park home (`PH`, the Elmhurst code-8 wall) — NOT system-built (ADR-0019: "do -# not key system-built on 8"). A park home's wall is a proprietary panel system -# our EWI/IWI model doesn't represent, so it is never solid-wall-suitable. -_WALL_PARK_HOME: Final[int] = 8 +_WALL_SYSTEM_BUILT_GOV_API: Final[int] = 8 +# Park home — NOT system-built; its proprietary panel is a system our EWI/IWI +# model doesn't represent, so it is never solid-wall-suitable. The gov-EPC API +# lodges a park home as code 10 per the RdSAP `WallConstructionCode` enumeration +# (10 = "park home wall" in every schema version). It is excluded implicitly +# (code 10 is not a constructable key) — the earlier code-8 exclusion mistook the +# gov "system built" code for a park home and suppressed the whole system-built +# cohort's EWI/IWI Recommendation. +_WALL_PARK_HOME: Final[int] = 10 # `wall_insulation_type`: 4 = as-built / assumed (uninsulated) — the trigger. _WALL_AS_BUILT: Final[int] = 4 # `wall_insulation_type` the overlay lodges: 1 = external, 3 = internal. @@ -61,6 +80,7 @@ _SOLID_WALL_INSULATION_MM: Final[int] = 100 _CONSTRUCTABLE_OPTIONS: Final[dict[int, tuple[MeasureType, ...]]] = { _WALL_SOLID_BRICK: (_EXTERNAL_MEASURE_TYPE, _INTERNAL_MEASURE_TYPE), _WALL_SYSTEM_BUILT: (_EXTERNAL_MEASURE_TYPE, _INTERNAL_MEASURE_TYPE), + _WALL_SYSTEM_BUILT_GOV_API: (_EXTERNAL_MEASURE_TYPE, _INTERNAL_MEASURE_TYPE), _WALL_TIMBER_FRAME: (_INTERNAL_MEASURE_TYPE,), } @@ -149,7 +169,7 @@ def recommend_solid_wall( if not isinstance(construction, int): return None # a free-text site-notes construction is not a code we key on if construction == _WALL_PARK_HOME: - return None # park home (code 8) — proprietary panel, never EWI/IWI + return None # park home (gov code 10) — proprietary panel, never EWI/IWI measure_types = _CONSTRUCTABLE_OPTIONS.get(construction) if not measure_types: return None From aa4446757fd4e99e48ad580cfd678de73d9bbd90 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 28 Jul 2026 10:33:52 +0000 Subject: [PATCH 3/3] =?UTF-8?q?Amend=20ADR-0019:=20gov-API=20code=208=20is?= =?UTF-8?q?=20system=20built,=20not=20park=20home=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record why the generator now keys system-built on both code 6 and the gov-EPC API code 8, citing the GOV.UK RdSAP WallConstructionCode XSD, and flag the remaining mapper-level enum-normalisation work as open. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz --- docs/adr/0019-wall-insulation-eligibility.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/adr/0019-wall-insulation-eligibility.md b/docs/adr/0019-wall-insulation-eligibility.md index e77eebee5..a940de2f7 100644 --- a/docs/adr/0019-wall-insulation-eligibility.md +++ b/docs/adr/0019-wall-insulation-eligibility.md @@ -8,6 +8,8 @@ The solid-wall Recommendation Generator must decide, per Property, which wall-in **System-built** is keyed on `wall_construction == 6` (`WALL_SYSTEM_BUILT`; the Elmhurst `SY System build` label). This code is currently *overloaded*: `B Basement wall` also maps to 6 (`BASEMENT_WALL_CONSTRUCTION_CODE`, `mapper.py:2100`), so the generator additionally guards on `main_wall_is_basement` — a basement wall is never solid-wall-insulation-suitable and is excluded regardless of construction. Because `main_wall_is_basement` is presently derived as `wall_construction == 6`, *every* code-6 wall is treated as basement today, so the system-built branch is inert until the calculator disambiguates system-built from basement (target: MAIN `wall_construction == 6` with `main_wall_is_basement` False — tracked in Hestia-Homes/Model#1177). The strict-xfail pin `test_system_built_generator_offers_ewi_and_iwi_each_pinning_its_after` is the tripwire for that fix. Note `wall_construction == 8` is **Park home** (`PH`) on the Elmhurst path, *not* system-built — do not key system-built on 8. +> **Amendment (2026-07, `fix/wall-construction-enum-mapping`).** The "do not key system-built on 8" clause was wrong for the *gov-EPC API* path — the path that populates every production cert. Per the authoritative GOV.UK RdSAP `WallConstructionCode` enumeration (communitiesuk/epb-data-warehouse `.../RdSAP/UDT/SAP-Domains.xsd`), **8 = "system built"** in every schema version (17.0-21.0.1), and **10 = "park home wall"**; only the Elmhurst summary path reuses 8 for park home. Because the gov→internal translator (`mapper._api_wall_construction_code`) does *not* remap 8→6, a system-built wall reaches the generator as code 8 and was suppressed as a park home — withholding EWI/IWI from ~5.1k certs (~2.1k uninsulated main walls) in a 2026 DB sweep. The generator now keys system-built on **both** code 6 (Elmhurst/internal) and code 8 (gov API), matching `u_wall`'s existing `_GOV_API_WALL_CODE_TO_TYPE[8] = WALL_SYSTEM_BUILT`, and excludes park homes on the gov-canonical code 10. The deeper fix — normalising the whole gov enum into the internal `WALL_*` space at the mapper boundary (so `fabric_description` export, overlays, and the code-6 basement overload stop diverging) — remains open; it is higher-risk because gov 8→internal 6 collides with `BASEMENT_WALL_CONSTRUCTION_CODE` and the Model#1177 basement disambiguation. + | Construction | Cavity fill | IWI | EWI | |---|---|---|---| | Cavity | ✅ only | ❌ | ❌ |