From de7bfc5dfef858199ce58a5a61bbedcbdf750f73 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 1 Jul 2026 15:56:33 +0000 Subject: [PATCH] =?UTF-8?q?Reclassify=20Gas=20CPSU=20dumping-ground=20over?= =?UTF-8?q?rides=20via=20the=20generalized=20main-heating=20reclassify=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- ..._storage.py => reclassify_main_heating.py} | 46 ++++---- .../scripts/test_reclassify_hhrsh_storage.py | 70 ------------ tests/scripts/test_reclassify_main_heating.py | 101 ++++++++++++++++++ 3 files changed, 126 insertions(+), 91 deletions(-) rename scripts/{reclassify_hhrsh_storage.py => reclassify_main_heating.py} (71%) delete mode 100644 tests/scripts/test_reclassify_hhrsh_storage.py create mode 100644 tests/scripts/test_reclassify_main_heating.py diff --git a/scripts/reclassify_hhrsh_storage.py b/scripts/reclassify_main_heating.py similarity index 71% rename from scripts/reclassify_hhrsh_storage.py rename to scripts/reclassify_main_heating.py index 952b205b7..a51743aa4 100644 --- a/scripts/reclassify_hhrsh_storage.py +++ b/scripts/reclassify_main_heating.py @@ -1,9 +1,11 @@ -"""One-time re-classification of HHRSH overrides funnelled into old storage. +"""One-time re-classification of main-heating overrides funnelled into a garbage +drawer for want of a correct archetype (#1376). -#1376 / ADR-0044: with no High Heat Retention archetype, the LLM classified -"Electric Storage Systems: High heat retention storage heaters" to -``"Electric storage heaters, old"`` (SAP 401) — scoring the *best* modern storage -as the *worst* old large-volume type. An audit found 111 such rows. +Covers both dumping grounds the ``main_heating_guard`` now rescues: +- **HHRSH** (ADR-0044): "High heat retention storage heaters" → "Electric storage + heaters, old" (SAP 401), the worst old type — 111 rows. +- **Gas CPSU** (ADR-0045): solid-fuel room heaters (open fire / + back boiler / + closed + boiler) and electric "NA"-type boilers → "Gas CPSU" (120, mains gas). The live classifier now applies ``main_heating_guard`` deterministically (so new intakes are correct); this fixes the rows written before it. The **same guard** @@ -11,9 +13,9 @@ decides the correction here, so the backfill and the live path cannot drift. Updates the TEXT ``property_overrides.override_value`` (what the modelling reads — the immediate fix) unconditionally. The ``landlord_main_heating_system_overrides`` -``value`` classifier cache is a ``main_heating_system`` **pgEnum**; the new HHRSH -archetype is an FE-owned value, so its cache write is **deferred** until the -Drizzle migration adds it (the Class-A/B pattern). +``value`` classifier cache is a ``main_heating_system`` **pgEnum**; the new +archetypes are FE-owned values, so their cache writes are **deferred** until the +Drizzle migration adds them (the Class-A/B pattern). DRY-RUN BY DEFAULT: prints the counts it would change and writes nothing. Pass ``--apply`` to execute inside a transaction. Idempotent — only rows whose stored @@ -31,13 +33,14 @@ from domain.epc.property_overrides.main_heating_guard import main_heating_guard from scripts.e2e_common import build_engine, load_env -def hhrsh_storage_corrections( +def main_heating_corrections( stored: Iterable[tuple[str, str]], ) -> dict[str, str]: - """``(description, stored override_value)`` → the HHRSH archetype value, for the - descriptions the main-heating guard resolves to HHRSH whose stored value is not - already it. Descriptions the guard defers and rows already on the target are - omitted, so re-running against corrected data is a no-op.""" + """``(description, stored override_value)`` → the faithful archetype value, for + the descriptions the main-heating guard resolves (HHRSH, solid-fuel room + heaters, electric boiler) whose stored value differs. Descriptions the guard + defers and rows already on the target are omitted, so re-running against + corrected data is a no-op.""" corrections: dict[str, str] = {} for description, value in stored: member = main_heating_guard(description) @@ -86,15 +89,15 @@ _ENUM_VALUES = text( def reclassify(conn: Connection, *, apply: bool) -> tuple[int, set[str]]: - """Re-map old-storage HHRSH overrides onto the high-heat-retention archetype. - Returns the number of ``property_overrides`` rows found and the set of target - values the live ``main_heating_system`` pgEnum does not yet carry (cache- - deferred until the FE migration).""" + """Re-map main-heating overrides off their garbage-drawer archetypes onto the + faithful ones the guard resolves. Returns the number of ``property_overrides`` + rows found and the set of target values the live ``main_heating_system`` pgEnum + does not yet carry (cache-deferred until the FE migration).""" stored = [(r.description, r.value) for r in conn.execute(_DISTINCT)] enum_values = {r[0] for r in conn.execute(_ENUM_VALUES)} total = 0 deferred: set[str] = set() - for description, new_value in hhrsh_storage_corrections(stored).items(): + for description, new_value in main_heating_corrections(stored).items(): params = {"description": description, "new_value": new_value} total += conn.execute(_OVERRIDES_COUNT, params).scalar() or 0 in_enum = new_value in enum_values @@ -124,9 +127,10 @@ def main() -> None: verb = "re-classified" if args.apply else "would re-classify" print( - f"{verb} {total} high-heat-retention storage override row(s) off " - "'Electric storage heaters, old' onto the HHRSH archetype (SAP 409) — " - "property_overrides / TEXT, what the modelling reads." + f"{verb} {total} main-heating override row(s) off their garbage-drawer " + "archetypes (old storage / Gas CPSU) onto faithful HHRSH / solid-fuel / " + "electric-boiler archetypes — property_overrides / TEXT, what the " + "modelling reads." ) if deferred: print( diff --git a/tests/scripts/test_reclassify_hhrsh_storage.py b/tests/scripts/test_reclassify_hhrsh_storage.py deleted file mode 100644 index c3b025c53..000000000 --- a/tests/scripts/test_reclassify_hhrsh_storage.py +++ /dev/null @@ -1,70 +0,0 @@ -"""The HHRSH reclassify maps the "old storage" dumping ground onto the high-heat- -retention archetype, leaves other storage subtypes alone, and is idempotent -(#1376 / ADR-0044).""" - -from __future__ import annotations - -from domain.epc.property_overlays.main_heating_system_overlay import ( - main_heating_overlay_for, -) -from domain.epc.property_overrides.main_heating_guard import main_heating_guard -from domain.epc.property_overrides.main_heating_system_type import ( - MainHeatingSystemType, -) -from scripts.reclassify_hhrsh_storage import hhrsh_storage_corrections - - -def test_hhrsh_rows_are_remapped_off_old_storage() -> None: - # Arrange — HHRSH funnelled into old storage (the bug), plus a genuinely-old - # storage row and a gas row that must be left untouched. - stored = [ - ( - "electric storage systems: high heat retention storage heaters", - "Electric storage heaters, old", - ), - ( - "electric storage systems: old (large volume) storage heaters", - "Electric storage heaters, old", - ), - ("gas boiler", "Gas boiler, regular"), - ] - - # Act - corrections = hhrsh_storage_corrections(stored) - - # Assert — only the HHRSH row moves, to its own archetype. - assert corrections == { - "electric storage systems: high heat retention storage heaters": ( - "Electric storage heaters, high heat retention" - ) - } - - -def test_every_guard_target_round_trips_to_a_resolvable_archetype() -> None: - # The HHRSH archetype the guard emits must decode to a real SAP heating code — - # a guard/overlay drift can't silently write an unmodellable value (ADR-0044). - member = main_heating_guard( - "Electric Storage Systems: High heat retention storage heaters" - ) - - assert member is not None - assert member in MainHeatingSystemType - simulation = main_heating_overlay_for(member.value, 0) - assert simulation is not None - assert simulation.heating is not None - assert simulation.heating.sap_main_heating_code is not None - - -def test_already_corrected_rows_need_no_change() -> None: - # Act / Assert — idempotent. - assert ( - hhrsh_storage_corrections( - [ - ( - "electric storage systems: high heat retention storage heaters", - "Electric storage heaters, high heat retention", - ) - ] - ) - == {} - ) diff --git a/tests/scripts/test_reclassify_main_heating.py b/tests/scripts/test_reclassify_main_heating.py new file mode 100644 index 000000000..12ce4254d --- /dev/null +++ b/tests/scripts/test_reclassify_main_heating.py @@ -0,0 +1,101 @@ +"""The main-heating reclassify maps the garbage-drawer archetypes (old storage, +Gas CPSU) onto the faithful ones the guard resolves, leaves correctly-classified +rows alone, and is idempotent (#1376 / ADR-0044, ADR-0045).""" + +from __future__ import annotations + +from domain.epc.property_overlays.main_heating_system_overlay import ( + main_heating_overlay_for, +) +from domain.epc.property_overrides.main_heating_guard import main_heating_guard +from domain.epc.property_overrides.main_heating_system_type import ( + MainHeatingSystemType, +) +from scripts.reclassify_main_heating import main_heating_corrections + + +def test_garbage_drawer_rows_are_remapped_to_faithful_archetypes() -> None: + # Arrange — HHRSH funnelled into old storage, and the Gas CPSU dumping ground + # (solid-fuel room heaters + electric "NA" boiler); plus correctly-classified + # rows (genuine old storage, a gas combi) that must be left untouched. + stored = [ + ( + "electric storage systems: high heat retention storage heaters", + "Electric storage heaters, old", + ), + ("solid fuel room heaters: open fire in grate", "Gas CPSU"), + ( + "solid fuel room heaters: open fire with back boiler (no radiators)", + "Gas CPSU", + ), + ( + "solid fuel room heaters: closed room heater with boiler (no radiators)", + "Gas CPSU", + ), + ("boiler: a rated na", "Gas CPSU"), + # Correctly classified — the guard does not claim these. + ( + "electric storage systems: old (large volume) storage heaters", + "Electric storage heaters, old", + ), + ("boiler: a rated combi", "Gas boiler, combi"), + ] + + # Act + corrections = main_heating_corrections(stored) + + # Assert — every mis-classified row moves to its faithful archetype. + assert corrections == { + "electric storage systems: high heat retention storage heaters": ( + "Electric storage heaters, high heat retention" + ), + "solid fuel room heaters: open fire in grate": ( + "Solid fuel room heater, open fire" + ), + "solid fuel room heaters: open fire with back boiler (no radiators)": ( + "Solid fuel room heater, open fire with back boiler" + ), + "solid fuel room heaters: closed room heater with boiler (no radiators)": ( + "Solid fuel room heater, closed with boiler" + ), + "boiler: a rated na": "Electric boiler", + } + + +def test_every_guard_target_round_trips_to_a_resolvable_archetype() -> None: + # Every archetype the guard emits for the rescued descriptions must decode to a + # real SAP heating code — a guard/overlay drift can't silently write an + # unmodellable value (ADR-0044, ADR-0045). + descriptions = [ + "Electric Storage Systems: High heat retention storage heaters", + "Solid fuel room heaters: Open fire in grate", + "Solid fuel room heaters: Open fire with back boiler (no radiators)", + "Solid fuel room heaters: Closed room heater with boiler (no radiators)", + "Boiler: A rated NA", + ] + + for description in descriptions: + member = main_heating_guard(description) + + assert member is not None, description + assert member in MainHeatingSystemType, description + simulation = main_heating_overlay_for(member.value, 0) + assert simulation is not None, description + assert simulation.heating is not None, description + assert simulation.heating.sap_main_heating_code is not None, description + + +def test_already_corrected_rows_need_no_change() -> None: + # Act / Assert — idempotent. + assert ( + main_heating_corrections( + [ + ( + "electric storage systems: high heat retention storage heaters", + "Electric storage heaters, high heat retention", + ), + ("boiler: a rated na", "Electric boiler"), + ] + ) + == {} + )