Slice 64: bulk-update cohort 000474 hand-built for Cat A diff parity

Closes 36 of the 50 mapper-vs-hand-built load-bearing divergences by
populating fields the Elmhurst mapper extracts but the original
cohort hand-built left at their `make_minimal_sap10_epc` / dataclass-
default values. Every change is cascade-equivalent — none alter
`_FIXTURE_PINS["000474"]` SapResult fields (all 11 1e-4 pins remain
GREEN against worksheet `SAP value 62.2584`).

Per-SapBuildingPart additions (Main, Ext1, Ext2):
- `wall_thickness_measured`: False → True. Summary §7 lodges Wall
  Thickness 280 mm explicitly; the cascade doesn't read this field
  (grep `wall_thickness_measured` across domain/sap/ returns no
  consumer outside test fixtures), so flipping it is field-level-
  only.
- `floor_type`, `floor_construction_type`, `floor_insulation_type_str`,
  `floor_u_value_known`: surfaced from Summary §9 ("G Ground floor" /
  "U Above unheated space" / "T Suspended timber" / "A As built" /
  U-value Known = No). Strings carry the lodged text for cross-mapper
  parity; cascade reads the int codes on SapFloorDimension.
- `roof_insulation_location`, `roof_insulation_thickness`: surfaced
  from Summary §8 ("J Joists" + "100 mm"). Cascade's `u_roof` for
  age B at thickness=100 returns the same 0.40 W/m²K as the age-B
  default (thickness=None falls through to `_ROOF_BY_AGE['B']=0.40`),
  so the cascade output is identical.

SapVentilation additions (all cascade-equivalent — `None` defaults to
0 throughout the §2 cascade chain):
- 6 explicit zero counts (`open_flues`, `closed_flues`, `boiler_flues`,
  `other_flues`, `passive_vents`, `flueless_gas_fires`)
- `pressure_test="Not available"` (descriptive, no test was lodged)
- `draught_lobby=True` (the legacy field; cascade reads
  `has_draught_lobby=False` which is set already, so True on the
  legacy field has no cascade effect)

Top-level additions via `make_minimal_sap10_epc`:
- `extensions_count=2` (Slice 54 fix on mapper made this surface; the
  hand-built was carrying the pre-Slice-54 hard-coded 0)
- `blocked_chimneys_count=0`, `dwelling_type="Mid-Terrace house"`,
  `built_form="Mid-Terrace"`, `property_type="House"`

Post-construction mutations (helper doesn't expose these as kwargs):
- `has_conservatory=False`, `any_unheated_rooms=False`,
  `number_of_storeys=2`, `hydro=False`, `photovoltaic_array=False`

Diff count: 50 → **14**. The remaining 14 are real semantic gaps for
the next slices to close:

  Cat B (mapper needs to surface 7 fields):
    - country_code (Elmhurst mapper produces None; should set 'ENG')
    - sap_heating.water_heating_fuel (None vs 26 — gas main heating
      should imply gas water heating fuel)
    - main_heating_details[0].boiler_flue_type (None vs 2 — Summary
      §14.1 lodges "Balanced" flue type)
    - main_heating_details[0].emitter_temperature ('Unknown' vs 1)
    - main_heating_details[0].main_heating_number (None vs 1)
    - sap_ventilation.has_draught_lobby (None vs False)
    - dual-encoded central_heating_pump_age int/str

  Cat C (structural shape, 2 diffs):
    - sap_windows: LEN 7 vs 5 (mapper 1:1 with §11 table vs hand-built
      collapsed by glazing-type group, preserving total area —
      cascade-equivalent but not field-equal)
    - sap_building_parts[*].party_wall_construction: None vs 0
      (cohort convention sentinel; the cohort 000474 docstring
      established `0 = "Unable to determine"`)

  Cat B handbuilt-needs (hand-built should add 2 fields the mapper
  already surfaces):
    - sap_heating.shower_outlets (mapper extracts 'Non-electric shower')
    - sap_heating.number_baths (mapper extracts 1)

11 cohort cascade pins still GREEN; pyright net-zero (0 errors on
the touched fixture file). Tracer-bullet diff test stays RED with
14 divergences (was 50).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-25 16:49:37 +00:00
parent 01d234dd0b
commit b5cbfe83de

View file

@ -49,13 +49,25 @@ _WC_CAVITY = 4
def build_epc() -> EpcPropertyData:
"""EpcPropertyData mirroring the Elmhurst 000474 inputs."""
"""EpcPropertyData mirroring the Elmhurst 000474 inputs.
Field-level parity with `from_elmhurst_site_notes(Summary_000474.
pdf)` for the load-bearing field allow-list every cohort hand-
built doubles as the ground-truth diff target for the Elmhurst
mapper. Cascade-equivalent encoding-only fields (descriptive floor/
roof strings, ventilation zero counts) are populated explicitly to
eliminate noise from `test_from_elmhurst_site_notes_matches_hand_
built_NNNNNN` diffs without altering the SAP cascade output (the
Section-10a 1e-4 pins in `test_e2e_elmhurst_sap_score.py` remain
GREEN against the worksheet PDF).
"""
main = SapBuildingPart(
identifier=BuildingPartIdentifier.MAIN,
construction_age_band="B",
wall_construction=_WC_CAVITY,
wall_insulation_type=4,
wall_thickness_measured=False,
# Summary §7 lodges Wall Thickness 280 mm explicitly; matches mapper.
wall_thickness_measured=True,
party_wall_construction=0,
sap_floor_dimensions=[
SapFloorDimension(
@ -72,13 +84,22 @@ def build_epc() -> EpcPropertyData:
),
],
wall_thickness_mm=380,
# Mapper-extracted descriptive fields (cascade reads the int
# codes on SapFloorDimension; these strings carry the lodged
# Summary text for cross-mapper field parity).
floor_type="Ground floor",
floor_construction_type="Suspended timber",
floor_insulation_type_str="As built",
floor_u_value_known=False,
roof_insulation_location="Joists",
roof_insulation_thickness=100,
)
extension_1 = SapBuildingPart(
identifier=BuildingPartIdentifier.EXTENSION_1,
construction_age_band="B",
wall_construction=_WC_CAVITY,
wall_insulation_type=4,
wall_thickness_measured=False,
wall_thickness_measured=True,
party_wall_construction=0,
sap_floor_dimensions=[
# Ext1 hangs off the main from the first storey upward — its
@ -98,13 +119,19 @@ def build_epc() -> EpcPropertyData:
),
],
wall_thickness_mm=380,
floor_type="Above unheated space",
floor_construction_type="Suspended timber",
floor_insulation_type_str="As built",
floor_u_value_known=False,
roof_insulation_location="Joists",
roof_insulation_thickness=100,
)
extension_2 = SapBuildingPart(
identifier=BuildingPartIdentifier.EXTENSION_2,
construction_age_band="B",
wall_construction=_WC_CAVITY,
wall_insulation_type=4,
wall_thickness_measured=False,
wall_thickness_measured=True,
party_wall_construction=0,
sap_floor_dimensions=[
SapFloorDimension(
@ -115,11 +142,18 @@ def build_epc() -> EpcPropertyData:
),
],
wall_thickness_mm=380,
floor_type="Ground floor",
floor_construction_type="Suspended timber",
floor_insulation_type_str="As built",
floor_u_value_known=False,
# Summary §8 Ext2: "PN Pitched (slates/tiles), no access" + Joists
# + Insulation Thickness "Unknown" → mapper leaves thickness=None.
roof_insulation_location="Joists",
)
# PDF lodges "PCDF boiler reference: 16839 Vaillant ecoTEC pro 28 88.70%".
# The 16839 is the BRE PCDB index_number (Table 105 Vaillant ecoTEC pro
# 28kW VUW GB 286/5-3, 2005-2015, winter eff 88.7%, summer eff 87.0%).
return make_minimal_sap10_epc(
epc = make_minimal_sap10_epc(
total_floor_area_m2=56.79,
country_code="ENG",
postcode="bd3 8aq",
@ -130,11 +164,29 @@ def build_epc() -> EpcPropertyData:
low_energy_fixed_lighting_bulbs_count=8,
sap_windows=list(SECTION_6_VERTICAL_WINDOWS),
percent_draughtproofed=78,
extensions_count=2,
blocked_chimneys_count=0,
dwelling_type="Mid-Terrace house",
built_form="Mid-Terrace",
property_type="House",
sap_ventilation=SapVentilation(
extract_fans_count=2,
sheltered_sides=2,
has_suspended_timber_floor=False,
has_draught_lobby=False,
# SAP10.2 §2 — explicit zero counts match the mapper, which
# parses the Summary's "No. of <flue>" rows. None / 0 are
# cascade-equivalent (the (11)+(13a)+(13b) chain treats
# absent counts as zero), but setting 0 explicitly closes
# the cross-mapper field diff for free.
open_flues_count=0,
closed_flues_count=0,
boiler_flues_count=0,
other_flues_count=0,
passive_vents_count=0,
flueless_gas_fires_count=0,
draught_lobby=True,
pressure_test="Not available",
),
sap_heating=make_sap_heating(
main_heating_details=[
@ -145,6 +197,15 @@ def build_epc() -> EpcPropertyData:
],
),
)
# Top-level cert-lodgement booleans / counts the Elmhurst mapper
# surfaces from the Summary PDF but `make_minimal_sap10_epc` doesn't
# expose as kwargs. Set post-construction (dataclass is non-frozen).
epc.has_conservatory = False
epc.any_unheated_rooms = False
epc.number_of_storeys = 2
epc.hydro = False
epc.photovoltaic_array = False
return epc
# ============================================================================