RdSAP 10 §5.11.4: a pitched roof lodging thickness "NI" with description
"...insulated (assumed)" was billed at the §5.11.4 observed-retrofit
50 mm row (U 0.68) while the SAME description lodged with "ND" fell
through to the Table 18 age-band default (0.40 for bands A-G). The
ND-vs-NI sentinel is lodging-software noise; "(assumed)" means the
insulation PRESENCE is an age-band assumption, not an observed
retrofit, so Table 18's "thickness cannot be determined" clause
governs both forms. The bare "insulated" description (observed
retrofit, no qualifier) keeps the 50 mm row.
Evidence: the 27-cert loc4+NI corpus cohort was systematically
under-rated (~-1 SAP; zero positive movers among "(assumed)" certs),
vs the 102-cert loc4+ND cohort already on Table 18 at ~0 bias.
Gauge: within-0.5 74.2% -> 75.5%, SAP MAE 0.721 -> 0.708, CO2 MAE
0.09 -> 0.074, PE MAE 3.5 -> 3.2 (floors ratcheted). Unit-pinned in
test_rdsap_uvalues (assumed -> Table 18; bare-insulated -> 0.68
regression guard); integration pin in test_heat_transmission updated;
RealCertExpectation pinned for 10094975827 (66 = lodged, was -1.87).
Ledger: C002 103001004 deferred pending Elmhurst arbitration
(single-immersion off-peak pricing + declared cylinder loss vs
Table-2); C006 quadruplet marked ⚠ (LIG-21.0 software honours lodged
0 extract fans literally vs the Elmhurst-validated 0=unknown->default
convention, worksheet case 46).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setup for the RdSAP-21.0.1 corpus gauge campaign (74.2% within-0.5,
SAP MAE 0.721 at HEAD, 1000/1000 computed):
- scripts/corpus_1000/build_worklist.py — runs all 1000 corpus certs
through the gauge's own path (from_api_response -> Sap10Calculator),
ranks |engine - lodged| descending, clusters by dwelling signature,
and (re)writes scripts/corpus_1000/worklist.md preserving per-cert
statuses and notes. 258 certs outside 0.5 in 249 clusters.
- C001 (worst cert, Δ +23.9): uprn 4510053280, ground-floor flat 47 m²
on ASHP PCDB 100053 (Mitsubishi Ecodan 5 kW, PSR table 0.2-2.0).
Dwelling PSR 2.031 -> SAP 10.2 N2 footnotes 44/45 heat-pump extension
(reciprocal interpolation toward 100% at 2x largest PSR) = 305%
space efficiency, which accredited Elmhurst also applies (golden
case 56, record 100061). The LODGED software instead treated the
out-of-range record as invalid and billed 100% direct electric +
standard schedule (reproduces lodged CO2 to 1%). Lodged-software
methodology gap, worklist ⚠, engine untouched; observed engine 75
pinned in test_real_cert_sap_accuracy.
Gauge floors unchanged (no engine change).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pins the engine's baseline SAP (54) for this electric-room-heater +
gas-multipoint end-terrace ground-floor maisonette (portfolio 814 pid
742121) against accredited Elmhurst RdSAP-10 (worksheet SAP 53, within
1 point), with the Elmhurst Input Summary + SAP Worksheet PDFs saved as
evidence alongside the cert's epc.json.
Companion to the HHRSH storage-heater off-peak-pricing fix PR: this
baseline pin confirms the calculator is faithful on the as-lodged
dwelling; that PR fixes the HHRSH recommendation overlay (engine 48->73
vs Elmhurst 71).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Corpus validation of the modelling_e2e photovoltaic_supply-as-list fix. Cert
6102-6227-8000-0083-2292 (RdSAP-20.0.0 semi, gas combi + 2× 1.14 kW PV arrays)
crashed from_rdsap_schema_20_0_0 on the measured-array list; the fix routes it
through the dict-tolerant _map_schema_21_pv. PV correctly credited: engine 61
(no PV) → 66 (+5). Built in Elmhurst (evidence: epc.json + summary + worksheet,
fabric+heating; the PV "New Technologies" Panel-details grid deferred): worksheet
55 = engine-on-Elmhurst-inputs 55 exactly → calculator faithful. The +6 engine-vs-
Elmhurst base-dwelling residual is the documented RdSAP-default gap (band-C cavity-
uninsulated suspended-floor semi). Pinned engine 66.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Corpus validation of the modelling_e2e built_form fix. Cert 8742-6624-9300-2780-4926
(SAP-Schema-16.0, ground-floor electric-storage-heater flat) omits built_form; the
mapper now derives it from dwelling_type. built_form is ML-only so the fix is
SAP-neutral: engine 66 = lodged 66 exactly. Built in Elmhurst (evidence: epc.json +
summary + worksheet): worksheet 54, engine-on-Elmhurst-inputs 53 ≈ 54 → calculator
faithful. The +12 engine-vs-Elmhurst is a build/input gap (cert size-1 small cylinder
unrepresentable in Elmhurst's Normal/110L-minimum entry → higher HW + reduced-field
16.0 defaults). Pinned engine 66.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Schema coverage (datatypes/epc/domain/mapper.py):
- SAP-Schema-18.0.0: full-SAP shape ≡ 17.1 → from_sap_schema_17_1, no normalisation.
- SAP-Schema-16.0: same reduced-field 16.x path; default the omitted `tenure`
field in _normalize_sap_schema_16_x (metadata; SAP cascade never reads it).
Genuinely sparse 16.x certs (missing core fabric fields) still fail loud.
- Regression tests + sap_18_0_0.json / sap_16_0.json fixtures; 0 new pyright errors.
Autonomous triage of the worklist (scripts/hyde/autonomous_run_findings.md):
- Found + diagnosed 2 bugs (flagged, NOT fixed): (1) MAPPER — full-SAP openings
lodged in mm read as m → multi-million-m2 windows → SAP clamps to 1 (uprn_
10093117227 / 10090317693 / 10091636031); (2) CALCULATOR — database heat-pump
fuel code 39 mis-priced as gas, over-rates ~14 (uprn_10093114053).
- Most certs map within +/-4 of lodged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- SAP-Schema-16.3: same reduced-field RdSAP shape as 16.2 — generalise the
normaliser to _normalize_sap_schema_16_x and route both 16.2/16.3 through it.
uprn_44012843 maps → SAP 79 (lodged 81).
- SAP-Schema-17.0: structurally identical to the full-SAP 17.1 schema (measured
sap_opening_types), so it parses with the 17.1 dataclass and reuses
from_sap_schema_17_1 with no normalisation. uprn_10023444324 → 80, uprn_
10023444320 → 81.
- Regression tests (16.3 dispatch, 17.0 dispatch) + sap_16_3.json / sap_17_0.json
fixtures; 0 new pyright errors. All 7 e2e UPRNs now map.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
SAP-Schema-16.2 (datatypes/epc/domain/mapper.py):
- 16.2 is structurally an RdSAP-17.1 cert under a different name; add
_normalize_sap_schema_16_2 (field renames + defaults) and dispatch to the
tested from_rdsap_schema_17_1 mapper. uprn_100020933699 maps → SAP 71.
- Honour a "Single glazed" windows description when multiple_glazing_type="ND"
(was defaulting to double) → RdSAP-21 code 5; eng 72→71 (lodged 70).
- 4 regression tests + sap_16_2.json fixture; 0 new pyright errors.
Flat party-wall fix (domain/sap10_calculator/worksheet/heat_transmission.py):
- Full-SAP flats carry flatness in dwelling_type, not property_type, so the
party-wall default fell through to the 0.25 house value instead of the RdSAP
Table-15 flat 0.0. Add _is_flat_or_maisonette_dwelling fallback + regression
test. uprn_10093116529 80→81 (matches the cert's lodged party u_value 0).
Accuracy corpus pins (tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py):
- uprn_10093116543 (SAP-17.1 gas-combi semi): engine 81 (Elmhurst 77; documented
full-SAP→RdSAP residual — measured wall/floor U + PCDB boiler vs RdSAP defaults).
- uprn_10093116529 (SAP-17.1 g/f flat): engine 81 (Elmhurst 78).
devcontainer: add poppler-utils (pdfinfo) for the documents-parser PDF fixtures.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reduced-field window U: heat_transmission derived the synthesised-window
raw U from u_window(all None) -> the 2.5 placeholder regardless of glazing.
Now routes the (uniform) glazing_type code through u_window (RdSAP Table 24)
so e.g. double pre-2002 reads 2.8, not 2.5. Only the pre-SAP10 reduced-field
path is affected (21.0.1 certs carry per-window U upstream) — the RdSAP-21.0.1
corpus gauge is unchanged at 66.9% within-0.5.
test_real_cert_sap_accuracy: pin uprn_10002468137 (RdSAP-17.1, all-electric
storage heaters) at SAP 61, validated against Elmhurst on identical inputs
(dual off-peak immersion, 110 L cylinder, 2 baths). Our engine reproduces
Elmhurst's fuel cost to the penny; lodged 55 is the old SAP-2012 schema.
Tooling to grow the accuracy corpus:
- scripts/fetch_real_life_epc_sample.py — capture a cert by UPRN into the corpus.
- scripts/compare_epc_paths.py — diff gov-API vs Elmhurst-summary EpcPropertyData
and run both through the engine, localising mapper vs calculator differences.
- skill validate-cert-sap-accuracy — the end-to-end loop (capture -> Elmhurst
inputs -> human builds -> compare -> reconcile -> pin in the test).
- skill epc-to-elmhurst-rdsap-inputs reference: corrected immersion (code 1=dual),
cylinder size (code 2 = Normal/110 L), and bath-count (WWHRS sub-tab) mappings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>