"Homes Without an EPC" and "Expired EPCs" were both derived from estimatedSql,
which asks "was this picture gap-filled?". That conflates two different questions:
provenance (was it modelled?) and coverage (does a certificate exist?).
They used to agree, because a gap-fill implied no certificate. Model ADR-0054 ends
that: the gov EPC API only serves certs registered since 2012, so a pre-2012
dwelling is gap-filled even though a real, stale certificate exists — and gets the
new source='expired'. On portfolio 824, 375 of the 403 "without an EPC" homes have
exactly that: a real certificate, just an out-of-date one. A different, and far more
actionable, problem than never having been certified.
Two defects fell out of the same conflation:
- Every query matched source='predicted' literally, and `source` is plain text
with no enum or CHECK. An 'expired' row matches neither the lodged nor the
predicted alias, so the home would have read as having a VALID CURRENT cert —
silently gone from both cards, no Estimated badge, nothing to catch it.
- isExpiredSql read epl.registration_date alone, but 714 of 824's 4,873 lodged
certs carry only inspection_date. All 714 counted as current; 170 are in fact
>10 years old. lodgementDateSql already coalesced the two; expiry didn't.
The cards now partition on coverage: withoutEpcSql = no lodged AND no historical
record; isExpiredSql = a cert of either kind, out of date. estimatedSql keeps its
own meaning and spans the whole predicted slot, so an expired home is estimated AND
has an EPC — both true. The `AND estimated = false` guard is gone from the Expired
card: an expired home IS estimated, so it excluded the very homes being counted.
Legacy portfolios are untouched (632: 8/1662 before and after). 824 moves to
913 → 1,083 expired immediately (the inspection_date fix), then to 25 / 1,461 once
Model writes expired rows. Safe to deploy BEFORE Model — the predicates just find
none. The reverse was not safe, which is why this goes first.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Captures the grill-with-docs outcome: flat app-owned Tags, four assignment
paths (incl. in-app bulk upload, no FastAPI), tags as a Run-filter key extending
ADR-0008, hard-delete + cascade, reporting-by-tag deferred to the reporting
redesign. Design only — no implementation yet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Flat, portfolio-owned, many-to-many label for grouping Properties for bulk
actions. Captured during the tagging grill-with-docs session.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Updates the portfolio Property-table flagged-ambiguity note for the two changes
in the previous commit: Main Fuel now reads epc_main_heating_detail beneath the
override (no longer override-only for new-approach), and Wall/Roof/Heating are
now filterable via coarse prefix/substring buckets while still displaying the
full free-text description.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Records the decision to make the portfolio property-list override-aware for
Property Type / Built Form / Construction Age / Main Fuel, extending ADR-0008's
override → EPC-derived → Unknown precedence to a second surface via shared
epcSources fragments. Adds a CONTEXT.md Flagged-ambiguities note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds the fabric-first constraint to the app-authored scenario flow:
- Create: a "Fabric first" toggle in the Measures step (Step 2) with an
explanation, mirrored in the Step 3 review and carried through the
"Use as template" (?from=) flow.
- List card: a "Fabric first" chip in the constraints box, shown only
when the scenario is fabric-first.
- Detail page: an always-shown "Fabric first" config row (chip +
explanation, or "Off").
Domain layer (TDD): validateScenarioConfig carries the flag and defaults
it to false; scenarioToInsertRow persists it; findExactDuplicate treats
it as part of the immutable config identity, so two scenarios differing
only in fabric-first are distinct. Threaded through ScenarioListItem,
both query mappers, the create route, and the new-scenario loader.
Backend already reads scenario.fabric_first for id-based modelling runs,
so no dispatch changes are needed.
Docs: adds a "Fabric first" glossary entry to CONTEXT.md, softens the
stale "only measure-constraint" line, and pins the distinction from the
"Fabric only" quick-start.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add bulk document download as a row-selection flow on the Live Reporting
Documents tab: a "Bulk download" button flips the existing DocumentTable
into selection mode (per-row tickers + select-all across all filtered
pages); "Download selected" zips those properties' documents and emails a
link (shown in-app while the page stays open). Scoped by the tab's
existing Project/group selector.
Two-step trigger (ADR-0008 convention): insert an app-owned tasks row
(task_source "app:bulk_document_download", selection config JSON in
tasks.inputs, source_id = portfolio) then dispatch only { task_id } to
POST /v1/documents/bulk-download. Documents are matched by
landlord_property_id, so the ticked ids ARE the selection — a property
with no landlord id has no documents and is un-tickable.
Resilience: the client now parses responses defensively (a platform 504's
HTML body no longer surfaces as "Unexpected token 'A'…"), and the POST
route sets maxDuration=60 so the synchronous backend resolution has
headroom before the browser gets a 504.
Docs: CONTEXT.md gains Project + Bulk document download; ADR-0011 records
why the marker lives in task_source (worker re-reads the task) vs service.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gross/Net cost, Project delivery, Current stock, Likely downgrade/upgrade
(band movement) added to CONTEXT.md from the reporting-redesign grilling
session. ADR-0010 records the compliance-window filter as a report-view
parameter, not a Scenario constraint. Also commits the previously
untracked ADR-0001/0002 docs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Amends ADR-0008 after challenge: the Modelling run reuses the BulkUpload
trigger convention (app inserts the tasks row + config subtask with inputs
JSON, passes task_id to the distributor) instead of a dedicated
modelling_run table. No migration; status and progress come from the task
system by construction; dispatch failure marks the task failed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Grilling-session outcomes for the bulk-trigger-modelling feature: the
ModellingRun/Run-filter glossary entries and the decision record — insert-only
run rows, filters (not ids) to the distributor lambda, backend-owned filter
resolution with a shared-code-path count endpoint, run_id correlation into
task records for derived in-flight state, warn-don't-block concurrency.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Left uncommitted by the previous session; restores the "### Building parts"
heading it had accidentally swallowed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Internal wall and floor insulation (and room-in-roof) involve major
internal works, and landlords often exclude them. Encode that domain
knowledge once as DISRUPTIVE_MEASURES in the scenario model (with
plain-language reasons; guarded by a key-validity test) and surface it
three ways:
- measures step: allowed disruptive chips carry an amber "disruptive"
tag + amber border, with the reason in the tooltip; a legend explains
the marking
- "Low disruption" quick-start now excludes exactly this set, so the
preset and the badges cannot drift apart
- review step: an amber nudge lists any disruptive measures still
allowed before saving
New CONTEXT.md term: Disruptive measure.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Users could only create scenarios buried inside the CSV-upload modal, at
trigger time. This adds a first-class Scenarios tab: a gallery of the
portfolio's scenarios, a read-only detail page, and a 3-step creation
journey (goal → measures → review) that saves app-authored scenario rows
per ADR-0003.
Domain rules (TDD'd in src/lib/scenarios/model.test.ts, 19 tests):
- goal semantics: "Increasing EPC" requires a target band; all other
goals are maximise-within-budget with goal_value forced null
- exclusions-only measure model: normalised (dedupe+sort), unknown keys
rejected, all-excluded rejected, empty set valid; serialised to the
backend's brace text format (parses the legacy space-padded variant)
- insert mapping: multi_plan=true, is_default=false, budget per property
- exact-config duplicate detection ignoring name (review-step warning)
- status derived from plan existence — no stored status column
Queries follow the ADR's discipline: gallery status = one grouped
COUNT(DISTINCT property_id) per portfolio; detail = one scoped count
(index-only, powers the blocked-delete message); delete = single atomic
DELETE guarded on NOT EXISTS(plans). Rename is the only other mutation —
configuration is immutable.
Verified live on portfolio 814: create (400 on missing band, 201 on CO₂
draft), derived Awaiting/Modelled in the gallery, rename, delete of the
draft (200) and blocked delete of the modelled scenario (409, "338
properties…" matching the portfolio's true count).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR-0002: Effective performance is the canonical "current" for display and
reporting; lodged shown alongside (supersedes the mid-migration "reporting
stays lodged" note). CONTEXT.md gains EPC-provenance and provenance-signal
terms, and flags that the UI "Current EPC" surfaces show Effective.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 0222-0225 batch backfilled plan_id/material_* onto recommendation
inside drizzle's single migration transaction. On the production table
(~26M rows) that held an AccessExclusiveLock for hours, blocked unrelated
migrations, exhausted EBS throughput credits, and could not report
progress or resume.
Split schema DDL from data backfill:
- 0222/0224: keep only instant metadata-only DDL (ADD COLUMN + FK
NOT VALID); drop the inline CREATE INDEX.
- 0223/0225: backfills removed (now no-ops pointing to the script).
- New src/app/db/backfill-recommendation-denormalization.ts: idempotent,
resumable, batched (committed per batch) backfill that then builds the
indexes CONCURRENTLY and validates the FKs online.
- CONTEXT.md: document recommendation->plan (1:1) and recommendation->
material (now at most 1; legacy multi-material rows reconciled).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>