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(¶m.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, ¶m.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 ®istered.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}