diff --git a/domain/modelling/generators/roof_recommendation.py b/domain/modelling/generators/roof_recommendation.py index 9a3fff36..0d4f356d 100644 --- a/domain/modelling/generators/roof_recommendation.py +++ b/domain/modelling/generators/roof_recommendation.py @@ -49,6 +49,16 @@ def recommend_roof_insulation( for part in epc.sap_building_parts if part.identifier is BuildingPartIdentifier.MAIN ) + + # Room-in-roof safety guard (ADR-0021): a room-in-roof carries its + # insulation on its own sloping/stud/gable surfaces (RdSAP 10 §3.10, Table + # 17/18), which the loft/sloping overlay's flat `roof_insulation_thickness` + # bump cannot model. Without this guard a RR with an uninsulated loft would + # fall through to the loft fallback and mis-recommend loft insulation. + # Defer until a dedicated RR branch lands. + if main.sap_room_in_roof is not None: + return None + roof_type: str = (main.roof_construction_type or "").lower() # Dispatch by roof type (ADR-0021). Order matters: a sloping ceiling is diff --git a/tests/domain/modelling/test_roof_recommendation.py b/tests/domain/modelling/test_roof_recommendation.py index fa27d03b..63f28ce6 100644 --- a/tests/domain/modelling/test_roof_recommendation.py +++ b/tests/domain/modelling/test_roof_recommendation.py @@ -7,6 +7,7 @@ from datatypes.epc.domain.epc_property_data import ( BuildingPartIdentifier, EpcPropertyData, SapBuildingPart, + SapRoomInRoof, ) from domain.modelling.scoring.overlay_applicator import apply_simulations from domain.modelling.product import Product @@ -63,6 +64,25 @@ def test_already_insulated_loft_yields_no_recommendation() -> None: assert recommendation is None +def test_room_in_roof_yields_no_recommendation_pending_a_dedicated_branch() -> None: + # Arrange — an uninsulated loft the fallback would otherwise top up, but the + # part is a room-in-roof. The simple loft/sloping overlay can't model RR + # insulation (its sloping/stud/gable surfaces carry their own U-values via + # Table 17/18), so the generator must defer rather than mis-fire loft. + baseline: EpcPropertyData = build_epc() + main: SapBuildingPart = _part(baseline, BuildingPartIdentifier.MAIN) + main.roof_insulation_thickness = 0 + main.sap_room_in_roof = SapRoomInRoof(floor_area=9.0, construction_age_band="D") + + # Act + recommendation: Recommendation | None = recommend_roof_insulation( + baseline, _StubProducts() + ) + + # Assert + assert recommendation is None + + def test_loft_option_carries_cost_from_roof_area_and_product() -> None: # Arrange baseline: EpcPropertyData = build_epc() # MAIN roof area 14.85 m^2