Record the fabric-first two-phase decision as ADR-0061

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-09 11:45:56 +00:00
parent 4d7434a954
commit 38206c227b

View file

@ -0,0 +1,73 @@
---
status: accepted (extends ADR-0016)
---
# Fabric First is a two-phase optimisation with strict envelope priority
Landlords with a fabric-first retrofit policy require the building envelope —
insulation and windows — to be treated before heating systems and renewables
are considered. The legacy engine carried this as `enforce_fabric_first` on
the plan-API request body (`funding_optimiser.optimise_with_scenarios`): an
optimiser pass over fabric-only measures with the full budget, then a second
pass over the remainder with the leftover budget and the residual target,
summing approximate per-measure SAP points. The new engine needed the same
capability on the truthful-re-score Optimiser core (ADR-0016).
Decided in a grilling session with Khalim, 2026-07-09.
## Decision
**`fabric_first` is a Scenario attribute, and the Optimiser honours it as two
sequential `optimise_package` phases in which the envelope has strict first
claim on the budget** (`optimise_package_fabric_first`, ADR-0016 core reused
per phase).
- **The flag lives on the `scenario` table** (FE-owned Drizzle schema:
`fabric_first boolean NOT NULL DEFAULT false`), mirrored in `ScenarioModel`
and the domain `Scenario`. A Plan's provenance stays derivable from its
Scenario row alone — unlike the legacy request-body flag, the same
scenario_id cannot produce differently-constrained plans. **Deploy order**:
the Drizzle migration must land before this mirror, or every scenario read
crashes on the missing column.
- **Fabric = the building envelope** (`FABRIC_MEASURE_TYPES`): wall (CWI /
EWI / IWI), roof (loft / sloping-ceiling / flat-roof), floor (suspended /
solid) insulation, plus double / secondary glazing — the legacy list
exactly. Lighting, tune-ups, secondary-heating removal, heating and solar
all wait for phase 2. Mechanical ventilation is unclassified: it is never
selected, only injected as a forced Measure Dependency (ADR-0016) of the
fabric that triggers it, in whichever phase that happens — and only once.
- **Phase 1** runs `optimise_package` over the fabric groups with the full
budget: least-cost-to-target, repair, max-gain fallback. If the truthful
post-fabric score meets the Scenario target, the package is fabric-only —
surplus budget is left unspent, per the ADR-0016 no-overshoot doctrine.
- **Phase 2** (target unmet, or no target) optimises every group phase 1 did
not consume — non-fabric, plus fabric groups it left unpicked, which may
re-enter on their post-fabric worth — under the leftover budget. Candidates
are valued **against the fabric-applied dwelling**: warm-start signals are
re-scored and every package re-score is prefixed with the phase-1 overlays
(`_PrefixedScorer`), so a heating system whose worth changes once the
envelope is treated is re-ranked truthfully, and the returned Score remains
the whole-package figure against the true baseline (bills and the role-3
cascade stay honest).
- **Strict priority, not target-aware compromise**: phase 1 commits the
max-gain fabric package even when a cheaper fabric/heating split would have
reached the target — a £4,000 budget buys floor insulation and leaves the
£3,200 boiler unaffordable, and the target is missed rather than the fabric
skipped. This is the landlord's explicit trade.
- **Every goal honours the flag**, not just Increasing EPC: with no SAP
target the two phases are both max-gain, so fabric still gets first claim
on the budget.
- **No Plan-contract change**: the Scenario row is the provenance; the
best-practice cascade already orders fabric before heating in the persisted
measures.
## Consequences
- The Modelling orchestrator branches once, on `scenario.fabric_first`,
between `optimise_package` and `optimise_package_fabric_first`; everything
downstream (attribution, bills, persistence) is unchanged.
- Phase 2 re-scores each remaining Option once against the post-fabric
dwelling — a handful of extra calculator calls per fabric-first Plan.
- A fabric-first Plan can undershoot a target a plain Plan would have reached
within the same budget. This is by design and should be communicated when
scenario results are compared.