docs: handover for §8c Space cooling + 000490 SAP-score diagnostic

Two tickets in order for the next agent:

1. Ticket A — Investigate the 000490 +3 SAP overshoot. Corrects the
   previous agent's claim that "wiring water_heating_from_cert is the
   easy win"; that's already done. Real driver is the boiler efficiency
   cascade selecting 0.80 instead of the PDF Manufacturer-declared
   0.882 (Vaillant Ecotec Pro). Time-boxed diagnostic; flag and defer
   if expensive.

2. Ticket B — §8c Space cooling (xlsx rows 435-466, lines (100)..(108)).
   All 6 Elmhurst fixtures = 0 cooling. Small slice; mirror §8 pattern.

Includes spec anchors (Qcool formula sign, Jun-Aug inclusion rule),
codebase pointers, slice plan, and the standard "do not" list.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-20 23:03:15 +00:00
parent bb827803ac
commit 67af2e9b43

View file

@ -0,0 +1,207 @@
# Handover — §8c Space cooling + 000490 SAP-score diagnostic
**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**:
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):
- **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.
---
## §A — Ticket A: 000490 SAP-score gap diagnostic
### 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:
| 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
```
Drivers (ordered by likely SAP impact):
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.
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.
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.
---
## §B — Ticket B: §8c Space cooling
### 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
### B.2 SAP10.2 spec anchors (already extracted)
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).`
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 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)
```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: ...
```
### 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.
### 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)
```
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
```
Smaller than §8 because there's no upstream-drift surprise to chase (all fixtures = 0).
---
## §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)
## §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
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
```
User invokes `/grill-me` first when ready. Audit + 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.
- 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.