Gov-API certs that lodge a gas boiler as main_heating_category=2 + a PCDB
main_heating_index_number but omit sap_main_heating_code were carried with
a null code, so the heating generators (which gated on sap_main_heating_code
alone) never evaluated them for a boiler upgrade or a system tune-up — they
silently got nothing (£0 works, plateau at D). The SAP calculator already
handles this shape (cert_to_inputs._is_wet_boiler_main), so the calculator
modelled these as wet boilers while the recommenders ignored them.
Mirror the calculator's classification in the generator:
- _is_wet_boiler / _is_electric_boiler fall back, when no SAP code is
lodged, to a PCDB Table 105 boiler index or main_heating_category in
{1,2} (heat pumps excluded); electric boilers stay excluded by the code
range when lodged, else by an electricity main fuel.
- _already_condensing reads the boiler's declared PCDB winter efficiency
when there is no Table 4b code, so a modern condensing boiler lodged this
way still isn't offered a pointless like-for-like swap (gets a tune-up).
Verified read-only on portfolio 814 / scenario 1271: pids 742304, 742306,
742322, 742325, 742336 now get a gas_boiler_upgrade (two cross D->C on the
boiler alone) where they had £0 works; predicted properties whose heating
donor is a PCDB-shape boiler (e.g. 742095/742102/742105) also recover a
tune-up. Fixes the A#2 slice of #1388 (and unblocks 742302/742367).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PRD #1385 phase 2. The 20.0.0 and 21.0.0 mappers dropped insulated_door_u_value
(top-level Optional float), unlike 21.0.1. heat_transmission reads it as the
measured door U-value override (cert_to_inputs -> heat_transmission:809), so
dropping it forced every door onto the RdSAP default U-value.
Verification: new test 3 passed (21.0.0 sample lodges 3; 20.0.0 latent -> inject
1.5 + absent -> None); corpus 6002 passed; SAP-accuracy regressions pass; pyright
unchanged (39 -> 39).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The neighbour SAP-divergence cohort was (postcode, property_type, built_form),
which mixed electric- and gas-heated dwellings. Electricity scores materially
lower in SAP than mains gas for identical fabric, so a mixed-fuel postcode
produced cross-fuel false outliers — an electric dwelling flagged only for being
electric among gas neighbours.
Add the main-heating fuel class to the cohort key (electricity variants — 29/30
+ off-peak 31-40 — collapse to one class; every other code is its own bucket).
Now every flagged divergence is a same-fuel comparison worth investigating. On
portfolio 814 this refined 20 raw divergences to 17 same-fuel ones while keeping
the genuine within-fuel outliers (e.g. the all-electric WC2B 4AW flats at SAP
26/38 vs a cohort median of 67).
Reaches the fuel via epc_main_heating_detail through the indexed property_id —
still never touches the recommendation table. Surfaced by the portfolio-814
recommendation audit (Work item B, #1388).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PRD #1385 phase 2. The 21.0.0 mapper dropped mechanical_ventilation_index_number
and mechanical_vent_duct_type, unlike 21.0.1. cert_to_inputs reads both (PCDB
Table 4f/329 heat-recovery/SFP lookup + the Table 329 in-use factor), so an
MVHR/MEV cert on the 21.0.0 path fell back to the SAP default efficiency.
Mirrors 21.0.1 exactly -- only the two fields it actually maps. The other four
mechanical_vent_duct_* schema fields have no mapper OR calculator consumer even
on 21.0.1 (verified), so they're deliberately left alone rather than populating
dead domain fields.
Verification: new test 1 passed (harvested 21.0.0 sample lodges index=12,
duct_type=3); corpus 6002 passed; SAP-accuracy regressions pass; pyright
unchanged (39 -> 39).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PRD #1385 phase 2. 21.0.0 was the last mapper still dropping the lodged
percent_draughtproofed (top-level int) — every other path (17.0/…/21.0.1) reads
it. Dropping it understated the SAP §2 draughtproofing/infiltration credit for
21.0.0 certs.
Verification: new test 1 passed (harvested 21.0.0 sample lodges 100); corpus
6002 passed; SAP-accuracy regressions pass; pyright unchanged (39 -> 39).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
PRD #1385, building-part fabric family. The 17.0 mapper hardcoded
floor_insulation_thickness / sap_room_in_roof to None and omitted
flat_roof_insulation_thickness, all of which the 17.0 schema lodges and the
19.0/20.0/21.x paths read:
* floor_insulation_thickness: "NI" (no measured thickness) -> None so u_floor
reaches its age-band default, else pass through (mirrors the 19.0 note).
* flat_roof_insulation_thickness: pass through (was dropped).
* sap_room_in_roof: map to SapRoomInRoof(floor_area, construction_age_band),
None if absent (mirrors 19.0).
17.0-only: the 17.1 SapBuildingPart schema has none of these fields, so its None
there is correct (verified against the dataclass). roof_insulation_thickness is
intentionally left raw -- the sloping-ceiling resolve helper moves roof U-values
and is a separate follow-up.
Verification: new test 3 passed; test_mapper_corpus 6002 passed; SAP-accuracy /
RealCertExpectation regressions pass; pyright unchanged (39 -> 39, pre-existing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>