The differ and the HubSpot fetch already named notes_for_surveyor, but the
SQLModel had no such field, so check_for_db_update_trigger raised
AttributeError on every deal and the scrape could not diff at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An oil STORAGE combi (Worcester Greenstar Heatslave II, PCDB 18415,
store_type 1) zeroed its internal-store loss: pcdb_combi_loss_override
defers on store_type in {1,2,3} and a combi has no cylinder, so line
(56) dropped to zero — under-costing hot water and over-rating SAP by
+2.0..+4.9 across the five LRHA WAVE 3 oil combis.
SAP 10.2 Table 2 note b) now includes the store loss when the combi's
efficiency is from the PCDB: the PCDB store volume (field 42) and
insulation thickness (field 44) feed Table 2/2a with the Table 2b
storage-combi temperature factor, reproducing the accredited Elmhurst
P960 worksheet exactly (5.4175 kWh/day, Jan 167.94, ~1977 kWh/yr).
Cohort: within-0.5 55.3% -> 58.3%, MAE 0.675 -> 0.531; ratchet
tightened accordingly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#1684. `_main_2_space_heating_fuel_cost_gbp_per_kwh` billed an
electric Main 2 at Main 1's space-heating rate. That was a no-op on a
homogeneous off-peak cohort (storage+storage, direct+direct → equal
rates) but mis-billed the HETEROGENEOUS case: an off-peak STORAGE Main 1
(5.5 p/kWh low) + an on-peak DIRECT-ACTING panel Main 2 (15.29 p/kWh
high) rode the storage low rate, under-costing the panel half and
over-rating SAP.
Flip it to bill Main 2 at its OWN Table 12a Grid 1 SH row, the same
per-system resolution Main 1 and a non-electric Main 2 already use — and
the same one `_main_2_high_rate_fraction` already applies, so the scalar
cost and the high-rate split are now consistent.
The docstring's "regresses certs 13/34 (Parkers Hill / Dunley Road)"
warning was stale: those names appear nowhere but that docstring, and
the full guard suite shows zero new regressions (only the two
pre-existing `test_heating_systems_corpus` fails on main). LRHA WAVE 3
fixture 497655371974 (storage+panel) moves +15.3 → -2.5 vs PasHub; cohort
MAE 0.799 → 0.675 (ratchet re-baselined 0.80 → 0.68). within-0.5 holds at
55.3% — the -2.5 residual is PasHub-vs-Elmhurst divergence, not a bug, so
the fixture stays out of the within-0.5 pin. A ground-truth Elmhurst pin
is queued (see docs/HANDOVER_1684_ELMHURST_INPUTS_497655371974.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A dwelling with two main heating systems lodges a "Main Heating 2" block and a
"Percentage of space heating provided by Main Heating 1" split (e.g. 20% fan +
80% slimline storage, or 50% storage + 50% panel heaters). The survey model held
a single `main_heating`, and the extractor read the whole §Heating section
first-match — so it captured only Main 1, modelled it as 100% of the load, and
dropped Main 2 and the split (also bleeding Main 2's fields into Main 1). Every
such dwelling over-rated.
- `HeatingAndHotWater` now holds `main_heating_2` + `main_heating_1_percent`.
- The extractor slices the "Main Heating Systems" region per system on the
"Main Heating N" markers and reads Main 1's percentage; a single-main dwelling
(no "Main Heating 2") parses byte-identically to before.
- `_map_sap_heating` factors the per-system detail into `_pashub_main_heating_
detail(main, fraction)` and emits two `MainHeatingDetail`s, setting Main 2's
`main_heating_fraction = 100 - Main 1 %` (the calculator reads Main 2's share
and gives Main 1 the remainder).
5 of the 6 dual-main dwellings converge (within-0.5 53.4% → 55.3%, MAE 0.799);
single-main fixtures and the gov-API / Elmhurst / Guinness / Wythenshawe corpora
are unaffected. The 6th (497655371974, storage+panel) regresses because the
calculator deliberately bills an electric Main 2 at Main 1's rate
(`_main_2_space_heating_fuel_cost_gbp_per_kwh`, deferred §10a off-peak slice) —
the correct two-main mapping now exposes that calculator gap; tracked separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A PasHub Manual-Entry electric boiler lodges "Direct acting" in its "Heating
System (Boiler):" age-band cell. `_PASHUB_MANUAL_BOILER_AGE_BAND_TO_TABLE_4B`
covers only gas/liquid Table 4b rows, so the descriptor resolved to None and the
boiler stayed uncoded — taking the generic 0.80 gas-boiler seasonal efficiency
AND, on a Dual meter, `_table_12a_system_for_main`'s None-fallback that bills the
whole main-heating load at the off-peak LOW rate. The two errors pull opposite
ways, so an identical mis-mapping over-rated one dwelling and under-rated another
(fixture 497712825571 +9.5 on a Dual meter; 461386632387 -6.2 on a Single meter).
A direct-acting electric "boiler with radiators" is SAP 10.2 Table 4a code 191
(direct-acting electric boiler, efficiency 1.00, §12 Rule 3 tariff). Code it
directly in the manual-boiler resolver (gated on fuel Electricity + no PCDB
product), so the single code value drives the efficiency table, the Table 12a
pricing row and the tariff dispatch together. Both fixtures converge:
497712825571 → +0.6, 461386632387 → +0.4. LRHA within-0.5 52.4% → 53.4%,
MAE 0.835. Renamed `_pashub_manual_boiler_table_4b_code` → `_pashub_manual_
boiler_code` (it now also returns the Table 4a electric code).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An electric room/panel-heater dwelling (main SAP code 691) on a Dual meter is a
10-hour tariff by RdSAP 10 §12 Rule 3, so its main heating bills the secondary's
identical electricity at a 0.50-blended 11.09 p/kWh. Its SECONDARY heating,
however, resolved its tariff from the bare meter string — where a Dual meter
defaults to 7-hour (`tariff_from_meter_type`) — and billed at the 7-hour
all-high-rate 15.29 p/kWh. The ~10% secondary fraction was over-priced by ~4 p/kWh,
under-rating SAP by ~1 point across the LRHA WAVE 3 code-691 cohort.
Route the dwelling's resolved `_rdsap_tariff(epc)` into `_secondary_fuel_cost_gbp_
per_kwh` / `_secondary_off_peak_rate_gbp_per_kwh`, exactly as the main-heating cost
path (`_space_heating_fuel_cost_gbp_per_kwh`) and the secondary CO2 path
(`_secondary_heating_co2_factor_kg_per_kwh`) already do — removing the internal
meter-only resolution that diverged from the dwelling's single §12 tariff.
Storage-heater dwellings (7-hour by §12) are unchanged — their secondary correctly
stays at 15.29 p (regression-pinned in test_cert_to_inputs). LRHA WAVE 3 within-0.5
49.5% → 52.4%, MAE 0.977; gov-API, Elmhurst RealCert, Guinness and Wythenshawe
corpora unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clears the three non-table-add blocks tracked in #1639 plus a latent age-band
bug they surfaced. LRHA WAVE 3 cohort: 77 → 103 of 104 computing.
Block A (exact cylinder volume): a measured cylinder lodges its litres on a
separate "Exact cylinder volume:" line; parse it in the extractor, carry it on
WaterHeating.cylinder_volume_measured_l, route the "Exact cylinder volume" band
to SAP cylinder-size code 6, and thread the litres into SapHeating (the
calculator reads them only when the code is 6). Verified end-to-end on the real
166 L fixture.
Block B (Controls line-wrap): PasHub wraps a long "Controls:" value across two
PDF lines and _get_in read only the first, truncating the label so the mapper
missed its Table 4e code. Re-join the continuation when the value ends on a
conjunction/comma (grammatically incomplete) so "…room thermostat and TRVs"
matches the existing code 2314. Scoped to Controls: — the shared _get_in is
unchanged; a wider truncation audit is tracked separately.
Age-band normalisation: the heat_network_age_band '2007 - 2011' block was one
facet of a broader bug — PasHub lodges the construction age band in mixed
templates ("K: …" prefixed, tight "1991-1995", space-padded "2007 - 2011") and
only the letter form was normalised, so bare year ranges were SILENTLY
MIS-RATED by the age-band-keyed U-value/DLF cascades. Normalise every template
to its RdSAP letter in _extract_age_band: unblocks the heat-network dwellings
AND corrects ~30 already-computing fixtures (within-0.5 42.9% → 49.5%,
MAE 3.05 → 1.02). Sibling Guinness cohort unchanged (82.9% / 0.36).
Block C (gable 'None') was already resolved on main — no change needed.
Fixture 461178278096 lodges no main heating at all (MissingMainFuelType);
tolerated as a known gap in the harness and ticketed (#1680) for the modelling
decision. LRHA ratchets re-baselined to the corrected cohort (0.49 / 1.05).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>