Green on arrival: both fall out of the summary the run now returns and of
the guarded site lookup. Pinned after verifying they bite — an unguarded
DomnaSites[name] completes an "ECO" run that renames files under a
different site's alias.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Green on arrival: @task_handler records inputs and reads source_id from
the body key matching the source's value. Pinned because it is the only
place the new sharepoint_site member is proven against a real Postgres
enum rather than a Python one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Green on arrival: reading the request from the SQS record body is what
taking the task lane gave us. Verified the guard bites by restoring the
raw-event read, under which the "dry" run renames a live file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Passed on arrival: threading the summary back up through the traversal
already carried subfolder results. Pinned so the merge cannot regress.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Six properties failed task 4d006fab (portfolio 854 / scenario 1334) with
DegeneratePredictionError. None has a lodged EPC, so each was predicted from a
neighbouring-cert cohort — and every cohort cert lodges its single building part
as `identifier: "complete"`. from_api_string accepted only "Main Dwelling" /
"Main building", so "complete" fell to OTHER, _has_main_part returned False, and
prediction refused to proceed.
"complete" is what SAP 9.91/9.92-era software writes for an unextended dwelling:
semantically identical to "Main Dwelling". Casing varies within a single postcode
(cert 8824-7422-1180-6934-1902 lodges "complete", 199 Highfield Road "Complete"),
so matching is now case-folded.
The API's `identifier` is free text with no schema enum — api.yml declares the
whole cert body `additionalProperties: true` and documents no field of it, and
`identifier` is absent from the 17 vocabularies at /api/codes. So the recognised
set can only grow by discovery, and each miss has cost an incident ("Main
building" → task a40e71c4; "complete" → task 4d006fab). Hence three changes, not
one:
- "complete" → MAIN, case-insensitively.
- Bare "Extension" → EXTENSION_1. The regex required a digit, so 668 corpus
occurrences silently dropped a real extension from the structure. This reverses
a prior deliberate pin whose stated rationale (RdSAP10 §1.2's 4-extension cap)
does not apply — there is no out-of-range number in a bare "Extension".
- _anchor_lone_building_part: a cert lodging exactly ONE part describes the whole
dwelling whatever it is called, so an unclassified lone part becomes MAIN.
Multi-part certs are untouched — there the identifier carries real information
and guessing would invent structure the cert does not state.
Also removes a false claim from DegeneratePredictionError's message: it asserted
the template was "lodged with a null part identifier", which was hardcoded, never
checked against a real cert, and wrong. It misdirected this investigation.
Verified against the live gov API: all 13 cohort certs across BL1 8EB, BL3 6XJ
and BL4 0RA now resolve a MAIN part. The full modelling e2e was NOT run (no local
AWS creds for the geospatial lookup) — this clears the blocking error, it does not
prove the predictions are good.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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
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
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
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>
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>
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>
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>
A Plan became default only when its Scenario was flagged default or the
property had no Plan yet. Scenarios are created is_default=False, so in
practice only the first-ever Plan was ever promoted — re-modelling a
property appended a new Plan but left the default frozen on the first run,
so a re-model never surfaced on the FE (property 792168 kept its 2026-07-22
scenario-1328 default through three same-scenario re-runs).
Also promote when the re-run models the SAME scenario that currently owns
the property's default, so a re-run refreshes the shown Plan in place (the
save demotes the stale one). A re-run of a different scenario still does not
hijack the default. New PlanRepository.default_plan_scenario_by_property
surfaces each property's default scenario for the decision.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>