Fabric spend comes out of the shared budget before phase 2 🟩

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 <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-09 11:41:22 +00:00
parent 7f4d313932
commit 48ce18dbb6

View file

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