5 certs in a 2026 API sample raised `KeyError: 'rr_common_wall_area_m2'`
and were blocked from computing. Root cause: `_part_geometry`'s early
return (taken when a building part lodges no sap_floor_dimensions —
e.g. a party-wall-only or RR-only extension as bp[0]) returned only 6 of
the 9 keys the full return exposes, omitting rr_common_wall_area_m2,
rr_gable_area_m2 and cantilever_floor_area_m2. The §3.9 RR contribution
block reads geom["rr_common_wall_area_m2"] / ["rr_gable_area_m2"] for
EVERY part, so the floorless part's truncated dict raised KeyError at
heat_transmission.py:974.
Fix: the early return now exposes all 9 keys, the three RR/cantilever
geometry values defaulting to 0.0 — correct, since a part with no floor
dimensions has no derivable RR shell or cantilever (no floor area).
Pure contract-completion bug; no spec/U-value change.
Regression test pins the invariant directly: a floorless part's
_part_geometry keys must equal a with-floors part's keys. Validated: all
5 certs now compute (4 within ~2 SAP of lodged; the 5th, 8536, has a
separate residual). §4 suite 2393 passed; heat_transmission.py pyright
unchanged at 12, test file at 71.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 2b. Timber frame (wall_construction=5) takes internal wall insulation but
not external (not constructable — ADR-0019), so the generator offers IWI only.
Cascade pin: the IWI Option reproduces the re-lodged timber-frame after at
abs(diff) <= 1e-4 (general Table 6 insulation-thickness bucket, not the solid-
brick documentary path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 2a. New recommend_solid_wall emits one Main-wall Recommendation carrying
External + Internal wall-insulation Options for an uninsulated (wall_insulation
_type=4) solid-brick (wall_construction=3) main wall, each priced at the heat-
loss wall area. Cascade pin: the generator's EWI and IWI Options reproduce
their respective re-lodged afters at abs(diff) <= 1e-4.
Detection keys on wall_construction code, not description (ADR-0019 note
corrected): the Elmhurst ingestion path leaves walls[].description empty, so
the code is the only cross-path signal; codes 1-5 are consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
above) → None
The 2026 sample lodges roof_construction=6 (1 cert, "Thatched, with
additional insulation") and =7 (6 certs, "(same dwelling above)" /
"(another dwelling above)"), both raising UnmappedApiCode and blocking
the cert. roof_construction_type is read ONLY for the §3 "sloping
ceiling" cos(30°) inclined-surface factor (Slice 89); the base roof
U-value comes from the global roofs[].description. Neither code is a
sloping ceiling:
- 6 = thatched — U set by the description, not this field;
- 7 = same/another dwelling above — an internal ceiling with no roof
heat loss (the roof-side analogue of floor_construction code 0,
governed by the roof_heat_loss / description path).
Map both to None: carries no information the cascade consumes here and
correctly avoids the cos(30°) false-trigger. Empirically inert and
validated — roof W/K is byte-identical whether 6/7 map to None or to an
explicit pitched string across all code-6/7 certs in the sample. 5 of
the 7 now compute (e.g. thatched cert 2276 SAP 62.8 vs lodged 63); the
other 2 also carry a gable_wall_type 2/3 raise (separate, worksheet-
backed slice).
Dict value type widened to Optional[str]. §4 suite 2392 passed; mapper.py
pyright unchanged at 32; new tests suppress reportPrivateUsage (net-zero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Captures the grill-with-docs session for solid-wall insulation: CONTEXT.md
gains EWI/IWI Measure Types + the Wall Insulation Eligibility rule (+ a
flagged-ambiguity that the three planning flags stay distinct, never recollapsed
to legacy restricted_measures). ADR-0019 records the eligibility policy (cavity
-> cavity only; brick/system -> IWI+EWI; timber -> IWI only; cob/stone -> none;
conservation/flat block EWI, listed/heritage block both). ADR-0020 records
conservation/listed/heritage as three distinct Property attributes sourced by
extending the geospatial S3 repo (flags co-located with lat/long).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Slice 1 of solid-wall insulation. BuildingPartOverlay gains a
wall_insulation_thickness field; the generic applicator already folds it onto
SapBuildingPart by name. With wall_insulation_type=1 (EWI) / 3 (IWI) + 100 mm,
the calculator derives the post-insulation U-value (§5.8 documentary path,
λ=0.04 default) — and for IWI also lowers the thermal-mass parameter. Two new
Elmhurst before/after cascade pins (solid-brick EWI + IWI, cert 001431)
reproduce the re-lodged after at abs(diff) <= 1e-4 across SAP/CO2/PE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A 2026-register cert (4519-9056-4002-0222-4802) omits the top-level
post_town entirely (its town sits only in address_line_3 "BARNSTAPLE").
RdSapSchema21_0_x declares post_town as a required no-default field, so
from_dict raised "missing required field 'post_town'" and blocked the
whole cert from computing.
post_town is address metadata the SAP cascade never reads (no consumer
in domain/sap10_calculator/), so default an absent post_town to "" in a
from_api_response pre-processor (mirroring _normalize_shower_outlets) —
inert for the calculation, keeps the cert mappable. The schema dataclass
can't simply give post_town a default: it is a plain (non-kw_only)
dataclass with 57 required fields after post_town, so a mid-list default
would break field ordering.
Validated: cert 4519 now maps (post_town="") and computes SAP cont 74.68
vs lodged 75. §4 suite 2392 passed; mapper.py pyright unchanged at 32;
new tests suppress reportPrivateUsage (net-zero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 2026 sample's second-largest mapper raise: 37 certs lodge
sap_floor_dimensions.floor_construction=0, which raised UnmappedApiCode
and blocked the cert. Code 0 is the "not recorded / not applicable"
sentinel — 33/37 pair it with floor_heat_loss=6 ("another dwelling
below", an upper-floor flat with no ground floor to describe); the rest
carry mixed Solid / unheated-space descriptions. There is no single
construction to assert.
Map code 0 → None, which defers to RdSAP 10 Table 19 ("where floor
construction is unknown" → age-band default) — identical to how an
unlodged floor_construction (the 993 None certs) is already handled, and
honest about the absence (cf. the no-misleading-insulation_type rule).
Empirically inert and validated: across all 37 code-0 certs the cascade
floor W/K is byte-identical whether code 0 maps to None or to an explicit
"Solid" string — the another-dwelling-below floors compute to 0.0 W/K
(handled via floor_heat_loss + property_type=Flat + floors[].description,
per the _API_FLOOR_HEAT_LOSS_TO_FLOOR_TYPE code-6 note), and the few
genuine ground/unheated floors hit the same age-band default either way.
All 37 now compute (were raising).
Dict value type widened to Optional[str] for the None entry; helper
already returns Optional[str]. §4 suite + schema-mapper tests green
(pre-existing test_total_floor_area failure unrelated); mapper.py pyright
unchanged at 32; new test suppresses reportPrivateUsage (net-zero).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A random 1000-cert Jan–May 2026 EPB-register sample surfaced 53 certs
lodging sap_floor_dimensions.floor_construction=3, which raised
UnmappedApiCode and blocked the whole cert from computing (~44 of the
sample's mapper raises). RdSAP 10 field 3-1 "Floor construction"
enumerates the lowest-floor construction as solid / suspended timber /
suspended, not timber, and the spec's "Suspended not timber (structural
infiltration 0)" makes the split load-bearing.
Map code 3 to the canonical "Suspended, not timber" string (the same
value the site-notes mapper already emits — cross-mapper parity):
- u_floor takes the suspended BS EN ISO 13370 branch via the
"Suspended" prefix (_floor_is_suspended_from_description), and
- _has_suspended_timber_floor_per_spec's exact-match
`!= "Suspended timber"` gate correctly does NOT fire, so the §5 (12)
0.1/0.2 floor-infiltration adjustment is skipped (structural
infiltration 0) — exactly the spec rule for not-timber suspended.
Validated: all 5 sampled code-3 certs now compute (e.g.
0340-2877-5570-2606-5965 floor_construction_type="Suspended, not
timber", SAP cont 60.12 vs lodged 60). Confirmed against the cert's own
global floor descriptions ("Suspended, …", floor_heat_loss=7).
Code semantics established from the RdSAP 10 spec + the lodged certs'
human-readable floor descriptions (the EPB /api/codes endpoint carries
no floor_construction enum). §4 suite + schema-mapper tests green
(the pre-existing test_total_floor_area failure is unrelated). mapper.py
pyright unchanged at 32; new test suppresses reportPrivateUsage to keep
net-zero new errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Record S0380.218 shipped, bump HEAD/next-slice, note both certs are
0-residual cross-validated golden fixtures and flag the optional
Summary-path regression guard as the cheap follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two certs fetched fresh from the GOV.UK EPB register, each with an
Elmhurst Summary PDF (input) and a dr87 worksheet PDF (the (1)..(286)
ground truth):
0340-2467-9260-2006-6521 (Summary_000922 / dr87-0001-000922)
5500-5070-0822-0201-3663 (Summary_000920 / dr87-0001-000920)
Both run through BOTH front-ends — from_api_response and
from_elmhurst_site_notes — and through the rating + demand cascades.
Cross-mapper parity holds: the two paths agree to <1e-4 on continuous
SAP, fuel cost, CO2 and PE. Both paths reproduce the worksheet exactly:
0340: (255) cost 776.4295, (272) CO2 2875.0498, (286) PE 16474.5616;
fabric (33) 171.6188, (37) 205.9358; SAP int 70 = lodged.
5500: (255) cost 751.8295, (272) CO2 2423.4547, (286) PE 14397.0118;
fabric (33) 141.1226, (37) 167.3696; SAP int 66 = lodged.
Pinned in two tables of test_golden_fixtures.py:
- _EXPECTATIONS / test_golden_cert_residual_matches_pin — SAP/PE/CO2
residual vs the integer-rounded lodged register (SAP resid +0 both).
- _WORKSHEET_PE_CO2 / test_golden_cert_pe_co2_matches_worksheet —
PE (286)/(4) and CO2 (272) vs the worksheet at +0.0000 (the
load-bearing 1e-4 check; lodged register is integer-rounded).
Dropped-field audit (raw JSON keys vs the schema-21.0.1 dataclass
fields consumed by from_dict) re-run on both fresh JSONs: no new
silently-dropped fields — only created_at metadata and the
shower_outlet_type/shower_wwhrs keys already handled by
_normalize_shower_outlets (mapper.py:2047). No calculator or mapper
change required; this is pure validation + regression-pinning.
Full §4 suite: 2392 passed, 1 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>