Commit graph

7886 commits

Author SHA1 Message Date
Jun-te Kim
f69d345493 docs(corpus): correct 100061275133 pin comment — real driver is Table 11
The prior comment blamed a stale build-side combi artifact (already fixed
this session) and my earlier conversational claim that Elmhurst's overall
SAP used regional weather was wrong too — verified the worksheet's (5)
dwelling volume and wind table match our UK-average rating cascade exactly.

The actual, now-quantified driver: Elmhurst's worksheet (201)/(202)/(215)
show it unconditionally applies SAP 10.2 Table 11's 10% secondary/portable-
electric-heater fraction based on main heating category alone, with no
secondary lodged on the cert. Our `_secondary_fraction` deliberately gates
this on an actual lodged/forced secondary (see the adjacent S-B30 note:
applying it unconditionally regressed the full 1000-cert corpus MAE by
+0.16). Forcing Table 11 on for just this cert moves 76.33 -> 73.62 (~62%
of the 76.33-vs-72 gap) — confirms this, not a mapping error, drives the
residual. Documented so the next reader doesn't re-litigate it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:41:09 +00:00
Daniel Roth
701a6aa5cd Merge branch 'main' into feature/abri-api-integration 2026-07-03 09:40:18 +00:00
Daniel Roth
71a309ad0a
Merge pull request #1442 from Hestia-Homes/feature/abri-api-integration
Abri API Integration: check for tenant data fetch trigger
2026-07-03 10:33:44 +01:00
Jun-te Kim
5abc87ea18 chore(corpus): reconciliation pass over the 1-2 SAP band, no new bug found
Per the PR #1439 handoff: swept every cert now sitting at |Δ| in [1.0, 2.0)
(58 certs) using the cost-vs-demand signature bucketer (why_diverge.py's
logic) plus field_bias.py's full-corpus subgroup scan (re-run clean after
the two engine fixes above — no |meanΔ|>=0.5 systematic subgroup with
coherent median).

Breakdown: 17 room-in-roof (already spec-mandated/worksheet-validated per
PRs #1440/#1441), 7 electric cost/tariff variants (spec-faithful vs lodged,
consistent with the diagnosed electric cluster), 4 non-electric cost
residuals (spec-faithful), ~26 non-electric fabric/demand certs. Manually
inspected geometry (floor areas/perimeters/heights, wall/roof codes,
window/door counts) for the 4 largest fabric/demand outliers in the band
(100031768368, 100070450818, 100110097281, 100051051866) — no data-entry
anomaly comparable to the earlier 12109191 decimal-floor-area bug. This
confirms the handoff's "our calc faithful, lodged differs" pattern holds
across the band; no engine bug survived the pass. Notes appended per-cert
in worklist.md (`<-`, preserved by build_worklist.py reruns).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:26:05 +00:00
Khalim Conn-Kowlessar
dd30903ace Merge remote-tracking branch 'origin/main' into fix/electric-room-heater-overlay-companions 2026-07-03 09:25:11 +00:00
Khalim Conn-Kowlessar
244c5b65ed Document heating-companion coherence: category/control forced, fuel/meter deferred (ADR-0048)
ADR-0048 records why a system-replacing heating override must emit a
complete companion set — category and control archetype-forced for
coherence, fuel and meter deferred where multiple values are valid, and
an unmapped companion logged rather than silently inherited. Adds the
Heating Companion Set glossary term to CONTEXT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:21:47 +00:00
Jun-te Kim
6c1828e8bf chore(hyde): fix 100061275133 Elmhurst build — 0 fans + PCDB exact boiler
ventilation(): the cert lodges no extract-fan count and RdSAP 10 Table 5's
age-band B default is 0, but the build entered 1 — inflating Elmhurst's own
(8)/(21) infiltration lines relative to both the cert and our engine.

space_heating(): switched from the generic "Combi condens" SAP-Table dialog
code (84% efficiency, no PCDB record) to elmhurst_lib.set_pcdb_boiler(18908)
via clear_main_heating_code — resolves the exact lodged product (Worcester
Greenstar 4000, 88.70%) and its correct keep-hot classification (None),
matching the SAP engine's own PCDB lookup and removing the "Standard Combi"
keep-hot artifact the reused generic code carried.

Also corrected the file's stale docstring (described a different property
entirely — mid-terrace/250mm/DUAL meter/11 windows/lodged 63 — left over
from a copy-paste).

Verified against the refreshed worksheet: (21) shelter-adjusted ACH 0.6938
now EXACT vs our engine's ventilation_from_cert; (61) combi loss monthly
kWh matches ours to ~0.01 kWh (was carrying a ~360 kWh/yr keep-hot artifact).
Both open items from the PR #1439 handoff are resolved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:20:12 +00:00
Khalim Conn-Kowlessar
962a443ed2 A complete room-heater overlay does not log a companion gap 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:17:06 +00:00
Khalim Conn-Kowlessar
390e3e89d7 The overlay logs an error when it cannot complete a heating companion set 🟩
A system-replacing override whose category or control is not yet mapped
now logs an error naming the archetype and code, so the incomplete
companion set (which otherwise silently inherits the replaced system's
value) is visible for review — log-and-continue, matching flag_fuel_mismatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:16:25 +00:00
Khalim Conn-Kowlessar
ea113b58fb The overlay logs an error when it cannot complete a heating companion set 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:15:31 +00:00
Khalim Conn-Kowlessar
ffd21689fd An electric room heaters override sets the room-heater charge control 🟩
Control 2601 (SAP Table 4e Group 6 room-heater, +0.3 C conservative
default) now overlaid for electric room heaters, matching the gas/oil/
solid siblings, so a replaced storage heater's manual charge control
2401 (+0.7 C) no longer survives and over-warms the modelled dwelling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:13:32 +00:00
Khalim Conn-Kowlessar
cfd9799500 An electric room heaters override sets the room-heater charge control 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:13:03 +00:00
Jun-te Kim
dee72bbdfa fix(rdsap): §5 (12) floor infiltration falls back to floors[].description
_has_suspended_timber_floor_per_spec only read the Main bp's per-part
floor_construction_type lodgement; the gov-API mapper frequently leaves
that None even when the global epc.floors[].description carries an
explicit "Suspended, ..." observation. _main_floor_u_value already fell
back to this joined description for the U-value calc — the infiltration
rule did not, so a genuinely suspended-timber floor silently entered
(12)=0 instead of 0.2 unsealed. Extracted the shared
_effective_floor_description helper and added a tri-state resolver
(explicit lodgement -> description, excluding "not timber" -> Table 19
footnote-1 age-A/B default) so both paths agree.

Cert 100061275133 (Elmhurst-validated build, PR #1439 handoff): engine
77.24 -> 76.33, now an exact match to lodged (76). Corpus gauge 77.7% ->
77.8% within-0.5, MAE 0.637 -> 0.636; floors ratcheted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:12:19 +00:00
Khalim Conn-Kowlessar
bf032d17d0 An electric room heaters override sets the room-heater heating category 🟩
Category 10 (SAP Table 4a Room heaters) now overlaid for electric room
heaters (691-701), so a replaced storage heater's category 7 no longer
survives onto the effective cert and mis-bills via the Table 12a resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:12:19 +00:00
Khalim Conn-Kowlessar
5e7e51d8b2 An electric room heaters override sets the room-heater heating category 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:11:38 +00:00
Daniel Roth
05ca0bb280 move static method below non-statics 2026-07-03 08:33:10 +00:00
Daniel Roth
a36fe00132 Abri tenant data fetch resolves abri deals by associated project id 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:28:25 +00:00
Daniel Roth
5df1237b90 Abri tenant data fetch requires a parseable new commencement date 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:27:42 +00:00
Daniel Roth
47842da7b9 Abri tenant data fetch requires a parseable new commencement date 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:27:22 +00:00
Daniel Roth
b4d2d9cd37 Abri tenant data fetch ignores already-set commencement dates 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:26:20 +00:00
Daniel Roth
c18f4feb3e Abri tenant data fetch ignores already-set commencement dates 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:26:02 +00:00
Daniel Roth
426aa9a15e Abri tenant data fetch does not trigger for non-abri deals 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:25:26 +00:00
Daniel Roth
0b8f0f17a9 Abri tenant data fetch does not trigger for non-abri deals 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:25:07 +00:00
Daniel Roth
2f1dfaa5a6 Abri tenant data fetch triggers when commencement date is first set 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:24:36 +00:00
Daniel Roth
da0ef3e5e0 Abri tenant data fetch triggers when commencement date is first set 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:24:11 +00:00
Daniel Roth
050bb78313 Merge branch 'main' into feature/abri-api-integration 2026-07-03 08:20:08 +00:00
Jun-te Kim
3fd7c5708d feat(hyde): PCDB exact-boiler entry (set_pcdb_boiler + clear_main_heating_code)
Cracks the 'PCDB disabled' blocker. The PCDF boiler reference resolves the exact
lodged boiler (e.g. 18908 -> Worcester Greenstar 4000, 88.70%) when: (1) the main
heating CODE is empty server-side (else the field + magnifying glass are
aspNetDisabled), and (2) the index is entered via REAL keyboard + Tab (a JS-set
.value doesn't survive the AutoPostBack). Now Elmhurst uses the same boiler
efficiency as the SAP engine, so worksheet vs calculator compare like-for-like.

Verified on 100061275133: Elmhurst now shows 'Efficiency of main space heating
system 1 = 88.70%' (was generic 84%). Includes updated worksheet evidence.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 07:49:23 +00:00
Jun-te Kim
b9d36f72cb chore(hyde): refresh 100061275133 Elmhurst PDFs after draught-proofing rebuild
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:45:58 +00:00
Jun-te Kim
ad8d6534bb chore(hyde): fix 100061275133 draught-proofing (doors draught-proofed, cert=100%)
Third Playwright-entry error found via input reconciliation: my build left
draught-proofed doors at 0 (Elmhurst computed 33% draught proofing), but the
cert lodges percent_draughtproofed=100. Our calculator correctly uses 100% ->
infiltration 42.66 W/K; the ~19 W/K ventilation gap to Elmhurst was purely my
build error. On matched inputs our fabric+TB (109.38) equals Elmhurst's total
fabric (37) EXACTLY, confirming the calculator is RdSAP-faithful. Committed for
build-faithfulness record; the engine's +1.24 vs lodged 76 is not a bug.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 06:29:37 +00:00
Jun-te Kim
8b8cc96d43 chore(corpus): mapper faithfulness audit — EPC-API->EpcPropertyData is faithful
Audits identity pass-through fields (dims, counts, thicknesses) raw cert vs
EpcPropertyData across all 1000 certs. All apparent mismatches are legitimate
mapper behavior the first-pass audit didn't model: window_wall_type=4 windows
correctly split to sap_roof_windows (not dropped); empty placeholder building
parts correctly discarded; 'measured' insulation-thickness sentinel resolved to
the wall_insulation_thickness_measured value. Remaining per-BP diffs were an
index-misalignment artifact (raw BP[i] vs epc BP[i] after empty BP0 dropped).
Conclusion: mapper is faithful; divergences are Playwright-entry or calc-side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 06:18:02 +00:00
Jun-te Kim
61934af97b chore(hyde): fix 100061275133 Elmhurst build faithfulness (lighting 5 LED, wall thickness 400)
Input reconciliation (Elmhurst summary vs EpcPropertyData vs raw cert) exposed
two Playwright-entry errors: lighting entered as 6 incandescent (cert has 5 LED,
100% low-energy) and wall thickness left at Elmhurst's 260mm default (cert lodges
400mm). Fixing both moved Elmhurst 67->69. Confirmed the MAPPER is faithful
(every fabric/heating/lighting field matches the raw cert) and the engine uses
the correct PCDB 18908 efficiency (88.7%). The remaining Elmhurst gap to lodged
76 is the un-enterable PCDB boiler (automation falls back to generic BGW 84% +
keep-hot); the engine (77) is faithful. Not a mapping error.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 06:13:09 +00:00
Jun-te Kim
a593921a83 fix(rdsap): thin present wall-insulation no longer maps to uninsulated bucket
Field-level bias scan (scripts/corpus_1000/field_bias.py) flagged solid-brick +
internal-insulation certs (wall_con+ins=(3,3)) systematically under-rating
(median -0.40, n=18). Root cause: _insulation_bucket routed insulation that is
KNOWN PRESENT but with a thin lodged thickness (5-24mm, below the smallest
tabulated 50mm column) to bucket 0 (uninsulated, U 1.7) - WORSE than the
present-but-unknown case, which the RdSAP Table 6 footnote correctly routes to
the 50mm row. A known 10mm shouldn't read as higher-U than not knowing at all.

Now present insulation never falls to bucket 0; thin/unknown thickness both
round up to the 50mm row. Only genuinely absent insulation maps to 0.

Corpus cert 5117280 (10mm internal, no wall thickness -> bucket path): Δ -4.44
-> +1.08. Corpus MAE 0.641 -> 0.637, PE MAE 3.0 -> 2.9. Gauge 77.7% holds; 44
RealCert pins + 342 fabric tests green. TDD: 2 new unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 05:54:47 +00:00
Jun-te Kim
58125503f0 chore(corpus): why_diverge.py — categorize all 223 diverging certs by reason
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 05:19:20 +00:00
Jun-te Kim
4fba35acbd chore(corpus): demand-driven analyzer + confirm no generic fabric bug remains
Added scripts/corpus_1000/demand_cohort.py (isolates fabric/demand-driven
divergences from cost noise). Investigation conclusion: the demand-side tail is
one-offs, not a cluster. 12109191 (+13.1) is a LODGED decimal error (365.67 m²
floor, 10x its ground floor). 217060420 (-4.4) is spec-faithful (36.6 m2 lodged
glazing at spec U). The only >=3 structural clusters among diverging certs are
electric flats (already diagnosed spec-faithful). No generic fabric bug remains;
gauge 77.7% is near its structural ceiling. Marked 12109191 unbuildable in ledger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 05:09:10 +00:00
Jun-te Kim
6c47951d60 test(accuracy): pin code-195 electric storage-boiler certs as spec-faithful (§12 Rule 2)
Electric-tariff cluster investigation. The 112-cert electric-main cohort is
mixed-sign within every category/meter - no systematic tariff bias. Root cause
of the biggest non-xfail divergences (10012334488 +13.2, 10091578598 +7.81):
SAP code 195 = 'Electric water storage boiler' (Table 4a p.170), which SAP 10.2
§12 Rule 2 correctly bills mostly at the 7-hour off-peak rate. Both are pure
cost gaps (PE/CO2 match lodged -> demand right); the lodged software over-billed
the storage boilers at peak rate. The engine is SPEC-FAITHFUL - matching lodged
would tune against §12/Table-4a. Pinned to the spec-faithful engine values.

Adds scripts/corpus_1000/elec_cohort.py (electric-main cohort analyzer).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 04:55:42 +00:00
Jun-te Kim
bf53d29690 test(accuracy): pin 100051051866 + 10070056075 (clean single-main loop complete)
Final two clean single-main gas certs. Both small cost-side residuals (+1.04 /
-1.01 SAP) on cavity-insulated fabric already Elmhurst-validated on sibling
certs, with properly-insulated cylinders (insulation type 1, 50/80mm - not the
type-0/NULL zero-storage-loss bug). No fabric anomaly; the register data fully
explains the engine output. Pinned to observed engine 71 / 79.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 04:30:22 +00:00
Jun-te Kim
1032a4f128 test(accuracy): validate + pin 100061275133 (slice-5 external-insulation U confirmed)
2-storey semi, solid brick 400mm with EXTERNAL insulation, 300mm loft,
suspended uninsulated ground floor, gas combi, double glazing (glazed_type 13).
Built in accredited Elmhurst RdSAP10: roof (U 0.14, 5.459 W/K) and windows
(15.32 x eff U 1.3258 = 20.311 W/K) match the engine EXACTLY; the
externally-insulated wall is engine U 0.29 vs Elmhurst 0.30, validating the
slice-5 external-insulation default (100mm added over solid brick); suspended
ground floor engine 0.68 vs Elmhurst 0.70. Elmhurst's overall SAP 67 is not
comparable (reused-assessment Standard-Combi keep-hot loss, a build artifact).
Engine 77 vs lodged 76 (within 1.24). Pinned to the observed 77.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 04:28:25 +00:00
Jun-te Kim
17ced10e94 test(accuracy): validate + pin 217091901 (single-glazing confirmed vs Elmhurst)
Top-floor flat, solid brick uninsulated, uninsulated flat roof, gas combi,
SINGLE glazing (glazed_type 5). Built in accredited Elmhurst RdSAP10 and every
heat-loss element matches the engine EXACTLY: walls 53.18, flat roof 145.57,
floor 0, party 0, and — with the faithful single glazing — windows 34.7114 W/K
(8.62 m2 x eff U 4.0268). Confirms the mapper's glazed_type 5 -> single (raw U
4.8) is spec-correct per epc_codes.csv, not the double it superficially
resembles. Engine 61 vs lodged 62; the -1.18 is a cost-side residual, not a
fabric bug. Pinned to the observed engine 61.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 04:16:47 +00:00
Jun-te Kim
6f2b73e613 chore(hyde): fix window-grid automation (keep good row, delete stale below) + pin 100050881708
SOLVED the window-grid blocker (per Junte's "keep one and edit, don't
delete all"): set_single_window now ADDS the fresh window (it inserts at
DOM index 0), then deletes the STALE rows BELOW it by index (high->low,
never index 0) via the per-row Delete image button + JS-clicked Yes modal.
The freshly-added row survives as the single window; no 0.00 junk strands
to block the Recommendations gate. Verified end-to-end: cert 100050881708
built, worksheet downloaded, walls (0.70) + roof (0.19) match the engine
EXACTLY. Combined with the earlier fill+Tab (area postback) + frame/gap
(validation) fixes, the per-cert loop now runs autonomously on clean
single-main certs.

Pin: 100050881708 (semi, cavity filled, pitched 225mm, suspended ground
floor, combi) engine 72 = lodged 70 +2; walls/roof accredited-Elmhurst-
matched. NB the build entered the suspended ground floor as an upper
"above unheated space" floor, so Elmhurst's worksheet SAP (61) is not a
clean comparison — the engine uses the correct §3.12 suspended-ground U
(0.74); the pin rests on the walls+roof match + lodged.

No engine change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 03:55:48 +00:00
Jun-te Kim
b4dbaef7f2 chore(hyde): window-grid entry findings (fill+Tab persistence, frame/gap required)
Continued hardening for the per-cert Elmhurst loop. Confirmed findings:
- The window Width/Height must be set via Playwright .fill() + Tab (fires
  the ASP.NET AutoPostBack so the server commits the area); a JS-set of
  .value bypasses the postback and Elmhurst persists a 0.00-area row.
- Frame Type (PVC) + Glazing Gap (12 mm) are REQUIRED per row or the
  Recommendations gate blocks the worksheet ("Frame Type/Glazing Gap must
  be entered"). add_combined_window now sets both.

KNOWN BLOCKER (documented for the next attempt): the window grid is not
yet reliably automatable when stale rows exist on the shared assessment.
New rows insert at index 0; the delete flow reliably removes only index 0
(the newest), so accumulated 0.00 junk rows BELOW cannot be targeted, and
grid reads are timing-inconsistent across postbacks. A robust handler
needs to either (a) drive the per-row Delete by matching the row's
server-side id rather than DOM index, or (b) start each build from a
freshly-created assessment (not the shared one) so the grid is empty.
Until then the per-cert loop runs only on the FIRST build after a manual
window reset. build_100050881708.py is the WIP template.

No engine change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 03:44:49 +00:00
Jun-te Kim
7987c94876 chore(hyde): harden Elmhurst automation for per-cert builds (window cleanup + reset)
Support for the per-cert Elmhurst worksheet loop:
- goto()/save_close(): fall back networkidle -> domcontentloaded on
  timeout so an incomplete-validation state (half-set RR, 0-area window)
  no longer hangs navigation.
- add_combined_window(): select glazing first (AutoPostBack), then JS
  input+change+blur the width/height with settle waits so the Area cell
  computes before Add (a plain .fill() raced the postback -> 0.00 row).
- set_single_window(): leave the grid holding exactly one row, deleting
  rows carried over from a prior build of the SHARED assessment (the
  window grid is the only accumulating page).
- reset_transient_state(): return the shared assessment to neutral after
  a run (clear RR age band, secondary = No, meter = Single).

KNOWN RESIDUAL: the window grid still intermittently persists a 0.00 area
through Save & Close (Elmhurst's "each Add wipes the previous row" +
last-row-undeletable behaviour), and the secondary-heating dialog's modal
background can intercept the cascade click. Reliable autonomous grinding of
per-cert builds needs these two hardened further; the loop currently
completes clean single-main gas certs (see the 47084930 flat-roof slice)
but stalls on secondary-heating + stale-window combinations. build_
100021924710.py is the WIP template for that harder shape.

No engine change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:54:35 +00:00
Jun-te Kim
8e33a8de53 test(accuracy): ratchet 77.3% -> 77.7% via flat-roof insulation-thickness fix (Elmhurst-validated)
A FLAT roof lodges its insulation thickness in the DEDICATED gov-EPC API
`flat_roof_insulation_thickness` field (e.g. "75mm"), leaving
`roof_insulation_thickness` None (that field is for pitched-loft joists).
`heat_transmission_from_cert` read only the latter, so a measured
flat-roof thickness was ignored and the roof billed at the uninsulated
age-band flat default (age E = 1.5) instead of its Table-16 insulated U.

Fixed by preferring `flat_roof_insulation_thickness` when the part is a
flat roof — the exact mirror of the existing rafter-thickness branch.
An "AB"/"NI" (as-built/unknown) value parses to None and keeps the
age-band default, unchanged; only measured thicknesses move.

PER-CERT ELMHURST VALIDATION (cert 47084930, top-floor flat, flat roof
75 mm): built on the lodged inputs in accredited Elmhurst RdSAP10-Online
(evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf). The
worksheet bills "insulated flat roof" at U 0.5 (floor 0.70 + wall 0.25
also matching the engine). The fix takes the engine roof 96.4 -> 32.1 W/K
(= 64.26 x 0.5, Elmhurst-exact), PE +47 -> +0.2, SAP 69.51 -> 74.34 =
lodged 74.

Gauge: within 77.3% -> 77.7%, SAP MAE 0.648 -> 0.641, CO2 0.074 -> 0.072,
PE 3.1 -> 2.97. Unit-pinned in test_heat_transmission
(flat_roof_insulation_thickness -> U 0.5); RealCertExpectation 47084930
= 74 (Elmhurst-validated). Also adds the build script build_47084930.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:27:57 +00:00
Jun-te Kim
11a637c6bd test(accuracy): ratchet 77.0% -> 77.3% via unsized-cylinder storage-loss fix
A hot-water cylinder lodging gov-API `cylinder_size=0` ("size not
determined", with has_hot_water_cylinder=true) resolved to a None volume
in `_cylinder_storage_loss_override` (which reads `_cylinder_volume_l_
from_code`, returning None for code 0). The subsequent `if volume_l is
None: return None` then DROPPED the cylinder's Table-2 storage loss
entirely — under-costing the DHW and over-rating the dwelling. Meanwhile
the Table-13 high-rate-fraction path already used `_hot_water_cylinder_
volume_l` (which defaults size 0 to 110 L), so the two DHW paths
disagreed on the same cylinder.

RdSAP 10 §10.5 Table 28 ("if the actual size is not determined, the size
is taken as according to Table 28") makes an unsized present cylinder the
110 L "Normal" baseline, which STILL incurs the storage loss. Fixed by
defaulting the storage-loss volume to 110 L for the explicit size-0 case.

Gated on the EXPLICIT 0 (not None): a full-SAP cert whose RdSAP
cylinder_size is simply unlodged (None, e.g. pinned 10091568921) keeps
its own cylinder handling rather than a forced 110 L RdSAP default.

7 corpus certs lodge cylinder_size=0 + has_cylinder=true. Gauge: within
77.0% -> 77.3%, SAP MAE ~flat (0.648), CO2 0.074 -> 0.073, PE 3.2 -> 3.05
(the previously-dropped storage loss now correctly counted in the demand
cascade). Unit-pinned in test_cert_to_inputs (storage loss non-None for
size 0); RealCertExpectation 200004017091 = 71.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:02:27 +00:00
Jun-te Kim
8e487a9bae docs(elmhurst): validate slice-5 wall fix against accredited Elmhurst worksheet (uprn 200004296092)
Built the cert in accredited Elmhurst RdSAP10-Online on the lodged register
inputs; downloaded Input Summary + SAP Worksheets (committed alongside).

Confirmed:
- Slice 5 DIRECTION validated: the externally-insulated solid-brick wall is
  billed insulated by both tools (Elmhurst U 0.48, engine U 0.29), far below
  the uninsulated 1.40 the pre-fix §5.8 gate produced.
- ROOF matches EXACTLY (U 0.30, 9.35 W/K).
- Engine 71.74 = lodged 71; Elmhurst current SAP 66 (its "unknown external
  insulation" default is more conservative + this build used a generic combi
  and no keyed wall thickness, so 66 is not a clean engine-comparison).

Follow-up lead (documented in ELMHURST_FINDINGS.md): the engine feeds the
UNMEASURED lodged wall_thickness (360 mm, measured=N) into the §5.6 formula,
whereas RdSAP §5.3 uses measured thickness / Table-3 default otherwise. The
party-wall "difference" is NOT a bug (engine 0.25 is spec-correct, Table 15
row 4). Also adds the build script scripts/hyde/build_200004296092.py.

No engine change in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:49:58 +00:00
Jun-te Kim
0e784cbb1c test(accuracy): ratchet SAP MAE 0.658 -> 0.647 via wall added-insulation unknown-thickness default
A SOLID-BRICK or STONE wall lodging External/Internal insulation
(wall_insulation_type 1/3) but no measured insulation thickness (gov-API
"NI" -> None) was billed at its UNINSULATED §5.6/Table-6 U-value: the §5.8
added-insulation R-value adjustment gates on `insulation_thickness_mm > 0`,
so an unknown thickness fell straight through to the raw solid-wall U
(~1.4-1.7). This over-counted wall heat loss and under-rated the dwelling.

RdSAP 10 §5.4 (PDF p.32): "the U-value with additional insulation is based
on the thickness of insulation of mineral wool type (assume 100 mm if
thickness is unknown)". Apply that 100 mm default so the §5.8 Table-14
R-value fires. Scoped to solid brick / stone (the only constructions with
a §5.8 R-value path in u_wall) so cavity (composite path) and timber /
system-built walls are untouched.

Localised by deep-diving corpus cert 200004296092 (end-terrace, solid
brick 360 mm "with external insulation", NI): walls 101.7 W/K (U~1.40,
HLP 3.46 = absurd for an insulated wall) -> ~0.29; PE +59.6 -> ~0; engine
64.35 (Δ -6.65) -> 71.74 = lodged 71. ~8 solid-brick/stone corpus certs
carry this NI-thickness-with-insulation shape.

Gauge: within-0.5 held at 77.0%, SAP MAE 0.658 -> 0.647, CO2 0.074 ->
0.073, PE 3.2 -> 3.1 (floor ratcheted). Unit-pinned in test_rdsap_uvalues
(brick 360 mm external NI -> 0.29) + RealCertExpectation 200004296092 = 72.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:24:56 +00:00
Jun-te Kim
cb089a630c test(accuracy): ratchet SAP MAE 0.668 -> 0.658 via instantaneous-electric-DHW off-peak fix
An instantaneous point-of-use electric water heater (WHC 907/909, no
cylinder) on an off-peak tariff was billed 100% at the off-peak LOW rate
by the generic electric-off-peak else-branch. But SAP 10.2 §12 (PDF line
2680) computes the off-peak on-peak HW proportion via Table 13, "dependent
on the total floor area and the CYLINDER size" — it presumes a stored-water
cylinder charged overnight. An instantaneous heater has NO cylinder, heats
on demand, and cannot shift to the off-peak window, so 100% of its
consumption bills at the HIGH rate (Table 12a WH high-rate fraction 1.0).

Both the SAP-cost rate (`_hot_water_fuel_cost_gbp_per_kwh`) and the
ADR-0014 / CO2-PE fraction (`_hot_water_high_rate_fraction`) fixed
consistently (low-rate scalar -> 7-hour high rate; fraction 0.0 -> 1.0).

Localised by deep-diving corpus cert 74061136 (HHR-storage mid-floor flat,
WHC 909): PE matched lodged (+1.6, roof+floor zero-loss) while SAP over-rated
+7.72 — the cost-only signature. Its DHW was ours £59 vs lodged £344 (5.8x);
the tariff was the whole gap. Fix: +7.72 -> -1.25 (residual is separate small
fabric). 7 corpus certs carry electric-instantaneous DHW on off-peak; the 3
outside 0.5 all move sharply inward (MAE the win, not within-0.5 crossings).

RdSAP 10 §12 tariff routing confirmed spec-correct (Unknown meter + storage
409 -> off-peak 7-hour, Rule 2) — the bug was the DHW rate, not the tariff.
Unit-pinned in test_cert_to_inputs; RealCertExpectation 74061136 = 72.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 20:04:58 +00:00
Jun-te Kim
928ce293d0 docs(ledger): re-rank at 77.0% + triage electric-tariff & fabric tail
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:48:10 +00:00
Jun-te Kim
06a2c413f5 test(accuracy): ratchet 75.5% -> 77.0% via secondary-fuel collision fix on cost path
The gov-API `secondary_fuel_type` enums 5 (anthracite) / 9 (dual fuel) /
33 (coal) collide in VALUE with same-valued RdSAP-10 Table-32 codes for
OTHER fuels (5=bulk LPG, 9=LPG SC11F, 33=elec 10h-low). The main-fuel
boundary and the CO2/display `_secondary_fuel_cost_gbp_per_kwh` already
canonicalise these via `canonical_fuel_code`, but the SAP-DRIVING
`_fuel_cost` cascade had a SEPARATE inline secondary rate that passed the
raw enum straight to the price lookup — so the two paths diverged and the
cost path silently mis-priced.

Anthracite (enum 5) was billed at the bulk-LPG code-5 rate 12.19 p/kWh
instead of 3.64 — a 3x over-cost on the secondary that under-rated every
solid-fuel-secondary dwelling. Worst: corpus cert 100050355518 (semi,
2 extensions, anthracite room-heater secondary) lodged 36 / engine 20.8
-> 35.5. Its CO2/PE already matched lodged (the demand cascade was right)
— a pure cost-side gap, which is what localised it.

Fix: canonicalise `secondary_fuel` before the Table-32 lookup in
`_fuel_cost`, mirroring the main-fuel boundary. 55 corpus certs carry a
colliding secondary.

Gauge: within-0.5 75.5% -> 77.0%, SAP MAE 0.708 -> 0.668 (floors
ratcheted). The prior enum-9 test only guarded the display helper; new
test_fuel_cost_secondary_colliding_fuel_priced_at_canonical_rate pins the
_fuel_cost cost path directly. RealCertExpectation pinned for
100050355518 (36 = lodged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:43:31 +00:00
Jun-te Kim
a11b10f427 docs(skill): bank corpus-1000 findings (cost triangulation, software divergences, assumed-roof rule)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 19:12:48 +00:00
Jun-te Kim
c380377948 test(accuracy): ratchet 74.2% -> 75.5% via 'insulated (assumed)' roof Table-18 fix
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>
2026-07-02 19:06:26 +00:00