The control gap surfaces as UnmappedSapCode on main and as UnmappedPasHubLabel
once the #1557 control mapper lands (unmapped community-heating control), so the
harness must swallow both; only the closed fuel-specific MissingMainFuelType is
dropped as a fuel-drop tripwire.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
secondary_fuel_type was hardcoded None in the from_site_notes mapper,
dropping the fuel for the ~67 Guinness GMCA 205 properties that lodge a
"Panel, convector or radiant heaters" secondary heater. Adds
_pashub_secondary_fuel_code (ADR-0015 pattern, mirrors
_pashub_main_fuel_code) mapping "Electricity" -> SAP10 code 30, with
"No Secondary Heating"/blank -> None and unknown labels strict-raising
UnmappedPasHubLabel.
Fixes#1564
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`_map_sap_heating` set cylinder geometry/insulation but left
`water_heating_code` / `water_heating_fuel` / `immersion_heating_type`
None (and immersion as a raw label). For cylinder dwellings that loses
the HW-system typing the calculator's water cascade keys off.
Resolve them at the mapper boundary, gated on cylinder presence (combi
dwellings keep the None default so HW inherits the main system, unchanged):
- `water_heating_code` from the `system` label — "From main heating 1" -> 901,
"Electric immersion" -> 903, community -> 950; strict-raises an unknown label.
- `water_heating_fuel` = standard electricity (30) for the electric-immersion
WHC 903; None otherwise (901 inherits the main fuel).
- `immersion_heating_type` via the Elmhurst map (Single -> 2), cylinder-gated.
Combi-dominated cohort so the accuracy impact is negligible by design
(Tier 3): within-0.5 and MAE unchanged (12.9% / 2.56), harness still
206-xfail with no new hard failures.
Closes#1565
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_map_sap_window` copied the raw `glazing_type` survey label; the
calculator's isinstance-int guards read it as non-int and fell to the
double-pre-2002 U=2.8 default for every window — over-counting heat loss
on the ~852 windows the survey dates to 2002-2021 (RdSAP Table 24 U=2.0).
Add `_pashub_glazing_type_int` mapping the surveyed labels to the SAP10
cascade glazing codes the calculator reads for window U
(`_GLAZING_CODE_TO_UWINDOW`) and solar g (`_G_PERPENDICULAR_BY_GLAZING_TYPE`):
before 2002 -> 3, 2002-2021 -> 2, unknown install date -> 3 (pre-2002
default), post-2022 -> 13. Strict-raises `UnmappedPasHubLabel` on an unknown
label; blank passes through as "".
Completes the fabric stack (party-wall/wall-construction/insulation/glazing):
cohort MAE 2.79 -> 2.66, within-0.5 11.4% -> 12.9%, signed -0.04 -> +0.33.
Closes#1562
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer pushback: the gov EPC code table (epc_codes.csv) defines
energy_efficiency_rating 0 generically as "N/A", not specifically
"party wall". The payload carries no other identifying field, so the
actual cause can't be determined from the data alone. The fix itself
is unaffected (still calc-neutral regardless of cause) — just
correcting comments/test naming to not assert an unconfirmed fact.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Task 9d271e98-96e6-4be6-bb50-e9be6f954003 (portfolio 796) failed 21
"Top-floor flat" properties with "EnergyElement: missing required
field 'description'". Every one lodges a second walls[] entry that is
a bare zero-rated party-wall boundary element
({"energy_efficiency_rating": 0, "environmental_efficiency_rating": 0},
no description key at all) — unlike the sibling floors/roofs
"(other premises above/below)" sentinel convention, which does carry
text. Confirmed schema-wide across all 21 failing certs.
Default description to "" rather than fabricate construction text:
_joined_descriptions (heat_transmission.py) already filters out falsy
descriptions, so this is calc-neutral — it only unblocks the parse.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
`from_site_notes` copied the raw `walls_insulation_type` survey label onto
the building part; the calculator's `_int_or_none` read it as `None`, so
the ~101 filled-cavity walls in the cohort lost their lower-U credit and
were over-counted.
Add `_pashub_wall_insulation_type_int` (mirroring the wall-construction
helper and the Elmhurst `_ELMHURST_WALL_INSULATION_TO_SAP10` sibling)
mapping the surveyed labels to the SAP10 wall-insulation codes `u_wall`
consumes ("As built" -> 4 assumed/default, "Filled Cavity" -> 2,
"External" -> 1), strict-raising `UnmappedPasHubLabel` on an unknown label.
Stacked on #1560, this is the fabric fix that removes the cohort's
systematic SAP bias: mean signed -0.79 -> -0.04, MAE 3.11 -> 2.79,
within-0.5 8.5% -> 11.4%.
Closes#1561
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`from_site_notes` copied the raw `walls_construction_type` survey label
onto the building part; the calculator's `_int_or_none` read it as `None`,
so every wall fell back to the age-band + thickness U-value and lost the
solid/cavity/timber/system-built distinction.
Add `_pashub_wall_construction_int` (mirroring `_pashub_party_wall_
construction_int`) mapping the four surveyed labels to the WALL_* codes
`u_wall` consumes, strict-raising `UnmappedPasHubLabel` on an unknown
label and passing a blank label through as the empty-string "no lodging"
sentinel (the field is `Union[int, str]`). On the Guinness GMCA cohort
this trims SAP MAE 3.22 -> 3.11 and lifts within-1.0 12.9% -> 14.9%.
Closes#1560
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Task 9d271e98-96e6-4be6-bb50-e9be6f954003 (portfolio 796) failed property
715358 with "SapHeating: missing required field 'cylinder_size'": cert
0685-2881-6867-9029-1761 lodges has_hot_water_cylinder="false" but omits
sap_heating.cylinder_size entirely. Mirrors the already-handled inverse
gap in _normalize_sap_schema_16_x, so default to code 1 ("no cylinder")
rather than fail loud.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FIELD_MAP and upsert_deal already map planning_authority, designated_area,
article_pd_rights, listed_building, design_constraints, planning_comments,
planning_status, and planning_suggested_approach, but from_deal_id_get_info
never requested them from the HubSpot API. Since HubSpot only returns
explicitly requested properties, these always came back None regardless
of the deal's actual data, so refreshes never populated them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Foundation for PRD #1555: runs each PAS Hub site-note PDF through the
extractor -> EpcPropertyData -> Sap10Calculator and gauges the computed
SAP against pashub's own SAP-10.2 `pre_sap` (from hubspot_deal_data).
- test_pashub_sap_accuracy.py: hybrid gate. Per-fixture "must compute"
(xfail on the known in-progress mapper gaps MissingMainFuelType /
UnmappedSapCode / UnmappedPasHubLabel) + aggregate within-0.5 ratchet
floor, mirroring test_sap_accuracy_corpus.py.
- 205 image-stripped site-note PDFs + manifest.json. Images stripped so
the repo footprint stays ~52MB while the text layer the extractor reads
is byte-identical.
- build_pashub_accuracy_fixtures.py: provenance/rebuild from S3 +
hubspot_deal_data.
All 206 currently xfail on the known heating-string mapper gaps; each fix
(#1556-1568) flips its fixtures to computing and ratchets the floor.
Refs #1555#1568
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`from_site_notes` copied the raw `party_wall_construction_type` survey
label onto the building part; the calculator read `None` and applied the
U=0.25 house default to every party wall — phantom heat loss on the
~165/205 solid party walls that RdSAP 10 Table 15 rates U=0.0.
Add `_pashub_party_wall_construction_int` (mirroring `_pashub_main_fuel_code`)
mapping the six surveyed labels to the SAP10 codes `u_party_wall` consumes,
strict-raising `UnmappedPasHubLabel` on an unknown label, and wire it into
both site-note building-part mappers. On the Guinness GMCA cohort this
halves the systematic SAP under-rate (mean signed -1.58 -> -0.85).
Refs #1559
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_map_sap_heating resolves the raw emitter label (e.g. Radiators) to its
SAP10 emitter code via _pashub_heat_emitter_code, reusing the Elmhurst
emitter map. Blank passes through; an unrecognised label strict-raises
UnmappedPasHubLabel at the mapper boundary (ADR-0015) instead of
resurfacing as the calculator's UnmappedSapCode: heat_emitter_type.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>