# Handover — post S0380.77..80 + cert 000565 §4 HW cascade fully spec-correct Branch: `feature/per-cert-mapper-validation`. **HEAD `760a893c`**. Predecessor: [`HANDOVER_POST_S0380_76.md`](HANDOVER_POST_S0380_76.md). ## Slices committed this session (S0380.77..80) Four spec-cited slices closed the entire §4 HW cascade for cert 000565 from +1399 kWh HW pin to **EXACT**. | Slice | Commit | Spec | Cert 000565 closure | |---|---|---|---| | **S0380.77** | `a33904c5` | SAP 10.2 §4 line 7700 + Table 3 (p.159) — primary loss applies to the heat generator that feeds the cylinder, not the space-heating main. WHC 914 routes the gate to the DHW main. | (59)m EXACT | | **S0380.78** | `509ef4fb` | SAP 10.2 Appendix J §J2 step 2a (p.81) bath formula + §10a line (247a) (p.145) electric-shower cost. Coupled fixes: §1x.0 section-bounded shower extractor + (247a) added to fallback `total_cost`. | (45)m EXACT | | **S0380.79** | `f9551355` | SAP 10.2 §4 line 7693 (p.137) `(57)m = (56)m × (V−Vs)/V` solar adjustment + Table 2b note b) + RdSAP §3 (p.57) `_separately_timed_dhw=True` when cylinder lodged. | (57)m EXACT, (62)m EXACT | | **S0380.80** | `760a893c` | SAP 10.2 Table 4c (p.169) "No boiler interlock — regular boiler: DHW −5%" + RdSAP §3 (p.57) boiler interlock definition. Combi-fed cylinder + cyl-stat absent → −5pp DHW efficiency. | **hot_water_kwh EXACT** | **Test baseline at HEAD `760a893c`:** 551 pass + 9 expected `test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on every touched file. ## Cert 000565 state (HEAD `760a893c`) | Pin | Cascade | Worksheet | Δ | Cause | |---|---:|---:|---:|---| | **sap_score (int)** | **28** | **29** | **−1** | Rounding boundary; continuous SAP 28.4680 lands 0.041 below 28.5 cutoff | | sap_score_continuous | 28.4680 | 28.5087 | −0.041 | Downstream of total_cost +£3.62 (deferred ADR-0010 gas tariff) | | ecf | 5.3910 | 5.3866 | +0.004 | Downstream of total_cost | | total_fuel_cost_gbp | 4683.88 | 4680.26 | +3.62 | **Deferred ADR-0010 gas tariff** (Table 12 £0.0364 vs Table 32 £0.0348) | | co2_kg_per_yr | 6438.71 | 6447.63 | −8.92 | Lighting + Main-1 small CO2 factor residual | | **space_heating_kwh** | **58936.06** | **59008.35** | **−72.29** | **RR fold-in (RdSAP §3.10 detailed-RR geometry)** | | main_heating_fuel | 34668.27 | 34710.79 | −42.52 | Follows space_heating via 1/COP | | **hot_water_kwh** | **3755.03** | **3755.03** | **✓ 0 EXACT** | §4 cascade fully closed | | lighting | 1387.02 | 1384.84 | +2.19 | Sub-spec | | pumps_fans | 255.00 | 252.52 | +2.48 | MEV PCDB record missing | ### §4 HW cascade line refs all EXACT | Line | Cascade | Worksheet | Δ | |---|---:|---:|---:| | (45)m sum energy_content | 1286.3266 | 1286.3266 | ✓ 0 | | (46)m sum distribution_loss | 192.9490 | 192.95 | ✓ <1e-3 | | (57)m sum solar_storage | 596.9725 | 596.9725 | ✓ <1e-4 | | (59)m sum primary_loss | 1176.77 | 1174.79 | +1.98 (sub-2 kWh rounding) | | (61)m combi_loss | 0.00 | 0.00 | ✓ 0 | | (62)m sum total_demand | 3060.07 | 3060.07 | ✓ <1e-3 | | (64)m sum (after solar) | 2778.72 | 2778.7213 | ✓ <1e-4 | | (64a)m electric shower | 702.94 | 702.94 | ✓ <1e-4 | | (217)m water-heater eff | 0.74 | 0.74 | ✓ EXACT | | (219) HW fuel kWh | 3755.03 | 3755.0288 | ✓ <1e-3 | The §4 line-by-line trace is the most diagnostically transparent state cert 000565 has been in. Any future cascade refactor should be validated by checking each line stays EXACT, not just the SapResult-level pins. ## Slice S0380.77 — primary loss WHC 914 routing **Bug:** `_primary_loss_override(epc, main, primary_age)` was called with `main = _first_main_heating(epc)` (Main 1 = HP for cert 000565). The `_primary_loss_applies` gate then keyed off the HP's category = None, returned False, and (59)m was zeroed despite the cert having an external cylinder fed by Main 2 (gas combi). **Spec:** SAP 10.2 §4 line 7700 + Table 3 (PDF p.159): > Primary circuit loss applies when hot water is heated by a heat generator > (e.g. boiler) connected to a hot water storage vessel via insulated or > uninsulated pipes (the primary pipework). The eligibility is determined by the heat generator that feeds the cylinder — for cert 000565 that's Main 2 (gas combi via WHC 914), not Main 1 (HP). **Fix:** `_primary_loss_override` resolves its `main` via `_water_heating_main(epc)` (the WHC-914 resolver) rather than `_first_main_heating`. Signature drops the `main` parameter. Test: `test_whc_914_dhw_routes_primary_loss_gate_to_second_main_heating_per_sap_table_3`. ## Slice S0380.78 — §1x.0 shower extractor + (247a) fallback cost **Bug A (extractor):** `_extract_baths_and_showers` used `self._lines.index("Connected")` (global search) to anchor the shower roster. Cert 000565 lodges 4 extensions whose §3 building-parts list contains "Connected" / "Exposed" / "Sheltered" wall elevation flags earlier in the document. The global match landed on a wall row; the digit-check `num_line.isdigit()` failed on "0.00" and the shower list came back empty. **Bug B (calculator fallback):** `calculator.py` STANDARD-tariff path already plumbed `instant_shower_cost_gbp` via `fuel_cost(...)`. The fallback scalar path for TEN_HOUR / `_ZERO_FUEL_COST_RESULT` certs was silently dropping `electric_shower_kwh × other_fuel_cost` from total cost. Cert 000565 (Dual-meter TEN_HOUR + 1 electric shower) trips this branch — fix A surfaced the £93/yr under-count. **Spec:** - SAP 10.2 Appendix J §J2 step 2a (p.81): `N_bath = 0.13 N + 0.19` when shower also present; `0.35 N + 0.50` when no shower. 2.7× swing. - SAP 10.2 §10a (p.145): `Energy for instantaneous electric shower(s) (64a) × 0.01 = (247a)` — feeds (255) total cost. **Fixes:** - `_extract_baths_and_showers` routes the "Connected" lookup through `_section_lines("1x.0 Baths and Showers", "18.0 Flue Gas Heat Recovery System")`. Both anchors are single-occurrence in the Elmhurst Summary PDF schema. - `calculator.py` fallback `total_cost` adds `inputs.electric_shower_kwh_per_yr × inputs.other_fuel_cost_gbp_per_kwh`. Tests: - `test_summary_000565_extractor_finds_electric_shower_in_section_1x_0` - `test_total_fuel_cost_includes_247a_electric_shower_in_fallback_path` ### Why coupled Splitting the fixes would flip sap_score from 29 → 30 mid-state: the extractor fix corrects (45)m to EXACT but exposes (64a) electric-shower kWh, which without (247a) cost flow makes total_cost too low → ECF too low → SAP rating too high. Bundling keeps sap_score within rounding. ## Slice S0380.79 — (57)m solar storage + separately_timed_dhw cylinder default **Bug A:** `_cylinder_storage_loss_override` returned raw (56)m as `solar_storage_monthly_kwh_override`. SAP 10.2 §4 (62)m formula uses (57)m (the solar-adjusted storage loss), not (56)m. For cert 000565 with solar HW + combined cylinder, (62)m was over-counting by (56)m × Vs/V ≈ 395 kWh/yr. **Bug B:** `_separately_timed_dhw` gated only on `main.main_heating_category == 4` (heat pumps), returning False for boiler-family + cylinder configs. Cert 000565 (gas combi + cylinder + no cyl-stat) fell through to TF = 0.78; worksheet uses 0.702 (with the 0.9 multiplier for separately-timed DHW). 10% TF over-count drove +98 kWh into (56)m. **Spec:** - SAP 10.2 §4 line 7693 (p.137): ``` If the vessel contains dedicated solar storage or dedicated WWHRS storage, (57)m = (56)m × [(47) - Vs] ÷ (47), else (57)m = (56)m where Vs is Vww from Appendix G3 or (H12) from Appendix H. ``` - SAP 10.2 Table 2b note b) (p.159): "Multiply Temperature Factor by 0.9 if there is separate time control of domestic hot water (boiler systems, warm air systems and heat pump systems)". - RdSAP 10 §3 (p.57) default table "Hot water separately timed": ``` No programmer, pre-1998 boiler: - No Programmer, pre-1998 boiler: - Yes Post-1998 boiler: - Yes ``` **Fixes:** - `_cylinder_storage_loss_override`: when `epc.solar_water_heating`, return `(56)m × (V−Vs)/V`. Vs = `round(volume_l × ⅓)` per S0380.76's combined-cylinder convention. - `_separately_timed_dhw(epc, main)`: signature gains `epc`; returns True when a cylinder is lodged in addition to the existing HP branch. Tests: - `test_cylinder_storage_loss_applies_57m_solar_adjustment_per_sap_4_line_7693` ### Cross-cohort impact — cert 0390 pin update Golden cert `0390-2954-3640-2196-4175` (Firebird oil combi PCDF 9005 + 160 L cylinder + cyl-stat=Y) was previously flagged at SAP residual −7 with the comment "traces to fabric heat-loss / oil-fuel cost cascade rather than the §4 HW path". That diagnosis was wrong: cert 0390's §4 HW cascade WAS applying TF = 0.60 instead of TF = 0.54 — `cyl-stat=Y` + programmer-present default → separately_timed=True per RdSAP §3, which the cohort heuristic was missing. Pin updated −7 → −6 per [[feedback-golden-residuals-near-zero]]. ## Slice S0380.80 — Table 4c −5% DHW for missing boiler interlock **Bug:** Cascade water-efficiency for cert 000565 used PCDB summer η = 79% directly. Worksheet uses (217)m = 74%. Investigation in this session resolved the −5pp gap to SAP 10.2 Table 4c. **Spec:** SAP 10.2 Table 4c (p.169-170): ``` (2) Efficiency adjustment due to control system Space DHW No boiler interlock - regular boiler (...) −5 −5 No boiler interlock - combi −5 0 Note c): These do not accumulate as no thermostatic control or presence of a bypass means that there is no boiler interlock. ``` RdSAP 10 §3 (p.57) "Boiler interlock" definition: > Assumed present if there is a room thermostat and (for stored hot > water systems heated by the boiler) a cylinder thermostat. Otherwise > not interlocked. A PCDB-listed boiler feeding a cylinder without a cylinder thermostat has no boiler interlock → −5pp DHW. A combi-fed cylinder routes the boiler as a regular boiler for the DHW circuit (instantaneous-DHW capability is bypassed), so the regular-boiler row (DHW −5%) applies. **Fix:** `cert_to_inputs.py` water-efficiency branch: ```python if ( epc.has_hot_water_cylinder and epc.sap_heating.cylinder_thermostat != "Y" and water_pcdb_main is not None ): water_eff -= 0.05 ``` Test: `test_table_4c_no_boiler_interlock_applies_minus_5_dhw_adjustment_when_cylinder_lodged_without_thermostat`. **Effect on other certs:** - Combi-only certs (no cylinder): condition fails → no change. - ASHP cohort certs: water_pcdb_main is None (HP not in Table 105) → no change. - Boiler + cylinder + cyl-stat=Y certs (e.g. cert 0390): cyl-stat present → condition fails → no change. - Boiler + cylinder + cyl-stat=N + PCDB Table 105 record: −5% applies. Only cert 000565 in the current test suite has this shape. ## Why sap_score=28 (not 29) at HEAD `760a893c` S0380.80 closes the cascade to spec-correct values. The remaining deviation is a documented **deferred** gap: ``` worksheet HW cost = 3755.0288 × £0.0348/kWh = £130.6750 (RdSAP Table 32) cascade HW cost = 3755.0288 × £0.0364/kWh = £136.6831 (SAP 10.2 Table 12) ---------- Δ = +£6.01 ``` The £0.16/100 gas-price delta inflates HW cost by ~£6, exactly the total_fuel_cost residual (+£3.62 net after smaller offsets) AND the continuous SAP deviation (+0.041). ECF = cost / (TFA + 45) is the forcing function: lower cost → higher SAP rating; higher cost → lower SAP rating. The cascade is pricing UP, so SAP rating drops below the 28.5 integer boundary. **Fix is the deferred ADR-0010 cohort-wide repricing**, not a single- cert patch (see Open thread #6 below). After ADR-0010 lands, projected cert 000565 sap_score = **29 ✓ EXACT** (continuous projected at ≈ 28.51, well within rounding of worksheet 28.5087). ## Open work — prioritised next slices ### #1 (largest) — ADR-0010 mains gas tariff Table 32 vs Table 12 **Magnitude:** Cohort-wide. For cert 000565: +£3.62 cost → +0.041 continuous SAP → flips sap_score 29→28. For other gas-DHW certs: similar order-of-magnitude. **Cascade:** Uses SAP 10.2 Table 12 prices (£0.0364/kWh mains gas). Worksheet uses RdSAP 10 Table 32 (£0.0348/kWh). **Tractability:** Requires ADR-0010 amendment + coordinated cohort re- pin (every golden + Elmhurst worksheet cert's pinned cost shifts). NOT a single-cert slice. **Suggested approach:** 1. Read ADR-0010 to understand the current price-table decision and what's blocking the switch to Table 32. 2. Identify which Elmhurst worksheets in the cohort actually use Table 32 (the U985 ones definitely do). 3. Stand up a parallel `RDSAP10_TABLE_32_PRICES` constant alongside `SAP_10_2_SPEC_PRICES`. 4. Re-pin all golden + Elmhurst e2e expectations under Table 32. 5. ADR-0010 amendment commit that frames the policy decision. This is the highest-leverage single change for the Elmhurst worksheet cohort. After this lands, cert 000565 → sap_score 29 EXACT, plus likely several other open-residual certs close. ### #2 — RR (room-in-roof) fold-in for cert 000565 space_heating −72 **Magnitude:** −72 kWh space_heating (cert 000565) → −42 kWh main_heating_fuel via 1/COP. **Cascade:** Doesn't fully implement RdSAP §3.10 detailed-RR geometry + area formula. Cert 000565 has RR on every part (5 BPs) with detailed gable wall lengths, slopes, common walls. **Tractability:** Single-cert slice, but needs spec-citation work in the heat_transmission cascade. The detailed-RR area formula is in RdSAP §3.10 (PDF p.30-35, "Room in roof"). ### #3 — Lighting CO2 factor Δ−0.0025 (tariff-blended Table 12d) **Magnitude:** −3.16 kg CO2 (lighting) and similar Δ−0.0025 on pumps_fans CO2 factor. Same cause: cascade uses code 30 (standard electricity) Table 12d factors; worksheet uses TEN_HOUR Grid 1 blend of codes 33 (10h low) + 34 (10h high). **Cascade:** `lighting_co2_factor_kg_per_kwh=_effective_monthly_co2_factor( lighting_monthly_kwh, _STANDARD_ELECTRICITY_FUEL_CODE)` at `cert_to_inputs.py:4054`. Same shape for pumps_fans at line 4050. **Tractability:** Clean spec citation. Mirror what S0380.65 did for main_heating_co2_factor (Table 12a Grid 1 high/low blend) for lighting and pumps_fans. Only affects off-peak tariff certs (cert 000565 is the only Elmhurst worksheet fixture on Dual-meter; cohort-2 has some). ### #4 — MEV pumps_fans +2.48 (PCDB MEV table missing) **Magnitude:** +2.48 kWh pumps_fans (cert 000565). PCDB MEV record table not in the repo (cert lodges PCDF 500755). External-data acquisition gates this; not solvable in code. ### #5 — HP SAP code → main_heating_category=4 in mapper Cert 000565 Main 1 has sap_main_heating_code=224 but no PCDB Table 362 ref → mapper sets category=None. The TODO in `mapper.py:_elmhurst_main_heating_category` says this is deferred because of HP-on-E7 cost cascade + Table 4f MEV component coupling. Now that S0380.80 has surfaced the cleaner cascade, the coupling cost analysis can be redone. Couples with #4 (MEV). ### #6 — 12 gas-combi PV certs at +0.5..+1.6 PE Unchanged from prior handover. No worksheets available; re-pinned at current residuals. ### #7 — 5 SAP-integer-residual certs Unchanged. All API-only (no worksheets). User has agreed not to chase these without worksheet ground truth. ## How to run the baseline ```bash PYTHONPATH=/workspaces/model python -m pytest \ backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \ backend/documents_parser/tests/test_elmhurst_extractor.py \ backend/documents_parser/tests/test_elmhurst_end_to_end.py \ domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \ domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py \ domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \ domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \ --no-cov -q ``` Expected: **551 pass + 9 expected `test_sap_result_pin[000565-*]` fails** at HEAD `760a893c`. The 9 expected fails (verbatim from the latest run): ``` sap_score sap_score_continuous ecf total_fuel_cost_gbp co2_kg_per_yr space_heating_kwh_per_yr main_heating_fuel_kwh_per_yr lighting_kwh_per_yr pumps_fans_kwh_per_yr ``` `hot_water_kwh_per_yr` was the 10th fail in baselines `a532f75d` through `f9551355`; now passes at HEAD `760a893c`. ## Files touched this session | File | Slices | Change | |---|---|---| | `backend/documents_parser/elmhurst_extractor.py` | S0380.78 | `_extract_baths_and_showers` uses `_section_lines("1x.0 Baths and Showers", "18.0 Flue Gas Heat Recovery System")` instead of `self._lines.index("Connected")` | | `backend/documents_parser/tests/test_summary_pdf_mapper_chain.py` | S0380.78 | New test `test_summary_000565_extractor_finds_electric_shower_in_section_1x_0` | | `domain/sap10_calculator/calculator.py` | S0380.78 | Fallback scalar `total_cost` adds `electric_shower_kwh × other_fuel_cost` | | `domain/sap10_calculator/tests/test_calculator.py` | S0380.78 | New test `test_total_fuel_cost_includes_247a_electric_shower_in_fallback_path` | | `domain/sap10_calculator/rdsap/cert_to_inputs.py` | S0380.77, S0380.79, S0380.80 | `_primary_loss_override` resolves DHW main internally; `_separately_timed_dhw(epc, main)` cylinder-default; `_cylinder_storage_loss_override` applies (57)m solar adjustment; water_eff `−= 0.05` for Table 4c boiler-interlock | | `domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py` | S0380.77, S0380.79, S0380.80 | 3 new tests pinning the spec rules above | | `domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py` | S0380.79 | Cert 0390 pin updated −7 → −6 with revised notes citing S0380.79 | ## Spec source quick-reference - **SAP 10.2 full specification**: `domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf` - Appendix J §J2 step 2a (bath formula): p.81 - §4 (45)..(65) HW worksheet: p.135-137 - §4 line 7693 (57)m solar adjustment: p.137 - §4 line 7700 + Table 3 (primary loss): p.159 - §10a (245)..(255) cost worksheet: p.145 - Table 2 (HW storage loss factor): p.158 - Table 2b (HW storage loss temperature factor + notes a/b): p.159 - Table 3 (primary circuit loss): p.159 - Table 3a (combi loss): p.160 - Table 4b (gas/oil boiler seasonal efficiency): p.168 - Table 4c (efficiency adjustments — boiler interlock, etc.): p.169-170 - Appendix D2.1 (using PCDB efficiency values): p.57 - Appendix D2.2 (condensing boiler corrections): p.58 - **RdSAP 10 specification**: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf` - §3 default table (boiler interlock, separately timed DHW, pipework insulation): p.57 - §10.11 Table 29 (solar panel defaults): p.58 - **S10TP-12** (BRE seasonal efficiency of condensing boilers): `domain/sap10_calculator/docs/specs/sap10 technical papers/S10TP-12 - Seasonal efficiency of condensing boilers - V1.2.pdf` - **SAP 10.3** at `domain/sap10_calculator/docs/specs/sap-10-3-full-specification-2026-01-13.pdf`: **DO NOT reference** (project tracks 10.2 only per [[feedback-sap-10-2-only-never-10-3]]) ## Memory updated this session - `project_cert_000565_recovery_state` — full S0380.77/78/79/80 history, cumulative closure table, attribution of remaining residuals to deferred ADR-0010 gas tariff - `MEMORY.md` — index entry refreshed