Model/docs/adr/0061-fabric-first-is-a-two-phase-optimisation.md
Khalim Conn-Kowlessar 38206c227b Record the fabric-first two-phase decision as ADR-0061
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 11:45:56 +00:00

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 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.