Skip to main content

stackless_core/engine/
plan.rs

1//! Step planning: one validated definition + the derived graph → the
2//! ordered steps every substrate executes (§3/§4 share the sequence:
3//! provision integrations → prepare → start services → health gate).
4
5use serde::Serialize;
6
7use crate::def::{DefError, DependencyGraph, Node, StackDef};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum StepKind {
12    /// Provision a hosted third-party integration (Clerk in v0).
13    ProvisionIntegration,
14    /// Materialize a service's source into instance-owned space.
15    Materialize,
16    /// The once-after-materialization hook.
17    Setup,
18    /// The every-up hook, after dependencies are ready.
19    Prepare,
20    /// Start (or deploy) the service.
21    Start,
22    /// Gate on the service's health contract through its public origin.
23    HealthGate,
24}
25
26impl StepKind {
27    fn id_prefix(self) -> &'static str {
28        match self {
29            Self::ProvisionIntegration => "integration",
30            Self::Materialize => "materialize",
31            Self::Setup => "setup",
32            Self::Prepare => "prepare",
33            Self::Start => "start",
34            Self::HealthGate => "health",
35        }
36    }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
40pub struct Step {
41    /// Stable id, the journal's primary key: `"{kind}:{node}"`.
42    pub id: String,
43    pub kind: StepKind,
44    /// The service name the step belongs to.
45    pub node: String,
46}
47
48impl Step {
49    fn new(kind: StepKind, node: &str) -> Self {
50        Self {
51            id: format!("{}:{node}", kind.id_prefix()),
52            kind,
53            node: node.to_owned(),
54        }
55    }
56}
57
58impl StackDef {
59    /// Expand the topological order into lifecycle steps.
60    pub fn plan(&self) -> Result<Vec<Step>, DefError> {
61        let graph = DependencyGraph::derive(self)?;
62        let mut steps = Vec::new();
63        for node in graph.startup_order() {
64            match node {
65                Node::Integration(name) => {
66                    steps.push(Step::new(StepKind::ProvisionIntegration, name));
67                }
68                Node::Service(name) => {
69                    let Some(service) = self.services.get(name) else {
70                        continue;
71                    };
72                    steps.push(Step::new(StepKind::Materialize, name));
73                    if service.setup.is_some() {
74                        steps.push(Step::new(StepKind::Setup, name));
75                    }
76                    if service.prepare.is_some() {
77                        steps.push(Step::new(StepKind::Prepare, name));
78                    }
79                    steps.push(Step::new(StepKind::Start, name));
80                    steps.push(Step::new(StepKind::HealthGate, name));
81                }
82            }
83        }
84        Ok(steps)
85    }
86}