Commit graph

8551 commits

Author SHA1 Message Date
Jun-te Kim
2aba56c28e
Merge pull request #1696 from Hestia-Homes/fix/wall-construction-enum-mapping
Offer EWI/IWI on gov-API system-built walls (wall_construction code 8)
2026-07-28 18:04:46 +01:00
KhalimCK
4c0ff31454
Merge pull request #1693 from Hestia-Homes/debug/pashub-lrha-sap-gaps
PasHub LRHA WAVE 3: three spec-correct SAP fixes (58.3% → 62.1% within-0.5)
2026-07-28 12:14:58 +01:00
Jun-te Kim
aa4446757f Amend ADR-0019: gov-API code 8 is system built, not park home 🟪
Record why the generator now keys system-built on both code 6 and the
gov-EPC API code 8, citing the GOV.UK RdSAP WallConstructionCode XSD, and
flag the remaining mapper-level enum-normalisation work as open.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
2026-07-28 10:33:52 +00:00
Jun-te Kim
d1f064433c Offer EWI/IWI on a gov-API system-built wall (code 8) 🟩
Key the solid-wall Recommendation Generator's system-built branch on the
gov-EPC API code 8 (`_WALL_SYSTEM_BUILT_GOV_API`) alongside the internal
code 6, so a precast/no-fines-concrete system-built wall gets its EWI+IWI
Options. This mirrors `u_wall`, which already resolves code 8 as
system-built via `rdsap_uvalues._GOV_API_WALL_CODE_TO_TYPE[8] =
WALL_SYSTEM_BUILT`. Repoint the explicit park-home exclusion to the
gov-canonical park-home code 10. Supersedes ADR-0019's "do not key
system-built on 8", which read code 8 as the Elmhurst park-home label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
2026-07-28 10:33:16 +00:00
Jun-te Kim
79bf28d8cb Offer EWI/IWI on a gov-API system-built wall (code 8) 🟥
The gov-EPC API lodges a system-built wall as `wall_construction == 8`
per the authoritative GOV.UK RdSAP `WallConstructionCode` enumeration
(communitiesuk/epb-data-warehouse `.../SAP-Domains.xsd`: 8 = "system
built" in every schema version 17.0-21.0.1; park home is 10). The solid-
wall Recommendation Generator instead read code 8 as the Elmhurst
park-home label and suppressed the whole cohort's EWI/IWI Recommendation
(a 2026 DB sweep finds ~5.1k certs at code 8, ~2.1k uninsulated main
walls). Rewrite the mislabelled park-home test to pin the correct
behaviour (code 8 → EWI+IWI), and add a genuine park-home test on the
gov code 10 so the fix doesn't start over-offering on real park homes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
2026-07-28 10:33:06 +00:00
Daniel Roth
7f0056c700
Merge pull request #1694 from Hestia-Homes/fix/pashub-evidence-pagination
Paginate PasHub evidence list instead of capping at one page
2026-07-28 11:21:19 +01:00
Daniel Roth
fdccad9c37 Fetch every evidence page when a job has more files than one page holds 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:12:25 +00:00
Daniel Roth
3c52a0aad6 Raise truncation error when PasHub stops serving pages before the full evidence set arrives 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:12:03 +00:00
Daniel Roth
f02441b6c4 Raise truncation error when PasHub stops serving pages before the full evidence set arrives 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:11:44 +00:00
Daniel Roth
4efb7eccbc Fetch every evidence page when a job has more files than one page holds 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:07:28 +00:00
Daniel Roth
5a85403c3d Fetch every evidence page when a job has more files than one page holds 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 10:06:00 +00:00
Khalim Conn-Kowlessar
7b50e5b7f5 docs(pashub): Elmhurst input sheets for the Varsity Rise cluster
The 12 Varsity Rise (Louth) homes are the only community-heated dwellings in
the LRHA WAVE 3 cohort and the only residual over-rate cluster (~+1 SAP). Two
candidate causes cannot be adjudicated from PasHub's `pre_sap` alone and need
accredited Elmhurst worksheets: (A) an electric-immersion DHW fuel-code
collision that bills immersion as community biomass (code 42), and (B) a
suspected community-DHW solar over-credit (the parser drops solar-collector
geometry, so the engine takes Table-29 defaults).

Add page-by-page Elmhurst RdSAP input sheets for one cert of each type
(461386632388 group A, 497671579889 group B) with a "what to read back" list
of the worksheet lines needed to diff term-by-term. Also correct the immersion
single → dual note on the 497655371974 build sheet (the cert lodges Dual).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:23:11 +00:00
Khalim Conn-Kowlessar
4d9dd9aaf3 fix(sap): (203)-weight the dual-main Table 4e MIT adjustment
For a dwelling with two main systems heating different parts, the (93)m
Table 4e mean-internal-temperature adjustment was taken from system 1 alone
(`details[0]`). SAP 10.2 p.186 requires the (203)-weighted mean of each
system's control adjustment — (1 - (203)) for system 1, (203) for system 2 —
mirroring the Table 9b responsiveness weighting already applied two lines
above. Keyed to system 1, a dwelling led by a small-fraction system (e.g. a
20% HHRSH, adj 0.0 °C, ahead of an 80% manual-charge storage heater, adj
+0.7 °C) was modelled ~0.7 °C too cool → space heat under-counted → over-rated.

Compute the weighted adjustment at the call site. Shared `cert_to_inputs`
path, so it also lifts the accredited gov-API RdSAP corpus (81.5% → 81.7%,
guardrail). 5 Edmund Close +2.74 → +0.59; 4 Edmund Close -2.51 → -0.55.
LRHA cohort within-0.5 60.2% → 62.1%, MAE 0.496 → 0.445; ratchets re-based to
0.62 / 0.45. Also corrects an oil-combi docstring (462051619031 is a Grant
Vortex 10599, not the Worcester 18415).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:22:59 +00:00
Khalim Conn-Kowlessar
138f5688e0 fix(pashub): wire the surveyed measured cylinder heat-loss factor
A PasHub survey can lodge a manufacturer's measured cylinder loss ("What is
the cylinder measured heat loss: 0.94 kWh/24 hours") — the SAP 10.2 §4
(48)-(50) declared-loss factor that must override the Table 2 V×L×VF
insulation computation. The site-notes path dropped it two ways: the
extractor looked for "Cylinder Measured Heat Loss:" (matches 0 fixtures) while
the PDF label is "What is the cylinder measured heat loss:" (92 fixtures), and
the mapper never wired `cylinder_heat_loss` even when captured. So the
calculator fell back to the age-band insulation default — a lossier cylinder
than surveyed — over-costing hot water and under-rating SAP.

Fix the extractor label and add `_pashub_cylinder_measured_heat_loss` (parses
the leading float, returns None for "Not known"), mirroring the gov-API and
Elmhurst paths that already pass `cylinder_heat_loss` through. 3 Woodmans
Court -1.33 → -0.09; LRHA cohort within-0.5 58.3% → 60.2%, MAE 0.515 → 0.496.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:22:05 +00:00
Khalim Conn-Kowlessar
451d0ceb73 fix(sap): bill mixed per-window glazing at each window's own U
The window channel collapsed a dwelling with genuinely mixed per-window
glazing (distinct `glazing_type` codes, no lodged per-window U) onto
windows[0]'s U, billing the entire opening area at one window's value.
SAP 10.2 worksheet (26)-(27) sums each window's own RdSAP Table 24 U over
its own area. The PasHub site-notes path is the only source that lodges
heterogeneous per-window codes with no per-window U (the gov-API/Elmhurst
paths carry a uniform code or a lodged U), so it was the only one mis-billed.

Add a `windows_have_mixed_codes` branch (factoring `_window_u_raw_from_code`
out of `_synthesised_window_u_raw`) that bills each window individually with
the per-window curtain transform. Scoped to lodge no average → uniform-glazing
dwellings stay byte-identical. Symmetric: corrects over- and under-raters
(e.g. 8 Loveden View -1.39 → window channel 32.31 → 26.67 W/K). LRHA cohort
within-0.5 58.3% → 58.3%, MAE 0.531 → 0.515.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 09:21:40 +00:00
KhalimCK
035d2f5136
Merge pull request #1687 from Hestia-Homes/fix/epc-property-per-property-persistence
EPC persistence: write a per-property lodged copy, and refresh a stale stored cert on refetch
2026-07-27 16:44:55 +01:00
Jun-te Kim
dc2913b7db
Merge pull request #1688 from Hestia-Homes/fix/sap17-1-community-heating
Map full-SAP community heating (heat networks) instead of dropping it
2026-07-27 15:53:11 +01:00
Khalim Conn-Kowlessar
de08d93369 Refresh a stale stored gov cert on a refetch when its content differs
refetch_epc=True fetched a fresh cert but the recency tie-break (strict >)
dropped it whenever the stored row shared the cert's inspection date — so a
row written before a schema field/table existed (e.g. room-in-roof geometry)
could never be healed by re-fetching. The dwelling was modelled on stale
fabric indefinitely.

Reconcile instead of just comparing dates: on an equal inspection date,
refresh a public gov cert (uploaded_file_id NULL) when the fetched cert
differs from the stored one, so a plain re-fetch re-ingests it. One of our
own surveys (PasHub / ECMK / Elmhurst — uploaded_file_id set) is preserved on
the tie, keeping the survey-wins rule (ADR-0001 / #1589); an unchanged public
cert is still not re-persisted.

`_reconcile_lodged` layers the content refresh over `_newer_lodged` (recency
untouched). New `EpcRepository.survey_lodged_uprns` surfaces the row-level
uploaded_file_id provenance the hydrated EpcPropertyData drops. Together with
the property_id-anchored persist, a refetch_epc=True re-run now heals both a
missing epc_property row and a stale one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 14:34:26 +00:00
Daniel Roth
7680d9346e
Merge pull request #1686 from Hestia-Homes/feature/abri-api-void-job
Abri API: Log empty properties in hubspot as Void
2026-07-27 15:21:07 +01:00
Daniel Roth
5c596627ef Space the void marker off the existing note exactly once 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 14:06:34 +00:00
Daniel Roth
c3e6640240 tidy up 2026-07-27 13:42:00 +00:00
Daniel Roth
ed43ed48ca "Void. " not "Void ||" 2026-07-27 13:40:47 +00:00
Khalim Conn-Kowlessar
2b22b0f2ef Persist a property_id-anchored lodged EPC when the property owns no row
The lodged EPC read keys on UPRN (ADR-0001 recency tie-break), but writes
and the front end both key on property_id. When a dwelling is onboarded
under two portfolios, the shared UPRN surfaces the sibling's lodged row,
so `lodged_epc_is_new` stays False and the modelling run skips persisting
a copy for this property — leaving it with no epc_property row the FE can
read. The EPC is modelled but invisible (portfolio 850 / property 792235,
uprn 100021979925, also under property 753896 in portfolio 830).

Drive the lodged persist off "does THIS property own a lodged row?"
(property_id-scoped) rather than "is the cert new?" (UPRN-scoped): add
`EpcRepository.property_ids_with_lodged_epc` and persist a per-property
copy whenever the cert is new OR the property has no lodged row of its
own. Idempotent (save_batch is replace-by-property_id), and the recency
skip still holds for a property that already owns its row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 13:39:31 +00:00
Daniel Roth
c053c41b0a Describe the void marker for coordinators and in the glossary 🟪
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:35:36 +00:00
Daniel Roth
b77769fdca Leave an already-flagged deal alone and fail loudly on a failed flag 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:34:50 +00:00
Daniel Roth
a397eba48d Persist the deal's extra booking information from the scrape 🟩
The differ and the HubSpot fetch already named notes_for_surveyor, but the
SQLModel had no such field, so check_for_db_update_trigger raised
AttributeError on every deal and the scrape could not diff at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:34:49 +00:00
Daniel Roth
bd01a08da8 Flag a void property at the front of the deal's booking information 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:29:50 +00:00
Daniel Roth
f742795434 Flag a void property at the front of the deal's booking information 🟥
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 13:28:50 +00:00
Jun-te Kim
9c3b901b8a Add Elmhurst accreditation evidence for the Apt 2 community cert 🟩
Reproducible RdSAP-10 build (build_100010472748.py) + downloaded Input Summary
and SAP Worksheets (accredited SAP 62 D) + the entry crib sheet, as the
ground-truth for the community-heating mapping fix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:22:51 +00:00
Jun-te Kim
4e8c648189 Extract shared community heat-source flattening helper 🟪
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:22:23 +00:00
Jun-te Kim
efd171fd55 Pin full-SAP community cert recovering out of band F end-to-end 🟩
Apt 2 (Tracey's Place) through from_api_response -> cert_to_inputs ->
calculate_sap_from_inputs: priced as mains gas (fuel 51), SAP 67 D — recovered
from the pre-fix 27 F. Accredited Elmhurst RdSAP is 62 D; lodged 71 C.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:19:41 +00:00
Jun-te Kim
c74579cf2c Set CHP heat fraction + back-up boiler fuel for full-SAP heat networks 🟩
Completes the community taxonomy: CHP (302) carries its chp_fraction and
boiler fuel for the SAP 302 worksheet; heat pump (304) rides the existing
fuel/code path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:17:54 +00:00
Jun-te Kim
58084a316f Map full-SAP community CHP + heat-pump networks 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:17:54 +00:00
Jun-te Kim
3539414a1d Map full-SAP community heat-source mix to its SAP Table 4a code 🟩
Derive 301 (boilers) / 302 (CHP) / 304 (heat pump) from the lodged
heat_source_type mix and set it on the category-6 main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:14:44 +00:00
Jun-te Kim
cca2c18ef9 Map full-SAP community boilers to SAP code 301 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:14:44 +00:00
Jun-te Kim
8d03e6aad8 Map full-SAP community heat source to its mains-gas fuel 🟩
Full SAP lodges the heat-network fuel in sap_community_heating_systems (which the
schema dropped); resolve the dominant heat source's Table 12 fuel onto the
category-6 main so the engine prices it as a network, not electricity.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:11:02 +00:00
Jun-te Kim
86043e267c Map full-SAP community heat source to its mains-gas fuel 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:11:02 +00:00
KhalimCK
f4932569ba
Merge pull request #1685 from Hestia-Homes/feat/1639-pashub-lrha-wave3-residual
PasHub LRHA Wave 3: close residual mapper/calculator gaps (#1639)
2026-07-27 14:01:29 +01:00
Daniel Roth
a2c2681c18 extra_booking_information column 2026-07-27 12:57:08 +00:00
Khalim Conn-Kowlessar
ca27f0b9d8 Charge an oil storage-combi its PCDB internal-store water-heating loss 🟩
An oil STORAGE combi (Worcester Greenstar Heatslave II, PCDB 18415,
store_type 1) zeroed its internal-store loss: pcdb_combi_loss_override
defers on store_type in {1,2,3} and a combi has no cylinder, so line
(56) dropped to zero — under-costing hot water and over-rating SAP by
+2.0..+4.9 across the five LRHA WAVE 3 oil combis.

SAP 10.2 Table 2 note b) now includes the store loss when the combi's
efficiency is from the PCDB: the PCDB store volume (field 42) and
insulation thickness (field 44) feed Table 2/2a with the Table 2b
storage-combi temperature factor, reproducing the accredited Elmhurst
P960 worksheet exactly (5.4175 kWh/day, Jan 167.94, ~1977 kWh/yr).

Cohort: within-0.5 55.3% -> 58.3%, MAE 0.675 -> 0.531; ratchet
tightened accordingly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 12:43:52 +00:00
Daniel Roth
6c971b3405 Name the void-property outcome in the glossary and ops guide 🟪
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:28:39 +00:00
Daniel Roth
da789392ab Complete the task for a void property instead of dead-lettering it 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:26:42 +00:00
Daniel Roth
00adbcd519 Complete the task for a void property instead of dead-lettering it 🟥
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:26:12 +00:00
Daniel Roth
1bb45e2f4e Complete the tenant sync without contacts when the property is void 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:25:30 +00:00
Daniel Roth
a3e1174d0d Complete the tenant sync without contacts when the property is void 🟥
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:24:49 +00:00
Daniel Roth
6ee560af29 Keep the query-dump form of "no tenancy found" a rejection 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:23:49 +00:00
Daniel Roth
e5beded920 Report a void property when Abri finds no tenancy for the place 🟩
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:23:12 +00:00
Daniel Roth
a7ae408ab2 Report a void property when Abri finds no tenancy for the place 🟥
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:22:28 +00:00
Daniel Roth
ab7b5e388f
Merge pull request #1683 from Hestia-Homes/feature/abri-api-tweaks
Key Abri log/amend/abandon off client_booking_reference, not the survey-date edge
2026-07-27 12:10:22 +01:00
Khalim Conn-Kowlessar
a21d2cc579 Bill an electric second main at its own §12/Table-12a rate 🟩
Closes #1684. `_main_2_space_heating_fuel_cost_gbp_per_kwh` billed an
electric Main 2 at Main 1's space-heating rate. That was a no-op on a
homogeneous off-peak cohort (storage+storage, direct+direct → equal
rates) but mis-billed the HETEROGENEOUS case: an off-peak STORAGE Main 1
(5.5 p/kWh low) + an on-peak DIRECT-ACTING panel Main 2 (15.29 p/kWh
high) rode the storage low rate, under-costing the panel half and
over-rating SAP.

Flip it to bill Main 2 at its OWN Table 12a Grid 1 SH row, the same
per-system resolution Main 1 and a non-electric Main 2 already use — and
the same one `_main_2_high_rate_fraction` already applies, so the scalar
cost and the high-rate split are now consistent.

The docstring's "regresses certs 13/34 (Parkers Hill / Dunley Road)"
warning was stale: those names appear nowhere but that docstring, and
the full guard suite shows zero new regressions (only the two
pre-existing `test_heating_systems_corpus` fails on main). LRHA WAVE 3
fixture 497655371974 (storage+panel) moves +15.3 → -2.5 vs PasHub; cohort
MAE 0.799 → 0.675 (ratchet re-baselined 0.80 → 0.68). within-0.5 holds at
55.3% — the -2.5 residual is PasHub-vs-Elmhurst divergence, not a bug, so
the fixture stays out of the within-0.5 pin. A ground-truth Elmhurst pin
is queued (see docs/HANDOVER_1684_ELMHURST_INPUTS_497655371974.md).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-27 09:06:20 +00:00