docs: handover post S0380.200 — 6035+0240 closed; boiler-interlock −5pp OPEN

Captures the session's window/RR/dual-main work (S0380.196–200) and the
open priority: a spec-accurate per-system boiler-interlock −5pp (Table
4c(2)) adjustment. Root cause for case 6's remaining deltas (sys-1 eff 79
not 84 + HW 4824 vs 4902) is the "room thermostat present but no cylinder
thermostat → no interlock" path that the current {2101,2102} no-interlock
rule misses. 0240 shares the controls + cylinder_thermostat=N so it will
re-pin (apply spec uniformly). Secondary: dual-system Table 4f pumps.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-03 13:21:49 +00:00
parent 8ae978a646
commit 558aaf6d09

View file

@ -0,0 +1,154 @@
# 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.196200)
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`.
---
## OPEN (the priority) — 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.30.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 <noreply@anthropic.com>` 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.