# Handover — post S0380.96..103 (RIR Unknown + §9 floor extractor + MEV PCDB arc + HP-on-E7 cost split) Branch: `feature/per-cert-mapper-validation`. **HEAD `e3abe9b2`**. Predecessor: [`HANDOVER_POST_S0380_95.md`](HANDOVER_POST_S0380_95.md). ## Slices committed this session (S0380.96..103) Eight spec-cited slices. The first two closed remaining cert 000565 extractor/mapper gaps (RIR "Unknown" insulation + floor §9 "Insulation Thickness"). Slices .98..102 built the MEV PCDB decentralised cascade arc end-to-end (Tables 322 + 329 + SFPav formula + HP-category mapper + wiring). The final slice .103 closed the Table 12a Grid 2 MEV-fan cost split, completing the HP-on-E7 cost cascade. | Slice | Commit | Spec | Cert 000565 outcome | |---|---|---|---| | **S0380.96** | `32a4cf20` | RdSAP 10 §3.10.1 (PDF p.24) — "Unknown" insulation → Table 18 col 4 age-band default | BP[4] FC1 cascade U: 2.30 → **0.15 ✓ EXACT**. roof_w_per_k Δ +12.34 → +1.59 (closed -10.75). Continuous SAP Δ -0.44 → -0.20. | | **S0380.97** | `7121a86b` | RdSAP 10 §5.13 Table 20 (PDF p.47) — exposed/semi-exposed floor U by age × thickness | BP[2] Ext2 floor U: 0.51 → **0.22 ✓ EXACT**. floor_w_per_k **✓ EXACT**. **sap_score 28 → 29 ✓ EXACT**. Continuous SAP Δ -0.0001 (within 1e-4). | | **S0380.98** | `b3330821` | PCDF Spec Rev 6b §A.19 — PCDB Table 322 Format 427/428 | Foundation only. Typed parser + ETL + `decentralised_mev_record(pcdb_id)` lookup. 48 records ingested. No cascade integration. | | **S0380.99** | `433f4a49` | PCDF Spec Rev 6b §A.20 — PCDB Table 329 Format 430/432 | Foundation only. Typed parser + ETL + `mv_in_use_factors_record(system_type)`. 5 records ingested. No cascade integration. | | **S0380.100** | `44fb8c07` | SAP 10.2 §2.6.4 equation (1) + Table 4f line (230a) | New module `worksheet/mev.py` — `mev_sfp_av` + `mev_decentralised_kwh_per_yr` pure helpers. AAA tests pin cert 000565 worksheet values. No cascade integration. | | **S0380.101** | `1b183f9c` | SAP 10.2 Table 4a (PDF p.165) — Heat-pump category 4 | HP SAP codes 211-227 / 521-527 → `main_heating_category=4` in `_elmhurst_main_heating_category` (Elmhurst path). Cert 000565 Main 1 (SAP 224) flipped None→4. Transient regression: pumps_fans 255 → 125 (offset bug exposed). | | **S0380.102** | `a0413155` | SAP 10.2 §2.6.4 + Table 4f (230a) — Wire MEV cascade into `_table_4f_additive_components` | **pumps_fans_kwh_per_yr 255 → 252.5159 ✓ EXACT**. Schema + extractor + mapper for MV PCDF index / wet rooms / duct type. Elmhurst fan-count convention reverse-engineered from cert 000565 (TODO: validate on a 2nd MEV cert). | | **S0380.103** | `e3abe9b2` | SAP 10.2 Table 12a Grid 2 (PDF p.191) — `FANS_FOR_MECH_VENT` blended rate on off-peak | MEV-fan cost weighting: 127.5 kWh at 11.6644 p/kWh + 125 kWh at 13.2440 p/kWh → effective 12.4467 p/kWh. cost Δ +£0.39 → -£1.62 (sign flipped; SH cascade residual exposed). | **Test baseline at HEAD `e3abe9b2`:** 597 pass + 7 expected `000565` fails (was 585 + 9 at start of session, with .96+.97 closing the sap_score integer fail and .102 closing the pumps_fans fail). The ETL test count grew by ~25 with the new PCDB tables. Pyright net-zero per touched file across every slice. ## Cert 000565 state (HEAD `e3abe9b2`) ### Fabric subtotals | Component | Cascade W/K | Worksheet W/K | Δ | Status | |---|---:|---:|---:|---| | walls | 601.22 | 604.07 | -2.85 | sub-spec | | **party_walls** | **65.13** | 65.13 | ✓ EXACT | S0380.91 | | **floor** | **61.67** | 61.67 | ✓ EXACT | S0380.97 | | roof | 52.97 | 51.38 | +1.59 | residual +1.29 BP[1] formula | | 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 | | **total external area** | **862.34** | 857.64 | +4.70 | S0380.95 | ### SapResult pins (HEAD `e3abe9b2`) | Pin | Cascade | Worksheet | Δ | Status | |---|---:|---:|---:|---| | **sap_score (int)** | **29** | 29 | **✓ EXACT** | S0380.97 | | sap_score_continuous | 28.5269 | 28.5087 | +0.0182 | SH cascade-driven | | ecf | 5.3850 | 5.3866 | -0.0016 | SH cascade-driven | | total_fuel_cost_gbp | 4678.6372 | 4680.2593 | -1.6221 | SH cascade-driven | | co2_kg_per_yr | 6445.8198 | 6447.6263 | -1.8065 | mix: CO2 MEV split + SH | | space_heating_kwh_per_yr | 58980.8225 | 59008.3499 | -27.5274 | §3-§8 cascade gap | | main_heating_fuel_kwh_per_yr | 34694.6015 | 34710.7941 | -16.1926 | downstream of SH | | **hot_water_kwh_per_yr** | 3755.0288 | 3755.0288 | ✓ 0 EXACT | unchanged | | lighting_kwh_per_yr | 1387.0237 | 1384.8353 | +2.1884 | sub-spec | | **pumps_fans_kwh_per_yr** | **252.5159** | 252.5159 | **✓ 0 EXACT** | S0380.102 | ### Continuous SAP journey across this session | Slice | sap_score (int) | sap_score_continuous | Δ vs ws | |---|---:|---:|---:| | Pre-S0380.96 | 28 | 28.07 | -0.44 | | S0380.96 | 28 | 28.31 | -0.20 | | **S0380.97** | **29** | **28.5086** | **-0.0001** (within 1e-4!) | | S0380.98..100 | 29 | 28.5086 | -0.0001 (no cascade change) | | S0380.101 | 29 | 28.6942 | +0.1855 (transient — HP cat=4 only, MEV not yet wired) | | S0380.102 | 29 | 28.5043 | -0.0044 (MEV wired, restored balance) | | **S0380.103** | 29 | **28.5269** | **+0.0182** (MEV cost split exposed pre-existing SH residual) | Per user direction [[feedback-spec-floor-skepticism]] + [[feedback-spec-floor-skepticism]]: each slice closed a true spec- correct intermediate-value bug. The continuous-SAP residual is now driven by a §3-§8 SH cascade under-count (main_heating_fuel -16 kWh) that was previously masked by the +£2.01 pumps_fans cost over-count. ## Open work — prioritised next slices ### S0380.104 — Investigate §3-§8 space-heating cascade -27 kWh **The current biggest residual driver.** main_heating_fuel_kwh is -16.19 kWh under ws (34694.60 vs ws 34710.79) → SH cost £1.58 under ws → continuous-SAP +0.0182 OVER ws. Possible causes: 1. **Heat transmission HLC residual** — fabric subtotals net to net ~+29 W/K (post-S0380.95 fabric snapshot). Walls -2.85, roof +1.59, thermal_bridging +0.70, total_external_area +4.70. Roof BP[1] residual formula gap (+1.29 W/K, deferred from S0380.95) is the largest single localised item. 2. **Internal gains** — pumps_fans gains contribution changes with HP cat=4 path; verify against ws line (70) by month. 3. **Solar gains / utilization factor** — sub-spec window U-values leak into solar gains too. 4. **Mean internal temperature / per-month solve** — possible convergence-loop tolerance issue on this multi-BP cert. **Approach:** probe per-month `space_heat_requirement_kwh` vs ws line (98c)m to localise. The cohort certs (000474..000516) hit SH at 1e-4 so the §8 orchestrator IS correct on simpler dwellings — something cert-000565-specific (Detailed-RR + multi-BP + HP + MEV + FGHRS + solar HW + draught lobby) is the differentiator. Expected closure: continuous SAP +0.0182 → within 1e-4. ### S0380.105 — CO2 cascade MEV split (Table 12d monthly factors) Mirror of S0380.103 for CO2. Cert 000565 worksheet line (267): Pumps, fans and electric keep-hot 252.5159 × 0.1412 = 35.3349 Cascade `pumps_fans_co2_factor_kg_per_kwh = 0.14116` (kWh-weighted Table 12d monthly factor for code 30) → 35.6453 kg → +0.31 over ws. Cause: cascade uses a single Table 12d profile across all pumps_fans kWh. MEV fans have a different MONTHLY DISTRIBUTION than central- heating pumps + flue fans (MEV runs year-round at 0.5 ach; pumps run heating season only). The worksheet integrates separately. **Slice scope:** add `MevFanEntry`-style CO2 helper + new `pumps_fans_co2_factor_kg_per_kwh` resolution that weights the two streams. Impact: -0.31 kg/yr → continuous SAP downstream. ### S0380.106 — PE cascade MEV split (Table 12e monthly factors) Mirror of S0380.105 for primary energy. Analogous structure. ### S0380.107 — BP[1] residual formula refinement (roof) 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). ### S0380.108 — Lighting +2.19 kWh trace residual Cascade 1387.02 vs ws 1384.84. Sub-spec but breaks 1e-4 strict pin. §5 Appendix L lighting cascade. Likely a per-cert-lodging gap (bulb count, fixed/non-fixed lighting fraction). ### Deferred (unchanged from earlier handovers) - 12 gas-combi PV certs at +0.5..+1.6 PE (no worksheets) - 5 SAP-residual API-only certs (no worksheets) ## MEV PCDB arc — architecture summary The S0380.98..103 arc landed the entire MEV decentralised cascade end-to-end. Architecture (in dependency order): ``` PCDB pcdb10.dat ↓ ETL (etl.py, parser.py) Table 322 (per-fan SFP) ←→ Table 329 (per-ducting IUF) ↓ runtime lookups (__init__.py) decentralised_mev_record(pcdb_id) + mv_in_use_factors_record(system_type) ↓ worksheet/mev.py — pure helpers mev_sfp_av(fan_entries) → §2.6.4 equation (1) avg SFP mev_decentralised_kwh_per_yr(sfp_av, V) → Table 4f line (230a) kWh ↓ cert_to_inputs.py _mev_decentralised_kwh_per_yr_from_cert(epc) — composer reads epc.mechanical_ventilation_index_number, .wet_rooms_count, .mechanical_vent_duct_type builds Elmhurst per-fan count distribution invokes mev.py helpers ↓ _table_4f_additive_components(epc) adds the MEV contribution ↓ pumps_fans_kwh_per_yr final cascade output For COST (S0380.103): _pumps_fans_fuel_cost_gbp_per_kwh(tariff, mev_kwh, total_pumps_fans_kwh) → kWh-weighted blended rate (FANS_FOR_MECH_VENT vs ALL_OTHER_USES) CalculatorInputs.pumps_fans_fuel_cost_gbp_per_kwh: Optional[float] Calculator legacy path uses it via `or other_fuel_cost_gbp_per_kwh`. ``` **Open question for the next agent:** the Elmhurst per-fan-count convention in `_mev_decentralised_kwh_per_yr_from_cert` was reverse- engineered from cert 000565 alone: - Each PCDB-defined config (1..6) gets baseline count = 1 - Through-wall kitchen (5): wet_rooms_count fans total - Through-wall other wet (6): wet_rooms_count + 1 fans total When a 2nd MEV cert lands, validate this. May need to consult Elmhurst's documentation or the BRE-published SAP 10.2 implementation guide. ## 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/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 \ --no-cov -q ``` Expected: **597 pass + 7 expected `test_sap_result_pin[000565-*]` fails**. The 7 expected fails (verbatim): ``` 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 ``` All driven by the §3-§8 SH cascade residual + lighting trace + CO2 MEV-split gap. ## Files touched this session | File | Slices | Change | |---|---|---| | `backend/documents_parser/elmhurst_extractor.py` | .96, .97, .102 | "Unknown" insulation token; §9 "Insulation Thickness" cell; §12.1 MV PCDF/Wet-Rooms/Duct-Type fields | | `backend/documents_parser/tests/test_summary_pdf_mapper_chain.py` | .96, .97, .101, .103 | AAA tests for cert 000565 closures | | `datatypes/epc/domain/mapper.py` | .96, .97, .101, .102 | `_elmhurst_rir_insulation_thickness_mm` → `Optional[int]`; floor `insulation_thickness_mm` plumbing; HP SAP-code → category 4; MV duct-type mapper + PCDF plumbing | | `datatypes/epc/surveys/elmhurst_site_notes.py` | .97, .102 | `FloorDetails.insulation_thickness_mm`; `VentilationAndCooling.mechanical_ventilation_pcdf_reference` + `.wet_rooms_count` + `.duct_type` + `.approved_installation` | | `domain/sap10_calculator/tables/pcdb/__init__.py` | .98, .99 | `decentralised_mev_record` + `mv_in_use_factors_record` lookups | | `domain/sap10_calculator/tables/pcdb/etl.py` | .98, .99 | Table 322 + 329 typed ETL | | `domain/sap10_calculator/tables/pcdb/parser.py` | .98, .99 | `DecentralisedMevRecord` + `MvInUseFactorsRecord` + parsers | | `domain/sap10_calculator/tables/pcdb/data/pcdb_table_322_decentralised_mev.jsonl` | .98 | New file — 48 records | | `domain/sap10_calculator/tables/pcdb/data/pcdb_table_329_mv_in_use_factors.jsonl` | .99 | New file — 5 records | | `domain/sap10_calculator/worksheet/mev.py` | .100 | New module — `mev_sfp_av` + `mev_decentralised_kwh_per_yr` helpers | | `domain/sap10_calculator/worksheet/tests/test_mev.py` | .100 | AAA tests pinning cert 000565 SFPav | | `domain/sap10_calculator/rdsap/cert_to_inputs.py` | .102, .103 | `_mev_decentralised_kwh_per_yr_from_cert` composer; `_pumps_fans_fuel_cost_gbp_per_kwh` helper | | `domain/sap10_calculator/calculator.py` | .103 | `CalculatorInputs.pumps_fans_fuel_cost_gbp_per_kwh` field + legacy cost path | | `domain/sap10_calculator/tests/test_pcdb_etl.py` | .98, .99 | Added Tables 322, 329 to file list | | `domain/sap10_calculator/tests/test_pcdb_table_322_lookup.py` | .98 | New file — 3 tests | | `domain/sap10_calculator/tests/test_pcdb_table_329_lookup.py` | .99 | New file — 4 tests | ## Spec source quick-reference - **SAP 10.2 full specification**: `domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf` - §2.6.4 (p.16) — Decentralised MEV SFPav equation (1) — S0380.100 - §5 Table 4a (p.165) — Heat-pump category 4 — S0380.101 - §5 Table 4f (p.174) — Annual electricity for fans / pumps — S0380.100, .102 - §5 Table 4g (p.176) — Default SFP for MV systems — S0380.99 - §10a Table 12a (p.191) — High-rate fractions on off-peak tariffs — S0380.103 - **RdSAP 10 specification**: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf` - §3.10.1 (p.24) — Unknown insulation → Table 18 default — S0380.96 - §5.13 + Table 20 (p.47) — Exposed/semi-exposed floor U-values — S0380.97 - **PCDF Spec Rev 6b**: `domain/sap10_calculator/docs/specs/PCDF_Spec_Rev-06b_12_May_2021.pdf` - §A.19 Format 427 (Decentralised MEV) — S0380.98 - §A.20 Format 430 (MV In-Use Factors) — S0380.99 - **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 .96..103 + open-work analysis - `MEMORY.md` — index entry refreshed at HEAD `e3abe9b2` ## 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 7 cert 000565 fails are the work queue. - **Don't re-investigate any closed work** (.91..103). All settled. - **Don't add new helpers to `domain/sap10_ml/`** — deprecation path per [[project-sap10_ml-deprecation]]. New cascade helpers belong under `domain/sap10_calculator/`. - **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.