Commit graph

7999 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
471728db0a Forced ventilation is injected once across both fabric-first phases 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:42:54 +00:00
Khalim Conn-Kowlessar
0220bee87c Forced ventilation is injected once across both fabric-first phases 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:41:27 +00:00
Khalim Conn-Kowlessar
adf60b82b1 Fabric unpicked in phase 1 can re-enter phase 2 on post-fabric worth 🟩
Documents behaviour already delivered: phase 2 optimises every group phase 1
did not consume, with signals re-scored against the fabric-applied dwelling,
so glazing skipped on raw-baseline merit re-enters when it closes the target.
Test passed on arrival.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:41:22 +00:00
Khalim Conn-Kowlessar
48ce18dbb6 Fabric spend comes out of the shared budget before phase 2 🟩
Documents behaviour already delivered by the phase-2 structure: the leftover
budget is the envelope minus the committed fabric cost, so a heating system
the plain optimiser would buy can be priced out. Test passed on arrival.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:41:22 +00:00
Khalim Conn-Kowlessar
7f4d313932 Phase-2 candidates are valued against the post-fabric dwelling 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:40:34 +00:00
Khalim Conn-Kowlessar
705c86d5e6 Phase-2 candidates are valued against the post-fabric dwelling 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:40:29 +00:00
Khalim Conn-Kowlessar
e652780873 Fabric short of the target is topped up with non-fabric measures 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:39:21 +00:00
Khalim Conn-Kowlessar
e880ae868c Fabric short of the target is topped up with non-fabric measures 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:36:43 +00:00
Khalim Conn-Kowlessar
9ad2c2edfd Fabric-first scenario stops at fabric when the target is met 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:36:27 +00:00
Khalim Conn-Kowlessar
fcf46263bc Fabric-first scenario stops at fabric when the target is met 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:36:02 +00:00
Daniel Roth
1287cb1cf1
Merge pull request #1523 from Hestia-Homes/feature/abri-api-integration
Abri api integration: add missing reference to hubspot deal client booking reference
2026-07-09 11:39:23 +01:00
Daniel Roth
2a8878a0fb add missing reference to hubspot deal client booking reference 2026-07-09 10:32:34 +00:00
Daniel Roth
d235ce9f7d Merge branch 'main' into feature/abri-api-integration 2026-07-09 10:26:51 +00:00
Daniel Roth
274932b2b9
Merge pull request #1521 from Hestia-Homes/fix/hubspot-etl-import-error
Add utilities to hubspot etl dockerfile
2026-07-08 17:53:03 +01:00
Daniel Roth
4bf85375e7 add utilities to hubspot etl dockerfile 2026-07-08 16:49:55 +00:00
Daniel Roth
2c83c977e6
Merge pull request #1519 from Hestia-Homes/feature/abri-api-integration
Add logging to logic for deciding whether abri API should be triggered
2026-07-08 17:18:13 +01:00
Daniel Roth
a065857e48 more detailed logging 2026-07-08 16:06:41 +00:00
Daniel Roth
0306b43584 logging for debug purposes 2026-07-08 16:02:33 +00:00
Jun-te Kim
bd3ff8fcdc
Merge pull request #1513 from Hestia-Homes/fix/widen-cohort-search-v2
Widen EPC-prediction cohort search one step when the normal radius is too sparse
2026-07-08 16:17:57 +01:00
Jun-te Kim
8a04e9fca9
Merge pull request #1515 from Hestia-Homes/feature/hubspot-planning-fields
Add planning/address-profiling fields to HubSpot deal ETL sync
2026-07-08 16:14:01 +01:00
Jun-te Kim
04d1df1343 Add remaining planning/address-profiling fields to HubSpot ETL sync
Sync Design Constraints, Planning Comments, Planning Status, and
Planning Suggested Approach, matching the columns added in
Hestia-Homes/assessment-model#377.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 15:12:24 +00:00
KhalimCK
7f4fe43d6d
Merge pull request #1517 from Hestia-Homes/fix/bulk-download-worker-observability
Bulk download: readable email, deal-name folders, visible worker logs
2026-07-08 16:08:14 +01:00
Khalim Conn-Kowlessar
91db972d48 Bulk download: readable email, deal-name folders, visible worker logs
First successful live run surfaced three issues:

1. Email looked rubbish (a giant raw presigned URL). Now sends a proper HTML
   email with a 'Download documents' button plus a plain-text fallback, and a
   summary (N documents across M properties, expiry). Email delivery is now
   best-effort: a transport failure no longer loses an already-built package
   (the link is still on sub_task.outputs), and the SMTP connect has a 30s
   timeout so an unreachable SES endpoint fails fast instead of hanging to the
   900s Lambda timeout.

2. Every folder was 'address unavailable (...)': the resolver read property.address,
   but these are HubSpot deals with no property row. It now uses the deal's
   dealname from hubspot_deal_data.

3. No logs / no idea why a run took ~9 minutes: the worker's INFO logs were
   dropped (Lambda root logger defaults to WARNING). The handler now raises the
   level, and the orchestrator logs per-phase timing and volume (gather+plan,
   packaged N files / X MB, upload, email, total) so the slow phase is visible.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:02:38 +00:00
Daniel Roth
d7648fd5e0
Merge pull request #1514 from Hestia-Homes/feature/abri-api-integration
Map Hubspot outcomes to their Abri abandonment reason codes
2026-07-08 15:46:28 +01:00
Jun-te Kim
b8316418e1 Add planning-authority HubSpot deal properties to ETL sync
Sync Planning Authority, Designated Area, Article 4 PD Rights, and
Listed Building from HubSpot into hubspot_deal_data so downstream
consumers of the existing HubSpot ETL process can read them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 14:45:03 +00:00
Daniel Roth
31fc7ee115 use unsuccesseful outcoems constant directly in hubspot deal differ 🟪 2026-07-08 14:37:15 +00:00
Daniel Roth
4ac4dd8683 Collapse cancelled/no show to HubSpot's single 'Cancelled / No Show' value
'Cancelled' and 'no show' were separate strings in the outcome vocabulary,
but HubSpot only ever emits the one dropdown value 'Cancelled / No Show' -
so neither phantom string could ever match. Replace both with the real
value (mapped to NOACCESS, best-guess pending client), which also fixes the
long-standing differ mismatch: the abandonment trigger now actually fires on
Cancelled / No Show. Drops the now-unused REQT enum member.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:33:59 +00:00
Jun-te Kim
c9e0180c12 Widen EPC-prediction cohort search one step when the normal radius is too sparse
ADR-0034's nearby-postcode broadening degrades to a genuine
NoSameTypeComparablesError whenever the default 1000m/30-postcode reach has no
same-type comparable nearby (e.g. property_id=752685, portfolio 824 — the only
Maisonette within reach). Adds one configurable extra widening step
(EpcComparablePropertiesRepository.candidates_near now accepts
widen_nearby_postcodes), tried only when the normal-radius walk falls short of
`minimum` matches. modelling_e2e's handler wires this to a 3000m/60-postcode
PostcodesIoClient as the single wider step.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 14:29:21 +00:00
Daniel Roth
9a669dfcea Map every unsuccessful outcome to a reason code (best-guess, client to confirm)
Complete the outcome-to-code table so all five UNSUCCESSFUL_OUTCOMES map:
cancelled -> REQT (new enum member) and no show -> NOACCESS join the
existing three. Only no answer -> CARDED is firm; the rest are best-guesses
for client confirmation, noted inline with alternatives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:28:47 +00:00
Jun-te Kim
a76e49e133
Merge pull request #1507 from Hestia-Homes/worktree-rdsap-glazing-cascade-fix
Cascade raw API glazing_type on lodged sap_windows (was passed through raw)
2026-07-08 15:27:18 +01:00
Daniel Roth
13e3e78749 Share the unsuccessful-outcome vocabulary between differ and abandonment
Extract the abandonment outcome strings to a single UnsuccessfulOutcome
Literal + UNSUCCESSFUL_OUTCOMES tuple in domain.abri.models, alongside
SlotCode. The differ's NEGATIVE_OUTCOMES now references the tuple and the
reason-code mapping is keyed on the Literal, so the two can no longer
drift and a mistyped outcome fails type-checking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:23:33 +00:00
Daniel Roth
693356a220 Map Abri abandonment outcomes to their reason codes
Replace the CARDED placeholder in abandon_reason_for_outcome with the
real outcome-to-code table from the client: No Answer->CARDED,
Tenant Refusal->CRA (ambiguous, client follow-up pending),
Not Viable->NOACCESS. Only these three outcomes trip the abandonment
trigger today; any empty/unrecognised outcome raises
UnmappableOutcomeError since abandon_reason is mandatory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 14:14:06 +00:00
KhalimCK
3f596aecf7
Merge pull request #1510 from Hestia-Homes/fix/bulk-download-match-via-hubspot-deal-id
Bulk download: match uploaded_files via hubspot_deal_id bridge + worker observability
2026-07-08 15:13:18 +01:00
Jun-te Kim
f0bd5b9674
Merge pull request #1503 from Hestia-Homes/worktree-rdsap-glazing-bug
Derive multiple_glazed_proportion for 16.x certs with explicit glazing wording
2026-07-08 15:06:33 +01:00
Khalim Conn-Kowlessar
26857f8df7 Bulk download: match uploaded_files via hubspot_deal_id bridge + add worker observability
A live test returned 'no documents could be packaged' for 91 correctly-resolved
properties. Root cause: no upload source populates uploaded_files.landlord_property_id
(pashub/magic-plan/audit set hubspot_deal_id; ECMK sets hubspot_listing_id), so
matching on landlord_property_id found zero rows.

Match fix: the worker now joins uploaded_files -> hubspot_deal_data (on deal_id) ->
landlord_property_id, taking the property identity from the bridge. The repository
returns a small PropertyDocument read-model instead of the infra ORM row, so the
orchestrator no longer names infrastructure.postgres.* (resolves the leak dancafc
flagged) and the s3_upload_timestamp cast is gone. Coverage is limited to
deal-id-linked sources; listing_id/uprn-only files are a noted follow-up.

Observability: the empty-selection failure now carries stage counts
(selected/matched/planned/skipped) in both the message (-> the worker WARNING log)
and details (-> sub_task.outputs), and the run logs those counts. The next failure
says 'matched 0 documents' instead of failing opaquely.

ADR-0060 + CONTEXT.md updated (matching decision + considered options).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 13:58:46 +00:00
Daniel Roth
dd54c33c0e
Merge pull request #1508 from Hestia-Homes/feature/abri-api-integration
Correct reference to abri api tf state bucket in hubspot etl lambda
2026-07-08 14:16:09 +01:00
Daniel Roth
358fa1b01d correct reference to abri api tf state bucket in hubspot etl lambda 2026-07-08 13:14:07 +00:00
Jun-te Kim
41ded4740e Cascade raw API glazing_type on lodged sap_windows, not pass through raw
datatypes/epc/domain/mapper.py's from_rdsap_schema_{17_0,17_1,18_0,19_0,
20_0_0,21_0_0} all stored a lodged sap_windows[].glazing_type verbatim
instead of routing it through _api_cascade_glazing_type (which
_api_sap_window, used by 21_0_1, already does correctly). The raw API code
is the RdSAP-21 glazing enum (e.g. code 1 = "DG pre-2002"), not the SAP
10.2 Table 6b cascade enum the calculator's U/g-value tables are keyed on
(cascade code 1 = single glazed) -- so any lodged window whose raw code
happened to collide with a different cascade meaning was silently
mis-rated.

Found while validating PR #1503 (property 753950 / uprn 100021969385)
against Elmhurst: our engine's windows_w_per_k was 82.27 vs Elmhurst's
51.44 -- the dwelling's larger window (91% of glazed area) lodges raw
glazing_type=1 and was being modelled as single glazed instead of double,
the dominant contributor to a SAP 42 vs Elmhurst 44 gap. Fixing this
narrows windows_w_per_k to 50.26 (Elmhurst 51.44, ~2% off) and moves the
engine's score to 43.

Extracted the 5 duplicated reduced-schema-window blocks (17.0/17.1/18.0/
19.0/20.0.0, which lodge a minimal window shape with no glazing_gap/
frame_factor/transmission fields) into a shared _reduced_field_api_sap_
window helper that also now populates window_transmission_details/
frame_factor via the SAP10 lookup instead of leaving them None. 21.0.0's
richer inline block gets the same one-line cascade fix.

+6 regression tests (one per affected schema seam + 21.0.0). Full domain/
schema/prediction/comparable-properties/sap10_calculator/modelling_e2e
suites pass (2746 passed, only the 2 pre-existing unrelated failures);
component-accuracy gate 26/26 unaffected.
2026-07-08 13:05:43 +00:00
Daniel Roth
312e5e3339
Merge pull request #1505 from Hestia-Homes/feature/abri-api-integration
Terraform fix for hubspot etl
2026-07-08 13:51:00 +01:00
KhalimCK
5bd580f432
Merge pull request #1504 from Hestia-Homes/fix/bulk-download-select-by-landlord-property-id
Bulk download: hand-pick selection by landlord_property_id, not property.id
2026-07-08 13:49:26 +01:00
Daniel Roth
399e00606f tf correction for hubspot etl 2026-07-08 12:48:10 +00:00
Jun-te Kim
95322ac4ad Add Elmhurst build script + validated result for uprn 100021969385
Built the property-753950 cert (worklist P4, PR #1503) in the live RdSAP-10
Online tool via Playwright: SAP 44 (E), vs our engine's 42 and the lodged
2014 cert's 45 — a normal RdSAP-2012->SAP-10.2 recalculation spread, not a
defect surfaced by the multiple_glazed_proportion fix.

Also surfaced (and documented as a caveat, not fixed here) a pre-existing
SAP-16.2 gap: the cert lodges party_wall_length_m=0 on both floors despite
being semi-detached, which Elmhurst's validation rejects and this PR's
engine change doesn't touch.
2026-07-08 12:38:26 +00:00
Khalim Conn-Kowlessar
3c9b75cbce Hand-pick selection by landlord_property_id, not property.id
uploaded_files has no property_id — matching is on landlord_property_id — so the
property_ids path was pointless indirection (look property.id up in property just
to translate back to landlord_property_id). The FE holds landlord_property_id per
row anyway.

task.inputs hand-pick key is now landlord_property_ids: str[] (was property_ids:
int[]). resolve_selection unions two landlord_property_id sources — project_codes
expanded via hubspot_deal_data, and the hand-picked ids taken as given — and no
longer queries the property table at all. Trigger-only change; the domain plan,
orchestrator and matching already work in landlord_property_id.

ADR-0060, CONTEXT.md and the request schema updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 12:04:35 +00:00
Jun-te Kim
430ac9eaa4 Derive multiple_glazed_proportion for 16.x certs with explicit glazing wording
Property 753950 (uprn 100021969385, cert 0141-2860-6891-9124-5625,
SAP-Schema-16.2) hard-failed modelling_e2e: RdSapSchema17_1 requires
multiple_glazed_proportion as a non-optional int, and this 16.x cert omits
the field entirely, lodging only the multiple_glazing_type="ND" sentinel.

The cert's own window.description ("Fully double glazed") states the
glazing extent unambiguously, so _normalize_sap_schema_16_x now derives
0/100 from "single"/"double" wording when the field is absent, mirroring
the existing single-glazed multiple_glazing_type cascade. This is
worklist P4 (.claude/skills/expand-sap-accuracy-corpus/worklist.md) —
a flat default was tried previously and reverted because making such
certs mappable at all pulls them into the EPC-prediction donor pool and
tips near-tie similarity matches; deriving from explicit text (rather
than a blind default) was the suggested unblock.

Re-measured the component-accuracy gate as the worklist asked: it drops
(has_hot_water_cylinder 0.8687->0.8586, cylinder_insulation_type
0.3333->0.1667, door_count residual 0.3131->0.3333) via the same
donor-pool-composition mechanism as the prior #1245/ADR-0037
re-baselines, not a prediction-logic loosening. Re-baselined the floors
with that rationale recorded inline.
2026-07-08 11:53:51 +00:00
KhalimCK
22db9c43f3
Merge pull request #1497 from Hestia-Homes/feature/small-roof-solar-min-array
Sub-Ladder PV Configurations: small roofs get the array their caps allow (ADR-0058)
2026-07-08 12:50:41 +01:00
KhalimCK
bd24a789b9
Merge pull request #1498 from Hestia-Homes/feature/bulk-document-download
Bulk Document Download: emailed ZIP of a property set's documents (ADR-0059/0060)
2026-07-08 12:49:46 +01:00
Khalim Conn-Kowlessar
90fe2914e0 Select documents by HubSpot project_code(s), unioned with hand-picked property_ids
The 'all properties' selection resolved against property.portfolio_id, but a
portfolio spans multiple HubSpot projects — so 'all' pulled the whole portfolio,
not the project the user was looking at. The project↔property grain lives on
hubspot_deal_data, not property.

task.inputs is now {project_codes?: str[], property_ids?: int[], portfolio_id?}:
the route resolves the distinct landlord_property_id set as the union of every
property in the named project_codes (from hubspot_deal_data) and the hand-picked
property_ids; portfolio_id is optional and only names the package. Drops the
portfolio-scoped select_all. Cap now applies to the resolved set size.

ADR-0060, CONTEXT.md and the request schema updated to match. New router tests
cover project-code resolution, the union+dedup, null landlord_property_id drop,
and the empty-project rejection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 11:42:17 +00:00
Daniel Roth
86e6b83e57
Merge pull request #1500 from Hestia-Homes/feature/abri-api-integration
Abri API integration: temporary project code variable for testing
2026-07-08 12:30:10 +01:00
Khalim Conn-Kowlessar
9ec7987e97 PR review: best-effort read path, true per-member streaming, race-safe 409, local email 🟩
Addresses PR #1498 review:
- A per-file read failure (missing/deleted object, or a bucket the role can't
  reach) is now a SkippedDocument(reason=unreadable), not a whole-run abort;
  an all-unreadable selection still fails (no empty package). Best-effort on
  the read path (ADR-0060).
- Each ZIP member is streamed to a temp file (S3DocumentDownloader.download,
  boto download_file) and added from disk, so a single multi-GB member never
  hits the heap — the 'never held whole in memory' claim is now true.
- The 409 double-submit guard is DB-arbitrated: the sub_task insert is
  conditional on the task having none, so a race creates only one.
- Local env gets a placeholder recipient email so the route is exercisable.
- PackageEntry carries landlord_property_id so a read failure can be reported.
- TODO noting the UploadedFile.s3_upload_timestamp typing fix.
Two new tests: per-file read failure skips-and-reports; address fallback flows
to the folder name.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 11:08:46 +00:00
Khalim Conn-Kowlessar
0b52a28808 Read the property selection from task.inputs; route takes only task_id 🟩
Per PR review: the FE writes the selection config
({portfolio_id, property_ids?, select_all?}) into the FE-owned task.inputs and
passes only task_id, so a large hand-picked selection never travels in an HTTP
body. The route reads task.inputs, resolves to landlord_property_ids, caps, and
pins the recipe onto sub_task.inputs as before. Declares the FE-owned inputs
column on the TaskRow mirror so the backend can read it and the test schema
builds it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 10:41:14 +00:00