1use std::fmt;
12
13#[derive(Debug, Clone)]
20pub enum BuildError {
21 DuplicateNode { id: String },
23 MissingNode { from: String, to: String },
25 MissingEntryPoint,
27 MissingExitPoint,
29 InvalidEdgeDefinition {
31 from: String,
32 to: String,
33 reason: String,
34 },
35 InvalidFallback { node: String, reason: String },
37}
38
39impl fmt::Display for BuildError {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::DuplicateNode { id } => write!(f, "duplicate node id: '{}'", id),
43 Self::MissingNode { from, to } => {
44 write!(
45 f,
46 "edge references non-existent node: '{}' (in {}→{})",
47 to, from, to
48 )
49 }
50 Self::MissingEntryPoint => write!(f, "entry point not set"),
51 Self::MissingExitPoint => write!(f, "exit point not set"),
52 Self::InvalidEdgeDefinition { from, to, reason } => {
53 write!(f, "invalid edge {}→{}: {}", from, to, reason)
54 }
55 Self::InvalidFallback { node, reason } => {
56 write!(f, "invalid fallback for node '{}': {}", node, reason)
57 }
58 }
59 }
60}
61
62#[derive(Debug, Clone, Default)]
64pub struct BuildErrors(pub Vec<BuildError>);
65
66impl BuildErrors {
67 pub fn new() -> Self {
68 Self(Vec::new())
69 }
70
71 pub fn push(&mut self, e: BuildError) {
72 self.0.push(e);
73 }
74
75 pub fn is_empty(&self) -> bool {
76 self.0.is_empty()
77 }
78
79 pub fn len(&self) -> usize {
80 self.0.len()
81 }
82
83 pub fn iter(&self) -> impl Iterator<Item = &BuildError> {
84 self.0.iter()
85 }
86}
87
88impl fmt::Display for BuildErrors {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 if self.0.is_empty() {
91 write!(f, "no errors")
92 } else {
93 writeln!(f, "{} error(s):", self.0.len())?;
94 for e in &self.0 {
95 writeln!(f, " - {}", e)?;
96 }
97 Ok(())
98 }
99 }
100}
101
102impl std::error::Error for BuildError {}
103impl std::error::Error for BuildErrors {}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub enum DiagnosticSeverity {
110 Info,
112 Warning,
114}
115
116impl fmt::Display for DiagnosticSeverity {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 match self {
119 Self::Info => write!(f, "info"),
120 Self::Warning => write!(f, "warning"),
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
127pub enum DiagnosticCategory {
128 Cycle,
130 FallbackInCycle,
132 Unreachable,
134 ConditionOverlap,
136 EndNodeOutgoing,
138 Other,
140}
141
142impl std::fmt::Display for DiagnosticCategory {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 match self {
145 Self::Cycle => write!(f, "cycle"),
146 Self::FallbackInCycle => write!(f, "fallback-in-cycle"),
147 Self::Unreachable => write!(f, "unreachable"),
148 Self::ConditionOverlap => write!(f, "condition-overlap"),
149 Self::EndNodeOutgoing => write!(f, "end-node-outgoing"),
150 Self::Other => write!(f, "other"),
151 }
152 }
153}
154
155#[derive(Debug, Clone)]
157pub struct Diagnostic {
158 pub severity: DiagnosticSeverity,
159 pub category: DiagnosticCategory,
160 pub message: String,
161}
162
163impl fmt::Display for Diagnostic {
164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
165 write!(
166 f,
167 "[{}] ({}): {}",
168 self.severity, self.category, self.message
169 )
170 }
171}
172
173#[derive(Debug, Clone, Default)]
177pub struct GraphDiagnostics {
178 pub warnings: Vec<Diagnostic>,
179 pub infos: Vec<Diagnostic>,
180}
181
182impl GraphDiagnostics {
183 pub fn new() -> Self {
184 Self::default()
185 }
186
187 pub fn add_warning(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
188 self.warnings.push(Diagnostic {
189 severity: DiagnosticSeverity::Warning,
190 category,
191 message: message.into(),
192 });
193 }
194
195 pub fn add_info(&mut self, category: DiagnosticCategory, message: impl Into<String>) {
196 self.infos.push(Diagnostic {
197 severity: DiagnosticSeverity::Info,
198 category,
199 message: message.into(),
200 });
201 }
202
203 pub fn is_empty(&self) -> bool {
204 self.warnings.is_empty() && self.infos.is_empty()
205 }
206
207 pub fn has_warnings(&self) -> bool {
208 !self.warnings.is_empty()
209 }
210}
211
212impl fmt::Display for GraphDiagnostics {
213 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214 if !self.warnings.is_empty() {
215 writeln!(f, "{} warning(s):", self.warnings.len())?;
216 for w in &self.warnings {
217 writeln!(f, " - {}", w)?;
218 }
219 }
220 if !self.infos.is_empty() {
221 writeln!(f, "{} info(s):", self.infos.len())?;
222 for i in &self.infos {
223 writeln!(f, " - {}", i)?;
224 }
225 }
226 if self.is_empty() {
227 write!(f, "no issues found")
228 } else {
229 Ok(())
230 }
231 }
232}
233
234#[derive(Debug)]
240pub enum GraphError {
241 Terminal(TerminalError),
243}
244
245#[derive(Debug)]
247pub enum TerminalError {
248 InvalidGraph(String),
250 NodeNotFound(String),
252 MissingEdge { from: String, to: String },
254 NodeExecutionFailed {
256 node: String,
257 source: Box<dyn std::error::Error + Send + Sync>,
258 },
259 StepsExceeded { limit: usize },
261 LoopLimitExceeded { limit: usize },
263 BarrierTimeout {
265 node: String,
266 timeout: std::time::Duration,
267 },
268 BarrierCancelled { node: String },
270 Unrouted {
272 node: String,
274 attempted_conditions: Vec<ConditionEval>,
276 },
277 StateError(String),
279}
280
281#[derive(Debug, Clone)]
283pub enum ObservedError {
284 Warning { node: String, message: String },
286 Degraded { node: String, message: String },
288 PartialFailure {
290 node: String,
291 succeeded: usize,
292 failed: usize,
293 message: String,
294 },
295}
296
297impl fmt::Display for ObservedError {
298 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
299 match self {
300 Self::Warning { node, message } => write!(f, "node '{}': {}", node, message),
301 Self::Degraded { node, message } => write!(f, "node '{}' degraded: {}", node, message),
302 Self::PartialFailure {
303 node,
304 succeeded,
305 failed,
306 message,
307 } => {
308 write!(
309 f,
310 "node '{}' partial: {}/{} ok, {}",
311 node,
312 succeeded,
313 succeeded + failed,
314 message
315 )
316 }
317 }
318 }
319}
320
321#[derive(Debug, Clone)]
323pub struct ConditionEval {
324 pub edge: String,
326 pub condition: Option<String>,
328 pub matched: bool,
330}
331
332impl fmt::Display for GraphError {
335 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336 match self {
337 Self::Terminal(e) => write!(f, "[terminal] {}", e),
338 }
339 }
340}
341
342impl fmt::Display for TerminalError {
343 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344 match self {
345 Self::InvalidGraph(msg) => write!(f, "invalid graph: {msg}"),
346 Self::NodeNotFound(name) => write!(f, "node not found: {name}"),
347 Self::MissingEdge { from, to } => {
348 write!(
349 f,
350 "goto '{}' from '{}' failed: no edge {}→{} exists",
351 to, from, from, to
352 )
353 }
354 Self::NodeExecutionFailed { node, source } => {
355 write!(f, "node '{node}' execution failed: {source}")
356 }
357 Self::StepsExceeded { limit } => {
358 write!(f, "step limit {limit} exceeded (potential infinite loop)")
359 }
360 Self::LoopLimitExceeded { limit } => write!(f, "loop limit exceeded: {limit}"),
361 Self::BarrierTimeout { node, timeout } => {
362 write!(f, "barrier '{node}' timed out after {timeout:?}")
363 }
364 Self::BarrierCancelled { node } => {
365 write!(
366 f,
367 "barrier '{node}' cancelled: consumer dropped the signal channel"
368 )
369 }
370 Self::Unrouted {
371 node,
372 attempted_conditions,
373 } => {
374 write!(f, "node '{}' has no matching outgoing edge", node)?;
375 if !attempted_conditions.is_empty() {
376 write!(f, ". evaluated: [")?;
377 for (i, ce) in attempted_conditions.iter().enumerate() {
378 if i > 0 {
379 write!(f, ", ")?;
380 }
381 write!(f, "{}={}", ce.edge, ce.matched)?;
382 }
383 write!(f, "]")?;
384 }
385 Ok(())
386 }
387 Self::StateError(msg) => write!(f, "state error: {msg}"),
388 }
389 }
390}
391
392impl std::error::Error for GraphError {
393 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
394 match self {
395 Self::Terminal(TerminalError::NodeExecutionFailed { source, .. }) => {
396 Some(source.as_ref())
397 }
398 Self::Terminal(_) => None,
399 }
400 }
401}
402
403impl std::error::Error for TerminalError {
404 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
405 match self {
406 Self::NodeExecutionFailed { source, .. } => Some(source.as_ref()),
407 _ => None,
408 }
409 }
410}