ResolvedPropertyOverride now carries original_spreadsheet_description (populated
by both postgres readers). overlays_from builds the main-heating overlay directly
via _main_heating_overlay, resolving the band: an explicit boiler_efficiency_band
row wins, else parse-on-read from the heating description (ships before FE). Band
applied after the ADR-0067 fuel re-point. 28 overlay tests green, pyright clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse-on-read from the main_heating_system row's original_spreadsheet_description
(ships before FE); an explicit boiler_efficiency_band override row wins when
present; band applied after fuel resolution (oil re-point); no band -> no slot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_main_heating_detail_efficiency uses the override winter (§206) and the Eq D1
water branch uses the override (winter,summer), both ahead of the PCDB/Table 4b
defaults — the same precedence the PCDB winter_efficiency_pct occupies. Guarded
on WHC 901 + not-electric-immersion for the water leg. Override-supplied only, so
the lodged-cert corpus is unmoved (ADR-0068). 256 calculator tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The override-supplied seasonal efficiency must take precedence in both the §206
winter path (main_heating_efficiency) and the Appendix D Eq D1 summer water path,
the same precedence slot the PCDB winter_efficiency_pct occupies (ADR-0068).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C-and-below reuse accredited Table 4b pairs; A/B pending Elmhurst (no slot yet,
no invented numbers); non-banded boilers (solid/electric/CPSU) ignore the band.
The base code is unchanged; only the efficiency slot is set (ADR-0068).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BoilerEfficiencyBand (A-G + UNKNOWN sentinel) and boiler_efficiency_band_guard,
the shared deterministic parser for the fifth Heating Companion (ADR-0068).
UNKNOWN is never persisted; absence of a band -> no value -> Table 4b default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Boiler Efficiency Band is the 5th Heating Companion (ADR-0068). The shared
parser extracts the SEDBUK A-G band from the Landlord 'Heating' description
(`Boiler: <A-G> rated ...`), primary-system-1 on multi-system cells, None
when absent. Single source of truth for read/backfill/classifier.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The two new `SapHeating` fields were mapped from the API but dropped by
`EpcPostgresRepository._compose`, which the ADR-0036 structural guard caught.
Allow-listing them would have been wrong. `PropertyBaselineOrchestrator.run`
(applications/modelling_e2e/handler.py:885) always re-derives the persisted
baseline from the composed object, so an unpersisted field means the deployed
baseline keeps pricing heat-pump DHW as direct electric — the Dovestone cert
would still land at SAP 67 even with the mapper fix in place. Several plan
paths read the stored EPC too (refetch_epc=False, no lodged cert, stored cert
newer, stored survey wins).
Round-trip fidelity also matters for churn: `_reconcile_lodged` compares
`fetched != stored`, so a field that survives the API mapper but not `_compose`
makes that inequality permanently true and re-saves every public cert forever.
Needs the matching Drizzle columns in assessment-model before deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A faithfully-built Elmhurst RdSAP10 (AP50 3.2, MVHR unit 500167, phantom
secondary + open-flue cleared) reproduces the engine EXACTLY: worksheet 80 =
engine 80, fabric heat loss 41.2 vs 41.6 W/K. The earlier 74 was Elmhurst
data-entry fidelity (four artefacts), not calculator error. Refresh the saved
worksheet/summary PDFs to the SAP-80 build; pin unchanged at 80.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
Root-caused the 2675-vs-1839 kWh communal-DHW delta: the engine correctly
applies the SAP §4.3 distribution-loss factor (1/(1.05×1.50)); the difference is
the DHW energy content (64) alone. The engine's 1698 kWh tracks the cert's own
lodged RHI water heating (1909) far better than Elmhurst's reduced-data 1168,
and its £113 is closer to the cert's lodged £147 than Elmhurst's £78 — so the
engine is more faithful to the cert here, not buggy. Comment accuracy only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
The dwelling has no hot-water cylinder (has_hot_water_cylinder false), so the
2675-vs-1839 kWh DHW-energy gap is not a cylinder storage loss as previously
stated — it is an un-root-caused SAP §4.3 community-DHW demand/distribution-loss
difference. No behaviour change; comment accuracy only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
Full-SAP SAP-Schema-18.0.0 cert 9556-3047-3304-5355-1200 (Dovestone Gardens
Apt 4): communal heat-pump DHW + direct-electric room heaters. Was 67 (D) with
DHW mis-billed as direct-electric immersion; now 80 with DHW on the community
heat-pump fuel. Accredited Elmhurst worksheet 74 (evidence saved); the +6 is the
documented full-SAP measured-U/MVHR vs RdSAP age-band residual.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
A water-only community scheme (water_heating_code 950/951/952) is served
by its own heat network, independent of the space main. _water_heating_fuel_code
now resolves the network's SAP Table 12 community fuel (e.g. 41 = heat from
electric heat pump @ 4.24 p/kWh, COP already priced into the row) instead of
defaulting to the space main's fuel. For a direct-electric main this had
mis-billed communal heat-pump DHW at the 13.19p standard-electric rate.
Dovestone Apt 4 (cert 9556-3047-3304-5355-1200): hot-water cost £353 → £113,
SAP 67 → 80.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
Water-only community hot water (water_heating_code 950/951/952,
community_heating_use 2) carries its own heat source in
sap_community_heating_systems, independent of the space main. Expose the
network's heat-fraction-weighted efficiency (COP×100) + Table-12 fuel on
SapHeating so the calculator can price DHW on the network instead of
dropping it to a direct-electric immersion cylinder.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DX8oAGsGkBHq3U4dsxYRzz
v6.57.0 (released 29 July 2026) breaks ordinary reads across unrelated AWS
services — SSM GetParameter returns SerializationException, IAM GetPolicy
returns a 302, ECR returns InvalidSignatureException — with no config
change. Reported upstream as hashicorp/terraform-provider-aws#49170, open
with no root cause identified; 6.56.0 is confirmed good.
Every stack declared `>= 5.0` with no upper bound and no lock files are
committed, so each CI run silently resolved whatever HashiCorp had shipped
most recently. That is how a provider released today broke a pipeline
nobody had touched. Pinning exactly makes the deployed version a reviewed
decision rather than a discovery.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
An unrelated in-progress edit that was already in the working tree when
this branch's work started, swept in by a bare `git add -A`. It belongs to
its author, not to this PR.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
partial binds plan eagerly and without introducing a parameter the caller
never passes, so there is no late-binding question to reason about.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Green on arrival — rename() records rejections rather than raising.
Pinned after verifying it bites: letting the rejection escape fails the
child sub_task, which turns the whole Task red.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Properties are planned and renamed one at a time, so only those with work
get a row and a run killed by the timeout still leaves every property it
reached both renamed and recorded.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The walk now yields a PropertyPlan per address-list row, and rename()
applies one. run() composes the two, so single-pass callers are unchanged.
Knowing a property's size before the first rename is what makes "when is
this property finished?" answerable.
Tests that reached through the old private _process_folder are rewritten
against the public run(), so they survive this kind of change.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The local invoke now sends an SQS-shaped payload — a direct invoke is the
one shape that cannot reproduce the dry_run bug class. Test modules route
protected-method calls through typed helpers so both pass pyright strict.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The task lane pulls the task domain types, both task repositories and four
Postgres modules into the image, plus SQLAlchemy, SQLModel and a driver.
Terraform gains the DB credentials block and five Postgres env vars; the
deploy job gains the three DB secrets the shared workflow already declares.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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