Commit graph

1049 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
6f8724aa04 Route sloping-ceiling roof descriptions deterministically off the loft ladder 🟩
Compose the party-ceiling and sloping-ceiling guards in the roof
GuardedColumnClassifier so a PitchedWithSlopingCeiling override resolves to the
Pitched, sloping ceiling family instead of being routed onto Pitched, N mm loft
insulation by the LLM (ADR-0066).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:53:56 +00:00
Khalim Conn-Kowlessar
82ac8c341b Compose the party-ceiling and sloping-ceiling roof guards ahead of the LLM 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:50:13 +00:00
Khalim Conn-Kowlessar
02a71feae1 Resolve a measured sloping-ceiling depth to its ladder member 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:48:50 +00:00
Khalim Conn-Kowlessar
870cb04631 Resolve a depthless sloping-ceiling marker to as built 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:47:11 +00:00
Khalim Conn-Kowlessar
6603d98c2c Resolve a depthless sloping-ceiling marker to as built 🟥
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:46:49 +00:00
Khalim Conn-Kowlessar
c01f48d4fd Carry the sloping-ceiling insulation depth into the overlay 🟪
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:38:00 +00:00
Khalim Conn-Kowlessar
e384e37f5f Carry the sloping-ceiling insulation depth into the overlay 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:26:48 +00:00
Khalim Conn-Kowlessar
9cccf83000 Defer an as-built sloping-ceiling override to the age-band default 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:25:37 +00:00
Khalim Conn-Kowlessar
bec6e17347 Recognise the pitched sloping-ceiling descriptions as roof types 🟩
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:24:28 +00:00
Khalim Conn-Kowlessar
35a0c6e972 Source export fabric from structured SAP fields; fix stale-cert selection
The "latest EPC per UPRN" selection ordered by e.id DESC, but row-id does not
track recency: a gov-EPC cert re-ingested after a PasHub survey lands with a
higher e.id, so the header/perf reads silently picked the stale gov cert (wrong
TFA, lodgement, property_type, current band) on 3 of portfolio 838. Order by the
effective lodgement date (registration -> completion -> inspection) DESC NULLS
LAST, e.id DESC tiebreak.

Compose the descriptive fabric from the structured SAP fields a re-survey lodges
(no gov-EPC prose): walls/roof/floor from the main epc_building_part, heating and
hot water from epc_main_heating_detail + the water-heating codes, decoded with
the SAP engine's own code space (fabric_description.py). Structured fabric wins
where present and falls back to the gov-EPC prose otherwise; windows and lighting
stay on prose (their per-element codes do not reconstruct the dwelling phrase).

Add built_form (epc_property, code-decoded) and property_age_band (building part
construction_age_band -> RdSAP date range) columns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 19:38:12 +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
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
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
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
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
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
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
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