feat(modelling): room-in-roof safety guard defers the roof generator

A room-in-roof carries its insulation on its own sloping/stud/gable surfaces
(RdSAP 10 §3.10, Table 17/18), which the roof overlay's flat
roof_insulation_thickness bump cannot model. Without a guard a RR with an
uninsulated loft fell through to the loft fallback and mis-recommended 300 mm
loft insulation. Return None when the main part lodges a sap_room_in_roof,
deferring until a dedicated RR branch lands (ADR-0021).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-05 10:06:08 +00:00
parent 8323d9cf07
commit f33bb9d52d
2 changed files with 30 additions and 0 deletions

View file

@ -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

View file

@ -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