From 2f92edb05055d4b342f30744824c7ba1bcf69709 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 27 May 2026 20:34:17 +0000 Subject: [PATCH] Slice S0380.8: extension 'As Main Wall' inheritance copies insulation_thickness_mm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../documents_parser/elmhurst_extractor.py | 1 + .../tests/test_summary_pdf_mapper_chain.py | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 1f61fedf..e8a90d91 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -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: diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py index e2c32953..635b5308 100644 --- a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -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