Commit graph

201 commits

Author SHA1 Message Date
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
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
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
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
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
927384e5a9 Resolve electric CPSU from the property's fuel, keeping gas CPSU on 120 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:16:10 +00:00
Khalim Conn-Kowlessar
8b0176976d Resolve electric CPSU from the property's fuel, keeping gas CPSU on 120 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 16:12:00 +00:00
Jun-te Kim
646f66ff89 Resolve dominant-single glazing deterministically in the mix guard
A landlord glazing split like "4% Double glazing 2002 or later, 96% Single
Glazing" is a near-uniform *single*-glazing dwelling, but the LLM classifier
latched onto the 4% minority and wrote a "Double glazing…" override — over-
crediting the dwelling.

glazing_mix_guard rescued genuine mixes (no type >= 90%) to MIXED but returned
None for near-uniform splits, trusting the LLM to have applied the dominant type.
It hadn't. Single glazing is era-free, so a dominant-single split is fully
determined: the guard now returns SINGLE for it (dominant double/triple still
carry era ambiguity, so they stay the LLM's job — the existing "96% double" case
still defers).

Adds a portfolio-scoped backfill (reuses the same guard, so it can't drift from
the live path) and applied it to portfolio 796 (Hyde): 115 glazing overrides
corrected from a spurious Double back to Single.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:59:18 +00:00
Khalim Conn-Kowlessar
de7bfc5dfe Reclassify Gas CPSU dumping-ground overrides via the generalized main-heating reclassify 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 15:58:29 +00:00
Jun-te Kim
10e260be23
Merge pull request #1395 from Hestia-Homes/fix/neighbour-divergence-fuel-cohort
neighbour_divergence: key cohorts on main-heating fuel
2026-07-01 15:49:38 +01:00
Daniel Roth
0488696df8
Merge pull request #1397 from Hestia-Homes/fix/1376-hhrsh-baseline-archetype
Add the HHRSH baseline archetype so it stops scoring as old storage (#1376)
2026-07-01 15:41:52 +01:00
Khalim Conn-Kowlessar
2f577c3ebc Re-map old-storage HHRSH overrides onto the high-heat-retention archetype 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:34:49 +00:00
Khalim Conn-Kowlessar
04484edc67 Re-map old-storage HHRSH overrides onto the high-heat-retention archetype 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:33:24 +00:00
Jun-te Kim
009394bb19 neighbour_divergence: key cohorts on main-heating fuel
The neighbour SAP-divergence cohort was (postcode, property_type, built_form),
which mixed electric- and gas-heated dwellings. Electricity scores materially
lower in SAP than mains gas for identical fabric, so a mixed-fuel postcode
produced cross-fuel false outliers — an electric dwelling flagged only for being
electric among gas neighbours.

Add the main-heating fuel class to the cohort key (electricity variants — 29/30
+ off-peak 31-40 — collapse to one class; every other code is its own bucket).
Now every flagged divergence is a same-fuel comparison worth investigating. On
portfolio 814 this refined 20 raw divergences to 17 same-fuel ones while keeping
the genuine within-fuel outliers (e.g. the all-electric WC2B 4AW flats at SAP
26/38 vs a cohort median of 67).

Reaches the fuel via epc_main_heating_detail through the indexed property_id —
still never touches the recommendation table. Surfaced by the portfolio-814
recommendation audit (Work item B, #1388).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 14:24:31 +00:00
Daniel Roth
baaadcdf98
Merge pull request #1392 from Hestia-Homes/fix/1376-water-heating-house-coal
Reclassify water-heating overrides off the house-coal dumping ground (#1376)
2026-07-01 15:19:39 +01:00
Khalim Conn-Kowlessar
51abd15289 Re-map house-coal water-heating overrides onto faithful biomass and immersion archetypes 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:59:44 +00:00
Khalim Conn-Kowlessar
ffbef2d047 Re-map house-coal water-heating overrides onto faithful biomass and immersion archetypes 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 13:58:39 +00: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
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
Daniel Roth
90abcd9f54 Merge branch 'main' into incorrect-overrides 2026-07-01 12:30:15 +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
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
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
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
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
db27a6153f
Merge pull request #1374 from Hestia-Homes/audit/bad-lodged-source-data
fix(audit): Class C — silence effective-lodged-divergence for implausible lodged scores (#1361)
2026-07-01 09:15:45 +01:00
Daniel Roth
7f63702295
Merge pull request #1369 from Hestia-Homes/audit/ara-newest-plan-guardrails
Guard the Ara portfolio audit against the 26m-row recommendation seq-scan
2026-07-01 08:52:27 +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
Daniel Roth
db66b3b273
Merge pull request #1372 from Hestia-Homes/fix/1361-class-a-heating-classification-taxonomy
Fix PRD #1361 Class A: complete the landlord-heating classification taxonomy
2026-07-01 08:16:45 +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
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
Jun-te Kim
59508eb033 Add scripts/data_exports.py: principal-pitch export on the new DDD model
Replaces sfr/principal_pitch/2_export_data.py, which read the retired
plan_recommendations m2m and recommendation_materials table. The new model
links a recommendation to its plan directly (recommendation.plan_id), keeps
materials inline on the recommendation (material_id), marks the chosen plan
per (scenario, property) with is_default, and stores post-works SAP/EPC and
savings on the plan row (the new SAP calculator's output).

Takes a portfolio id, resolves every modelled scenario (those with plans),
and writes one workbook with a properties sheet per scenario. EPC descriptive
fields are sourced live from the EPC service (property_details_epc is dead);
property_type falls back override -> cert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 17:39:46 +00:00
Daniel Roth
afe1801273 implausible-lodged-score check fires for bad upstream EPC data (lodged < 13, gap >= 15) 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 15:27:19 +00:00
Daniel Roth
e70df63ee6 implausible-lodged-score check fires for bad upstream EPC data (lodged < 13, gap >= 15) 🟥
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 15:26:29 +00:00
Daniel Roth
fe1d04983b effective-lodged-divergence is silent for implausible lodged scores (< 13) 🟩
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 15:25:02 +00:00
Khalim Conn-Kowlessar
4a110d13cd Guard the portfolio audit against the 26m-row recommendation seq-scan
The `recommendation` table (~26m rows) has no index on `plan_id`, so any query
reaching it via `plan_id` — including the audit's own rollup — seq-scans the
whole table and saturates the shared DB (it blocked the DB during the
portfolio-796 audit). Make the audit safe-by-default:

- statement_timeout (120s) on the audit connection — a hard ceiling so a bad
  plan aborts instead of hammering the DB.
- The recommendation rollup (the two solar checks) is now opt-in via
  --with-recommendations, and EXPLAIN-gated: it refuses to run (raising
  RecommendationScanError) when the plan contains a Seq Scan on recommendation,
  which it does on any large portfolio until idx_recommendation_plan_id exists.
- SKILL.md documents the plan_id-no-index trap, the reach-via-property_id /
  EXPLAIN-first / confirm-with-user rules, and the index as the real fix.

Verified on 796/1268: default run is bounded and completes (2,952 anomalies over
31,919 properties); --with-recommendations aborts pre-scan portfolio-wide but is
allowed for a single property.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 13:23:26 +00:00
Daniel Roth
71b304fa47 decrease lambda concurrency from 32 to 12 2026-06-29 13:32:53 +00:00
KhalimCK
29a0cc7f47
Merge pull request #1344 from Hestia-Homes/tooling/modelling-anomaly-audit
feat(audit): pluggable modelling-anomaly audit + audit-ara-portfolio skill
2026-06-29 11:18:26 +01:00
Khalim Conn-Kowlessar
02ef67fd8f chore(debug): summary_to_sap.py — Elmhurst Summary PDF -> our SAP + trail 🟪
Runs a Summary PDF through the chain-test path (pdftotext ->
ElmhurstSiteNotesExtractor -> from_elmhurst_site_notes) into Sap10Calculator
and dumps SAP + per-end-use kWh + the intermediate worksheet trail, for
diffing our calc against the accompanying Elmhurst U985 worksheet PDF.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 21:26:30 +00:00
Khalim Conn-Kowlessar
2d6b078bd8 feat(audit): self-improvement loop in the skill + provenance convention 🟩
Add Phase 6 (self-improve) to audit-ara-portfolio: when a run confirms a
novel systematic problem, codify it as a check — gated on systematic (>=5
props, root-caused), not-already-covered, and /grill-me-pressure-tested.
Each check records provenance (motivating cause + example properties) so the
registry stays sharp and compounds every run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:57:57 +00:00
Khalim Conn-Kowlessar
e3c9107313 feat(audit): --scenario filter + audit-ara-portfolio skill 🟩
Script takes an optional --scenario to restrict to one scenario's plans. New
skill drives the full loop: run the deterministic scan, review groups,
deep-dive samples via run_modelling_e2e, characterise sub-classes, and
cross-reference open PRs/ADRs — then proposes new checks to codify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:49:48 +00:00
Khalim Conn-Kowlessar
37c7a2c186 feat(audit): solar + high-SAP checks; group under scripts/audit/ 🟩
Add recommendation-level rollups (solar SAP points + solar bill saving) and
checks: impossible-sap-over-100 (found 1), excessive-solar-sap (oversized
array, 51), low-solar-bill-savings (SEG/self-consumption pricing, 83),
unusually-high-post-sap (21). Move to scripts/audit/anomalies.py
(python -m scripts.audit.anomalies).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:46:23 +00:00
Khalim Conn-Kowlessar
39fd1c9e42 feat(audit): pluggable modelling-anomaly audit over the DB 🟩
A check-registry that reads property/baseline/plan/scenario and flags odd
results (plan-below-baseline, already-meets-goal-with-works, band/score
mismatch, zero-works-post-differs, effective-lodged divergence, negative
bill savings). Writes modelling_audit.md/.csv. Adding a check = one
decorated function.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 19:38:28 +00:00