use std::fmt;
#[derive(Debug, Clone)]
pub enum BuildError {
DuplicateNode { id: String },
MissingNode { from: String, to: String },
MissingEntryPoint,
MissingExitPoint,
InvalidEdgeDefinition { from: String, to: String, reason: String },
}
impl fmt::Display for BuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DuplicateNode { id } => write!(f, "duplicate node id: '{}'", id),
Self::MissingNode { from, to } => {
write!(f, "edge references non-existent node: '{}' (in {}→{})", to, from, to)
}
Self::MissingEntryPoint => write!(f, "entry point not set"),
Self::MissingExitPoint => write!(f, "exit point not set"),
Self::InvalidEdgeDefinition { from, to, reason } => {
write!(f, "invalid edge {}→{}: {}", from, to, reason)
}
}
}
}
impl std::error::Error for BuildError {}
#[derive(Debug)]
pub enum GraphError {
Terminal(TerminalError),
Recoverable(RecoverableError),
Observed(ObservedError),
}
#[derive(Debug)]
pub enum TerminalError {
InvalidGraph(String),
NodeNotFound(String),
MissingEdge { from: String, to: String },
NodeExecutionFailed { node: String, source: Box<dyn std::error::Error + Send + Sync> },
StepsExceeded { limit: usize },
LoopLimitExceeded { limit: usize },
EdgePolicyExceeded { edge: String, limit: usize },
BarrierTimeout { node: String, timeout: std::time::Duration },
BarrierCancelled { node: String },
Unrouted {
node: String,
attempted_conditions: Vec<ConditionEval>,
},
StateError(String),
}
#[derive(Debug)]
pub enum RecoverableError {
Retryable {
node: String,
attempt: usize,
max_attempts: usize,
reason: String,
},
FallbackTriggered {
from: String,
to: String,
reason: String,
},
}
#[derive(Debug, Clone)]
pub enum ObservedError {
Warning { node: String, message: String },
Degraded { node: String, message: String },
PartialFailure { node: String, succeeded: usize, failed: usize, message: String },
}
#[derive(Debug, Clone)]
pub struct ConditionEval {
pub edge: String,
pub condition: Option<String>,
pub matched: bool,
}
impl fmt::Display for GraphError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Terminal(e) => write!(f, "[terminal] {}", e),
Self::Recoverable(e) => write!(f, "[recoverable] {}", e),
Self::Observed(e) => write!(f, "[observed] {}", e),
}
}
}
impl fmt::Display for TerminalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidGraph(msg) => write!(f, "invalid graph: {msg}"),
Self::NodeNotFound(name) => write!(f, "node not found: {name}"),
Self::MissingEdge { from, to } => {
write!(f, "goto '{}' from '{}' failed: no edge {}→{} exists", to, from, from, to)
}
Self::NodeExecutionFailed { node, source } => {
write!(f, "node '{node}' execution failed: {source}")
}
Self::StepsExceeded { limit } => {
write!(f, "step limit {limit} exceeded (potential infinite loop)")
}
Self::LoopLimitExceeded { limit } => write!(f, "loop limit exceeded: {limit}"),
Self::EdgePolicyExceeded { edge, limit } => {
write!(f, "edge '{edge}' policy limit {limit} exceeded (cycle protection)")
}
Self::BarrierTimeout { node, timeout } => {
write!(f, "barrier '{node}' timed out after {timeout:?}")
}
Self::BarrierCancelled { node } => {
write!(f, "barrier '{node}' cancelled: consumer dropped the signal channel")
}
Self::Unrouted { node, attempted_conditions } => {
write!(f, "node '{}' has no matching outgoing edge", node)?;
if !attempted_conditions.is_empty() {
write!(f, ". evaluated: [")?;
for (i, ce) in attempted_conditions.iter().enumerate() {
if i > 0 { write!(f, ", ")?; }
write!(f, "{}={}", ce.edge, ce.matched)?;
}
write!(f, "]")?;
}
Ok(())
}
Self::StateError(msg) => write!(f, "state error: {msg}"),
}
}
}
impl fmt::Display for RecoverableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Retryable { node, attempt, max_attempts, reason } => {
write!(f, "node '{node}' retry {}/{}, reason: {}", attempt, max_attempts, reason)
}
Self::FallbackTriggered { from, to, reason } => {
write!(f, "fallback edge {}→{} triggered: {}", from, to, reason)
}
}
}
}
impl fmt::Display for ObservedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Warning { node, message } => write!(f, "node '{}': {}", node, message),
Self::Degraded { node, message } => write!(f, "node '{}' degraded: {}", node, message),
Self::PartialFailure { node, succeeded, failed, message } => {
write!(f, "node '{}' partial: {}/{} ok, {}", node, succeeded, succeeded + failed, message)
}
}
}
}
impl std::error::Error for GraphError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Terminal(TerminalError::NodeExecutionFailed { source, .. }) => {
Some(source.as_ref())
}
_ => None,
}
}
}
impl std::error::Error for TerminalError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::NodeExecutionFailed { source, .. } => Some(source.as_ref()),
_ => None,
}
}
}
impl std::error::Error for RecoverableError {}
impl std::error::Error for ObservedError {}