4.1 KiB
| 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
scenariotable (FE-owned Drizzle schema:fabric_first boolean NOT NULL DEFAULT false), mirrored inScenarioModeland the domainScenario. 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_packageover 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, betweenoptimise_packageandoptimise_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.