diff --git a/docs/sap-spec/HANDOVER_NEXT.md b/docs/sap-spec/HANDOVER_NEXT.md index 8f1eaace..bcc71ae8 100644 --- a/docs/sap-spec/HANDOVER_NEXT.md +++ b/docs/sap-spec/HANDOVER_NEXT.md @@ -1,14 +1,17 @@ -# Handover — §10a Fuel costs (xlsx ~614–740) +# Handover — Appendix L lighting closure → §11a / §12a / §13a sweep **For the agent picking up the next chunk of work.** Read this BEFORE invoking `/grill-me`. Read all of it. Caveman mode is the house style — terse, technical, no filler. Owner: `khalim@domna.homes`. Branch: `ara-backend-design-prd`. -This handover covers **the §10a Fuel costs worksheet block** — xlsx rows ~614–740, spec lines 8044–8084. Line refs (240)..(255). Mirrors the §9a slice you've just shipped: refactor existing `calculator.py` inline cost arithmetic into a worksheet-shape orchestrator with named line-ref outputs. +Two tickets in priority order: + +1. **Immediate — Appendix L lighting predictor swap.** Closes the residual +9.2% cost gap on 000474 that §4 HW exposed. ~1 slice, ~50 LoC. +2. **Section sweep — §11a / §12a / §13a "Individual heating systems incl micro-CHP".** Worksheet line refs (256)..(272). Builds the rating + CO2 + primary-energy per-end-use cascade on top of the §10a fuel-cost orchestrator. ~3 slices. Hard rules (unchanged): - **Caveman mode** house style. -- **Tolerance**: don't loosen test tolerances to make them pass. If the orchestrator can't hit the locked tolerance, pause and ask the user. +- **Tolerance**: don't loosen test tolerances to make them pass. If a refactor can't hit the locked tolerance, pause and ask the user. - **Spec PDFs**: don't scan more than ~50 lines without checking with the user. - **Commit per slice**, one slice = one commit, AAA test convention (`# Arrange / # Act / # Assert`), `Co-Authored-By: Claude Opus 4.7 ` trailer. @@ -19,207 +22,286 @@ Hard rules (unchanged): Last commits on this branch: ``` -15d6b781 pcdb followup: e2e mapper-chain regression test -7d4f3d78 pcdb followup: 000474 fixture lodges main_heating_index_number=16839; e2e ceiling 7 → 2 -1b43c95c pcdb followup: 000490 fixture lodges main_heating_index_number=10328 (Vaillant Ecotec Pro) -e63516cb docs: SPEC_COVERAGE PCDB integration row + slice progress + gap-list update -a104dd55 pcdb slice 3: cert_to_inputs precedence cascade — Table 105 overrides Table 4a/4b -23678228 pcdb slice 2: runtime gas_oil_boiler_record lookup via Table 105 NDJSON -fe04cd3a pcdb slice 1: pcdb10.dat ETL → 8 per-table NDJSON files + parser + 8 tests -53c393bf docs: SPEC_COVERAGE §9a row + slice progress table + PCDB gap-list update -380b6781 §9a slice 2: CalculatorInputs.energy_requirements + cert_to_inputs wiring (atomic) -2b5fc6a5 §9a slice 1: space_heating_fuel_monthly_kwh orchestrator + EnergyRequirementsResult -05d9dc73 docs: SPEC_COVERAGE §8f row + slice progress table -43cc16bc §8f slice 1: fabric_energy_efficiency_kwh_per_m2_yr + 6-fixture conformance -c9f15a2e docs: SPEC_COVERAGE §8c row + slice progress table -f3797066 §8c slice 3: CalculatorInputs + MonthlyEntry + SapResult + cert_to_inputs wiring (atomic) -3b9fa936 §8c slice 2: 6-fixture ALL_FIXTURES conformance with shared template constants -cf28eec4 §8c slice 1: space_cooling_monthly_kwh orchestrator + utilisation_factor_loss leaf +c9eb231a §4 HW slice 3: docs — SPEC_COVERAGE row + Remaining work + golden note +02fc9e4d §4 HW slice 2: Equation D1 monthly water-eff cascade +760e25de §4 HW slice 1: PCDB Table 3b combi-loss override +ae8c9461 docs: §10a slice 3 — ADR-0010 amendment + SPEC_COVERAGE row +adfa7f60 §10a slice 2: cert_to_inputs._fuel_cost + calculator delegation +0f255165 §10a slice 1: table_32 + table_12a + fuel_cost orchestrator +6d6767ce docs: handover for §10a Fuel costs + SPEC_COVERAGE PCDB-followup updates ``` -347 tests passing. 0 new pyright errors on touched modules. Sections §1, §3, §5, §6, §7, §8, §8c, §8f, §9a (single-main), §13 Full. PCDB Table 105 Gas/Oil Boilers wired into the efficiency cascade end-to-end (mapper → domain → calculator), 4 PCDB-listed corpus golden certs flowing through PCDB precedence. +486 tests passing on the domain package. Sections fully implemented: §1, §3, §4 (combi-gas + PCDB Table 3b + Eq D1), §5, §6, §7, §8, §8c, §8f, §9a (single-main), §10a (RdSAP10 Table 32 + Table 12a + note (a) standing), §13 SAP-rating equation. PCDB Tables 105 (gas/oil boilers) + combi-loss fields wired end-to-end. -**000490 + 000474 post-PCDB residuals (live, after this branch):** +**000474 + 000490 post-§4 HW residuals:** -| metric | 000490 actual | 000490 PDF | 000474 actual | 000474 PDF | +| metric | 000474 actual | 000474 PDF | 000490 actual | 000490 PDF | |---|---|---|---|---| -| SAP score | 63 | 57 (+6) | 63 | 62 (+1) | -| main_heating_fuel kWh | 13001.3 | 13003.85 (-0.02%) | 12304.8 | ~11968 (+2.8%) | -| total_fuel_cost £ | 706.23 | 807.54 (-12.5%) | 651.85 | 655.69 (-0.6%) | -| e2e SAP ceiling | 6 | — | 2 | — | +| SAP score | 59 | 62 (Δ-3) | 60 | 57 (Δ+3) | +| main_heating_fuel kWh | 12304.8 | ~11968 (+2.8%) | 13001.3 | 13003.85 (≤0.1%) | +| **HW fuel kWh** | **2291.6** ✓ | **2291.78 (≤0.1%)** | **2846.8** ✓ | **2850.57 (≤0.1%)** | +| lighting kWh | 528.1 | ~169 (back-derived) | ? (similar overshoot) | ? | +| total_fuel_cost £ | 715.83 | 655.69 (+9.2%) | 769.70 | 807.54 (−4.7%) | +| e2e SAP ceiling | 3 (was 4 post-§10a) | — | 3 (was 2 post-§10a) | — | -000474 is near-closure. 000490 widened on cost (not kWh) because the cert pre-dates the 14-Mar-2025 SAP10.2 amendment — pre-amendment gas unit prices were ~13% higher than what `domain.sap.tables.table_12.SAP_10_2_SPEC_PRICES` carries. ADR-0010 §3 Validation Cohort discipline says this is *known* spec-version drift, not a calculator regression. The fuel-kWh closure is the spec-faithful direction. +Two residuals remain: +- **000474 lighting overshoots ~3x** (528 vs ~169 back-derived). This is the dominant remaining cost residual — drives the +£60 over PDF. +- **000490 cost residual −4.7%** is spec-version drift per ADR-0010 §3 Validation Cohort (cert pre-dates 14-Mar-2025 amendment; PDF cost embeds older cert-assessor prices). HW kWh closure is the spec-faithful direction. --- -## §B — Ticket: §10a Fuel costs +## §B — Ticket 1: Appendix L lighting predictor swap ### B.1 Mission -Implement §10a (xlsx rows ~614–740) — produce per-end-use cost lines (240)..(255) via a new `worksheet/fuel_cost.py` orchestrator. Refactor `calculator.py`'s inline cost arithmetic to delegate to the orchestrator; expose worksheet-line-shape `FuelCostResult` on `CalculatorInputs`. Six Elmhurst fixtures already cover this implicitly via `total_fuel_cost_gbp` — just need to surface the per-line refs. +Replace the legacy `domain.ml.demand.predicted_lighting_kwh` (heuristic `9.3 × TFA × (1 − reduction)`) with the **spec-faithful Appendix L L1-L12 annual lighting kWh** that already exists in `domain.sap.worksheet.internal_gains`. Single slice. Closes 000474 cost residual from +9.2% toward ~0%. -### B.2 SAP10.2 spec anchors (lines 8044–8084) +### B.2 Why -| Line ref | Description | -|---|---| -| (240a)-(240e) | Space heating main 1 cost. Off-peak: (240a) Table 12a high-rate fraction, (240b) low-rate = 1-(240a), (240c) high-rate cost = (211)×(240a)×price×0.01, (240d) low-rate cost, (240e) collapses to other-fuel single-rate cost. | -| (241a)-(241e) | Space heating main 2 cost — zero-branch in scope A (no two-main fixtures). | -| (242a)-(242e) | Secondary heating cost. | -| (243)-(247) | Water heating cost. (243) high-rate fraction (Table 12 — note: not Table 12a; HW uses a different row). (244) = 1-(243). (245)/(246) high/low cost. (247) other-fuel single-rate. | -| (247a) | Instant electric shower (64a) × price. Zero in all 6 fixtures. | -| (248) | Space cooling cost (221) × price. Zero in all 6 fixtures (§8c f_C=0). | -| (249) | Pumps/fans/keep-hot cost (231) × price. Spec note: if off-peak, list each (230a)..(230g) separately by Table 12a. | -| (250) | Lighting cost (232) × price. | -| (251) | Additional standing charges (Table 12 note (a) — currently NOT applied for rating purposes for standard electricity tariff). | -| (252) | Energy saving/generation Appendix M/N items (PV credit, micro-CHP). Currently `pv_credit` is computed inline at calculator.py:282-283. | -| (253)/(254) | Appendix Q items (saved/used). Zero in our corpus. | -| (255) | Total = sum of all (240).. through (254). Currently `total_cost = max(0.0, main_heating_cost + ... − pv_credit)` at calculator.py:296. | +The Appendix L cascade is already implemented correctly for the **§5 internal-gains side** (used to compute (67) lighting gains as monthly watts). But the **cost side** (`inputs.lighting_kwh_per_yr`) reads the heuristic `predicted_lighting_kwh`. They diverge by ~3x on 000474. + +For 000474: TFA=71.55, N=1.8896, 8 low-energy bulbs (LED/CFL undistinguished). +- Heuristic: `9.3 × 71.55 × (1 − 0.45) ≈ 366` kWh/yr (with rough LED+CFL share assumption) → actual 528 lodged. +- Spec Appendix L cascade with daylight factor + λ_b + λ_req + topup: ~169 kWh/yr. ### B.3 Current state in code -Calculator does the cost arithmetic inline in `calculate_sap_from_inputs` (calculator.py:295-305): +[packages/domain/src/domain/ml/demand.py:222-243](packages/domain/src/domain/ml/demand.py#L222-L243) — `predicted_lighting_kwh` heuristic. Used by cert_to_inputs to populate `CalculatorInputs.lighting_kwh_per_yr`. + +[packages/domain/src/domain/sap/worksheet/internal_gains.py:208-265](packages/domain/src/domain/sap/worksheet/internal_gains.py#L208-L265) — `_lighting_gains_monthly_w` (private) builds `e_l_annual_kwh` (line 253) per Appendix L L1-L12, but **doesn't expose it**. The annual kWh figure is computed then converted to monthly W gains. + +Search for `e_l_annual_kwh` to confirm — it's a local variable inside the gains function. + +### B.4 Likely shape ```python -main_heating_cost = main_fuel_kwh * inputs.space_heating_fuel_cost_gbp_per_kwh -secondary_heating_cost = secondary_fuel_kwh * inputs.secondary_heating_fuel_cost_gbp_per_kwh -hot_water_cost = inputs.hot_water_kwh_per_yr * inputs.hot_water_fuel_cost_gbp_per_kwh -pumps_fans_cost = inputs.pumps_fans_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh -lighting_cost = inputs.lighting_kwh_per_yr * inputs.other_fuel_cost_gbp_per_kwh -total_cost = max(0.0, main_heating_cost + secondary_heating_cost + hot_water_cost + pumps_fans_cost + lighting_cost - pv_credit) +# packages/domain/src/domain/sap/worksheet/internal_gains.py + +def annual_lighting_kwh( + *, + total_floor_area_m2: float, + n_occupants: float, + fixed_lighting_capacity_lm: float, + fixed_lighting_efficacy_lm_per_w: float, + daylight_factor: float, +) -> float: + """SAP 10.2 Appendix L L1-L12 — annual lighting kWh/yr (cost side). + Mirrors the kWh internal to `_lighting_gains_monthly_w` but surfaces + it for `inputs.lighting_kwh_per_yr` to consume.""" + # extract the e_l_annual_kwh derivation; either factor it out into + # this fn and have the gains fn call it, OR have both fns share + # a small `_lighting_annual_kwh_components` helper. + ... ``` -Missing relative to spec: -1. **Table 12a high-rate fractions** for off-peak tariffs (electric storage / heat pumps) — currently `space_heating_fuel_cost_gbp_per_kwh` is a single scalar; for off-peak certs the spec splits into high-rate × fraction + low-rate × (1-fraction). The cert_to_inputs `_off_peak_main_heating_cost(...)` path collapses both into the effective per-kWh cost. -2. **(251) Standing charges** — Table 12 note (a) excludes standing charges from the rating ECF in the standard tariff case but includes them when a non-standard tariff is selected. Currently not applied at all. -3. **(252) Per-row Appendix M/N** — currently single `pv_credit` scalar. Spec wants a row per (233a)-(235d) (PV / wind / hydro / micro-CHP). -4. **Worksheet-line surfacing** — none of (240a)..(254) are exposed individually. Only the (255) total bubbles up via `total_fuel_cost_gbp`. - -### B.4 Likely shape (mirror §9a pattern — composite slot on CalculatorInputs) +Then `cert_to_inputs` swaps the call: ```python -# packages/domain/src/domain/sap/worksheet/fuel_cost.py +# Before: +lighting_kwh = predicted_lighting_kwh( + total_floor_area_m2=epc.total_floor_area_m2, + cfl_count=epc.cfl_fixed_lighting_bulbs_count, + led_count=epc.led_fixed_lighting_bulbs_count, + incandescent_count=epc.incandescent_fixed_lighting_bulbs_count, +) +# After: reuse the §5 cascade inputs (already computed for internal_gains_from_cert). +lighting_kwh = annual_lighting_kwh( + total_floor_area_m2=epc.total_floor_area_m2, + n_occupants=..., # Appendix J Table 1b (already computed for §4) + fixed_lighting_capacity_lm=..., # cert-derived, mirror §5 path + fixed_lighting_efficacy_lm_per_w=..., # cert-derived, mirror §5 path + daylight_factor=..., # L2a/L2b — already computed for §5 +) +``` + +The cert→Appendix-L-inputs derivation already lives in `internal_gains_from_cert` (called from cert_to_inputs at line ~1146). Reuse it. + +### B.5 Slice plan + +``` +S1 — annual_lighting_kwh free function in worksheet/internal_gains.py + (factor out e_l_annual_kwh from _lighting_gains_monthly_w; both + callers share the derivation). Synthetic unit test pins 000474 + PDF value (~169 kWh/yr) given the cert's bulb counts. +S2 — cert_to_inputs swaps lighting_kwh derivation from predicted_lighting_ + kwh to annual_lighting_kwh. Two ALL_FIXTURES e2e closures: 000474 + cost residual closes +9.2% → ~0% (ceiling 3 → 1 or below); + 000490 ceiling re-checks. +S3 — docs: SPEC_COVERAGE Appendix L row + ADR-0010 amendment if needed. +``` + +### B.6 Tests + +- Synthetic: `annual_lighting_kwh(TFA=71.55, N=1.8896, C_L_fixed=..., ε_fixed=..., D=...) == 169 ± 5` against a hand-computed Appendix L example. +- ALL_FIXTURES: 6 fixtures already pin lighting kWh tuples via §5 — those don't change. The cost-side closure shows in `inputs.lighting_kwh_per_yr` matching the §5 internal `e_l_annual_kwh` exactly. +- e2e: 000474 SAP score ceiling tightens 3 → 1 (or below). + +### B.7 Don't list + +- Don't delete `predicted_lighting_kwh` immediately — leave it in `domain.ml.demand` with a deprecation comment in case external callers rely on it. Future slice rips it. +- Don't change the §5 lighting GAINS path. The kWh figure feeds both gains and cost — just expose it. + +--- + +## §C — Ticket 2: §11a / §12a / §13a "Individual heating systems incl micro-CHP" + +### C.1 Mission + +Build the per-end-use rating + CO2 + primary-energy cascade on top of the §10a `FuelCostResult`. SAP 10.2 worksheet line refs: + +- **§11a SAP rating** — (256), (257), (258). Spec lines ~8085-8110. +- **§12a CO2 emissions** — (259)..(265). Spec lines ~8110-8150. Per-end-use CO2 mirroring §10a's (240)..(250) cost structure. +- **§13a Primary Energy** — (266)..(272). Same per-end-use mirror but with primary-energy factors. + +The calculator currently aggregates these inline (calculator.py:441-505 has `co2 = main_heating_co2 + secondary_heating_co2 + ...` and `primary_energy_kwh = max(0, space_heating_primary_kwh + ... − pv_primary_offset_kwh)`). The cascade is correct at aggregate level but does NOT produce the per-end-use line refs in worksheet shape. + +Worksheet-shape fidelity rule (memory `feedback_worksheet_shape_fidelity`): mirror SAP10.2 sections completely with per-line dataclasses, even when no consumer needs the per-line detail. + +### C.2 SAP10.2 spec anchors + +User needs to point at PDF pages — likely around pages 32-38 for §11a/§12a/§13a (after §10 fuel costs at pages 29-32). The xlsx at the repo root maps line refs to cells. + +### C.3 Likely shape + +Mirror §10a's pattern: + +```python +# packages/domain/src/domain/sap/worksheet/sap_rating.py @dataclass(frozen=True) -class FuelCostResult: - """SAP 10.2 §10a worksheet line refs (240)..(255).""" - # Main system 1 (off-peak split) - main_1_high_rate_fraction: float # (240a) - main_1_low_rate_fraction: float # (240b) - main_1_high_rate_cost_gbp: float # (240c) - main_1_low_rate_cost_gbp: float # (240d) - main_1_other_fuel_cost_gbp: float # (240e) - main_1_total_cost_gbp: float # (240) — sum - # Main system 2 — zero-branch placeholders - main_2_high_rate_fraction: float # (241a) - main_2_low_rate_fraction: float # (241b) - main_2_high_rate_cost_gbp: float # (241c) - main_2_low_rate_cost_gbp: float # (241d) - main_2_other_fuel_cost_gbp: float # (241e) - main_2_total_cost_gbp: float # (241) - # Secondary - secondary_high_rate_fraction: float # (242a) - ... # (242b)..(242e) - secondary_total_cost_gbp: float # (242) - # Water heating - water_high_rate_fraction: float # (243) - water_low_rate_fraction: float # (244) - water_high_rate_cost_gbp: float # (245) - water_low_rate_cost_gbp: float # (246) - water_other_fuel_cost_gbp: float # (247) - water_total_cost_gbp: float # sum - # Other end-use rows - instant_shower_cost_gbp: float # (247a) - space_cooling_cost_gbp: float # (248) - pumps_fans_cost_gbp: float # (249) - lighting_cost_gbp: float # (250) - standing_charges_gbp: float # (251) — zero in standard tariff - pv_credit_gbp: float # (252) — keep negative - appendix_q_saved_gbp: float # (253) — zero in corpus - appendix_q_used_gbp: float # (254) — zero in corpus - total_cost_gbp: float # (255) — Σ minus PV +class SapRatingResult: + """SAP 10.2 §11a line refs (256)..(258).""" + ecf_gbp_per_m2_per_yr: float # (256) + sap_rating_continuous: float # (257) un-rounded + sap_rating_integer: int # (258) rounded + +def sap_rating_from_fuel_cost(...) -> SapRatingResult: + ... + +# packages/domain/src/domain/sap/worksheet/co2_emissions.py +@dataclass(frozen=True) +class Co2EmissionsResult: + """SAP 10.2 §12a line refs (259)..(265). Per-end-use CO2 in kg/yr.""" + main_1_co2_kg_per_yr: float # (259) + main_2_co2_kg_per_yr: float # (260) + secondary_co2_kg_per_yr: float # (261) + hot_water_co2_kg_per_yr: float # (262) + pumps_fans_co2_kg_per_yr: float # (263) + lighting_co2_kg_per_yr: float # (264) + pv_credit_co2_kg_per_yr: float # (265) — negative + total_co2_kg_per_yr: float # sum, clamped >=0 + +# packages/domain/src/domain/sap/worksheet/primary_energy.py +@dataclass(frozen=True) +class PrimaryEnergyResult: + """SAP 10.2 §13a line refs (266)..(272). Per-end-use kWh primary/yr.""" + # mirror Co2EmissionsResult shape with primary energy factors + ... ``` -Slice plan suggestion (mirror §9a 3-slice cadence): +Per-end-use CO2 factors source from **Table 12 / Table 32** (same values per RdSAP10 §19.2). Per-end-use PEF source from **Table 12 / Table 32** primary energy factor column. + +### C.4 Calculator coupling + +Mirror §10a path (i) — cert_to_inputs precompute. `CalculatorInputs` gains: +- `sap_rating: SapRatingResult` +- `co2_emissions: Co2EmissionsResult` +- `primary_energy: PrimaryEnergyResult` + +`calculate_sap_from_inputs` reads them; deletes the inline CO2 + primary-energy blocks. The `SapResult` fields stay (sap_score, co2_kg_per_yr, primary_energy_kwh_per_yr, etc.) — populated from the composite slots. + +### C.5 Slice plan ``` -Slice 1 — FuelCostResult + fuel_cost_from_energy_requirements orchestrator (no off-peak split: - assume single rate; all 28 fields populated). Synthetic test pins (255) total matches the - calculator's current inline math. -Slice 2 — CalculatorInputs.fuel_cost composite slot + cert_to_inputs wiring + calculator delegation - (calculator.py:295-305 becomes `total_cost = inputs.fuel_cost.total_cost_gbp`). Round-trip - test asserts SapResult.total_fuel_cost_gbp unchanged from pre-refactor. -Slice 3 — Table 12a high-rate fraction lookup + off-peak split for main heating / HW / pumps/fans. - Touches the 4 cert paths that lodge off-peak meter_type. Spec (251) standing charges - deferred (Table 12 note (a) — needs separate slice gated on the standard-tariff rule). -Slice 4 — docs: SPEC_COVERAGE §10a row + slice progress table. +Slice 1 — worksheet/co2_emissions.py: Co2EmissionsResult + orchestrator + Table 12/32 CO2 factor lookups (or reuse table_12.co2_factor_kg_per_kwh). Synthetic unit tests + 6-fixture conformance via SapResult.co2_kg_per_yr parity. +Slice 2 — worksheet/primary_energy.py: PrimaryEnergyResult + orchestrator. Same shape. +Slice 3 — worksheet/sap_rating.py: SapRatingResult (refactor of worksheet/rating.py: existing `energy_cost_factor` + `sap_rating` + `sap_rating_integer` move into a single orchestrator that takes a FuelCostResult and returns the result dataclass). +Slice 4 — CalculatorInputs composite slots + cert_to_inputs wiring + calculator delegation (drop inline CO2/PE blocks). Re-verify 6-fixture e2e. +Slice 5 — docs: SPEC_COVERAGE rows for §11a / §12a / §13a, ADR-0010 amendment if cost target needs cross-ref. ``` -### B.5 What to defer (name as remaining-work in slice 4) +### C.6 Tests -- **(251) Additional standing charges** — Table 12 note (a) rule. Currently zero. Needs separate slice covering when standing charges enter the rating ECF. -- **(252) Per-Appendix-M/N row split** — keep `pv_credit_gbp` as a single scalar in scope A; expand to per-line (233a)..(235d) when wind / hydro / micro-CHP fixtures land. -- **(253)/(254) Appendix Q** — zero placeholders; populate when a Q-item cert lands. -- **(247a) Instant electric shower** — line (64a) currently always 0 from `_hot_water_fuel_kwh_per_yr`. Wire when electric-shower cert lodges. -- **Off-peak per-row (230a)..(230g) split** — spec line 8076 says "if off-peak tariff, list each of (230a) to (230g) separately and apply fuel price according to Table 12a". Out of scope; requires the §9a (230a)-(230h) Table 4f pumps/fans breakdown first. +- Synthetic: per-orchestrator unit tests (kWh × factor arithmetic per end-use, PV credit sign, total clamps). +- 6-fixture ALL_FIXTURES: re-pin `SapResult.co2_kg_per_yr` + `primary_energy_kwh_per_yr` per fixture if PDF lodges them. +- e2e ceiling on 000474 SAP score: should stay at 1 (or wherever §B closes it). -### B.6 Calculator coupling +### C.7 Don't list -Mirror §9a path (i) — cert_to_inputs precompute. `CalculatorInputs.fuel_cost: FuelCostResult` composite slot. `calculate_sap_from_inputs` collapses to: - -```python -fuel_cost = inputs.fuel_cost -total_cost = fuel_cost.total_cost_gbp -# (existing co2 + primary energy paths consume `main_fuel_kwh` etc. as before — they're orthogonal) -``` - -Existing scalar CalculatorInputs fields (`space_heating_fuel_cost_gbp_per_kwh`, etc.) stay as inputs to the orchestrator. Calculator stops reading them — symmetric with how §9a left `main_heating_efficiency` in place but stopped using it after `energy_requirements` landed. - -### B.7 Tests - -- **Synthetic** unit tests on the orchestrator: single-rate cost = kWh × £/kWh × 1.0; off-peak split honours Table 12a fractions; PV credit subtracts at the spec-correct precedence; zero-branch lines stay zero. -- **6-fixture ALL_FIXTURES** at orchestrator level — call with fixture LINE_211 / LINE_215 / LINE_219 / lighting / pumps_fans pins + SAP_10_2_SPEC_PRICES, assert (255) total matches PDF cost. **Will partially fail for 000490** (spec-version drift). Recommend pinning per-fixture LINE_255_TOTAL_FUEL_COST_GBP at the **current state** (after PCDB) so future improvements show as tightening; document the 000490 spec-version delta in the docstring as already done for the e2e SAP score test. -- **Cert-round-trip** in `test_cert_to_inputs.py`: build a typical cert, run cert_to_inputs → calculator, assert `inputs.fuel_cost.total_cost_gbp == result.total_fuel_cost_gbp` to float equality (pre-refactor parity). - -### B.8 Don't list - -- Do **not** touch the existing fuel-cost-per-kWh wiring in `cert_to_inputs._space_heating_fuel_cost_gbp_per_kwh / _hot_water_fuel_cost_gbp_per_kwh / _other_fuel_cost_gbp_per_kwh`. These are the orchestrator's *inputs*; keep them. -- Do **not** modify SAP_10_2_SPEC_PRICES — the Table 12 prices are spec-current. The 000490 cost gap is spec-version drift, not a price-table bug. -- Do **not** loosen e2e tolerance ceilings on 000490 / 000474 unless the refactor breaks parity. The current ceilings (6 / 2) reflect post-PCDB state and should hold across a pure refactor. +- Don't touch §13 `worksheet/rating.py` Equation 7-9 logic — the formulas are spec-faithful and already pinned. The refactor moves them under a `sap_rating_from_fuel_cost` orchestrator. +- Don't introduce a new CO2 factor table — use the existing `table_12.co2_factor_kg_per_kwh`. RdSAP10 §19.2 says CO2 is unchanged from SAP10.2 Table 12. +- Don't widen e2e ceilings to absorb refactor regressions. If anything regresses, pause + ask. --- -## §C — Codebase pointers +## §D — Codebase pointers -- §9a orchestrator template: [packages/domain/src/domain/sap/worksheet/energy_requirements.py](../../packages/domain/src/domain/sap/worksheet/energy_requirements.py) — 16-field dataclass + free function. Mirror this shape for `FuelCostResult`. -- Calculator inline cost code (target of refactor): [packages/domain/src/domain/sap/calculator.py:280-305](../../packages/domain/src/domain/sap/calculator.py#L280-L305). -- cert_to_inputs cost-per-kWh resolution: [packages/domain/src/domain/sap/rdsap/cert_to_inputs.py](../../packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) — search for `_space_heating_fuel_cost_gbp_per_kwh` etc. -- Table 12 lookups: [packages/domain/src/domain/sap/tables/table_12.py](../../packages/domain/src/domain/sap/tables/table_12.py). -- Existing PCDB integration (recent precedent for new ETL/lookup pattern): [packages/domain/src/domain/sap/tables/pcdb/](../../packages/domain/src/domain/sap/tables/pcdb/). -- §9a tests: [packages/domain/src/domain/sap/worksheet/tests/test_energy_requirements.py](../../packages/domain/src/domain/sap/worksheet/tests/test_energy_requirements.py). -- e2e SAP score tests: [packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py](../../packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py). -- SPEC_COVERAGE: [docs/sap-spec/SPEC_COVERAGE.md](SPEC_COVERAGE.md). -- ADR-0009 / ADR-0010: [docs/adr/0009-deterministic-sap-calculator.md](../adr/0009-deterministic-sap-calculator.md), [docs/adr/0010-sap10-calculator-spec-target-and-validation.md](../adr/0010-sap10-calculator-spec-target-and-validation.md). +### Appendix L lighting (ticket 1) +- Legacy heuristic: [packages/domain/src/domain/ml/demand.py:222](packages/domain/src/domain/ml/demand.py#L222) `predicted_lighting_kwh` +- Spec cascade (gains side): [packages/domain/src/domain/sap/worksheet/internal_gains.py:204](packages/domain/src/domain/sap/worksheet/internal_gains.py#L204) `_lighting_gains_monthly_w` — extract `e_l_annual_kwh` +- cert_to_inputs callsite: [packages/domain/src/domain/sap/rdsap/cert_to_inputs.py](packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) — search for `predicted_lighting_kwh` + +### §11a/§12a/§13a sweep (ticket 2) +- §10a orchestrator (template): [packages/domain/src/domain/sap/worksheet/fuel_cost.py](packages/domain/src/domain/sap/worksheet/fuel_cost.py) — mirror the kwargs-orchestrator + Result dataclass shape. +- Existing inline CO2 + primary-energy: [packages/domain/src/domain/sap/calculator.py:467-505](packages/domain/src/domain/sap/calculator.py#L467-L505). +- SAP rating equations: [packages/domain/src/domain/sap/worksheet/rating.py](packages/domain/src/domain/sap/worksheet/rating.py) — `energy_cost_factor`, `sap_rating`, `sap_rating_integer`. +- Table 12 factors: [packages/domain/src/domain/sap/tables/table_12.py](packages/domain/src/domain/sap/tables/table_12.py) — CO2 + PEF columns already typed. + +### Spec / docs +- SAP10.2 PDF: `docs/sap-spec/sap-10-2-full-specification-2025-03-14.pdf`. §11/§12/§13 are around pages 32-38 (ask the user for precise spec-page anchors before scanning). +- RdSAP10 PDF: `docs/sap-spec/RdSAP 10 Specification 10-06-2025.pdf`. §19.2 confirms CO2 + PEF match SAP10.2 Table 12. +- ADR-0010 (cost target): [docs/adr/0010-sap10-calculator-spec-target-and-validation.md](docs/adr/0010-sap10-calculator-spec-target-and-validation.md). Carries the §10a amendment + handover-time deferred list. +- SPEC_COVERAGE: [docs/sap-spec/SPEC_COVERAGE.md](docs/sap-spec/SPEC_COVERAGE.md). + +### Fixtures +- 000474 + 000490 are the two fixtures with PDF e2e expectations (`test_e2e_elmhurst_sap_score.py`). 4 others (000477, 000480, 000487, 000516) only pin worksheet-section LINE_* values. +- Lighting fixture inputs: each `_elmhurst_worksheet_*.py` has CFL/LED/incandescent counts on the cert build_epc(). --- -## §D — Skills +## §E — Skills The dev container ships `/grill-me`, `/tdd`, `/caveman`. Default flow: ``` -/grill-me → walk down design tree, recommend scope cuts -/tdd implement §10a Fuel costs → one test → one impl → repeat +/grill-me → walk down design tree, recommend scope cuts +/tdd implement Appendix L lighting swap → one test → one impl → repeat ``` -User invokes `/grill-me` first when ready. Audit B.5 / B.7 / B.8 + surface unknowns first; wait for `/grill-me`. +Same chain for §11a/§12a/§13a. Consider `/grill-with-docs` if domain language drifts. --- -## §E — Definitely do NOT +## §F — Definitely do NOT -- Do **not** touch the §10a-adjacent §11a (256-258) rating chain — already Full. -- Do **not** scan more than ~50 lines of spec PDF without asking the user for the specific table/page. -- Do **not** modify existing fixture `build_epc()` to change boiler / PCDB lodgement — only 000490 + 000474 carry PCDB pointers per real PDF lodgement. Other 4 fixtures don't lodge PCDB and stay on Table 4a defaults until the user provides a PCDB code. -- Do **not** loosen test tolerances to make tests pass — ask the user. -- Do **not** invoke `/ultrareview` yourself. -- Do **not** widen the e2e SAP-score ceiling on 000474 (currently 2). It's tight on purpose post-PCDB. +- Do **not** loosen the 000474 e2e ceiling 3 → 4 etc. to mask the lighting overshoot. The whole point of the Appendix L slice is to close that ceiling further. +- Do **not** scan more than ~50 lines of spec PDF without asking the user for the specific page/table range. +- Do **not** touch `inputs.fuel_cost` (the §10a precompute) or `inputs.energy_requirements` (the §9a precompute). They're load-bearing and tested. +- Do **not** invoke `/ultrareview` yourself — it's user-triggered. +- Do **not** delete the legacy scalar cost fields (`space_heating_fuel_cost_gbp_per_kwh` etc.) from `CalculatorInputs` in this work — they're a synthetic-test backwards-compat shim per the §10a slice-2c fallback. Rip out only in a dedicated cleanup ticket after the synthetic test corpus migrates. + +--- + +## §G — Known follow-ups (named on §10a + §4 HW deferred lists) + +Reference: ADR-0010 amendment "Deferred work" + SPEC_COVERAGE §4 HW / §10a "Remaining work" sections. + +**Worksheet:** +- Table 12a `Table12aSystem` cert→row mapping for off-peak electric mains (currently zero-sentinel fallback). +- Table 13 immersion / HP-DHW-only WH fractions. +- Off-peak per-row (230a)-(230g) Table 12a split for pumps/fans (spec line 8076). +- (247a) Instant electric shower kWh routing. +- (252) per-row Appendix M/N split (PV / wind / hydro / micro-CHP). +- (253)/(254) Appendix Q routes. +- §4 cylinder + solar + WWHRS + PV diverter + FGHRS branches. +- §4 PCDB Table 3b storage / FGHRS rows + Table 3c two-profile + Electric CPSU Appendix F. + +**Infra / cleanup:** +- Drop legacy scalar fuel-cost fields from `CalculatorInputs` once synthetic test corpus migrates to `fuel_cost=...`. +- §11 FEE compliance conditions (different ventilation / HW / lighting / gains column) — relevant for new-build compliance, not the rating sweep. +- Heat-pump Appendix N cascade via PCDB Table 362 (replaces SCOP 2.30 Table 4a fallback for `main_category=4`). +- Table D1/D2/D3 condensing-boiler control-class corrections (Ecodesign). + +--- + +End of handover. Read in full before `/grill-me`.