diff --git a/domain/sap10_calculator/docs/HANDOVER_POST_S0380_95.md b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_95.md new file mode 100644 index 00000000..5076d8c8 --- /dev/null +++ b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_95.md @@ -0,0 +1,258 @@ +# 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. diff --git a/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_95.md b/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_95.md new file mode 100644 index 00000000..88bcfc13 --- /dev/null +++ b/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_95.md @@ -0,0 +1,254 @@ +# Next-agent prompt — post S0380.91..95 + +Branch: `feature/per-cert-mapper-validation`. +HEAD: `fa6974bd`. + +Read these in order before any tool call: + +1. [`HANDOVER_POST_S0380_95.md`](HANDOVER_POST_S0380_95.md) — full state +2. [`HANDOVER_POST_S0380_90.md`](HANDOVER_POST_S0380_90.md) — predecessor (background) + +Also load these memories before starting: + +- `project_cert_000565_recovery_state` — slice history + per-pin state +- `reference_unmapped_sap_code` — calculator strict-raise pattern +- `project_sap10_ml_deprecation` — `domain/sap10_ml/` is on the + deprecation path; new cascade helpers should land under + `domain/sap10_calculator/` +- `feedback_sap_10_2_only_never_10_3` — **CRITICAL** — never reference + SAP 10.3 spec +- `feedback_spec_citation_in_commits` — quote spec text + page in + commit messages +- `feedback_verify_handover_claims` — verify spec citations + numeric + claims before implementing the prescribed fix +- `feedback_zero_error_strict` — pyright net-zero per touched file +- `feedback_commit_per_slice` — one slice = one commit +- `feedback_aaa_test_convention` — every new test uses literal + `# Arrange / # Act / # Assert` headers +- `feedback_e2e_validation_philosophy` — component pins at <1e-3; + SAP integer delta=0; no adaptive ceilings +- `feedback_abs_diff_over_pytest_approx` — use `abs(x - y) <= tol` + instead of `pytest.approx` to keep pyright net-zero +- `feedback_no_misleading_insulation_type` — field names should reflect + what they semantically contain + +## Critical user direction + +The user's **primary metric is `sap_score_continuous`** (not just +integer `sap_score`). However the user has explicitly stated: + +> "It's okay if we temp drift away from continuous SAP, as long as we +> are actually fixing true problems with the intermediate values. +> Eventually, I expect the error of continuous SAP to be zero but +> that is only possible if we fix all of the sub components and +> remain true to spec." + +**Implication:** ship spec-correct slices even when they cause +transient continuous-SAP drift. Closing real intermediate-value bugs +is the path to zero error. + +## State summary + +This session shipped **S0380.91..95** — five spec-cited slices that +closed major fabric + ventilation gaps for cert 000565: + +1. **S0380.91** (`83218630`) — party-wall CF U=0.2 (RdSAP 10 Table + 15 row 3). party_walls ✓ EXACT. sap_score 27 → 28. +2. **S0380.92** (`a7894b11`) — AP4 + MEV decentralised plumbing + (SAP 10.2 §2 (17a)/(18)/(23a)/(24c)). sap_score 28 → **29 ✓ + EXACT**. (18) + (21) + (25)m all ✓ EXACT. +3. **S0380.93** (`23aaa4fa`) — Floor above partially-heated U=0.7 + (RdSAP 10 §5.14). BP[1] floor ✓ EXACT. +4. **S0380.94** (`78c57c0d`) — RIR "400+ mm PUR or PIR" extractor + + mapper + cascade fixes (RdSAP 10 Table 17 col 3b). BP[2] Stud + Wall 2 ✓ EXACT. +5. **S0380.95** (`fa6974bd`) — Detailed-RR residual area cascade + (RdSAP 10 §3.10.1). **thermal_bridging ✓** + **total_external + _area ✓ close**. sap_score 29 → 28 transient (continuous crossed + 28.5). + +**Cert 000565 state at HEAD `fa6974bd`:** + +| Pin | Cascade | Worksheet | Δ | +|---|---:|---:|---:| +| sap_score (int) | 28 | 29 | -1 | +| **sap_score_continuous** | **28.07** | 28.51 | **-0.44** | +| space_heating_kwh | 59541.61 | 59008.35 | +533.26 | +| **hot_water_kwh** | 3755.03 | 3755.03 | ✓ 0 EXACT | +| **party_walls W/K** | 65.13 | 65.13 | ✓ EXACT | +| **thermal_bridging W/K** | 129.35 | 128.65 | +0.70 | +| **doors W/K** | 11.10 | 11.10 | ✓ EXACT | +| **total external area** | 862.34 | 857.64 | +4.70 | +| floor W/K | 70.37 | 61.67 | +8.70 | +| roof W/K | 63.72 | 51.38 | +12.34 | +| walls W/K | 601.22 | 604.07 | -2.85 | + +## Recommended next slice — S0380.96 BP[4] Flat Ceiling 1 "Unknown PUR or PIR" + +**Highest-leverage remaining single-cause closure for cert 000565.** + +Cert 000565 BP[4] Ext4 Summary §8.1 lodges: +``` +Flat Ceiling 1 5.00 × 1.00 Unknown PUR or PIR Default U=0.15 +``` + +Worksheet line (30): `Roof room Ext4 Flat Ceiling 1: 5 × 0.15 = 0.75 W/K` + +Pre-slice the extractor reads "Unknown" as a non-thickness token and +drops it. The mapper sees `insulation = ""` → `_elmhurst_rir_ +insulation_thickness_mm("")` returns 0 (uninsulated). Cascade hits +Table 17 row 0 → U=2.30. Cascade over-counts by (2.30 - 0.15) × 5 = +**+10.75 W/K**. + +### Audit steps + +1. Read RdSAP 10 §3.10.1 (PDF p.24) for "Unknown" thickness fallback. + Worksheet U=0.15 matches `u_rr_default_all_elements(ENG, M)` (probed + earlier) — so the cascade's existing None → Table 18 col 4 fallback + IS the spec-correct path. +2. Read the BP[4] rir_age (= M for cert 000565). Verify `u_rr_default + _all_elements(ENG, "M")` returns 0.15. +3. Probe `domain/sap10_ml/rdsap_uvalues.py:_u_rr_table_17` to confirm + the `insulation_thickness_mm is None` → `u_rr_default_all_elements` + path. +4. Read the Elmhurst extractor + mapper code paths for "Unknown" + thickness token. + +### Suggested implementation + +The cleanest path treats "Unknown thickness with known insulation +material" as `insulation_thickness_mm=None` (not 0). The cascade's +existing path handles None → Table 18 col 4 default. + +**Slice span:** +1. Extractor (`elmhurst_extractor.py:_parse_rir_surface_row`): + recognise "Unknown" as a valid `insulation` value (currently the + allow-list checks `_RIR_INSULATION_THICKNESS_RE` or + `("As Built", "None")` — extend to include `"Unknown"`). +2. Mapper (`datatypes/epc/domain/mapper.py:_elmhurst_rir_insulation + _thickness_mm`): translate `"Unknown"` → `None` (not 0). Today the + function returns int (always), so the schema change requires + `Optional[int]` return type + downstream updates. + + Alternative: change to `Optional[int]` return; update + `SapRoomInRoofSurface.insulation_thickness_mm` consumers to handle + None. + +3. Cascade: no change needed — existing None → Table 18 col 4 + fallback is spec-correct. + +4. Tests (AAA): + - Extractor: `test_summary_000565_bp4_flat_ceiling_1_extracts_ + unknown_thickness_lodgement` + - Mapper: `test_summary_000565_bp4_flat_ceiling_1_maps_unknown_ + to_none_thickness_per_rdsap_10_section_3_10_1` + - Cascade pin: BP[4] FC1 cascade U = 0.15 vs ws 0.15 + +### Expected outcome + +- cert 000565 BP[4] FC1 cascade U: 2.30 → **0.15 ✓ EXACT** +- roof_w_per_k: 63.72 → 53.97 (Δ+12.34 → +2.59) +- Continuous SAP: −0.44 → ~ −0.10 (much closer to zero) +- Integer sap_score: 28 → potentially 29 (if continuous crosses 28.5 + back up) +- SH: +533 → ~+230 kWh + +### Cohort safety + +- Cohort fixtures (000474..000516) all lodge concrete insulation values + for Flat Ceiling lodgements; "Unknown" is cert 000565-specific. +- Need to verify cohort: any other Summary cert with "Unknown" in RIR + insulation cell? Run: `grep -l "Unknown" $(ls backend/documents_ + parser/tests/fixtures/Summary_*.pdf | xargs -I {} sh -c 'pdftotext + -layout {} - | grep -lq Unknown')` (or similar audit). + +## Beyond S0380.96 — remaining cert 000565 work-items + +### S0380.97 — BP[2] Ext2 floor 200 mm insulation extractor + +Summary §9 lodges "Insulation Thickness: 200 mm" + "Insulation Type: +Retro-fitted" for Ext2. Extractor's `_floor_details_from_lines` +doesn't read it. Worksheet U=0.22, cascade currently 0.51 (Δ +0.29 × +30 m² = +8.70 W/K). + +**Slice span:** +- Extract per-extension "Insulation Thickness" inside §9 block (scoped) +- Schema: `FloorDetails.insulation_thickness_mm: Optional[int]` +- Mapper: plumb to `SapBuildingPart.floor_insulation_thickness` +- Cascade: no change (existing `u_exposed_floor` reads thickness) + +Per user direction OK to drift continuous SAP further (this fix LOWERS +heat loss → SAP UP → drifts away from worksheet). Spec-correct. + +### S0380.98 — BP[1] Ext1 residual formula refinement + +BP[1] residual +3.68 m² over. Simplified A_RR formula `12.5 × √(34/ +1.5) = 59.51` minus 37.58 lodged walls = 21.93 vs ws 18.25. + +Hypothesis: Ext1's RR height = 3.0 m (not 2.45 m). Formula may need +to be height-aware. Investigation needed. + +Impact: roof -1.29 W/K. Small. + +### Deferred (unchanged) + +- 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) + +## Standard workflow per slice + +1. Read SAP 10.2 / RdSAP 10 spec page for the change — quote it in commit +2. Probe current cascade output; identify exact spec-vs-cascade gap +3. Write failing test FIRST (AAA structure) +4. Implement helper / change +5. Verify test passes +6. Run full handover suite (command below) +7. Check pyright on touched files — net-zero from baseline + (use `git stash` + re-run pyright to compute baseline) +8. Commit with spec citation + verbatim quote +9. Update relevant memory if state changed + +## 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**. + +After S0380.96 lands the expected fail count likely drops by 1 +(sap_score back to EXACT) and continuous-SAP residual closes to +~−0.1. Other downstream pins (SH, fuel, cost, CO2) move with SH. + +## 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 of the .85..95 closures.** All settled. +- **Don't add new helpers to `domain/sap10_ml/`** — deprecation path + per [[project-sap10_ml-deprecation]]. +- **Don't chase the 12 gas-combi PV certs or the 5 SAP-residual certs + without worksheets** — user has explicitly de-prioritised. +- **Don't avoid spec-correct closures because continuous SAP drifts + transiently** — user explicitly OK'd it. + +## Memory hygiene + +After the next slice, update: +- `project_cert_000565_recovery_state` — append closure + open work- + items refresh +- `MEMORY.md` index — refresh HEAD + one-line summary + +Good luck.