SAP 10.2 Table 4e (PDF p.171-174) "Heating system controls" — 8 groups
covering boiler / HP / heat-network / electric-storage / warm-air /
room-heater / other systems, ~40 codes total. Pre-S0380.88 the cascade
dispatch dict had spotty coverage:
- Group 1 (BOILER): partial (12 of 13 codes)
- Group 2 (HEAT PUMP): added in S0380.87 (10 codes)
- Groups 3, 4, 5, 6, 7: completely missing
Codes from missing groups silently defaulted to type 2 — the same
failure mode that hid the cert 000565 2207→type-3 bug for ~22 slices
until S0380.87 surfaced it. Per the user-requested strict-raise
philosophy ("we keep debugging silent fallbacks"), this slice
forecloses the pattern at the dispatch boundary.
Corpus audit (full JSON sweep) at HEAD c0328f4e:
2103, 2104, 2106, 2110, 2113 (Group 1 — covered)
2206, 2207 (Group 2 — covered after S0380.87)
2307 (Group 3 — silently mis-classified)
2401 (Group 4 — silently mis-classified)
2603 (Group 6 — silently mis-classified)
Three corpus codes (2307, 2401, 2603) were silently routed to type 2
when their spec types are 2 / 3 / 3 respectively.
Fix:
- `_CONTROL_TYPE_BY_CODE` extended to full Table 4e coverage
(Groups 0-7), with per-group spec citation in comments
- New `UnmappedSapCode(ValueError)` exception class mirroring the
`UnmappedApiCode` / `UnmappedElmhurstLabel` mapper-side pattern
per [[reference-unmapped-api-code]]
- `_control_type` flipped to strict-raise: lodging absent (None /
0 / "") returns modal type 2 default; lodging present but
unmapped raises `UnmappedSapCode("main_heating_control", code)`
The strict / not-strict distinction is principled: cascade-helper
value defaults (u_wall, u_floor, ...) stay total per RdSAP §6.2.3
"assume as-built if no evidence". Code-dispatch sites strict-raise
because an unmapped code means the spec table coverage is incomplete
— a forcing function for spec-completion slices rather than a
silent miscalculation.
Tests (3 new, AAA-structure):
- `test_main_heating_control_code_table_4e_full_coverage_groups_0_through_7`
pins ~20 codes across Groups 3-7 to their spec-correct control
types (Table 4e PDF p.171-174 verbatim)
- `test_cert_to_inputs_raises_unmapped_sap_code_on_unknown_main_heating_control`
pins the strict-raise contract: lodging present but unmapped
(e.g. test code 2998) raises `UnmappedSapCode` with the field
name + value attached
- `test_cert_to_inputs_does_not_raise_when_main_heating_control_is_missing`
pins the absent-lodging contract: None / "" / 0 returns modal
type 2 default — same behaviour as pre-S0380.88 for legitimately
missing data
Test baseline: 564 pass (was 561 + 3 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Cohort + golden +
cert 9501 unaffected (their codes were all already covered or
silently routed to type 2 which is now explicit).
Pyright net-zero per touched file. The new `not code` absent-
lodging check replaces the original `code is None or code == "" or
code == 0` triple-check (pyright flagged `is None` as redundant given
`main_heating_control: Union[int, str]` annotation; runtime data
exhibits None / "" on Main 2 records that lack space-heating
controls — cert 000565 Main 2 is one such case).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>