docs: handover + next-agent prompt post S0380.105..109 (MEV trifecta + window routing + Connected gable + §5.7/5.8 brick formula)

Captures the 5-slice session that took cert 000565 continuous SAP
from +0.0182 → -0.0059 (magnitude 67% smaller) via spec-cited
intermediate-value closures.

  HANDOVER_POST_S0380_109.md     full state + per-slice movement
                                 + per-pin journey + lessons learned
  NEXT_AGENT_PROMPT_POST_S0380_109.md   focused briefing pointing
                                 at S0380.110 (Lighting g×FF closure
                                 — leading remaining residual at
                                 -2.17 kWh) and S0380.111 (roof
                                 window U formula refinement).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-05-30 18:16:16 +00:00
parent efb203f7ad
commit 98a4b5b9e6
2 changed files with 567 additions and 0 deletions

View file

@ -0,0 +1,323 @@
# Handover — post S0380.105..109 (MEV CO2/PE + window routing + Connected gable + §5.7/5.8 brick formula)
Branch: `feature/per-cert-mapper-validation`. **HEAD `efb203f7`**.
Predecessor: [`HANDOVER_POST_S0380_103.md`](HANDOVER_POST_S0380_103.md).
## Slices committed this session (S0380.105..109)
Five spec-cited slices targeting cert 000565 continuous-SAP closure.
The MEV trifecta completed first (.105/.106), then a routing fix
(.107) surfaced and re-shaped the fabric residuals, then two
spec-correct fabric closures (.108/.109) drove the fabric residual
from -0.99 W/K → +0.03 W/K and continuous SAP from +0.0182 → -0.0059
(magnitude 67% smaller).
| Slice | Commit | Spec | Cert 000565 outcome |
|---|---|---|---|
| **S0380.105** | `8a3aaf7a` | SAP 10.2 Table 12a Grid 2 + Table 12d (PDF p.191, p.194) — MEV CO2 split | `pumps_fans_co2_kg_per_yr` ✓ EXACT (35.3349 vs ws (267)). Total CO2 sign-flipped -1.81 → -2.12 (exposed downstream main_heating CO2 -2.43). |
| **S0380.106** | `8effa2d0` | SAP 10.2 Table 12a Grid 2 + Table 12e (p.191, p.195) — MEV PE split | `pumps_fans_pe_kwh_per_yr` ✓ EXACT (383.3797 vs ws (281)). PE 62228.49 → 62227.06. MEV cascade trifecta cost/CO2/PE COMPLETE. |
| **S0380.107** | `b7fa5f74` | RdSAP 10 §3.7.1 (PDF p.21) + §8.2 (p.50) — window/rooflight routing | New 4-rule heuristic uses BP roof type alongside glazing/U. Closes windows ✓ EXACT. Net fabric HTC -0.99 → +0.33 W/K. Continuous SAP +0.0182 → -0.0128 (magnitude 30% smaller). Integer SAP TRANSIENTLY 29→28 (crossed 28.5 rounding boundary). S0380.103 cost test reframed to pin rate not total. |
| **S0380.108** | `9159e91f` | RdSAP 10 §3.9.2 step (d) + Table 4 row 4 (PDF p.22-23) — Connected RR gables | New `connected_wall` kind: deducts area from A_RR but skips W/K. Closes roof +1.59 → +0.30 W/K (81% closed) + TB +0.71 → +0.15 (79%) + area +4.70 → +1.02 m² (78%). **Integer SAP RECOVERED to 29 ✓ EXACT.** Continuous SAP sign-flipped under (-0.0128 → +0.0293). |
| **S0380.109** | `efb203f7` | RdSAP 10 §5.7 Table 13 + §5.8 Table 14 (PDF p.41-42) — solid brick + insulation formula | §5.7+§5.8 chain replaces Table-6 bucket for SOLID_BRICK + lodged thickness + External/Internal insulation. Also adds Table 6 footnote (a) cap on §5.6 stone formula (only when not dry-lined). Walls -1.54 → +0.01 W/K (essentially closed). **Continuous SAP magnitude 80% improved (+0.0293 → -0.0059).** All SH-driven downstream residuals magnitude-reduced 65-80%. |
**Test baseline at HEAD `efb203f7`:** **608 pass + 7 expected
`test_sap_result_pin[000565-*]` fails**. Pyright net-zero per
touched file across every slice.
## Cert 000565 state (HEAD `efb203f7`)
### Fabric subtotals — essentially closed
| Component | Cascade W/K | Worksheet W/K | Δ | Status |
|---|---:|---:|---:|---|
| walls | 604.08 | 604.07 | **+0.01** | sub-spec float drift |
| **party_walls** | **65.13** | 65.13 | ✓ EXACT | |
| **floor** | **61.67** | 61.67 | ✓ EXACT | |
| roof | 51.68 | 51.38 | **+0.30** | sub-spec (S0380.108 closed 81%) |
| **windows** | **11.48** | 11.48 | ✓ EXACT | S0380.107 |
| roof_windows | 3.15 | 3.58 | -0.43 | cascade U formula gap (see §A below) |
| **doors** | **11.10** | 11.10 | ✓ EXACT | |
| thermal_bridging | 128.80 | 128.65 | +0.15 | sub-spec (S0380.108 closed 79%) |
| total external area | 858.66 | 857.64 | +1.02 | sub-spec (S0380.108 closed 78%) |
| **total W/K** | **937.09** | 937.06 | **+0.03** | essentially closed |
### SapResult pins (HEAD `efb203f7`)
| Pin | Cascade | Worksheet | Δ | Status |
|---|---:|---:|---:|---|
| **sap_score (int)** | **29** | 29 | **✓ EXACT** | S0380.108 |
| sap_score_continuous | 28.5028 | 28.5087 | -0.0059 | 80% smaller than .104 baseline |
| ecf | 5.3874 | 5.3866 | +0.0008 | 50% smaller than .104 |
| total_fuel_cost_gbp | 4680.78 | 4680.26 | +0.52 | was -1.62 (.104) / -2.62 (.108) |
| co2_kg_per_yr | 6448.34 | 6447.63 | +0.72 | was -1.81 (.104) |
| space_heating_kwh_per_yr | 59020.02 | 59008.35 | +11.67 | was -27.5 (.104) |
| main_heating_fuel_kwh_per_yr | 34717.66 | 34710.79 | +6.87 | was -16.2 (.104) |
| **hot_water_kwh_per_yr** | 3755.03 | 3755.03 | ✓ EXACT | unchanged |
| lighting_kwh_per_yr | 1382.67 | 1384.84 | -2.17 | rooflight g×FF default-vs-lodged drift |
| **pumps_fans_kwh_per_yr** | **252.5159** | 252.5159 | ✓ EXACT | S0380.102 |
| pumps_fans_co2_kg_per_yr | 35.3349 | 35.3349 | ✓ EXACT | S0380.105 |
| pumps_fans_pe_kwh_per_yr | 383.3797 | 383.3796 | ✓ EXACT | S0380.106 |
### Continuous SAP journey
| Slice | Δ vs ws | Notes |
|---|---:|---|
| Pre-S0380.105 | +0.0182 | Post-S0380.103 baseline (MEV cost split) |
| S0380.105 | +0.0182 | CO2 doesn't feed ECF — no continuous change |
| S0380.106 | +0.0182 | PE doesn't feed ECF either |
| S0380.107 | **-0.0128** | Window routing fix; 30% magnitude reduction; integer SAP transiently 28 |
| S0380.108 | **+0.0293** | Connected gable deduction; integer SAP back to 29; sign-flipped |
| S0380.109 | **-0.0059** | Solid brick §5.7+§5.8; 80% magnitude reduction from .108 |
**Magnitude trajectory:** 0.0182 → 0.0128 → 0.0293 → **0.0059**.
Net 67% improvement from session start.
## Open work — prioritised next slices
### S0380.110 — Lighting rooflight g×FF default-vs-lodged drift (low-medium leverage)
**Current residual:** -2.17 kWh/yr (cascade UNDER ws). After S0380.107
windows correctly route to sap_roof_windows, the cascade applies the
Appendix L L2a daylight factor formula with rooflight contribution
using `_G_LIGHT_DEFAULT = 0.80` and `_FRAME_FACTOR_DEFAULT = 0.70`
regardless of the lodged glazing/frame on each rooflight.
For cert 000565:
- Item 2 (Ext2 NR rooflight, 1.2 m², Triple glazing PVC frame):
actual g×FF = 0.70 × 0.70 = 0.49 (cascade uses 0.56)
- Item 5 (Ext4 A rooflight, 0.5 m², Double glazing Wood frame):
actual g×FF = 0.80 × 0.70 = 0.56 (cascade uses 0.56 ✓)
Area-weighted: cascade overstates G_L by ~0.052 × 1.7 m² → DF
slightly too low → lighting kWh slightly low.
**Spec:** SAP 10.2 Appendix L L2a (PDF p.~74) — G_L numerator should
use each window's own g_perpendicular and frame_factor, not defaults.
**Fix location:** `domain/sap10_calculator/worksheet/internal_gains.py`
function `_daylight_factor_from_cert`, the `rooflight_g_l_numerator`
computation around line 613-618 — iterate `epc.sap_roof_windows` and
use each one's actual `g_perpendicular` + `frame_factor` instead of
defaults.
**Expected closure:** lighting -2.17 → ~0 kWh/yr. Tiny continuous-SAP
ripple (lighting feeds CO2/cost/PE via the Table 12 monthly factors).
### S0380.111 — Roof window U formula refinement (low leverage)
**Current residual:** -0.43 W/K (cascade UNDER ws). Cascade computes
roof window effective U via `1 / (1/U_raw + 0.04)` = 1.852 for U_raw =
2.0. Worksheet uses U_eff = 2.1062 for the same raw U.
Reverse-engineered: 1/2.1062 = 0.4748; 0.5 (=1/U_raw) - 0.4748 =
0.0252 — so the spec correction for roof windows differs from the
vertical-window +0.04 by a factor of 0.0648.
**Spec hunt:** SAP 10.2 §3.2 / Table 6c (PDF p.51). Table 6c has a
distinct "U-value** (roof window)" column with values higher than the
vertical-glazing column (by typically ~+0.2-0.3 W/m²K). The exact
correction depends on the spec's definition of roof-window surface
resistances vs vertical-window film coefficients.
**Likely fix:** in `heat_transmission.py` the roof-window effective U
should use a lookup keyed on the lodged glazing type rather than a
flat +0.04 correction (which is the SAP10.2 §3.2 "windows" formula,
not the rooflight one).
**Expected closure:** roof_windows -0.43 → 0 W/K. HTC change +0.43
W/K → continuous SAP -0.0015 (cascade more under). The roof_windows
closure makes the residual SHIFT in the same direction as current
-0.0059, so net continuous SAP slightly worse before lighting closes.
### S0380.112 — Walls precision +0.01 W/K (sub-spec)
Tiny float rounding artifact in BP[0] alt_wall_1 (granite + dry-line):
cascade computes raw U=2.3405, ws displays U=2.34, A×U product diff is
0.01 W/K. Rounding to 2 d.p. in the §5.6 dry-line path was added in
S0380.109 — verify it fires for this case.
### Deferred (unchanged from earlier handovers)
- 12 gas-combi PV certs at +0.5..+1.6 PE (no worksheets)
- 5 SAP-residual API-only certs (no worksheets)
## What this session learned
### Pattern: spec-correct intermediate fixes can sign-flip end-result residuals
Each of S0380.107, .108, .109 shifted end-result residuals (cost,
CO2, SH, continuous SAP) by amounts larger than the closure itself —
because the cascade's pre-slice residuals were partially CANCELLING.
Removing one mis-handled component exposes other residuals that were
masked.
The user's stated philosophy makes this explicit:
> "It's okay if we temp drift away from continuous SAP, as long as we
> are actually fixing true problems with the intermediate values.
> Eventually, I expect the error of continuous SAP to be zero but
> that is only possible if we fix all of the sub components and
> remain true to spec."
The trajectory bears this out: 5 slices → continuous SAP magnitude
0.0182 → 0.0059 (67% improvement) through multiple sign-flips along
the way.
### Pattern: existing snapshot tests need updating when they pin downstream metrics
S0380.103 cost test (`test_summary_000565_mev_fans_cost_uses_table_
12a_grid_2_fans_for_mech_vent_rate`) was originally written against
`total_fuel_cost_gbp` with a tight `< +£0.05` threshold. After
S0380.107 broke that threshold (cascade catches up on fabric HTC and
total cost shifts by £2+), the test was reframed to pin
`inputs.pumps_fans_fuel_cost_gbp_per_kwh` directly — the specific
metric S0380.103 closes, decoupled from downstream changes.
Similarly, golden cert 6035 PE/CO2 pins were updated in S0380.109 per
[[feedback-golden-residuals-near-zero]] — the cascade got closer to
the actual EPC value, which is the intended direction.
When future slices fire on a cert that's pinned with downstream
metrics, the same pattern applies: update the pin or reframe to a
narrower intermediate.
## MEV PCDB arc — architecture summary (unchanged from .103 handover)
The S0380.98..106 arc landed the entire MEV decentralised cascade
end-to-end. Architecture in dependency order:
```
PCDB pcdb10.dat
↓ ETL (etl.py, parser.py)
Table 322 (per-fan SFP) ←→ Table 329 (per-ducting IUF)
↓ runtime lookups (__init__.py)
decentralised_mev_record(pcdb_id) + mv_in_use_factors_record(system_type)
worksheet/mev.py — pure helpers
mev_sfp_av(fan_entries) → §2.6.4 equation (1) avg SFP
mev_decentralised_kwh_per_yr(sfp_av, V) → Table 4f line (230a) kWh
cert_to_inputs.py
_mev_decentralised_kwh_per_yr_from_cert(epc)
reads epc.mechanical_ventilation_index_number, .wet_rooms_count,
.mechanical_vent_duct_type
invokes mev.py helpers
_table_4f_additive_components(epc) adds MEV → pumps_fans_kwh_per_yr
For COST (S0380.103):
_pumps_fans_fuel_cost_gbp_per_kwh(tariff, mev_kwh, total_pumps_fans_kwh)
→ kWh-weighted blended rate (FANS_FOR_MECH_VENT vs ALL_OTHER_USES)
For CO2 (S0380.105):
_pumps_fans_co2_factor_kg_per_kwh(tariff, mev_kwh, total_pumps_fans_kwh, monthly)
→ kWh-weighted blend of FANS_FOR_MECH_VENT + ALL_OTHER_USES Table 12d factors
For PE (S0380.106):
_pumps_fans_primary_factor(tariff, mev_kwh, total_pumps_fans_kwh, monthly)
→ kWh-weighted blend of FANS_FOR_MECH_VENT + ALL_OTHER_USES Table 12e factors
```
All three helpers fall back to the existing ALL_OTHER_USES rate on
STANDARD-tariff certs and no-MEV certs (cohort-safe). The MEV
cascade trifecta is now COMPLETE for cert 000565.
## How to run the baseline
```bash
PYTHONPATH=/workspaces/model python -m pytest \
backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \
backend/documents_parser/tests/test_elmhurst_extractor.py \
backend/documents_parser/tests/test_elmhurst_end_to_end.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py \
domain/sap10_calculator/worksheet/tests/test_mev.py \
domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \
domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \
domain/sap10_calculator/tests/test_pcdb_table_322_lookup.py \
domain/sap10_calculator/tests/test_pcdb_table_329_lookup.py \
--no-cov -q
```
Expected: **608 pass + 7 expected `test_sap_result_pin[000565-*]`
fails**.
The 7 expected fails (verbatim):
```
sap_score_continuous
ecf
total_fuel_cost_gbp
co2_kg_per_yr
space_heating_kwh_per_yr
main_heating_fuel_kwh_per_yr
lighting_kwh_per_yr
```
All driven by the residual lighting -2.17 kWh + roof window U formula
gap -0.43 W/K + sub-spec walls float drift +0.01 W/K. The first two
are the open S0380.110 / S0380.111 work fronts.
## Files touched this session
| File | Slices | Change |
|---|---|---|
| `domain/sap10_calculator/rdsap/cert_to_inputs.py` | .105, .106 | `_pumps_fans_co2_factor_kg_per_kwh` + `_pumps_fans_primary_factor` helpers + wire into call sites |
| `datatypes/epc/domain/mapper.py` | .107, .108 | Survey-aware `_is_elmhurst_roof_window` predicate; `_elmhurst_bp_roof_type` helper; Connected-gable routing to new `connected_wall` kind |
| `domain/sap10_calculator/worksheet/heat_transmission.py` | .108, .109 | `connected_wall` branch (deducts area, no W/K); pass `wall_thickness_mm` to per-BP main wall `u_wall` |
| `domain/sap10_ml/rdsap_uvalues.py` | .109 | `_u_brick_thin_wall_age_a_to_e` (§5.7 Table 13); `_r_insulation_table_14` (§5.8 Table 14 interpolation); §5.7+§5.8 branch in `u_wall`; Table 6 footnote (a) cap on §5.6 stone (only when not dry-lined); 2 d.p. rounding on §5.6 dry-line result |
| `domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py` | .109 | Re-pin cert 6035 PE/CO2 expectations |
| `backend/documents_parser/tests/test_summary_pdf_mapper_chain.py` | .105, .106, .107, .108, .109 | AAA tests for each slice; S0380.103 test reframed to pin cost rate directly |
## Spec source quick-reference
- **SAP 10.2 full specification**: `domain/sap10_calculator/docs/specs/sap-10-2-full-specification-2025-03-14.pdf`
- §3.2 / Table 6c (p.51) — Window/rooflight U formula — S0380.111 target
- §10a Table 12a Grid 2 (p.191) — Other electricity uses high-rate fraction — S0380.105, .106
- §10b Table 12d (p.194) — Monthly CO2 factors — S0380.105
- §10c Table 12e (p.195) — Monthly PE factors — S0380.106
- Appendix L L2a (p.~74) — Daylight factor G_L — S0380.110 target
- **RdSAP 10 specification**: `domain/sap10_calculator/docs/specs/RdSAP 10 Specification 10-06-2025.pdf`
- §3.7.1 (p.21) — Window vs roof window classification — S0380.107
- §3.9.2 step (d) (p.23) — Connected gable area deduction — S0380.108
- §5.6 (p.40) + Table 12 (p.41) — Stone wall thin-wall formula — S0380.109 (cap)
- §5.7 (p.41) + Table 13 (p.41) — Brick wall U₀ by thickness — S0380.109
- §5.8 (p.41-42) + Table 14 (p.42) — Insulation R formula — S0380.109
- §8.2 (p.50) — Glazed walls/roof routing — S0380.107
- Table 4 row 4 (p.22) — Connected gable U=0 — S0380.108
- Table 6 footnote (a) (p.34) — §5.6 formula cap — S0380.109
- **SAP 10.3 at** `sap-10-3-full-specification-2026-01-13.pdf`: **DO NOT reference** ([[feedback-sap-10-2-only-never-10-3]])
## Memory updated this session
- `project_cert_000565_recovery_state` — appended .105/.106/.107/.108/.109 closures + open-work analysis
- `MEMORY.md` — refreshed at HEAD `efb203f7`
## What NOT to do
- **Don't reference SAP 10.3** ([[feedback-sap-10-2-only-never-10-3]]).
- **Don't widen pin tolerances or xfail residual gaps**
([[feedback-zero-error-strict]]). The 7 cert 000565 fails are the
work queue. When a slice surfaces a downstream pin that drifts (e.g.
the integer SAP rounding flip in S0380.107), bring it back via a
complementary closure in a subsequent slice (S0380.108 pattern).
- **Don't re-investigate any closed work** (.91..109). All settled.
- **Don't add new helpers to `domain/sap10_ml/`** — deprecation path
per [[project-sap10_ml-deprecation]]. New cascade helpers belong
under `domain/sap10_calculator/`. (S0380.109 extended existing
helpers in `rdsap_uvalues.py` — acceptable since the file is the
current authoritative wall-U-value table and a migration plan
hasn't yet landed for it specifically.)
- **Don't avoid spec-correct closures because continuous SAP drifts
away or sign-flips** — user explicitly OK'd transient drift. Zero
error achievable only when every component is spec-correct.
- **Don't pin downstream-only metrics with tight thresholds**
S0380.103 cost test pattern. Pin the narrowest intermediate the
slice changes.
## Memory hygiene
After the next slice, update:
- `project_cert_000565_recovery_state` — append slice closure +
refresh the open work-items table
- `MEMORY.md` — refresh HEAD + one-line summary
Good luck.

View file

@ -0,0 +1,244 @@
# Next-agent prompt — post S0380.105..109
Branch: `feature/per-cert-mapper-validation`.
HEAD: `efb203f7`.
Read these in order before any tool call:
1. [`HANDOVER_POST_S0380_109.md`](HANDOVER_POST_S0380_109.md) — full state
2. [`HANDOVER_POST_S0380_103.md`](HANDOVER_POST_S0380_103.md) — predecessor (background)
Also load these memories before starting:
- `project_cert_000565_recovery_state` — per-slice history + per-pin state
- `reference_unmapped_sap_code` — calculator strict-raise pattern
- `project_sap10_ml_deprecation``domain/sap10_ml/` is on the
deprecation path; new cascade helpers should land under
`domain/sap10_calculator/`
- `feedback_sap_10_2_only_never_10_3`**CRITICAL** — never reference
SAP 10.3 spec
- `feedback_spec_citation_in_commits` — quote spec text + page in
commit messages
- `feedback_verify_handover_claims` — verify spec citations + numeric
claims before implementing the prescribed fix
- `feedback_zero_error_strict` — pyright net-zero per touched file
- `feedback_commit_per_slice` — one slice = one commit
- `feedback_aaa_test_convention` — every new test uses literal
`# Arrange / # Act / # Assert` headers
- `feedback_e2e_validation_philosophy` — component pins at <1e-3;
SAP integer delta=0; no adaptive ceilings
- `feedback_abs_diff_over_pytest_approx` — use `abs(x - y) <= tol`
instead of `pytest.approx` to keep pyright net-zero
- `feedback_spec_floor_skepticism` — skeptical of "spec-precision
floor" claims; verify the spec citation against the PDF first
- `feedback_golden_residuals_near_zero` — golden pins should be
re-pinned closer to zero as the cascade improves
## Critical user direction
The user's **primary metric is `sap_score_continuous`** (not just
integer `sap_score`). The user has explicitly stated:
> "It's okay if we temp drift away from continuous SAP, as long as we
> are actually fixing true problems with the intermediate values.
> Eventually, I expect the error of continuous SAP to be zero but
> that is only possible if we fix all of the sub components and
> remain true to spec."
And:
> "We should aim to get SAP continue exact, along with all sections.
> But we'll see."
**Implication:** ship spec-correct slices even when they cause
transient continuous-SAP drift. Sign-flips are expected and OK —
they mean a previously-cancelling residual is now exposed.
## State summary
This session shipped **S0380.105..109** — five spec-cited slices.
Trifecta-complete on MEV cascade (cost/CO2/PE), then three fabric
closures that moved continuous SAP from +0.0182 → -0.0059 (magnitude
67% smaller).
1. **S0380.105** (`8a3aaf7a`) — MEV CO2 split via Table 12a Grid 2 +
Table 12d. `pumps_fans_co2` ✓ EXACT.
2. **S0380.106** (`8effa2d0`) — MEV PE split via Table 12a Grid 2 +
Table 12e. `pumps_fans_pe` ✓ EXACT. MEV trifecta COMPLETE.
3. **S0380.107** (`b7fa5f74`) — Window/rooflight routing via BP roof
type (RdSAP 10 §3.7.1 + §8.2). Windows ✓ EXACT. Net fabric HTC
-0.99 → +0.33 W/K. Continuous SAP +0.0182 → -0.0128. Integer SAP
transiently 28 (rounding boundary).
4. **S0380.108** (`9159e91f`) — Connected RR gables deduct from A_RR
(RdSAP 10 §3.9.2 step d + Table 4 row 4). Roof/TB/area all closed
~80%. **Integer SAP recovered to 29 ✓ EXACT.** Continuous SAP
sign-flipped to +0.0293.
5. **S0380.109** (`efb203f7`) — Solid brick + insulation via §5.7
Table 13 + §5.8 Table 14. Walls -1.54 → +0.01 W/K (essentially
closed). **Continuous SAP magnitude 80% improved (+0.0293 →
-0.0059).** All SH-downstream residuals magnitude-reduced 65-80%.
**Cert 000565 state at HEAD `efb203f7`:**
| Pin | Cascade | Worksheet | Δ |
|---|---:|---:|---:|
| **sap_score (int)** | **29** | 29 | **✓ EXACT** |
| sap_score_continuous | 28.5028 | 28.5087 | -0.0059 |
| ecf | 5.3874 | 5.3866 | +0.0008 |
| total_fuel_cost_gbp | 4680.78 | 4680.26 | +0.52 |
| co2_kg_per_yr | 6448.34 | 6447.63 | +0.72 |
| space_heating_kwh_per_yr | 59020.02 | 59008.35 | +11.67 |
| main_heating_fuel_kwh_per_yr | 34717.66 | 34710.79 | +6.87 |
| **pumps_fans_kwh_per_yr** | **252.5159** | 252.5159 | **✓ 0 EXACT** |
| **hot_water_kwh_per_yr** | 3755.0288 | 3755.0288 | ✓ 0 EXACT |
| lighting_kwh_per_yr | 1382.6657 | 1384.8353 | -2.17 |
**Fabric (cascade vs ws):**
| Component | Δ W/K |
|---|---:|
| walls | +0.01 (sub-spec float drift) |
| roof | +0.30 |
| windows | ✓ 0 EXACT |
| roof_windows | -0.43 (cascade U formula gap) |
| TB | +0.15 |
| **total** | **+0.03** (essentially closed) |
## Recommended next slice — S0380.110 § Lighting rooflight g×FF default-vs-lodged drift
**Current residual:** -2.17 kWh/yr (cascade UNDER ws lighting).
### Why it's now the leading residual
After S0380.107 windows correctly route to sap_roof_windows, the
cascade applies the Appendix L L2a daylight factor formula with
rooflight contribution using `_G_LIGHT_DEFAULT = 0.80` and
`_FRAME_FACTOR_DEFAULT = 0.70` regardless of the lodged glazing/frame
on each rooflight (`domain/sap10_calculator/worksheet/internal_gains.py`
function `_daylight_factor_from_cert` at lines ~613-618).
For cert 000565:
- Item 2 (Ext2 rooflight, 1.2 m², Triple PVC): actual g×FF = 0.70 × 0.70 = 0.49 (cascade uses 0.56)
- Item 5 (Ext4 rooflight, 0.5 m², Double Wood): actual g×FF = 0.80 × 0.70 = 0.56 (cascade uses 0.56 ✓)
Area-weighted cascade OVERSTATES rooflight G_L contribution by
~0.052 × 1.7 m² → DF too low → cascade lighting kWh too low.
### Spec citation target
SAP 10.2 Appendix L §L2a (PDF p.~74) — the G_L numerator formula sums
over each window with its OWN glazing-type g_perpendicular and frame
factor, not a fixed default. Verify by reading the L2a / Table 6d
section before implementing.
### Investigation approach
1. Confirm the L2a spec formula uses per-window g and FF.
2. Probe the cascade vs worksheet for cert 000565 daylight factor:
```python
from domain.sap10_calculator.worksheet.tests._elmhurst_worksheet_000565 import build_epc
from domain.sap10_calculator.worksheet.internal_gains import _daylight_factor_from_cert, OvershadingCategory
from domain.sap10_calculator.rdsap.cert_to_inputs import _rooflight_total_area_m2_from_cert
epc = build_epc()
rooflight_area = _rooflight_total_area_m2_from_cert(epc)
df = _daylight_factor_from_cert(epc, OvershadingCategory.AVERAGE, rooflight_area)
# cascade df ~ 1.34; ws implied df from continuous E_L ~ 1.34 + small delta
```
3. Change `_daylight_factor_from_cert` to iterate `epc.sap_roof_windows`
for the rooflight numerator, summing `area × g_perpendicular ×
frame_factor × 1.0` (Z_L = 1.0 for rooflights per Table 6d note 2).
4. Sanity-check cohort: cohort certs that have rooflights (e.g. 000516
W6) lodge similar g/FF as the current defaults → minimal cohort
change.
### Expected closure
- lighting_kwh_per_yr -2.17 → ~0 kWh/yr
- continuous SAP -0.0059 → small change (lighting feeds CO2/cost/PE
via Table 12 monthly factors)
## Alternative next slice — S0380.111 § Roof window U formula refinement
**Current residual:** -0.43 W/K (cascade UNDER ws on roof_windows).
Cascade computes roof window effective U via `1 / (1/U_raw + 0.04)` =
1.852 for U_raw = 2.0. Worksheet uses U_eff = 2.1062 for the same raw
U. The cascade's vertical-window formula doesn't apply to rooflights
— SAP 10.2 Table 6c has a distinct "U-value (roof window)" column.
**Spec hunt:** SAP 10.2 §3.2 / Table 6c (PDF p.51) — has separate
"U-value** (roof window)" column. The note says "Roof pitch 45°
(unless horizontal), wooden or PVC". The Table 6c values for the
glazing types lodged on cert 000565 rooflights (Double 2002-2021
@ U=2.0 raw, Triple 2002-2021 @ U=2.0 raw) should give U_eff = 2.11.
**Fix location:** `domain/sap10_calculator/worksheet/heat_transmission.py`
roof window U computation — should use Table 6c roof-window column
keyed on glazing type rather than the +0.04 vertical-window formula.
**Lower leverage** than S0380.110 — closes -0.43 W/K HTC →
~-0.0015 continuous SAP shift. The roof_windows closure makes the
residual SHIFT in the same direction as current -0.0059, so net
continuous SAP slightly worse before S0380.110 lighting closes.
## Standard workflow per slice
1. Read SAP 10.2 / RdSAP 10 spec page for the change — quote it in commit
2. Probe current cascade output; identify exact spec-vs-cascade gap
3. Write failing test FIRST (AAA structure)
4. Implement helper / change
5. Verify test passes
6. Run full handover suite (command below)
7. Check pyright on touched files — net-zero from baseline
(use `git stash` + re-run pyright to compute baseline)
8. Commit with spec citation + verbatim quote
9. Update relevant memory if state changed
## How to run the baseline
```bash
PYTHONPATH=/workspaces/model python -m pytest \
backend/documents_parser/tests/test_summary_pdf_mapper_chain.py \
backend/documents_parser/tests/test_elmhurst_extractor.py \
backend/documents_parser/tests/test_elmhurst_end_to_end.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
domain/sap10_calculator/worksheet/tests/test_appendix_h_solar.py \
domain/sap10_calculator/worksheet/tests/test_mev.py \
domain/sap10_calculator/rdsap/tests/test_cert_to_inputs.py \
domain/sap10_calculator/rdsap/tests/test_golden_fixtures.py \
domain/sap10_calculator/tests/test_pcdb_table_322_lookup.py \
domain/sap10_calculator/tests/test_pcdb_table_329_lookup.py \
--no-cov -q
```
Expected: **608 pass + 7 expected `test_sap_result_pin[000565-*]`
fails**.
After S0380.110 the lighting pin should close to ✓ EXACT (6 expected
fails). After both .110 and .111, the remaining sub-spec residuals
should be in a closure-ready state for the final continuous-SAP push.
## What NOT to do
- **Don't reference SAP 10.3** ([[feedback-sap-10-2-only-never-10-3]]).
- **Don't widen pin tolerances or xfail residual gaps**
([[feedback-zero-error-strict]]). The 7 cert 000565 fails are the
work queue.
- **Don't re-investigate any closed work** (.91..109). All settled.
- **Don't add new helpers to `domain/sap10_ml/`** — deprecation path
per [[project-sap10_ml-deprecation]]. New cascade helpers belong
under `domain/sap10_calculator/`.
- **Don't avoid spec-correct closures because continuous SAP drifts
away** — user explicitly OK'd transient drift. Zero error
achievable only when every component is spec-correct.
- **Don't pin downstream-only metrics with tight thresholds** — pin
the narrowest intermediate the slice changes.
## Memory hygiene
After the next slice, update:
- `project_cert_000565_recovery_state` — append closure + open work-
items refresh
- `MEMORY.md` index — refresh HEAD + one-line summary
Good luck.