Commit graph

786 commits

Author SHA1 Message Date
Daniel Roth
576fc948a6 Gate community-heated dwellings from ASHP recommendation 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:17:41 +00:00
Daniel Roth
afe6638b36 Gate community-heated dwellings from HHRSH recommendation 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 14:13:57 +00:00
Jun-te Kim
460d687b58
Merge pull request #1389 from Hestia-Homes/fix/rdsap-mapper-main-heating-controls
Normalize RdSAP main_heating_controls mapping on the degraded 17.0 path
2026-07-01 14:58:20 +01:00
Daniel Roth
221e310fd8
Merge pull request #1382 from Hestia-Homes/incorrect-overrides
Fix HHRSH eligibility for on-gas-street electric dwellings; rename mains_gas → gas_connection_available
2026-07-01 14:53:14 +01:00
Jun-te Kim
366adaf8c8
Merge pull request #1387 from Hestia-Homes/fix/rdsap-mapper-lighting-parity
Normalize RdSAP lighting mapping on the degraded 17.0/19.0 paths
2026-07-01 13:48:20 +01:00
Jun-te Kim
04c29cc02a Normalize RdSAP main_heating_controls mapping on the degraded 17.0 path
PRD #1385, heating-controls family. The 17.0 API mapper omitted
main_heating_controls entirely, leaving the domain field None while the
19.0/20.0/21.x paths map the first lodged control via _map_energy_element. 17.0
lodges main_heating_controls (List[EnergyElement]) in all 23 local fixtures.

The mapped element is the same EnergyElement type 17.0 already carries for
main_heating / window, so this is straight parity (not new logic). Guarded on a
non-empty list -> None, matching the siblings.

Verification: new test 2 passed; test_mapper_corpus 6002 passed; recommendation
+ heating-control tests 85 passed (the domain EnergyElement field is distinct
from the subscriptable dict HeatingControlRecommender reads via the Property
wrapper, so no recommender destabilisation); SAP-accuracy regressions pass;
pyright unchanged (39 -> 39, pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:44:02 +00:00
KhalimCK
9db8b60a54
Merge pull request #1384 from Hestia-Homes/fix/1376-glazing-proportion-reconcile
Defer aggregate-mix glazing overrides to the cert's per-window glazing (#1376)
2026-07-01 13:37:28 +01:00
Jun-te Kim
a3bdcccfb1 Normalize RdSAP lighting mapping on the degraded 17.0/19.0 paths
PRD #1385, lighting family. The 17.0 and 19.0 API mappers hardcoded every
fixed-lighting bulb count to 0, dropping the lodged lighting mix that the
reference-complete 17.1/18.0/20.0 paths read. The schema lodges total +
low-energy OUTLET counts (ADR-0028); mirror the reference split — low-energy
outlets → low_energy bulbs, the remainder → incandescent — so the SAP
lighting-energy calc sees the real low-energy credit instead of assuming none.

led/cfl stay 0 (reduced-field certs don't split those); 21.0.x use a different
bulb-based lighting schema and are out of scope. The `total - low_energy`
subtraction is safe: all 37 local 17.0/19.0 fixtures have low_energy <= total
(no negative incandescent), matching the reference paths that already do this
unclamped.

Verification: new test 2 passed; test_mapper_corpus 6002 passed; SAP-accuracy /
RealCertExpectation regressions 56 passed (no pinned shifts); pyright unchanged
(39 -> 39, pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:30:17 +00:00
Daniel Roth
90abcd9f54 Merge branch 'main' into incorrect-overrides 2026-07-01 12:30:15 +00:00
Jun-te Kim
cf4c56df2e
Merge pull request #1386 from Hestia-Homes/fix/rdsap-mapper-ventilation-parity
Normalize RdSAP ventilation mapping on the degraded 17.0/19.0 paths
2026-07-01 13:26:25 +01:00
Jun-te Kim
e05f873282 Normalize RdSAP ventilation mapping on the degraded 17.0/19.0 paths
PRD #1385, ventilation family. The 17.0 and 19.0 API mappers dropped lodged
ventilation fields that the reference-complete 17.1/18.0/20.0 paths read:

  * open_fireplaces_count -> open_chimneys_count: hardcoded 0 on both
    (understating §2 infiltration).
  * percent_draughtproofed: omitted on both.
  * 17.0 additionally passed no sap_ventilation block, so the §2 cascade fell
    back to NATURAL + its default sheltered_sides=2. Restored from the lodged
    mechanical_ventilation + built_form via the existing _api_* helpers.

Semantic parity verified (not just type-equality): built_form and
mechanical_ventilation use the same gov code lists the 17.1/18.0/19.0/20.0/21.x
paths already map through these helpers; all 244 RdSAP fixtures resolve with zero
UnmappedApiCode, and the strict-coverage guard raises on any future divergence.
For the current corpus the 17.0 score-mover is sheltered_sides from built_form
(every 17.0 cert lodges mechanical_ventilation=0 -> NATURAL).

Adds a regression test asserting both paths carry the fields through
from_api_response on real certs.

Verification: new test 2 passed; test_mapper_corpus 6002 passed; SAP-accuracy /
RealCertExpectation regressions pass; pyright unchanged (39 -> 39, pre-existing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 12:17:54 +00:00
Jun-te Kim
12529a3f23 Merge remote-tracking branch 'origin/main' into fix/rdsap-17-0-sap-heating-drops
# Conflicts:
#	datatypes/epc/domain/mapper.py
2026-07-01 12:16:29 +00:00
Daniel Roth
d65ae00a72 Fix integration test: LPG solid-brick dwelling now correctly offered HHRSH (#1378)
The solid-brick LPG fixture (main_fuel_type=27, gas_connection_available=True)
was previously excluded from HHRSH by the mains_gas bug. After the fix it is
correctly offered HHRSH, so the integration test's product catalogue and
assertion are updated to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 11:14:36 +00:00
Khalim Conn-Kowlessar
0ebff1bcc2 Re-classify aggregate-mix glazing overrides flattened to Double onto MIXED 🟩
One-time script (dry-run default, --apply in a transaction, idempotent) reusing the
live glazing_mix_guard so the backfill and classifier cannot drift. Maps every
genuine percentage-mix override (neither type >= 90%) off a flattened single type
onto MIXED (no overlay -> the cert's per-window glazing is kept). property_overrides
TEXT updated now; the glazing pgEnum cache write for 'Mixed glazing' is deferred
until the FE adds the member. Dry-run against the audited DB reports 647 rows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:09:42 +00:00
Khalim Conn-Kowlessar
f11fc9bc04 Resolve a MIXED glazing override to no overlay, keeping the cert per-window glazing 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:06:50 +00:00
Jun-te Kim
003defcf55 Add find-weird-recommendations skill + its two detectors
A focused sibling to audit-ara-portfolio: that skill audits baselines/plans/SAP;
this one audits the *recommendations themselves* — why a measure was or wasn't
offered. Motivated by the portfolio-814 review (Khalim's HHRSH-on-community-
heating, missing-HHRSH, missing-secondary-heating-removal, and a neighbour split).

Adds:
- .claude/skills/find-weird-recommendations/SKILL.md — scan -> neighbour scan ->
  live re-model deep-dive -> root-cause -> codify, with a seeded known-bug
  catalogue and the query-safety rules inherited from audit-ara-portfolio.
- scripts/audit/anomalies.py: new `plan-stops-short-of-goal` HIGH check — the
  default plan ends below the goal band on an unlimited-budget scenario (the
  deterministic worklist for "why didn't this get recommended X"). Adds
  scenario_budget to the bundle/query so budget-capped scenarios are excluded.
- scripts/audit/neighbour_divergence.py: groups a portfolio by (postcode,
  property_type, built_form) and flags effective-SAP outliers vs the cohort
  median. Never touches the 26m-row recommendation table, so it is safe
  portfolio-wide.
- Tests for both (12 passing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:06:03 +00:00
Khalim Conn-Kowlessar
4e4a2d21c4 Leave a uniform or unparseable glazing description to the LLM classifier 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:05:10 +00:00
Khalim Conn-Kowlessar
f959cc14c9 Classify an aggregate-mix glazing description as MIXED 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:03:28 +00:00
Jun-te Kim
cd798d3513 Fix RdSAP-17.0 mapper dropping the rest of the lodged sap_heating block
`from_rdsap_schema_17_0` was an older copy that hardcoded a cluster of
lodged `sap_heating` fields to None while every other API path
(19.0/20.0/…) reads them from `schema.sap_heating`. PR #1380 restored the
secondary-heating pair; this restores the rest of the same block, all of
which `RdSapSchema17_0` lodges:

  MainHeatingDetail: boiler_flue_type, fan_flue_present,
    central_heating_pump_age, main_heating_index_number (PCDB efficiency index)
  SapHeating: cylinder_thermostat, cylinder_insulation_thickness

Impact — cylinder_thermostat (12/23 local 17.0 fixtures) read as "no
thermostat", so the water-heating worksheet over-applied its ×1.3 penalty
and `recommend_cylinder_thermostat` could fire on a dwelling that already
has one; cylinder_insulation_thickness (13/23) discarded the measured value
so the cylinder-loss cascade fell back to age-band defaults;
main_heating_index_number lost the PCDB efficiency lookup.

Mirrors the 19.0 path exactly. secondary_*_type left untouched so this PR
stays independent of #1380 (no overlapping lines). Adds a regression test
asserting the whole block survives `from_api_response` on a real 17.0 cert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 11:02:32 +00:00
Jun-te Kim
6d0dfe5860
Merge pull request #1380 from Hestia-Homes/fix/rdsap-17-0-secondary-heating
Fix RdSAP-17.0 mapper dropping lodged secondary heating
2026-07-01 12:00:39 +01:00
Daniel Roth
9b58bebfb8 Rename SapEnergySource.mains_gas to gas_connection_available 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 10:59:21 +00:00
KhalimCK
9968e2b9df
Merge pull request #1379 from Hestia-Homes/fix/1376-flat-roof-insulation-thickness
Score flat roofs by their known insulation depth, not the age-band default (#1376)
2026-07-01 11:58:46 +01:00
Daniel Roth
a174301136 rename mains_gas to gas_connection_available 2026-07-01 10:50:58 +00:00
Jun-te Kim
3282cef04e Fix RdSAP-17.0 mapper dropping lodged secondary heating
The `from_rdsap_schema_17_0` path hardcoded `sap_heating.secondary_heating_type`
and `secondary_fuel_type` to None, unlike every other API path (19.0/20.0/…),
which read them from `schema.sap_heating`. The gov EPC API *does* populate a
secondary heating SAP code for a genuine fixed secondary (e.g. 691 "electric
room heaters", 694), so the hardcode silently discarded it.

Because `recommend_secondary_heating_removal` gates purely on
`sap_heating.secondary_heating_type`, any 17.0 cert carrying a fixed secondary
would never get its removal recommendation. Surfaced while auditing portfolio
814's recommendations; 814 has 17.0 certs but none with a fixed secondary, so
the defect was latent there — this restores parity with the other paths so it
cannot bite.

Mirrors the 19.0 path exactly (secondary_fuel_type via `_api_secondary_fuel_type`).
Adds a regression guard that injects a fixed secondary into a real 17.0 cert and
asserts the code survives the mapping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:41:52 +00:00
Daniel Roth
5d99956133 On-gas-street electric dwelling (API fuel code 29) is offered HHRSH 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 10:36:52 +00:00
Khalim Conn-Kowlessar
32e296ca3f Re-classify flat-roof overrides with a known depth onto their thickness member 🟩
One-time script (dry-run default, --apply in a transaction, idempotent) mapping a
flat roof's stated depth (flat: Nmm) onto the nearest Table 16 rung member so the
overlay scores it by thickness, not the age-band default. Updates property_overrides
(TEXT) immediately; the roof_type pgEnum cache writes are deferred until the FE adds
the FLAT_*MM members. Dry-run against the audited DB reports 102 rows, deferring
Flat 50/100/150 mm.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:19:09 +00:00
Khalim Conn-Kowlessar
acf05584d6 Add flat-roof insulation-thickness taxonomy members lock-step with the overlay 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:16:35 +00:00
Khalim Conn-Kowlessar
736b5877e9 Carry a flat roof's known insulation depth into the overlay 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 10:13:31 +00:00
Daniel Roth
266a276dae revert changes 2026-07-01 09:52:06 +00:00
Khalim Conn-Kowlessar
9af90aa718 Re-classify party-ceiling roof overrides mis-read as external roofs 🟩
One-time script (dry-run default, --apply in a transaction, idempotent) that maps
every party-ceiling override (another/same dwelling or premises above) currently
stored as an external roof value onto the party-ceiling member, reusing the live
roof_party_ceiling_guard so the backfill and the classifier cannot drift. Updates
property_overrides (TEXT, the modelling read path) and the roof classifier cache;
dry-run against the audited DB reports 106 rows. No FE migration — the party-
ceiling values already exist in the roof pgEnum.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:46:01 +00:00
Daniel Roth
0192dd8efd HHRSH not offered to solid-biomass dwelling with mains_gas=True despite off-gas heating fuel 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 09:41:09 +00:00
Khalim Conn-Kowlessar
4396e5fb22 Let a deterministic guard override the fallback classifier per description 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:39:16 +00:00
Khalim Conn-Kowlessar
fe663deef5 Leave a genuine (non-party-ceiling) roof description to the LLM classifier 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:38:03 +00:00
Khalim Conn-Kowlessar
7f00ee69e1 Resolve every party-ceiling marker variant to its member 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:37:21 +00:00
Khalim Conn-Kowlessar
6fc10683d2 Resolve a party-ceiling roof marker to the party-ceiling member 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:35:24 +00:00
Daniel Roth
a8821c9cdb
Merge pull request #1370 from Hestia-Homes/feature/ventilation-audit-changes
Ventilation audit changes: directly populate Total Effective Area for trickle vents from form data, rather than calculating it
2026-07-01 09:20:54 +01:00
Daniel Roth
c73846b448 Merge branch 'main' into audit/bad-lodged-source-data 2026-07-01 07:49:21 +00:00
Daniel Roth
0c701ba3de
Merge pull request #1373 from Hestia-Homes/fix/1361-class-b-predicted-null-lodged
Drop the phantom Lodged Performance on predicted Properties (#1361 Class B)
2026-07-01 08:35:58 +01:00
Khalim Conn-Kowlessar
7e06aa63c6 Backfill the phantom Lodged Performance on predicted Properties to NULL 🟩
One-time script (dry-run default, --apply in a transaction, idempotent) that
NULLs the four lodged_* columns on every predicted-source baseline row — a
predicted EPC exists and no lodged one does — leaving Effective, the bill block,
and rebaseline_reason intact. Dry-run against the audited DB reports 12,236 rows.
Must run after the FE-owned Drizzle ALTER ... DROP NOT NULL lands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:17:57 +00:00
Khalim Conn-Kowlessar
b9bec18f44 Round-trip a null Lodged Performance through persistence 🟩
The four lodged_* columns are nullable as a unit; from_domain writes them all
NULL when a predicted Property has no Lodged Performance, and to_domain
reconstructs lodged=None (lodged_sap_score is the read discriminator, mirroring
the bill block). The production lodged_* columns are FE-owned (Drizzle) and need
a companion ALTER ... DROP NOT NULL migration before the backend writes NULL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:13:04 +00:00
Khalim Conn-Kowlessar
da805bce21 Persist no Lodged Performance for a predicted Property 🟩
The PropertyBaselineOrchestrator now reads no Lodged Performance off a predicted
Property's neighbour-synthesised EPC: lodged is None when source_path is
'predicted', so no phantom lodged figure is manufactured. The Effective half is
unchanged. Rebaseliner port + PropertyBaselinePerformance.lodged widen to
Optional; the pristine-cert paths assert non-None (lodged is None only for a
predicted Property, which is always physical_state_changed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:10:57 +00:00
Khalim Conn-Kowlessar
5d63118d5a Persist no Lodged Performance for a predicted Property 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 22:07:27 +00:00
Khalim Conn-Kowlessar
585c8b1501 One-time script to re-classify stale heating overrides
Deterministic catalogue remap (no LLM) that re-resolves the stored archetype for
the canonical RdSAP heating descriptions the under-populated taxonomy
mis-classified. Dry-run by default; --apply writes inside a transaction;
idempotent. Updates property_overrides.override_value (TEXT — what the modelling
reads, the actual band-G fix: 3,028 rows incl. 2,210 community boilers + 770
panel/convector room heaters). Cache (the Drizzle-owned main_heating_system
pgEnum) updates are deferred per-value until the FE-repo enum migration adds the
new archetypes — surfaced explicitly by the script.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:30:59 +00:00
Khalim Conn-Kowlessar
2fab2ad82e Recognise community CHP and boilers as a heat-network system 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 20:15:38 +00:00
Khalim Conn-Kowlessar
c2a842e9b9 Drag a gas room heater's coherent companions 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:04:30 +00:00
Khalim Conn-Kowlessar
60e27c80a1 Recognise gas room-heater variants by their SAP codes 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 18:03:07 +00:00
Khalim Conn-Kowlessar
34302adce5 Drag an oil room heater's coherent companions 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:55:07 +00:00
Khalim Conn-Kowlessar
fe97a7fc1c Recognise a modern oil room heater as its own archetype 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:53:54 +00:00
Khalim Conn-Kowlessar
098427e620 Drag heat-network category and a community gas fuel for community boilers 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:38:08 +00:00
Khalim Conn-Kowlessar
c7b2fe7a1b Recognise community boilers as a heat-network system 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:36:41 +00:00