stackless_core/engine/
error.rs1use crate::def::DefError;
6use crate::fault::{ErrorContext, Fault, codes};
7use crate::state::StateError;
8use crate::substrate::SubstrateFault;
9
10#[derive(Debug, thiserror::Error)]
11pub enum EngineError {
12 #[error(transparent)]
13 Def(#[from] DefError),
14
15 #[error(transparent)]
16 State(#[from] StateError),
17
18 #[error("instance {instance:?} lives on substrate {existing:?}, not {requested:?}")]
19 SubstrateMismatch {
20 instance: String,
21 existing: String,
22 requested: String,
23 },
24
25 #[error("substrate {substrate:?} does not run pinned checkouts (--source)")]
26 SourceOverrideUnsupported { substrate: String },
27
28 #[error(
29 "service {service:?} --source path {path:?} is already pinned by active instance {other:?}"
30 )]
31 SourceOverrideShared {
32 instance: String,
33 service: String,
34 path: String,
35 other: String,
36 },
37
38 #[error("step {step} on instance {instance:?} failed: {fault}")]
39 Step {
40 instance: String,
41 step: String,
42 fault: SubstrateFault,
43 },
44
45 #[error("substrate {substrate} rejected the definition: {fault}")]
46 SubstrateValidation {
47 substrate: String,
48 fault: SubstrateFault,
49 },
50
51 #[error("teardown of {instance:?} left survivors: {survivors:?}")]
52 TeardownSurvivors {
53 instance: String,
54 survivors: Vec<String>,
55 },
56}
57
58impl Fault for EngineError {
59 fn code(&self) -> &'static str {
60 match self {
61 Self::Def(err) => err.code(),
62 Self::State(err) => err.code(),
63 Self::SubstrateMismatch { .. } => codes::ENGINE_SUBSTRATE_MISMATCH,
64 Self::SourceOverrideUnsupported { .. } => codes::ENGINE_SOURCE_OVERRIDE_UNSUPPORTED,
65 Self::SourceOverrideShared { .. } => codes::ENGINE_SOURCE_OVERRIDE_SHARED,
66 Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => fault.code,
69 Self::TeardownSurvivors { .. } => codes::ENGINE_TEARDOWN_SURVIVORS,
70 }
71 }
72
73 fn remediation(&self) -> String {
74 match self {
75 Self::Def(err) => err.remediation(),
76 Self::State(err) => err.remediation(),
77 Self::SubstrateMismatch {
78 instance, existing, ..
79 } => format!(
80 "the substrate is chosen at creation only: operate on {instance:?} without \
81 --on (it resolves to {existing:?}), or pick a new name for a {existing:?}-free \
82 instance"
83 ),
84 Self::SourceOverrideUnsupported { .. } => {
85 "commit and push, then pin the ref in stackless.toml — cloud substrates deploy \
86 committed refs only"
87 .into()
88 }
89 Self::SourceOverrideShared { other, .. } => format!(
90 "use a separate git worktree per parallel agent, omit --source so stackless \
91 materializes per-instance checkouts, or `stackless down {other}` before reusing \
92 this checkout"
93 ),
94 Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => {
95 fault.remediation.clone()
96 }
97 Self::TeardownSurvivors { survivors, .. } => format!(
98 "these resources still exist and may bill or hold state: {survivors:?}; re-run \
99 `stackless down` to retry, or remove them on the substrate and re-run to verify"
100 ),
101 }
102 }
103
104 fn step(&self) -> Option<&str> {
105 match self {
106 Self::Step { step, .. } => Some(step),
107 _ => None,
108 }
109 }
110
111 fn instance(&self) -> Option<&str> {
112 match self {
113 Self::State(err) => err.instance(),
114 Self::SubstrateMismatch { instance, .. }
115 | Self::SourceOverrideShared { instance, .. }
116 | Self::Step { instance, .. }
117 | Self::TeardownSurvivors { instance, .. } => Some(instance),
118 _ => None,
119 }
120 }
121
122 fn context(&self) -> ErrorContext {
123 match self {
124 Self::Step { fault, .. } | Self::SubstrateValidation { fault, .. } => {
125 fault.context.as_ref().clone()
126 }
127 _ => ErrorContext::default(),
128 }
129 }
130}