From 58ff7d88810a4bbafd74c541f7e1992b3756cb7e Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 3 Jun 2026 21:22:04 +0000 Subject: [PATCH] docs: handover for golden-cert mapper/cascade bugs (roof S0380.210 + community fuel collision) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records post-S0380.209 state: 0240 verdict (true SAP 72, register 73 = unpreserved 2013+ pump, proven 0=Unknown via 13 pairs), and three open threads — roof Ext1 "insulated (assumed)" U over-count (needs case 11 worksheet), community fuel-code collision (API 18-25 vs Table-12 biomass 18-25; cert 9390 CO2 6x low; needs 9390 worksheet), and 0390 +7 demand-side gap. Plus the audit table of all 5 non-zero-SAP golden certs. Co-Authored-By: Claude Opus 4.8 --- .../docs/HANDOVER_MAPPER_BUGS.md | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md diff --git a/domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md b/domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md new file mode 100644 index 00000000..0ac86d11 --- /dev/null +++ b/domain/sap10_calculator/docs/HANDOVER_MAPPER_BUGS.md @@ -0,0 +1,180 @@ +# Handover — golden-cert mapper/cascade bugs (post-0240 wall fix) + +Point-in-time note. Start from [`AGENT_GUIDE.md`](AGENT_GUIDE.md) for methodology, +the 1e-4 bar, the per-line debugging loop, and the suite command. This records the +state after closing the 0240 investigation and fixing the first of several +API-mapper/cascade bugs the audit surfaced. + +- **Branch:** `feature/per-cert-mapper-validation` +- **HEAD:** `844fc22f` (S0380.209). Confirm with `git rev-parse HEAD`. +- **Baseline:** `2383 passed, 1 skipped, 0 failed` (AGENT_GUIDE §4 suite command). +- **Next slice number:** **S0380.210**. + +--- + +## What this session shipped + +| slice | what | +|---|---| +| **S0380.208** | Promoted **simulated case 7** (combi swap of case 6) to an e2e fixture. PROVED the condensing-oil-combi (SAP code 130, no cylinder, combi instantaneous DHW, Eq D1, Table 3a keep-hot) path is **exact at 1e-4** with zero calculator changes → exonerated the heating as the source of 0240's residual. | +| **S0380.209** | Fixed the **API-path wall U** "as built, insulated (assumed)" bug — routes to the as-built age-band row, not the 50 mm retrofit bucket. New `_described_as_retrofit_insulated` in `heat_transmission.py`. Worksheet-validated by case 9 (sandstone J → 0.35) + case 10 (solid brick J → 0.35). Re-pinned 0240 PE +1.8687 → +5.5044, CO2 +0.0907 → +0.2757 (SAP integer 72 unchanged). | + +Both also carry a memory: [[project_case7_combi_exonerated]], [[project_as_built_insulated_assumed_bug]]. + +--- + +## The 0240 verdict — RESOLVED, not closable to 73 + +**0240's true SAP is 72**, proven three independent ways: +- Elmhurst **case 8** (pump=Unknown, 0240's actual lodged value) → worksheet SAP **72**. +- Elmhurst **case 9** (correct sandstone wall 0.35 + sloping roof 0.25) → SAP **72**. +- Our cascade with **both** the wall (done) and roof (pending) bugs fixed → SAP cont **72.31**. + +The register's **73 requires a "2013 or later" circulation pump (41 kWh)** — Elmhurst +**case 7** with that pump = 73. But 0240's API lodges `central_heating_pump_age=0` += **Unknown** → 115 kWh. The encoding was **proven from 13 API+Summary pairs**: +`0`="Unknown" (9 pairs), `2`="2013 or later" (4 pairs). The open-data export did not +preserve the pump age that produced the lodged 73. **It is genuinely unreachable from +the JSON.** Do not chase 0240 to 73; re-pin to its correct 72 once the roof lands. + +`pump_age` enum (verified): `0`=Unknown→115, `1`=Pre-2013→165, `2`=2013+→41 +(`_TABLE_4F_CIRCULATION_PUMP_KWH_BY_AGE` in `cert_to_inputs.py`). + +--- + +## THE METHODOLOGY LESSON FROM THIS SESSION (read this) + +The 0240 baseline (cont 72.39) was **two offsetting bugs**: a wall U **under**-count +(0.25 vs 0.35, less loss) masking an Ext1 roof U **over**-count (0.68 vs ~0.25, more +loss). Fixing one alone moves the residual the "wrong" way — fix both +([[feedback_software_no_special_handling]]). And: **Elmhurst is the arbiter, not the +spec text** — twice this session a confident spec/first-principles read was wrong and +a generated worksheet settled it (`NI`=not-indicated not "none"; pump `0`=Unknown). +**Generate a worksheet rather than guess a U-value or factor.** The user generates +Elmhurst worksheets readily (cases 7–10 done) — ask for one. + +--- + +## OPEN THREAD 1 — Roof fix (S0380.210), needs **case 11** + +Same root cause as the wall (`project_as_built_insulated_assumed_bug`): the API mapper +populates `epc.roofs` with "Pitched, insulated (assumed)"; `u_roof` +([`rdsap_uvalues.py:708`](../../sap10_ml/rdsap_uvalues.py)) routes +`insulation_thickness_mm==0 and _described_as_insulated(description)` → **0.68** +(retrofit 50 mm). 0240's Ext1 is `roof_construction=5` "vaulted ceiling", `NI` thickness, +band J → cascade **0.68**, should be a Table 18 sloping-ceiling value. + +**Why it's harder than the wall:** +- Just swapping to `_described_as_retrofit_insulated` makes thickness-0 fall to the + Table 16 ladder → **2.30** (uninsulated). `NI`/unknown must route to the age-band + default, not 0 mm. +- `u_roof` receives the **joined all-roofs description** (can't tell which call is the + vaulted Ext1) and **no construction type** — needs the per-BP sloping/vaulted signal + threaded through (heat_transmission already computes `is_flat_roof`; add an + `is_sloping_ceiling`/vaulted flag similarly). +- **Code-5 "vaulted" is not recognised as a sloping ceiling** — only code-8 + "sloping ceiling" is (`_api_resolve_sloping_ceiling_thickness` gates on `==8`; + `heat_transmission` keys the cos(30°) area factor on the "sloping ceiling" substring). +- **Value ambiguity (needs case 11):** Table 18 (RdSAP 10 p.45) band J gives + col (1) joists/unknown **0.16**, col (2) **at rafters 0.20**, col (3) flat / + "sloping ceiling + unheated space above" (footnote b) **0.25**. Case 9 (code-8 + sloping, Unknown) → Elmhurst **0.25**, but that may not equal code-5 vaulted (could + be 0.20). All three give 0240 integer 72 — it's a PE-pin precision question. + +**Generate case 11:** a **vaulted Ext1 roof (`roof_construction` vaulted), insulation +Unknown/NI, band J**; report worksheet **`(30)` U** (0.20 or 0.25?). Then implement the +roof fix to match, re-pin 0240 (both bugs fixed → cont ≈ 72.31, integer 72). + +Simulated worksheet `(30)` decompositions seen so far: case 9 Ext1 sloping-Unknown = +0.25; case 7/8/10 roofs are all 400 mm loft (col 1, 0.11) so they do NOT exercise the +sloping path. + +--- + +## OPEN THREAD 2 — Community fuel-code collision (cert 9390), needs a **9390 worksheet** + +Cert **9390-2722-3520**: SAP +4 (calc 71 / lodged 67), PE ≈ matches (204 vs 205), but +**CO2 0.44 t vs lodged 2.8 t**. Community scheme (`sap_main_heating_code=301`, +`main_fuel_type=20`). + +**Root cause:** `co2_factor_kg_per_kwh` / `unit_price_p_per_kwh` / +`primary_energy_factor` in `tables/table_12.py` + `table_32.py` use an +"accept-either-API-or-Table-12-code" lookup that checks the **Table-12 code first**: + +```python +if fuel_code in CO2_KG_PER_KWH: # API 20 IS ALSO a T12 code (wood logs, 0.028) + return CO2_KG_PER_KWH[fuel_code] # ← returns 0.028, never translates +translated = API_FUEL_TO_TABLE_12.get(fuel_code) # 20 → 51 (community gas, 0.21) — unreached +``` + +API community fuels **18–25** (`API_FUEL_TO_TABLE_12`: 18→75,19→76,20→51,21→52,22→53, +23→55,24→54,25→41) all **collide** with Table-12 codes 18–25 (biomass), so they silently +get the biomass factor. The heat-network CO2 path +(`cert_to_inputs.py` ~L2801-2819: `_co2_factor_kg_per_kwh(main) * scaling`) thus emits +`0.028 × 1.25 (1/0.80 heat-source-eff) = 0.035` instead of `0.21 × 1.25`. + +**Why the fix needs care (don't just translate):** a naive translate of the heat-network +fuel via `API_FUEL_TO_TABLE_32` corrects CO2 (0.44 → 3.03 t, ≈ lodged 2.8) but +**over-corrects PE** (204 → 219.9 vs lodged 205) and **doesn't move the SAP +4** (cost +is a *separate* community issue — price/standing-charge/heat-source-eff scaling). The +community scheme's declared PE/CO2/cost factors must be pinned against a **9390 Elmhurst +worksheet** before committing. This touches the community-heating corpus (the "touchy" +S0380.180-184 area — see [[project_heating_systems_corpus]]); run the heating-systems +corpus test after any change. + +The right long-term shape: translate API→Table-12 **explicitly** at the known-API-code +boundary instead of the ambiguous T12-first "accept-either" — but verify it against the +whole cohort. + +--- + +## OPEN THREAD 3 — Cert 0390 +7 (demand-side), no worksheet needed to diagnose + +**0390-2954-3640**: SAP +7 (calc 67 / lodged 60), PE 137 vs **165** (−28/m²), CO2 12.3 vs +15. Large **360 m² age-F** detached. The boiler is **correctly** resolved (PCDB index +**9005** = "Firebird S", 86.4% winter — a real modern oil-boiler retrofit; not a bug). +The gap is a **demand-side under-count** (~−28 PE/m² = thousands of kWh). Suspects: +cavity wall "as built, **partial** insulation (assumed)" routing; age-F roof "insulated +(assumed)"; floor (solid, no insulation) 81 W/K; ventilation/TB. Walk the demand +cascade (§2.4 helpers + the lodged register subsystem ratings) to localise. This is a +known long-standing gap (see the cert's `notes:` in `test_golden_fixtures.py`). + +--- + +## Full audit — all golden certs with non-zero SAP residual + +| cert | SAP resid | diagnosis | +|---|---|---| +| 0390-2954-3640 | +7 | demand-side under-count (Thread 3); boiler eff correct | +| 9390-2722-3520 | +4 | community fuel-code collision → CO2 6× low (Thread 2) | +| 0240-0200-5706 | −1 | NOT a bug — unpreserved 2013+ pump; true SAP 72 | +| 2130-1033-4050 | +1 | minor fabric precision (multi-part solid-brick wall); low value | +| 7536-3827-0600 | +1 | minor fabric precision (multi-bp D/L/F cavity); low value | + +All others pin at residual 0. + +--- + +## Worksheets + +- **Available** (user-generated, `sap worksheets/golden fixture debugging/simulated case N/`): + case 7 (combi), case 8 (unknown pump), case 9 (sandstone wall + sloping roof), + case 10 (solid-brick wall). Case 7's Summary is the only one mirrored into tracked + fixtures (`backend/.../Summary_001431_case7.pdf`, used by the e2e pin). +- **Needed:** **case 11** (vaulted Ext1 roof, NI, band J — Thread 1) and a **9390** + worksheet (community PE/CO2/cost — Thread 2). + +## Pointers +- Golden pins + slice history: `tests/domain/sap10_calculator/rdsap/test_golden_fixtures.py`. +- Wall fix: `domain/sap10_calculator/worksheet/heat_transmission.py` + (`_described_as_retrofit_insulated`, the `wall_ins_present` gate) + + `tests/.../worksheet/test_heat_transmission.py`. +- Roof code: `domain/sap10_ml/rdsap_uvalues.py` `u_roof` (L708 + Table 18 dicts L637-668). +- Community/heat-network CO2/PE/cost: `cert_to_inputs.py` ~L2749-2880, L1837 + (`_main_fuel_code`), `tables/table_12.py` + `table_32.py` (`co2_factor_kg_per_kwh`, + `API_FUEL_TO_TABLE_12/32`). +- Process: one slice = one commit, spec citation (page+line), AAA tests, `abs(x-y)<=tol` + not `pytest.approx`, `Co-Authored-By: Claude Opus 4.8 `. + SAP 10.2 only. No tolerance widening. mapper.py + cert_to_inputs.py each carry 32 + pre-existing pyright errors; heat_transmission.py + rdsap_uvalues.py carry their own — + baseline-compare with `git stash` for net-zero NEW.