Commit graph

1030 commits

Author SHA1 Message Date
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
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
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
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
48d54675c3 A Reducing-CO2 scenario maximises carbon reduction, not SAP 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Khalim Conn-Kowlessar
66748ba26d Dependency signals are priced in the goal objective's currency 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:10:06 +00:00
Daniel Roth
208113ae6d send AM or PM instead of trying to parse morning or afternoon 2026-07-10 10:45:57 +00:00
Khalim Conn-Kowlessar
b37ee071b7 Fabric-first skips the phase-2 re-score when it cannot change the plan 🟪
Review findings on PR #1526, behaviour-preserving:

- No fabric groups, or phase 1 committed nothing → delegate to the plain
  optimise_package: the phase-2 prefix would be empty and its re-scoring
  would reproduce the role-1 signals the groups already carry, one full
  SAP-calculator run per Option for zero effect (the common case on
  already-insulated stock).
- Phase 1 consumed every group → return the fabric package instead of a
  phase-2 pass that can only select the empty package.
- _rescored_groups takes start_sap from the caller: the post-fabric
  baseline score is the phase-1 package score already in hand, not a
  fresh calculator run.
- fabric_types → phase_one_types: the set holds everything phase 1
  committed, injected dependencies included — that inclusion is what the
  outstanding-dependencies filter relies on, so the name must not invite
  narrowing it to FABRIC_MEASURE_TYPES.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:29:45 +00:00
Daniel Roth
7997d88375 minor updates following Abri feedback 2026-07-09 16:27:50 +00:00
Khalim Conn-Kowlessar
4d7434a954 Hoist consumed-group lookup out of the phase-2 comprehension 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:45:56 +00:00
Khalim Conn-Kowlessar
caa0847b70 Scenario carries its Fabric First flag from the scenario table 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:44:31 +00:00
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
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
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
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
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
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
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
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
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
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
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
b89b9fe944 Name the packaged file by Document Type with the original extension 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:07:25 +00:00
Khalim Conn-Kowlessar
bd928828e3 Skip and report a document with no Document Type 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:06:26 +00:00
Khalim Conn-Kowlessar
810e910961 Keep only the latest file of a Document Type in each property's folder 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:04:58 +00:00
Khalim Conn-Kowlessar
28b7b43372 Keep only the latest file of a Document Type in each property's folder 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:04:19 +00:00
Daniel Roth
84d0e1e0d5 Date an abandonment to the deal's own dates, not now() 🟩
date_abandoned now resolves to the third failed attempt's confirmed
survey date, falling back to its last submission date, rather than the
processing date. This dates the OpenHousing cancellation to when the
job actually lapsed even if the trigger message lags or redelivers.

- domain: abandonment_date() encodes the confirmed-survey -> last-
  submission fallback
- DealAbandonment carries both dates; abandon_job raises
  AbandonmentDateUnknownError when a deal has neither
- last_submission_date now flows through the trigger message and request
- the injected clock is dropped: nothing reads now() any more

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 08:51:39 +00:00
Khalim Conn-Kowlessar
fbedf4541b A small roof derives Sub-Ladder rungs below Google's smallest config 🟪
North-drop now runs once: the selection computes the usable (north-dropped)
ladder, feasibility filters it, and Sub-Ladder derivation consumes it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:10:37 +00:00
Khalim Conn-Kowlessar
c1a8c27bc1 Sub-Ladder rungs fill from the highest-yield segment first 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:06:30 +00:00
Khalim Conn-Kowlessar
1a34e788d1 A small roof derives Sub-Ladder rungs below Google's smallest config 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:05:37 +00:00
Daniel Roth
4c79fb3c8e update comment 2026-07-07 16:30:36 +00:00
Daniel Roth
d884edd456 Map a HubSpot outcome to an abandonment reason code 🟩
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:38:31 +00:00
Daniel Roth
2512d428a1 Map a HubSpot outcome to an abandonment reason code 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:38:10 +00:00
Daniel Roth
c206a76b22 Abandon a job through the relay canceljob route 🟥
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 15:30:24 +00:00
KhalimCK
9ff4de0563
Merge pull request #1481 from Hestia-Homes/feature/modelling-trigger-run
Modelling Run Distributor: POST /v1/modelling/trigger-run
2026-07-07 14:36:45 +01:00
Khalim Conn-Kowlessar
cc4bf4394e Both postcode batchers share one group-preserving packing core 🟪
Review feedback (#1481): the address batcher and the Modelling Run batcher
implemented the same greedy packing; the core moves to
utilities/grouped_batching.py and both become thin wrappers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 13:24:02 +00:00
Khalim Conn-Kowlessar
c64bd8fd85 A recorded batch failure carries structured details onto the sub_task outputs 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:50:10 +00:00
Khalim Conn-Kowlessar
8a3c0f3e77 A recorded batch failure carries structured details onto the sub_task outputs 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:49:56 +00:00
Khalim Conn-Kowlessar
30e3054146 A failed batch sub_task restarts for a re-run 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:08:47 +00:00
Khalim Conn-Kowlessar
5b60717133 A run with finished and queued batches rolls up in progress, not waiting 🟩
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 11:04:50 +00:00
Jun-te Kim
bc1cca77db
Merge pull request #1356 from Hestia-Homes/feature/historic-epc-repository
Historic EPC repository: DDD port + resolver for address→UPRN
2026-07-07 10:56:33 +01:00
Daniel Roth
6335796d6e
Merge pull request #1471 from Hestia-Homes/feature/abri-api-integration
Abri OpenHousing DomnaRelay — GetTenantData vertical (tenancy signatories to HubSpot)
2026-07-06 16:15:36 +01:00
Daniel Roth
06354a166e Phone-number selection policy lives in the domain layer 🟪
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:47:37 +00:00
Daniel Roth
10636c1752 Runner-up number fills the contact's secondary phone number 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:28:56 +00:00
Daniel Roth
78fcba45e5 Abri tenancy signatory becomes an associated HubSpot deal contact 🟥
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:22:52 +00:00