# Handover — post Slices S0380.174..176 Branch: `feature/per-cert-mapper-validation`. **HEAD `326066ee`**. Predecessor: [`HANDOVER_POST_S0380_173.md`](HANDOVER_POST_S0380_173.md). ## TL;DR Three slices landed on the heating-systems corpus: - **`.174`** closed §4 (62)m HW for all 5 community-heating variants; CH1 HW EXACT. - **`.175`** wired §14.1 Community Heating "Heating Controls SAP" into the mapper; CH1 + CH3 SAP / cost EXACT. - **`.176`** added Table 4b combi sub-row fall-through to the (61)m default gate; **oil 3 + oil 4 FULLY EXACT on all four metrics**. **41 variants total → 34 EXACT + 7 pinned** (was 36 + 5 pre-`.174`). Pinned count drops from 9 (in `.173` handover) to 7 because oil 3 + oil 4 fully closed; CH1 + CH3 reshape from "SAP/cost-pinned" to "SAP/cost EXACT + CO2/PE pinned" pending the (372) electrical- distribution Elmhurst factor mystery. | Slice | HEAD | Scope | |---|---|---| | S0380.174 | `4876140a` | **§4 storage + primary loss for community heating.** SAP 10.2 §4 line 1482 "Heat networks": primary circuit loss for insulated pipework + cylinderstat applies. Table 2b note b verbatim system-type list ("boiler / warm air / heat pump") OMITS community heating — ×0.9 multiplier doesn't apply. Three changes: new `_HEAT_NETWORK_PIPEWORK_INSULATION_FRACTION = 1.0`, new branch in `_primary_loss_applies` (heat-network main + WHC ∈ {901, 902, 914} → True), new `_table_2b_note_b_multiplier_applies` predicate gating ×0.9 by system type. CH1 HW useful (62)m closes 2339.24 → 2658.01 EXACT vs ws; HW fuel kWh closes 3391.90 → 3854.12 EXACT; (65)m heat gains 793.51 → 1221.62 EXACT. Cost/SAP signs flipped — exposed pre-existing §7 MIT +0.46 K over-count. | | S0380.175 | `eda07d12` | **§14.1 Community Heating heating_controls_sap extraction.** All 5 CH variants lodge "Heating Controls SAP: 2306" in §14.1 (bare 4-digit form), not in §14.0 "Main Heating Controls Sap" (which is empty for CH certs). Pre-slice mapper read only §14.0 → `main_heating_control=''` → cascade defaulted to `control_type=2` (off-hours (7, 8)). Code 2306 (Table 4e Group 3) → control_type=3 (off-hours (9, 8)), which closes the +0.46 K MIT (92)m residual that `.174` surfaced. Two changes: `_elmhurst_sap_control_code` accepts bare integer form, `_map_elmhurst_sap_heating` falls through to `mh.community_heating.heating_controls_sap` when §14.0 is empty. CH1 + CH3 ΔSAP_c -1.0572 → +0.0000 EXACT; Δcost +£24.36 → -£0.00 EXACT. | | S0380.176 | `326066ee` | **Table 4b combi sub-row dispatch for (61)m default.** SAP 10.2 §4 line 7702 + Table 4b row names: codes 128/129/130 are explicit combi sub-rows ("Combi oil boiler, ..."). Pre-slice `_table_3a_combi_loss_default_applies` gated only on `main_heating_category ∈ {1, 2, 3, 6}`; Elmhurst mapper leaves the category None on Table 4b liquid-fuel boilers so the cascade fell through to (61)m=0. Added `_TABLE_4B_COMBI_OR_CPSU_CODES` fall-through (set already exists in symmetric `_primary_loss_applies` Table 4b branch — see `.146`). **oil 3 + oil 4 ALL FOUR METRICS EXACT** (ΔSAP +2.5863/+2.5603 → ±0.0000, Δcost -£62/-£57 → ±0.00, ΔPE -967/-885 → ±0.00). Cohort 9 → 7 pinned. | Extended handover suite at HEAD: **933 pass + 1 skipped, 0 fail.** Pyright net-zero on all affected files. ## Current residual state at HEAD `326066ee` ### Cascade-OK tier (41 variants — all populated corpus folders) **34 variants EXACT** on all four metrics (|ΔSAP| < 1e-3, |Δcost| < £0.01, |ΔCO2| < 0.1 kg, |ΔPE| < 0.1 kWh): ``` ashp, gshp, electric 1, 2, 3, 5, 6, 7, 8, 9, 11, 12, 13, 14, oil 1, oil 2, oil 3, oil 4, oil 5, oil pcdb 1, oil pcdb 2, oil pcdb 3, pcdb 1, pcdb 3, solid fuel 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 ``` **7 variants pinned**: | Variant | SAP code | ΔSAP_c | Δcost | ΔCO2 | ΔPE | Closure driver | |---|---:|---:|---:|---:|---:|---| | CH6 (CHP/Coal) | 302 | −7.49 | +£172.68 | −2939.67 | +7481.57 | DLF=1.0 P960 quirk + CHP credit | | oil 6 (B30K) | 126 | +3.05 | −£69.79 | −240.66 | −1112.66 | -5pp interlock penalty on non-combi | | no system | 699 | +1.18 | −£27.15 | −49.83 | −562.44 | §A.2.2 portable-electric defaults | | CH4 (CHP/Oil) | 302 | +0.53 | −£12.16 | −4401.85 | +111.58 | SAP 302 CHP credit (CO2) | | CH2 (CHP/Gas) | 302 | +0.53 | −£12.16 | −1435.09 | +1123.01 | SAP 302 CHP credit (CO2 + PE) | | CH3 (HP/Elec) | 304 | +0.0000 | −£0.00 | −98.92 | −457.54 | (372) electrical-distribution CO2/PE + (367) HP scaling | | CH1 (Boilers/Gas) | 301 | +0.0000 | −£0.00 | −23.60 | −208.23 | (372) electrical-distribution CO2/PE Elmhurst factor | ### Blocked tier (0 variants) **Empty** (since `.170`). `_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` tuple in `test_heating_systems_corpus.py` remains empty; the parametrised raise-test is `pytest.mark.skipif`'d. ## Open fronts ranked by leverage ### 1. SAP 302 CHP CO2/PE credit cascade (3 variants — CH2, CH4, CH6) Highest cohort leverage: closes ~−8 SAP-equivalent across CH6 + the big CO2 / PE residuals on CH2 / CH4 simultaneously. Spec block 13b PE (PDF p.153) + 12b CO2: ``` Space heating from CHP (307a) × 100 ÷ (362) = ... (363) less credit emissions −(307a)×(361) ÷ (362) = ... (364) Water heated by CHP (310a) × 100 ÷ (362) = ... (365) less credit emissions −(310a)×(361) ÷ (362) = ... (366) Heat from heat source 2 [(307b)+(310b)] × 100 ÷ (467b) = ... (468) ``` RdSAP 10 §C defaults: CHP overall eff 75%, heat-to-power ratio 2.0 → heat_eff 50% + electric_eff 25%; boiler eff 80%. Verified against CH2/CH4/CH6 worksheet (461)/(462) = 25% / 50% exactly. **Per-line walk caveat (unresolved).** The Elmhurst worksheet (463) energy column = spec_formula × 0.8523 uniformly across non-CHP heat-network rows. The 0.8523 multiplier appears in CH1 (467) too. Mechanism unidentified; not RdSAP 10 / SAP 10.2 spec-derived. Walk the worksheet per-line before forming hypotheses. Likely 2-3 slices: (a) CHP credit + boiler-side eff for SH; (b) mirror for HW path; (c) the 0.8523 multiplier if load-bearing. The `.172` scaling helper already keys on `_HEAT_NETWORK_HEAT_SOURCE_EFFICIENCY` — add 302 there with weighted overall eff once the split formula is in place. The `.173` predicate `_is_community_heating_hw_from_main` also gates on table membership and will pick up SAP 302 automatically. ### 2. oil 6 (B30K) −5pp interlock cascade (1 variant, ΔSAP +3.05) Per-line walk in this session: cascade Eq D1 outputs (winter, summer) = (80, 68) → annual eff ~73% at Jan; ws (217)m Jan = 73.07. Cascade gives HW kWh 3823.38 vs ws 4099.59. Back-solving the worksheet: applying -5pp interlock penalty to BOTH winter and summer ((75, 63)) reproduces ws (217)m Jan = 73.07 EXACTLY. Cascade `eq_d1_interlock_penalty_pp` is gated on `no_interlock` = "cylinder thermostat absent". For oil 6 the cert lodges `cylinder_thermostat = 'Y'` so cascade sets penalty=0. But the worksheet applies -5pp anyway — likely a different gate for non-PCDB Table 4b regular boilers vs PCDB Table 105 boilers. Probable 1-slice fix: extend the interlock-penalty gate to fire for Table 4b non-combi boiler codes (124-127) when... [need spec citation on the exact rule — investigate SAP 10.2 §9.4.11 interlock conditions for Elmhurst's interpretation]. ΔSAP_c +3.05 → ±0.0000 expected; closes 4 metrics on a single variant. ### 3. "no system" §A.2.2 portable-electric defaults (1 variant, ΔSAP +1.18) Carried over from `.169`. Cascade thinks dwelling more efficient than worksheet. Probable §A.2.2 portable-electric defaults gap (responsiveness / control / Table 11). Probably 1 slice. ### 4. CH1 / CH3 (372)/(472) electrical-distribution CO2/PE (deferred) Worksheet (372) CO2 factor = 0.1994 (block 11a, rating cascade) and 0.2114 (block 11b, demand cascade). PE factor = 1.7591 / 2.1872. These don't match any Table 12 / 12d / 12e weighting I could derive from the SH (307) or (307)+(310) heating-demand monthly profile. (313) annual = 0.01 × (307) only — confirmed across all 5 CH variants (NOT 0.01 × ((307)+(310)) as spec text says). Once a factor source is identified, cascade should add an electricity-for-heat-distribution contribution to CO2/PE for heat-network mains. Deferred this session. Either reverse-engineer the Elmhurst formula from a wider set of variants or find BRE documentation on the (372) factor convention. ### 5. CH6 DLF=1.0 lodging in P960 (architectural — pinned forever) P960 input lodges `Distribution Loss: Two adjoining dwellings sharing a single heating system` + `Distribution Loss Value: 0.0` → ws (306) = 1.0000. Summary lodges nothing distinguishing CH6 from CH4. Per spec §C3.1 the manual-DLF override is legal but the Summary doesn't carry it. Two paths: (a) extend extractor to surface §17 Additional Information when Elmhurst eventually lodges it; (b) accept as pinned. Recommendation: **(b) — pin and document**. ## Critical discipline reinforced this session **Per-line walk worksheet → spec → fix.** All three slices landed via per-line worksheet dumps confirming the spec rule before implementation: - `.174` probed ws (56)/(57)/(59) and back-solved p=1.0 + TF=0.6 from the spec literal. - `.175` traced the cascade's MIT divergence to control_type=2 vs expected 3, then back-solved from worksheet (89) util_rest = 0.9898 + (90) T_rest = 16.11 confirming off-hours (9, 8). - `.176` ran the cascade `_apply_water_efficiency` trace to find `annual=1935.37` (= (45)) being passed when ws uses (62) = 2535.37 (= (45) + (46) + (61)) — exposing the missing combi_loss. **Spec-floor skepticism cuts BOTH ways.** `.174`'s spec-correct fix EXPOSED the §7 MIT bug that pre-slice offsetting bugs had masked. The chain `.174` → `.175` followed [[feedback-software-no-special-handling]]: apply spec-correct fix uniformly; the surfaced residual is the next slice's target, not a regression. **Pin diagnoses before forming hypotheses.** The (372) Elmhurst factor 0.1994 doesn't match any Table 12 derivation. Rather than guess, session pivoted to the next-tractable front (oil 3/4 combi loss) which closed cleanly. The (372) deferred entry documents what's known and what's tried. **Don't conflate `main_heating_category` and `sap_main_heating_code`.** Two `.176` slices ago, similar Elmhurst mapper artifact: the FAME oil boilers lodge `sap_main_heating_code=128/129` but the mapper leaves `main_heating_category=None`. Cascade dispatch helpers that gate on either field must check BOTH. The `_TABLE_4B_COMBI_OR_CPSU_CODES` set already existed for the symmetric `_primary_loss_applies` branch (per `.146`); adding the same fall-through to `_table_3a_combi_loss_default_applies` was a 4-line change with exact closure on 8 metrics (oil 3 + oil 4 × SAP/cost/CO2/PE). ## Standard slice workflow (unchanged) 1. Read spec page + identify rule (or Elmhurst worksheet pattern) 2. Probe one variant; verify diagnosis via monkey-patch / direct walk 3. Write failing AAA test (literal `# Arrange / # Act / # Assert`) 4. Implement helper / dispatch entry / mapper extension 5. Re-pin affected variants (DO NOT widen tolerance) 6. Run extended handover suite (command below) 7. Pyright net-zero check (`git stash` → pyright → `git stash pop` → pyright) 8. If mirroring Elmhurst against spec literal: add a row to `SAP_CALCULATOR.md §8 "Elmhurst-mirrored spec divergences"`. The ≥2-cert rule applies unless the new divergence shares its shape with an already-documented row. 9. Commit with spec citation + `Co-Authored-By: Claude Opus 4.7 ` 10. Update `project-heating-systems-corpus` + `MEMORY.md` index ## Test baseline at HEAD `326066ee` ```bash PYTHONPATH=/workspaces/model python -m pytest \ backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \ backend/documents_parser/tests/test_heating_systems_corpus.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_heat_transmission.py \ domain/sap10_calculator/worksheet/tests/test_internal_gains.py \ domain/sap10_calculator/worksheet/tests/test_solar_gains.py \ domain/sap10_calculator/worksheet/tests/test_dimensions.py \ domain/sap10_calculator/worksheet/tests/test_rating.py \ domain/sap10_calculator/worksheet/tests/test_ventilation.py \ domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py \ domain/sap10_calculator/worksheet/tests/test_mev.py \ domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \ domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \ domain/sap10_calculator/tests/test_pcdb_table_322_lookup.py \ domain/sap10_calculator/tests/test_pcdb_table_329_lookup.py \ domain/sap10_calculator/tests/test_table_12a.py \ --no-cov -q ``` Expected: **933 pass + 1 skipped, 0 fail.** ## Memories to load (in order) ``` project-heating-systems-corpus # HEAD 326066ee feedback-sap-10-2-only-never-10-3 feedback-software-no-special-handling feedback-spec-floor-skepticism feedback-worksheet-not-api-reference feedback-spec-citation-in-commits feedback-verify-handover-claims feedback-zero-error-strict feedback-commit-per-slice feedback-aaa-test-convention feedback-e2e-validation-philosophy feedback-abs-diff-over-pytest-approx feedback-golden-residuals-near-zero feedback-one-e-minus-4-across-the-board feedback-bigger-slices-for-uniform-work reference-unmapped-sap-code reference-unmapped-api-code project-oil-price-spec-divergence ``` ## What NOT to do - **Don't reference SAP 10.3** — track 10.2 deliberately. - **Don't widen pin tolerances** — re-pin smaller or find the spec gap. - **Don't add empirical gates** to keep cohort pins stable. - **Don't re-investigate Slices .91..176** — all settled. - **Don't add new helpers to `domain/sap10_ml/`** — on deprecation path. - **Don't treat ΔSAP=0.07 as "closed"** — target is <1e-4 vs worksheet. - **Don't form a spec hypothesis without per-line data.** The (372) Elmhurst factor 0.1994 is unexplained; don't bake guesses into the cascade. Reverse-engineer with more variants first, or find BRE documentation. - **Don't conflate `main_heating_category` and `sap_main_heating_code` in cascade gates.** The Elmhurst mapper leaves `category=None` on Table 4b liquid-fuel boilers; gates must check both fields. ## Master doc The canonical architecture + API + validation doc lives at [`domain/sap10_calculator/docs/SAP_CALCULATOR.md`](SAP_CALCULATOR.md) (§8.1 + §8.2 documented). If the (372) Elmhurst-factor mystery resolves and the formula turns out to be an Elmhurst-vs-spec divergence, add §8.3. ## Good luck.