Commit graph

8359 commits

Author SHA1 Message Date
Daniel Roth
7b5a88e7ac Treat a missing totalItemCount as a complete evidence list 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:58:56 +00:00
Daniel Roth
cc3fa54688 Return the full evidence list when the response is complete 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:58:16 +00:00
Daniel Roth
c95d3b20dc Fail loudly when PasHub truncates the evidence list 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:57:25 +00:00
Daniel Roth
d8f673a31f Fail loudly when PasHub truncates the evidence list 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:56:55 +00:00
Daniel Roth
f54b420ae1 Request evidence list with explicit paging parameters 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:55:53 +00:00
Daniel Roth
0378166263 Request evidence list with explicit paging parameters 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 12:54:49 +00:00
KhalimCK
1c42845450
Merge pull request #1663 from Hestia-Homes/fix/sheltered-stairwell-resistance
Sheltered stairwell Ru 2.1 + party-ceiling roof code 3
2026-07-22 13:11:02 +01:00
Khalim Conn-Kowlessar
21faf63c89 Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tests/domain/sap10_calculator/worksheet/test_heat_transmission.py
2026-07-22 12:03:52 +00:00
KhalimCK
0e99ad711e
Merge pull request #1662 from Hestia-Homes/fix/floor-to-unheated-space-heat-loss
Bill a flat's floor to an unheated space as semi-exposed
2026-07-22 10:17:26 +01:00
Khalim Conn-Kowlessar
190325b23b Merge remote-tracking branch 'origin/main' into HEAD
# Conflicts:
#	tests/infrastructure/epc_client/test_sap_accuracy_corpus.py
2026-07-22 08:38:04 +00:00
KhalimCK
69d16596c9
Merge pull request #1661 from Hestia-Homes/fix/persist-rafter-insulation-thickness
Persist the measured at-rafters roof insulation thickness
2026-07-22 09:31:16 +01:00
Khalim Conn-Kowlessar
da993b5359 Treat a lodged party ceiling as U=0 instead of a pitched roof 🟩
RdSAP 10 item 2-3(h) "another dwelling above" is U=0 (spec p.45: "There is no
heat loss through the roof of a building part that has the same dwelling above
or another dwelling above"). gov-API `roof_construction = 3` is that code, but
the mapper labelled it "Pitched (slates/tiles), no access to loft" (which is
code 5), so the per-part party override at heat_transmission.py:1172 never
matched and the part fell through to the dwelling_type-label-only
has_exposed_roof heuristic.

190 of the 200 corpus code-3 parts sit at index 0 and are already suppressed by
`_cert_lodges_exposed_roof` off the roof_insulation_location == "ND" signal. The
unmitigated set is the 10 parts at index > 0, where extensions default to
exposed and were billed a phantom pitched roof. Also removes a latent PV defect:
the bogus "Pitched" prefix satisfied `is_pitched` and divided PV array area by
cos(35 deg), inflating kWp.

Shipped with the stairwell fix so the corpus pin moves once: within-0.5
78.8% -> 79.6%, SAP MAE 0.626 -> 0.611, PE MAE 2.9 -> 2.7.

The roof change alone trades 4 certs out of the +-0.5 band for 2 in (one moving
4.4 SAP closer). Those 4 were investigated and are NOT a masked bug: the corpus
error distribution is symmetric (mean +0.076, median +0.03, sigma 1.606; 112
certs above +0.5 vs 102 below), ground-floor flats show 7 positive / 8 negative
with no excess over the null, and closing them needs HLC changes of 2.3-5.6% —
below the ~8.8% noise floor of the RHI anchor, calibrated on the 76 certs whose
SAP is exact (space-heating ratio 1.062 +/- 0.088).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:23:52 +00:00
Khalim Conn-Kowlessar
3f6129afc6 Bill a sheltered wall to a stairwell at Ru 2.1, not the corridor 0.5 🟩
RdSAP 10 §5.9 "U-values of sheltered walls" (PDF p.43): a wall between a
flat/maisonette and an unheated buffer takes SAP 10.2 §3.3's U = 1/(1/U0 + Ru),
and Ru depends on the BUFFER TYPE — "Use Ru = 0.5 m²K/W for corridors and
Ru = 2.1 m²K/W for stairwell."

The buffer type is lodged on `SapFlatDetails.heat_loss_corridor` (0-based per
item 1-11, PDF p.70: 0 none, 1 heated, 2 unheated corridor, 3 unheated
stairwell). That field was read NOWHERE under domain/ — corridor presence was
inferred only from a lodged sheltered alternative wall — so every sheltered wall
took the corridor 0.5 and every stairwell flat over-billed its sheltered wall,
under-rating the dwelling.

Distinct from `_SHELTERED_GABLE_ADDED_RESISTANCE_M2K_W`, which is RdSAP 10
Table 4 (p.22) for a room-in-roof sheltered gable — same 0.5, different rule,
deliberately untouched.

Corpus gauge ratcheted UP: within-0.5 78.8% -> 79.8% (+10 certs), SAP MAE
0.626 -> 0.613, PE MAE 2.9 -> 2.8. Failure set and pyright count identical to
main. Portfolio 824 is stairwell-heavy (60 properties at code 3); property
749898 over-bills 4.44 W/K on a 48 m2 flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 22:02:31 +00:00
Khalim Conn-Kowlessar
0ffc25506e Bill a flat's floor to an unheated space as semi-exposed 🟩
RdSAP 10 §3.12 (PDF p.26), the flats/maisonettes floor rules: "There is no heat
loss through the floor if there is another flat below. Otherwise the floor area
of the flat ... is ... a semi-exposed floor if there are unheated premises below
it (e.g. an enclosed garage) ... Semi-exposed (sheltered) floors are treated as
if they were fully exposed." §5.13 (p.48) confirms the U-value identity: Table 20
applies to exposed and semi-exposed alike.

API floor_heat_loss=2 -> floor_type "To unheated space" set no exposure signal,
so on a mid-/top-floor flat the dwelling-level heuristic (has_exposed_floor=False,
assuming a heated dwelling below) went unopposed and the ENTIRE floor was billed
at 0 W/K. 11 properties across portfolios 796+824, mean +4.02 SAP, 10 of 11
over-rating — we judged dwellings more efficient than the accredited assessment
and would withhold retrofit measures they need.

Exemplars: 750018 space heating -53.2% -> -8.1% vs the cert's own RHI figure,
SAP +8.22 -> +1.16; 749898 -43.0% -> +0.9%, SAP +6.29 -> -1.11.

SCOPED TO FLATS ON PURPOSE. §3.12 is the flats/maisonettes section, and the
accredited Elmhurst worksheet disagrees with Table 20 for a house: golden cert
7536-3827-0600-0600-0276 (detached) lodges floor_heat_loss=2 on its Main part and
the worksheet bills it on the BS EN ISO 13370 ground-floor cascade at U 0.97, not
Table 20's 1.20. An unscoped mapper-level fix broke that pin and the
uprn_100050881708 accuracy pin; the scoped fix leaves both untouched.

No regressions: identical failure set and pyright count vs main; the RdSAP-21.0.1
accuracy corpus gauge is unchanged at 78.8% within-0.5 / MAE 0.625 (correctly —
all 7 corpus certs lodging code 2 are houses).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 21:24:25 +00:00
Khalim Conn-Kowlessar
42ed6f8e46 Persist the explicit basement-wall determination 🟩
RdSAP 10 §5.17 / Table 23. `wall_is_basement` is not a lodged U-value — it is a
disambiguation flag selecting WHICH RdSAP default applies, so unlike
wall/roof/floor_u_value it must survive the round-trip.

Code 6 is canonically SYSTEM-BUILT and the gov-API path infers "basement" from a
code-6 heuristic; the Elmhurst site-notes mapper sets wall_is_basement=False
precisely to defeat it. Dropping the field inverted the answer — a system-built
wall came back as a basement wall, dragging the whole ground floor onto the
Table 23 basement-floor U-value via `has_basement`.

Declared on no RdSAP schema, so it never arrives via the gov API; the earlier
corpus scan showing 0 occurrences measured the wrong population. The Elmhurst
site-notes path sets it and those EPCs are persisted
(scripts/wchg_elmhurst_ingest.py).

Nullable is load-bearing: None = "not stated, use the heuristic", False =
"explicitly system-built, do not".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:58:26 +00:00
Khalim Conn-Kowlessar
d867838330 Persist the measured at-rafters roof insulation thickness 🟩
Add `rafter_insulation_thickness` to `EpcBuildingPartModel` (JSONB, matching the
sibling thickness columns so Union[str, int] survives) and wire it through
`from_domain` / `_to_building_part`.

Property 749719 lodges "250mm" -> Table 16 col (2) U 0.23. Dropped, the roof
billed at U 2.30 — a 10x error worth -9.8 SAP (lodged 73, calculated 63.2) that
moved the dwelling a whole band, C -> D, and drew a 9.63-point loft-insulation
recommendation on a roof rated 4/5 by the assessor.

The sibling wall/roof/floor_u_value and wall_is_basement fields have the same
drop but are deliberately NOT persisted: they are full-SAP artefacts and we
re-model as RdSAP, which uses default U-values. Their allow-list entries are
re-justified rather than removed — the previous "not read by the calculator" was
stale and is what hid this drop.

Requires the Drizzle migration in Hestia-Homes/assessment-model to land FIRST;
without the column, EPC reads fail outright.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:53:17 +00:00
Khalim Conn-Kowlessar
d2168d518f Persist the measured at-rafters roof insulation thickness 🟥
RdSAP 10 §5.11.2 — a roof insulated at rafters lodges its thickness in
`rafter_insulation_thickness`, not `roof_insulation_thickness`. The field had no
DB column, so it was mapped from the gov-EPC API and silently dropped on save;
every at-rafters roof then fell back to the Table 18 col (2) unknown-thickness
default (2.30 W/m²K for bands A-D — the spec's *uninsulated* value).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:53:16 +00:00
KhalimCK
1dd9a9a472
Merge pull request #1657 from Hestia-Homes/fix/optimiser-band-floor-rounding
Judge band membership on the published rating; assert modelling/baseline coherence
2026-07-21 18:03:25 +01:00
Khalim Conn-Kowlessar
a80d6cad05 Re-pin the trigger-attributes fixture to one that still buys lighting 🟪
0390's floor+ASHP package reaches post 68.772, which publishes as 69 = band C,
so under ADR-0064 it now meets the goal and stops before low-energy lighting —
the same over-purchase the ADR removes, one level up at the package. 0380 fires
the same three measures with identical trigger attributes, so every assertion
stands unchanged, and it lands at post 74.4, clear of a band floor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:57:22 +00:00
Khalim Conn-Kowlessar
223ef7c196 Report missing baselines once per batch, and seed the harness's own 🟩
The modelling_e2e handler models one property per run_modelling call, so a
per-property "no persisted baseline" line would be one per property across a
whole re-model. The harness has no Baseline stage by design and cannot drift
from one, so it now establishes its own the way the Baseline orchestrator
would; the Postgres-backed lane is where the assertion has something to catch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:46:25 +00:00
Khalim Conn-Kowlessar
29157811e0 Renumber the baseline-coherence ADR to 0066 🟪
ADR-0065 is claimed by Scenario Export on the open #1546; this branch is the
later of the two, so it moves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:38:49 +00:00
Khalim Conn-Kowlessar
735a3a1c8e Record the banding and baseline-coherence decisions 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:04:49 +00:00
Khalim Conn-Kowlessar
31be36cca5 Flag works bought for no real SAP gain 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:57:25 +00:00
Khalim Conn-Kowlessar
08d2b874d5 Flag works bought for no real SAP gain 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:53:59 +00:00
Khalim Conn-Kowlessar
8c2c19495c Flag a modelling baseline that disagrees with the persisted effective 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:53:04 +00:00
Khalim Conn-Kowlessar
31edd01999 Flag a modelling baseline that disagrees with the persisted effective 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:50:39 +00:00
Khalim Conn-Kowlessar
6f83418c4c Type-clean the anomaly check tests 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:18:09 +00:00
Khalim Conn-Kowlessar
68ffb0c47c Rank costed works on an already-compliant dwelling HIGH 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:12:58 +00:00
Khalim Conn-Kowlessar
0df40278e5 Rank costed works on an already-compliant dwelling HIGH 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:12:33 +00:00
Khalim Conn-Kowlessar
27060f4d81 Band every continuous SAP score through the published-rating rule 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:11:06 +00:00
Khalim Conn-Kowlessar
07a71ad504 Band a continuous SAP score on its published rating 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:07:55 +00:00
Khalim Conn-Kowlessar
08abd3131b Band a continuous SAP score on its published rating 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:07:11 +00:00
Khalim Conn-Kowlessar
bc2f6c8a82 Sell no works to a dwelling already at the goal band 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:05:55 +00:00
Khalim Conn-Kowlessar
8548b8304b Sell no works to a dwelling already at the goal band 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:04:48 +00:00
Khalim Conn-Kowlessar
31ad9448d2 Locate a band's floor on the continuous SAP scale 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:03:54 +00:00
Khalim Conn-Kowlessar
1e4526f33f Locate a band's floor on the continuous SAP scale 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:03:25 +00:00
Khalim Conn-Kowlessar
149d957248 Publish a half-point SAP rating rounded up 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:02:44 +00:00
Khalim Conn-Kowlessar
f55982a4ee Publish a half-point SAP rating rounded up 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 10:00:35 +00:00
KhalimCK
0fdb15ad65
Merge pull request #1645 from Hestia-Homes/fix/terraced-rir-gable-party
Reclassify terraced room-in-roof gables as party, not external
2026-07-20 12:14:08 +01:00
KhalimCK
08dc81332c
Merge pull request #1650 from Hestia-Homes/fix/pashub-sitenotes-boiler-thermostat-gable
PasHub Wythenshawe: fix 6 from_site_notes defects + re-pin harness to DB oracle (#1649)
2026-07-20 12:04:16 +01:00
Daniel Roth
dc96cf5aba
Merge pull request #1648 from Hestia-Homes/feature/abri-api-resource-field
Sync third_party_surveyor_identifier deal field from HubSpot
2026-07-20 11:02:11 +01:00
Khalim Conn-Kowlessar
26e0584855 Record Elmhurst-validation provenance finding (#1649)
Two outputs of validating the Wythenshawe cohort against accredited Elmhurst:

1. Harness note: the DB-oracle residual tail is not engine defects. The two
   worst under-raters, 6 Barry Road (ours 60.12, oracle 62) and 87 Alderue
   (ours 61.80, oracle 63), were re-keyed into accredited Elmhurst on our exact
   inputs — Elmhurst returned 60 and 62, matching our engine and both below the
   lodged oracle. So our engine computes correct RdSAP; the lodged certs carried
   a lighter input set (dropped the surveyed 17.4 m2 solid-brick alternative
   wall, +1.7 SAP). It's input-provenance drift, not a calculation gap — don't
   chase these as engine bugs or tune the engine to close them. Corrects the
   earlier "spec-literal alt-wall" note.

2. epc-to-elmhurst skill gotcha: give Elmhurst the RAW surveyed room height, not
   the engine's storey height. The mapper already adds +0.25 m to upper floors
   (_UPPER_FLOOR_HEIGHT_ADD_M), so feeding sap_floor_dimensions.room_height_m
   makes Elmhurst double-add the joist void (~1 SAP too low — it under-scored
   6 Barry 60->59 until the first-floor height was corrected 2.76 -> raw 2.51).

Comment/doc only; no behaviour change (harness still 89.2%/0.297, pyright clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 09:16:54 +00:00
Daniel Roth
d6226f8232 add docs for Ops 2026-07-20 08:54:12 +00:00
Daniel Roth
9c0bddd14d Add the Abri HubSpot ops guide, covering surveyor-change amendments
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:38:05 +00:00
Daniel Roth
1e38b44a9a A blank stored surveyor also counts as unset in the amendment check 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:37:33 +00:00
Daniel Roth
91dc359e0d A blank stored surveyor also counts as unset in the amendment check 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:36:50 +00:00
Daniel Roth
b4ce0ae83f An unchanged surveyor on a booked deal fires no amendment 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:36:05 +00:00
Daniel Roth
137ebd9456 Treat a blank surveyor as unset when checking for booking amendments 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:35:25 +00:00
Daniel Roth
234270a1ca Treat a blank surveyor as unset when checking for booking amendments 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:35:02 +00:00
Daniel Roth
26e2a98034 Amend the Abri booking when the deal's surveyor changes 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 08:34:43 +00:00