tramli 3.2.0

Constrained flow engine — state machines that prevent invalid transitions at build time
Documentation
use std::any::TypeId;
use std::collections::HashSet;
use std::fmt;

#[derive(Debug)]
pub struct FlowError {
    pub code: &'static str,
    pub message: String,
    pub source: Option<Box<dyn std::error::Error + Send + Sync>>,
    /// Types that were available in context when the error occurred.
    pub available_types: Option<HashSet<TypeId>>,
    /// Types that were expected but missing (if applicable).
    pub missing_types: Option<HashSet<TypeId>>,
}

impl FlowError {
    pub fn new(code: &'static str, message: impl Into<String>) -> Self {
        Self { code, message: message.into(), source: None, available_types: None, missing_types: None }
    }

    pub fn with_source(code: &'static str, message: impl Into<String>, source: impl std::error::Error + Send + Sync + 'static) -> Self {
        Self { code, message: message.into(), source: Some(Box::new(source)), available_types: None, missing_types: None }
    }

    /// Attach context snapshot to this error.
    pub fn with_context_snapshot(mut self, available: HashSet<TypeId>, missing: HashSet<TypeId>) -> Self {
        self.available_types = Some(available);
        self.missing_types = Some(missing);
        self
    }

    pub fn invalid_transition(from: &str, to: &str) -> Self {
        Self::new("INVALID_TRANSITION", format!("Invalid transition from {from} to {to}"))
    }

    pub fn missing_context(type_name: &str) -> Self {
        Self::new("MISSING_CONTEXT", format!("Missing context key: {type_name}"))
    }

    pub fn dag_cycle(detail: &str) -> Self {
        Self::new("DAG_CYCLE", format!("Auto/Branch transitions contain a cycle: {detail}"))
    }

    pub fn max_chain_depth() -> Self {
        Self::new("MAX_CHAIN_DEPTH", "Auto-chain exceeded maximum depth of 10")
    }
}

impl fmt::Display for FlowError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "[{}] {}", self.code, self.message)
    }
}

impl std::error::Error for FlowError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
    }
}