fix(elmhurst-mapper): carry §7 alternative-wall "Sheltered Wall" flag

The Elmhurst Summary §7 lodges "Alternative Wall N Sheltered Wall: Yes" for
a sub-area adjacent to an unheated buffer (e.g. a flat's corridor wall),
but the extractor dropped it and _map_elmhurst_alternative_wall never set
SapAlternativeWall.is_sheltered — so the cascade billed the sub-area at its
full exposed U instead of the RdSAP 10 Table 4 (p.22) sheltered U =
1/(1/U + 0.5).

The calculator already applies is_sheltered (_alt_wall_w_per_k) and the
gov-API path already wires sheltered_wall=="Y"; this brings the Elmhurst
front-end to parity. Three-part change: AlternativeWall.sheltered field +
_alternative_walls_from_lines parse ("Alternative Wall N Sheltered Wall") +
_map_elmhurst_alternative_wall is_sheltered=a.sheltered.

Surfaced by simulated case 34 (cert 001431 electric-storage flat): the
6.02 m² corridor wall billed at full U=1.50 (9.03 W/K) instead of the
sheltered 0.86 (5.18 W/K) — +3.85 W/K, -1.61 SAP. Post-fix the alt wall
matches the worksheet's (29a) 5.177 and case 34 closes from -1.61 to -0.30
(remaining residual is a separate window/wall area-allocation thread).

Elmhurst-mapper only: API SAP gauge unchanged (57.6% within 0.5); worksheet
harness 47/47 unaffected; regression gate green (3 pre-existing fails
unrelated); pyright net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-11 07:35:46 +00:00
parent f3dcd7b43e
commit 48b36d3d7e
4 changed files with 47 additions and 0 deletions

View file

@ -340,6 +340,12 @@ class ElmhurstSiteNotesExtractor:
dry_lined=self._local_bool(
lines, f"Alternative Wall {n} Dry-lining"
),
# RdSAP 10 Table 4 (p.22): a sheltered alt sub-area
# (adjacent to an unheated buffer, e.g. a flat corridor
# wall) adds R=0.5 m²K/W → U = 1/(1/U + 0.5).
sheltered=self._local_bool(
lines, f"Alternative Wall {n} Sheltered Wall"
),
))
return result

View file

@ -45,8 +45,10 @@ from datatypes.epc.domain.mapper import (
_elmhurst_dwelling_type, # pyright: ignore[reportPrivateUsage]
_elmhurst_glazing_type_code, # pyright: ignore[reportPrivateUsage]
_elmhurst_immersion_type_code, # pyright: ignore[reportPrivateUsage]
_map_elmhurst_alternative_wall, # pyright: ignore[reportPrivateUsage]
)
from datatypes.epc.surveys.elmhurst_site_notes import (
AlternativeWall as ElmhurstAlternativeWall,
FloorDetails as ElmhurstFloorDetails,
RoofDetails as ElmhurstRoofDetails,
)
@ -1545,6 +1547,35 @@ def test_summary_mapper_raises_on_unmapped_cylinder_insulation_label() -> None:
assert excinfo.value.value == "Polyester wool"
def test_map_elmhurst_alternative_wall_carries_sheltered_flag() -> None:
# Arrange — Elmhurst Summary §7 lodges "Alternative Wall N Sheltered
# Wall: Yes" for a sub-area adjacent to an unheated buffer (e.g. a flat's
# corridor wall). RdSAP 10 Table 4 (p.22) gives a sheltered wall an added
# R=0.5 m²K/W → U=1/(1/U+0.5). The cascade applies this via
# SapAlternativeWall.is_sheltered (the API path already wires it); the
# Elmhurst path must surface it too. Surfaced by simulated case 34 (cert
# 001431 flat: 6.02 m² corridor wall billed at full U=1.50 instead of
# the sheltered 0.86 → +3.85 W/K, -1.61 SAP).
sheltered = ElmhurstAlternativeWall(
area_m2=12.5, wall_type="CA Cavity", insulation="A As Built",
thickness_unknown=False, thickness_mm=250, u_value_known=False,
dry_lined=False, sheltered=True,
)
plain = ElmhurstAlternativeWall(
area_m2=12.5, wall_type="CA Cavity", insulation="A As Built",
thickness_unknown=False, thickness_mm=250, u_value_known=False,
dry_lined=False, sheltered=False,
)
# Act
mapped_sheltered = _map_elmhurst_alternative_wall(sheltered)
mapped_plain = _map_elmhurst_alternative_wall(plain)
# Assert
assert mapped_sheltered.is_sheltered is True
assert mapped_plain.is_sheltered is False
def test_elmhurst_dwelling_type_single_storey_flat_with_exposed_roof_is_top_floor() -> None:
# Arrange — a single-storey flat exposed BOTH top (external pitched
# roof, access to loft) AND bottom (floor over partially-heated space,

View file

@ -3864,6 +3864,11 @@ def _map_elmhurst_alternative_wall(
wall_insulation_thickness=None,
wall_thickness_mm=measured_thickness_mm,
is_basement=_elmhurst_wall_is_basement(a.wall_type),
# Summary §7 "Alternative Wall N Sheltered Wall: Yes" → RdSAP 10
# Table 4 (p.22) sheltered U = 1/(1/U + 0.5), applied by the
# cascade's `_alt_wall_w_per_k`. Mirror of the API path's
# `sheltered_wall == "Y"` wiring.
is_sheltered=a.sheltered,
)

View file

@ -71,6 +71,11 @@ class AlternativeWall:
thickness_mm: Optional[int]
u_value_known: bool
dry_lined: bool = False
# Summary §7 "Alternative Wall N Sheltered Wall: Yes/No". RdSAP 10
# Table 4 (p.22): a sheltered sub-area (adjacent to an unheated buffer
# such as a flat's corridor) carries an added R=0.5 m²K/W → U =
# 1/(1/U + 0.5). Drives SapAlternativeWall.is_sheltered.
sheltered: bool = False
@dataclass