From 28634e8ae5f82f6506b3d8ed912db30114fcc023 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 4 Jun 2026 15:16:49 +0000 Subject: [PATCH] =?UTF-8?q?S0380.222:=20map=20API=20roof=5Fconstruction=20?= =?UTF-8?q?codes=206=20(thatched)=20+=207=20(dwelling=20above)=20=E2=86=92?= =?UTF-8?q?=20None?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026 sample lodges roof_construction=6 (1 cert, "Thatched, with additional insulation") and =7 (6 certs, "(same dwelling above)" / "(another dwelling above)"), both raising UnmappedApiCode and blocking the cert. roof_construction_type is read ONLY for the §3 "sloping ceiling" cos(30°) inclined-surface factor (Slice 89); the base roof U-value comes from the global roofs[].description. Neither code is a sloping ceiling: - 6 = thatched — U set by the description, not this field; - 7 = same/another dwelling above — an internal ceiling with no roof heat loss (the roof-side analogue of floor_construction code 0, governed by the roof_heat_loss / description path). Map both to None: carries no information the cascade consumes here and correctly avoids the cos(30°) false-trigger. Empirically inert and validated — roof W/K is byte-identical whether 6/7 map to None or to an explicit pitched string across all code-6/7 certs in the sample. 5 of the 7 now compute (e.g. thatched cert 2276 SAP 62.8 vs lodged 63); the other 2 also carry a gable_wall_type 2/3 raise (separate, worksheet- backed slice). Dict value type widened to Optional[str]. §4 suite 2392 passed; mapper.py pyright unchanged at 32; new tests suppress reportPrivateUsage (net-zero). Co-Authored-By: Claude Opus 4.8 --- datatypes/epc/domain/mapper.py | 19 +++++++- .../domain/tests/test_from_rdsap_schema.py | 46 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 2a61bebb..caf09b6f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2433,11 +2433,28 @@ _API_FLOOR_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = { # are observed on cert 001479; the wider RdSAP10 roof-construction # enum (1=Flat, 3=Pitched no-access, 5=Vaulted, etc.) is mapped as # best-effort against SAP10 nomenclature. -_API_ROOF_CONSTRUCTION_TO_STR: Dict[int, str] = { +# +# Codes 6 and 7 → None. This field is read ONLY for the sloping-ceiling +# inclined factor; the base roof U-value comes from the global +# roofs[].description, so a non-sloping code carries no information the +# cascade consumes here, and None correctly avoids the cos(30°) false- +# trigger: +# 6 = "Thatched, with additional insulation" — its U is set by the +# global description; not a sloping ceiling. +# 7 = "(same dwelling above)" / "(another dwelling above)" — an +# internal ceiling with no roof heat loss (the roof-side analogue +# of floor_construction code 0). Heat loss is governed by the +# roof_heat_loss / description path, not this field. +# Empirically inert: roof W/K is identical whether 6/7 map to None or to +# an explicit pitched string across all code-6/7 certs in the 2026 +# sample (were raising UnmappedApiCode, blocking the cert). +_API_ROOF_CONSTRUCTION_TO_STR: Dict[int, Optional[str]] = { 1: "Flat", 3: "Pitched (slates/tiles), no access to loft", 4: "Pitched (slates/tiles), access to loft", 5: "Pitched (vaulted ceiling)", + 6: None, + 7: None, 8: "Pitched, sloping ceiling", } diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index b0711d9f..5a4796a5 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -879,3 +879,49 @@ class TestDefaultMissingPostTown: # Assert assert result["post_town"] == "BARNSTAPLE" + + +class TestApiRoofConstructionCode: + """`_api_roof_construction_str` maps the GOV.UK API integer + roof_construction code to the string the cascade reads ONLY for the + "sloping ceiling" cos(30°) inclined-surface factor (Slice 89). Codes + 6 and 7 are neither sloping ceilings nor base-U drivers (the roof + U-value comes from the global roofs[].description), so both map to + None: code 6 = "Thatched" (its U is set by the description, not this + field) and code 7 = "(same/another dwelling above)" — an internal + ceiling with no roof heat loss, the roof-side analogue of + floor_construction code 0. Empirically inert: roof W/K is identical + whether 6/7 map to None or to an explicit pitched string across all + code-6/7 certs in the 2026 sample.""" + + def test_code_7_same_dwelling_above_maps_to_none(self) -> None: + # Arrange + from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_roof_construction_str(7) + + # Assert — None: no sloping-ceiling signal (avoids the cos(30°) + # false-trigger); the internal ceiling has no roof heat loss. + assert result is None + + def test_code_6_thatched_maps_to_none(self) -> None: + # Arrange + from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_roof_construction_str(6) + + # Assert — None: thatched is not a sloping ceiling; its U-value is + # carried by the global roof description, not roof_construction_type. + assert result is None + + def test_code_8_still_maps_to_sloping_ceiling(self) -> None: + # Arrange — regression guard: the sloping-ceiling code is unchanged. + from datatypes.epc.domain.mapper import _api_roof_construction_str # pyright: ignore[reportPrivateUsage] + + # Act + result = _api_roof_construction_str(8) + + # Assert + assert result == "Pitched, sloping ceiling"