Docs: rewrite HANDOVER_NEXT.md for fresh agent pickup post-slice-25d

§1-§6 fully close (252/252). §7 closes 52/60 (LINE_92/93 marginal on 4
fixtures). §8-§12 not yet pinned. Handover now reads top-to-bottom with
current scoreboard, per-section work queue, spec page reference index,
and the section helper map for the new agent to extend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-23 23:17:43 +00:00
parent 147da90a5a
commit 144f08533f

View file

@ -1,368 +1,372 @@
# Handover — strict zero-error cascade pin closure for the 6 Elmhurst fixtures
# Handover — §7 LINE_92/93 + §8§12 sweep to abs=1e-4 closure
**For the agent picking up the next chunk of work.** Read this BEFORE any tool call.
Read it in full. The previous agents' errors are catalogued here so you don't
repeat them.
**Goal: every line ref of every output for every one of the 6 Elmhurst
fixtures pins against the U985 worksheet PDF at abs=1e-4.**
Owner: `khalim@domna.homes`. Branch: `ara-backend-design-prd`.
Spec PDFs in `docs/sap-spec/`: SAP 10.2 (14-03-2025), RdSAP 10 (10-06-2025), PCDF.
---
## §A — Hard rules. Internalise these BEFORE anything else.
## §A — Hard rules. Internalise before any code.
### A.1 What this project IS
This repo replicates the **rdSAP calculation engine** to bit-level fidelity
against 6 known test vectors (the U985 Elmhurst worksheets):
- **Inputs**: Summary_NNNNNN.pdf (cert lodgement) for each of 6 fixtures
- **Inputs**: `Summary_NNNNNN.pdf` (cert lodgement) for each of 6 fixtures
(000474, 000477, 000480, 000487, 000490, 000516).
- **Intermediate values**: U985-0001-NNNNNN.{pdf,txt} lodges every
worksheet line ref (1) through (258+) to 4 decimal places.
- **Intermediate values**: `U985-0001-NNNNNN.{pdf,txt}` lodges every
worksheet line ref (1) through (282+) to 4 decimal places.
- **Final outputs**: SAP rating (continuous + integer), ECF, total fuel cost,
CO2, primary energy, per-end-use kWh.
It is a deterministic numerical function with fully-known test vectors.
### A.2 The bar: abs=1e-4 on EVERY pin, every fixture, every line ref
### A.2 The bar: abs=1e-4 on EVERY pin
**Every SAP-result field AND every section line ref must pin to PDF at abs=1e-4.**
- The PDF lodges 4 d.p. display precision. abs=1e-4 is the floor of "match
what the PDF says".
- **NO `rel=…` tolerances.**
- **NO `<= 0.5` continuous SAP ceilings.**
- **NO `xfail` markers on cascade pins.**
- **NO "documented widening".**
- The PDF lodges 4 d.p. display precision. abs=1e-4 is the floor of "match what
the PDF says".
- **No `rel=...` tolerances.** Slice 19b removed `rel=0.15` (fuel cost) and
`rel=0.05` (fuel cost) precedents. Never re-add these.
- **No `<= 0.5` continuous SAP ceilings.** Slice 19a removed these. Never re-add.
- **No `xfail` markers on cascade pins.** A failing pin is a calculator bug or
fixture defect to fix.
- **No "documented widening".** There is no such thing for this project.
A failing pin is a calculator bug or fixture defect. If you can't close
it in this slice, leave it failing — that's the next slice's work.
If a pin can't be closed in the current slice, **leave it failing**. The failing
pin is the next slice's work. Tolerances are NEVER widened to make the suite
green. CI red is fine while bugs are being fixed.
### A.3 Past mistakes — DO NOT REPEAT
### A.3 Past-agent mistakes — DO NOT REPEAT
The user is frustrated with previous agents because:
1. **Treated SAP integer Δ=0 as "closed"** — that's a weak gate (hides ±0.5
1. **Treating SAP integer Δ=0 as "closed"** — that's a weak gate (hides ±0.5
continuous drift). The real gate is per-line-ref abs=1e-4.
2. **Widened tolerances** to make tests green (`rel=0.15`, `<=0.5`). Every such
widening masked a real residual.
3. **Tested sections in isolation** using `fixture.LINE_X` PDF values AS INPUTS.
That doesn't test the cascade — it tests the section formula given correct
inputs. The cascade can still drift.
4. **Missed fixture defects** — multiple fixtures had missing or wrong
lodgement (bulbs, windows, sap_heating, detailed RR, exposed_floor,
door_count, per-window U). When a cascade pin fails, ALWAYS audit the
fixture against the PDF first.
5. **Labelled code "SAP 10.3"** when implementing SAP 10.2 (mostly cleaned in
slice 21a; `tables/table_12.py` retains intentional 10.2-vs-10.3
comparison).
6. **Diagnosed downstream first**. The cascade is upstream→downstream
(§1 → §2 → §3 → §4 → §5 → §6 → §7 → §8 → §9a → §10a → §11a → §12). A
downstream pin failure (e.g. `total_fuel_cost_gbp`) is meaningless to
diagnose until upstream pins close.
2. **Widening tolerances** to make tests green.
3. **Testing sections in isolation** using `fixture.LINE_X` PDF values AS
INPUTS. The cascade test walks `cert_to_inputs(epc)`, NOT isolated calls.
4. **Missing fixture defects** — When a cascade pin fails, audit the
fixture against the PDF FIRST. Many lodgements have been incomplete.
5. **Diagnosing downstream first**. Cascade is upstream→downstream
(§1 → §2 → §3 → §4 → §5 → §6 → §7 → §8 → §9a → §10a → §11a → §12).
A downstream pin failure is meaningless to diagnose until upstream pins
close.
If you find yourself about to widen a tolerance, add an xfail, or skip a
fixture — **stop and ask the user.** Those are anti-patterns for this project.
fixture — **stop and ask the user.**
### A.4 Reporting format — use the matrix
The user prefers the **per-(fixture × line-ref) matrix** for cohort scoreboard
updates. Example shape (use this exactly when reporting cascade-pin status):
### A.4 Reporting format — matrix not prose
```
field | 000474 | 000477 | 000480 | 000487 | 000490 | 000516
sap_score (int) | ✓ | ✓ | ✓ | ✗ | ✓ | ✓
sap_score_continuous | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
ecf | ✗ | ✗ | ✓ | ✗ | ✗ | ✗
sec 474 477 480 487 490 516 total
--- ---- ---- ---- ---- ---- ---- -----
§1 2/2 2/2 2/2 2/2 2/2 2/2 12/12
§3 4/4 4/4 4/4 4/4 4/4 4/4 24/24
...
```
Or with numeric residuals when finer granularity helps:
Or numeric residuals when finer granularity helps:
```
fixture | LINE_31 Δ | LINE_33 Δ | LINE_36 Δ | LINE_37 Δ
000474 | 0.0014 | 0.0296 | 0.0002 | 0.0294
000477 | 0.0004 | 0.1246 | ✓ | 0.1244
fixture | LINE_92 Δ | LINE_93 Δ
000474 | 0.00013 | 0.00013
000477 | 0.00016 | 0.00016
...
```
✓ = within `abs=1e-4`. Numeric value = the actual diff. This format lets the
user scan visually and spot per-fixture vs per-line patterns. Use it instead
of prose summaries when reporting scoreboard state.
✓ = within abs=1e-4. Use this format instead of prose summaries.
### A.5 Workflow rules
- **Don't scan >50 lines of spec PDF without checking with the user** for the
specific page/table range. Spec PDFs are big and the user has the page
anchors. (Table 11 = page 188, Table 12 = 189, Table 12a = 191, Table 3a/b/c
= 160/161/162 already given.)
- **Don't scan >50 lines of spec PDF without checking with the user** for
the page anchor. The user has the page references and prefers to give
them up-front rather than have you fumble through the spec.
- **One slice = one commit**. AAA test convention (`# Arrange / # Act /
# Assert`). Co-Authored-By trailer.
- **Don't touch SAP rating constants in `worksheet/rating.py`**
`ENERGY_COST_DEFLATOR=0.42`, `ECF_LOG_THRESHOLD=3.5`, `SAP_LOG_COEFF=113.7`,
`SAP_LOG_CONSTANT=117.0`. SAP 10.2 (14-03-2025) per ADR-0010. Pinned by 8+
tests.
- **Don't auto-update unrelated git status changes** — see deletions/new files
in `git status` that aren't from your work? Don't touch them without asking.
`SAP_LOG_CONSTANT=117.0`. SAP 10.2 per ADR-0010, pinned by 8+ tests.
- **Don't auto-update unrelated `git status` entries**. The pre-existing
deletion of `docs/sap-spec/rdsap-10-specification-2025-06-10.pdf` and
the untracked `docs/sap-spec/RdSAP 10 Specification 10-06-2025.pdf`
are stable; don't touch.
- **Don't invoke `/ultrareview`** — user-triggered only.
- **Caveman mode** for prose. Terse. Technical. No filler.
- **Terse prose.** No filler.
- **Delete `_TEMP.py` diagnostic files before commit.**
---
## §B — Current state (as of 2026-05-23)
## §B — Current state
### B.1 Cascade pin scoreboard
### B.1 Cascade pin scoreboard (per-section)
Two test files contain the strict pins:
```
sec 474 477 480 487 490 516 total
--- ---- ---- ---- ---- ---- ---- -----
§1 2/2 2/2 2/2 2/2 2/2 2/2 12/12 ✓
§2 16/16 16/16 16/16 16/16 16/16 16/16 96/96 ✓
§3 4/4 4/4 4/4 4/4 4/4 4/4 24/24 ✓
§4 9/9 9/9 9/9 9/9 9/9 9/9 54/54 ✓
§5 9/9 9/9 9/9 9/9 9/9 9/9 54/54 ✓
§6 2/2 2/2 2/2 2/2 2/2 2/2 12/12 ✓
§7 8/10 8/10 8/10 10/10 8/10 10/10 52/60
---------- ------------------------------------------------------ -------
total 304/312 (97.4%)
```
1. **`test_e2e_elmhurst_sap_score.py::test_sap_result_pin[fixture-field]`** —
top-level SapResult fields. 66 cases (11 fields × 6 fixtures). Currently
**18 PASS / 48 FAIL** at abs=1e-4.
2. **`test_section_cascade_pins.py`** — per-section line refs walking
`<section>_from_cert(epc)` against PDF. Currently **151 PASS / 35 FAIL**:
- §1 (dimensions): 12 PASS / 0 FAIL ✓
- §2 (ventilation): 96 PASS / 0 FAIL ✓
- §3 (heat losses): 1 PASS / 23 FAIL
- §4 (water heating): 42 PASS / 12 FAIL
- §5-§12: not yet pinned
**§1§6 fully close for all 6 fixtures (252/252).** Only §7 LINE_92/93
on 4 fixtures (000474/477/480/490) remains in the cascade.
Total: **169 PASS / 83 FAIL** across the strict pins. 4 of 6 fixtures fully
close §1+§2+§4. 000487 is the worst (RR fixture defect propagates everywhere).
(Post-slice-25d: section_cascade_pins 304 PASS / 8 FAIL, e2e SapResult
33 PASS / 39 FAIL. §3 + §4 + §5 + §6 ALL fully close for all 6
fixtures. §7 closes 52/60 — only LINE_92/93 marginal ~0.0001 K
residual on 000474/477/480/490 remains (the precision artefact
discovered in slice 26c, no spec-grounded fix identified). Cascade
total closure: 312 tests, 304 PASS = 97.4%.)
### B.2 SapResult pin matrix (post-slice-22/23)
### B.2 SapResult pin matrix (e2e)
```
field | 474 | 477 | 480 | 487 | 490 | 516
-----------------------------------|-----|-----|-----|-----|-----|-----
-----------------------------------+-----+-----+-----+-----+-----+-----
sap_score (int) | ✓ | ✓ | ✓ | ✗ | ✓ | ✓
sap_score_continuous | ✗ | ✗ | ✗ | ✗ | ✗ |
ecf | ✗ | ✗ | ✓ | ✗ | ✗ | ✗
sap_score_continuous | ✗ | ✗ | ✗ | ✗ | ✗ | ✓
ecf | ✓ | ✓ | ✓ | ✗ | ✓ | ✓
total_fuel_cost_gbp | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
co2_kg_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
space_heating_kwh_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
main_heating_fuel_kwh_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
secondary_heating_fuel_kwh_per_yr | ✓ | ✗ | ✗ | ✗ | ✗ | ✗
hot_water_kwh_per_yr | ✗ | ✗ | ✗ | ✗ | ✗ | ✗
lighting_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ |
hot_water_kwh_per_yr | ✓ | ✗ | ✓ | ✓ | ✗ | ✗
lighting_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ |
pumps_fans_kwh_per_yr | ✓ | ✓ | ✓ | ✓ | ✓ | ✓
```
5 of 6 fixtures hit SAP integer Δ=0 (000487 is the holdout). But continuous SAP
is still off by sub-SAP-point amounts on every fixture — none of `sap_score_
continuous` is closed at abs=1e-4.
27 SapResult pin PASS / 39 FAIL. Most downstream fails will close as
§8/§9a/§10a/§11a/§12 land. 000516 `sap_score_continuous` already
passes — a useful sanity check that the full cascade is consistent
when the upstream sections close.
### B.3 §3 residuals after slice 27b (RdSAP10 §15 element-area rounding)
### B.3 Recent slices (in reverse order — newest first)
```
fixture | LINE_31 Δ | LINE_33 Δ | LINE_36 Δ | LINE_37 Δ
000474 | ✓ | ✓ | ✓ | ✓
000477 | ✓ | ✓ | ✓ | ✓
000480 | ✓ | ✓ | ✓ | ✓
000487 | 8.83 | 37.79 | 1.32 | 39.11
000490 | ✓ | ✓ | ✓ | ✓
000516 | ✓ | ✓ | ✓ | ✓
```
**§3 now closes for 5 of 6 fixtures at abs=1e-4.** Slice 27b applied the
RdSAP10 §15 (p.66) rounding policy: "All element areas (gross) including
window areas: 2 d.p." Per-element gross wall / party / roof / floor /
window / door / alt-wall / RR-sub-area inputs to the §3 cascade are now
rounded to 2 d.p. before A × U.
The remaining work is on 000487 — the worst fixture — driven by an
RR detailed-surface lodgement defect + a U=0.86 external-gable variant
our `gable_wall` enum doesn't handle. That's slice 25.
### B.4 §4 residuals
```
fixture | section §4 pin status
000474 | 9/9 ✓
000477 | 5/9 (combi loss LINE_61m diverges → cascades to 62/64/65)
000480 | 9/9 ✓
000487 | 1/9 (LINE_43 + every monthly fails — HW lodgement defect)
000490 | 9/9 ✓
000516 | 9/9 ✓
```
### B.5 Recent slices (in reverse order — newest first)
```
Slice 25d: 000487 §4 LINE_65 closure — derive LINE_64A electric-shower kWh from cert (Appendix J step 8, p.82)
Slice 25c: 000477 §4/§5/§6 closure — SAP10.2 Table 3c (p.162) M+L lower bound 100.0 → 100.2
Slice 25b: 000487 §4 closure (7/8) — has_electric_shower + mixer/electric counts on SapHeating, Appendix J step 2a fix
Slice 25a: 000487 §3 closure — detailed RR + gable_wall_external + Ext1 alt U=1.9 + §3.8 max-floor roof + half-up rounding
Slice 26c: §7 mean internal temp cascade pin (60 cases, 44 PASS) — LINE_85..94
Slice 26b: §6 solar gains cascade pin (12 cases, 10 PASS) + SapRoofWindow solar attrs + plumb to §6 cascade
Slice 26: §5 internal gains cascade pin (54 cases, 50 PASS / 4 FAIL) + rooflight plumb to daylight factor
Slice 27b: §3 element-area + door-area rounding to 2 d.p. per RdSAP10 §15 (p.66)
Slice 27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12
Slice 24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure
ac68cf88 Slice 23: 000516 detailed RR + exposed_floor + door_count fixture lodgement
6be8fdb7 Slice 22: per-window curtain resistance fix (mixed glazing)
024244ec Slice 21d: §3 cascade pins + heat_transmission_section_from_cert helper
778b150c Slice 21e: §4 water heating cascade pins (42/54 PASS)
5b7dbe2c Slice 21c: §2 cascade pins + ventilation_from_cert helper (96 PASS)
c1472330 Slice 21b: §1 cascade pins (12/12 PASS)
20424a2d Slice 21a: relabel SAP 10.3 → SAP 10.2 in calculator docstrings
4c2f37f6 Slice 19b: drop loose-tolerance fuel cost tests (rel=0.15, rel=0.05)
6bfb0614 Slice 19a: strict cascade-pin scoreboard for SapResult vs U985 PDFs
e2d9f77d Slice 20: lodge per-window u_value on mixed-glazing fixtures
5e34594d Slice 18a: sap_heating lodgement on 000480 / 487 / 516
8786b907 Slice 17: wire Appendix L inputs into 000480 / 487 / 516
25d: 000487 §4 LINE_65 closure — derive (64a) electric-shower kWh from cert (App J step 8, p.82)
25c: 000477 §4/§5/§6 closure — SAP10.2 Table 3c (p.162) M+L lower bound 100.0 → 100.2
25b: 000487 §4 LINE_43-64 closure — has_electric_shower + Appendix J step 2a Nbath branch
25a: 000487 §3 full closure — RR detailed surfaces + gable_wall_external + §3.8 max-floor roof + half-up rounding
26c: §7 mean internal temp cascade pin (60 cases, 52 PASS)
26b: §6 solar gains cascade pin + SapRoofWindow solar attrs + plumb to §6 cascade
26: §5 internal gains cascade pin + rooflight daylight plumb
27b: §3 element-area rounding to 2 d.p. per RdSAP10 §15 (p.66)
27: BS EN ISO 13370 floor U rounded to 2 d.p. per RdSAP10 §5.12 (p.46)
24: rooflight (line 27a) — SapRoofWindow datatype + 000516 cascade closure
```
---
## §C — Work queue (in priority order)
## §C — Work queue (priority order)
### C.1 Slice 24 — ~~Rooflight (line 27a) heat transmission, for 000516~~ DONE
### C.1 §7 LINE_92/93 marginal residual (8 fails, 4 fixtures)
Done. 000516 PDF lodged 1.18 m² rooflight on line (27a) at U_eff=2.9930 →
3.5317 W/K. Wired by adding `SapRoofWindow` datatype to `EpcPropertyData`
and iterating `epc.sap_roof_windows` alongside vertical windows in
`heat_transmission_from_cert` — same SAP10.2 §3.2 curtain transform R=0.04
applied; rooflight area subtracted from main part's roof gross. Raw U=3.40
sourced from RdSAP10 Table 24 (p.50/113) "Roof window" column.
Per the matrix above. Diff is ~0.000100.00016 K per failing case — just
above the 1e-4 threshold. The PDF passes LINE_87 (T_living) and LINE_90
(T_elsewhere) for the same 4 fixtures, but the weighted combination
LINE_92 = `(91) × T_living + (1 - (91)) × T_elsewhere` drifts.
§3 LINE_33 residual for 000516: 0.8215 W/K → 0.0038 W/K. Remaining 0.0038
is the same pre-existing wall-perimeter + per-window curtain precision
drift biting 000474/477/480/490 — closes in slice 27.
Hypotheses to test:
1. **PDF uses rounded T_living/T_elsewhere** at some precision higher than
4 d.p. but lower than full float in the weighted sum. The cascade pin
on LINE_87/90 passes at abs=1e-4 because both my full-precision and
the PDF's higher-precision values round to the same 4-d.p. display.
2. **PDF rounds LINE_92 to specific d.p. before later use**, but the
stored value doesn't quite match the in-memory full-precision combo.
3. **A spec-defined intermediate rounding step in §7 step 9** (RdSAP10
§15 doesn't list MIT in its rounding list — only U-values and areas).
### C.2 Slice 25 — ~~000487 §3 RR + external gable variant~~ DONE (slice 25a)
Diagnostic: write a TEMP test that prints my T_living[m], T_elsewhere[m],
LINE_91, and computes the weighted sum at several precision levels (4 d.p.,
5 d.p., 6 d.p., full). Compare each to the PDF's LINE_92[m]. If 5-d.p.
matches the PDF for all 4 fixtures and 12 months, the rule is "round
T_living + T_elsewhere to 5 d.p. before combining". Ask the user for the
SAP10.2 §7 spec page (likely §9.3 or near, page ~28-32) before applying
any new rounding rule.
§3 now fully closes for 000487. Remaining work: §4 HW lodgement (slice 25b
— 000487 cert has 1 bath + 1 electric shower, no mixer outlet; calc treats
"no mixer outlets" as "no shower", bumping Nbath from 0.13N+0.19 to
0.35N+0.50 and over-counting bath volume 2.5×).
000516 + 000487 §7 already close at 10/10 — so the artefact isn't
universal. Compare their T_living[m] values against the failing fixtures
to spot the trigger pattern.
Spec source: SAP 10.2 Appendix J step 2a (p.81) — `Nbath = 0.13N + 0.19 if
shower also present (including electric); = 0.35N + 0.50 if no shower
present`. Fix needs: lodge electric-shower presence on cert, plumb
`has_electric_shower` through `water_heating_section_from_cert`, OR the
fixture-shower-count refactor that closes 000477 LINE_61 simultaneously.
### C.2 §8 space heating cascade pin (lines 9599)
### C.3 Slice 26+ — §5 / §6 / §7 / §8 / §9a / §10a / §11a / §12 cascade pins
Fixtures lodge:
- `LINE_95_M_USEFUL_GAINS_W` (12-tuple)
- `LINE_97_M_HEAT_LOSS_RATE_W` (12-tuple)
- `LINE_98A_M_SPACE_HEATING_KWH` (12-tuple)
- `LINE_98C_M_TOTAL_SPACE_HEATING_KWH` (12-tuple, same as 98a for current fixtures)
- `LINE_98C_ANNUAL_KWH` (scalar)
- `LINE_99_PER_M2_KWH` (scalar)
The cascade pin work continues in worksheet order. For each section:
§8 orchestrator: `domain.sap.worksheet.space_heating.space_heating_monthly_kwh`.
Section helper to add: `space_heating_section_from_cert(epc)` in
`cert_to_inputs.py`. Inputs needed: §7 (MIT + η_whole), §1 (TFA, volume),
§2 (effective_monthly_ach), §3 (total HLC), §5+§6 (total gains), climate.
Same composition pattern as `mean_internal_temperature_section_from_cert`.
1. Identify the cert→inputs cascade entry point. May need to extract a
`<section>_from_cert(epc)` helper from `cert_to_inputs` (mirroring slice
21c's `ventilation_from_cert`, 21d's `heat_transmission_section_from_cert`,
21e's `water_heating_section_from_cert`, 26's
`internal_gains_section_from_cert`).
2. Map fixture `LINE_X_<NAME>` constants to result struct attributes.
3. Add scalar + monthly pin tests at abs=1e-4 to `test_section_cascade_pins.py`.
4. Run, see failures, diagnose. Fixture defect or calculator bug — fix in place,
no widening.
Add pin tests at the end of `test_section_cascade_pins.py` mirroring the
`_SECTION_7_MONTHLY_PINS` shape.
Sections still to pin:
- ~~**§5 internal gains** (lines 66-73 + 232 lighting kWh)~~ DONE (slice 26)
- ~~**§6 solar gains** (lines 83-84)~~ DONE (slice 26b — 5/6 fixtures close, 000477/487 cascade from §4)
- ~~**§7 mean internal temperature** (lines 85-94)~~ MOSTLY DONE (slice 26c — 44/60 PASS; LINE_92/93 marginal ~0.0001 K residual on 000474/477/480/490 needs investigation; 000487 cascades from §3/§4 defects).
- **§8 space heating** (lines 95-99). 4 monthly + 2 annual.
- **§9a energy requirements** (lines 201, 206-208, 211-215, 219). 5 scalar + 2
monthly. Currently only the annual aggregates show on `SapResult` — may need
monthly exposure.
- **§10a fuel costs** (lines 240-255). 17+ line refs.
- **§11a SAP rating** (lines 256-258). 3 line refs.
- **§12 environmental** (lines 261-282). CO2 + primary energy + EI rating.
### C.3 §8c space cooling cascade pin (lines 100108)
Some fixtures' constants for these sections may be missing — check first. PDF
extraction commands (sample for §9a):
```
awk '/^9a\. Energy requirements/,/^10a\./' "sap worksheets/U985-0001-NNNNNN.txt"
```
All 6 fixtures lodge `f_C=0` (no air conditioning), so:
- LINE_103 cooling gains = (0,)×12
- LINE_107 monthly cooling = (0,)×12
- LINE_107 annual = 0
- LINE_108 per m² = 0
### C.4 Slice 27 — ~~Floor-U precision~~ DONE (mostly)
LINE_101 utilisation factor collapses to 1.0 (γ ≤ 0 branch); LINE_106
intermittency monthly is the spec default mask. Fixture constants
`LINE_101_M_UTILISATION_FACTOR_LOSS = SECTION_8C_ETA_LOSS_ALL_ONE`,
`LINE_106_M_INTERMITTENCY_FACTOR = SECTION_8C_INTERMITTENCY_MONTHLY`,
`LINE_107_M_SPACE_COOLING_KWH = SECTION_8C_ALL_ZERO_MONTHLY`.
Done. The §5.12 spec mandates "rounded to two decimal places" for BS EN ISO
13370 floor U-values, which my calc was skipping. Applied `round(U, 2)` to
both suspended-timber and solid-floor branches in `u_floor` — closed
000474/477/490 from ~0.030.13 W/K residual to under 0.002 W/K on each.
§8c orchestrator: `domain.sap.worksheet.space_cooling`. Section helper
likely trivial since all inputs collapse to zero.
Remaining 0.00130.0075 W/K residual is wall + party-wall area precision —
PDF stores 2-d.p.-rounded element areas (e.g. `36.4500 m²` for a wall I
compute as `36.4492 m²`). Closing these needs the §3 area-rounding spec
rule — see slice 27b below.
### C.4 §8f Fabric Energy Efficiency (line 109)
### C.4b Slice 27b — ~~§3 element-area rounding~~ DONE
Single scalar: `LINE_109_FEE_KWH_PER_M2`. Per spec, (109) = (98a)/TFA +
(108). For all 6 fixtures (98b) solar space heating = 0, so Σ(98a) =
Σ(98c) → LINE_109 = LINE_99 + LINE_108 = LINE_99 (no AC).
Done. RdSAP10 §15 (p.66) lodges the rounding policy: "All element areas
(gross) including window areas: 2 d.p." Applied to gross wall + party
wall + roof + floor + window + door + alt-wall + RR-sub-area inputs in
`heat_transmission_from_cert`. §3 cascade pins (LINE_31/33/36/37) now
close at abs=1e-4 for 5 of 6 fixtures; 000487 alone remains failing on
the RR defect (slice 25).
§8f orchestrator: `domain.sap.worksheet.fabric_energy_efficiency`.
### C.5 Slice 28 — Continuous SAP / fuel cost / CO2 closure
### C.5 §9a energy requirements (lines 201, 206219)
Once §1-§9a all close at abs=1e-4, the downstream pins
(`total_fuel_cost_gbp`, `ecf`, `sap_score_continuous`, `co2_kg_per_yr`) tighten
mechanically. Re-run the SapResult pin matrix; whatever still fails has a
section-specific residual to chase.
Lodged on fixtures:
- LINE_211 main heating fuel (annual)
- LINE_215 secondary heating fuel (annual)
- LINE_219 hot water fuel (annual)
- Plus LINE_201, 206208, 213215 monthly tuples possibly
Already partially exposed on `SapResult` (`main_heating_fuel_kwh_per_yr`,
`secondary_heating_fuel_kwh_per_yr`, `hot_water_kwh_per_yr`). Pin tests
at the cascade level walk `energy_requirements_from_cert` (or compose
inside cert_to_inputs).
### C.6 §10a fuel costs (lines 240255)
17+ line refs. Already exposed via `SapResult.total_fuel_cost_gbp`.
Cascade tests should pin each component (main fuel cost, secondary,
hot water, pumps/fans, lighting, PV credit, standing charges).
§10a orchestrator: `domain.sap.worksheet.fuel_cost.fuel_cost`.
### C.7 §11a SAP rating (lines 256258)
3 line refs:
- LINE_256 ECF (energy cost factor)
- LINE_257 SAP score continuous
- LINE_258 SAP score integer
Already on `SapResult` as `ecf`, `sap_score_continuous`, `sap_score`.
e2e pins exist. Add explicit cascade pins for symmetry.
`rating.py` constants are immutable per ADR-0010 — do not touch.
### C.8 §12 environmental (lines 261282)
CO2 + primary energy + EI rating monthly + annual. Already partly on
`SapResult.co2_kg_per_yr`. Big section with many line refs.
---
## §D — How to work (toolbox)
## §D — Workflow toolbox
### D.1 Cascade pin diagnostic loop
### D.1 Adding a section cascade pin (the standard pattern)
1. **Find or extract** a `<section>_from_cert(epc)` helper in
`domain.sap.rdsap.cert_to_inputs`. If it doesn't exist, add one
mirroring `internal_gains_section_from_cert` or `mean_internal_
temperature_section_from_cert` — compose upstream section helpers
then call the orchestrator with the result's fields.
2. **Add a `_SECTION_X_PINS` tuple** to `test_section_cascade_pins.py`
mapping `("LINE_X_<NAME>", "result_attr_name")`.
3. **Add a parametrised test** that walks every `(fixture, line_ref)`
pair and asserts `_pin(actual, expected, ...)` at abs=1e-4.
4. **Run, see failures, diagnose. Fixture defect or calculator bug —
fix in place, no widening.**
### D.2 Diagnostic pattern
When a pin fails:
1. Add a TEMP diagnostic test in `packages/domain/src/domain/sap/worksheet/tests/test_<thing>_diag_TEMP.py` that dumps the cascade output alongside the PDF expected.
2. Compare element-by-element against the PDF block (use `awk` to extract the relevant §X PDF block).
3. Identify the drift source — fixture defect or calc bug.
4. Fix. Re-run the pin test.
5. **Delete the TEMP file before committing.** Never commit `_TEMP.py` files.
1. Add a TEMP test file `test_<thing>_diag_TEMP.py` that dumps the
per-component breakdown alongside PDF expected values.
2. `awk '/^X\. Section/,/^Y\./' "sap worksheets/U985-0001-NNNNNN.txt"`
to extract the PDF block.
3. Identify the drift source — fixture defect (audit fixture first)
or calc bug.
4. Fix. Re-run the pin.
5. **Delete the TEMP file before committing.**
### D.2 Spec lookups
### D.3 Spec page references already in hand
User has given these page anchors:
- Table 11 (secondary heating fraction): p 188
- Table 12 (fuel prices/CO2/PEF): p 189
- Table 12a (standing charges, off-peak): p 191
- Table 3a (water heating single-system): p 160
- Table 3b (water heating combi PCDB): p 161
- Table 3c (water heating two-profile): p 162
```
RdSAP 10 (10-06-2025):
§3.1 precision rule p.16
§3.6 wall area p.19
§3.7.1 window area p.20
§3.8 roof area (max-floor) p.20
§3.9 RR simplified p.21
§3.10 RR detailed p.21
Table 4 (RR gable walls) p.22
§5.12 floor U + Table 19 p.46
§5.13 + Table 20 exposed floor p.47
§5.17 + Table 23 basement p.48
§5.18 curtain wall p.48
Table 24 (window U) p.50 (Standard | Roof window cols)
§15 rounding rules p.66
Table 11 (secondary fraction) p.188
Table 12 (fuel/CO2/PEF) p.189
Table 12a (standing/off-peak) p.191
For other pages, **ask the user.** Don't scan more than ~50 lines of spec PDF
without permission.
### D.3 PDF extraction
Worksheet PDFs are in `sap worksheets/` (note the space — quote in shell).
Each fixture has `U985-0001-NNNNNN.{pdf,txt}` (intermediate values) and
`Summary_NNNNNN.pdf` (cert lodgement).
PDF blocks for sections (sample for §3):
```bash
awk '/^3\. Heat losses/,/Thermal mass parameter/' "sap worksheets/U985-0001-000474.txt"
SAP 10.2 (14-03-2025):
Appendix J §2a Nbath p.81
Appendix J §8 electric shower p.82
Table J4 (shower flow/power) p.83
Table J5 (behavioural fbeh) p.83
Table 3a (HW combi keep-hot) p.160
Table 3b (HW combi profile M) p.161
Table 3c (HW combi M+L / M+S) p.162
```
### D.4 Section helpers (cascade-pin enablers)
For new pages **ask the user**. Spec PDFs are big.
Already extracted in `domain.sap.rdsap.cert_to_inputs`:
- `dimensions_from_cert(epc) -> Dimensions` (§1)
- `ventilation_from_cert(epc) -> VentilationResult` (§2, slice 21c)
- `heat_transmission_section_from_cert(epc) -> HeatTransmission` (§3, slice 21d)
- `water_heating_section_from_cert(epc) -> WaterHeatingResult` (§4, slice 21e)
### D.4 Spec-grounded patterns we've discovered
For §5/§6/§7/§8/§9a/§10a/§11a/§12 you may need to extract similar helpers.
The existing `internal_gains_from_cert`, `solar_gains_from_cert`, etc. mostly
exist already — check whether they're already public on the worksheet/* module.
- **RdSAP §15 rounding**: U-values + element gross areas to 2 d.p. —
apply at the BOUNDARY between RdSAP input and SAP calculator. See
`heat_transmission.py` for the pattern (`_round_half_up`).
- **Half-up rounding, not banker's**: Python's `round(17.125, 2) = 17.12`
but SAP wants 17.13. The `_round_half_up` helper in `heat_transmission.py`
is the right utility — reuse it for any new §15 boundary you cross.
- **§3.8 roof area = MAX of floor areas across levels**, not the top
floor area. Bites when an extension's footprint steps back.
- **Assessor-lodged U overrides cascade**: cert PDFs lodge measured U
for some walls/gables. The `u_value` field on `SapRoomInRoofSurface`
and `SapAlternativeWall` honours this. When extending to new surface
types, follow the same pattern.
### D.5 Hard rules summary card
### D.5 Section helper map (cert→inputs cascade entry points)
```
domain.sap.rdsap.cert_to_inputs
dimensions_from_cert(epc) §1 → Dimensions
ventilation_from_cert(epc) §2 → VentilationResult
heat_transmission_section_from_cert(epc) §3 → HeatTransmission
water_heating_section_from_cert(epc) §4 → WaterHeatingResult
internal_gains_section_from_cert(epc) §5 → InternalGainsResult
solar_gains_section_from_cert(epc) §6 → SolarGainsResult
mean_internal_temperature_section_from_cert(epc) §7 → MeanInternalTemperatureResult
-- next to add --
space_heating_section_from_cert(epc) §8 → SpaceHeatingResult
space_cooling_section_from_cert(epc) §8c → SpaceCoolingResult
fabric_energy_efficiency_from_cert(epc) §8f → float (kWh/m²)
energy_requirements_section_from_cert(epc) §9a → EnergyRequirementsResult
fuel_cost_section_from_cert(epc) §10a → FuelCostResult
sap_rating_section_from_cert(epc) §11a → (ecf, sap_continuous, sap_int)
environmental_section_from_cert(epc) §12 → EnvironmentalResult
```
### D.6 Hard rules summary card
| do | don't |
|----|-------|
@ -371,84 +375,107 @@ exist already — check whether they're already public on the worksheet/* module
| Leave failing pins, fix one at a time | Widen tolerance / add xfail |
| Quote PDF page when asking for spec | Scan >50 lines of PDF without asking |
| `[[reference-style]]` cross-links in memory | Bare prose references |
| Use `_round_half_up`, not Python `round` | Banker's rounding at §15 boundaries |
| Delete `_TEMP.py` before commit | Commit diagnostic scripts |
---
## §E — Key files
## §E — File map
```
docs/sap-spec/sap-10-2-full-specification-2025-03-14.pdf Spec PDF
docs/sap-spec/HANDOVER_NEXT.md This file
docs/sap-spec/PARITY_FINDINGS.md Older findings
sap worksheets/ U985 + Summary PDFs
docs/sap-spec/
sap-10-2-full-specification-2025-03-14.pdf SAP 10.2 spec
RdSAP 10 Specification 10-06-2025.pdf RdSAP 10 spec
HANDOVER_NEXT.md this file
pcdb_table_105_gas_oil_boilers.jsonl PCDB combi records
sap worksheets/ U985 + Summary PDFs
packages/domain/src/domain/sap/calculator.py Top-level SAP10.2 orchestrator
packages/domain/src/domain/sap/rdsap/cert_to_inputs.py Cert→CalculatorInputs
+ section_from_cert helpers
packages/domain/src/domain/sap/tables/table_12.py SAP 10.2 Table 12 (price/CO2/PEF)
packages/domain/src/domain/sap/tables/table_12a.py Off-peak high-rate fraction
packages/domain/src/domain/sap/tables/table_32.py RdSAP 10 Table 32 (cost prices)
packages/domain/src/domain/sap/calculator.py Top-level SAP10.2 orchestrator
packages/domain/src/domain/sap/rdsap/cert_to_inputs.py Cert→CalculatorInputs + section helpers
packages/domain/src/domain/sap/tables/table_12.py Table 12 (price/CO2/PEF)
packages/domain/src/domain/sap/tables/table_12a.py Off-peak high-rate fraction
packages/domain/src/domain/sap/tables/table_32.py RdSAP10 Table 32 (cost prices)
packages/domain/src/domain/sap/worksheet/
dimensions.py §1
ventilation.py §2 + VentilationResult
heat_transmission.py §3 + HeatTransmission
water_heating.py §4 + WaterHeatingResult + water_heating_from_cert
internal_gains.py §5 + InternalGainsResult + internal_gains_from_cert
solar_gains.py §6 + solar_gains_from_cert
mean_internal_temperature.py §7
heat_transmission.py §3 + HeatTransmission + _round_half_up helper
water_heating.py §4 + WaterHeatingResult + electric_shower_monthly_kwh
internal_gains.py §5 + InternalGainsResult
solar_gains.py §6 + SolarGainsResult + RoofWindowInput
mean_internal_temperature.py §7 + MeanInternalTemperatureResult
space_heating.py §8 + SpaceHeatingResult
fabric_energy_efficiency.py §8f
space_cooling.py §8c
fabric_energy_efficiency.py §8f
energy_requirements.py §9a + EnergyRequirementsResult
fuel_cost.py §10a + FuelCostResult
rating.py §11/§13 SAP rating equations (10.2 constants — DO NOT TOUCH)
rating.py §11/§13 SAP rating equations (DO NOT TOUCH constants)
packages/domain/src/domain/sap/worksheet/tests/
test_section_cascade_pins.py Strict per-section line-ref pins (THE work)
test_e2e_elmhurst_sap_score.py SapResult-field pins + monthly_infiltration_ach pin
test_e2e_elmhurst_sap_score.py SapResult-field pins
_elmhurst_worksheet_NNNNNN.py The 6 fixture modules (1 per fixture)
_elmhurst_fixtures.py ALL_FIXTURES registry
test_dimensions.py / _ventilation.py / _heat_transmission.py / ...
← LEGACY per-section isolation tests; use PDF values as INPUTS.
Keep them but understand they don't test the cascade.
test_*.py Legacy per-section isolation tests
datatypes/epc/domain/epc_property_data.py
SapBuildingPart + sap_room_in_roof
SapRoomInRoof + detailed_surfaces
SapRoomInRoofSurface + u_value override, kind enum:
"slope" | "flat_ceiling" | "stud_wall" |
"gable_wall" | "gable_wall_external"
SapAlternativeWall + u_value override
SapRoofWindow area + u_value_raw + orientation +
pitch_deg + g_perpendicular + frame_factor
SapHeating + electric_shower_count, mixer_shower_count,
number_baths
```
---
## §F — Definitely do NOT
- Do **not** widen any tolerance, ever.
- Do **not** widen any tolerance.
- Do **not** add xfail to cascade pins.
- Do **not** "investigate later" by widening — fix it or leave it failing.
- Do **not** assume the calculator is wrong before auditing the fixture.
- Do **not** touch `rating.py` constants.
- Do **not** scan unread spec PDF pages without asking the user.
- Do **not** invoke `/ultrareview`.
- Do **not** auto-update unrelated `git status` items (deletions / new files
that aren't from your work).
- Do **not** auto-update unrelated `git status` items.
- Do **not** use Python `round()` at a §15 boundary — use `_round_half_up`.
---
## §G — Quick orient
```bash
# Run full cohort pin matrix
# Run the full cascade scoreboard
python -m pytest \
packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-header --no-cov --tb=no -q
# Run §3 pins only
python -m pytest packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py::test_section_3_line_refs_match_pdf -v --no-header --no-cov --tb=no
# Run §7 only
python -m pytest packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
-k "section_7" --no-cov --tb=no -q
# Run a single SapResult pin to see numeric diff
# Per-fixture residual diffs for a section
python -m pytest packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py \
-k "section_7 and 000474" --no-cov --tb=line
# Single SapResult pin numeric diff
python -m pytest \
"packages/domain/src/domain/sap/worksheet/tests/test_e2e_elmhurst_sap_score.py::test_sap_result_pin[000477-space_heating_kwh_per_yr]" \
--no-cov 2>&1 | grep AssertionError
# PDF §X block
# Extract a PDF §X block for a fixture
awk '/^X\. Section/,/^Y\./' "sap worksheets/U985-0001-NNNNNN.txt"
# Wider regression check
python -m pytest packages/domain/src/domain/sap/worksheet/tests/ \
packages/domain/src/domain/sap/tests/ packages/domain/src/domain/ml/ \
--no-header --no-cov --tb=no -q | tail -5
```
End of handover. Read §A again before starting.