Bundled slice closing the next 6 silent-fallback dispatch sites flagged
by the post-S0380.89 audit per [[reference-unmapped-sap-code]]:
1. PV pitch (RdSAP 10 §11.1 — codes 1..5 → 0/30/45/60/90°)
2. PV overshading (SAP 10.2 Table M1 — codes 1..4 → 1.0/0.8/0.5/0.35)
3. Meter type (RdSAP cert enum 1..5 → Tariff enum)
4. Tariff → (high, low) rate (RdSAP 10 Table 32 — 4 of 5 Tariffs)
5. Heat-network DLF by age band (SAP 10.2 Table 12c — A..M)
6. Secondary heating fraction by main_heating_category (SAP Table 11)
Each dispatch follows the established strict / total split:
- Absent lodging (None / 0 / "") → cascade's modal-default value
- Lodging present but unmapped → `UnmappedSapCode(field, value)`
`UnmappedSapCode` promoted from `cert_to_inputs.py` to new module
`domain/sap10_calculator/exceptions.py` so `tables/table_12a.py` can
raise it too (the meter-type dispatch lives there). `cert_to_inputs`
re-exports it for backward compat with existing test imports.
Corpus audit at HEAD 6d02d205 (full JSON sweep):
PV pitch codes: {2, 3} — covered
PV overshading codes: {1, 2} — covered
meter_type codes: {1, 2, 3} — covered (incl. digit-string '2')
main_heating_category: {2, 4, 6, 7, 10} — covered
All corpus codes already in dispatch dicts — no production regression
expected.
**One silent runtime fix surfaced by the strict-raise rollout**: the
GOV.UK API lodges `meter_type` as a digit-string (e.g. `'2'`) on many
certs, but the original `_METER_STR_TO_INT` dict only had word aliases
("single", "dual", "unknown"). Pre-S0380.90 the digit-string fell
through to the silent `return Tariff.STANDARD` default. Adding a
`key.isdigit() → int(key)` short-circuit routes these through the int
enum correctly. Confirmed 125 golden cert fixtures previously running
on this silent default — all now passing with explicit STANDARD via
the int dispatch path (not via the silent fallback).
Tests (6 new, AAA-structure):
- `test_pv_pitch_deg_full_table_coverage_per_rdsap_10_section_11_1`
- `test_pv_overshading_factor_full_table_m1_coverage`
- `test_meter_type_dispatch_full_table_12a_coverage` (incl. digit-string)
- `test_tariff_high_low_rates_full_dispatch_coverage`
- `test_heat_network_dlf_full_table_12c_age_band_coverage`
- `test_secondary_heating_fraction_for_category_full_table_11_coverage`
Each test pins: spec-correct codes → expected dispatch result; absent
lodging → modal default; lodging present but unmapped → `UnmappedSapCode`
with field + value attached.
Test baseline: 574 pass (was 568 + 6 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Cohort + golden +
cert 9501 unaffected. Pyright net-zero per touched file.
Open silent-fallback inventory now empty per
[[reference-unmapped-sap-code]] — the cascade dispatch boundary is
now fully strict-raise-gated for code translations. Cascade VALUE
defaults (u_wall, u_floor, etc.) remain total per RdSAP §6.2.3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>