"""Scenario — the named retrofit brief the Modelling stage scores against. Built by a user in the scenario-builder UI and persisted before any modelling fires; the pipeline is handed only its id and reads it back via a `ScenarioRepository`. This is the thin slice the Modelling stage uses today: the goal + budget that the Optimiser will consume (#1160) and `is_default` (which drives `plan.is_default`). The legacy file-path / portfolio-aggregate columns are not modelled. Carries no phases — multi-phase is deferred (ADR-0005). See CONTEXT.md. """ from dataclasses import dataclass from typing import Optional from domain.modelling.measure_type import MeasureType _NO_EXCLUSIONS: frozenset[MeasureType] = frozenset() @dataclass(frozen=True) class Scenario: """A retrofit brief: its goal, optional budget, and whether it is the Property's default Scenario. `goal` / `goal_value` are the lodged target (e.g. "INCREASING_EPC" → band "C"); carried for the Optimiser, not yet enforced. `exclusions` are the measure types the brief bars from the run (the only measure-scoping the live ``scenario`` table persists — there is no inclusions column). Empty means nothing is barred.""" id: int goal: str goal_value: str budget: Optional[float] is_default: bool exclusions: frozenset[MeasureType] = _NO_EXCLUSIONS def considered_measures(self) -> Optional[frozenset[MeasureType]]: """The measure-type allowlist the Scenario's exclusions imply: every modelled measure minus the excluded ones, or None (consider every measure) when nothing is excluded.""" if not self.exclusions: return None return frozenset(MeasureType) - self.exclusions