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>
12 KiB
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:
- 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_certintocert_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. - Ticket B — Implement §8c Space cooling. xlsx rows 435–466, 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 | 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_certIS the active path incert_to_inputs._hw_kwh_and_gains_from_cert(filecert_to_inputs.py, line ~738). Legacypredicted_hot_water_kwhis a fallback only whenepc.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_fixturestests).
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:
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):
-
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.
- Currently selects ~0.78-0.80 from a static SAP10 efficiency table by
-
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_yrfrom 14334 → ~13000 kWh.
-
§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_kfrom 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.
- Calculator's
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 435–466) — 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.pymodule 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)
@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
- §7 orchestrator (per-zone η pattern): packages/domain/src/domain/sap/worksheet/mean_internal_temperature.py
- cert→inputs adapter: 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
- §8 fixture pins (template):
_elmhurst_worksheet_000490.pySECTION_7_*+LINE_85..LINE_99constants - §8 ALL_FIXTURES test: test_space_heating.py
- Extract script template:
/tmp/extract_section8.py(rewrite for §8c — only ~30 lines)
§D — Commit chain to inspect
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_pointsceiling 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
/ultrareviewyourself.