Model/docs/sap-spec/HANDOVER_NEXT.md
Khalim Conn-Kowlessar 67af2e9b43 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>
2026-05-20 23:03:15 +00:00

12 KiB
Raw Blame History

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:

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.

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)

@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

§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_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.