Skip to main content

sim_lib_standard_core/
scenario.rs

1//! Bounded, explicitly authorized characterization scenario execution.
2
3use std::{collections::BTreeSet, sync::Arc};
4
5use sim_kernel::{Cx, Datum, Error, Result, Symbol};
6
7/// A semantic observation lane selected by a characterization scenario.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
9pub enum ScenarioObservationLane {
10    /// The returned value or stable failure record.
11    ValueOrFailure,
12    /// Ordered runtime events.
13    Events,
14    /// Ordered operation or library receipts.
15    Receipts,
16    /// The browseable Card face.
17    Browse,
18}
19
20/// One ordered, semantic scenario input and the authority required to apply it.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct ScenarioInput {
23    /// Stable identity of this input within the scenario.
24    pub id: Symbol,
25    /// Authority exercised while applying this input.
26    pub authority: Symbol,
27    /// Canonical input data.
28    pub datum: Datum,
29}
30
31impl ScenarioInput {
32    /// Construct a declared input.
33    pub fn new(id: Symbol, authority: Symbol, datum: Datum) -> Self {
34        Self {
35            id,
36            authority,
37            datum,
38        }
39    }
40}
41
42/// Hard bounds for one scenario execution.
43#[derive(Clone, Copy, Debug, PartialEq, Eq)]
44pub struct ScenarioLimits {
45    /// Maximum number of ordered inputs.
46    pub max_inputs: usize,
47    /// Maximum number of observations across selected lanes.
48    pub max_observations: usize,
49}
50
51impl ScenarioLimits {
52    /// Construct explicit scenario bounds.
53    pub const fn new(max_inputs: usize, max_observations: usize) -> Self {
54        Self {
55            max_inputs,
56            max_observations,
57        }
58    }
59}
60
61/// Complete metadata required before a repeatable scenario may execute.
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct ScenarioSpec {
64    /// Stable scenario identity.
65    pub id: Symbol,
66    /// Stable identity of the installed setup, independent of host state.
67    pub setup: Symbol,
68    /// Every authority the scenario is permitted to exercise.
69    pub authorities: BTreeSet<Symbol>,
70    /// Required execution bounds; `None` is rejected by preflight.
71    pub limits: Option<ScenarioLimits>,
72    /// Ordered semantic inputs.
73    pub inputs: Vec<ScenarioInput>,
74    /// Explicit observation lanes.
75    pub observation_lanes: BTreeSet<ScenarioObservationLane>,
76}
77
78impl ScenarioSpec {
79    /// Start a scenario declaration with stable scenario and setup identities.
80    pub fn new(id: Symbol, setup: Symbol) -> Self {
81        Self {
82            id,
83            setup,
84            authorities: BTreeSet::new(),
85            limits: None,
86            inputs: Vec::new(),
87            observation_lanes: BTreeSet::new(),
88        }
89    }
90
91    /// Declare an authority available to the scenario.
92    pub fn with_authority(mut self, authority: Symbol) -> Self {
93        self.authorities.insert(authority);
94        self
95    }
96
97    /// Declare hard execution bounds.
98    pub fn with_limits(mut self, limits: ScenarioLimits) -> Self {
99        self.limits = Some(limits);
100        self
101    }
102
103    /// Append an ordered semantic input.
104    pub fn with_input(mut self, input: ScenarioInput) -> Self {
105        self.inputs.push(input);
106        self
107    }
108
109    /// Select an observation lane.
110    pub fn observing(mut self, lane: ScenarioObservationLane) -> Self {
111        self.observation_lanes.insert(lane);
112        self
113    }
114
115    pub(crate) fn validate(
116        &self,
117        supported_lanes: &BTreeSet<ScenarioObservationLane>,
118    ) -> Result<()> {
119        let Some(limits) = self.limits else {
120            return Err(Error::Eval(format!(
121                "scenario {} is missing limits",
122                self.id
123            )));
124        };
125        if limits.max_inputs == 0 || limits.max_observations == 0 {
126            return Err(Error::Eval(format!(
127                "scenario {} has a zero bound",
128                self.id
129            )));
130        }
131        if self.inputs.len() > limits.max_inputs {
132            return Err(Error::Eval(format!(
133                "scenario {} exceeds its input bound",
134                self.id
135            )));
136        }
137        if self.observation_lanes.len() > limits.max_observations {
138            return Err(Error::Eval(format!(
139                "scenario {} exceeds its observation bound",
140                self.id
141            )));
142        }
143        if self.observation_lanes.is_empty() {
144            return Err(Error::Eval(format!(
145                "scenario {} selects no observation lanes",
146                self.id
147            )));
148        }
149        if let Some(input) = self
150            .inputs
151            .iter()
152            .find(|input| !self.authorities.contains(&input.authority))
153        {
154            return Err(Error::Eval(format!(
155                "scenario {} input {} uses undeclared authority {}",
156                self.id, input.id, input.authority
157            )));
158        }
159        if let Some(lane) = self
160            .observation_lanes
161            .iter()
162            .find(|lane| !supported_lanes.contains(lane))
163        {
164            return Err(Error::Eval(format!(
165                "scenario {} selects unsupported lane {lane:?}",
166                self.id
167            )));
168        }
169        Ok(())
170    }
171}
172
173/// Effectful body of a scenario, invoked only after batch preflight succeeds.
174pub type ScenarioDriver = Arc<dyn Fn(&mut Cx, &ScenarioSpec) -> Result<()> + Send + Sync + 'static>;
175
176/// Registered scenario metadata and its bounded driver.
177#[derive(Clone)]
178pub struct CharacterizationScenario {
179    /// Explicit scenario contract.
180    pub spec: ScenarioSpec,
181    pub(crate) driver: ScenarioDriver,
182}
183
184impl CharacterizationScenario {
185    /// Pair a scenario contract with its driver.
186    pub fn new(spec: ScenarioSpec, driver: ScenarioDriver) -> Self {
187        Self { spec, driver }
188    }
189}