Commit graph

8080 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
5dab7beae5 Map the default plan's selected measures onto the export row 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:29:26 +00:00
Khalim Conn-Kowlessar
14763638e2 Lock model-A scoping: exclude properties without a default plan 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:27:39 +00:00
Khalim Conn-Kowlessar
1bb1e605e2 Read a scenario's default-plan properties as export rows 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:26:51 +00:00
Khalim Conn-Kowlessar
d70c1b65cc Read a scenario's default-plan properties as export rows 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:26:17 +00:00
Khalim Conn-Kowlessar
65fb4fabad Extract per-Property row construction behind a shape helper 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:16:27 +00:00
Khalim Conn-Kowlessar
9c1e79efc6 Carry Property identity and Effective-EPC fields onto the export row 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:15:40 +00:00
Khalim Conn-Kowlessar
6fc94b1cc4 Carry Property identity and Effective-EPC fields onto the export row 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:15:18 +00:00
Khalim Conn-Kowlessar
c2aca6d2e6 Carry the Plan's post-works figures onto the export row 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:14:41 +00:00
Khalim Conn-Kowlessar
6a6ffb695c Carry the Plan's post-works figures onto the export row 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:14:13 +00:00
Khalim Conn-Kowlessar
91c88743f7 Pivot solar PV with a battery to its own column 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:13:22 +00:00
Khalim Conn-Kowlessar
f758216bf1 Pivot solar PV with a battery to its own column 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:12:51 +00:00
Khalim Conn-Kowlessar
a6eaac5fe3 Total each Property's retrofit cost from its measure costs 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:12:23 +00:00
Khalim Conn-Kowlessar
df768d882e Total each Property's retrofit cost from its measure costs 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:11:59 +00:00
Khalim Conn-Kowlessar
b52b38e6f3 Roll up each Property's SAP points and savings across its measures 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:11:36 +00:00
Khalim Conn-Kowlessar
478ee20aaa Roll up each Property's SAP points and savings across its measures 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:11:01 +00:00
Khalim Conn-Kowlessar
23ff1e73a8 Guarantee every measure column on every scenario sheet 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:10:16 +00:00
Khalim Conn-Kowlessar
04be6b57a2 Guarantee every measure column on every scenario sheet 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:09:40 +00:00
Khalim Conn-Kowlessar
c4e1cc4229 Pivot a Property's measure cost onto its scenario-sheet column 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:09:00 +00:00
Khalim Conn-Kowlessar
1a0e87e593 Pivot a Property's measure cost onto its scenario-sheet column 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:08:26 +00:00
Khalim Conn-Kowlessar
01e8b8c289 Record the Scenario Export design as ADR-0065 with glossary terms 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:08:26 +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
Jun-te Kim
64abeb9066 Map SAP-Schema-15.0's "Main building" identifier to MAIN
Properties 741843/741872 (uprns 100060714155/100021944594) failed
modelling_e2e subtasks 6ac7841a-9338-4efe-97e5-7e0d19b3055d and
c5450f03-5b0d-4068-a3b3-d1470bc0af57 with a bare StopIteration: their
SAP-Schema-15.0 (2011-era LIG-lodged) certs identify the main dwelling's
building part as "Main building" rather than "Main Dwelling", so
from_api_string fell to OTHER and left the property with no MAIN part —
crashing wall_recommendation.py's unguarded next(...).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 15:03:28 +00:00
Daniel Roth
0a7b2c1d66
Merge pull request #1538 from Hestia-Homes/feature/sitenotes-mapping-stores-uprn
Persist resolved UPRN on PasHub site-notes rows (#1537)
2026-07-10 14:38:35 +01:00
Daniel Roth
b5c05166db Persist resolved UPRN on PasHub site-notes rows (#1537)
Thread the UPRN the PashubService has already resolved for a job into the
PasHub site-notes mapper so the EpcPropertyData aggregate is born with its
uprn set, and the existing save_epc_property_data path persists it.

- EpcPropertyDataMapper.from_site_notes gains uprn: Optional[int] = None and
  sets it unconditionally (site notes never carry a UPRN natively).
- parse_site_notes_pdf / _parse_pashub forward the uprn; the Elmhurst branch
  is untouched.
- PashubService coerces uprn str -> int in one place, carries it on the
  internal upload record, and passes it into parse_site_notes_pdf.
- No new lookups; jobs with no known UPRN still persist null, unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 13:19:28 +00:00
Jun-te Kim
086294fe36
Merge pull request #1535 from Hestia-Homes/verify/sap15-uprn-100010344955
Fix two SAP-15/16.x reduced-field mapper gaps blocking portfolio 824 modelling
2026-07-10 13:04:07 +01:00
Jun-te Kim
c0b4fd1e6a Fix has_hot_water_cylinder derivation to lodge the string convention, not a bool
The prior commit derived a Python bool (`cylinder_size != 1`), but
`from_rdsap_schema_17_1` reads the field via `schema.has_hot_water_cylinder
== "true"` — every real 15.0/16.x fixture lodges the lowercase string
"true"/"false", not a JSON boolean. A bare bool compares False against
that string check either way, so the derivation silently mapped every
cylinder-present cert to has_hot_water_cylinder=False too.

Caught by the requested true-branch test (mutating sap_16_2.json, which
lodges cylinder_size=2/has_hot_water_cylinder="true", to omit the field)
— it failed under the original bool-typed fix. Now emits "true"/"false"
strings to match the lodged convention.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 11:58:07 +00:00
Jun-te Kim
4f9bec7a4a Derive has_hot_water_cylinder from cylinder_size when the 16.x-family cert omits it
Task b9fcd354-2a33-4fc4-a64e-388c060a055c (portfolio 824 / scenario 1278)
also failed property_id 749229 (UPRN 100010349400) with "RdSapSchema17_1:
missing required field 'has_hot_water_cylinder'". Cert
9568-3034-6211-6089-5960 (SAP-Schema-15.0) lodges `sap_heating.cylinder_size`
but omits the separate top-level `has_hot_water_cylinder` boolean
RdSapSchema17_1 also requires.

Unlike `multiple_glazed_proportion` (deliberately left un-defaulted a few
lines above — a prior guessed default regressed the accuracy gate),
`cylinder_size` isn't a proxy needing inference: RdSAP 10 Table 28 defines
code 1 as "no cylinder" — literally the same fact `has_hot_water_cylinder`
encodes. Confirmed 1:1 across every real 15.0/16.x fixture that lodges both
fields (cylinder_size == 1 <-> false, every other code <-> true).

Added to the shared `_normalize_sap_schema_16_x`, so it covers the whole
15.0/16.0/16.1/16.2/16.3 reduced-field family, not just 15.0.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 11:48:12 +00:00
KhalimCK
660e42b920
Merge pull request #1526 from Hestia-Homes/feature/fabric-first-scenario
Fabric-first scenario constraint: two-phase optimisation (ADR-0061)
2026-07-10 12:48:06 +01:00
Jun-te Kim
121b3d991d
Merge pull request #1533 from Hestia-Homes/fix/epc-schema-allowlist
Unsupported EPC schema versions skip with a warning instead of crashing
2026-07-10 12:43:40 +01:00
Daniel Roth
d72f800be8
Merge pull request #1534 from Hestia-Homes/feature/abri-api-integration
Abri: always send a bookable resource on log/amend (+ survey-time labels)
2026-07-10 12:39:53 +01:00
KhalimCK
8b01d2518c
Merge pull request #1527 from Hestia-Homes/feature/goal-aligned-objectives
Goal-aligned optimiser objectives: CO2 and bill £ (ADR-0062)
2026-07-10 12:35:07 +01:00
Jun-te Kim
2851a0151b Drop phantom alternative-wall building parts lodged inline (reinstates dropped fix)
Task b9fcd354-2a33-4fc4-a64e-388c060a055c (portfolio 824 / scenario 1278)
still failed 5 of 128 batches with NotNullViolation on
epc_building_part.construction_age_band even after the SAP-Schema-15.0
mapper landed (PR #1531). Cert 8169-6926-5310-0447-8996 (UPRN
100010344955, SAP-Schema-15.0) lodges a bare alternative-wall record
(wall_area/wall_construction/wall_insulation_type, no identifier, age
band, or floor dimensions) as a second sap_building_parts element rather
than under a distinct sap_alternative_wall_1 key. RdSapSchema17_1 (which
the 15.0/16.x reduced-field mappers delegate to) has no alt-wall slot to
route it to, so it persisted as a phantom building part with a null age
band.

_drop_building_parts_without_age_band already solved exactly this shape
of problem for lodging artifacts (ae81a81e), but that commit sits on
fix/drop-building-part-without-age-band, an unmerged branch that has
since drifted far behind main. Reinstating the same filter (fresh, not
cherry-picked) fixes both the original artifact case and this SAP-15
alt-wall case, since RdSapSchema17_1 has nowhere else to put it either
way.

Verified against all 174 properties across the task's originally-failing
batches: zero remaining null-age-band building parts.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 11:34:17 +00:00
Jun-te Kim
b1467a6b2f Guard build_property_report against None from unmapped-schema certs
from_api_response now returns None for an unsupported schema_type
instead of raising, so the harness must check before dereferencing
the mapped EpcPropertyData — otherwise an AttributeError replaces
the expected ValueError in calculator_error.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-10 11:34:12 +00:00
Khalim Conn-Kowlessar
7996eedf55 Goal-objective test uses the shared fixtures and a real boiler code 🟪
Review findings on PR #1527:

- The overlay constants, ScoredOption builder, ventilation dependency and
  selected_types helper come from the shared _optimiser_fixtures module
  (landed on the fabric-first base) instead of local copies; the boiler
  overlay is the shared BOILER_OVERLAY (SAP Table 4a code 104, a mains-gas
  combi) rather than code 201, which is neither a boiler nor a heat pump.
  _IWI_OVERLAY (solid-wall internal, type 3) stays local — no shared
  equivalent — and the carbon stubs stay bespoke (the shared StubScorer has
  no CO2 knob).
- The optimise_package_fabric_first import is lifted to module scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:17:27 +00:00
Khalim Conn-Kowlessar
98d2f7aa16 Goal-aligned dispatch reads one enum-keyed table 🟪
Review findings on PR #1527:

- The 'which goals are goal-aligned' set lived in two adjacent if-chains
  (_objective_for and _require_budget_for_goal_aligned) that had to stay in
  sync — a new goal-aligned goal added to one but not the other would slip
  the budget guard. Both now read a single _GOAL_ALIGNED_OBJECTIVES table.
- The goal strings are the canonical PortfolioGoal enum values, not
  re-declared string constants, so goal-value drift can't silently degrade
  a goal to max-SAP; _target_sap reads the enum too.
- _scored_candidate_groups takes objective without a default (its only
  caller passes it).
- scoring.py: 'cached: float | None' -> Optional[float] per the CLAUDE.md
  'Use Optional over | None' rule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:17:27 +00:00
Khalim Conn-Kowlessar
daf4449d0d Optimiser objective is required on the private helpers, hoisted in repair 🟪
Review findings on PR #1527:

- The objective is required (no sap_rating default) on _repair_to_target,
  _best_repair_candidate and _rescored_groups: every caller already passes
  it, and a default would let a future call path silently optimise SAP for
  a carbon/bill goal while pyright stayed green. The default stays on the
  public optimise_package / optimise_package_fabric_first entry points.
- _best_repair_candidate hoists objective(current) out of the candidate
  loop: current is loop-invariant, so for the Energy-Savings bill objective
  this was one full BillDerivation.derive per candidate per repair iteration
  for the same score.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:17:27 +00:00
Jun-te Kim
b540cf1ca2 Unsupported EPC schema versions are skipped with a warning, not a crash
An unrecognised schema_type used to raise ValueError and abort the whole
call; now from_api_response logs a warning and returns None so one
unmapped cert doesn't break a batch. Schema 15.0 already has a mapper
(PR #1531) so it's unaffected; only genuinely unmapped versions skip.
2026-07-10 11:17:07 +00:00
Khalim Conn-Kowlessar
b71bc788a2 Record goal-aligned Optimiser objectives as ADR-0062
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:11:07 +00:00
Khalim Conn-Kowlessar
c70f6730a3 Remove the superseded role-1 impacts scorer; signals carry the objective currency 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:11:07 +00:00
Khalim Conn-Kowlessar
e329c50fa3 Fabric-first phase 2 re-scores candidates in the goal objective's currency 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:11:06 +00:00
Khalim Conn-Kowlessar
ffaf89935b Fabric-first phase 2 re-scores candidates in the goal objective's currency 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Khalim Conn-Kowlessar
6296b2d8fc A goal-aligned scenario without a budget fails loudly 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Khalim Conn-Kowlessar
102b250e53 A goal-aligned scenario without a budget fails loudly 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Khalim Conn-Kowlessar
3576d05370 An Energy-Savings scenario prices packages at the live fuel rates 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Khalim Conn-Kowlessar
6b64104dc5 An Energy-Savings scenario prices packages at the live fuel rates 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00