Slice S0380.8: extension 'As Main Wall' inheritance copies insulation_thickness_mm

Regression fix surfaced by the first-attempt cert 0350 prediction
test. `_extract_extensions` in `backend/documents_parser/elmhurst_
extractor.py` builds a synthetic `WallDetails` for any extension
that lodges "As Main Wall: Yes" (copying the Main bp's wall fields
so the cascade gets the same wall config for the extension). Slice
S0380.4 added a new `insulation_thickness_mm` field to `WallDetails`
but did NOT update the inheritance code at line 559-567 — so any
multi-bp cert with an "As Main Wall" extension was losing the lodged
wall insulation thickness on its extension bps, regardless of cert.

Cert 0350-2968-2650-2796-5255 is the first multi-bp ASHP cohort cert
through the Summary path (Main + 1st Extension, both "CA Cavity / FE
Filled Cavity + External / 100 mm"). The dr87 worksheet line ref
(29a) lodges:
  Main: 19.4575 W/K  (77.83 m² × 0.25 W/m²K)
  Ext1:  1.3025 W/K  ( 5.21 m² × 0.25 W/m²K)
  total: 20.7600 W/K
Pre-fix Summary cascade produced walls_w_per_k 22.2188 (over by
+1.46 W/K) because Ext1's missing thickness defaulted to a higher
U-value path. Post-fix walls_w_per_k = **20.7600 — exact match
against worksheet (29a) sum**.

One-line fix at `elmhurst_extractor.py:567`:
+ insulation_thickness_mm=main_walls.insulation_thickness_mm,

Forcing function: cert 0350 first-attempt SAP moves from Δ -4.7365
to Δ -4.5829 — small +0.1536 SAP gain from walls alone. The
remaining ~-4.58 SAP residual on cert 0350 has other contributors
to investigate in subsequent slices (HW kWh 1206 vs predicted target,
HTC 173.42 vs worksheet (39) avg — likely floor / ventilation / PV
gaps not yet covered by Summary mapper).

Added focused unit test
`test_summary_0350_ext1_inherits_main_wall_insulation_thickness`
that pins the inheritance contract directly on the mapper boundary
(bp[0].wall_insulation_thickness == bp[1].wall_insulation_thickness
== "100mm"). Will fail if a future field-addition to WallDetails
again forgets to update the synthetic-WallDetails inheritance block.

Pyright net-zero across both edited files.

Regression suite: 676 pass + 10 fail (= handover baseline 669 + 10
+ 7 new GREEN unit tests across Slices S0380.2..S0380.8).

Spec / cohort context:
- Affects ALL multi-bp Elmhurst Summary certs with "As Main Wall:
  Yes" extensions, not just cert 0350. None of the previously-
  closed cohort certs (001479, 0330) exercised this path — both
  single-bp dwellings.
- SAP 10.2 §3.7 / Table S5 — composite filled-cavity-plus-external
  U-value calc, keyed on lodged insulation thickness.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-27 20:34:17 +00:00 committed by Jun-te Kim
parent 360bf03fe6
commit 2f92edb050
2 changed files with 31 additions and 0 deletions

View file

@ -563,6 +563,7 @@ class ElmhurstSiteNotesExtractor:
u_value_known=main_walls.u_value_known,
party_wall_type=main_walls.party_wall_type,
thickness_mm=main_walls.thickness_mm,
insulation_thickness_mm=main_walls.insulation_thickness_mm,
alternative_walls=self._alternative_walls_from_lines(wall_lines),
)
else:

View file

@ -59,6 +59,7 @@ _SUMMARY_001479_PDF = _FIXTURES / "Summary_001479.pdf"
_SUMMARY_000897_PDF = _FIXTURES / "Summary_000897.pdf"
_SUMMARY_000784_PDF = _FIXTURES / "Summary_000784.pdf"
_SUMMARY_000899_PDF = _FIXTURES / "Summary_000899.pdf"
_SUMMARY_000903_PDF = _FIXTURES / "Summary_000903.pdf"
# GOV.UK EPB API JSON for cert 001479 — the API-path counterpart of the
# Summary_001479.pdf fixture. Together they drive the API ≡ Summary
@ -620,6 +621,35 @@ def test_summary_0380_cylinder_block_surfaces_full_15_1_lodging() -> None:
assert epc.sap_heating.cylinder_thermostat == "Y"
def test_summary_0350_ext1_inherits_main_wall_insulation_thickness() -> None:
# Arrange — cert 0350-2968-2650-2796-5255 is a multi-bp dwelling
# (Main + 1st Extension). Its Summary §7 Walls block lodges
# "1st Extension / As Main Wall / Yes" — the extension's walls
# inherit Main's lodgings (CA Cavity, FE Filled Cavity + External,
# 100 mm). The `_extract_extensions` "As Main Wall" inheritance
# at `elmhurst_extractor.py:559-567` builds a new WallDetails by
# copying Main's fields, but the field set it copies was frozen
# before Slice S0380.4 added `insulation_thickness_mm` — so the
# extension's `WallDetails.insulation_thickness_mm` falls through
# to its dataclass default (None), and the mapper surfaces
# `wall_insulation_thickness=None` on bp[1]. The cascade then
# routes Ext1's composite walls off the lodged-thickness path,
# over-stating Ext1 `external_walls_w_per_k` against worksheet
# line ref (29a) "External walls Ext1 5.21 0.25 1.3025".
pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000903_PDF)
site_notes = ElmhurstSiteNotesExtractor(pages).extract()
# Act
epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes)
# Assert — Ext1 inherits Main's 100 mm thickness and the EPC
# surfaces "100mm" on bp[1] (matching bp[0]).
assert len(epc.sap_building_parts) == 2
main_bp, ext1_bp = epc.sap_building_parts
assert main_bp.wall_insulation_thickness == "100mm"
assert ext1_bp.wall_insulation_thickness == "100mm"
def test_summary_0380_full_chain_sap_within_spec_floor_of_worksheet() -> None:
# Arrange — cert 0380-2471-3250-2596-8761 (Summary_000899.pdf /
# dr87-0001-000899.pdf) is the first heat-pump cert under per-cert