Skip to main content

determa_state/runtime/
mod.rs

1//! The Determa State runtime engine (SPEC §5). Run-to-completion dispatch over a resolved
2//! [`Machine`]: hierarchical states with LCA exit/entry, esvs with scope, history,
3//! defer, timers over a virtual clock, orthogonal regions + `done`, choice
4//! pseudostates, active objects (spawn/publish/scope), faults, and snapshot/restore.
5
6use crate::cel::{self, CelError, Env};
7use crate::machine::{HistoryKind, Machine, NodeId, StateDef, StateKind};
8use crate::model::Action;
9use crate::value::{map1, Value};
10use std::collections::{BTreeMap, BTreeSet, HashSet, VecDeque};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Status {
14    Active,
15    Faulted,
16    Terminated,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Mode {
21    Auto,
22    Manual,
23}
24
25pub type InstanceId = String;
26
27#[derive(Debug, Clone, PartialEq)]
28pub struct QueuedEvent {
29    pub etype: String,
30    pub payload: Value,
31    pub origin: Option<String>,
32}
33
34#[derive(Debug, Clone, PartialEq)]
35enum QItem {
36    Event(QueuedEvent),
37    Timer { state: NodeId, after_idx: usize },
38}
39
40impl QItem {
41    fn label(&self) -> String {
42        match self {
43            QItem::Event(e) => e.etype.clone(),
44            QItem::Timer { .. } => "__time__".into(),
45        }
46    }
47}
48
49#[derive(Debug, Clone)]
50pub struct ArmedTimer {
51    pub state: NodeId,
52    pub after_idx: usize,
53    pub due: u64,
54}
55
56#[derive(Debug, Clone)]
57pub struct DeadLetterRecord {
58    pub event: QueuedEvent,
59    pub reason: String,
60}
61
62#[derive(Debug, Clone)]
63enum HistRecord {
64    Shallow(Vec<NodeId>),
65    Deep(Vec<NodeId>),
66}
67
68pub struct Instance {
69    pub id: InstanceId,
70    pub parent: Option<InstanceId>,
71    pub def_id: String,
72    pub def_version: i64,
73    pub status: Status,
74    pub mode: Mode,
75    pub active: BTreeSet<NodeId>,
76    pub esvs: BTreeMap<String, Value>,
77    pub external_source: BTreeMap<String, Value>,
78    queue: VecDeque<QItem>,
79    deferred: Vec<QItem>,
80    pub timers: Vec<ArmedTimer>,
81    pub spawn_counter: u64,
82    pub dead_letter: Vec<DeadLetterRecord>,
83    history: BTreeMap<NodeId, HistRecord>,
84    done_emitted: HashSet<NodeId>,
85}
86
87/// A per-RTC-step observer record (SPEC §8 / §14).
88#[derive(Debug, Clone, Default)]
89pub struct StepRecord {
90    pub event: String,
91    pub transition: Option<String>,
92    pub entered: Vec<String>,
93    pub exited: Vec<String>,
94    pub published: Vec<String>,
95    pub spawned: Vec<String>,
96    pub faulted: bool,
97}
98
99#[derive(Debug, Clone, Default)]
100pub struct RunResult {
101    pub published: Vec<String>,
102    pub spawned: Vec<String>,
103}
104
105// ===================== tree helpers =====================
106
107fn ancestors_inclusive(m: &Machine, n: NodeId) -> Vec<NodeId> {
108    let mut out = Vec::new();
109    let mut cur = Some(n);
110    while let Some(c) = cur {
111        out.push(c);
112        cur = m.get(c).parent;
113    }
114    out
115}
116
117fn proper_ancestors(m: &Machine, n: NodeId) -> Vec<NodeId> {
118    let mut out = Vec::new();
119    let mut cur = m.get(n).parent;
120    while let Some(c) = cur {
121        out.push(c);
122        cur = m.get(c).parent;
123    }
124    out
125}
126
127fn is_ancestor_or_equal(m: &Machine, anc: NodeId, n: NodeId) -> bool {
128    let mut cur = Some(n);
129    while let Some(c) = cur {
130        if c == anc {
131            return true;
132        }
133        cur = m.get(c).parent;
134    }
135    false
136}
137
138fn is_strictly_below(m: &Machine, anc: NodeId, n: NodeId) -> bool {
139    n != anc && is_ancestor_or_equal(m, anc, n)
140}
141
142fn is_leaf(m: &Machine, n: NodeId) -> bool {
143    matches!(m.get(n).kind, StateKind::Simple | StateKind::Final)
144}
145
146fn active_leaves(inst: &Instance, m: &Machine) -> Vec<NodeId> {
147    inst.active
148        .iter()
149        .copied()
150        .filter(|&n| is_leaf(m, n))
151        .collect()
152}
153
154fn esv_key(state: &StateDef, name: &str) -> String {
155    format!("{}::{}", state.path, name)
156}
157
158fn nearest_declaring(inst: &Instance, m: &Machine, scope: NodeId, name: &str) -> Option<NodeId> {
159    for s in scope_chain(m, scope) {
160        if inst.active.contains(&s) && m.get(s).esvs.iter().any(|(n, _)| n == name) {
161            return Some(s);
162        }
163    }
164    None
165}
166
167fn resolve_visible(inst: &Instance, m: &Machine, scope: NodeId) -> BTreeMap<String, Value> {
168    let mut out = BTreeMap::new();
169    for s in scope_chain(m, scope) {
170        if !inst.active.contains(&s) {
171            continue;
172        }
173        let sd = m.get(s);
174        for (name, _) in &sd.esvs {
175            if out.contains_key(name) {
176                continue;
177            }
178            if let Some(v) = inst.esvs.get(&esv_key(sd, name)) {
179                out.insert(name.clone(), v.clone());
180            }
181        }
182    }
183    out
184}
185
186/// Esv-scope chain: walk up from `scope`, stopping AT a submachine boundary
187/// (inclusive) — the parent's esvs are not visible inside an inlined submachine
188/// (SPEC §5.6.1).
189fn scope_chain(m: &Machine, scope: NodeId) -> Vec<NodeId> {
190    let mut chain = vec![scope];
191    let mut cur = scope;
192    loop {
193        if m.get(cur).is_sm_boundary {
194            break;
195        }
196        match m.get(cur).parent {
197            Some(p) => {
198                chain.push(p);
199                cur = p;
200                if m.get(cur).is_sm_boundary {
201                    break;
202                }
203            }
204            None => break,
205        }
206    }
207    chain
208}
209
210// ===================== step buffer =====================
211
212struct SpawnReq {
213    parent: InstanceId,
214    scope: NodeId,
215    def_id: String,
216    payload: BTreeMap<String, Value>,
217    result_var: Option<String>,
218}
219
220#[derive(Default)]
221struct StepBuf {
222    deliveries: Vec<(InstanceId, QueuedEvent)>,
223    undirected: Vec<(InstanceId, String, Value)>,
224    spawns: Vec<SpawnReq>,
225    published: Vec<String>,
226    spawned: Vec<String>,
227    spawned_def: Vec<String>,
228    stop: bool,
229}
230
231// ===================== engine =====================
232
233pub struct Engine {
234    pub defs: BTreeMap<(String, i64), Machine>,
235    pub latest: BTreeMap<String, i64>,
236    pub instances: BTreeMap<InstanceId, Instance>,
237    pub clock: u64,
238    pub mode: Mode,
239    pub observer: Option<Box<dyn FnMut(&StepRecord) + Send>>,
240    run_published: Vec<String>,
241    run_spawned: Vec<String>,
242}
243
244impl Default for Engine {
245    fn default() -> Self {
246        Self::new()
247    }
248}
249
250#[derive(Debug)]
251pub enum EngineError {
252    NotFound(String),
253    Validation(String),
254    Faulted(String),
255    Other(String),
256}
257
258#[derive(Debug, Clone)]
259pub struct StateView {
260    pub instance: InstanceId,
261    pub def: String,
262    pub status: Status,
263    pub config: Vec<String>,
264    pub esvs: BTreeMap<String, Value>,
265}
266
267#[derive(Debug, Clone)]
268pub struct ListView {
269    pub id: InstanceId,
270    pub def: String,
271    pub parent: Option<InstanceId>,
272    pub status: Status,
273    pub config: Vec<String>,
274}
275
276#[derive(Debug, Clone)]
277pub struct InspectView {
278    pub instance: InstanceId,
279    pub status: Status,
280    pub config: Vec<String>,
281    pub esvs: BTreeMap<String, Value>,
282    pub enabled: Vec<String>,
283    pub queue: Vec<QueuedEvent>,
284    pub deferred: Vec<QueuedEvent>,
285    pub timers: Vec<ArmedTimer>,
286    pub history: BTreeMap<String, String>,
287    pub dead_letter: Vec<DeadLetterRecord>,
288}
289
290impl Engine {
291    pub fn new() -> Self {
292        Engine {
293            defs: BTreeMap::new(),
294            latest: BTreeMap::new(),
295            instances: BTreeMap::new(),
296            clock: 0,
297            mode: Mode::Auto,
298            observer: None,
299            run_published: Vec::new(),
300            run_spawned: Vec::new(),
301        }
302    }
303
304    pub fn set_observer<F: FnMut(&StepRecord) + Send + 'static>(&mut self, f: F) {
305        self.observer = Some(Box::new(f));
306    }
307
308    pub fn register(&mut self, m: Machine) {
309        let id = m.id.clone();
310        let ver = m.version;
311        self.defs.insert((id.clone(), ver), m);
312        let e = self.latest.entry(id).or_insert(0);
313        if ver > *e {
314            *e = ver;
315        }
316    }
317
318    fn def_ref(&self, id: &str, version: i64) -> &Machine {
319        self.defs
320            .get(&(id.to_string(), version))
321            .expect("definition registered")
322    }
323
324    pub fn def(&self, id: &str, version: Option<i64>) -> Result<&Machine, EngineError> {
325        let v = match version {
326            Some(v) => v,
327            None => *self
328                .latest
329                .get(id)
330                .ok_or_else(|| EngineError::NotFound(format!("definition '{id}'")))?,
331        };
332        self.defs
333            .get(&(id.to_string(), v))
334            .ok_or_else(|| EngineError::NotFound(format!("definition '{id}@{v}'")))
335    }
336
337    pub fn create_root(
338        &mut self,
339        id: &str,
340        def_id: &str,
341        version: Option<i64>,
342        external: &BTreeMap<String, Value>,
343    ) -> Result<(), EngineError> {
344        if self.instances.contains_key(id) {
345            return Err(EngineError::Other(format!("instance '{id}' already exists")));
346        }
347        let (did, ver) = {
348            let m = self.def(def_id, version)?;
349            (m.id.clone(), m.version)
350        };
351        let mut inst = new_instance(id, None, did, ver, self.mode, external);
352        let m = self.def_ref(&inst.def_id, inst.def_version).clone();
353        let mut buf = StepBuf::default();
354        let mut rec = StepRecord::default();
355        enter(&mut inst, &m, m.top, self.clock, &Value::Null, None, &mut buf, &mut rec);
356        descend(&mut inst, &m, m.top, self.clock, &Value::Null, None, &mut buf, &mut rec);
357        self.instances.insert(id.to_string(), inst);
358        self.commit(buf)?;
359        self.run_to_quiescence()?;
360        Ok(())
361    }
362
363    pub fn instance(&self, id: &str) -> Result<&Instance, EngineError> {
364        self.instances
365            .get(id)
366            .ok_or_else(|| EngineError::NotFound(format!("instance '{id}'")))
367    }
368
369    pub fn validate_event(&self, inst_id: &str, etype: &str, payload: &Value) -> Result<(), String> {
370        let inst = self
371            .instances
372            .get(inst_id)
373            .ok_or_else(|| format!("instance '{inst_id}' not found"))?;
374        let m = self.def_ref(&inst.def_id, inst.def_version);
375        validate_event_payload(m, etype, payload)
376    }
377
378    pub fn inject(&mut self, inst_id: &str, etype: &str, payload: Value) -> Result<bool, EngineError> {
379        let ok = {
380            let inst = self.instances.get(inst_id).ok_or_else(|| {
381                EngineError::NotFound(format!("instance '{inst_id}'"))
382            })?;
383            let m = self.def_ref(&inst.def_id, inst.def_version);
384            validate_event_payload(m, etype, &payload).is_ok()
385        };
386        if ok {
387            let inst = self.instances.get_mut(inst_id).unwrap();
388            inst.queue.push_back(QItem::Event(QueuedEvent {
389                etype: etype.to_string(),
390                payload,
391                origin: None,
392            }));
393        }
394        Ok(ok)
395    }
396
397    pub fn send(&mut self, inst_id: &str, etype: &str, payload: Value) -> Result<RunResult, EngineError> {
398        self.run_published.clear();
399        self.run_spawned.clear();
400        let accepted = self.inject(inst_id, etype, payload)?;
401        if accepted && self.mode == Mode::Auto {
402            self.run_to_quiescence()?;
403        }
404        Ok(RunResult {
405            published: std::mem::take(&mut self.run_published),
406            spawned: std::mem::take(&mut self.run_spawned),
407        })
408    }
409
410    pub fn advance(&mut self, duration_ms: u64) -> Result<RunResult, EngineError> {
411        self.run_published.clear();
412        self.run_spawned.clear();
413        self.clock = self.clock.saturating_add(duration_ms);
414        loop {
415            // pick the earliest-due armed timer across all instances
416            let pick: Option<(InstanceId, usize, u64)> = {
417                let mut best: Option<(InstanceId, usize, u64)> = None;
418                for (iid, inst) in &self.instances {
419                    for (i, t) in inst.timers.iter().enumerate() {
420                        if t.due <= self.clock {
421                            let cand = (iid.clone(), i, t.due);
422                            match &best {
423                                None => best = Some(cand),
424                                Some(b) => {
425                                    if (t.due, &iid.clone()) < (b.2, &b.0) {
426                                        best = Some(cand);
427                                    }
428                                }
429                            }
430                        }
431                    }
432                }
433                best
434            };
435            match pick {
436                None => break,
437                Some((iid, i, _)) => {
438                    let inst = self.instances.get_mut(&iid).unwrap();
439                    let timer = inst.timers.remove(i);
440                    inst.queue.push_back(QItem::Timer {
441                        state: timer.state,
442                        after_idx: timer.after_idx,
443                    });
444                }
445            }
446        }
447        self.run_to_quiescence()?;
448        Ok(RunResult {
449            published: std::mem::take(&mut self.run_published),
450            spawned: std::mem::take(&mut self.run_spawned),
451        })
452    }
453
454    pub fn step(&mut self, inst_id: &str, n: usize) -> Result<Vec<StepRecord>, EngineError> {
455        self.run_published.clear();
456        self.run_spawned.clear();
457        let mut records = Vec::new();
458        for _ in 0..n {
459            let has = self
460                .instances
461                .get(inst_id)
462                .map(|i| !i.queue.is_empty())
463                .unwrap_or(false);
464            if !has {
465                break;
466            }
467            let rec = self.dispatch_one(inst_id)?;
468            records.push(rec);
469            self.run_to_quiescence()?;
470        }
471        Ok(records)
472    }
473
474    pub fn env_change(
475        &mut self,
476        inst_id: &str,
477        changed: BTreeMap<String, Value>,
478    ) -> Result<RunResult, EngineError> {
479        let mut payload = BTreeMap::new();
480        payload.insert("changed".to_string(), Value::Map(changed));
481        self.send(inst_id, "env", Value::Map(payload))
482    }
483
484    pub fn set_mode(&mut self, mode: Mode) {
485        self.mode = mode;
486        for inst in self.instances.values_mut() {
487            inst.mode = mode;
488        }
489    }
490    pub fn get_mode(&self) -> Mode {
491        self.mode
492    }
493
494    /// Migrate eligible quiescent instances to a newer registered version (SPEC §10.2).
495    pub fn migrate_quiescent(&mut self) -> Result<(), EngineError> {
496        // gather instance ids + their current def
497        let candidates: Vec<(InstanceId, String, i64)> = self
498            .instances
499            .iter()
500            .filter(|(_, i)| i.status == Status::Active && i.queue.is_empty() && i.deferred.is_empty())
501            .map(|(id, i)| (id.clone(), i.def_id.clone(), i.def_version))
502            .collect();
503        for (iid, def_id, v_old) in candidates {
504            if let Some(new_ver) = self.find_migration_target(&def_id, v_old) {
505                self.apply_migration(&iid, &def_id, v_old, new_ver)?;
506            }
507        }
508        Ok(())
509    }
510
511    fn find_migration_target(&self, def_id: &str, from: i64) -> Option<i64> {
512        let latest = *self.latest.get(def_id)?;
513        if latest <= from {
514            return None;
515        }
516        // find the new def and a migration from->new
517        for v in (from + 1)..=latest {
518            if let Some(new_m) = self.defs.get(&(def_id.to_string(), v)) {
519                if new_m.migrations.iter().any(|mig| mig.from == from && mig.to == v) {
520                    return Some(v);
521                }
522            }
523        }
524        None
525    }
526
527    fn apply_migration(
528        &mut self,
529        iid: &str,
530        def_id: &str,
531        from: i64,
532        to: i64,
533    ) -> Result<(), EngineError> {
534        let migration = {
535            let new_m = self.defs.get(&(def_id.to_string(), to)).cloned();
536            let old_m = self.defs.get(&(def_id.to_string(), from)).cloned();
537            let (new_m, old_m) = match (new_m, old_m) {
538                (Some(n), Some(o)) => (n, o),
539                _ => return Ok(()),
540            };
541            new_m
542                .migrations
543                .iter()
544                .find(|m| m.from == from && m.to == to)
545                .cloned()
546                .map(|m| (m, new_m, old_m))
547        };
548        let (migration, new_m, old_m) = match migration {
549            Some(x) => x,
550            None => return Ok(()),
551        };
552        // current leaf name (single-leaf instances only)
553        let leaf_name = {
554            let inst = self.instances.get(iid).unwrap();
555            let leaves = active_leaves(inst, &old_m);
556            if leaves.len() != 1 {
557                return Ok(()); // not in migration domain
558            }
559            old_m.get(leaves[0]).id.clone()
560        };
561        // `when` check
562        if let Some(when) = &migration.when {
563            let _inst = self.instances.get(iid).unwrap();
564            let mut env = Env::new();
565            env = env.with("state", Value::Str(leaf_name.clone()));
566            if !cel::eval_bool(when, &env).unwrap_or(false) {
567                return Ok(());
568            }
569        }
570        // state_map covers current leaf
571        let new_leaf_name = match migration.state_map.get(&leaf_name) {
572            Some(n) => n.clone(),
573            None => return Ok(()),
574        };
575        let new_leaf = match new_m.by_name.get(&new_leaf_name).copied() {
576            Some(n) => n,
577            None => return Ok(()),
578        };
579        // apply: remap active config, keep esvs (top-level keys identical), run esvs transform
580        let inst = self.instances.get_mut(iid).unwrap();
581        inst.active.clear();
582        for a in ancestors_inclusive(&new_m, new_leaf) {
583            inst.active.insert(a);
584        }
585        inst.def_version = to;
586        // run esvs transforms over current esvs (state binding = leaf)
587        let mut buf = StepBuf::default();
588        let mut rec = StepRecord::default();
589        let event = Value::Null;
590        // build env from top esvs + state
591        let scope = new_m.top;
592        let mut env = build_env(inst, &new_m, scope, &event);
593        env = env.with("state", Value::Str(leaf_name.clone()));
594        for a in &migration.esvs {
595            // run a single assign-style action directly with this env
596            if let Action::Assign(pairs) = a {
597                for (name, expr) in pairs {
598                    if let Ok(val) = cel::eval(expr, &env) {
599                        if let Some(st) = nearest_declaring(inst, &new_m, scope, name) {
600                            let decl = new_m
601                                .get(st)
602                                .esvs
603                                .iter()
604                                .find(|(n, _)| n == name)
605                                .map(|(_, d)| d.clone())
606                                .unwrap();
607                            if let Some(c) = val.coerce_to(&decl.ty) {
608                                inst.esvs.insert(esv_key(new_m.get(st), name), c);
609                            }
610                        }
611                    }
612                }
613            } else {
614                let _ = run_one(inst, &new_m, scope, a, &event, None, &mut buf, &mut rec);
615            }
616        }
617        let _ = (buf, rec);
618        Ok(())
619    }
620
621    // -------- quiescence --------
622
623    fn run_to_quiescence(&mut self) -> Result<(), EngineError> {
624        let mut guard = 0u32;
625        loop {
626            guard += 1;
627            if guard > 200_000 {
628                return Err(EngineError::Other("runaway quiescence loop".into()));
629            }
630            let target = self
631                .instances
632                .iter()
633                .filter(|(_, i)| i.status == Status::Active && i.mode == Mode::Auto && !i.queue.is_empty())
634                .map(|(id, _)| id.clone())
635                .next();
636            match target {
637                Some(id) => {
638                    self.dispatch_one(&id)?;
639                }
640                None => break,
641            }
642        }
643        Ok(())
644    }
645
646    fn dispatch_one(&mut self, inst_id: &str) -> Result<StepRecord, EngineError> {
647        let (did, ver) = {
648            let inst = self.instances.get(inst_id).ok_or_else(|| {
649                EngineError::NotFound(format!("instance '{inst_id}'"))
650            })?;
651            (inst.def_id.clone(), inst.def_version)
652        };
653        let m = self.def_ref(&did, ver).clone();
654        let item = self.instances.get_mut(inst_id).unwrap().queue.pop_front();
655        let rec = match item {
656            None => StepRecord::default(),
657            Some(qi) => {
658                let rec = self.execute_rtc(inst_id, &m, qi);
659                if let Some(o) = &mut self.observer {
660                    o(&rec);
661                }
662                rec
663            }
664        };
665        Ok(rec)
666    }
667
668    fn execute_rtc(&mut self, inst_id: &str, m: &Machine, item: QItem) -> StepRecord {
669        let mut rec = StepRecord {
670            event: item.label(),
671            ..Default::default()
672        };
673        let etype = match &item {
674            QItem::Event(e) => Some(e.etype.clone()),
675            QItem::Timer { .. } => None,
676        };
677        let event_value = match &item {
678            QItem::Event(e) => event_binding(e),
679            QItem::Timer { .. } => Value::Null,
680        };
681
682        // snapshot for rollback
683        let backup = self.instances.get(inst_id).map(|i| Instance::snapshot_state(i));
684
685        // env: update external source
686        if etype.as_deref() == Some("env") {
687            if let Some(changed) = changed_of(&event_value) {
688                for (k, v) in changed {
689                    self.instances
690                        .get_mut(inst_id)
691                        .unwrap()
692                        .external_source
693                        .insert(k, v);
694                }
695            }
696        }
697
698        let handlers = match &item {
699            QItem::Event(e) => find_handlers(self.instances.get_mut(inst_id).unwrap(), m, &e.etype, &event_value),
700            QItem::Timer { state, after_idx } => vec![Handler { state: *state, kind: HKind::Timer(*after_idx) }],
701        };
702
703        let mut buf = StepBuf::default();
704
705        if handlers.is_empty() {
706            let defer_set = effective_defer(self.instances.get(inst_id).unwrap(), m);
707            let do_defer = match &item {
708                QItem::Event(e) => defer_set.contains(&e.etype),
709                QItem::Timer { .. } => false,
710            };
711            if do_defer {
712                self.instances.get_mut(inst_id).unwrap().deferred.push(item);
713            }
714            return rec;
715        }
716
717        // execute handlers
718        let mut faulted: Option<CelError> = None;
719        for h in &handlers {
720            let inst = self.instances.get_mut(inst_id).unwrap();
721            if let Err(e) = execute_handler(inst, m, h, &event_value, etype.as_deref(), &mut buf, &mut rec) {
722                faulted = Some(e);
723                break;
724            }
725        }
726
727        if let Some(fault) = faulted {
728            // rollback
729            if let Some(b) = backup {
730                let inst = self.instances.get_mut(inst_id).unwrap();
731                inst.restore_state(b);
732            }
733            rec.faulted = true;
734            rec.published.clear();
735            rec.spawned.clear();
736            rec.entered.clear();
737            rec.exited.clear();
738            rec.transition = None;
739            if let QItem::Event(e) = &item {
740                self.instances.get_mut(inst_id).unwrap().dead_letter.push(DeadLetterRecord {
741                    event: e.clone(),
742                    reason: fault.to_string(),
743                });
744            }
745            let has_err = scope_has_error_handler(self.instances.get(inst_id).unwrap(), m);
746            if has_err {
747                self.instances.get_mut(inst_id).unwrap().queue.push_front(QItem::Event(QueuedEvent {
748                    etype: "error".into(),
749                    payload: Value::Map(error_payload(&fault)),
750                    origin: None,
751                }));
752            } else {
753                self.instances.get_mut(inst_id).unwrap().status = Status::Faulted;
754            }
755            return rec;
756        }
757
758        // commit cross-instance effects
759        let _ = self.commit(buf);
760
761        // post-step: done + termination + defer replay
762        let m2 = m.clone();
763        self.post_step(inst_id, &m2);
764
765        // stop action?
766        if self.instances.get(inst_id).map(|i| i.status == Status::Active).unwrap_or(false) {
767            // handle pending stop items already enqueued as events in the queue loop
768        }
769
770        rec
771    }
772
773    fn post_step(&mut self, inst_id: &str, m: &Machine) {
774        // Completion (SPEC §5.6 / §5.6.1): any active composite whose active leaf is
775        // final, or orthogonal whose regions are all final, generates a `done` event
776        // for its parent. `top` completion terminates a spawned instance instead.
777        let complete: Vec<NodeId> = self
778            .instances
779            .get(inst_id)
780            .map(|i| complete_states(i, m))
781            .unwrap_or_default();
782        for s in complete {
783            if s == m.top {
784                continue;
785            }
786            let already = self.instances.get(inst_id).unwrap().done_emitted.contains(&s);
787            if already {
788                continue;
789            }
790            let leaf_name = self
791                .instances
792                .get(inst_id)
793                .map(|i| composite_leaf_name(i, m, s))
794                .unwrap_or_default();
795            let inst = self.instances.get_mut(inst_id).unwrap();
796            inst.done_emitted.insert(s);
797            inst.queue.push_back(QItem::Event(QueuedEvent {
798                etype: "done".into(),
799                payload: map1("state", Value::Str(leaf_name)),
800                origin: None,
801            }));
802        }
803        // termination via top-level final (spawned instances only) or stop
804        self.handle_termination(inst_id, m);
805        // defer replay
806        let inst = self.instances.get_mut(inst_id).unwrap();
807        replay_deferred(inst, m);
808    }
809
810    fn handle_termination(&mut self, inst_id: &str, m: &Machine) {
811        let (terminate, parent_id) = {
812            let inst = match self.instances.get(inst_id) {
813                Some(i) if i.status == Status::Active => i,
814                _ => return,
815            };
816            let top_final = inst.active.iter().any(|&n| {
817                m.get(n).kind == StateKind::Final && m.get(n).parent == Some(m.top)
818            });
819            let stop_pending = inst
820                .queue
821                .iter()
822                .any(|q| matches!(q, QItem::Event(e) if e.etype == "__stop__"));
823            let do_term = (top_final && inst.parent.is_some()) || stop_pending;
824            (do_term, inst.parent.clone())
825        };
826        if terminate {
827            let inst = self.instances.get_mut(inst_id).unwrap();
828            // remove any pending stop marker
829            inst.queue.retain(|q| !matches!(q, QItem::Event(e) if e.etype == "__stop__"));
830            // run exit actions from leaves up
831            let leaves: Vec<NodeId> = inst.active.iter().copied().filter(|&n| is_leaf(m, n)).collect();
832            let mut sink = StepBuf::default();
833            for leaf in leaves {
834                let mut rec = StepRecord::default();
835                exit_up_to(inst, m, leaf, Some(m.top), &mut sink, &mut rec);
836            }
837            inst.active.clear();
838            inst.status = Status::Terminated;
839            inst.done_emitted.clear();
840            // commit any exit-time publishes (rare) then deliver done to parent
841            // (we enqueue directly to parent here)
842            let _ = sink;
843            if let Some(parent) = parent_id {
844                if let Some(p) = self.instances.get_mut(&parent) {
845                    if p.status == Status::Active {
846                        p.queue.push_back(QItem::Event(QueuedEvent {
847                            etype: "done".into(),
848                            payload: Value::Null,
849                            origin: Some(inst_id.to_string()),
850                        }));
851                    }
852                }
853            }
854        }
855    }
856
857    fn commit(&mut self, buf: StepBuf) -> Result<(), EngineError> {
858        self.run_published.extend(buf.published);
859        self.run_spawned.extend(buf.spawned_def);
860
861        // directed deliveries
862        for (target, ev) in buf.deliveries {
863            if let Some(t) = self.instances.get_mut(&target) {
864                if t.status == Status::Active {
865                    t.queue.push_back(QItem::Event(ev));
866                }
867            }
868        }
869        // undirected (scope + subscription)
870        for (publisher, etype, payload) in buf.undirected {
871            let scope = self
872                .instances
873                .get(&publisher)
874                .and_then(|p| {
875                    let m = self.defs.get(&(p.def_id.clone(), p.def_version))?;
876                    m.events.get(&etype).map(|d| d.scope)
877                })
878                .unwrap_or(crate::machine::Scope::Internal);
879            let targets = self.undirected_targets(&publisher, &etype, scope);
880            for t in targets {
881                if let Some(ti) = self.instances.get_mut(&t) {
882                    if ti.status == Status::Active {
883                        ti.queue.push_back(QItem::Event(QueuedEvent {
884                            etype: etype.clone(),
885                            payload: payload.clone(),
886                            origin: Some(publisher.clone()),
887                        }));
888                    }
889                }
890            }
891        }
892        // spawns
893        for sp in buf.spawns {
894            let parent_id = sp.parent.clone();
895            let n = {
896                let p = self.instances.get_mut(&parent_id).unwrap();
897                p.spawn_counter += 1;
898                p.spawn_counter
899            };
900            let child_id = format!("{parent_id}/{n}");
901            // result var in parent
902            if let Some(res) = &sp.result_var {
903                let m_clone = self
904                    .defs
905                    .get({
906                        let p = self.instances.get(&parent_id).unwrap();
907                        &(p.def_id.clone(), p.def_version)
908                    })
909                    .cloned();
910                if let Some(m) = m_clone {
911                    let p = self.instances.get_mut(&parent_id).unwrap();
912                    if let Some(st) = nearest_declaring(p, &m, sp.scope, res) {
913                        p.esvs.insert(esv_key(m.get(st), res), Value::Str(child_id.clone()));
914                    }
915                }
916            }
917            // create child
918            let child_m = match self.def(&sp.def_id, None) {
919                Ok(cm) => cm.clone(),
920                Err(_) => continue,
921            };
922            let mut child = new_instance(
923                &child_id,
924                Some(parent_id.clone()),
925                child_m.id.clone(),
926                child_m.version,
927                self.mode,
928                &BTreeMap::new(),
929            );
930            // seed external esvs from spawn payload by name
931            for (name, _) in &child_m.get(child_m.top).esvs.clone() {
932                if let Some(v) = sp.payload.get(name) {
933                    child.external_source.insert(name.clone(), v.clone());
934                }
935            }
936            let mut cbuf = StepBuf::default();
937            let mut crec = StepRecord::default();
938            enter(&mut child, &child_m, child_m.top, self.clock, &Value::Null, None, &mut cbuf, &mut crec);
939            descend(&mut child, &child_m, child_m.top, self.clock, &Value::Null, None, &mut cbuf, &mut crec);
940            self.instances.insert(child_id, child);
941            // recursively commit child's entry-time effects
942            self.commit(cbuf)?;
943        }
944        Ok(())
945    }
946
947    fn undirected_targets(
948        &self,
949        publisher: &str,
950        etype: &str,
951        scope: crate::machine::Scope,
952    ) -> Vec<InstanceId> {
953        use crate::machine::Scope;
954        // build candidate set by scope
955        let candidates: Vec<InstanceId> = match scope {
956            Scope::Internal => vec![publisher.to_string()],
957            Scope::Global => self.instances.keys().cloned().collect(),
958            Scope::Local => {
959                // publisher's tree: ancestors + descendants + self
960                let mut set: HashSet<InstanceId> = HashSet::new();
961                // ancestors
962                let mut cur = Some(publisher.to_string());
963                while let Some(id) = cur {
964                    set.insert(id.clone());
965                    cur = self.instances.get(&id).and_then(|i| i.parent.clone());
966                }
967                // descendants
968                let mut stack = vec![publisher.to_string()];
969                while let Some(id) = stack.pop() {
970                    set.insert(id.clone());
971                    for (other_id, other) in &self.instances {
972                        if other.parent.as_deref() == Some(id.as_str()) {
973                            stack.push(other_id.clone());
974                        }
975                    }
976                }
977                set.into_iter().collect()
978            }
979        };
980        candidates
981            .into_iter()
982            .filter(|cid| {
983                if scope == Scope::Internal && cid == publisher {
984                    return true; // always self-receive internal
985                }
986                let inst = match self.instances.get(cid) {
987                    Some(i) => i,
988                    None => return false,
989                };
990                let m = self.defs.get(&(inst.def_id.clone(), inst.def_version));
991                m.map(|mm| mm.subscribe.iter().any(|s| s == etype))
992                    .unwrap_or(false)
993            })
994            .collect()
995    }
996
997    // -------- views / snapshot --------
998
999    pub fn state_view(&self, inst_id: &str) -> Result<StateView, EngineError> {
1000        let inst = self.instance(inst_id)?;
1001        let m = self.def_ref(&inst.def_id, inst.def_version);
1002        Ok(StateView {
1003            instance: inst_id.to_string(),
1004            def: format!("{}@{}", inst.def_id, inst.def_version),
1005            status: inst.status,
1006            config: config_of(inst, m),
1007            esvs: reported_esvs(inst, m),
1008        })
1009    }
1010
1011    pub fn list_view(&self) -> Vec<ListView> {
1012        self.instances
1013            .iter()
1014            .map(|(id, inst)| {
1015                let m = self.def_ref(&inst.def_id, inst.def_version);
1016                ListView {
1017                    id: id.clone(),
1018                    def: format!("{}@{}", inst.def_id, inst.def_version),
1019                    parent: inst.parent.clone(),
1020                    status: inst.status,
1021                    config: config_of(inst, m),
1022                }
1023            })
1024            .collect()
1025    }
1026
1027    pub fn inspect_view(&self, inst_id: &str) -> Result<InspectView, EngineError> {
1028        let inst = self.instance(inst_id)?;
1029        let m = self.def_ref(&inst.def_id, inst.def_version);
1030        let history = inst
1031            .history
1032            .iter()
1033            .map(|(n, h)| {
1034                let kind = match h {
1035                    HistRecord::Shallow(_) => "shallow",
1036                    HistRecord::Deep(_) => "deep",
1037                };
1038                (m.get(*n).path.clone(), kind.to_string())
1039            })
1040            .collect();
1041        Ok(InspectView {
1042            instance: inst_id.to_string(),
1043            status: inst.status,
1044            config: config_of(inst, m),
1045            esvs: reported_esvs(inst, m),
1046            enabled: enabled_events(inst, m),
1047            queue: inst.queue.iter().filter_map(|q| match q {
1048                QItem::Event(e) => Some(e.clone()),
1049                _ => None,
1050            }).collect(),
1051            deferred: inst.deferred.iter().filter_map(|q| match q {
1052                QItem::Event(e) => Some(e.clone()),
1053                _ => None,
1054            }).collect(),
1055            timers: inst.timers.clone(),
1056            history,
1057            dead_letter: inst.dead_letter.clone(),
1058        })
1059    }
1060
1061    /// `enabled_events(instance)` — sorted declared event types the current active
1062    /// configuration can handle (SPEC §14).
1063    pub fn enabled_events(&self, inst_id: &str) -> Result<Vec<String>, EngineError> {
1064        let inst = self.instance(inst_id)?;
1065        let m = self.def_ref(&inst.def_id, inst.def_version);
1066        Ok(enabled_events(inst, m))
1067    }
1068
1069    pub fn snapshot(&self, inst_id: &str) -> Result<Snapshot, EngineError> {
1070        let inst = self.instance(inst_id)?;
1071        let m = self.def_ref(&inst.def_id, inst.def_version);
1072        Snapshot::from_instance(inst, m)
1073    }
1074
1075    pub fn restore(&mut self, snap: Snapshot) -> Result<(), EngineError> {
1076        let m = self
1077            .defs
1078            .get(&(snap.def_id.clone(), snap.def_version))
1079            .ok_or_else(|| EngineError::NotFound(format!("definition '{}@{}'", snap.def_id, snap.def_version)))?
1080            .clone();
1081        let inst = snap.into_instance(&m);
1082        self.instances.insert(inst.id.clone(), inst);
1083        Ok(())
1084    }
1085}
1086
1087// ===================== snapshot =====================
1088
1089#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1090pub struct Snapshot {
1091    pub def_id: String,
1092    pub def_version: i64,
1093    pub id: InstanceId,
1094    pub parent_id: Option<InstanceId>,
1095    pub status: String,
1096    pub state_config: Vec<String>,
1097    pub esvs: BTreeMap<String, Value>,
1098    pub external_source: BTreeMap<String, Value>,
1099    pub queue: Vec<SnapEvent>,
1100    pub deferred: Vec<SnapEvent>,
1101    pub timers: Vec<SnapTimer>,
1102    pub dead_letter: Vec<SnapDeadLetter>,
1103    pub history: BTreeMap<String, SnapHist>,
1104    pub spawn_counter: u64,
1105    pub mode: String,
1106    #[serde(default)]
1107    pub clock: u64,
1108}
1109
1110#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1111pub struct SnapEvent {
1112    pub etype: String,
1113    pub payload: Value,
1114    pub origin: Option<String>,
1115}
1116#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1117pub struct SnapTimer {
1118    pub state: String,
1119    pub after_idx: usize,
1120    pub due: u64,
1121}
1122#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1123pub struct SnapDeadLetter {
1124    pub event: SnapEvent,
1125    pub reason: String,
1126}
1127#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
1128pub struct SnapHist {
1129    pub kind: String,
1130    pub states: Vec<String>,
1131}
1132
1133impl Snapshot {
1134    fn from_instance(inst: &Instance, m: &Machine) -> Result<Snapshot, EngineError> {
1135        let state_config: Vec<String> = active_leaves(inst, m)
1136            .into_iter()
1137            .map(|n| m.get(n).path.clone())
1138            .collect();
1139        let history = inst
1140            .history
1141            .iter()
1142            .map(|(n, h)| {
1143                let (kind, states) = match h {
1144                    HistRecord::Shallow(s) => ("shallow", s.clone()),
1145                    HistRecord::Deep(s) => ("deep", s.clone()),
1146                };
1147                (
1148                    m.get(*n).path.clone(),
1149                    SnapHist {
1150                        kind: kind.to_string(),
1151                        states: states.iter().map(|x| m.get(*x).path.clone()).collect(),
1152                    },
1153                )
1154            })
1155            .collect();
1156        Ok(Snapshot {
1157            def_id: inst.def_id.clone(),
1158            def_version: inst.def_version,
1159            id: inst.id.clone(),
1160            parent_id: inst.parent.clone(),
1161            status: status_str(inst.status).to_string(),
1162            state_config,
1163            esvs: inst.esvs.clone(),
1164            external_source: inst.external_source.clone(),
1165            queue: snap_events(inst.queue.iter()),
1166            deferred: snap_events(inst.deferred.iter()),
1167            timers: inst
1168                .timers
1169                .iter()
1170                .map(|t| SnapTimer {
1171                    state: m.get(t.state).path.clone(),
1172                    after_idx: t.after_idx,
1173                    due: t.due,
1174                })
1175                .collect(),
1176            dead_letter: inst
1177                .dead_letter
1178                .iter()
1179                .map(|d| SnapDeadLetter {
1180                    event: SnapEvent {
1181                        etype: d.event.etype.clone(),
1182                        payload: d.event.payload.clone(),
1183                        origin: d.event.origin.clone(),
1184                    },
1185                    reason: d.reason.clone(),
1186                })
1187                .collect(),
1188            history,
1189            spawn_counter: inst.spawn_counter,
1190            mode: match inst.mode {
1191                Mode::Auto => "auto",
1192                Mode::Manual => "manual",
1193            }
1194            .into(),
1195            clock: 0,
1196        })
1197    }
1198
1199    fn into_instance(self, m: &Machine) -> Instance {
1200        let status = match self.status.as_str() {
1201            "faulted" => Status::Faulted,
1202            "terminated" => Status::Terminated,
1203            _ => Status::Active,
1204        };
1205        let mode = match self.mode.as_str() {
1206            "manual" => Mode::Manual,
1207            _ => Mode::Auto,
1208        };
1209        let leaves: Vec<NodeId> = self
1210            .state_config
1211            .iter()
1212            .filter_map(|p| m.by_path.get(p).copied())
1213            .collect();
1214        let mut inst = Instance {
1215            id: self.id,
1216            parent: self.parent_id,
1217            def_id: self.def_id,
1218            def_version: self.def_version,
1219            status,
1220            mode,
1221            active: BTreeSet::new(),
1222            esvs: self.esvs,
1223            external_source: self.external_source,
1224            queue: self.queue.into_iter().map(|e| QItem::Event(QueuedEvent {
1225                etype: e.etype,
1226                payload: e.payload,
1227                origin: e.origin,
1228            })).collect(),
1229            deferred: self.deferred.into_iter().map(|e| QItem::Event(QueuedEvent {
1230                etype: e.etype,
1231                payload: e.payload,
1232                origin: e.origin,
1233            })).collect(),
1234            timers: self.timers.into_iter().filter_map(|t| {
1235                m.by_path.get(&t.state).copied().map(|state| ArmedTimer {
1236                    state,
1237                    after_idx: t.after_idx,
1238                    due: t.due,
1239                })
1240            }).collect(),
1241            spawn_counter: self.spawn_counter,
1242            dead_letter: self.dead_letter.into_iter().map(|d| DeadLetterRecord {
1243                event: QueuedEvent {
1244                    etype: d.event.etype,
1245                    payload: d.event.payload,
1246                    origin: d.event.origin,
1247                },
1248                reason: d.reason,
1249            }).collect(),
1250            history: self.history.into_iter().filter_map(|(path, h)| {
1251                let state = m.by_path.get(&path).copied()?;
1252                let states: Vec<NodeId> = h
1253                    .states
1254                    .iter()
1255                    .filter_map(|p| m.by_path.get(p).copied())
1256                    .collect();
1257                let rec = if h.kind == "shallow" {
1258                    HistRecord::Shallow(states)
1259                } else {
1260                    HistRecord::Deep(states)
1261                };
1262                Some((state, rec))
1263            }).collect(),
1264            done_emitted: HashSet::new(),
1265        };
1266        for leaf in leaves {
1267            for a in ancestors_inclusive(m, leaf) {
1268                inst.active.insert(a);
1269            }
1270        }
1271        inst
1272    }
1273}
1274
1275fn snap_events<'a>(items: impl Iterator<Item = &'a QItem>) -> Vec<SnapEvent> {
1276    items
1277        .filter_map(|q| match q {
1278            QItem::Event(e) => Some(SnapEvent {
1279                etype: e.etype.clone(),
1280                payload: e.payload.clone(),
1281                origin: e.origin.clone(),
1282            }),
1283            QItem::Timer { .. } => None,
1284        })
1285        .collect()
1286}
1287
1288fn status_str(s: Status) -> &'static str {
1289    match s {
1290        Status::Active => "active",
1291        Status::Faulted => "faulted",
1292        Status::Terminated => "terminated",
1293    }
1294}
1295
1296// ===================== instance helpers =====================
1297
1298fn new_instance(
1299    id: &str,
1300    parent: Option<InstanceId>,
1301    def_id: String,
1302    def_version: i64,
1303    mode: Mode,
1304    external: &BTreeMap<String, Value>,
1305) -> Instance {
1306    Instance {
1307        id: id.to_string(),
1308        parent,
1309        def_id,
1310        def_version,
1311        status: Status::Active,
1312        mode,
1313        active: BTreeSet::new(),
1314        esvs: BTreeMap::new(),
1315        external_source: external.clone(),
1316        queue: VecDeque::new(),
1317        deferred: Vec::new(),
1318        timers: Vec::new(),
1319        spawn_counter: 0,
1320        dead_letter: Vec::new(),
1321        history: BTreeMap::new(),
1322        done_emitted: HashSet::new(),
1323    }
1324}
1325
1326impl Instance {
1327    fn snapshot_state(&self) -> InstanceState {
1328        InstanceState {
1329            active: self.active.clone(),
1330            esvs: self.esvs.clone(),
1331            timers: self.timers.clone(),
1332            history: self.history.clone(),
1333            done_emitted: self.done_emitted.clone(),
1334            deferred: self.deferred.clone(),
1335            spawn_counter: self.spawn_counter,
1336            external_source: self.external_source.clone(),
1337            status: self.status,
1338            queue: self.queue.clone(),
1339        }
1340    }
1341    fn restore_state(&mut self, s: InstanceState) {
1342        self.active = s.active;
1343        self.esvs = s.esvs;
1344        self.timers = s.timers;
1345        self.history = s.history;
1346        self.done_emitted = s.done_emitted;
1347        self.deferred = s.deferred;
1348        self.spawn_counter = s.spawn_counter;
1349        self.external_source = s.external_source;
1350        self.status = s.status;
1351        self.queue = s.queue;
1352    }
1353}
1354
1355struct InstanceState {
1356    active: BTreeSet<NodeId>,
1357    esvs: BTreeMap<String, Value>,
1358    timers: Vec<ArmedTimer>,
1359    history: BTreeMap<NodeId, HistRecord>,
1360    done_emitted: HashSet<NodeId>,
1361    deferred: Vec<QItem>,
1362    spawn_counter: u64,
1363    external_source: BTreeMap<String, Value>,
1364    status: Status,
1365    queue: VecDeque<QItem>,
1366}
1367
1368// ===================== handler discovery =====================
1369
1370enum HKind {
1371    Transition(usize),
1372    Timer(usize),
1373}
1374struct Handler {
1375    state: NodeId,
1376    kind: HKind,
1377}
1378
1379fn find_handlers(inst: &Instance, m: &Machine, etype: &str, event_value: &Value) -> Vec<Handler> {
1380    let leaves = active_leaves(inst, m);
1381    let mut found: Vec<Handler> = Vec::new();
1382    let mut seen: HashSet<NodeId> = HashSet::new();
1383    for leaf in leaves {
1384        for s in ancestors_inclusive(m, leaf) {
1385            if !inst.active.contains(&s) {
1386                continue;
1387            }
1388            if let Some(list) = m.get(s).on_events.get(etype) {
1389                let env = build_env(inst, m, s, event_value);
1390                let mut matched = None;
1391                for (i, t) in list.iter().enumerate() {
1392                    let pass = match &t.guard {
1393                        Some(g) => cel::eval_bool(g, &env).unwrap_or(false),
1394                        None => true,
1395                    };
1396                    if pass {
1397                        matched = Some(i);
1398                        break;
1399                    }
1400                }
1401                if let Some(i) = matched {
1402                    if seen.insert(s) {
1403                        found.push(Handler { state: s, kind: HKind::Transition(i) });
1404                    }
1405                }
1406                break; // this state claims the event for this leaf
1407            }
1408        }
1409    }
1410    found
1411}
1412
1413fn scope_has_error_handler(inst: &Instance, m: &Machine) -> bool {
1414    inst.active
1415        .iter()
1416        .any(|&n| m.get(n).on_events.contains_key("error"))
1417}
1418
1419// ===================== transition execution =====================
1420
1421fn execute_handler(
1422    inst: &mut Instance,
1423    m: &Machine,
1424    h: &Handler,
1425    event_value: &Value,
1426    etype: Option<&str>,
1427    buf: &mut StepBuf,
1428    rec: &mut StepRecord,
1429) -> Result<(), CelError> {
1430    match &h.kind {
1431        HKind::Timer(idx) => {
1432            let after = m.get(h.state).after[*idx].clone();
1433            if let Some(g) = &after.guard {
1434                let env = build_env(inst, m, h.state, event_value);
1435                if !cel::eval_bool(g, &env)? {
1436                    return Ok(());
1437                }
1438            }
1439            run_actions(inst, m, h.state, &after.action, event_value, etype, buf, rec)?;
1440            if let Some(target) = after.target {
1441                rec.transition = Some(m.get(target).id.clone());
1442                external_transition(inst, m, h.state, target, &[], event_value, etype, buf, rec)?;
1443            }
1444            Ok(())
1445        }
1446        HKind::Transition(idx) => {
1447            let list = m.get(h.state).on_events.get(etype.unwrap_or("")).cloned();
1448            let list = match list {
1449                Some(l) => l,
1450                None => return Ok(()),
1451            };
1452            let t = list[*idx].clone();
1453            if t.target.is_none() || t.internal {
1454                run_actions(inst, m, h.state, &t.action, event_value, etype, buf, rec)?;
1455                rec.transition = None;
1456                return Ok(());
1457            }
1458            let target = t.target.unwrap();
1459            if t.local {
1460                rec.transition = Some(m.get(target).id.clone());
1461                run_actions(inst, m, h.state, &t.action, event_value, etype, buf, rec)?;
1462                local_transition(inst, m, h.state, target, event_value, etype, buf, rec)?;
1463                return Ok(());
1464            }
1465            rec.transition = Some(m.get(target).id.clone());
1466            let is_choice = m.get(target).kind == StateKind::Choice;
1467            if is_choice {
1468                run_actions(inst, m, h.state, &t.action, event_value, etype, buf, rec)?;
1469                let final_target = resolve_choice(inst, m, h.state, target, event_value, etype, buf, rec)?;
1470                external_transition(inst, m, h.state, final_target, &[], event_value, etype, buf, rec)?;
1471            } else {
1472                external_transition(inst, m, h.state, target, &t.action, event_value, etype, buf, rec)?;
1473            }
1474            Ok(())
1475        }
1476    }
1477}
1478
1479fn resolve_choice(
1480    inst: &mut Instance,
1481    m: &Machine,
1482    source: NodeId,
1483    start: NodeId,
1484    event_value: &Value,
1485    etype: Option<&str>,
1486    buf: &mut StepBuf,
1487    rec: &mut StepRecord,
1488) -> Result<NodeId, CelError> {
1489    let mut cur = start;
1490    loop {
1491        let branches = m.get(cur).choice.clone().unwrap_or_default();
1492        let env = build_env(inst, m, source, event_value);
1493        let mut chosen: Option<usize> = None;
1494        for (i, b) in branches.iter().enumerate() {
1495            let pass = match &b.guard {
1496                Some(g) => cel::eval_bool(g, &env)?,
1497                None => true,
1498            };
1499            if pass {
1500                chosen = Some(i);
1501                break;
1502            }
1503        }
1504        let i = chosen.ok_or_else(|| CelError::Other("no choice branch matched".into()))?;
1505        let b = branches[i].clone();
1506        run_actions(inst, m, source, &b.action, event_value, etype, buf, rec)?;
1507        if m.get(b.target).kind == StateKind::Choice {
1508            cur = b.target;
1509            continue;
1510        }
1511        return Ok(b.target);
1512    }
1513}
1514
1515fn external_transition(
1516    inst: &mut Instance,
1517    m: &Machine,
1518    source: NodeId,
1519    target: NodeId,
1520    action: &[Action],
1521    event_value: &Value,
1522    etype: Option<&str>,
1523    buf: &mut StepBuf,
1524    rec: &mut StepRecord,
1525) -> Result<(), CelError> {
1526    let lca = lca_external(m, source, target);
1527    let anchor = child_below(m, lca, source);
1528    // exit: active states strictly below lca, on the source side
1529    let mut to_exit: Vec<NodeId> = inst
1530        .active
1531        .iter()
1532        .copied()
1533        .filter(|&x| is_strictly_below(m, lca, x) && is_ancestor_or_equal(m, anchor, x))
1534        .collect();
1535    to_exit.sort_by(|a, b| m.get(*b).depth.cmp(&m.get(*a).depth));
1536    // record history BEFORE descendants are removed (deepest composites first)
1537    for &x in &to_exit {
1538        if m.get(x).has_history() {
1539            record_history(inst, m, x);
1540        }
1541    }
1542    for x in to_exit {
1543        exit_state(inst, m, x, buf, rec);
1544    }
1545    if !action.is_empty() {
1546        run_actions(inst, m, source, action, event_value, etype, buf, rec)?;
1547    }
1548    let path = entry_path_below(m, lca, target);
1549    for &s in &path {
1550        enter(inst, m, s, 0, event_value, etype, buf, rec);
1551    }
1552    if let Some(&tgt) = path.last() {
1553        descend(inst, m, tgt, 0, event_value, etype, buf, rec);
1554    }
1555    Ok(())
1556}
1557
1558fn local_transition(
1559    inst: &mut Instance,
1560    m: &Machine,
1561    source: NodeId,
1562    target: NodeId,
1563    event_value: &Value,
1564    etype: Option<&str>,
1565    buf: &mut StepBuf,
1566    rec: &mut StepRecord,
1567) -> Result<(), CelError> {
1568    let mut to_exit: Vec<NodeId> = inst
1569        .active
1570        .iter()
1571        .copied()
1572        .filter(|&x| is_strictly_below(m, source, x))
1573        .collect();
1574    to_exit.sort_by(|a, b| m.get(*b).depth.cmp(&m.get(*a).depth));
1575    for &x in &to_exit {
1576        if m.get(x).has_history() {
1577            record_history(inst, m, x);
1578        }
1579    }
1580    for x in to_exit {
1581        exit_state(inst, m, x, buf, rec);
1582    }
1583    let path = entry_path_below(m, source, target);
1584    for &s in &path {
1585        enter(inst, m, s, 0, event_value, etype, buf, rec);
1586    }
1587    if let Some(&tgt) = path.last() {
1588        descend(inst, m, tgt, 0, event_value, etype, buf, rec);
1589    }
1590    Ok(())
1591}
1592
1593/// Deepest proper ancestor common to source and target.
1594fn lca_external(m: &Machine, source: NodeId, target: NodeId) -> NodeId {
1595    let sa = proper_ancestors(m, source);
1596    let ta: HashSet<NodeId> = proper_ancestors(m, target).into_iter().collect();
1597    for a in sa {
1598        if ta.contains(&a) {
1599            return a;
1600        }
1601    }
1602    m.top
1603}
1604
1605/// The child of `anc` that is an ancestor-or-equal of `n`.
1606fn child_below(m: &Machine, anc: NodeId, n: NodeId) -> NodeId {
1607    let mut cur = n;
1608    while let Some(p) = m.get(cur).parent {
1609        if p == anc {
1610            return cur;
1611        }
1612        cur = p;
1613    }
1614    n
1615}
1616
1617fn entry_path_below(m: &Machine, lca: NodeId, target: NodeId) -> Vec<NodeId> {
1618    let chain = ancestors_inclusive(m, target); // [target, ..., top]
1619    let mut path: Vec<NodeId> = chain.into_iter().rev().collect(); // [top, ..., target]
1620    while path.first().map(|&x| x != lca).unwrap_or(false) {
1621        path.remove(0);
1622    }
1623    if path.first() == Some(&lca) {
1624        path.remove(0);
1625    }
1626    path
1627}
1628
1629// ===================== entry / exit / descend =====================
1630
1631/// Activate, initialize esvs, run entry actions, arm timers.
1632fn enter(
1633    inst: &mut Instance,
1634    m: &Machine,
1635    n: NodeId,
1636    clock: u64,
1637    event_value: &Value,
1638    etype: Option<&str>,
1639    buf: &mut StepBuf,
1640    rec: &mut StepRecord,
1641) {
1642    inst.active.insert(n);
1643    let sd = m.get(n);
1644    // A submachine root seeds its `external` esvs from `with:` (CEL over the parent
1645    // scope) before its esvs initialize (SPEC §5.6.1).
1646    if sd.is_sm_boundary && !sd.sm_with.is_empty() {
1647        if let Some(parent) = sd.parent {
1648            let env = build_env(inst, m, parent, event_value);
1649            for (name, expr) in &sd.sm_with {
1650                if let Ok(v) = crate::cel::eval(expr, &env) {
1651                    inst.external_source.insert(name.clone(), v);
1652                }
1653            }
1654        }
1655    }
1656    for (name, decl) in sd.esvs.clone() {
1657        let val = if decl.external {
1658            inst.external_source.get(&name).cloned().unwrap_or(Value::Null)
1659        } else {
1660            decl.init.clone().unwrap_or(Value::Null)
1661        };
1662        inst.esvs.insert(esv_key(sd, &name), val);
1663    }
1664    let entry = sd.entry.clone();
1665    let _ = run_actions(inst, m, n, &entry, event_value, etype, buf, rec);
1666    let afters = sd.after.clone();
1667    for (i, a) in afters.iter().enumerate() {
1668        inst.timers.push(ArmedTimer {
1669            state: n,
1670            after_idx: i,
1671            due: clock.saturating_add(a.duration_ms),
1672        });
1673    }
1674    rec.entered.push(sd.id.clone());
1675}
1676
1677/// Take initial transitions / restore history into substates (state `n` already entered).
1678fn descend(
1679    inst: &mut Instance,
1680    m: &Machine,
1681    n: NodeId,
1682    clock: u64,
1683    event_value: &Value,
1684    etype: Option<&str>,
1685    buf: &mut StepBuf,
1686    rec: &mut StepRecord,
1687) {
1688    let sd = m.get(n);
1689    match sd.kind {
1690        StateKind::Composite => {
1691            let has_hist = sd.has_history();
1692            let hist = inst.history.get(&n).cloned();
1693            if has_hist && hist.is_some() {
1694                restore_history(inst, m, n, clock, event_value, etype, hist.unwrap(), buf, rec);
1695            } else if let Some(init) = sd.initial.clone() {
1696                for a in &init.action {
1697                    let _ = run_actions(inst, m, n, std::slice::from_ref(a), event_value, etype, buf, rec);
1698                }
1699                enter(inst, m, init.target, clock, event_value, etype, buf, rec);
1700                descend(inst, m, init.target, clock, event_value, etype, buf, rec);
1701            }
1702        }
1703        StateKind::Orthogonal => {
1704            let has_hist = sd.has_history();
1705            let hist = inst.history.get(&n).cloned();
1706            if has_hist && hist.is_some() {
1707                restore_history(inst, m, n, clock, event_value, etype, hist.unwrap(), buf, rec);
1708            } else {
1709                let regions = sd.regions.clone();
1710                for r in &regions {
1711                    for a in &r.initial.action {
1712                        let _ = run_actions(inst, m, n, std::slice::from_ref(a), event_value, etype, buf, rec);
1713                    }
1714                    enter(inst, m, r.initial.target, clock, event_value, etype, buf, rec);
1715                    descend(inst, m, r.initial.target, clock, event_value, etype, buf, rec);
1716                }
1717            }
1718        }
1719        _ => {}
1720    }
1721}
1722
1723fn restore_history(
1724    inst: &mut Instance,
1725    m: &Machine,
1726    state: NodeId,
1727    clock: u64,
1728    event_value: &Value,
1729    etype: Option<&str>,
1730    hist: HistRecord,
1731    buf: &mut StepBuf,
1732    rec: &mut StepRecord,
1733) {
1734    match hist {
1735        HistRecord::Shallow(direct) => {
1736            for child in direct {
1737                enter(inst, m, child, clock, event_value, etype, buf, rec);
1738                descend(inst, m, child, clock, event_value, etype, buf, rec);
1739            }
1740        }
1741        HistRecord::Deep(leaves) => {
1742            for leaf in leaves {
1743                let path = entry_path_below(m, state, leaf);
1744                for &s in &path {
1745                    enter(inst, m, s, clock, event_value, etype, buf, rec);
1746                }
1747            }
1748        }
1749    }
1750}
1751
1752fn exit_state(
1753    inst: &mut Instance,
1754    m: &Machine,
1755    n: NodeId,
1756    buf: &mut StepBuf,
1757    rec: &mut StepRecord,
1758) {
1759    let sd = m.get(n);
1760    inst.done_emitted.remove(&n);
1761    let exit = sd.exit.clone();
1762    let _ = run_actions(inst, m, n, &exit, &Value::Null, None, buf, rec);
1763    for (name, _) in sd.esvs.clone() {
1764        inst.esvs.remove(&esv_key(sd, &name));
1765    }
1766    inst.timers.retain(|t| t.state != n);
1767    inst.active.remove(&n);
1768    rec.exited.push(sd.id.clone());
1769}
1770
1771fn exit_up_to(
1772    inst: &mut Instance,
1773    m: &Machine,
1774    leaf: NodeId,
1775    lca: Option<NodeId>,
1776    buf: &mut StepBuf,
1777    rec: &mut StepRecord,
1778) {
1779    let mut cur = Some(leaf);
1780    while let Some(c) = cur {
1781        if lca == Some(c) {
1782            break;
1783        }
1784        exit_state(inst, m, c, buf, rec);
1785        cur = m.get(c).parent;
1786    }
1787}
1788
1789fn record_history(inst: &mut Instance, m: &Machine, state: NodeId) {
1790    let sd = m.get(state);
1791    match sd.history {
1792        HistoryKind::Shallow => {
1793            let direct: Vec<NodeId> = sd
1794                .children
1795                .iter()
1796                .copied()
1797                .filter(|&c| inst.active.contains(&c))
1798                .collect();
1799            inst.history.insert(state, HistRecord::Shallow(direct));
1800        }
1801        HistoryKind::Deep => {
1802            let leaves: Vec<NodeId> = inst
1803                .active
1804                .iter()
1805                .copied()
1806                .filter(|&n| is_leaf(m, n) && is_strictly_below(m, state, n))
1807                .collect();
1808            inst.history.insert(state, HistRecord::Deep(leaves));
1809        }
1810        HistoryKind::None => {}
1811    }
1812}
1813
1814// ===================== actions =====================
1815
1816fn run_actions(
1817    inst: &mut Instance,
1818    m: &Machine,
1819    scope: NodeId,
1820    actions: &[Action],
1821    event_value: &Value,
1822    etype: Option<&str>,
1823    buf: &mut StepBuf,
1824    rec: &mut StepRecord,
1825) -> Result<(), CelError> {
1826    for a in actions {
1827        run_one(inst, m, scope, a, event_value, etype, buf, rec)?;
1828    }
1829    Ok(())
1830}
1831
1832fn run_one(
1833    inst: &mut Instance,
1834    m: &Machine,
1835    scope: NodeId,
1836    action: &Action,
1837    event_value: &Value,
1838    etype: Option<&str>,
1839    buf: &mut StepBuf,
1840    _rec: &mut StepRecord,
1841) -> Result<(), CelError> {
1842    let env = build_env(inst, m, scope, event_value);
1843    match action {
1844        Action::Assign(pairs) => {
1845            for (name, expr) in pairs {
1846                let val = cel::eval(expr, &env)?;
1847                let decl_state = nearest_declaring(inst, m, scope, name)
1848                    .ok_or_else(|| CelError::Other(format!("assign to undeclared '{name}'")))?;
1849                let sd = m.get(decl_state);
1850                let decl = sd
1851                    .esvs
1852                    .iter()
1853                    .find(|(n, _)| n == name)
1854                    .map(|(_, d)| d.clone())
1855                    .unwrap();
1856                if decl.external {
1857                    return Err(CelError::Other(format!("cannot assign to external esv '{name}'")));
1858                }
1859                let coerced = val
1860                    .coerce_to(&decl.ty)
1861                    .ok_or_else(|| CelError::Type(format!("'{name}' expects {}", decl.ty)))?;
1862                inst.esvs.insert(esv_key(sd, name), coerced);
1863            }
1864            Ok(())
1865        }
1866        Action::Publish { event, to, payload } => {
1867            let pmap = eval_payload(payload, &env)?;
1868            buf.published.push(event.clone());
1869            if let Some(to_expr) = to {
1870                for t in eval_targets(to_expr, &env)? {
1871                    buf.deliveries.push((
1872                        t,
1873                        QueuedEvent {
1874                            etype: event.clone(),
1875                            payload: Value::Map(pmap.clone()),
1876                            origin: Some(inst.id.clone()),
1877                        },
1878                    ));
1879                }
1880            } else {
1881                buf.undirected.push((inst.id.clone(), event.clone(), Value::Map(pmap)));
1882            }
1883            Ok(())
1884        }
1885        Action::Refresh { only } => {
1886            if etype != Some("env") {
1887                return Err(CelError::Other("refresh only valid while handling env".into()));
1888            }
1889            if let Some(changed) = changed_of(event_value) {
1890                let names: Vec<String> = match only {
1891                    Some(lst) => lst.clone(),
1892                    None => changed.keys().cloned().collect(),
1893                };
1894                for name in names {
1895                    if let Some(v) = changed.get(&name) {
1896                        if let Some(st) = nearest_declaring(inst, m, scope, &name) {
1897                            inst.esvs.insert(esv_key(m.get(st), &name), v.clone());
1898                        }
1899                    }
1900                }
1901            }
1902            Ok(())
1903        }
1904        Action::Spawn { def, payload, result } => {
1905            let pmap = eval_payload(payload, &env)?;
1906            buf.spawns.push(SpawnReq {
1907                parent: inst.id.clone(),
1908                scope,
1909                def_id: def.clone(),
1910                payload: pmap,
1911                result_var: result.clone(),
1912            });
1913            buf.spawned.push(def.clone());
1914            buf.spawned_def.push(def.clone());
1915            Ok(())
1916        }
1917        Action::Stop => {
1918            buf.stop = true;
1919            // enqueue a stop marker so post_step terminates
1920            inst.queue.push_back(QItem::Event(QueuedEvent {
1921                etype: "__stop__".into(),
1922                payload: Value::Null,
1923                origin: Some(inst.id.clone()),
1924            }));
1925            Ok(())
1926        }
1927    }
1928}
1929
1930fn eval_payload(pairs: &[(String, String)], env: &Env) -> Result<BTreeMap<String, Value>, CelError> {
1931    let mut out = BTreeMap::new();
1932    for (k, expr) in pairs {
1933        out.insert(k.clone(), cel::eval(expr, env)?);
1934    }
1935    Ok(out)
1936}
1937
1938fn eval_targets(expr: &str, env: &Env) -> Result<Vec<InstanceId>, CelError> {
1939    match cel::eval(expr, env)? {
1940        Value::Str(s) => Ok(vec![s]),
1941        Value::List(l) => Ok(l
1942            .into_iter()
1943            .filter_map(|x| x.as_str_value().map(|s| s.to_string()))
1944            .collect()),
1945        Value::Null => Ok(vec![]),
1946        other => Err(CelError::Type(format!("publish.to must be string/list, got {}", other.type_name()))),
1947    }
1948}
1949
1950// ===================== defer =====================
1951
1952fn effective_defer(inst: &Instance, m: &Machine) -> HashSet<String> {
1953    let mut out = HashSet::new();
1954    for &n in &inst.active {
1955        for d in &m.get(n).defer {
1956            out.insert(d.clone());
1957        }
1958    }
1959    out
1960}
1961
1962fn replay_deferred(inst: &mut Instance, m: &Machine) {
1963    let defer_set = effective_defer(inst, m);
1964    let (mut move_front, mut remaining) = (Vec::new(), Vec::new());
1965    for item in inst.deferred.drain(..) {
1966        let still = match &item {
1967            QItem::Event(e) => defer_set.contains(&e.etype),
1968            QItem::Timer { .. } => false,
1969        };
1970        if still {
1971            remaining.push(item);
1972        } else {
1973            move_front.push(item);
1974        }
1975    }
1976    inst.deferred = remaining;
1977    let mut new_q = VecDeque::new();
1978    for item in move_front {
1979        new_q.push_back(item);
1980    }
1981    while let Some(x) = inst.queue.pop_front() {
1982        new_q.push_back(x);
1983    }
1984    inst.queue = new_q;
1985}
1986
1987// ===================== completion (orthogonal + submachine) =====================
1988
1989/// Active composite/orthogonal states that have reached completion: an orthogonal
1990/// whose regions are all final, or a composite whose active leaf is final (incl. an
1991/// inlined submachine root, SPEC §5.6.1).
1992fn complete_states(inst: &Instance, m: &Machine) -> Vec<NodeId> {
1993    let leaves = active_leaves(inst, m);
1994    let mut out = Vec::new();
1995    for &s in &inst.active {
1996        let sd = m.get(s);
1997        match sd.kind {
1998            StateKind::Orthogonal => {
1999                if regions_all_final(inst, m, s) {
2000                    out.push(s);
2001                }
2002            }
2003            StateKind::Composite => {
2004                if leaves.iter().any(|&l| {
2005                    is_ancestor_or_equal(m, s, l) && m.get(l).kind == StateKind::Final
2006                }) {
2007                    out.push(s);
2008                }
2009            }
2010            _ => {}
2011        }
2012    }
2013    out
2014}
2015
2016/// The active leaf name within a composite (for the `done` payload).
2017fn composite_leaf_name(inst: &Instance, m: &Machine, composite: NodeId) -> String {
2018    for l in active_leaves(inst, m) {
2019        if is_ancestor_or_equal(m, composite, l) && m.get(l).kind == StateKind::Final {
2020            return m.get(l).id.clone();
2021        }
2022    }
2023    String::new()
2024}
2025
2026fn regions_all_final(inst: &Instance, m: &Machine, ortho: NodeId) -> bool {
2027    let regions = m.get(ortho).regions.clone();
2028    if regions.is_empty() {
2029        return false;
2030    }
2031    for r in &regions {
2032        let any_final = r.states.iter().any(|&s| subtree_has_active_final(inst, m, s));
2033        if !any_final {
2034            return false;
2035        }
2036    }
2037    true
2038}
2039
2040fn subtree_has_active_final(inst: &Instance, m: &Machine, n: NodeId) -> bool {
2041    if inst.active.contains(&n) && m.get(n).kind == StateKind::Final {
2042        return true;
2043    }
2044    for &c in &m.get(n).children {
2045        if subtree_has_active_final(inst, m, c) {
2046            return true;
2047        }
2048    }
2049    false
2050}
2051
2052// ===================== env helpers =====================
2053
2054fn build_env(inst: &Instance, m: &Machine, scope: NodeId, event_value: &Value) -> Env {
2055    let mut env = Env::new();
2056    for (k, v) in resolve_visible(inst, m, scope) {
2057        env = env.with(k, v);
2058    }
2059    env = env.with("id", Value::Str(inst.id.clone()));
2060    env = env.with("parent", inst.parent.clone().map(Value::Str).unwrap_or(Value::Null));
2061    env = env.with("event", event_value.clone());
2062    env
2063}
2064
2065fn event_binding(e: &QueuedEvent) -> Value {
2066    let mut m = BTreeMap::new();
2067    m.insert("type".to_string(), Value::Str(e.etype.clone()));
2068    m.insert("payload".to_string(), e.payload.clone());
2069    Value::Map(m)
2070}
2071
2072/// event.payload.changed as a Map (for env events), else None.
2073fn changed_of(event_value: &Value) -> Option<BTreeMap<String, Value>> {
2074    let payload = field(event_value, "payload")?;
2075    let changed = field(&payload, "changed")?;
2076    if let Value::Map(m) = changed {
2077        Some(m)
2078    } else {
2079        None
2080    }
2081}
2082
2083fn field(v: &Value, name: &str) -> Option<Value> {
2084    if let Value::Map(m) = v {
2085        m.get(name).cloned()
2086    } else {
2087        None
2088    }
2089}
2090
2091fn error_payload(fault: &CelError) -> BTreeMap<String, Value> {
2092    let mut m = BTreeMap::new();
2093    m.insert("message".to_string(), Value::Str(fault.to_string()));
2094    m
2095}
2096
2097// ===================== validation =====================
2098
2099fn validate_event_payload(m: &Machine, etype: &str, payload: &Value) -> Result<(), String> {
2100    // reserved lifecycle events are not declared and skip payload typing
2101    if matches!(etype, "env" | "error" | "done" | "initial" | "entry" | "exit") {
2102        return Ok(());
2103    }
2104    let decl = match m.events.get(etype) {
2105        Some(d) => d,
2106        None => return Err(format!("undeclared event '{etype}'")),
2107    };
2108    let pm = match payload {
2109        Value::Map(m) => m.clone(),
2110        Value::Null => BTreeMap::new(),
2111        _ => return Err("payload must be a map".to_string()),
2112    };
2113    for (name, field) in &decl.payload {
2114        match pm.get(name) {
2115            None => {
2116                if field.required {
2117                    return Err(format!("missing required field '{name}'"));
2118                }
2119            }
2120            Some(v) => {
2121                if !v.matches_type(&field.ty) {
2122                    return Err(format!("field '{name}' expects {}", field.ty));
2123                }
2124            }
2125        }
2126    }
2127    for name in pm.keys() {
2128        if !decl.payload.iter().any(|(n, _)| n == name) {
2129            return Err(format!("unexpected field '{name}'"));
2130        }
2131    }
2132    Ok(())
2133}
2134
2135// ===================== views helpers =====================
2136
2137fn config_of(inst: &Instance, m: &Machine) -> Vec<String> {
2138    let mut v: Vec<String> = active_leaves(inst, m).into_iter().map(|n| m.get(n).id.clone()).collect();
2139    v.sort();
2140    v
2141}
2142
2143fn reported_esvs(inst: &Instance, m: &Machine) -> BTreeMap<String, Value> {
2144    let mut leaves = active_leaves(inst, m);
2145    leaves.sort_by_key(|&n| std::cmp::Reverse(m.get(n).depth));
2146    let mut out = BTreeMap::new();
2147    for leaf in leaves {
2148        for (k, v) in resolve_visible(inst, m, leaf) {
2149            out.entry(k).or_insert(v);
2150        }
2151    }
2152    out
2153}
2154
2155/// Reserved lifecycle event names never reported as enabled (SPEC §14).
2156const RESERVED_LIFECYCLE_EVENTS: &[&str] = &["initial", "entry", "exit", "env", "error", "done"];
2157
2158/// `enabled_events(instance)` — the sorted declared event types the current active
2159/// configuration can handle (SPEC §14). An event type is enabled iff some active
2160/// state declares an `on_events` handler for it, considering each active leaf and its
2161/// ancestor chain and all orthogonal regions. Structural and guard-agnostic;
2162/// reserved lifecycle events are excluded.
2163fn enabled_events(inst: &Instance, m: &Machine) -> Vec<String> {
2164    let mut set = BTreeSet::new();
2165    for leaf in active_leaves(inst, m) {
2166        for s in ancestors_inclusive(m, leaf) {
2167            for ev in m.get(s).on_events.keys() {
2168                if !RESERVED_LIFECYCLE_EVENTS.contains(&ev.as_str()) {
2169                    set.insert(ev.clone());
2170                }
2171            }
2172        }
2173    }
2174    set.into_iter().collect()
2175}
2176
2177// ===================== views helpers =====================