Skip to main content

meerkat_machine_kernels/
runtime.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use meerkat_machine_schema::{
4    EffectEmit, Expr, HelperSchema, MachineSchema, Quantifier, TransitionSchema, TypeRef, Update,
5};
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use thiserror::Error;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
10pub enum KernelValue {
11    Bool(bool),
12    U64(u64),
13    String(String),
14    NamedVariant { enum_name: String, variant: String },
15    Seq(Vec<KernelValue>),
16    Set(BTreeSet<KernelValue>),
17    Map(BTreeMap<KernelValue, KernelValue>),
18    None,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23enum KernelValueRepr {
24    Bool {
25        value: bool,
26    },
27    U64 {
28        value: u64,
29    },
30    String {
31        value: String,
32    },
33    NamedVariant {
34        enum_name: String,
35        variant: String,
36    },
37    Seq {
38        items: Vec<KernelValue>,
39    },
40    Set {
41        items: Vec<KernelValue>,
42    },
43    Map {
44        entries: Vec<(KernelValue, KernelValue)>,
45    },
46    None,
47}
48
49impl From<&KernelValue> for KernelValueRepr {
50    fn from(value: &KernelValue) -> Self {
51        match value {
52            KernelValue::Bool(value) => Self::Bool { value: *value },
53            KernelValue::U64(value) => Self::U64 { value: *value },
54            KernelValue::String(value) => Self::String {
55                value: value.clone(),
56            },
57            KernelValue::NamedVariant { enum_name, variant } => Self::NamedVariant {
58                enum_name: enum_name.clone(),
59                variant: variant.clone(),
60            },
61            KernelValue::Seq(values) => Self::Seq {
62                items: values.clone(),
63            },
64            KernelValue::Set(values) => Self::Set {
65                items: values.iter().cloned().collect(),
66            },
67            KernelValue::Map(values) => Self::Map {
68                entries: values
69                    .iter()
70                    .map(|(key, value)| (key.clone(), value.clone()))
71                    .collect(),
72            },
73            KernelValue::None => Self::None,
74        }
75    }
76}
77
78impl From<KernelValueRepr> for KernelValue {
79    fn from(value: KernelValueRepr) -> Self {
80        match value {
81            KernelValueRepr::Bool { value } => Self::Bool(value),
82            KernelValueRepr::U64 { value } => Self::U64(value),
83            KernelValueRepr::String { value } => Self::String(value),
84            KernelValueRepr::NamedVariant { enum_name, variant } => {
85                Self::NamedVariant { enum_name, variant }
86            }
87            KernelValueRepr::Seq { items } => Self::Seq(items),
88            KernelValueRepr::Set { items } => Self::Set(items.into_iter().collect()),
89            KernelValueRepr::Map { entries } => Self::Map(entries.into_iter().collect()),
90            KernelValueRepr::None => Self::None,
91        }
92    }
93}
94
95impl Serialize for KernelValue {
96    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97    where
98        S: Serializer,
99    {
100        KernelValueRepr::from(self).serialize(serializer)
101    }
102}
103
104impl<'de> Deserialize<'de> for KernelValue {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: Deserializer<'de>,
108    {
109        Ok(KernelValueRepr::deserialize(deserializer)?.into())
110    }
111}
112
113#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
114pub struct KernelState {
115    pub phase: String,
116    pub fields: BTreeMap<String, KernelValue>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub struct KernelInput {
121    pub variant: String,
122    pub fields: BTreeMap<String, KernelValue>,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct KernelEffect {
127    pub variant: String,
128    pub fields: BTreeMap<String, KernelValue>,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct TransitionOutcome {
133    pub transition: String,
134    pub next_state: KernelState,
135    pub effects: Vec<KernelEffect>,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Error)]
139pub enum TransitionRefusal {
140    #[error("unknown input variant `{variant}` for machine `{machine}`")]
141    UnknownInputVariant { machine: String, variant: String },
142    #[error("invalid input payload for machine `{machine}` variant `{variant}`: {reason}")]
143    InvalidInputPayload {
144        machine: String,
145        variant: String,
146        reason: String,
147    },
148    #[error("no matching transition for machine `{machine}` in phase `{phase}` on `{variant}`")]
149    NoMatchingTransition {
150        machine: String,
151        phase: String,
152        variant: String,
153    },
154    #[error(
155        "ambiguous transitions for machine `{machine}` in phase `{phase}` on `{variant}`: {transitions:?}"
156    )]
157    AmbiguousTransition {
158        machine: String,
159        phase: String,
160        variant: String,
161        transitions: Vec<String>,
162    },
163    #[error("evaluation error in machine `{machine}` transition `{transition}`: {reason}")]
164    EvaluationError {
165        machine: String,
166        transition: String,
167        reason: String,
168    },
169}
170
171#[derive(Debug, Clone)]
172pub struct GeneratedMachineKernel {
173    schema: MachineSchema,
174}
175
176impl GeneratedMachineKernel {
177    #[must_use]
178    pub fn new(schema: MachineSchema) -> Self {
179        Self { schema }
180    }
181
182    #[must_use]
183    pub fn schema(&self) -> &MachineSchema {
184        &self.schema
185    }
186
187    pub fn initial_state(&self) -> Result<KernelState, TransitionRefusal> {
188        let mut state = KernelState {
189            phase: self.schema.state.init.phase.clone(),
190            fields: self
191                .schema
192                .state
193                .fields
194                .iter()
195                .map(|field| (field.name.clone(), default_value_for_type(&field.ty)))
196                .collect(),
197        };
198
199        for init in &self.schema.state.init.fields {
200            let value = self.eval_expr(&state, &BTreeMap::new(), &init.expr, "<init>")?;
201            state.fields.insert(init.field.clone(), value);
202        }
203
204        Ok(state)
205    }
206
207    pub fn transition(
208        &self,
209        state: &KernelState,
210        input: &KernelInput,
211    ) -> Result<TransitionOutcome, TransitionRefusal> {
212        let input_variant = self
213            .schema
214            .inputs
215            .variant_named(&input.variant)
216            .map_err(|_| TransitionRefusal::UnknownInputVariant {
217                machine: self.schema.machine.clone(),
218                variant: input.variant.clone(),
219            })?;
220
221        for field in &input_variant.fields {
222            let Some(value) = input.fields.get(&field.name) else {
223                return Err(TransitionRefusal::InvalidInputPayload {
224                    machine: self.schema.machine.clone(),
225                    variant: input.variant.clone(),
226                    reason: format!("missing field `{}`", field.name),
227                });
228            };
229            if !value_matches_type(value, &field.ty) {
230                return Err(TransitionRefusal::InvalidInputPayload {
231                    machine: self.schema.machine.clone(),
232                    variant: input.variant.clone(),
233                    reason: format!("field `{}` does not match declared type", field.name),
234                });
235            }
236        }
237
238        let mut matches = Vec::new();
239        for transition in &self.schema.transitions {
240            if !transition.from.iter().any(|phase| phase == &state.phase) {
241                continue;
242            }
243            if transition.on.variant != input.variant {
244                continue;
245            }
246
247            let mut bindings = BTreeMap::new();
248            let mut malformed = false;
249            for binding in &transition.on.bindings {
250                let Some(value) = input.fields.get(binding) else {
251                    malformed = true;
252                    break;
253                };
254                bindings.insert(binding.clone(), value.clone());
255            }
256            if malformed {
257                return Err(TransitionRefusal::InvalidInputPayload {
258                    machine: self.schema.machine.clone(),
259                    variant: input.variant.clone(),
260                    reason: "transition binding missing from payload".into(),
261                });
262            }
263
264            let guards_hold = transition.guards.iter().try_fold(true, |acc, guard| {
265                let value = self.eval_expr(state, &bindings, &guard.expr, &transition.name)?;
266                let as_bool =
267                    value
268                        .as_bool()
269                        .map_err(|reason| TransitionRefusal::EvaluationError {
270                            machine: self.schema.machine.clone(),
271                            transition: transition.name.clone(),
272                            reason: format!("guard `{}` {reason}", guard.name),
273                        })?;
274                Ok::<bool, TransitionRefusal>(acc && as_bool)
275            })?;
276
277            if guards_hold {
278                matches.push((transition, bindings));
279            }
280        }
281
282        match matches.len() {
283            0 => Err(TransitionRefusal::NoMatchingTransition {
284                machine: self.schema.machine.clone(),
285                phase: state.phase.clone(),
286                variant: input.variant.clone(),
287            }),
288            1 => {
289                let Some((transition, bindings)) = matches.pop() else {
290                    return Err(TransitionRefusal::NoMatchingTransition {
291                        machine: self.schema.machine.clone(),
292                        phase: state.phase.clone(),
293                        variant: input.variant.clone(),
294                    });
295                };
296                self.apply_transition(state, transition, &bindings)
297            }
298            _ => Err(TransitionRefusal::AmbiguousTransition {
299                machine: self.schema.machine.clone(),
300                phase: state.phase.clone(),
301                variant: input.variant.clone(),
302                transitions: matches
303                    .iter()
304                    .map(|(transition, _)| transition.name.clone())
305                    .collect(),
306            }),
307        }
308    }
309
310    fn apply_transition(
311        &self,
312        state: &KernelState,
313        transition: &TransitionSchema,
314        bindings: &BTreeMap<String, KernelValue>,
315    ) -> Result<TransitionOutcome, TransitionRefusal> {
316        let mut next_state = state.clone();
317        for update in &transition.updates {
318            self.apply_update(&mut next_state, bindings, update, &transition.name)?;
319        }
320        next_state.phase = transition.to.clone();
321
322        let mut effects = Vec::new();
323        for effect in &transition.emit {
324            effects.push(self.render_effect(&next_state, bindings, effect, &transition.name)?);
325        }
326
327        Ok(TransitionOutcome {
328            transition: transition.name.clone(),
329            next_state,
330            effects,
331        })
332    }
333
334    pub fn evaluate_helper(
335        &self,
336        state: &KernelState,
337        helper_name: &str,
338        args: &BTreeMap<String, KernelValue>,
339    ) -> Result<KernelValue, TransitionRefusal> {
340        let helper = self
341            .schema
342            .helpers
343            .iter()
344            .chain(self.schema.derived.iter())
345            .find(|candidate| candidate.name == helper_name)
346            .ok_or_else(|| TransitionRefusal::EvaluationError {
347                machine: self.schema.machine.clone(),
348                transition: "<helper>".to_string(),
349                reason: format!("unknown helper `{helper_name}`"),
350            })?;
351
352        let mut bindings = BTreeMap::new();
353        for param in &helper.params {
354            let Some(value) = args.get(&param.name) else {
355                return Err(TransitionRefusal::EvaluationError {
356                    machine: self.schema.machine.clone(),
357                    transition: "<helper>".to_string(),
358                    reason: format!("missing helper arg `{}`", param.name),
359                });
360            };
361            if !value_matches_type(value, &param.ty) {
362                return Err(TransitionRefusal::EvaluationError {
363                    machine: self.schema.machine.clone(),
364                    transition: "<helper>".to_string(),
365                    reason: format!("helper arg `{}` does not match declared type", param.name),
366                });
367            }
368            bindings.insert(param.name.clone(), value.clone());
369        }
370
371        self.eval_helper(state, &bindings, helper, "<helper>")
372    }
373
374    fn render_effect(
375        &self,
376        state: &KernelState,
377        bindings: &BTreeMap<String, KernelValue>,
378        effect: &EffectEmit,
379        transition_name: &str,
380    ) -> Result<KernelEffect, TransitionRefusal> {
381        let mut fields = BTreeMap::new();
382        for (name, expr) in &effect.fields {
383            fields.insert(
384                name.clone(),
385                self.eval_expr(state, bindings, expr, transition_name)?,
386            );
387        }
388        Ok(KernelEffect {
389            variant: effect.variant.clone(),
390            fields,
391        })
392    }
393
394    fn apply_update(
395        &self,
396        state: &mut KernelState,
397        bindings: &BTreeMap<String, KernelValue>,
398        update: &Update,
399        transition_name: &str,
400    ) -> Result<(), TransitionRefusal> {
401        match update {
402            Update::Assign { field, expr } => {
403                let value = self.eval_expr(state, bindings, expr, transition_name)?;
404                state.fields.insert(field.clone(), value);
405            }
406            Update::Increment { field, amount } => {
407                let current = state
408                    .fields
409                    .get(field)
410                    .cloned()
411                    .unwrap_or(KernelValue::U64(0));
412                let value = current
413                    .as_u64()
414                    .map_err(|reason| self.eval_error(transition_name, reason))?;
415                state.fields.insert(
416                    field.clone(),
417                    KernelValue::U64(value.saturating_add(*amount)),
418                );
419            }
420            Update::Decrement { field, amount } => {
421                let current = state
422                    .fields
423                    .get(field)
424                    .cloned()
425                    .unwrap_or(KernelValue::U64(0));
426                let value = current
427                    .as_u64()
428                    .map_err(|reason| self.eval_error(transition_name, reason))?;
429                let next = value.checked_sub(*amount).ok_or_else(|| {
430                    self.eval_error(transition_name, format!("underflow decrementing `{field}`"))
431                })?;
432                state.fields.insert(field.clone(), KernelValue::U64(next));
433            }
434            Update::MapInsert { field, key, value } => {
435                let key = self.eval_expr(state, bindings, key, transition_name)?;
436                let value = self.eval_expr(state, bindings, value, transition_name)?;
437                let mut map = state
438                    .fields
439                    .get(field)
440                    .cloned()
441                    .unwrap_or(KernelValue::Map(BTreeMap::new()))
442                    .into_map()
443                    .map_err(|reason| self.eval_error(transition_name, reason))?;
444                map.insert(key, value);
445                state.fields.insert(field.clone(), KernelValue::Map(map));
446            }
447            Update::SetInsert { field, value } => {
448                let value = self.eval_expr(state, bindings, value, transition_name)?;
449                let mut set = state
450                    .fields
451                    .get(field)
452                    .cloned()
453                    .unwrap_or(KernelValue::Set(BTreeSet::new()))
454                    .into_set()
455                    .map_err(|reason| self.eval_error(transition_name, reason))?;
456                set.insert(value);
457                state.fields.insert(field.clone(), KernelValue::Set(set));
458            }
459            Update::SetRemove { field, value } => {
460                let value = self.eval_expr(state, bindings, value, transition_name)?;
461                let mut set = state
462                    .fields
463                    .get(field)
464                    .cloned()
465                    .unwrap_or(KernelValue::Set(BTreeSet::new()))
466                    .into_set()
467                    .map_err(|reason| self.eval_error(transition_name, reason))?;
468                set.remove(&value);
469                state.fields.insert(field.clone(), KernelValue::Set(set));
470            }
471            Update::SeqAppend { field, value } => {
472                let value = self.eval_expr(state, bindings, value, transition_name)?;
473                let mut seq = state
474                    .fields
475                    .get(field)
476                    .cloned()
477                    .unwrap_or(KernelValue::Seq(Vec::new()))
478                    .into_seq()
479                    .map_err(|reason| self.eval_error(transition_name, reason))?;
480                seq.push(value);
481                state.fields.insert(field.clone(), KernelValue::Seq(seq));
482            }
483            Update::SeqPrepend { field, values } => {
484                let values = self.eval_expr(state, bindings, values, transition_name)?;
485                let mut prefix = values
486                    .into_seq()
487                    .map_err(|reason| self.eval_error(transition_name, reason))?;
488                let mut seq = state
489                    .fields
490                    .get(field)
491                    .cloned()
492                    .unwrap_or(KernelValue::Seq(Vec::new()))
493                    .into_seq()
494                    .map_err(|reason| self.eval_error(transition_name, reason))?;
495                prefix.append(&mut seq);
496                state.fields.insert(field.clone(), KernelValue::Seq(prefix));
497            }
498            Update::SeqPopFront { field } => {
499                let mut seq = state
500                    .fields
501                    .get(field)
502                    .cloned()
503                    .unwrap_or(KernelValue::Seq(Vec::new()))
504                    .into_seq()
505                    .map_err(|reason| self.eval_error(transition_name, reason))?;
506                if !seq.is_empty() {
507                    seq.remove(0);
508                }
509                state.fields.insert(field.clone(), KernelValue::Seq(seq));
510            }
511            Update::SeqRemoveValue { field, value } => {
512                let value = self.eval_expr(state, bindings, value, transition_name)?;
513                let mut seq = state
514                    .fields
515                    .get(field)
516                    .cloned()
517                    .unwrap_or(KernelValue::Seq(Vec::new()))
518                    .into_seq()
519                    .map_err(|reason| self.eval_error(transition_name, reason))?;
520                if let Some(index) = seq.iter().position(|item| item == &value) {
521                    seq.remove(index);
522                }
523                state.fields.insert(field.clone(), KernelValue::Seq(seq));
524            }
525            Update::SeqRemoveAll { field, values } => {
526                let values = self.eval_expr(state, bindings, values, transition_name)?;
527                let values = values
528                    .into_seq()
529                    .map_err(|reason| self.eval_error(transition_name, reason))?;
530                let mut seq = state
531                    .fields
532                    .get(field)
533                    .cloned()
534                    .unwrap_or(KernelValue::Seq(Vec::new()))
535                    .into_seq()
536                    .map_err(|reason| self.eval_error(transition_name, reason))?;
537                for value in values {
538                    if let Some(index) = seq.iter().position(|item| item == &value) {
539                        seq.remove(index);
540                    }
541                }
542                state.fields.insert(field.clone(), KernelValue::Seq(seq));
543            }
544            Update::Conditional {
545                condition,
546                then_updates,
547                else_updates,
548            } => {
549                let condition = self.eval_expr(state, bindings, condition, transition_name)?;
550                let condition = condition
551                    .as_bool()
552                    .map_err(|reason| self.eval_error(transition_name, reason))?;
553                let branch = if condition {
554                    then_updates
555                } else {
556                    else_updates
557                };
558                for nested in branch {
559                    self.apply_update(state, bindings, nested, transition_name)?;
560                }
561            }
562            Update::ForEach {
563                binding,
564                over,
565                updates,
566            } => {
567                let values = self.eval_expr(state, bindings, over, transition_name)?;
568                let iterable = values
569                    .into_iterable()
570                    .map_err(|reason| self.eval_error(transition_name, reason))?;
571                for value in iterable {
572                    let mut nested = bindings.clone();
573                    nested.insert(binding.clone(), value);
574                    for nested_update in updates {
575                        self.apply_update(state, &nested, nested_update, transition_name)?;
576                    }
577                }
578            }
579        }
580        Ok(())
581    }
582
583    fn eval_expr(
584        &self,
585        state: &KernelState,
586        bindings: &BTreeMap<String, KernelValue>,
587        expr: &Expr,
588        transition_name: &str,
589    ) -> Result<KernelValue, TransitionRefusal> {
590        match expr {
591            Expr::Bool(value) => Ok(KernelValue::Bool(*value)),
592            Expr::U64(value) => Ok(KernelValue::U64(*value)),
593            Expr::String(value) => Ok(KernelValue::String(value.clone())),
594            Expr::NamedVariant { enum_name, variant } => Ok(KernelValue::NamedVariant {
595                enum_name: enum_name.clone(),
596                variant: variant.clone(),
597            }),
598            Expr::EmptySet => Ok(KernelValue::Set(BTreeSet::new())),
599            Expr::EmptyMap => Ok(KernelValue::Map(BTreeMap::new())),
600            Expr::SeqLiteral(items) => Ok(KernelValue::Seq(
601                items
602                    .iter()
603                    .map(|item| self.eval_expr(state, bindings, item, transition_name))
604                    .collect::<Result<Vec<_>, _>>()?,
605            )),
606            Expr::CurrentPhase => Ok(KernelValue::String(state.phase.clone())),
607            Expr::Phase(phase) => Ok(KernelValue::String(phase.clone())),
608            Expr::Field(field) => state.fields.get(field).cloned().ok_or_else(|| {
609                self.eval_error(transition_name, format!("unknown field `{field}`"))
610            }),
611            Expr::Binding(binding) => bindings.get(binding).cloned().ok_or_else(|| {
612                self.eval_error(transition_name, format!("unknown binding `{binding}`"))
613            }),
614            Expr::Variant(variant) => Ok(KernelValue::String(variant.clone())),
615            Expr::None => Ok(KernelValue::None),
616            Expr::IfElse {
617                condition,
618                then_expr,
619                else_expr,
620            } => {
621                let condition = self.eval_expr(state, bindings, condition, transition_name)?;
622                let condition = condition
623                    .as_bool()
624                    .map_err(|reason| self.eval_error(transition_name, reason))?;
625                if condition {
626                    self.eval_expr(state, bindings, then_expr, transition_name)
627                } else {
628                    self.eval_expr(state, bindings, else_expr, transition_name)
629                }
630            }
631            Expr::Not(inner) => {
632                let value = self.eval_expr(state, bindings, inner, transition_name)?;
633                Ok(KernelValue::Bool(!value.as_bool().map_err(|reason| {
634                    self.eval_error(transition_name, reason)
635                })?))
636            }
637            Expr::And(items) => {
638                for item in items {
639                    let value = self.eval_expr(state, bindings, item, transition_name)?;
640                    if !value
641                        .as_bool()
642                        .map_err(|reason| self.eval_error(transition_name, reason))?
643                    {
644                        return Ok(KernelValue::Bool(false));
645                    }
646                }
647                Ok(KernelValue::Bool(true))
648            }
649            Expr::Or(items) => {
650                for item in items {
651                    let value = self.eval_expr(state, bindings, item, transition_name)?;
652                    if value
653                        .as_bool()
654                        .map_err(|reason| self.eval_error(transition_name, reason))?
655                    {
656                        return Ok(KernelValue::Bool(true));
657                    }
658                }
659                Ok(KernelValue::Bool(false))
660            }
661            Expr::Eq(left, right) => Ok(KernelValue::Bool(
662                self.eval_expr(state, bindings, left, transition_name)?
663                    == self.eval_expr(state, bindings, right, transition_name)?,
664            )),
665            Expr::Neq(left, right) => Ok(KernelValue::Bool(
666                self.eval_expr(state, bindings, left, transition_name)?
667                    != self.eval_expr(state, bindings, right, transition_name)?,
668            )),
669            Expr::Add(left, right) => Ok(KernelValue::U64(
670                self.eval_expr(state, bindings, left, transition_name)?
671                    .as_u64()
672                    .map_err(|reason| self.eval_error(transition_name, reason))?
673                    + self
674                        .eval_expr(state, bindings, right, transition_name)?
675                        .as_u64()
676                        .map_err(|reason| self.eval_error(transition_name, reason))?,
677            )),
678            Expr::Sub(left, right) => {
679                let left = self
680                    .eval_expr(state, bindings, left, transition_name)?
681                    .as_u64()
682                    .map_err(|reason| self.eval_error(transition_name, reason))?;
683                let right = self
684                    .eval_expr(state, bindings, right, transition_name)?
685                    .as_u64()
686                    .map_err(|reason| self.eval_error(transition_name, reason))?;
687                let value = left.checked_sub(right).ok_or_else(|| {
688                    self.eval_error(transition_name, "subtraction underflow".to_string())
689                })?;
690                Ok(KernelValue::U64(value))
691            }
692            Expr::Gt(left, right) => self.compare_values(
693                state,
694                bindings,
695                left,
696                right,
697                transition_name,
698                |left, right| left > right,
699            ),
700            Expr::Gte(left, right) => self.compare_values(
701                state,
702                bindings,
703                left,
704                right,
705                transition_name,
706                |left, right| left >= right,
707            ),
708            Expr::Lt(left, right) => self.compare_values(
709                state,
710                bindings,
711                left,
712                right,
713                transition_name,
714                |left, right| left < right,
715            ),
716            Expr::Lte(left, right) => self.compare_values(
717                state,
718                bindings,
719                left,
720                right,
721                transition_name,
722                |left, right| left <= right,
723            ),
724            Expr::Contains { collection, value } => {
725                let collection = self.eval_expr(state, bindings, collection, transition_name)?;
726                let value = self.eval_expr(state, bindings, value, transition_name)?;
727                Ok(KernelValue::Bool(match collection {
728                    KernelValue::Seq(items) => items.contains(&value),
729                    KernelValue::Set(items) => items.contains(&value),
730                    KernelValue::Map(items) => items.contains_key(&value),
731                    KernelValue::String(items) => match value {
732                        KernelValue::String(needle) => items.contains(&needle),
733                        _ => false,
734                    },
735                    KernelValue::NamedVariant { .. }
736                    | KernelValue::Bool(_)
737                    | KernelValue::U64(_)
738                    | KernelValue::None => false,
739                }))
740            }
741            Expr::SeqStartsWith { seq, prefix } => {
742                let seq = self.eval_expr(state, bindings, seq, transition_name)?;
743                let prefix = self.eval_expr(state, bindings, prefix, transition_name)?;
744                let seq = seq
745                    .into_seq()
746                    .map_err(|reason| self.eval_error(transition_name, reason))?;
747                let prefix = prefix
748                    .into_seq()
749                    .map_err(|reason| self.eval_error(transition_name, reason))?;
750                Ok(KernelValue::Bool(seq.starts_with(&prefix)))
751            }
752            Expr::SeqElements(inner) => {
753                let inner = self.eval_expr(state, bindings, inner, transition_name)?;
754                let seq = inner
755                    .into_seq()
756                    .map_err(|reason| self.eval_error(transition_name, reason))?;
757                Ok(KernelValue::Set(seq.into_iter().collect()))
758            }
759            Expr::Len(inner) => {
760                let inner = self.eval_expr(state, bindings, inner, transition_name)?;
761                let len = match inner {
762                    KernelValue::Seq(items) => items.len(),
763                    KernelValue::Set(items) => items.len(),
764                    KernelValue::Map(items) => items.len(),
765                    KernelValue::String(items) => items.chars().count(),
766                    KernelValue::NamedVariant { .. }
767                    | KernelValue::Bool(_)
768                    | KernelValue::U64(_)
769                    | KernelValue::None => {
770                        return Err(self.eval_error(
771                            transition_name,
772                            "Len expects seq, set, map, or string".to_string(),
773                        ));
774                    }
775                };
776                Ok(KernelValue::U64(len as u64))
777            }
778            Expr::Head(inner) => {
779                let inner = self.eval_expr(state, bindings, inner, transition_name)?;
780                let seq = inner
781                    .into_seq()
782                    .map_err(|reason| self.eval_error(transition_name, reason))?;
783                Ok(seq.first().cloned().unwrap_or(KernelValue::None))
784            }
785            Expr::MapKeys(inner) => {
786                let inner = self.eval_expr(state, bindings, inner, transition_name)?;
787                let map = inner
788                    .into_map()
789                    .map_err(|reason| self.eval_error(transition_name, reason))?;
790                Ok(KernelValue::Set(map.keys().cloned().collect()))
791            }
792            Expr::MapGet { map, key } => {
793                let map = self.eval_expr(state, bindings, map, transition_name)?;
794                let map = map
795                    .into_map()
796                    .map_err(|reason| self.eval_error(transition_name, reason))?;
797                let key = self.eval_expr(state, bindings, key, transition_name)?;
798                Ok(map.get(&key).cloned().unwrap_or(KernelValue::None))
799            }
800            Expr::Some(inner) => self.eval_expr(state, bindings, inner, transition_name),
801            Expr::Call { helper, args } => {
802                let helper = self
803                    .schema
804                    .helpers
805                    .iter()
806                    .chain(self.schema.derived.iter())
807                    .find(|candidate| candidate.name == *helper)
808                    .ok_or_else(|| {
809                        self.eval_error(transition_name, format!("unknown helper `{helper}`"))
810                    })?;
811
812                let mut nested_bindings = BTreeMap::new();
813                for (param, arg) in helper.params.iter().zip(args.iter()) {
814                    nested_bindings.insert(
815                        param.name.clone(),
816                        self.eval_expr(state, bindings, arg, transition_name)?,
817                    );
818                }
819                self.eval_helper(state, &nested_bindings, helper, transition_name)
820            }
821            Expr::Quantified {
822                quantifier,
823                binding,
824                over,
825                body,
826            } => {
827                let over = self.eval_expr(state, bindings, over, transition_name)?;
828                let iterable = over
829                    .into_iterable()
830                    .map_err(|reason| self.eval_error(transition_name, reason))?;
831
832                match quantifier {
833                    Quantifier::Any => {
834                        for value in iterable {
835                            let mut nested = bindings.clone();
836                            nested.insert(binding.clone(), value);
837                            if self
838                                .eval_expr(state, &nested, body, transition_name)?
839                                .as_bool()
840                                .map_err(|reason| self.eval_error(transition_name, reason))?
841                            {
842                                return Ok(KernelValue::Bool(true));
843                            }
844                        }
845                        Ok(KernelValue::Bool(false))
846                    }
847                    Quantifier::All => {
848                        for value in iterable {
849                            let mut nested = bindings.clone();
850                            nested.insert(binding.clone(), value);
851                            if !self
852                                .eval_expr(state, &nested, body, transition_name)?
853                                .as_bool()
854                                .map_err(|reason| self.eval_error(transition_name, reason))?
855                            {
856                                return Ok(KernelValue::Bool(false));
857                            }
858                        }
859                        Ok(KernelValue::Bool(true))
860                    }
861                }
862            }
863        }
864    }
865
866    fn eval_helper(
867        &self,
868        state: &KernelState,
869        bindings: &BTreeMap<String, KernelValue>,
870        helper: &HelperSchema,
871        transition_name: &str,
872    ) -> Result<KernelValue, TransitionRefusal> {
873        self.eval_expr(state, bindings, &helper.body, transition_name)
874    }
875
876    fn compare_values(
877        &self,
878        state: &KernelState,
879        bindings: &BTreeMap<String, KernelValue>,
880        left: &Expr,
881        right: &Expr,
882        transition_name: &str,
883        predicate: impl FnOnce(&KernelValue, &KernelValue) -> bool,
884    ) -> Result<KernelValue, TransitionRefusal> {
885        let left = self.eval_expr(state, bindings, left, transition_name)?;
886        let right = self.eval_expr(state, bindings, right, transition_name)?;
887        Ok(KernelValue::Bool(predicate(&left, &right)))
888    }
889
890    fn eval_error(&self, transition_name: &str, reason: impl Into<String>) -> TransitionRefusal {
891        TransitionRefusal::EvaluationError {
892            machine: self.schema.machine.clone(),
893            transition: transition_name.to_string(),
894            reason: reason.into(),
895        }
896    }
897}
898
899impl KernelValue {
900    fn as_bool(&self) -> Result<bool, String> {
901        match self {
902            Self::Bool(value) => Ok(*value),
903            other => Err(format!("expected bool, found {other:?}")),
904        }
905    }
906
907    fn as_u64(&self) -> Result<u64, String> {
908        match self {
909            Self::U64(value) => Ok(*value),
910            other => Err(format!("expected u64, found {other:?}")),
911        }
912    }
913
914    pub fn as_string(&self) -> Result<&str, String> {
915        match self {
916            Self::String(value) => Ok(value.as_str()),
917            other => Err(format!("expected string, found {other:?}")),
918        }
919    }
920
921    pub fn as_named_variant(&self, expected_enum: &str) -> Result<&str, String> {
922        match self {
923            Self::NamedVariant { enum_name, variant } if enum_name == expected_enum => {
924                Ok(variant.as_str())
925            }
926            Self::NamedVariant { enum_name, .. } => Err(format!(
927                "expected named variant of enum `{expected_enum}`, found enum `{enum_name}`"
928            )),
929            other => Err(format!("expected named variant, found {other:?}")),
930        }
931    }
932
933    fn into_seq(self) -> Result<Vec<KernelValue>, String> {
934        match self {
935            Self::Seq(items) => Ok(items),
936            other => Err(format!("expected seq, found {other:?}")),
937        }
938    }
939
940    fn into_set(self) -> Result<BTreeSet<KernelValue>, String> {
941        match self {
942            Self::Set(items) => Ok(items),
943            other => Err(format!("expected set, found {other:?}")),
944        }
945    }
946
947    fn into_map(self) -> Result<BTreeMap<KernelValue, KernelValue>, String> {
948        match self {
949            Self::Map(items) => Ok(items),
950            other => Err(format!("expected map, found {other:?}")),
951        }
952    }
953
954    fn into_iterable(self) -> Result<Vec<KernelValue>, String> {
955        match self {
956            Self::Seq(items) => Ok(items),
957            Self::Set(items) => Ok(items.into_iter().collect()),
958            Self::Map(items) => Ok(items.into_keys().collect()),
959            other => Err(format!("expected iterable, found {other:?}")),
960        }
961    }
962}
963
964fn default_value_for_type(ty: &TypeRef) -> KernelValue {
965    match ty {
966        TypeRef::Bool => KernelValue::Bool(false),
967        TypeRef::U32 | TypeRef::U64 => KernelValue::U64(0),
968        TypeRef::String => KernelValue::String(String::new()),
969        TypeRef::Named(name) if named_type_is_u64(name) => KernelValue::U64(0),
970        TypeRef::Named(_) => KernelValue::String(String::new()),
971        TypeRef::Enum(name) => KernelValue::NamedVariant {
972            enum_name: name.clone(),
973            variant: String::new(),
974        },
975        TypeRef::Option(_) => KernelValue::None,
976        TypeRef::Set(_) => KernelValue::Set(BTreeSet::new()),
977        TypeRef::Seq(_) => KernelValue::Seq(Vec::new()),
978        TypeRef::Map(_, _) => KernelValue::Map(BTreeMap::new()),
979    }
980}
981
982fn value_matches_type(value: &KernelValue, ty: &TypeRef) -> bool {
983    match (value, ty) {
984        (KernelValue::Bool(_), TypeRef::Bool) => true,
985        (KernelValue::U64(_), TypeRef::U32 | TypeRef::U64) => true,
986        (KernelValue::String(_), TypeRef::String) => true,
987        (KernelValue::U64(_), TypeRef::Named(name)) if named_type_is_u64(name) => true,
988        (KernelValue::String(_), TypeRef::Named(name)) if !named_type_is_u64(name) => true,
989        (KernelValue::NamedVariant { enum_name, .. }, TypeRef::Enum(name)) if enum_name == name => {
990            true
991        }
992        (KernelValue::None, TypeRef::Option(_)) => true,
993        (inner, TypeRef::Option(inner_ty)) => value_matches_type(inner, inner_ty),
994        (KernelValue::Set(values), TypeRef::Set(inner_ty)) => values
995            .iter()
996            .all(|value| value_matches_type(value, inner_ty)),
997        (KernelValue::Seq(values), TypeRef::Seq(inner_ty)) => values
998            .iter()
999            .all(|value| value_matches_type(value, inner_ty)),
1000        (KernelValue::Map(values), TypeRef::Map(key_ty, value_ty)) => {
1001            values.iter().all(|(key, value)| {
1002                value_matches_type(key, key_ty) && value_matches_type(value, value_ty)
1003            })
1004        }
1005        _ => false,
1006    }
1007}
1008
1009fn named_type_is_u64(name: &str) -> bool {
1010    matches!(name, "BoundarySequence" | "TurnNumber")
1011}
1012
1013#[cfg(test)]
1014mod tests {
1015    use std::collections::BTreeMap;
1016
1017    use meerkat_machine_schema::{
1018        canonical_machine_schemas, input_lifecycle_machine, runtime_control_machine,
1019    };
1020
1021    use super::{
1022        GeneratedMachineKernel, KernelInput, KernelValue, TransitionOutcome, TransitionRefusal,
1023    };
1024
1025    #[allow(clippy::expect_used)]
1026    #[test]
1027    fn every_catalog_machine_builds_an_initial_state() {
1028        for schema in canonical_machine_schemas() {
1029            let kernel = GeneratedMachineKernel::new(schema.clone());
1030            let state = kernel.initial_state().expect("initial state");
1031            assert_eq!(state.phase, schema.state.init.phase);
1032        }
1033    }
1034
1035    #[allow(clippy::expect_used)]
1036    #[test]
1037    fn input_lifecycle_queue_accepted_transition_executes() {
1038        let kernel = GeneratedMachineKernel::new(input_lifecycle_machine());
1039        let state = kernel.initial_state().expect("initial state");
1040        let outcome = kernel
1041            .transition(
1042                &state,
1043                &KernelInput {
1044                    variant: "QueueAccepted".into(),
1045                    fields: BTreeMap::new(),
1046                },
1047            )
1048            .expect("queue accepted transition");
1049        assert_eq!(outcome.transition, "QueueAccepted");
1050        assert_eq!(outcome.next_state.phase, "Queued");
1051    }
1052
1053    #[allow(clippy::expect_used)]
1054    #[test]
1055    fn runtime_control_rejects_unknown_input_variant() {
1056        let kernel = GeneratedMachineKernel::new(runtime_control_machine());
1057        let state = kernel.initial_state().expect("initial state");
1058        let refusal = kernel
1059            .transition(
1060                &state,
1061                &KernelInput {
1062                    variant: "DoesNotExist".into(),
1063                    fields: BTreeMap::new(),
1064                },
1065            )
1066            .expect_err("unknown input variant should fail");
1067        assert!(matches!(
1068            refusal,
1069            TransitionRefusal::UnknownInputVariant { .. }
1070        ));
1071    }
1072
1073    #[allow(clippy::expect_used)]
1074    #[test]
1075    fn input_payload_types_are_checked() {
1076        let kernel = GeneratedMachineKernel::new(runtime_control_machine());
1077        let state = kernel.initial_state().expect("initial state");
1078        let refusal = kernel
1079            .transition(
1080                &state,
1081                &KernelInput {
1082                    variant: "SubmitWork".into(),
1083                    fields: BTreeMap::from([
1084                        ("work_id".into(), KernelValue::String("work-1".into())),
1085                        ("content_shape".into(), KernelValue::U64(99)),
1086                        ("handling_mode".into(), KernelValue::String("Queue".into())),
1087                        ("request_id".into(), KernelValue::None),
1088                        ("reservation_key".into(), KernelValue::None),
1089                    ]),
1090                },
1091            )
1092            .expect_err("typed payload mismatch should fail");
1093        assert!(matches!(
1094            refusal,
1095            TransitionRefusal::InvalidInputPayload { .. }
1096        ));
1097    }
1098
1099    #[allow(clippy::expect_used)]
1100    #[test]
1101    fn flow_run_rejects_string_payloads_for_enum_fields() {
1102        use meerkat_machine_schema::flow_run_machine;
1103
1104        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1105        let state = kernel.initial_state().expect("initial state");
1106        let refusal = kernel
1107            .transition(
1108                &state,
1109                &KernelInput {
1110                    variant: "CreateRun".into(),
1111                    fields: BTreeMap::from([
1112                        (
1113                            "step_ids".into(),
1114                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1115                        ),
1116                        (
1117                            "ordered_steps".into(),
1118                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1119                        ),
1120                        (
1121                            "step_has_conditions".into(),
1122                            KernelValue::Map(BTreeMap::from([(
1123                                KernelValue::String("step-a".into()),
1124                                KernelValue::Bool(false),
1125                            )])),
1126                        ),
1127                        (
1128                            "step_dependencies".into(),
1129                            KernelValue::Map(BTreeMap::from([(
1130                                KernelValue::String("step-a".into()),
1131                                KernelValue::Seq(vec![]),
1132                            )])),
1133                        ),
1134                        (
1135                            "step_dependency_modes".into(),
1136                            KernelValue::Map(BTreeMap::from([(
1137                                KernelValue::String("step-a".into()),
1138                                KernelValue::NamedVariant {
1139                                    enum_name: "DependencyMode".into(),
1140                                    variant: "All".into(),
1141                                },
1142                            )])),
1143                        ),
1144                        (
1145                            "step_branches".into(),
1146                            KernelValue::Map(BTreeMap::from([(
1147                                KernelValue::String("step-a".into()),
1148                                KernelValue::None,
1149                            )])),
1150                        ),
1151                        (
1152                            "step_collection_policies".into(),
1153                            KernelValue::Map(BTreeMap::from([(
1154                                KernelValue::String("step-a".into()),
1155                                KernelValue::String("All".into()),
1156                            )])),
1157                        ),
1158                        (
1159                            "step_quorum_thresholds".into(),
1160                            KernelValue::Map(BTreeMap::from([(
1161                                KernelValue::String("step-a".into()),
1162                                KernelValue::U64(0),
1163                            )])),
1164                        ),
1165                        ("escalation_threshold".into(), KernelValue::U64(0)),
1166                        ("max_step_retries".into(), KernelValue::U64(0)),
1167                        ("max_active_nodes".into(), KernelValue::U64(0)),
1168                        ("max_active_frames".into(), KernelValue::U64(0)),
1169                        ("max_frame_depth".into(), KernelValue::U64(0)),
1170                    ]),
1171                },
1172            )
1173            .expect_err("string enum payload should fail");
1174        assert!(matches!(
1175            refusal,
1176            TransitionRefusal::InvalidInputPayload { .. }
1177        ));
1178    }
1179
1180    fn flow_named_variant(enum_name: &str, variant: &str) -> KernelValue {
1181        KernelValue::NamedVariant {
1182            enum_name: enum_name.into(),
1183            variant: variant.into(),
1184        }
1185    }
1186
1187    fn flow_create_run_input(
1188        step_ids: &[&str],
1189        step_has_conditions: &[(&str, bool)],
1190        step_dependencies: &[(&str, &[&str])],
1191        step_dependency_modes: &[(&str, &str)],
1192        step_branches: &[(&str, Option<&str>)],
1193    ) -> KernelInput {
1194        let ordered_steps = step_ids
1195            .iter()
1196            .map(|step_id| KernelValue::String((*step_id).into()))
1197            .collect::<Vec<_>>();
1198        let step_ids = ordered_steps.clone();
1199        let step_has_conditions = step_has_conditions
1200            .iter()
1201            .map(|(step_id, has_conditions)| {
1202                (
1203                    KernelValue::String((*step_id).into()),
1204                    KernelValue::Bool(*has_conditions),
1205                )
1206            })
1207            .collect::<BTreeMap<_, _>>();
1208        let step_dependencies = step_dependencies
1209            .iter()
1210            .map(|(step_id, dependencies)| {
1211                (
1212                    KernelValue::String((*step_id).into()),
1213                    KernelValue::Seq(
1214                        dependencies
1215                            .iter()
1216                            .map(|dependency| KernelValue::String((*dependency).into()))
1217                            .collect::<Vec<_>>(),
1218                    ),
1219                )
1220            })
1221            .collect::<BTreeMap<_, _>>();
1222        let step_dependency_modes = step_dependency_modes
1223            .iter()
1224            .map(|(step_id, mode)| {
1225                (
1226                    KernelValue::String((*step_id).into()),
1227                    flow_named_variant("DependencyMode", mode),
1228                )
1229            })
1230            .collect::<BTreeMap<_, _>>();
1231        let step_branches = step_branches
1232            .iter()
1233            .map(|(step_id, branch)| {
1234                (
1235                    KernelValue::String((*step_id).into()),
1236                    match branch {
1237                        Some(branch_id) => KernelValue::String((*branch_id).into()),
1238                        None => KernelValue::None,
1239                    },
1240                )
1241            })
1242            .collect::<BTreeMap<_, _>>();
1243        let step_collection_policies = step_dependency_modes
1244            .keys()
1245            .map(|step_id| {
1246                (
1247                    step_id.clone(),
1248                    flow_named_variant("CollectionPolicyKind", "All"),
1249                )
1250            })
1251            .collect::<BTreeMap<_, _>>();
1252        let step_quorum_thresholds = step_dependency_modes
1253            .keys()
1254            .map(|step_id| (step_id.clone(), KernelValue::U64(0)))
1255            .collect::<BTreeMap<_, _>>();
1256
1257        KernelInput {
1258            variant: "CreateRun".into(),
1259            fields: BTreeMap::from([
1260                ("step_ids".into(), KernelValue::Seq(step_ids)),
1261                ("ordered_steps".into(), KernelValue::Seq(ordered_steps)),
1262                (
1263                    "step_has_conditions".into(),
1264                    KernelValue::Map(step_has_conditions),
1265                ),
1266                (
1267                    "step_dependencies".into(),
1268                    KernelValue::Map(step_dependencies),
1269                ),
1270                (
1271                    "step_dependency_modes".into(),
1272                    KernelValue::Map(step_dependency_modes),
1273                ),
1274                ("step_branches".into(), KernelValue::Map(step_branches)),
1275                (
1276                    "step_collection_policies".into(),
1277                    KernelValue::Map(step_collection_policies),
1278                ),
1279                (
1280                    "step_quorum_thresholds".into(),
1281                    KernelValue::Map(step_quorum_thresholds),
1282                ),
1283                ("escalation_threshold".into(), KernelValue::U64(0)),
1284                ("max_step_retries".into(), KernelValue::U64(0)),
1285                ("max_active_nodes".into(), KernelValue::U64(0)),
1286                ("max_active_frames".into(), KernelValue::U64(0)),
1287                ("max_frame_depth".into(), KernelValue::U64(0)),
1288            ]),
1289        }
1290    }
1291
1292    fn flow_step_input(variant: &str, step_id: &str) -> KernelInput {
1293        KernelInput {
1294            variant: variant.into(),
1295            fields: BTreeMap::from([("step_id".into(), KernelValue::String(step_id.into()))]),
1296        }
1297    }
1298
1299    fn flow_has_effect(outcome: &TransitionOutcome, variant: &str) -> bool {
1300        outcome
1301            .effects
1302            .iter()
1303            .any(|effect| effect.variant == variant)
1304    }
1305
1306    #[allow(clippy::expect_used, clippy::panic)]
1307    fn flow_helper_bool(
1308        kernel: &GeneratedMachineKernel,
1309        state: &super::KernelState,
1310        helper_name: &str,
1311        step_id: &str,
1312    ) -> bool {
1313        match kernel
1314            .evaluate_helper(
1315                state,
1316                helper_name,
1317                &BTreeMap::from([("step_id".into(), KernelValue::String(step_id.into()))]),
1318            )
1319            .expect("helper evaluation should succeed")
1320        {
1321            KernelValue::Bool(value) => value,
1322            other => panic!("expected bool helper result, got {other:?}"),
1323        }
1324    }
1325
1326    #[allow(clippy::expect_used)]
1327    #[test]
1328    fn flow_run_dispatch_requires_recorded_true_condition() {
1329        use meerkat_machine_schema::flow_run_machine;
1330
1331        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1332        let state = kernel.initial_state().expect("initial state");
1333        let created = kernel
1334            .transition(
1335                &state,
1336                &flow_create_run_input(
1337                    &["conditional"],
1338                    &[("conditional", true)],
1339                    &[("conditional", &[])],
1340                    &[("conditional", "All")],
1341                    &[("conditional", None)],
1342                ),
1343            )
1344            .expect("create run");
1345        let running = kernel
1346            .transition(
1347                &created.next_state,
1348                &KernelInput {
1349                    variant: "StartRun".into(),
1350                    fields: BTreeMap::new(),
1351                },
1352            )
1353            .expect("start run");
1354
1355        let refusal = kernel
1356            .transition(
1357                &running.next_state,
1358                &flow_step_input("DispatchStep", "conditional"),
1359            )
1360            .expect_err("dispatch should be refused before condition result");
1361        assert!(matches!(
1362            refusal,
1363            TransitionRefusal::NoMatchingTransition { .. }
1364        ));
1365
1366        let condition_passed = kernel
1367            .transition(
1368                &running.next_state,
1369                &flow_step_input("ConditionPassed", "conditional"),
1370            )
1371            .expect("record condition");
1372        let dispatched = kernel
1373            .transition(
1374                &condition_passed.next_state,
1375                &flow_step_input("DispatchStep", "conditional"),
1376            )
1377            .expect("dispatch after condition passed");
1378        assert_eq!(dispatched.transition, "DispatchStep");
1379    }
1380
1381    #[allow(clippy::expect_used)]
1382    #[test]
1383    fn flow_run_dispatch_requires_dependencies_to_be_ready() {
1384        use meerkat_machine_schema::flow_run_machine;
1385
1386        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1387        let state = kernel.initial_state().expect("initial state");
1388        let created = kernel
1389            .transition(
1390                &state,
1391                &flow_create_run_input(
1392                    &["dep", "gated"],
1393                    &[("dep", false), ("gated", false)],
1394                    &[("dep", &[]), ("gated", &["dep"])],
1395                    &[("dep", "All"), ("gated", "All")],
1396                    &[("dep", None), ("gated", None)],
1397                ),
1398            )
1399            .expect("create run");
1400        let running = kernel
1401            .transition(
1402                &created.next_state,
1403                &KernelInput {
1404                    variant: "StartRun".into(),
1405                    fields: BTreeMap::new(),
1406                },
1407            )
1408            .expect("start run");
1409
1410        let refusal = kernel
1411            .transition(
1412                &running.next_state,
1413                &flow_step_input("DispatchStep", "gated"),
1414            )
1415            .expect_err("dispatch should be refused before dependency completes");
1416        assert!(matches!(
1417            refusal,
1418            TransitionRefusal::NoMatchingTransition { .. }
1419        ));
1420
1421        let dep_dispatched = kernel
1422            .transition(&running.next_state, &flow_step_input("DispatchStep", "dep"))
1423            .expect("dispatch dependency");
1424        let dep_completed = kernel
1425            .transition(
1426                &dep_dispatched.next_state,
1427                &flow_step_input("CompleteStep", "dep"),
1428            )
1429            .expect("complete dependency");
1430        let gated_dispatched = kernel
1431            .transition(
1432                &dep_completed.next_state,
1433                &flow_step_input("DispatchStep", "gated"),
1434            )
1435            .expect("dispatch gated step after dependency");
1436        assert_eq!(gated_dispatched.transition, "DispatchStep");
1437    }
1438
1439    #[allow(clippy::expect_used)]
1440    #[test]
1441    fn flow_run_dispatch_blocks_losing_branch_after_winner_completes() {
1442        use meerkat_machine_schema::flow_run_machine;
1443
1444        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1445        let state = kernel.initial_state().expect("initial state");
1446        let created = kernel
1447            .transition(
1448                &state,
1449                &flow_create_run_input(
1450                    &["first", "second"],
1451                    &[("first", false), ("second", false)],
1452                    &[("first", &[]), ("second", &[])],
1453                    &[("first", "All"), ("second", "All")],
1454                    &[("first", Some("winner")), ("second", Some("winner"))],
1455                ),
1456            )
1457            .expect("create run");
1458        let running = kernel
1459            .transition(
1460                &created.next_state,
1461                &KernelInput {
1462                    variant: "StartRun".into(),
1463                    fields: BTreeMap::new(),
1464                },
1465            )
1466            .expect("start run");
1467        let first_dispatched = kernel
1468            .transition(
1469                &running.next_state,
1470                &flow_step_input("DispatchStep", "first"),
1471            )
1472            .expect("dispatch first");
1473        let first_completed = kernel
1474            .transition(
1475                &first_dispatched.next_state,
1476                &flow_step_input("CompleteStep", "first"),
1477            )
1478            .expect("complete first");
1479        let refusal = kernel
1480            .transition(
1481                &first_completed.next_state,
1482                &flow_step_input("DispatchStep", "second"),
1483            )
1484            .expect_err("dispatch should be refused once branch winner completed");
1485        assert!(matches!(
1486            refusal,
1487            TransitionRefusal::NoMatchingTransition { .. }
1488        ));
1489    }
1490
1491    #[allow(clippy::expect_used)]
1492    #[test]
1493    fn flow_run_condition_rejection_skips_step_and_blocks_later_dispatch() {
1494        use meerkat_machine_schema::flow_run_machine;
1495
1496        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1497        let state = kernel.initial_state().expect("initial state");
1498        let created = kernel
1499            .transition(
1500                &state,
1501                &flow_create_run_input(
1502                    &["conditional"],
1503                    &[("conditional", true)],
1504                    &[("conditional", &[])],
1505                    &[("conditional", "All")],
1506                    &[("conditional", None)],
1507                ),
1508            )
1509            .expect("create run");
1510        let running = kernel
1511            .transition(
1512                &created.next_state,
1513                &KernelInput {
1514                    variant: "StartRun".into(),
1515                    fields: BTreeMap::new(),
1516                },
1517            )
1518            .expect("start run");
1519        let rejected = kernel
1520            .transition(
1521                &running.next_state,
1522                &flow_step_input("ConditionRejected", "conditional"),
1523            )
1524            .expect("reject condition");
1525        assert!(flow_has_effect(&rejected, "EmitStepNotice"));
1526
1527        let refusal = kernel
1528            .transition(
1529                &rejected.next_state,
1530                &flow_step_input("DispatchStep", "conditional"),
1531            )
1532            .expect_err("skipped step should no longer dispatch");
1533        assert!(matches!(
1534            refusal,
1535            TransitionRefusal::NoMatchingTransition { .. }
1536        ));
1537    }
1538
1539    #[allow(clippy::expect_used)]
1540    #[test]
1541    fn flow_run_fail_step_emits_supervisor_escalation_at_threshold() {
1542        use meerkat_machine_schema::flow_run_machine;
1543
1544        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1545        let state = kernel.initial_state().expect("initial state");
1546        let created = kernel
1547            .transition(
1548                &state,
1549                &KernelInput {
1550                    variant: "CreateRun".into(),
1551                    fields: BTreeMap::from([
1552                        (
1553                            "step_ids".into(),
1554                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1555                        ),
1556                        (
1557                            "ordered_steps".into(),
1558                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1559                        ),
1560                        (
1561                            "step_has_conditions".into(),
1562                            KernelValue::Map(BTreeMap::from([(
1563                                KernelValue::String("step-a".into()),
1564                                KernelValue::Bool(false),
1565                            )])),
1566                        ),
1567                        (
1568                            "step_dependencies".into(),
1569                            KernelValue::Map(BTreeMap::from([(
1570                                KernelValue::String("step-a".into()),
1571                                KernelValue::Seq(vec![]),
1572                            )])),
1573                        ),
1574                        (
1575                            "step_dependency_modes".into(),
1576                            KernelValue::Map(BTreeMap::from([(
1577                                KernelValue::String("step-a".into()),
1578                                flow_named_variant("DependencyMode", "All"),
1579                            )])),
1580                        ),
1581                        (
1582                            "step_branches".into(),
1583                            KernelValue::Map(BTreeMap::from([(
1584                                KernelValue::String("step-a".into()),
1585                                KernelValue::None,
1586                            )])),
1587                        ),
1588                        (
1589                            "step_collection_policies".into(),
1590                            KernelValue::Map(BTreeMap::from([(
1591                                KernelValue::String("step-a".into()),
1592                                flow_named_variant("CollectionPolicyKind", "All"),
1593                            )])),
1594                        ),
1595                        (
1596                            "step_quorum_thresholds".into(),
1597                            KernelValue::Map(BTreeMap::from([(
1598                                KernelValue::String("step-a".into()),
1599                                KernelValue::U64(0),
1600                            )])),
1601                        ),
1602                        ("escalation_threshold".into(), KernelValue::U64(1)),
1603                        ("max_step_retries".into(), KernelValue::U64(0)),
1604                        ("max_active_nodes".into(), KernelValue::U64(0)),
1605                        ("max_active_frames".into(), KernelValue::U64(0)),
1606                        ("max_frame_depth".into(), KernelValue::U64(0)),
1607                    ]),
1608                },
1609            )
1610            .expect("create run");
1611        let running = kernel
1612            .transition(
1613                &created.next_state,
1614                &KernelInput {
1615                    variant: "StartRun".into(),
1616                    fields: BTreeMap::new(),
1617                },
1618            )
1619            .expect("start run");
1620        let dispatched = kernel
1621            .transition(
1622                &running.next_state,
1623                &flow_step_input("DispatchStep", "step-a"),
1624            )
1625            .expect("dispatch step");
1626        let failed = kernel
1627            .transition(
1628                &dispatched.next_state,
1629                &flow_step_input("FailStep", "step-a"),
1630            )
1631            .expect("fail step");
1632        assert!(flow_has_effect(&failed, "AppendFailureLedger"));
1633        assert!(flow_has_effect(&failed, "EscalateSupervisor"));
1634    }
1635
1636    #[allow(clippy::expect_used)]
1637    #[test]
1638    fn flow_run_any_dependency_all_skipped_sets_skip_truth() {
1639        use meerkat_machine_schema::flow_run_machine;
1640
1641        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1642        let state = kernel.initial_state().expect("initial state");
1643        let created = kernel
1644            .transition(
1645                &state,
1646                &flow_create_run_input(
1647                    &["dep_a", "dep_b", "gated"],
1648                    &[("dep_a", false), ("dep_b", false), ("gated", false)],
1649                    &[
1650                        ("dep_a", &[]),
1651                        ("dep_b", &[]),
1652                        ("gated", &["dep_a", "dep_b"]),
1653                    ],
1654                    &[("dep_a", "All"), ("dep_b", "All"), ("gated", "Any")],
1655                    &[("dep_a", None), ("dep_b", None), ("gated", None)],
1656                ),
1657            )
1658            .expect("create run");
1659        let running = kernel
1660            .transition(
1661                &created.next_state,
1662                &KernelInput {
1663                    variant: "StartRun".into(),
1664                    fields: BTreeMap::new(),
1665                },
1666            )
1667            .expect("start run");
1668        let dep_a_skipped = kernel
1669            .transition(&running.next_state, &flow_step_input("SkipStep", "dep_a"))
1670            .expect("skip dep_a");
1671        let dep_b_skipped = kernel
1672            .transition(
1673                &dep_a_skipped.next_state,
1674                &flow_step_input("SkipStep", "dep_b"),
1675            )
1676            .expect("skip dep_b");
1677
1678        assert!(flow_helper_bool(
1679            &kernel,
1680            &dep_b_skipped.next_state,
1681            "StepDependencyShouldSkip",
1682            "gated",
1683        ));
1684        assert!(!flow_helper_bool(
1685            &kernel,
1686            &dep_b_skipped.next_state,
1687            "StepDependencyReady",
1688            "gated",
1689        ));
1690    }
1691
1692    #[allow(clippy::expect_used)]
1693    #[test]
1694    fn flow_run_quorum_collection_truth_is_machine_derived() {
1695        use meerkat_machine_schema::flow_run_machine;
1696
1697        let kernel = GeneratedMachineKernel::new(flow_run_machine());
1698        let state = kernel.initial_state().expect("initial state");
1699        let created = kernel
1700            .transition(
1701                &state,
1702                &KernelInput {
1703                    variant: "CreateRun".into(),
1704                    fields: BTreeMap::from([
1705                        (
1706                            "step_ids".into(),
1707                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1708                        ),
1709                        (
1710                            "ordered_steps".into(),
1711                            KernelValue::Seq(vec![KernelValue::String("step-a".into())]),
1712                        ),
1713                        (
1714                            "step_has_conditions".into(),
1715                            KernelValue::Map(BTreeMap::from([(
1716                                KernelValue::String("step-a".into()),
1717                                KernelValue::Bool(false),
1718                            )])),
1719                        ),
1720                        (
1721                            "step_dependencies".into(),
1722                            KernelValue::Map(BTreeMap::from([(
1723                                KernelValue::String("step-a".into()),
1724                                KernelValue::Seq(vec![]),
1725                            )])),
1726                        ),
1727                        (
1728                            "step_dependency_modes".into(),
1729                            KernelValue::Map(BTreeMap::from([(
1730                                KernelValue::String("step-a".into()),
1731                                flow_named_variant("DependencyMode", "All"),
1732                            )])),
1733                        ),
1734                        (
1735                            "step_branches".into(),
1736                            KernelValue::Map(BTreeMap::from([(
1737                                KernelValue::String("step-a".into()),
1738                                KernelValue::None,
1739                            )])),
1740                        ),
1741                        (
1742                            "step_collection_policies".into(),
1743                            KernelValue::Map(BTreeMap::from([(
1744                                KernelValue::String("step-a".into()),
1745                                flow_named_variant("CollectionPolicyKind", "Quorum"),
1746                            )])),
1747                        ),
1748                        (
1749                            "step_quorum_thresholds".into(),
1750                            KernelValue::Map(BTreeMap::from([(
1751                                KernelValue::String("step-a".into()),
1752                                KernelValue::U64(2),
1753                            )])),
1754                        ),
1755                        ("escalation_threshold".into(), KernelValue::U64(0)),
1756                        ("max_step_retries".into(), KernelValue::U64(0)),
1757                        ("max_active_nodes".into(), KernelValue::U64(0)),
1758                        ("max_active_frames".into(), KernelValue::U64(0)),
1759                        ("max_frame_depth".into(), KernelValue::U64(0)),
1760                    ]),
1761                },
1762            )
1763            .expect("create run");
1764        let running = kernel
1765            .transition(
1766                &created.next_state,
1767                &KernelInput {
1768                    variant: "StartRun".into(),
1769                    fields: BTreeMap::new(),
1770                },
1771            )
1772            .expect("start run");
1773        let registered = kernel
1774            .transition(
1775                &running.next_state,
1776                &KernelInput {
1777                    variant: "RegisterTargets".into(),
1778                    fields: BTreeMap::from([
1779                        ("step_id".into(), KernelValue::String("step-a".into())),
1780                        ("target_count".into(), KernelValue::U64(3)),
1781                    ]),
1782                },
1783            )
1784            .expect("register targets");
1785        let dispatched = kernel
1786            .transition(
1787                &registered.next_state,
1788                &flow_step_input("DispatchStep", "step-a"),
1789            )
1790            .expect("dispatch step");
1791        let first_success = kernel
1792            .transition(
1793                &dispatched.next_state,
1794                &KernelInput {
1795                    variant: "RecordTargetSuccess".into(),
1796                    fields: BTreeMap::from([
1797                        ("step_id".into(), KernelValue::String("step-a".into())),
1798                        ("target_id".into(), KernelValue::String("worker-1".into())),
1799                    ]),
1800                },
1801            )
1802            .expect("record first success");
1803        assert!(flow_helper_bool(
1804            &kernel,
1805            &first_success.next_state,
1806            "CollectionFeasible",
1807            "step-a",
1808        ));
1809        assert!(!flow_helper_bool(
1810            &kernel,
1811            &first_success.next_state,
1812            "CollectionSatisfied",
1813            "step-a",
1814        ));
1815        let second_success = kernel
1816            .transition(
1817                &first_success.next_state,
1818                &KernelInput {
1819                    variant: "RecordTargetSuccess".into(),
1820                    fields: BTreeMap::from([
1821                        ("step_id".into(), KernelValue::String("step-a".into())),
1822                        ("target_id".into(), KernelValue::String("worker-2".into())),
1823                    ]),
1824                },
1825            )
1826            .expect("record second success");
1827        assert!(flow_helper_bool(
1828            &kernel,
1829            &second_success.next_state,
1830            "CollectionSatisfied",
1831            "step-a",
1832        ));
1833    }
1834
1835    #[allow(clippy::expect_used)]
1836    #[test]
1837    fn named_numeric_aliases_accept_u64_payloads() {
1838        use meerkat_machine_schema::{external_tool_surface_machine, input_lifecycle_machine};
1839
1840        let external_tool_kernel = GeneratedMachineKernel::new(external_tool_surface_machine());
1841        let external_tool_state = external_tool_kernel.initial_state().expect("initial state");
1842        let staged_add = external_tool_kernel
1843            .transition(
1844                &external_tool_state,
1845                &KernelInput {
1846                    variant: "StageAdd".into(),
1847                    fields: BTreeMap::from([(
1848                        "surface_id".into(),
1849                        KernelValue::String("alpha".into()),
1850                    )]),
1851                },
1852            )
1853            .expect("stage add");
1854        let applied = external_tool_kernel
1855            .transition(
1856                &staged_add.next_state,
1857                &KernelInput {
1858                    variant: "ApplyBoundary".into(),
1859                    fields: BTreeMap::from([
1860                        ("surface_id".into(), KernelValue::String("alpha".into())),
1861                        ("applied_at_turn".into(), KernelValue::U64(7)),
1862                    ]),
1863                },
1864            )
1865            .expect("turn-number payload should be accepted");
1866        assert_eq!(applied.transition, "ApplyBoundaryAdd");
1867
1868        let input_lifecycle_kernel = GeneratedMachineKernel::new(input_lifecycle_machine());
1869        let input_lifecycle_state = input_lifecycle_kernel
1870            .initial_state()
1871            .expect("initial state");
1872        let queued = input_lifecycle_kernel
1873            .transition(
1874                &input_lifecycle_state,
1875                &KernelInput {
1876                    variant: "QueueAccepted".into(),
1877                    fields: BTreeMap::new(),
1878                },
1879            )
1880            .expect("queue accepted");
1881        let staged = input_lifecycle_kernel
1882            .transition(
1883                &queued.next_state,
1884                &KernelInput {
1885                    variant: "StageForRun".into(),
1886                    fields: BTreeMap::from([(
1887                        "run_id".into(),
1888                        KernelValue::String("run-1".into()),
1889                    )]),
1890                },
1891            )
1892            .expect("stage for run");
1893        let applied = input_lifecycle_kernel
1894            .transition(
1895                &staged.next_state,
1896                &KernelInput {
1897                    variant: "MarkApplied".into(),
1898                    fields: BTreeMap::from([(
1899                        "run_id".into(),
1900                        KernelValue::String("run-1".into()),
1901                    )]),
1902                },
1903            )
1904            .expect("mark applied");
1905        let boundary_marked = input_lifecycle_kernel
1906            .transition(
1907                &applied.next_state,
1908                &KernelInput {
1909                    variant: "MarkAppliedPendingConsumption".into(),
1910                    fields: BTreeMap::from([("boundary_sequence".into(), KernelValue::U64(1))]),
1911                },
1912            )
1913            .expect("boundary-sequence payload should be accepted");
1914        assert_eq!(boundary_marked.transition, "MarkAppliedPendingConsumption");
1915    }
1916
1917    #[allow(clippy::expect_used)]
1918    #[test]
1919    fn kernel_value_map_roundtrips_through_json() {
1920        let value = KernelValue::Map(BTreeMap::from([
1921            (
1922                KernelValue::String("step-a".into()),
1923                KernelValue::NamedVariant {
1924                    enum_name: "StepRunStatus".into(),
1925                    variant: "Completed".into(),
1926                },
1927            ),
1928            (
1929                KernelValue::String("step-b".into()),
1930                KernelValue::Seq(vec![
1931                    KernelValue::String("dep-1".into()),
1932                    KernelValue::String("dep-2".into()),
1933                ]),
1934            ),
1935        ]));
1936
1937        let encoded = serde_json::to_string(&value).expect("serialize kernel value");
1938        let decoded: KernelValue =
1939            serde_json::from_str(&encoded).expect("deserialize kernel value");
1940
1941        assert_eq!(decoded, value);
1942    }
1943}