From 48ce18dbb6083c65b15b01b62865860dbeaf85ff Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 11:41:22 +0000 Subject: [PATCH] =?UTF-8?q?Fabric=20spend=20comes=20out=20of=20the=20share?= =?UTF-8?q?d=20budget=20before=20phase=202=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents behaviour already delivered by the phase-2 structure: the leftover budget is the envelope minus the committed fabric cost, so a heating system the plain optimiser would buy can be priced out. Test passed on arrival. Co-Authored-By: Claude Fable 5 --- .../modelling/test_optimiser_fabric_first.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/domain/modelling/test_optimiser_fabric_first.py b/tests/domain/modelling/test_optimiser_fabric_first.py index f345d6e75..eeffd181b 100644 --- a/tests/domain/modelling/test_optimiser_fabric_first.py +++ b/tests/domain/modelling/test_optimiser_fabric_first.py @@ -150,6 +150,30 @@ def test_fabric_short_of_target_is_topped_up_with_non_fabric_measures() -> None: assert abs(package.score.sap_continuous - 85.0) <= 1e-9 +def test_fabric_spend_comes_out_of_the_shared_budget_before_phase_two() -> None: + # Arrange — the £8000 heat pump alone would fit the £8500 budget and reach + # the target, but fabric first commits the £1000 wall first, leaving £7500: + # the heat pump no longer fits. Fabric priority wins over the target. + groups: list[list[ScoredOption]] = [ + [_scored("cavity_wall_insulation", gain=5.0, cost=1000.0, overlay=_WALL_OVERLAY)], + [_scored("air_source_heat_pump", gain=20.0, cost=8000.0, overlay=_HEATING_OVERLAY)], + ] + scorer = _StubScorer(base=60.0, wall=5.0, roof=0.0, heating=20.0) + + # Act — target 78 (gain 18). + package: OptimisedPackage = optimise_package_fabric_first( + groups=groups, + scorer=scorer, + baseline_epc=build_epc(), + budget=8500.0, + target_sap=78.0, + ) + + # Assert — wall only; the target is missed rather than the fabric skipped. + assert _selected_types(package) == {"cavity_wall_insulation"} + assert abs(package.score.sap_continuous - 65.0) <= 1e-9 + + class _InteractionScorer: """A stub whose boiler gain collapses once the wall is insulated (+10 raw, +3 post-fabric) while the heat pump's holds (+8 either way) — so a phase 2