Add `rafter_insulation_thickness` to `EpcBuildingPartModel` (JSONB, matching the
sibling thickness columns so Union[str, int] survives) and wire it through
`from_domain` / `_to_building_part`.
Property 749719 lodges "250mm" -> Table 16 col (2) U 0.23. Dropped, the roof
billed at U 2.30 — a 10x error worth -9.8 SAP (lodged 73, calculated 63.2) that
moved the dwelling a whole band, C -> D, and drew a 9.63-point loft-insulation
recommendation on a roof rated 4/5 by the assessor.
The sibling wall/roof/floor_u_value and wall_is_basement fields have the same
drop but are deliberately NOT persisted: they are full-SAP artefacts and we
re-model as RdSAP, which uses default U-values. Their allow-list entries are
re-justified rather than removed — the previous "not read by the calculator" was
stale and is what hid this drop.
Requires the Drizzle migration in Hestia-Homes/assessment-model to land FIRST;
without the column, EPC reads fail outright.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RdSAP 10 §5.11.2 — a roof insulated at rafters lodges its thickness in
`rafter_insulation_thickness`, not `roof_insulation_thickness`. The field had no
DB column, so it was mapped from the gov-EPC API and silently dropped on save;
every at-rafters roof then fell back to the Table 18 col (2) unknown-thickness
default (2.30 W/m²K for bands A-D — the spec's *uninsulated* value).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve#1601 and land #1602/#1603 on the merged-#1609 tree, adjudicated
against Khalim's verified ground truth (not pre_sap).
#1601 (upper-floor +0.25 m joist void) — RESOLVED as keep-raw. The
gov-API/Elmhurst mappers add 0.25 m to upper-storey heights because their
lodged height is a clear internal ceiling. Against verified truth RAW beats
+0.25 on all 7 verified dwellings (16 Stillwater raw -0.35 within-band vs
+0.25 -0.75; every verified property is already under truth and +0.25 only
lowers SAP). pashub surveys internal dimensions (202/205 "Measurements
Location: Internal") and its upper-ground height gap is modal 0.00, so the
joist void is not differentially present and a blanket +0.25 is unsafe.
_map_floor_dimensions keeps the raw surveyed height with a RESOLVED note.
#1602 — semi-exposed / exposed floor exposure. The surveyed "Floor type"
"Semi Exposed (unheated)" now sets is_above_partially_heated_space (RdSAP
§5.14 U=0.7) and "Exposed Floor" sets is_exposed_floor (§5.13 Table 20), on
the lowest storey (floor==0) only, mirroring the gov-API/Elmhurst siblings.
New strict-raise _pashub_floor_exposure (ADR-0015).
#1603a — alternative walls. New AlternativeWall survey dataclass +
extract_alternative_walls (the "Alternative Wall"->"Windows" section) +
_map_pashub_alternative_walls -> sap_alternative_wall_1/2. The cascade
deducts each alt-wall area from the part's main opaque wall (no double
count), so a mixed-facade dwelling is billed at each sub-area's own
construction. The alt-wall block lodges "As Built" (capital B) — aliased to
the existing default insulation code.
#1603b — extension floor construction. ExtensionConstruction gains a floor
block (parsed like the main building) and _map_extension_building_part now
threads floor_type/construction/insulation/u_value_known, which the main
path already did and the extension silently dropped.
Cohort: within-0.5 50.2% -> 51.2%, MAE 0.652 -> 0.624, 0 strict-raise
errors across all 205. Verified-truth safe: none of the 7 verified
dwellings have alt walls, and #1602 moves the one it touches (2 Philips
exposed ext floor) by -0.00. pyright 0-new (mapper baseline 39). Goldens
test_full_building_construction + end_to_end EXTENSION_1 updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three silent-drop extraction fixes the gov-API/Elmhurst mappers already apply
but `from_site_notes` uniquely omitted — each let a favorable default reach the
SAP-10.2 calculator and over-rate the dwelling. Same calculator, so the whole
gap was extraction fidelity (gov-API corpus 78.6% within-0.5 vs pashub 23.9%).
#1598 — electric-shower + bath counts: `_map_sap_heating` built `shower_outlets`
but never set `electric_shower_count`/`mixer_shower_count`/`number_baths`, so
the cascade defaulted to 1 gas mixer / 0 electric, billing electric-shower hot
water off the cheap main system. 100/205 fixtures lodge "Electric Shower".
#1599 — floor label: pashub lodges "Ground Floor" (capital F); the calculator's
RdSAP §5(12) suspended-timber floor-infiltration gate is case-sensitive on
"Ground floor", so the 0.2-ACH term was silently dropped. Normalized at the
mapper boundary. 98/205 suspended-timber.
#1600 — water-heating "None": the RdSAP 10 §10.7 "no water heating system"
signal was discarded and HW inherited the gas combi (58 Hackle 67.7 vs
verified 59). Coded to WHC 999 so the existing electric-immersion + Table
28/29 default fires (also folds in #1594: blank cylinder size is no cylinder,
not a phantom one).
Guinness GMCA 205 cohort: within-0.5 23.9% -> 50.2% (103/205), MAE 1.410 ->
0.650. Ratchets tightened to 0.50 / 0.65. Verified vs Khalim's ground truth
(16 Stillwater 78.2 -> 75.7 vs 76). #1601 (upper-floor +0.25 m) held — it
empirically over-corrects this cohort (internal-vs-external dimension question,
under investigation). pyright: 0 new errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second sanctioned exception to "ratchets never loosen" — needs @KhalimCK's
call, since the prior note expected to re-tighten here. Exactly one cohort
property is WHC 950 (16 Bingley Close, +5.946 -> +6.818 = the whole +0.0043);
within-0.5 unchanged at 23.9%. Its pre_sap is verified correct but its ~+6
residual is a known extraction bug, and this hybrid gauge cannot adjudicate a
calculator fix (#1592 Problem 2). The gov-API corpus, which gauges the
calculator alone, improves.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Enumerates the 3 corpus certs that can move (all WHC 950) individually per
#1592 sub-task D: two improve (one crossing inside 0.5), one worsens and is
handed to sub-task C for an accredited oracle. No loosening required.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pytest.approx is untyped under pyright strict, so the new assertions added
3 errors against the repo's zero-new-errors bar. Switches to the abs(...)
<= tol form already dominant in this file. No behaviour change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SAP 10.2 Table 3 ("For heat networks apply the formula above with p = 1.0
and h = 3 for all months") and §4.3's "insulated pipework" literal both
scope on the DHW source, so a water-only network gets the same row as a
network space main. Was falling back to the RdSAP §3 age-band default —
band E → p=0, h=5 — billing 64.58 kWh in January against the spec's 23.26.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SAP 10.2 §4.3 (p.24) scopes itself to "where hot water is provided by a
heat network" and requires the Table 3 primary loss, so the gate keys on
the DHW source. Checked off the WHC before the main-keyed branches so a
non-boiler space main (cert 9093's warm air) can't zero it out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
950 boilers / 951 CHP / 952 community heat pump differ only in plant
efficiency, which SAP 10.2 §4.3 puts in the Table 12 heat price — so the
dwelling's delivered-heat demand must not vary with it. Passes on the
previous commit's expression; verified RED against the prior impl, where
951 leaked 0.80/0.75 and 952 under-billed its DHW fuel by 3.15×.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SAP 10.2 §4.3 (p.24) puts the water-heating efficiency in the Table 12
heat price, and C4 (p.53) scopes the plant efficiency to CO2/PE only, so
worksheet (310) = (64) × (305a) × (306) carries no efficiency term. The
dwelling-side divisor was applying it a second time.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Read stored lodged EPCs whatever refetch_epc says, so a stored assessment
can win the ADR-0001 Recency Tie-Break against the gov register.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backs out the WHC-950/951/952 primary-loss (d3c0b9e4) and flat-rate-charging
(fecf4f62) changes to cert_to_inputs.py: they fire on ANY cert with those
water-heating codes, regressing the gov-API RdSAP corpus
(test_api_path_sap_accuracy_on_rdsap_21_0_1_corpus MAE 0.627 -> 0.630), and an
Opus audit found the water-only branch has a pre-existing plant-efficiency
double count they flow more kWh through. The calculator work + the correct
fix (water_eff = 1.0/(DLF*1.05)) + an Elmhurst-worksheet regression are
tracked in #1592, isolated from the extractor accuracy PR. The cylinder-
insulation MAPPER fix (f5e4814b) is retained.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Khalim-authorised MAE loosening: coding window orientation exposes the known
SAP-10.2-vs-lodged offset the zeroed solar gains were masking; within-0.5 is
the north-star metric and jumps 20.0% -> 23.9%.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>