Commit graph

7432 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
244c5b65ed Document heating-companion coherence: category/control forced, fuel/meter deferred (ADR-0048)
ADR-0048 records why a system-replacing heating override must emit a
complete companion set — category and control archetype-forced for
coherence, fuel and meter deferred where multiple values are valid, and
an unmapped companion logged rather than silently inherited. Adds the
Heating Companion Set glossary term to CONTEXT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:21:47 +00:00
Khalim Conn-Kowlessar
962a443ed2 A complete room-heater overlay does not log a companion gap 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:17:06 +00:00
Khalim Conn-Kowlessar
390e3e89d7 The overlay logs an error when it cannot complete a heating companion set 🟩
A system-replacing override whose category or control is not yet mapped
now logs an error naming the archetype and code, so the incomplete
companion set (which otherwise silently inherits the replaced system's
value) is visible for review — log-and-continue, matching flag_fuel_mismatch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:16:25 +00:00
Khalim Conn-Kowlessar
ea113b58fb The overlay logs an error when it cannot complete a heating companion set 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:15:31 +00:00
Khalim Conn-Kowlessar
ffd21689fd An electric room heaters override sets the room-heater charge control 🟩
Control 2601 (SAP Table 4e Group 6 room-heater, +0.3 C conservative
default) now overlaid for electric room heaters, matching the gas/oil/
solid siblings, so a replaced storage heater's manual charge control
2401 (+0.7 C) no longer survives and over-warms the modelled dwelling.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:13:32 +00:00
Khalim Conn-Kowlessar
cfd9799500 An electric room heaters override sets the room-heater charge control 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:13:03 +00:00
Khalim Conn-Kowlessar
bf032d17d0 An electric room heaters override sets the room-heater heating category 🟩
Category 10 (SAP Table 4a Room heaters) now overlaid for electric room
heaters (691-701), so a replaced storage heater's category 7 no longer
survives onto the effective cert and mis-bills via the Table 12a resolver.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:12:19 +00:00
Khalim Conn-Kowlessar
5e7e51d8b2 An electric room heaters override sets the room-heater heating category 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:11:38 +00:00
Jun-te Kim
cf50c53cad
Merge pull request #1431 from Hestia-Homes/fix/refetch-solar-reuse-stored
Fix refetch_solar=False dropping solar from re-modelled plans (reuse stored Solar instead)
2026-07-02 15:27:44 +01:00
Jun-te Kim
100949073f refetch_solar=False models with stored Solar insights and never calls Google 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:21:46 +00:00
Jun-te Kim
c1a558d2b5 refetch_solar=False models with stored Solar insights and never calls Google 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 14:19:31 +00:00
Jun-te Kim
e08048244a
Merge pull request #1429 from Hestia-Homes/fix/property-overlay-storage-category
Set electric-storage category (7) on storage property overlays
2026-07-02 12:51:58 +01:00
Jun-te Kim
12e8e52eaa
Merge pull request #1428 from Hestia-Homes/test/booth-close-sap-accuracy-corpus
Add UPRN 100020942571 (8 Booth Close) to the SAP accuracy corpus
2026-07-02 12:51:38 +01:00
Jun-te Kim
4ca3491eb2
Merge pull request #1426 from Hestia-Homes/fix/hhrsh-storage-offpeak-pricing
Fix HHRSH storage heaters priced at peak rate (engine 48 vs accredited Elmhurst 71)
2026-07-02 12:50:32 +01:00
KhalimCK
61321d0d47
Merge pull request #1422 from Hestia-Homes/fix/roof-vaulted-nd-sentinel-eligibility
Roof eligibility resolves sentinel thickness by age band (vaulted joins sloping)
2026-07-02 12:45:51 +01:00
Jun-te Kim
e27ecf4a26 Set electric-storage category (7) on storage property overlays
`_category_for` mapped room heaters / heat pumps / heat networks but
returned None for electric-storage-heater codes (401-409), so a storage
system override emitted `sap_main_heating_code=409` +
`main_heating_category=None` while `_control_for` right beside it correctly
set the storage charge control — an asymmetry. Map storage codes to
category 7 so the overlaid cert is internally consistent (code 401-409 <->
category 7), matching the control/fuel/meter companions the overlay drags.

Consistency follow-up to the modelling HHRSH off-peak-pricing fix (#1426):
that PR's Table 12a resolver already prices any storage-coded system
off-peak regardless of category, so this is NOT a live mispricing — it
removes a latent code<->category inconsistency that other category-keyed
logic (responsiveness, pumps/fans) could trip on.

Tests: storage/HHRSH overlay tests now also assert `main_heating_category==7`.
tests/domain/epc (347) + prediction/override/reclassify (118) green; source
pyright-clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:45:49 +00:00
Jun-te Kim
e79e83bbbe Add UPRN 100020942571 (8 Booth Close) to the SAP accuracy corpus
Pins the engine's baseline SAP (54) for this electric-room-heater +
gas-multipoint end-terrace ground-floor maisonette (portfolio 814 pid
742121) against accredited Elmhurst RdSAP-10 (worksheet SAP 53, within
1 point), with the Elmhurst Input Summary + SAP Worksheet PDFs saved as
evidence alongside the cert's epc.json.

Companion to the HHRSH storage-heater off-peak-pricing fix PR: this
baseline pin confirms the calculator is faithful on the as-lodged
dwelling; that PR fixes the HHRSH recommendation overlay (engine 48->73
vs Elmhurst 71).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:31:28 +00:00
Jun-te Kim
f5fbeb8e7e Fix HHRSH storage heaters priced at peak rate (engine 48 vs Elmhurst 71)
The high-heat-retention storage-heater (HHRSH) recommendation was computed
as LOWERING SAP and wrongly rejected by the Optimiser, leaving electric-
room-heater flats/maisonettes stuck at band E with no sensible plan.

Root cause: `_HHR_STORAGE_OVERLAY` sets `sap_main_heating_code=409` (HHR
storage) + `meter_type="Dual"` but left `main_heating_category` at the
pre-existing value (10 = electric room heaters). The SAP 10.2 Table 12a
space-heating high-rate-fraction resolver classifies by category, so it
misread the off-peak storage heating as direct-acting electric (1.00
high-rate) and priced 100% at the 7-hour peak rate (15.29p) instead of the
storage off-peak split (0.00, ~5.5p).

Verified against accredited Elmhurst RdSAP-10 on UPRN 100020942571
(8 Booth Close, SE28 8BW): engine HHRSH SAP 48 (peak-priced, cost £1087)
vs Elmhurst 71 (cost ~£611); with the fix the engine gives 73 / £576.

Two-part fix (defence in depth, so no future overlay/mapper can
reintroduce it):
- `_HHR_STORAGE_OVERLAY` now sets `main_heating_category=7`, keeping the
  overlaid EPC internally consistent.
- `_table_12a_system_for_main` now classifies electric storage heaters by
  their Table 4a SAP code (401-409) as well as `category==7`, checked
  before the room-heater branch, so a specific storage code is
  authoritative over a stale/inconsistent category.

Tests: new regression `test_storage_heater_classified_by_sap_code_over_stale_category`;
updated the HHRSH overlay pin. Full accuracy corpus still passes (no regressions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-02 11:24:03 +00:00
Jun-te Kim
ca3d4ce84b
Merge pull request #1423 from Hestia-Homes/feat/glazing-secondary-member
Add SECONDARY glazing: GlazingType member, overlay code 5, guard support + 796 backfill
2026-07-02 12:22:58 +01:00
Khalim Conn-Kowlessar
6d812d99c5 An unlodged roof type with ND thickness gets no roof recommendation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:20:55 +00:00
Khalim Conn-Kowlessar
0efae77fd2 An unlodged roof type with ND thickness gets no roof recommendation 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:18:55 +00:00
Jun-te Kim
7baab9376c Backfill uniform secondary-glazing overrides misfiled as Single glazing
39 Hyde-796 property_overrides rows ('100% secondary glazing (sap 9.94)')
plus the classifier-cache row; the cache phase self-gates on the
'Secondary glazing' pgEnum label landing (assessment-model#345).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:12:25 +00:00
Jun-te Kim
248e230d37 Dominant secondary-glazing split resolves deterministically to SECONDARY 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:10:40 +00:00
Jun-te Kim
787f0066c8 Dominant secondary-glazing split resolves deterministically to SECONDARY 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:09:47 +00:00
Khalim Conn-Kowlessar
63c889bd02 Roof dispatcher docstring and constants reflect the sentinel-aware pitched trigger 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:09:07 +00:00
Jun-te Kim
f873d5cad3 Landlord 'Secondary glazing' override overlays SAP10 glazing code 5 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:08:16 +00:00
Khalim Conn-Kowlessar
1d13dee38e ND loft roof on a pre-1950 dwelling gets loft insulation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:05:47 +00:00
Khalim Conn-Kowlessar
989cd61574 ND loft roof on a pre-1950 dwelling gets loft insulation 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:04:40 +00:00
Khalim Conn-Kowlessar
b33735ef30 NI (insulation present) and measured depths keep a vaulted roof ineligible 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:03:38 +00:00
Khalim Conn-Kowlessar
1dd2d19f78 ND on a limited-insulation age band yields no roof recommendation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:03:01 +00:00
Jun-te Kim
5c9ca727cd Landlord 'Secondary glazing' override overlays SAP10 glazing code 5 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 11:02:11 +00:00
Khalim Conn-Kowlessar
f3bdf0e884 Vaulted ND roof on a pre-1950 dwelling gets sloping-ceiling insulation 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 11:01:43 +00:00
Khalim Conn-Kowlessar
0d465346c9 Vaulted ND roof on a pre-1950 dwelling gets sloping-ceiling insulation 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:55:58 +00:00
Khalim Conn-Kowlessar
9e1a194b27 Record ADR-0047 roof sentinel eligibility + lodgement-sentinel glossary entry
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:55:57 +00:00
Jun-te Kim
c1169b2d39
Merge pull request #1402 from Hestia-Homes/fix/glazing-dominant-single-guard
Resolve dominant-single glazing deterministically in the mix guard
2026-07-02 10:19:16 +01:00
Jun-te Kim
0f6e6e4936
Merge pull request #1415 from Hestia-Homes/fix/repin-golden-0036-tune-up-after-1399
Re-pin golden cert 0036 after #1399 (its PCDB boiler now fires a zoned tune-up)
2026-07-02 10:01:33 +01:00
Jun-te Kim
0fa47b1047
Merge pull request #1409 from Hestia-Homes/fix/property-type-leading-token-guard
Guard property_type to its leading dwelling-type token
2026-07-02 10:00:03 +01:00
Jun-te Kim
761dcf2a82
Merge pull request #1406 from Hestia-Homes/fix/wood-logs-main-fuel-archetype
Add individual wood-logs main-fuel archetype (API 6 / RdSAP Table 32 code 20)
2026-07-02 09:57:25 +01:00
Jun-te Kim
c1a75fb45b
Merge pull request #1403 from Hestia-Homes/fix/1376-gas-cpsu-dumping-ground
Retire the Gas CPSU dumping ground: solid-fuel room heaters + electric boilers (#1376)
2026-07-02 09:57:15 +01:00
Jun-te Kim
dc868e87a9
Merge pull request #1405 from Hestia-Homes/fix/1376-offpeak-underfloor
Score off-peak electric underfloor on its own codes, not direct-acting single-meter (#1376)
2026-07-02 09:56:57 +01:00
Jun-te Kim
b94c45a392
Merge pull request #1407 from Hestia-Homes/fix/glazing-nd-single-all-schemas
Honour single-glazed ND certs on every reduced-field synthesis seam
2026-07-02 09:21:40 +01:00
Jun-te Kim
deec22302a Resolve dominant double/triple glazing when the era is stated
Addresses PR #1402 review: the guard only claimed dominant-single, leaving
the symmetric bug open — the LLM can flatten a dominant-double/triple split
onto its minority single (e.g. "96% Double glazing 2002 or later, 4% Single").

A dominant double/triple whose era is explicit ("pre-2002" / "2002 or later")
is just as fully determined as era-free single, so the guard now claims it via
_DOMINANT_MEMBER. Only a genuinely ambiguous era ("unknown age", unstated) still
defers to the LLM — the "96% double -> None" contract now holds solely for the
era-unknown case, not the era-stated one.

Backfill script reuses the same guard, so it now corrects any dominant split;
renamed reclassify_dominant_single_glazing.py -> reclassify_dominant_glazing.py
to match. Tests cover double/triple x pre-2002/2002-or-later and the still-
deferred unknown-age case; 14 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 08:07:13 +00:00
KhalimCK
51f1cafb75
Merge pull request #1408 from Hestia-Homes/audit/plan-stuck-in-low-band
Add plan-stuck-in-low-band audit check + refresh weird-recs catalogue
2026-07-02 00:08:06 +01:00
Khalim Conn-Kowlessar
44752344a0 Re-pin golden cert 0036 to expect the zoned tune-up its PCDB boiler now fires (#1399)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:56:12 +00:00
Jun-te Kim
88bae2166f Guard property_type to its leading dwelling-type token
The landlord property-type description is a "<dwelling type>: <built form>: <floor>"
split whose leading token IS the dwelling type; the built-form tail is not. The
LLM occasionally over-read the tail and flipped the type — a handful of
"Bungalow: EndTerrace" / "Bungalow: MidTerrace" dwellings were stored as House.

Adds property_type_guard (claims the recognised leading token: House / Bungalow /
Flat / Maisonette / Park home; defers unrecognised phrasings to the LLM) and wires
property_type through a GuardedColumnClassifier, so the built-form tail can never
flip the type and the live path is deterministic.

Applied the scoped backfill to portfolio 796 (Hyde): 3 rows corrected from House
back to Bungalow. No enum migration needed — the targets are original members.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:24:37 +00:00
Jun-te Kim
66d70b6833 Add plan-stuck-in-low-band audit check + refresh weird-recs catalogue
`plan-stops-short-of-goal` fires on anything below the goal band — 322/338
on portfolio 814, mostly flats a single band short of B, which drowns the
real hits. Add a sharper, band-keyed check that isolates the genuinely-
stuck tail: a plan that leaves the dwelling still rated E/F/G with the band
UNCHANGED from baseline on an unlimited-budget scenario — the "E->E, no big
movement" worklist. On portfolio 814 / scenario 1271 it flags just 4 (pids
742121, 742210, 742265, 742347) vs 322. No SAP-points threshold; the
strictly-worse case is left to plan-below-baseline-band so the two
partition cleanly.

Also refresh the find-weird-recommendations skill's Phase 4 catalogue:
register the new check in Phase 1, correct the stale 742121 note (it is an
electric maisonette whose HHRSH IS offered but scores -6.3 SAP, so the
Optimiser correctly drops it — not the old mains-gas story), and add the
742265 community-flat findings (HHRSH/ASHP gated out by heat-network
topology; vaulted-ceiling roof with ND insulation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:22:41 +00:00
Jun-te Kim
0e9bceb2d4 Honour single-glazed ND certs on every reduced-field synthesis seam
A pre-SAP10 RdSAP cert that omits per-window records lodges glazing as a
`multiple_glazing_type` code plus a `window.description` and a
`multiple_glazed_proportion`. When the numeric code is the "ND" (Not
Defined) sentinel, the reduced-field synthesis defaulted to the DG-modal
code 2 (double) — ignoring the two register signals that say otherwise.

`_normalize_sap_schema_16_x` already honours an explicit "Single glazed"
description for 16.x certs (rewriting multiple_glazing_type -> 5), but that
rule was never propagated to the 17.0 / 17.1 / 18.0 / 19.0 / 20.0.0
synthesis seams — they all blindly applied ND -> 2. So a genuinely
single-glazed dwelling was:

  1. scored as double-glazed, overstating its baseline SAP, and
  2. hidden from the glazing generator (which upgrades only single-glazed
     windows), producing no glazing recommendation.

Reproduced on cert 2780-3922-3202-6042-9200 (uprn 10033526327), a
single-glazed community-heated flat lodging multiple_glazing_type="ND",
window "Single glazed", multiple_glazed_proportion 0 — mapped to four
double-glazed windows, no glazing measure offered.

Extend the 16.x rule to all five reduced-field seams via one shared
resolver: on the ND fallback, when the description says "single" or the
multiple-glazed proportion is 0, synthesise single glazing
(`_api_cascade_glazing_type(5) == 1`, SAP10 cascade single, Table 24
U~4.8) — the exact single code the glazing generator's {1, 15} set
matches — else keep each seam's DG-modal default. Refines ADR-0028's ND
handling.

Tests: all five seams synthesise single from a "Single glazed" + 0%
proportion cert; the double-glazed control keeps the DG-modal default;
plus unit pins on the resolver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:10:58 +00:00
Jun-te Kim
5330bc20d9 Add individual wood-logs main-fuel archetype (API 6 / RdSAP Table 32 code 20)
MainFuelType had no individual wood-logs member — only "biomass (community)" —
so the LLM classifier funnelled "Solid Fuel: Wood Logs" into the community fuel,
inventing a community heat network the dwelling isn't on (and mislabelling the
connection). main_fuel had no deterministic guard at all, so nothing caught it.

Verified against domain/sap10_calculator/docs/specs: RdSAP 10 Specification
Table 32 lists "wood logs" as a solid fuel (code 20, 0.028 kgCO2e/kWh); the
calculator's input scheme (the gov EPC API fuel enum) codes it 6 -> Table 32 20
(sap_efficiencies._API_TO_TABLE32), and water_heating_overlay already pins the
same fuel to 6. So _FUEL_CODES["wood logs"] = 6 is confirmed, not guessed.

Adds MainFuelType.WOOD_LOGS + the _FUEL_CODES entry, a main_fuel_guard mirroring
water_heating_guard (claims the "wood log" token; dual fuel keeps its own member
since it has no "wood log" substring), and wires main_fuel through a
GuardedColumnClassifier so the live path is deterministic.

Applied the scoped backfill to portfolio 796 (Hyde): 21 rows off
"biomass (community)" -> "wood logs". property_overrides (TEXT) only; the
classifier-cache pgEnum member is deferred to the FE Drizzle migration.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 17:00:24 +00:00
Khalim Conn-Kowlessar
f50ee5f957 Reclassify electric-underfloor overrides via the generalized main-heating reclassify 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:51:22 +00:00
Khalim Conn-Kowlessar
9d106d9d6d Deterministically guard the electric-underfloor descriptions 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:48:45 +00:00