Commit graph

8104 commits

Author SHA1 Message Date
Jun-te Kim
7f7dcbc059 Default description on bare zero-rated party-wall elements in SAP-15.0 certs
Task 9d271e98-96e6-4be6-bb50-e9be6f954003 (portfolio 796) failed 21
"Top-floor flat" properties with "EnergyElement: missing required
field 'description'". Every one lodges a second walls[] entry that is
a bare zero-rated party-wall boundary element
({"energy_efficiency_rating": 0, "environmental_efficiency_rating": 0},
no description key at all) — unlike the sibling floors/roofs
"(other premises above/below)" sentinel convention, which does carry
text. Confirmed schema-wide across all 21 failing certs.

Default description to "" rather than fabricate construction text:
_joined_descriptions (heat_transmission.py) already filters out falsy
descriptions, so this is calc-neutral — it only unblocks the parse.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:28:55 +00:00
Jun-te Kim
8929a2115f
Merge pull request #1575 from Hestia-Homes/worktree-task-9d271e98-failures
Derive cylinder_size when has_hot_water_cylinder is false on SAP-15.0/16.x certs
2026-07-14 17:16:18 +01:00
Jun-te Kim
5d5fa73372 Derive cylinder_size when has_hot_water_cylinder is false on SAP-15.0/16.x certs
Task 9d271e98-96e6-4be6-bb50-e9be6f954003 (portfolio 796) failed property
715358 with "SapHeating: missing required field 'cylinder_size'": cert
0685-2881-6867-9029-1761 lodges has_hot_water_cylinder="false" but omits
sap_heating.cylinder_size entirely. Mirrors the already-handled inverse
gap in _normalize_sap_schema_16_x, so default to code 1 ("no cylinder")
rather than fail loud.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 16:11:24 +00:00
Daniel Roth
ff1302b514
Merge pull request #1545 from Hestia-Homes/backfill-epc-property-uprn
Script to backfill UPRN in epc_property for rows populated by sitenotes before the mapper included the UPRN
2026-07-14 17:02:55 +01:00
Jun-te Kim
42543b0f42
Merge pull request #1573 from Hestia-Homes/fix/hubspot-planning-fields-property-list
fix: request planning fields from HubSpot deal fetch
2026-07-14 16:57:33 +01:00
Jun-te Kim
bb0142d6f8 Request planning/address-profiling properties from HubSpot deal fetch
FIELD_MAP and upsert_deal already map planning_authority, designated_area,
article_pd_rights, listed_building, design_constraints, planning_comments,
planning_status, and planning_suggested_approach, but from_deal_id_get_info
never requested them from the HubSpot API. Since HubSpot only returns
explicitly requested properties, these always came back None regardless
of the deal's actual data, so refreshes never populated them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-14 15:49:42 +00:00
Daniel Roth
dc5c29c8c1
Merge pull request #1571 from Hestia-Homes/feat/pashub-accuracy-harness
PAS Hub SAP-accuracy regression harness — Guinness 205 cohort (foundation for PRD #1555)
2026-07-14 16:42:10 +01:00
KhalimCK
35ae228fa9
Merge pull request #1569 from Hestia-Homes/feature/pashub-heat-emitter-code-1556
Resolve PasHub site-notes emitter label to its SAP10 code (Guinness 205)
2026-07-14 16:40:55 +01:00
Daniel Roth
abab5bf9ac
Merge branch 'main' into feature/pashub-heat-emitter-code-1556 2026-07-14 16:34:28 +01:00
Khalim Conn-Kowlessar
e5fee3896a Add PAS Hub SAP-accuracy regression harness (Guinness 205 cohort)
Foundation for PRD #1555: runs each PAS Hub site-note PDF through the
extractor -> EpcPropertyData -> Sap10Calculator and gauges the computed
SAP against pashub's own SAP-10.2 `pre_sap` (from hubspot_deal_data).

- test_pashub_sap_accuracy.py: hybrid gate. Per-fixture "must compute"
  (xfail on the known in-progress mapper gaps MissingMainFuelType /
  UnmappedSapCode / UnmappedPasHubLabel) + aggregate within-0.5 ratchet
  floor, mirroring test_sap_accuracy_corpus.py.
- 205 image-stripped site-note PDFs + manifest.json. Images stripped so
  the repo footprint stays ~52MB while the text layer the extractor reads
  is byte-identical.
- build_pashub_accuracy_fixtures.py: provenance/rebuild from S3 +
  hubspot_deal_data.

All 206 currently xfail on the known heating-string mapper gaps; each fix
(#1556-1568) flips its fixtures to computing and ratchets the floor.

Refs #1555 #1568

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:32:18 +00:00
KhalimCK
e985c0cbf2
Merge pull request #1570 from Hestia-Homes/feat/pashub-party-wall-1559
PAS Hub mapper: party_wall_construction label -> SAP10 code (#1559)
2026-07-14 16:31:15 +01:00
Khalim Conn-Kowlessar
f9bf45de5c Map PasHub party_wall_construction labels to SAP10 codes
`from_site_notes` copied the raw `party_wall_construction_type` survey
label onto the building part; the calculator read `None` and applied the
U=0.25 house default to every party wall — phantom heat loss on the
~165/205 solid party walls that RdSAP 10 Table 15 rates U=0.0.

Add `_pashub_party_wall_construction_int` (mirroring `_pashub_main_fuel_code`)
mapping the six surveyed labels to the SAP10 codes `u_party_wall` consumes,
strict-raising `UnmappedPasHubLabel` on an unknown label, and wire it into
both site-note building-part mappers. On the Guinness GMCA cohort this
halves the systematic SAP under-rate (mean signed -1.58 -> -0.85).

Refs #1559

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:26:30 +00:00
Daniel Roth
64e31742b3 Strict-raise on an unrecognised PasHub main-heating emitter label 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:17:56 +00:00
Daniel Roth
5b0f25847c Normalize PasHub site-notes emitter label to its SAP10 code 🟩
_map_sap_heating resolves the raw emitter label (e.g. Radiators) to its
SAP10 emitter code via _pashub_heat_emitter_code, reusing the Elmhurst
emitter map. Blank passes through; an unrecognised label strict-raises
UnmappedPasHubLabel at the mapper boundary (ADR-0015) instead of
resurfacing as the calculator's UnmappedSapCode: heat_emitter_type.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:17:06 +00:00
Daniel Roth
8d45fcb544 Normalize PasHub site-notes emitter label to its SAP10 code 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:16:08 +00:00
Daniel Roth
2f0d5a25f1
Merge pull request #1554 from Hestia-Homes/feature/pashub-normalize-main-fuel-1552
Pashub sitenotes mapping: normalise main fuel 1552
2026-07-14 15:45:55 +01:00
Daniel Roth
3c49f4250e Merge branch 'main' into feature/pashub-normalize-main-fuel-1552 2026-07-14 14:36:11 +00:00
Daniel Roth
b91763fc1d Preserve PasHub blank-Fuel electric-system inference through fuel-code normalization 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:34:28 +00:00
Daniel Roth
c087376cdd
Merge pull request #1553 from Hestia-Homes/feature/pull-sap-and-emissions-from-pashub
Pull sap and emissions from pashub to store on EpcPropertyData when mapping from site notes
2026-07-14 15:32:08 +01:00
Daniel Roth
a8afcd1b3d Strict-raise on an unrecognised PasHub main-heating Fuel label 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:31:32 +00:00
Daniel Roth
1fead6e4c7 Strict-raise on an unrecognised PasHub main-heating Fuel label 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:30:49 +00:00
Daniel Roth
02ad8701d3 Normalize PasHub main-heating Mains gas to SAP fuel code 26 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:29:26 +00:00
Daniel Roth
52c5826afc Normalize PasHub main-heating Mains gas to SAP fuel code 26 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:27:34 +00:00
Daniel Roth
442352c378 Drop the unused DownloadedFile import from the client tests 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:07:23 +00:00
Daniel Roth
38cae15f4d Preserve SharePoint distribution when the site-note save fails loudly 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
e31d2ad2d1 Preserve SharePoint distribution when the site-note save fails loudly 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
62c0151469 Fill partial summaries best-effort, fail loudly, skip evidence-only jobs, and retry on Coordination Hub 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
377c030c77 Overlay PasHub as-surveyed performance onto saved Site-Notes property 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
43fac40595 Overlay PasHub as-surveyed performance onto saved Site-Notes property 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
ded8cf1e46 Raise loudly when RdSAP Summary fetch returns an HTTP error 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
eff4f64426 Raise loudly when RdSAP Summary fetch returns an HTTP error 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
83d92010de Raise UnauthorizedError when RdSAP Summary fetch returns 401 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
b596f32af1 Raise UnauthorizedError when RdSAP Summary fetch returns 401 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
af6c461c93 Map PasHub RdSAP Summary response to as-surveyed performance values 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Daniel Roth
85482cdf9e Map PasHub RdSAP Summary response to as-surveyed performance values 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:06:45 +00:00
Jun-te Kim
3dd86f296d
Merge pull request #1549 from Hestia-Homes/feature/expired-predicted-source
feat(modelling_e2e): condition prediction on the expired Historic EPC (ADR-0054)
2026-07-14 13:14:05 +01:00
Jun-te Kim
f0f9c8d8d7 feat(terraform): point modelling_e2e at the historic-EPC backup (ADR-0054)
Sets HISTORIC_EPC_S3_ROOT, the env var the handler reads to build the historic-EPC
reader. With it set, an EPC-less Property whose expired pre-2012 certificate is in
the backup has its prediction conditioned on that certificate and persisted as
source="expired" rather than "predicted" — which is what lets reporting tell "no EPC
at all" apart from "only an expired one".

The bucket comes from the shared remote state rather than a rebuilt string:
`retrofit_sap_data_bucket_name` is — despite the name — the retrofit-data-<stage>
bucket (shared/main.tf:167), and engine and fast-api already read it from that output.
modelling_e2e was the outlier hardcoding "retrofit-data-${var.stage}".

No IAM change: modelling_e2e_s3_read already grants GetObject + ListBucket across the
whole retrofit-data-<stage> bucket, which covers historical_epc/.

Note this makes 'expired' rows start appearing once deployed. assessment-model's
epcSources.ts joins only 'lodged' and 'predicted', so until it addresses the predicted
slot — source IN ('predicted','expired') — an 'expired' row matches neither and the
home drops out of both the "Homes Without an EPC" and "Expired EPCs" cards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:08:29 +00:00
Jun-te Kim
1457275fb5 fix(modelling_e2e): ship the Historic EPC import closure in the Lambda image
test_lambda_image_copies_full_import_closure caught this: importing the Historic
EPC S3 repo drags datatypes/epc/domain/historic_epc_matching.py into the handler's
init-time closure, and that reaches back into the legacy address matcher —
backend/address2UPRN/scoring.py and utils/pandas_utils.py. The image COPYed
neither, so the Lambda would have died at cold start with Runtime.ImportModuleError.

Copied file-by-file rather than `COPY backend/ backend/`: backend/ is the whole
legacy engine and the closure needs only these seven files. Their third-party
deps (pandas, requests) are already in requirements.txt, so no new pip installs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:02 +00:00
Jun-te Kim
efdb8e816c feat(modelling_e2e): condition prediction on the expired Historic EPC (ADR-0054)
ADR-0054 gives a prediction conditioned by an expired Historic EPC the source
"expired", so reporting can tell "no EPC at all" apart from "only an expired
one". It was implemented in IngestionOrchestrator — but the pipeline that
actually runs is modelling_e2e, which predicts through its own _predict_epc and
never adopted it. Result: zero `expired` rows have ever been written, and
`epc_property.source` holds only 'lodged' and 'predicted'.

Two things kept the flavour unreachable here, and both had to go:

  - _predict_epc never looked at the historic backup at all, so no prediction
    was ever conditioned.
  - _flush_writes passed the literal source="predicted", and _PropertyWrite had
    no field to carry a flavour, so one would have been dropped before the write
    even if computed.

_predict_epc now mirrors IngestionOrchestrator._predict: the expired cert's
stable attributes fill the gaps Landlord Overrides left (overrides still win
where both speak) and condition the cohort, and it returns the source alongside
the EPC. _PropertyWrite carries it; _flush_writes persists it. save_batch
already groups deletes by source family, so a mixed predicted/expired batch
clears the shared slot correctly.

Ships dark: the reader is built only when HISTORIC_EPC_S3_ROOT is set, so
without it every prediction stays plain "predicted", exactly as today.
DEFAULT_S3_ROOT names the dev bucket, so defaulting to it would have a prod
lambda silently reading dev data — Terraform sets the var per environment.

Note this also rescues properties that currently fail to model outright: an
expired cert can supply the property_type no override resolved, where today
that raises UnresolvedPropertyTypeError.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:15:39 +00:00
KhalimCK
59e4ccf4c7
Merge pull request #1547 from Hestia-Homes/feature/fetch-from-epc-property-on-uprn
Fetch a property's lodged EPC by UPRN, not property_id
2026-07-14 11:53:11 +01:00
Daniel Roth
c9cb59ea78 Break lodged-UPRN duplicates by latest inspection_date, then id
inspection_date is non-null on epc_property, so it is a better winner for
a shared-UPRN collision than insertion order: the latest-surveyed lodged
row wins. id DESC stays as the deterministic secondary, so a same-UPRN,
same-inspection_date tie falls to the highest id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:46:01 +00:00
Daniel Roth
f7328f54c5 Fetch a property's lodged EPC by UPRN, not property_id
An epc_property row can carry a null property_id (FE/historic ingestion
persists EPC rows never linked to a property row), so a property_id-keyed
read silently misses them. UPRN is the durable key both the property row
and the epc_property row share. Repoint the lodged reads (get_for_property
+ get_for_properties) onto UPRN; the predicted reads stay on property_id
because a predicted EPC deep-copies a neighbour's UPRN, so its UPRN column
is never the property's own.

UPRN is not unique on epc_property, so the read tie-break is now load-
bearing (it was dormant under property_id, where the write path guarantees
one lodged row per property): the most recently ingested (highest-id) row
wins. Write path stays keyed on (property_id, source).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:35:51 +00:00
Daniel Roth
f5d2bdd651 update script 2026-07-14 09:40:38 +00:00
Daniel Roth
08e11538cd script 2026-07-14 09:13:12 +00:00
KhalimCK
e1c99edc3f
Merge pull request #1542 from Hestia-Homes/fix/loft-topup-below-building-regs
Loft insulation eligible below the 270mm building-regs depth (ADR-0063)
2026-07-10 17:11:44 +01:00
Khalim Conn-Kowlessar
3bbe293f68 Gate the loft top-up strictly below the 270mm building-regs depth 🟩
Locks the gate edges — a lodged pitched loft below 270mm is recommended, a
loft at 270mm is left alone, and the 'Nmm+' form ('300mm+') parses to its
number and stays ineligible. These passed on arrival (they fell out of the
numeric-parse gate added in the previous commit); pinned as regression guards.
ADR-0063.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:07:48 +00:00
Khalim Conn-Kowlessar
95a0da4828 Recommend loft insulation for a loft below the building-regs depth 🟩
A lodged numeric loft depth below the 270mm building-regs compliance gate is
now eligible (topped up to the 300mm install depth), regardless of whether the
roof type is lodged, since a measured depth is a positive statement of a real
loft. Sentinels keep their ADR-0047 resolution. Fixes property 724702 (50mm
loft) which previously returned no recommendation. ADR-0063.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:06:44 +00:00
Khalim Conn-Kowlessar
2fe5b79dc4 Recommend loft insulation for a loft below the building-regs depth 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 16:03:39 +00:00
Khalim Conn-Kowlessar
ce0940c335 Record below-regs loft eligibility as ADR-0063
The roof generator offered loft insulation only when genuinely uninsulated
(ADR-0021), so a loft with a shallow lodged depth (e.g. 724702's 50mm) got
nothing and never entered the optimiser pool. Grilled with Khalim: the loft
branch becomes eligible when the lodged depth is a known numeric value below
the 270mm building-regs compliance gate, topped up to the 300mm install depth.
Numeric-lodged-depths only; sentinels keep their ADR-0047 resolution. Loft
branch only. CONTEXT 'Roof Insulation Eligibility' updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:59:51 +00:00
Jun-te Kim
59954db488
Merge pull request #1540 from Hestia-Homes/fix/sap15-main-building-identifier
Map SAP-Schema-15.0 "Main building" identifier to MAIN
2026-07-10 16:09:59 +01:00