Model/backend/documents_parser
Khalim Conn-Kowlessar 0aa40b63cd Slice S0380.132: strict-raise MissingMainFuelType on empty main_fuel_type
The cascade's `_main_fuel_code` previously returned None when
`MainHeatingDetail.main_fuel_type` was anything other than an int
(empty string, None, or an unmapped string label). The downstream
`table_32.unit_price_p_per_kwh(None)` then silently defaulted to mains
gas (3.48 p/kWh / CO2 0.21 kg/kWh / η 0.45 / PE 1.22) — a misleading
fallback where cost may happen to be close but CO2 / PE / efficiency
are completely wrong for the actual heating system.

Probe of the heating-systems corpus surfaced 26 of 41 controlled-
variable variants with `main_fuel_type=''`:

  Community heating 1/2/3/4/6 (Table 4a 301-304)        5
  Electric 11/12/13/14 (Table 4a 5xx/6xx/7xx)           4
  No system (SAP code 699)                              1
  Oil 2 (HVO) / oil 3 (FAME) / oil 4 (FAME) /
    oil 5 (bioethanol) / oil 6 (B30K) (Table 4b)        5
  Solid fuel 2..11 (Table 4a 150-160 + 600-636)        10
  pcdb 3 (lodges 'Bulk LPG' string — mapper dict gap)   1

Each pre-slice carried a residual pin in `_EXPECTATIONS` encoding the
broken mains-gas-default state. Solid fuel 8's +0.87 ΔSAP — the
"smallest open residual" the user asked to investigate next — turned
out to be the net of compensating cost/efficiency errors; the CO2
delta was +3525 kg/yr and PE +4103 kWh/yr because the cascade was
costing wood chips as mains gas.

Two changes land together:

1. Add `MissingMainFuelType(ValueError)` to
   `domain/sap10_calculator/exceptions.py`. Semantics distinct from
   the sibling `UnmappedSapCode` (which is for unmapped int dispatch
   codes; this is for "value not resolvable to a SAP fuel code at
   all"). The error message names the lodged value + the
   `sap_main_heating_code` hint so the upstream mapper fix is
   obvious.

2. `_main_fuel_code` in `cert_to_inputs.py` now raises
   `MissingMainFuelType` when `main_fuel_type` is not an int.
   `main is None` still returns None (genuinely no main heating).

The 26 blocked corpus variants are lifted out of the
`_EXPECTATIONS` residual-pin grid into a new tuple
`_BLOCKED_BY_MISSING_MAIN_FUEL_TYPE` driving a new parametrised test
`test_heating_systems_corpus_blocked_variant_raises_missing_main_fuel_type`
that asserts the raise for each blocked variant. As mapper-side fixes
land (deriving fuel from `sap_main_heating_code` via SAP 10.2 Table
4a/4b/4f, or extending `_ELMHURST_MAIN_FUEL_TO_SAP10`), variants move
back onto the residual-pin grid.

Mirrors the [[reference-unmapped-sap-code]] / [[reference-unmapped-
api-code]] strict-raise pattern: forcing function for spec/mapper
completion at the cascade boundary instead of silently producing
wrong outputs.

Extended handover suite at HEAD post-slice: 875 pass / 0 fail (was
874; +1 from the new `_main_fuel_code` strict-raise unit test;
26 blocked corpus pins replaced 1:1 by 26 assert-on-raise tests).

Pyright net-zero (43 → 43 — all pre-existing `pytest.approx` flags).

No golden fixture impact — every golden cert carries an int
`main_fuel_type`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 09:43:27 +00:00
..
handler address JTK review comments 2026-04-20 15:11:17 +00:00
tests Slice S0380.132: strict-raise MissingMainFuelType on empty main_fuel_type 2026-05-31 09:43:27 +00:00
__init__.py Map to RdSapSiteNotes from site notes JSON 🟥 2026-04-16 13:54:03 +00:00
db_writer.py include updating epc_property_data to pashub to ara workflow 2026-04-29 09:55:14 +00:00
elmhurst_extractor.py Slice S0380.128: extractor §14.0 closure falls back to "14.1 Community Heating" 2026-05-31 08:26:24 +00:00
extractor.py Handle wall thickness "Unmeasurable" 🟩 2026-04-30 16:41:16 +00:00
local_runner.py update local runner to work for elmhurst 2026-04-24 14:01:36 +00:00
parser.py load ecmk site notes to db 2026-04-29 11:20:47 +00:00
pdf.py update local runner to work for elmhurst 2026-04-24 14:01:36 +00:00