diff --git a/domain/sap10_calculator/docs/HANDOVER_POST_S0380_84.md b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_84.md new file mode 100644 index 00000000..40fa618e --- /dev/null +++ b/domain/sap10_calculator/docs/HANDOVER_POST_S0380_84.md @@ -0,0 +1,257 @@ +# Handover — post S0380.81..84 (Table 32 default + Table 12a Grid 2 CO2 + RR fold-in) + +Branch: `feature/per-cert-mapper-validation`. **HEAD `49622f55`**. +Predecessor: [`HANDOVER_POST_S0380_80.md`](HANDOVER_POST_S0380_80.md). + +## Slices committed this session (S0380.81..84) + +Four spec-cited slices, two clean closures + one extractor data-completion ++ one structural RR fix that surfaces the next named gap. + +| Slice | Commit | Spec | Cert 000565 outcome | +|---|---|---|---| +| **S0380.81** | `9338914f` | RdSAP 10 §19.1 (PDF p.80-81) — "use Table 32 prices (not Table 12) for §10a/§10b" | sap_score 28 → **29 EXACT** at the (28.5) rounding boundary. Cost residual £+3.62 → £−2.39. | +| **S0380.82** | `27ead127` | SAP 10.2 Table 12a Grid 2 (p.191) + Table 12d/12e (p.194-195) — "All other uses" off-peak dual-rate | CO2 residual −8.92 → **−3.08 kg/yr** (65% closed). Lighting + pumps_fans + electric_shower CO2/PE factors now blend Table 12d/12e high-rate × low-rate codes per Grid 2 fraction on off-peak certs. | +| **S0380.83** | `ed8fdc6a` | RdSAP 10 §3.10 + Summary PDF §8.1 schema | Extractor recognises `"Exposed"` + `"Connected"` `gable_type` (was Party / Sheltered / "Connected to heated space" only). Mapper elif extended to preserve cert 9501. Pure data-extraction completion; zero cascade impact. | +| **S0380.84** | `49622f55` | RdSAP 10 §3.9.2 + §3.10 + Table 4 (p.22) | Mapper drops Connected gables, routes Exposed → `gable_wall_external` with lodged U, surfaces Common Walls, applies spec area formula `L × (0.25 + H)` and `Σ` per-common-wall gable correction. Cascade adds `common_wall` kind handler. **11 per-BP RR surface areas EXACT vs worksheet PDF**. Cascade walls 322 → 443 W/K (43% closer to worksheet 604); party 153 → 93 (68% closer to worksheet 65). cert 000565 sap_score temporarily regressed 29 → 26 — see §"Why the regression is the correct signal" below. | + +**Test baseline at HEAD `49622f55`:** 555 pass + 9 expected +`test_sap_result_pin[000565-*]` cascade-gap fails. Pyright net-zero on +every touched file. Cohort + golden + cert 9501 unaffected. + +## Cert 000565 state (HEAD `49622f55`) + +| Pin | Cascade | Worksheet | Δ | Cause | +|---|---:|---:|---:|---| +| sap_score (int) | 26 | 29 | **−3** | RR fold-in (S0380.84) exposed BP main-wall gap; see §"BP main-wall residual −161 W/K diagnostic" | +| sap_score_continuous | 26.4972 | 28.5087 | −2.01 | Downstream of HTC over-count via space_heating +2591 | +| ecf | 5.5970 | 5.3866 | +0.21 | Downstream of cost +£182.6 | +| total_fuel_cost_gbp | 4862.88 | 4680.26 | +182.6 | Downstream of space_heating fuel cost | +| co2_kg_per_yr | 6684.52 | 6447.63 | +236.9 | Downstream of HP electricity over-fuel-use | +| **space_heating_kwh** | **61599.61** | **59008.35** | **+2591.3** | **BP main-wall cascade gap** (Curtain Wall −112 W/K + thin-wall alt −47 W/K — see diagnostic below) | +| main_heating_fuel | 36235.07 | 34710.79 | +1524.3 | Follows space_heating via 1/COP | +| **hot_water_kwh** | **3755.03** | **3755.03** | **✓ 0 EXACT** | §4 cascade fully closed (S0380.77..80) | +| lighting | 1387.02 | 1384.84 | +2.19 | Sub-spec | +| pumps_fans | 255.00 | 252.52 | +2.48 | MEV PCDB record missing (external data) | + +### §4 HW cascade line refs (all EXACT, unchanged) + +| Line | Cascade | Worksheet | Δ | +|---|---:|---:|---:| +| (45)m sum energy_content | 1286.3266 | 1286.3266 | ✓ 0 | +| (46)m sum distribution_loss | 192.9490 | 192.95 | ✓ <1e-3 | +| (57)m sum solar_storage | 596.9725 | 596.9725 | ✓ <1e-4 | +| (59)m sum primary_loss | 1176.77 | 1174.79 | +1.98 | +| (61)m combi_loss | 0.00 | 0.00 | ✓ 0 | +| (62)m sum total_demand | 3060.07 | 3060.07 | ✓ <1e-3 | +| (64)m sum (after solar) | 2778.72 | 2778.7213 | ✓ <1e-4 | +| (64a)m electric shower | 702.94 | 702.94 | ✓ <1e-4 | +| (217)m water-heater eff | 0.74 | 0.74 | ✓ EXACT | +| (219) HW fuel kWh | 3755.03 | 3755.0288 | ✓ <1e-3 | + +### Per-BP RR surface areas (all EXACT after S0380.84) + +Verified against the cert 000565 U985 worksheet "External Walls" + "Party +Walls" sections at 4 d.p. precision: + +| BP | Surface | Spec formula | Worksheet | Cascade | +|---|---|---|---:|---:| +| 0 | Main GW1 Exposed | 4 × 2.45 (Simplified, no CW) | 9.80 | 9.80 ✓ | +| 0 | Main GW2 Sheltered | 6 × 2.45 | 14.70 | 14.70 ✓ | +| 1 | Ext1 CW1 | 9 × (0.25 + 1.0) (Simplified + CW) | 11.25 | 11.25 ✓ | +| 1 | Ext1 CW2 | 5 × (0.25 + 1.8) | 10.25 | 10.25 ✓ | +| 1 | Ext1 GW2 Exposed | 8 × (0.25+9) − ((9−1)²+(9−1.8)²)/2 | 16.08 | 16.08 ✓ | +| 2 | Ext2 GW2 Exposed | 3 × 8 (Detailed) | 24.00 | 24.00 ✓ | +| 3 | Ext3 CW1 | 5 × (0.25 + 1.5) (Simplified + CW) | 8.75 | 8.75 ✓ | +| 3 | Ext3 CW2 | 7.5 × (0.25 + 0.3) | 4.13 | 4.13 ✓ | +| 3 | Ext3 GW1 Exposed | 9 × (0.25+7) − ((7−1.5)²+(7−0.3)²)/2 | 27.68 | 27.68 ✓ | +| 4 | Ext4 CW1 | 4 × 1 (Detailed) | 4.00 | 4.00 ✓ | +| 4 | Ext4 CW2 | 3.5 × 0.6 (Detailed) | 2.10 | 2.10 ✓ | + +### Cumulative cert 000565 closure (S0380.77 → .84) + +| Pin | .77→ | .78→ | .79→ | .80→ | .81→ | .82→ | .83→ | .84 | +|---|---:|---:|---:|---:|---:|---:|---:|---:| +| hot_water_kwh | +1399 | +260 | −238 | **✓0** | ✓0 | ✓0 | ✓0 | ✓0 | +| sap_score (int) | +1 | −1 | 0 | −1 | **✓0** | ✓0 | ✓0 | **−3** ⚠ | +| sap_score_continuous | +0.60 | −0.04 | +0.06 | −0.04 | +0.03 | +0.03 | +0.03 | −2.01 ⚠ | +| ecf | −0.06 | +0.00 | −0.01 | +0.00 | −0.00 | −0.00 | −0.00 | +0.21 ⚠ | +| total_fuel_cost_gbp | −53 | +3 | −5 | +4 | −2 | −2 | −2 | +183 ⚠ | +| co2_kg_per_yr | (n/a) | (n/a) | −58 | −9 | −9 | **−3** | −3 | +237 ⚠ | + +S0380.84 ⚠ rows are the documented BP main-wall surfacing (NOT a +regression of S0380.84's RR fix itself). + +## Why the regression is the correct signal + +S0380.84 closed the RR cascade routing to spec correctness. The 11 +per-BP RR surface areas pin EXACT vs worksheet at 4 d.p. The cascade +walls subtotal moved 322 → 443 W/K (worksheet 604, 43% closed); party +153 → 93 (worksheet 65, 68% closed). + +Pre-S0380.84 the cascade had two ~equal-magnitude bugs of opposite +sign that mostly cancelled at the SH-pin level: + + - **RR cascade**: Exposed gables wrongly routed to party_walls at + U=0.25 (cascade over-counts party_walls by ~88 W/K) + - **BP main-wall cascade**: Curtain Wall + thin-wall alt missing + (cascade under-counts walls by ~161 W/K) + +S0380.84 closed the first one. The second is now exposed as a +2591 +kWh space_heating residual. Per `[[feedback-spec-citation-in-commits]]` +and `[[feedback-spec-floor-skepticism]]` the spec-correct fix ships +even when the test pin temporarily regresses; the diagnostic signal +is sharper now. + +## BP main-wall residual −161 W/K diagnostic + +Probed per-BP at HEAD `49622f55`: + +| BP | Cascade U | Worksheet U | Δ contribution | Spec gap | +|---|---:|---:|---:|---| +| 0 Main | 0.32 | 0.35 | −1.6 W/K | sub-spec (Solid Brick A age, 75mm External insulation) | +| 0 Main alt1 | 0.32 | 2.34 | **−46.5 W/K** | `_insulation_bucket(thk=120, ins_present=False)` returns 100 not 0 (docstring intent vs current code) + thin-wall §6.6/§6.7 for U=2.34 | +| 1 Ext1 | 1.70 | 1.70 | ✓ 0 | ✓ Spec-correct (Stone Granite E age, Unknown insulation) | +| 2 Ext2 (Curtain Wall) | 0.60 | 1.40 | **−112.2 W/K** | `WALL_CURTAIN=9` defined `rdsap_uvalues.py:116` but no `_ENG_WALL` table entry; `u_wall` falls through to default Cavity table | +| 3 Ext3 (basement) | 0.45 | 0.45 | ✓ 0 | ✓ Spec-correct | +| 4 Ext4 (basement) | 0.35 | 0.35 | ✓ 0 | ✓ Spec-correct | + +Total: **−160.3 W/K** (matches the observed −161 W/K). + +### Gap #1 — Curtain Wall (largest, −112 W/K) + +**Where:** [domain/sap10_ml/rdsap_uvalues.py:116](../../sap10_ml/rdsap_uvalues.py) — `WALL_CURTAIN: Final[int] = 9` is defined but has no entry in `_ENG_WALL` and is not in the `known_types` set at `u_wall:373-376`. When the cert lodges `wall_construction=9`, `u_wall` falls through to `_DEFAULT_WALL_BY_AGE` (default cavity) and returns the cavity-wall U for that age band. + +**Cert 000565 BP[2] Ext2** lodges `Type: CW Curtain Wall` + `Curtain Wall Age: Post 2023` per Summary PDF §7. The "Curtain Wall Age" is a separate per-BP attribute from the dwelling-wide `construction_age_band` — the BP is age `H` (1991-1995) but the curtain wall itself was installed Post-2023. Worksheet uses Curtain Wall Post-2023 U=1.40. + +**Slice span:** + +1. Extractor (`backend/documents_parser/elmhurst_extractor.py`) — currently doesn't surface "Curtain Wall Age" from Summary §7 +2. `datatypes/epc/surveys/elmhurst_site_notes.py` — add `curtain_wall_age` to `WallDetails` +3. `datatypes/epc/domain/epc_property_data.py` — add `curtain_wall_age` to `SapBuildingPart` +4. `datatypes/epc/domain/mapper.py` — thread through both API + Elmhurst paths +5. `domain/sap10_ml/rdsap_uvalues.py` — Curtain Wall U-value lookup by age; add `WALL_CURTAIN` to `known_types` + +**Spec citation needed:** RdSAP 10 Table 6 or related — locate the canonical Curtain Wall U-values per age category. The worksheet says 1.40 for Post-2023; need to verify the full table. + +### Gap #2 — Thin-wall alt stone granite (−47 W/K) + +**Where:** Two coupled bugs. + +1. `domain/sap10_ml/rdsap_uvalues.py:160 _insulation_bucket` ignores `insulation_present=False` when `thickness_mm > 0`. Docstring says "when not present, the as-built (bucket 0) row applies regardless" but the code falls through to thickness-bucket selection. For BP[0] alt1 with `wall_insulation_type=4` (None) but `wall_insulation_thickness='120'`, bucket returns 100 not 0. + +2. The `wall_insulation_thickness='120'` on `SapAlternativeWall` is actually the **WALL thickness** lodged in Summary §7 ("Alternative Wall 1 Thickness: 120 mm"), NOT an insulation thickness. Per [[feedback-no-misleading-insulation-type]] this should land on a new `SapAlternativeWall.wall_thickness_mm` field (mirror of `SapBuildingPart.wall_thickness_mm`). + +3. Even with bucket 0, `_TYPICAL_STONE_UNINSULATED[0] = 1.7` + dry-lined adjustment = 1.32. Worksheet wants 2.34. This is the RdSAP 10 §6.6/§6.7 "thin-wall" formula for stone walls below typical thickness — needs implementing in `u_wall`. + +**Slice span:** + +1. Extractor — surface "Alt 1 Thickness" as wall thickness (currently mapped to `wall_insulation_thickness`) +2. `datatypes/epc/domain/epc_property_data.py` — `SapAlternativeWall.wall_thickness_mm: Optional[int]` +3. `datatypes/epc/domain/mapper.py` — populate `wall_thickness_mm` from Elmhurst extractor's alt-wall-thickness field +4. `domain/sap10_ml/rdsap_uvalues.py:160 _insulation_bucket` — short-circuit `if not insulation_present: return 0` per docstring intent (audit cohort for any cert with insulation_present=False AND thickness>0) +5. `domain/sap10_ml/rdsap_uvalues.py:329 u_wall` — RdSAP §6.6/§6.7 thin-wall formula keyed on `wall_thickness_mm` for stone constructions + +## Open work — prioritised next slices + +### S0380.85 — Curtain Wall (highest impact, −112 W/K of −161) + +Extract `curtain_wall_age` per BP + add Curtain Wall U-value lookup keyed +on age. Multi-layer (extractor + datatypes + mapper + cascade); a single +slice if the spec lookup is tractable. + +Spec citation candidate: RdSAP 10 Table 6 row for "CW Curtain Wall" +across age categories. The worksheet (cert 000565) gives Post-2023 → +U=1.40 as the empirical ground truth. + +Expected impact: cert 000565 cascade walls 443 → 555 (worksheet 604). +HTC fabric 795 → 907. SH residual +2591 → +~800 kWh. sap_score should +move 26 → ~28 (still 1-2 short of 29 due to remaining alt1 gap). + +### S0380.86 — Thin-wall alt stone granite (−47 W/K) + +Two coupled bugs in `rdsap_uvalues.py`: +1. `_insulation_bucket` short-circuit on `not insulation_present` +2. Thin-wall §6.6/§6.7 formula keyed on a new `wall_thickness_mm` + field on `SapAlternativeWall` + +Plus extractor + datatype + mapper plumbing for the wall_thickness +field. After both gaps close: cascade walls 555 → ~610 (matches +worksheet 604). HTC → ~960. SH should close to ~−72 (or smaller — +the original residual pre-S0380.84). + +### Deferred (unchanged from post-S0380.80 handover) + +- MEV PCDB Table 4f component for pumps_fans +2.5 (blocked on external + data acquisition; PCDF 500755 record needed) +- HP SAP code → main_heating_category=4 mapper extension (couples with + MEV; ship after MEV record acquired) +- 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: **555 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 +``` + +(was 8 + sap_score EXACT at HEAD `27ead127`; sap_score moved into the +fail list at HEAD `49622f55` per "Why the regression is the correct +signal" above). + +## Files touched this session + +| File | Slices | Change | +|---|---|---| +| `domain/sap10_calculator/rdsap/cert_to_inputs.py` | S0380.81, .82 | Added `RDSAP_10_TABLE_32_PRICES`; switched default; new `_other_use_co2_factor_kg_per_kwh` + `_other_use_primary_factor` helpers; wired into pumps_fans / lighting / electric_shower CO2 + PE fields | +| `domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py` | S0380.81, .82 | 2 new tests + 3 re-pinned scalar assertions to Table 32 | +| `backend/documents_parser/elmhurst_extractor.py` | S0380.83 | Added "Exposed" + "Connected" to `gable_type` recognition set | +| `backend/documents_parser/tests/test_summary_pdf_mapper_chain.py` | S0380.83, .84 | 2 new tests (extractor gable_type + mapper RR routing/areas) | +| `datatypes/epc/domain/mapper.py` | S0380.83, .84 | Mapper RR routing per §3.10 Table 4; drops Connected, routes Exposed external, surfaces Common Walls with §3.9.2 spec area formula | +| `datatypes/epc/domain/epc_property_data.py` | S0380.84 | `SapRoomInRoofSurface.kind` docstring extended with `common_wall` | +| `domain/sap10_calculator/worksheet/heat_transmission.py` | S0380.84 | Added `common_wall` kind handler (walls += area × U) | + +## Spec source quick-reference + +- **SAP 10.2 full specification**: `domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf` + - Table 12a (p.191) — Grid 1 SH + WH + Grid 2 "All other uses" high-rate fractions + - Table 12d (p.194) — monthly CO2 factors for electricity + - Table 12e (p.195) — monthly PE factors for electricity +- **RdSAP 10 specification**: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf` + - §3.9 + §3.10 (p.30-35) — Simplified Type 1/2 + Detailed RR + - Table 4 (p.22) — RR surface variants (gable_wall U-value rules) + - §19.1 (p.80-81) — Table 32 prices for §10a/§10b + - Table 32 (p.95) — RdSAP unit prices + standing charges + - Table 6 — wall U-values by construction + insulation bucket (Curtain Wall entry needed for S0380.85) + - §6.6 / §6.7 — thin-wall stone formula (S0380.86) +- **SAP 10.3 at** `domain/sap10_calculator/docs/specs/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` — S0380.81/.82/.83/.84 entries + + post-S0380.84 BP main-wall diagnostic table localising the −161 + W/K residual to Curtain Wall + thin-wall alt +- `MEMORY.md` — index entry refreshed at HEAD `49622f55` diff --git a/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_84.md b/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_84.md new file mode 100644 index 00000000..ba3c9535 --- /dev/null +++ b/domain/sap10_calculator/docs/NEXT_AGENT_PROMPT_POST_S0380_84.md @@ -0,0 +1,190 @@ +# Next-agent prompt — post S0380.84 + +Branch: `feature/per-cert-mapper-validation`. +HEAD: `49622f55`. + +Read these in order before any tool call: + +1. [`HANDOVER_POST_S0380_84.md`](HANDOVER_POST_S0380_84.md) — full state +2. [`HANDOVER_POST_S0380_80.md`](HANDOVER_POST_S0380_80.md) — predecessor (background; do not re-investigate) + +Also load these memories before starting: + +- `project_cert_000565_recovery_state` — cert 000565 slice history + per-BP diagnostic +- `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_spec_floor_skepticism` — "spec-precision floor" framing usually hides a real spec rule +- `feedback_verify_handover_claims` — verify spec citations + numeric claims before implementing +- `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 `# Arrange / # Act / # Assert` +- `feedback_no_misleading_insulation_type` — don't lodge `insulation_type` on uninsulated surfaces; "thickness" fields should track what they're named for +- `feedback_e2e_validation_philosophy` — component pins at <1e-3; SAP integer delta=0; no adaptive ceilings +- `feedback_golden_residuals_near_zero` — pin updates on golden certs are protocol when cascade closure surfaces real spec bugs + +## State summary + +This session shipped **S0380.81/.82/.83/.84** — Table 32 default +prices, Table 12a Grid 2 dual-rate CO2/PE, extractor gable_type +recognition, and the full RR fold-in cascade fix. cert 000565 sap_score +closed 28 → 29 EXACT at S0380.81; CO2 closed 65%; RR cascade structurally +spec-correct (11 per-BP surface areas EXACT vs worksheet at 4 d.p.). + +**The S0380.84 RR fix surfaced the next named gap**: cert 000565 BP +main-wall residual −161 W/K, localised in the handover to two +spec-cited cascade gaps: + + - **BP[2] Ext2 Curtain Wall: −112 W/K** (`WALL_CURTAIN=9` defined + but no `_ENG_WALL` table entry; `u_wall` falls through to default + Cavity for age H → 0.60, worksheet expects 1.40) + - **BP[0] Main alt1 thin-wall stone granite: −47 W/K** + (`_insulation_bucket(thk=120, ins_present=False)` returns 100 not 0 + per docstring intent; plus `wall_insulation_thickness=120` is + mislabelled wall thickness; plus RdSAP §6.6/§6.7 thin-wall formula + for U=2.34 ground truth) + +cert 000565 sap_score temporarily moved 29 → 26 because the RR fix +exposed the BP main-wall gap that was previously masking the SH +residual via cancellation. The cascade is more spec-correct now. + +| Pin | Δ | Cause | Next slice | +|---|---:|---|---| +| sap_score | −3 | BP main-wall under-count | Closes when S0380.85 + .86 land | +| space_heating_kwh | +2591 | Curtain Wall + thin-wall alt | S0380.85 + .86 | +| main_heating_fuel | +1524 | Follows space_heating via 1/COP | Downstream | +| co2 / cost / ECF / continuous SAP | (large) | Downstream of HTC over-count | Downstream | +| **hot_water_kwh** | **✓ 0 EXACT** | §4 cascade closed | done | +| lighting | +2.19 | Sub-spec | low priority | +| pumps_fans | +2.48 | MEV PCDB missing | blocked on data | + +## Recommended next slice — S0380.85 Curtain Wall closure + +**This is the highest-leverage single change available** (closes 70% of +the BP main-wall gap; −112 of −161 W/K). + +### Audit steps + +1. Read RdSAP 10 Table 6 — find the Curtain Wall row. Cert 000565 + worksheet pins U=1.40 for "Curtain Wall Post 2023" (cert age H). + Verify the per-age-category Curtain Wall U-values. +2. Read Summary §7 lodging for cert 000565 BP[2]: + ``` + Type: CW Curtain Wall + Curtain Wall Age: Post 2023 + U-value Known: No + ``` + The "Curtain Wall Age" is a separate per-BP attribute (not the + dwelling `construction_age_band`). Need to extract + plumb through. +3. Probe the extractor's current Wall section parser to find where + to slot the `curtain_wall_age` extraction. +4. Probe `_ENG_WALL` table — find which constructions have age-keyed + lookups (e.g. `WALL_SYSTEM_BUILT`) to mirror for `WALL_CURTAIN`. + +### Suggested implementation + +1. **Extractor**: + `backend/documents_parser/elmhurst_extractor.py` — parse "Curtain + Wall Age" line from the per-BP Wall block (Summary §7). +2. **datatype**: + - `datatypes/epc/surveys/elmhurst_site_notes.py:WallDetails` — + add `curtain_wall_age: Optional[str]` field + - `datatypes/epc/domain/epc_property_data.py:SapBuildingPart` — + add `curtain_wall_age: Optional[str]` field +3. **Mapper**: + `datatypes/epc/domain/mapper.py` — thread `curtain_wall_age` through + both API + Elmhurst paths to `SapBuildingPart`. +4. **Cascade**: + `domain/sap10_ml/rdsap_uvalues.py` — + - Add `WALL_CURTAIN` to `known_types` (line 373-376) so the code + selects the curtain wall lookup rather than falling through to the + cavity default + - Add `_ENG_WALL[(WALL_CURTAIN, 0)] = [...A-M U-values from Table 6]` + - Update `u_wall` signature to accept `curtain_wall_age` and + dispatch: when `wall_type == WALL_CURTAIN`, key the lookup on + `curtain_wall_age` ("Post 2023" / "Pre 2023" / etc.) instead of + the dwelling age band +5. **Failing test** (AAA): write the cascade-level pin first — + `test_summary_000565_ext2_curtain_wall_routes_to_u_value_1p40_per_rdsap_10_table_6` + asserts `heat_transmission_section_from_cert(epc).walls_w_per_k` + moves toward worksheet. + +### Expected outcome + +cert 000565 cascade walls 443 → 555 (worksheet 604). HTC fabric +795 → 907. SH residual +2591 → ~+800 kWh. sap_score should move 26 +→ ~28 (still 1-2 short of 29 due to remaining alt1 gap). + +After S0380.85 lands, S0380.86 (thin-wall alt) is the natural next +slice — closes the remaining −47 W/K + the dry-lining handling for +stone walls. + +## Alternative — S0380.86 first (thin-wall alt stone granite) + +Smaller in magnitude (−47 W/K) and slightly more complex +(involves 3 coupled bugs in `rdsap_uvalues.py` + a datatype shape +change for `SapAlternativeWall.wall_thickness_mm` per +[[feedback-no-misleading-insulation-type]]). Less attractive as a +standalone slice; ship after S0380.85 lands. + +## 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 in handover) +7. Check pyright on touched files — net-zero from baseline +8. Commit with spec citation +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: **555 pass + 9 expected `test_sap_result_pin[000565-*]` fails**. + +After S0380.85 lands, expected fails should drop to 7 or 8 (sap_score +likely still failing at Δ−1 or Δ−2 with the residual thin-wall gap +still open; main fail count reduction comes when S0380.86 also lands). + +## What NOT to do + +- **Don't re-investigate the RR cascade**. S0380.84 closed the structural + routing per RdSAP §3.9.2 + §3.10 + Table 4; the 11 per-BP RR surface + areas pin EXACT vs worksheet PDF. +- **Don't re-investigate Table 32 prices, Table 12a Grid 2 CO2/PE, or + the §4 HW cascade**. All spec-correct at HEAD `49622f55`. +- **Don't widen pin tolerances or xfail residual gaps** + ([[feedback-zero-error-strict]]). The 9 cert 000565 fails are the + work queue. +- **Don't revert S0380.84** — the test-pin "regression" is the + spec-correct cascade exposing the next named gap; reverting puts + the cascade back into compensating-bugs state. Per + [[feedback-spec-citation-in-commits]] + [[feedback-spec-floor-skepticism]] + ship the spec-correct fix and close the surfaced gap. +- **Don't reference SAP 10.3** ([[feedback-sap-10-2-only-never-10-3]]). +- **Don't chase the 12 gas-combi PV certs or the 5 SAP-residual certs + without worksheets** — user has explicitly de-prioritised. +- **Don't apply the S0380.85 Curtain Wall lookup to any non-curtain-wall + construction**. Gate strictly on `wall_construction == WALL_CURTAIN`. + +## Memory hygiene + +After the next slice, update: +- `project_cert_000565_recovery_state` — final cumulative closure table + (especially if sap_score returns to EXACT after S0380.85+.86 land). +- If you ship the Curtain Wall lookup: consider adding a memory entry + for the spec citation if the Table 6 row is non-obvious to find. + +Good luck.