Model/domain
Khalim Conn-Kowlessar 089e6ac9da Slice S0380.89: Table 4d responsiveness dispatch + screed-UFH bug fix + strict raise
SAP 10.2 Table 4d (PDF p.170) "Heating type and responsiveness ...
depending on heat emitter":

  Heat emitter                                        R
  -------------------------------------------------   -----
  Systems with radiators                              1.0
  Underfloor (wet) — pipes in insulated timber floor  1.0
  Underfloor (wet) — pipes in screed above insulation 0.75
  Underfloor (wet) — pipes in concrete slab           0.25
  Warm air via fan coil units                         1.0

Pre-S0380.89 the cascade `_responsiveness` had:

    if isinstance(emitter, int) and emitter == 2:
        return 0.25
    return 1.0

But the Elmhurst cert-side enum (`_ELMHURST_HEAT_EMITTER_TO_SAP10` at
`datatypes/epc/domain/mapper.py:3646`) maps:

    1 = Radiators
    2 = Underfloor (in screed)         ← spec R=0.75, NOT 0.25
    3 = Underfloor (timber floor)
    4 = Warm air
    5 = Fan coils

The cascade silently treated screed UFH (Elmhurst code 2) as
concrete-slab UFH (R=0.25 — Table 4d's most thermally massive UFH
variant). The bug halved the actual spec responsiveness — for a screed
UFH cert, off-period temperature reduction was computed with R=0.25
instead of R=0.75, materially under-counting MIT drop and over-counting
SH demand.

The bug is latent on cohort + golden because `_first_main_heating`
picks main[0] and almost all certs lodge radiators (emitter=1) there.
Corpus audit (full JSON sweep): emitter=2 appears on 2 records and
in both cases on a secondary main slot (e.g. golden cert
0240-0200-5706-2365-8010 main[1]) — never on the selected first main.
The fix preempts the next cert that lodges screed UFH on main[0].

Fix:

  - New `_RESPONSIVENESS_BY_EMITTER_CODE` dispatch dict reflecting
    Table 4d per the Elmhurst cert-side enum (1: 1.0, 2: 0.75, 3: 1.0,
    4: 1.0, 5: 1.0). "Concrete slab UFH" (Table 4d R=0.25) has no
    cert-side enum entry — that variant would need a new mapper code
    before the cascade can dispatch it.
  - `_responsiveness` flipped to strict-raise per [[reference-
    unmapped-sap-code]]: absent lodging (None / 0 / "") returns modal
    R=1.0 default; lodging present but unmapped raises
    `UnmappedSapCode("heat_emitter_type", value)`.

Tests (4 new, AAA-structure):

  - `test_heat_emitter_code_2_underfloor_in_screed_routes_to_responsiveness_0p75_per_table_4d`
    pins the bug fix: emitter=2 → R=0.75 (was 0.25)
  - `test_heat_emitter_code_dispatch_table_4d_full_coverage`
    pins all 5 Elmhurst emitter codes to spec-correct R
  - `test_responsiveness_raises_unmapped_sap_code_on_unknown_emitter`
    pins the strict-raise contract (hypothetical code 99 raises)
  - `test_responsiveness_default_1p0_when_emitter_lodging_absent`
    pins the absent-lodging contract (None / 0 / "" → 1.0)

Test baseline: 568 pass (was 564 + 4 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Cohort + golden
unaffected (all use emitter=1 on main[0]).

Pyright net-zero per touched file (one `pyright: ignore` added on the
absent-lodging test where `main_heating_control=None` is passed to a
dataclass declaring `Union[int, str]` — runtime data exhibits None
on certs lacking space-heating controls, so the test covers a real
codepath the type system doesn't model).

Per the user-requested "we keep debugging silent fallbacks" mandate,
this is the second slice (after S0380.88) in the strict-raise series.
Next candidates per [[reference-unmapped-sap-code]]: PV pitch +
overshading, meter→tariff, heat-network DLF, secondary-heating
fraction by category.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-01 16:28:47 +00:00
..
addresses standardist Address 2026-05-22 10:13:32 +00:00
data_transformation moved classifier data transformation to an easy one 2026-06-01 14:53:34 +00:00
epc pr review, move domain and orhcestration 2026-06-01 14:00:31 +00:00
sap10_calculator Slice S0380.89: Table 4d responsiveness dispatch + screed-UFH bug fix + strict raise 2026-06-01 16:28:47 +00:00
sap10_ml Slice S0380.86: §5.6 thin-wall stone + §5.8 dry-line closes BP[0] alt1 cascade gap 2026-06-01 16:28:47 +00:00
tasks added postcode splitter rewrite to ddd 2026-05-19 16:35:09 +00:00
postcode.py get rid of comments 2026-05-20 13:21:11 +00:00