# Handover — post S0380.200 (dual-main split done; boiler-interlock −5pp OPEN) Point-in-time note. Start from [`AGENT_GUIDE.md`](AGENT_GUIDE.md) for methodology, accuracy bar, and pipeline — this records *what this session did* and *what is open*. - **Branch:** `feature/per-cert-mapper-validation` - **HEAD:** `8ae978a6` (S0380.200) - **Baseline:** `2355 passed, 1 skipped, 0 failed`. Verify with the AGENT_GUIDE §4 suite command. --- ## What this session shipped (S0380.196–200) The through-line: **golden certs 6035 and 0240 were both closed to SAP-exact** by finding real API-mapper bugs (not "lodged divergence"), each confirmed against a user-generated Elmhurst worksheet ("simulated case 5/6"). | Slice | What | Spec | |---|---|---| | **.196** | API Simplified Type 1 room-in-roof: `room_in_roof_type_1` gables (length-only, no height) weren't deducted from the A_RR shell → whole shell billed as roof at U_RR=2.30 (+52 W/K). Route them through `detailed_surfaces` (gable area = L × 2.45 default RR storey height). **6035 SAP −2→+0 exact**, PE +19.16→+1.84. | RdSAP 10 §3.9.1(e) p.21; Table 4 p.22 | | **.197** | Promoted "simulated case 5" (detached sandstone RR) to e2e fixture (`001431_case5`, 11 pins @1e-4). Fixed sandstone wall label `"SS"`→2 (`_ELMHURST_WALL_CODE_TO_SAP10`) + `_parse_thickness_mm` for "400+ mm" roof insulation (trailing `+` was dropped → u_roof fell to age default). | — | | **.198** | **API `window_wall_type=4` → roof window.** These are roof-of-room rooflights; the mapper flattened them into `sap_windows` (vertical, (27), U=2.0) instead of `sap_roof_windows` ((27a), inclined U=2.30 + 45° solar). The inclined solar dominates → **0240 SAP −1→+0 exact**, PE +3.91→+1.95; 6035 PE +1.84→+1.37. Discriminator is `wall_type==4` NOT `window_type==2` (0390/7536 lodge window_type=2 on main walls). | SAP 10.2 §3 (27a); Table 6e Note 2 | | **.199** | Site-notes mirror of .198: extractor parses "Roof of Room" window rows (`_parse_window_from_anchors`); `_is_elmhurst_roof_window` location branch; `_ELMHURST_ROOF_WINDOW_U_BY_GLAZING["Double between 2002 and 2021"]=2.30`. Case 6 pinned on §3 windows (`test_section_3_roof_windows_case6_match_pdf`): (27)=22.7408, (27a)=13.0375 exact. | RdSAP 10 §3.7 | | **.200** | **SAP 10.2 §9a two-main-heating split** (203)/(204)/(205)/(207)/(213). The cascade lumped a 2-main dwelling into one system. Now `space_heating_fuel_monthly_kwh` splits demand (204) to sys1 @ (206) + (205) to sys2 @ (207); `_solve_month` sums main_1+main_2; `_main_heating_detail_efficiency` (new, the per-detail core of `_main_heating_efficiency`) gives each system its own efficiency. Site-notes: `_map_elmhurst_main_heating_2` inherits Main 1's fuel when §14.1 omits Fuel Type. Cost/CO2/PE main_2 paths were already wired. 0240 unchanged (identical Eq-D1 systems). | SAP 10.2 §9a | Two new e2e fixtures: `001431_case5` (full SapResult, S0380.197) and `001431_case6` (§3 windows only, S0380.199 — see why below). Source PDFs tracked under `sap worksheets/golden fixture debugging/simulated case {5,6}/`; Summaries mirrored to `backend/documents_parser/tests/fixtures/Summary_001431_case{5,6}.pdf`. --- ## ⚠️ CORRECTION (post S0380.201) — the interlock priority was ALREADY DONE The "priority" below was **misdiagnosed**. At HEAD the cascade already produces case 6 (206) sys-1 eff = **79.0** and (207) sys-2 eff = **84.0**, matching the worksheet exactly. The cylinder-thermostat interlock path (`no_stored_hw_interlock = has_cylinder and cylinder_thermostat != "Y"`) has existed since **S0380.141**; the room-thermostat path since S0380.177. `no_interlock = no_room_thermostat OR no_stored_hw_interlock` — it does NOT only catch 2101/2102. Toggling case 6 `cylinder_thermostat` N→Y flips eff 0.79→0.84, confirming the −5pp fires. Golden **0240 is a combi** (`has_hot_water_cylinder=False`) → correctly NOT penalised; its predicted re-pin from the interlock is void. The misread came from `energy_requirements_section_from_cert` (a §2.4 debug helper using raw `_main_heating_efficiency`, which reports 84 — the real `cert_to_inputs` cascade applies the −5pp at ~line 6071). See [[feedback-verify-handover-claims]]. **S0380.201 landed the SECONDARY item** (dual-system aux pumps): SAP 10.2 Table 4f note c) second main-system circulation pump, gated on a lodged `main_heating_fraction > 0`. Case 6 (231) 241 → **356** EXACT (= 41 Main-1 pump + 115 Main-2 pump + 200 oil aux). 0240 re-pinned (pumps 315 → 430, integer 73 → 72, resid +0 → -1, PE +2.8092, CO2 +0.1385) — anticipated and authorised below. 000565 protected by the fraction>0 gate (its Main 2 is a DHW-only combi, fraction 0). **Remaining case-6 gaps for full-SapResult promotion** (vs P960-0001-001431): - (98c) space demand cascade **12145.31** vs ws **11991.96** (+1.28%) — living-area MIT (87) ~0.3 °C low in winter; multi-causal (gains/heat-loss). - (219) hot water cascade **4824.74** vs ws **4902.86** (−1.6%) — §4 walk needed. Once both close, promote case 6 to a full SapResult e2e fixture (pin grid below). --- ## OPEN (was the priority, now DONE) — boiler-interlock −5pp efficiency adjustment, per main system **Goal:** a RdSAP-10/SAP-10.2 **spec-accurate** implementation of the boiler interlock efficiency adjustment, applied **per main heating system**, done in the established pattern of this domain (per-line walk → cite spec → TDD → re-pin). This is the last gap blocking full closure of simulated **case 6**, and it will also re-pin golden **0240**. ### The evidence (simulated case 6) `sap worksheets/golden fixture debugging/simulated case 6/` — detached, dual **oil** boiler (both SAP code **127**, base seasonal eff **84%**), radiators 51% (control **2106**) + underfloor 49% (control **2110**). Its P960 worksheet: | line | worksheet | meaning | |---|---|---| | (206) main sys-1 eff | **79.0** | 84 − **5pp** | | (207) main sys-2 eff | **84.0** | base, no penalty | | (216) water-heater eff | **72.0** | also penalised (DHW leg of the −5pp) | | "Temperature adjustment" | 0.0000 | **flow temp has NO effect** — this is NOT a flow-temperature feature | Summary §14 lodges it explicitly: system 1 **"Boiler Interlock: No"**, system 2 **"Boiler Interlock: Yes"**. The 84→79 is the SAP 10.2 **Table 4c(2)** "no boiler interlock" −5pp **Space + DHW** adjustment (same mechanism as the AGENT_GUIDE "oil 6" worked example, S0380.177 — but that one fired off control 2101). ### Why control 2106 (which HAS a room thermostat) is "no interlock" Per RdSAP 10 boiler-interlock rules (find + cite the exact §; the existing `_NO_INTERLOCK_CONTROLS = {2101, 2102}` block in `cert_to_inputs.py` ~line 1238 quotes "RdSAP 10 §3 p.57: boiler interlock is assumed present if there is a room thermostat and [time control], AND — when there is a hot-water cylinder — a cylinder thermostat; otherwise not interlocked"): system 1 serves the **DHW cylinder**, the cylinder is present (`Hot Water Cylinder Present: Yes`) but **`Cylinder Thermostat: No`** → interlock **not** present → −5pp, *despite* the room thermostat. System 2 (underfloor, separate part, no cylinder interaction) keeps interlock via its zone control → no penalty. So the determination is **not** "control ∈ {2101,2102}". It is, per system: `interlock present` ⇔ (room thermostat present, from the control code) AND (time/programmer control) AND (cylinder absent OR cylinder thermostat present). The current cascade only catches the "no room thermostat" path (2101/2102); it misses the "room thermostat present but no cylinder thermostat" path that 2106 hits here. ### This single root cause explains BOTH remaining case-6 deltas - space heating: sys-1 eff 79 not 84 → main fuel cascade 14925 vs ws **14736.96** - hot water: the −5pp DHW leg → cascade HW 4824 vs ws **4902.86** (lower cascade fuel ⇒ cascade eff too high ⇒ missing the penalty) ### 0240 will shift — and that is correct (apply the spec uniformly) Golden **0240** has the SAME controls (sys1 2106 / sys2 2110) AND the same `cylinder_thermostat = "N"` with a cylinder present. So the spec-correct rule applies the −5pp to 0240's system 1 too. 0240 is currently SAP-exact (continuous 72.55) **without** the penalty — that is an offsetting coincidence (it's API-only, ±0.5 bar, no worksheet). Per [[feedback-software-no-special-handling]] + [[feedback-spec-floor-skepticism]]: implement the spec rule, let 0240 shift, and **re-pin** it with a documented note. Expect 0240 continuous SAP to drop ~0.3–0.5 (may take the integer 73→72; if so the golden `expected_sap_resid` moves −1 and that is the new truth). Measure precisely and re-pin PE/CO2 too. ### Where to implement (per-line walk first, then TDD) 1. **Interlock determination.** Add a per-`MainHeatingDetail` helper, e.g. `_boiler_interlock_present(main, epc) -> bool`, encoding the RdSAP 10 rule above (room thermostat from control code + cylinder-thermostat gate when a cylinder is present). `epc.sap_heating.cylinder_thermostat` ("Y"/"N") and `cylinder_size`/`hot_water_cylinder_present` are the cylinder signals. The site-notes path already lodges `cylinder_thermostat` (mapper.py ~5183, string "Y"/"N"); the API path lodges it on `sap_heating.cylinder_thermostat` (0240 = "N"). 2. **Apply Table 4c(2) −5pp per system.** The existing −5pp lives near the `_NO_INTERLOCK_CONTROLS` block — find how it currently adjusts the seasonal efficiency for 2101/2102 and generalise it to fire on `not _boiler_interlock_present(main, epc)`, applied inside `_main_heating_detail_efficiency` so **each** main system gets its own adjustment (sys1 −5, sys2 0). Confirm the DHW leg (water-heater efficiency (216)) is penalised too — the §4 water-heating cascade reads `_main_heating_efficiency`; verify the −5pp flows there (case 6 (216)=72 is the check). 3. **Verify combi vs regular rows of Table 4c(2).** The "no interlock" −5pp has a combi row (Space −5 / DHW 0) and a regular-boiler row (Space −5 / DHW −5); the DHW leg is gated on cylinder presence. Case 6 is a regular oil boiler with a cylinder → DHW −5 applies (hence (216)=72). Read the table; don't assume. ### Validation target After the fix, **promote case 6 to a full SapResult e2e fixture** (it's currently §3-windows-only because the lumped efficiency made (211)/(219)/(231) non- comparable). Case 6 worksheet Block-1 pin grid (P960-0001-001431): - SAP 72 (258), continuous **71.6597**, ECF **2.0316** (257) - total fuel cost **1162.5374** (255), CO2 **5953.6679** (272) - (211) main sys-1 fuel **7741.6458**, (213) main sys-2 fuel **6995.3106** (SapResult.main_heating_fuel_kwh_per_yr should be the sum **14736.9564**) - hot water **4902.8601** (219), lighting **357.6571** (232) - pumps/fans **356.0** (231) — **see the SECOND open item below** ### SECONDARY open item — dual-system auxiliary pumps (Table 4f) Case 6 (231) = **356** = (230c) central-heating pump 156 + (230d) oil-boiler pump 200. Cascade gives **241**. Two boilers → two pump contributions per Table 4f (note c: "where there are two main heating systems include two figures from this table" — same note already used for the 0240 oil-pump in S0380.148). Needs the per-system pump aggregation. Smaller than the interlock fix; do it after, then case 6's (231) pin closes and the full e2e fixture lands. --- ## Process notes - One slice = one commit, spec citation (page + line) in the message, `Co-Authored-By: Claude Opus 4.8 ` trailer. - AAA tests (`# Arrange/# Act/# Assert`), `abs(x-y) <= tol` (not `pytest.approx`). - New code passes `pyright` strict, 0 errors. (mapper.py + cert_to_inputs.py each carry **32 pre-existing** errors — don't add to them; check with a `git stash` baseline comparison.) - The Elmhurst worksheet is ground truth at abs=1e-4. 0240 is API-only (±0.5 fallback) — case 6 is its worksheet-backed proxy for the heating archetype, but differs from 0240 on the boiler SAP code (127 vs 0240's 130 condensing combi), so pin case 6 to ITS OWN worksheet, not 0240's register. - Suite command + section/e2e harness layout: AGENT_GUIDE §2.6 + §4.