docs: handover for §10a Fuel costs + SPEC_COVERAGE PCDB-followup updates

HANDOVER_NEXT.md rewritten for §10a Fuel costs (xlsx rows ~614-740, spec lines 8044-8084). Covers (240)..(255) line refs — refactor calculator.py inline cost arithmetic into a worksheet-shape `FuelCostResult` orchestrator following the §9a precedent. Three-slice plan: orchestrator + dataclass + synthetic, atomic CalculatorInputs/cert_to_inputs wiring, Table 12a off-peak split.

SPEC_COVERAGE PCDB slice progress table picks up the two fixture-lodgement commits + the mapper-chain regression test; updated narrative confirms no domain-model changes were needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-21 13:49:04 +00:00
parent 15d6b78149
commit 6d6767ce62
2 changed files with 174 additions and 154 deletions

View file

@ -1,207 +1,225 @@
# Handover — §8c Space cooling + 000490 SAP-score diagnostic
# Handover — §10a Fuel costs (xlsx ~614740)
**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 **two tickets in order**:
This handover covers **the §10a Fuel costs worksheet block** — xlsx rows ~614740, spec lines 80448084. 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.
1. **Ticket A — Diagnose + (best-effort) close the 000490 e2e SAP-score gap.** §8 wiring exposed a +3 SAP overshoot. The previous agent claimed "the easy win is to wire `water_heating_from_cert` into `cert_to_inputs`" — that claim was **wrong** (it's already wired, see §A.2 below). The real gap is in upstream precision and is multi-source. Ticket A is to investigate and chip away, with explicit user check-ins before changing pinned expected values.
2. **Ticket B — Implement §8c Space cooling.** xlsx rows 435466, worksheet line refs (100)..(108). All 6 Elmhurst fixtures = 0 cooling. Should be a small slice.
Hard rules (same as previous handovers):
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.
- **Spec PDFs**: don't scan more than ~50 lines without checking with the user.
- **Fixture `build_epc()`**: don't modify — handover §11 of the original §6 handover is still in force. Local `_build_section_*_epc(fixture)` wrappers if you need to override windows.
- **Commit per slice**, one slice = one commit, AAA test convention (`# Arrange / # Act / # Assert`), Co-Authored-By trailer.
- **Commit per slice**, one slice = one commit, AAA test convention (`# Arrange / # Act / # Assert`), `Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>` trailer.
---
## §A — Ticket A: 000490 SAP-score gap diagnostic
## §A — Current state (what just shipped)
### A.1 Current state
After §8 wiring (commit `f6ab7626`) and `_currently_within_3_points` test update (in `f6ab7626` too), 000490 cert-driven calculator output is:
Last commits on this branch:
| Metric | Calculator | PDF | Delta |
|---|---|---|---|
| SAP integer | 60 | 57 | +3 |
| Continuous SAP | 60.22 | 57.40 | +2.82 |
| Annual space heating kWh | 11467.18 | 11183.275 | +2.5% |
| Annual HW fuel kWh | 3090.47 | 2850.57 | +8.4% |
| Total fuel cost £ | 756.99 | 807.54 | 6.3% |
| ECF | 2.4538 | 3.0539 | 20% |
### A.2 What's already wired (do NOT redo)
- ✅ `water_heating_from_cert` IS the active path in `cert_to_inputs._hw_kwh_and_gains_from_cert` (file `cert_to_inputs.py`, line ~738). Legacy `predicted_hot_water_kwh` is a fallback only when `epc.total_floor_area_m2 is None`. For Elmhurst fixtures TFA is always present.
- ✅ `internal_gains_from_cert` (§5), `solar_gains_from_cert` (§6), `mean_internal_temperature_monthly` (§7), `space_heating_monthly_kwh` (§8) all wired.
- ✅ Per-fixture orchestrator-level conformance: all 6 Elmhurst fixtures pass §3§8 at line-ref level (see `test_*.py::test_*_matches_elmhurst_worksheet_all_fixtures` tests).
### A.3 Where the gap actually is
The drift is in **scalar derivations the cert→inputs adapter computes from cert codes** — not in the per-month physics. Verified:
```python
inputs = cert_to_inputs(_w000490.build_epc())
inputs.main_heating_efficiency # → 0.8000
# PDF Vaillant Ecotec Pro Manufacturer-declared: 0.882
```
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
```
Drivers (ordered by likely SAP impact):
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.
1. **Water-heating efficiency** (`cert_to_inputs._legacy_water_heating_efficiency`)
- Currently selects ~0.78-0.80 from a static SAP10 efficiency table by `sap_main_heating_code`.
- PDF uses the **Manufacturer-declared** value (Vaillant Ecotec Pro 88.2%) from PCDB / cert efficiency lodgement.
- Look for: cert field carrying the manufacturer efficiency directly, OR the PCDB lookup that's currently stubbed (per ADR-0009 grill: `NoOpPcdbLookup`).
- Fixing this should drop HW fuel from 3090 → ~2750 kWh, closing ~340 kWh on annual fuel.
**000490 + 000474 post-PCDB residuals (live, after this branch):**
2. **Space heating delivered = q_heat / main_heating_efficiency**
- Same efficiency cascade. PDF (210) Jan = 88.2% throughout. Ours = 80%.
- q_heat itself is correct (we just nailed §8 to 1e-1 kWh).
- Fixing efficiency drops `main_heating_fuel_kwh_per_yr` from 14334 → ~13000 kWh.
| metric | 000490 actual | 000490 PDF | 000474 actual | 000474 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 | — |
3. **§3 transmission HLC precision** (~+2.5% on annual space heating)
- Calculator's `inputs.heat_transmission.total_w_per_k` = 236.6211 (matches PDF LINE_37).
- But cert_to_inputs derives `total_w_per_k` from windows + walls + roof + floor + thermal_bridging. If any of those is off, HLC drifts → §7 MIT drifts → §8 space heating drifts.
- This is much smaller than the efficiency gap.
### A.4 Recommended approach for Ticket A
**Do not** attempt to fix the entire efficiency cascade in one go — it touches §4 + §9 + §12 simultaneously and is a multi-day rewrite.
**Do** spend ~30 minutes investigating and either:
a) **Find a cert field that carries Manufacturer efficiency directly** that we're not reading (most likely: `epc.sap_heating.main_heating_details[0].efficiency_declared_pct` or similar). If present, wire it through `cert_to_inputs._main_heating_efficiency_pct` as a precedence-over-table override. Single-slice fix. **Surface findings to the user before implementing.**
b) **If no such cert field exists**, write a one-paragraph finding into `SPEC_COVERAGE.md` "Prioritised gap list" under "Boiler efficiency Manufacturer override" + ship as a known gap. Skip to Ticket B.
The user explicitly said in the previous turn: *"this is silly"* about the 000490 overshoot — meaning they want progress on it, but understand it might not be a one-line fix. Bias toward (a) if cheap; flag and defer if expensive.
### A.5 Don't update e2e tolerances during Ticket A
The `_currently_within_3_points` ceiling for 000490 should stay 3 until Ticket A actually moves it. Tightening to 2 / 1 only after physics improves; loosening is a regression.
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.
---
## §B — Ticket B: §8c Space cooling
## §B — Ticket: §10a Fuel costs
### B.1 Mission
Implement §8c (xlsx rows 435466) — produce `cooling_monthly_kwh` for all 12 months. All 6 Elmhurst fixtures = 0 (no air conditioning lodged). Should be the smallest section yet.
Worksheet lines you must close:
- (100)m heat loss rate L_m (cooling) — recomputed via §7 chain at cooling Ti (≈ same as heating MIT for our fixtures)
- (101)m utilisation factor for loss η_loss (cooling Table 10a, formula differs from Table 9a)
- (102)m useful loss = η × L
- (103)m gains (excluding pumps/fans Table 5a per spec — see §6 of the SAP10.2 PDF)
- (104)m cooling requirement Q_cool
- ∑(104) over Jun-Aug only (cooling inclusion rule — June to August, disregarding September to May)
- (105) cooled-area fraction f_C (almost always 0 in RdSAP — set 0 for all 6 fixtures)
- (106)m × intermittency factor (Table 10b)
- (107)m space cooling requirement after f_C
- (108) cooling per m² = (107) / TFA
Implement §10a (xlsx rows ~614740) — 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.
### B.2 SAP10.2 spec anchors (already extracted)
### B.2 SAP10.2 spec anchors (lines 80448084)
From the spec at `/tmp/sap102.txt`:
- Line 10320: `Qcool = 0.024 × (Gm - mLm) × nm × fcool × fintermittent` (note the SIGN — gains losses, opposite of heating)
- Line 10321: `Set Qcool to zero if negative or less than 1 kWh.`
- Line 10325: `Include the cooling requirements for each month from June to August (disregarding September to May).`
| 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. |
Table 10a (utilisation factor for cooling) is a separate formula from Table 9a heating η — page 186 of SAP10.2 spec.
Table 10b (intermittency factor) — page 187.
Table 10c (SEER for cooling system fuel calc) — page 187.
### B.3 Current state in code
### B.3 Existing state
- No `space_cooling.py` module exists.
- Calculator has no cooling fields.
- All 6 Elmhurst fixtures have `has_fixed_air_conditioning=False` → no cooling system → cooling fuel = 0.
### B.4 Likely shape (mirror §8 pattern)
Calculator does the cost arithmetic inline in `calculate_sap_from_inputs` (calculator.py:295-305):
```python
@dataclass(frozen=True)
class SpaceCoolingResult:
heat_loss_rate_monthly_w: tuple[float, ...] # (100)
utilisation_factor_loss_monthly: tuple[float, ...] # (101) — Table 10a
useful_loss_monthly_w: tuple[float, ...] # (102)
cooling_gains_monthly_w: tuple[float, ...] # (103)
cooling_requirement_monthly_kwh: tuple[float, ...] # (104)
intermittency_factor_monthly: tuple[float, ...] # (106)
space_cooling_monthly_kwh: tuple[float, ...] # (107) ← what the calculator consumes
space_cooling_kwh_per_yr: float
space_cooling_per_m2_kwh: float # (108)
def space_cooling_monthly_kwh(
*,
monthly_heat_transfer_coefficient_w_per_k: tuple[float, ...],
monthly_internal_temperature_c: tuple[float, ...],
monthly_external_temperature_c: tuple[float, ...],
monthly_total_gains_w: tuple[float, ...],
total_floor_area_m2: float,
cooled_area_fraction: float = 0.0, # f_C — almost always 0 for RdSAP
# Table 10b intermittency factor by control type, default to 0.25 (intermittent)
intermittency_factor: float = 0.25,
) -> SpaceCoolingResult: ...
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)
```
### B.5 Conformance budget
**All 6 fixtures = 0 cooling** for every line. So conformance is trivial: assert every per-line tuple is zero. The interesting test is the **synthetic positive case** — pin a non-zero cooled-area fraction + warm summer month + verify Q_cool emerges, then verify with `f_C = 0` it collapses to zero.
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)
```python
# packages/domain/src/domain/sap/worksheet/fuel_cost.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
```
Slice plan suggestion (mirror §9a 3-slice cadence):
```
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.
```
### B.5 What to defer (name as remaining-work in slice 4)
- **(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.
### B.6 Calculator coupling
After orchestrator: add `CalculatorInputs.space_cooling_monthly_kwh: tuple[float, ...]` field (defaults `(0.0,) * 12` for backwards-compat). The calculator's `MonthlyEntry` already has `space_heat_requirement_kwh`; either add `space_cool_requirement_kwh` or just expose the annual total on `SapResult`. Mirror §8 pattern.
### B.7 Slice plan (suggested)
Mirror §9a path (i) — cert_to_inputs precompute. `CalculatorInputs.fuel_cost: FuelCostResult` composite slot. `calculate_sap_from_inputs` collapses to:
```
Slice 1 — SpaceCoolingResult + space_cooling_monthly_kwh orchestrator
(Table 10a η_loss + Jun-Aug inclusion rule); synthetic positive
test (non-zero f_C, hot July) + synthetic zero test (f_C=0)
Slice 2 — Per-fixture SECTION_8C_COOLED_AREA_FRACTION=0 constants +
ALL_FIXTURES e2e (every line tuple = 0 across all 6 fixtures)
Slice 3 — CalculatorInputs.space_cooling_monthly_kwh + cert_to_inputs
wiring (atomic) + drop legacy if any
Slice 4 — SPEC_COVERAGE §8c row + slice progress table
```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)
```
Smaller than §8 because there's no upstream-drift surprise to chase (all fixtures = 0).
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.
---
## §C — Codebase pointers
- §8 orchestrator (close template): [packages/domain/src/domain/sap/worksheet/space_heating.py](../../packages/domain/src/domain/sap/worksheet/space_heating.py)
- §7 orchestrator (per-zone η pattern): [packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py](../../packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py)
- cert→inputs adapter: [packages/domain/src/domain/sap/rdsap/cert_to_inputs.py](../../packages/domain/src/domain/sap/rdsap/cert_to_inputs.py) (look for the chain at lines 870-960 for how §5/§6/§7/§8 stack)
- E2e SAP-score test: [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)
- §8 fixture pins (template): `_elmhurst_worksheet_000490.py` `SECTION_7_*` + `LINE_85..LINE_99` constants
- §8 ALL_FIXTURES test: [test_space_heating.py](../../packages/domain/src/domain/sap/worksheet/tests/test_space_heating.py)
- Extract script template: `/tmp/extract_section8.py` (rewrite for §8c — only ~30 lines)
- §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).
## §D — Commit chain to inspect
---
```bash
git log --oneline --grep '§8 slice'
# 9113f30a §8 slice 1 — orchestrator + summer clamp
# 1f078af7 §8 slice 2 — 6-fixture conformance
# f6ab7626 §8 slice 3 — calculator + cert_to_inputs wiring
# bb827803 docs — SPEC_COVERAGE §8 + slice progress
```
## §E — Skills
## §D — Skills
The dev container ships `/grill-me`, `/tdd`, `/caveman`. Default flow:
```
/grill-me → walk down design tree
/tdd implement §8c Space cooling → one test → one impl → repeat
/grill-me → walk down design tree, recommend scope cuts
/tdd implement §10a Fuel costs → one test → one impl → repeat
```
User invokes `/grill-me` first when ready. Audit + surface unknowns first; wait for `/grill-me`.
User invokes `/grill-me` first when ready. Audit B.5 / B.7 / B.8 + surface unknowns first; wait for `/grill-me`.
## §F — Definitely do NOT
---
- Do **not** claim "wiring water_heating_from_cert is the easy win" — that ship has sailed (see §A.2).
- Do **not** update the 000490 `_currently_within_3_points` ceiling unless Ticket A actually moves the SAP score.
## §E — 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** scan more than ~50 lines of a spec PDF before asking the user for the specific table/page.
- Do **not** modify existing fixture `build_epc()` functions.
- Do **not** invoke `/ultrareview` yourself.
- Do **not** widen the e2e SAP-score ceiling on 000474 (currently 2). It's tight on purpose post-PCDB.

View file

@ -2,7 +2,7 @@
Tracks which sections of the SAP 10.2 specification are implemented in `packages/domain/src/domain/sap/`. Per ADR-0009 the calculator is built from the spec, not reverse-engineered from cert data. This doc is the worksheet-driven roadmap for what remains.
Updated 2026-05-21 after §8c (slices `cf28eec4``f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5``380b6781`), and PCDB Table 105 integration (`fe04cd3a``a104dd55`).
Updated 2026-05-21 after §8c (slices `cf28eec4``f3797066`), §8f (`43cc16bc`), §9a single-main slices (`2b5fc6a5``380b6781`), PCDB Table 105 integration (`fe04cd3a``a104dd55`), and PCDB fixture lodgement (`1b43c95c``15d6b781`).
The canonical SAP10.2 algorithm lives in [`2026-05-19-17-18 RdSap10Worksheet.xlsx`](../../2026-05-19-17-18%20RdSap10Worksheet.xlsx) at the repo root — each line ref `(1)..(486)` maps to a cell. The worksheet sub-modules under `packages/domain/src/domain/sap/worksheet/` implement those line refs directly; Elmhurst worksheets validate end-to-end via `tests/_elmhurst_worksheet_*.py`.
@ -302,7 +302,9 @@ Status now: 100-cert MAE 4.49, 300-cert MAE 5.45, bias near zero (±0.2). Worksh
| ETL parser + 8 tests | `domain.sap.tables.pcdb.parser`: typed `GasOilBoilerRecord` + `RawPcdbRecord`. Ground-truth verified against ncm-pcdb.org.uk for Baxi 000098 / Potterton 000619 / Saunier 000732. Handles latin-1 encoding (degree-sign in addresses), `'obsolete'` status string, `'>70kW'` range indicator. | ✅ | `fe04cd3a` |
| ETL `run_etl` writes 8 JSONL files | One newline-delimited JSON file per table (105 typed; 122/143/313/353/362/391/506 raw). 17MB total. Runnable via `PYTHONPATH=packages/domain/src python -m domain.sap.tables.pcdb.etl`. Idempotent; commit JSONL alongside source `pcdb10.dat`. | ✅ | `fe04cd3a` |
| Runtime lookup `gas_oil_boiler_record(pcdb_id)` | `domain.sap.tables.pcdb` loads Table 105 NDJSON at import; ~50ms one-off, O(1) lookups thereafter. Returns None for unknown PCDB IDs → caller falls back to Table 4a/4b cascade. | ✅ | `23678228` |
| cert_to_inputs precedence (Table 105 only) | Appendix D2.1: PCDB winter overrides `main_heating_efficiency`; PCDB summer overrides `water_efficiency` scalar. Heat-network DLF override still wins where applicable. None of the 6 Elmhurst fixtures lodge a PCDB pointer; corpus golden certs that do see real efficiency changes (golden tolerance widened ±5 → ±7). | ✅ | `a104dd55` |
| cert_to_inputs precedence (Table 105 only) | Appendix D2.1: PCDB winter overrides `main_heating_efficiency`; PCDB summer overrides `water_efficiency` scalar. Heat-network DLF override still wins where applicable. None of the 6 Elmhurst fixtures lodge a PCDB pointer initially; corpus golden certs that do see real efficiency changes (golden tolerance widened ±5 → ±7). | ✅ | `a104dd55` |
| Elmhurst fixture PCDB lodgement: 000490 + 000474 | `_elmhurst_worksheet_000490.build_epc()` lodges `main_heating_index_number=10328` (Vaillant Ecotec Pro 28, winter 88.2%); `_elmhurst_worksheet_000474.build_epc()` lodges `16839` (Vaillant ecoTEC pro 28 VUW GB 286/5-3, winter 88.7%). 000474 e2e ceiling tightens 7 → 2 SAP points; 000490 widens 3 → 6 (spec-version drift on fuel cost — pre-amendment cert). `make_main_heating_detail` extended with `main_heating_index_number` / `main_heating_data_source` / `sap_main_heating_code` kwargs. | ✅ | `1b43c95c`, `7d4f3d78` |
| API → domain mapper-chain regression test | `test_api_to_domain_mapper_preserves_main_heating_index_number` parametrised over the 4 PCDB-listed golden corpus certs. Pins that `EpcPropertyDataMapper.from_api_response``cert_to_inputs` chain surfaces the PCDB pointer + applies PCDB winter efficiency. Confirms no domain-model changes were needed — `MainHeatingDetail.main_heating_index_number` has existed since schema 17_1 and all mapper paths from 17_1+ pass it through verbatim. | ✅ | `15d6b781` |
| Heat pump Appendix N cascade via Table 362 | Apply Appendix N in-use factor (×0.95), MCS installation factor (×1.39 for GSHP MCS-installed), design flow temperature adjustment. Replace SCOP 2.30 Table 4a fallback for `main_category=4`. | ⏸ deferred (typed Table 362 parser + Appendix N cascade) | — |
| Equation D1 monthly water heating cascade | Spec D2.1 (2): η_water_monthly = (Q_space + Q_water) / (Q_space/winter + Q_water/summer). Promotes water_eff scalar → 12-tuple. Refactor of `_hot_water_fuel_kwh_per_yr`. | ⏸ deferred (single-digit-% HW kWh precision for combi boilers) | — |
| Solid fuel boiler precedence via Table 122 | PCDB override for `main_category=3` (solid fuel) — typed parser + wiring. | ⏸ deferred | — |