From 6296b2d8fc0fdb35f29a3e5974f68093d79ad601 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 9 Jul 2026 12:59:46 +0000 Subject: [PATCH] =?UTF-8?q?A=20goal-aligned=20scenario=20without=20a=20bud?= =?UTF-8?q?get=20fails=20loudly=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 --- orchestration/modelling_orchestrator.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/orchestration/modelling_orchestrator.py b/orchestration/modelling_orchestrator.py index 111023694..102199f25 100644 --- a/orchestration/modelling_orchestrator.py +++ b/orchestration/modelling_orchestrator.py @@ -182,6 +182,7 @@ class ModellingOrchestrator: # The Optimiser speaks the goal's currency (ADR-0062): group signals, # dependency pricing and repair marginals are all measured by this # objective — SAP by default, carbon reduction for a Reducing-CO2 goal. + _require_budget_for_goal_aligned(scenario) objective: Callable[[Score], float] = _objective_for(scenario, bill_derivation) groups: list[list[ScoredOption]] = _scored_candidate_groups( scorer, @@ -437,6 +438,20 @@ def _scored_candidate_groups( return groups +def _require_budget_for_goal_aligned(scenario: Scenario) -> None: + """A goal-aligned Scenario is 'reduce as much as possible within this + budget' — undefined without one (unconstrained, it would recommend every + beneficial measure). Fail the misconfiguration loudly (ADR-0062).""" + if scenario.budget is None and scenario.goal in ( + _REDUCING_CO2_GOAL, + _ENERGY_SAVINGS_GOAL, + ): + raise ValueError( + f"scenario {scenario.id} has goal {scenario.goal!r} but no budget; " + "goal-aligned scenarios require a budget" + ) + + def _carbon_reduction(score: Score) -> float: """The Reducing-CO2 objective: annual kg CO2 below zero-point, negated so higher is better (a saved kg scores +1)."""