# Handover — post S0380.91..95 (party-wall + AP4/MEV + §5.14 floor + RIR insulation + Detailed-RR residual) Branch: `feature/per-cert-mapper-validation`. **HEAD `fa6974bd`**. Predecessor: [`HANDOVER_POST_S0380_90.md`](HANDOVER_POST_S0380_90.md). ## Slices committed this session (S0380.91..95) Five spec-cited slices targeting cert 000565 closure. The user clarified their primary metric is **`sap_score_continuous`** (not just integer `sap_score`), but is OK with temporary continuous-SAP drift as long as each slice closes a true spec-correct sub-component gap. Zero error is the eventual goal; achievable only when every component is spec-correct. | Slice | Commit | Spec | Cert 000565 outcome | |---|---|---|---| | **S0380.91** | `83218630` | RdSAP 10 §5.10 Table 15 row 3 (PDF p.42) — "Cavity masonry filled: 0.2 W/m²K" | party_walls 93.26 → **65.13 ✓ EXACT**. sap_score 27 → 28 (Δ-2 → -1). SH +1460 → +631 (57% closed). Continuous SAP Δ-1.16 → -0.52. New synthetic SAP10 code `WALL_CAVITY_FILLED_PARTY=11`. | | **S0380.92** | `a7894b11` | SAP 10.2 §2 (17a)/(18)/(23a)/(24c) AP4 + MEV | **sap_score 28 → 29 ✓ EXACT**. cascade (18) pressure_test_ach 2.4037 → **2.0287 ✓ EXACT** vs ws 2.0287. SH +631 → -367 (~75% closed). 5-layer slice: AP4 + MEV-decentralised plumbing across schema / extractor / mapper / cert_to_inputs. Coupling-aware bundling. | | **S0380.93** | `23aaa4fa` | RdSAP 10 §5.14 (PDF p.47) — "Floor above partially heated: 0.7 W/m²K" | BP[1] floor U: 0.76 → **0.70 ✓ EXACT**. floor_w_per_k 72.41 → 70.37 (Δ+10.74 → +8.70). sap_score 29 ✓ EXACT unchanged. Continuous SAP +0.26 → +0.30 (small drift). | | **S0380.94** | `78c57c0d` | RdSAP 10 Table 17 col 3b (PDF p.42-43) — "Stud wall PUR or PIR 400mm: 0.10 W/m²K" + extractor regex fixes | BP[2] Stud Wall 2 cascade U: 2.30 → **0.10 ✓ EXACT**. 4-layer slice: extractor regex `^\d+\+?\s*mm$` + mapper allow-list ("PUR or PIR") + canonical insulation_type "rigid_foam" + cascade `_is_rigid_foam`. roof_w_per_k 43.44 → 34.64 (closed 8.80 over-count). Continuous SAP Δ+0.26 → +0.51 (drift). | | **S0380.95** | `fa6974bd` | RdSAP 10 §3.10.1 + §3.9.1 (PDF p.21-22, p.24) — Detailed-RR residual area cascade | **thermal_bridging 116.89 → 129.35 ✓** vs ws 128.65 (Δ-11.76 → +0.70). **total_external_area 779.27 → 862.34 ✓** vs ws 857.64 (Δ-78.37 → +4.70). Big-leverage closure of the cascade -78 m² area gap. sap_score 29 → 28 transient regression (continuous crossed 28.5 → 28.07). Continuous SAP Δ+0.51 → -0.44 (absolute residual slightly closer to zero). | **Test baseline at HEAD `fa6974bd`:** 585 pass + 9 expected `000565` fails (was 574 + 9 at start of session). Cohort (000474/000477/000480/ 000487/000490/000516) + 9 golden API + 38 cohort-2 API all unaffected via discriminators (S0380.91: scoped to new code 11; S0380.92: defaulted None for cohort certs without AP4/MEV; S0380.93: floor type "Above partially heated" only on cert 000565 Ext1; S0380.94: cohort uses "Mineral or EPS" not "PUR or PIR"; S0380.95: discriminator filters out true Detailed-mode lodgements with full shell enumeration). Pyright net-zero per touched file across every slice. ## Cert 000565 state (HEAD `fa6974bd`) ### Fabric subtotals (post-S0380.95) | Component | Cascade W/K | Worksheet W/K | Δ | Notes | |---|---:|---:|---:|---| | walls | 601.22 | 604.07 | -2.85 | sub-spec | | **party_walls** | **65.13** | 65.13 | ✓ EXACT | S0380.91 | | floor | 70.37 | 61.67 | +8.70 | BP[2] Ext2 200mm insulation extractor gap | | roof | 63.72 | 51.38 | +12.34 | BP[4] FC1 + BP[1] residual (see below) | | windows | 9.60 | 11.48 | -1.88 | sub-spec | | roof_windows | 5.02 | 3.58 | +1.44 | sub-spec | | **doors** | **11.10** | 11.10 | ✓ EXACT | full pipeline plumbing | | **thermal_bridging** | **129.35** | 128.65 | +0.70 | S0380.95 | | **HTC fabric** | **966.51** | 937.06 | +29.45 | Net cascade over by ~29 W/K | ### Ventilation subtotals (post-S0380.92) | Line | Cascade | Worksheet | Δ | |---|---:|---:|---:| | (18) pressure_test_ach | 2.0287 | 2.0287 | ✓ EXACT | | (21) shelter-adj ach | 1.7244 | 1.7244 | ✓ EXACT | | mean (25)m | 2.1360 | 2.1360 | ✓ EXACT (was +0.149 over) | | mv_kind | EXTRACT_OR_PIV_OUTSIDE | (24c) | ✓ correct | | mv_system_ach | 0.5 | (23a) 0.5 | ✓ EXACT | ### SapResult pins (HEAD `fa6974bd`) | Pin | Cascade | Worksheet | Δ | Cause | |---|---:|---:|---:|---| | **sap_score (int)** | **28** | 29 | -1 | Continuous below 28.5 threshold | | sap_score_continuous | 28.07 | 28.51 | -0.44 | Cascade SH over-count drives lower SAP | | ecf | 5.43 | 5.39 | +0.05 | Downstream | | total_fuel_cost_gbp | 4720.79 | 4680.26 | +40.53 | Downstream | | co2_kg_per_yr | 6497.82 | 6447.63 | +50.20 | Downstream | | space_heating_kwh | 59541.61 | 59008.35 | **+533.26** | Cascade HTC over | | main_heating_fuel | 35031.76 | 34710.79 | +320.96 | SH × 1/COP=1.70 | | **hot_water_kwh** | 3755.03 | 3755.03 | ✓ 0 EXACT | unchanged | | lighting | 1387.02 | 1384.84 | +2.19 | sub-spec | | pumps_fans | 255.00 | 252.52 | +2.48 | MEV PCDB external data | ### Continuous SAP journey across this session | Slice | sap_score (int) | sap_score_continuous | Δ vs ws | |---|---:|---:|---:| | Pre-S0380.91 | 27 | 27.35 | -1.16 | | S0380.91 | 28 | 27.99 | -0.52 | | S0380.92 | 29 | 28.77 | +0.26 | | S0380.93 | 29 | 28.81 | +0.30 | | S0380.94 | 29 | 29.02 | +0.51 | | **S0380.95** | **28** | **28.07** | **-0.44** | User direction: continuous SAP residual is the primary metric. Temporary drift is OK when fixing real spec gaps; zero error achievable only when every component is spec-correct. ## Open work — prioritised next slices ### S0380.96 — BP[4] Flat Ceiling 1 "Unknown PUR or PIR" lodgement (highest leverage) **Spec-divergence question:** worksheet shows U=0.15 for the lodgement "Unknown thickness, PUR or PIR" → matches Table 17 col 4 (flat ceiling, PUR/PIR) at 200mm row. So Elmhurst applies a convention "Unknown thickness + known material → assumed 200mm". Per RdSAP 10 spec literal: `_u_rr_table_17` with `insulation_thickness _mm=None` falls back to `u_rr_default_all_elements` (Table 18 col 4). For age band M = 0.15. Coincidence? Or is the spec text consistent? Investigation needed: 1. Verify Elmhurst's 200mm convention against RdSAP spec edge cases 2. If "Unknown + known material" should fall back to Table 18 col 4 default, then U=0.15 for age M IS correct 3. Cert 000565 BP[4] rir_age=M → `u_rr_default_all_elements(ENG, M)` returns 0.15 (per probe). So if the cascade routes Flat Ceiling 1 through `insulation_thickness_mm=None` (not 0), it would return 0.15 ✓ Current fixture state: BP[4] Flat Ceiling 1 has `insulation_thickness _mm=0` (extractor stores "" → mapper returns 0). Worksheet expects the Table 18 col 4 fallback path (`None` → 0.15). **Cleanest fix:** when extractor sees "Unknown" in insulation cell, store it. Mapper translates "Unknown" → `insulation_thickness_mm=None` (not 0). Cascade's existing `_u_rr_table_17` handles None → `u_rr_default_all_elements`. Expected closure: - BP[4] Flat Ceiling 1 cascade U: 2.30 → 0.15 ✓ - roof_w_per_k: 63.72 → 53.97 (closes -10.75) - Then BP[1] residual +1.29 W/K remains → roof Δ ~+1.6 - Continuous SAP: -0.44 → ~ -0.10 (much closer to zero) - Integer sap_score may flip back to 29 ### S0380.97 — BP[2] Ext2 floor 200mm insulation thickness extractor **Spec citation:** RdSAP 10 §5.13 Table 20 (PDF p.47) — exposed/semi- exposed floor U-value by age band + insulation thickness. Cert 000565 Ext2 Summary §9 lodges "Insulation Thickness: 200 mm" but extractor's `_floor_details_from_lines` doesn't read it. Fixture: BP[2] ground floor `floor_insulation_thickness=None` → cascade returns 0.51 vs ws 0.22. **Slice span (multi-layer):** 1. Extractor: parse "Insulation Thickness" inside each §9 extension block 2. Schema: `FloorDetails.insulation_thickness_mm: Optional[int]` (currently has `insulation: str` only) 3. Mapper: plumb to `SapBuildingPart.floor_insulation_thickness` 4. Cascade: already reads `floor_ins_thickness` and dispatches via `u_exposed_floor` (Table 20) — no cascade change needed. Expected closure: - BP[2] floor U: 0.51 → 0.22 ✓ - floor_w_per_k: 70.37 → 61.67 ✓ EXACT vs ws (closes +8.70) - Continuous SAP: cascade SH would DROP (less heat loss) → continuous SAP UP — drifts AWAY from worksheet. Per user direction OK if spec-correct. ### S0380.98 — BP[1] residual formula refinement (lower priority) BP[1] Ext1 currently has residual +3.68 m² over worksheet (cascade 21.93 vs ws 18.25). The Simplified A_RR formula `12.5 × √(34/1.5)` gives 59.51 — minus 37.58 lodged walls = 21.93. Worksheet uses 18.25. Hypothesis: Ext1's RR height = 3.0 m (not 2.45 m assumed by formula). A height-aware formula like `A_RR = perimeter × actual_RR_height` might match. Need investigation against multiple Detailed-mode certs with non-2.45 RR heights. Impact if closed: roof -1.29 W/K (residual drops by 3.68 × 0.35). ### Deferred (unchanged from earlier handovers) - MEV PCDB Table 4f component for pumps_fans +2.5 (external data) - HP SAP code → main_heating_category=4 mapper extension - 12 gas-combi PV certs at +0.5..+1.6 PE (no worksheets) - 5 SAP-residual API-only certs (no worksheets) ## 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: **585 pass + 9 expected `test_sap_result_pin[000565-*]` fails**. The 9 expected fails (verbatim): ``` 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 ``` ## Files touched this session | File | Slices | Change | |---|---|---| | `datatypes/epc/domain/epc_property_data.py` | .92, .93 | `SapVentilation.air_permeability_ap4_m3_h_m2` + `SapVentilation.mechanical_ventilation_kind`; `SapFloorDimension.is_above_partially_heated_space` | | `datatypes/epc/surveys/elmhurst_site_notes.py` | .92 | `VentilationAndCooling.air_permeability_ap4_m3_h_m2` + `.mechanical_ventilation_type` | | `backend/documents_parser/elmhurst_extractor.py` | .92, .94 | §12.1/12.2 AP4 + MV-type parsing; `_RIR_INSULATION_THICKNESS_RE` extended to `^\d+\+?\s*mm$`; allow-list adds "PUR or PIR" | | `datatypes/epc/domain/mapper.py` | .91, .92, .93, .94 | CF→11 mapper entry; AP4 + MV-kind plumbing + `_ELMHURST_MV_TYPE_TO_KIND` + strict-raise; `_is_floor_above_partially_heated_space` helper; `_RIR_INSULATION_TYPE_TO_SAP10` adds "PUR or PIR"/"PUR"/"PIR" → "rigid_foam"; `_elmhurst_rir_insulation_thickness_mm` regex extended | | `domain/sap10_calculator/rdsap/cert_to_inputs.py` | .92 | `ventilation_from_cert` reads `air_permeability_ap4_m3_h_m2` + resolves `mechanical_ventilation_kind` name → enum; MEV sets `mv_system_ach=0.5` | | `domain/sap10_ml/rdsap_uvalues.py` | .91, .93, .94 | `WALL_CAVITY_FILLED_PARTY=11` constant + `u_party_wall` CF branch (0.2); `u_floor_above_partially_heated_space()` helper (0.7); `_RR_RIGID_FOAM_INSULATION_TYPES` adds "rigid_foam" | | `domain/sap10_calculator/worksheet/heat_transmission.py` | .93, .95 | Floor dispatch adds `is_above_partial → u_floor_above_partially_heated_space()` branch; Detailed-RR branch adds §3.10.1 residual area computation with `has_roof_lodgement` discriminator | ## Spec source quick-reference - **SAP 10.2 full specification**: `domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf` - §2 (p.12-13) — Infiltration / pressure test (AP50/AP4) — S0380.92 - §2 (p.13, 133) — MEV (23a/24c) — S0380.92 - **RdSAP 10 specification**: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf` - §3.9.1 (p.21-22) — Simplified A_RR formula — S0380.95 - §3.10.1 (p.24) — Detailed RR residual area — S0380.95 - §5.10 Table 15 (p.42) — Party-wall U-values — S0380.91 - §5.14 (p.47) — Floor above partially heated — S0380.93 - Table 17 col 3b (p.42-43) — Stud wall PUR/PIR — S0380.94 - **SAP 10.3 at** `sap-10-3-full-specification-2026-01-13.pdf`: **DO NOT reference** ([[feedback-sap-10-2-only-never-10-3]]) ## Memory updated this session - `project_cert_000565_recovery_state` — slice-by-slice closure table for .91..95 + remaining open-work analysis - `MEMORY.md` — index entry refreshed at HEAD `fa6974bd` ## What NOT to do - **Don't reference SAP 10.3** ([[feedback-sap-10-2-only-never-10-3]]). - **Don't widen pin tolerances or xfail residual gaps** ([[feedback-zero-error-strict]]). The 9 cert 000565 fails are the work queue. - **Don't re-investigate any closed work**: party-wall CF (.91), AP4/MEV (.92), §5.14 partially-heated (.93), RIR PUR or PIR (.94), §3.10.1 residual area (.95). All settled. - **Don't add new helpers to `domain/sap10_ml/`** — that folder is on the deprecation path per [[project-sap10_ml-deprecation]]. New cascade helpers should land under `domain/sap10_calculator/`. (Editing existing sap10_ml files for incremental fixes is fine.) - **Don't avoid spec-correct closures because continuous SAP drifts away** — user explicitly OK'd transient drift. Zero error achievable only when every component is spec-correct. ## Memory hygiene After the next slice, update: - `project_cert_000565_recovery_state` — append slice closure + refresh the open work-items table - `MEMORY.md` — refresh HEAD + one-line summary Good luck.