mecha10-core 0.6.5

Mecha10 core runtime and foundational types
Documentation
use std::path::PathBuf;
use thiserror::Error;

/// Main error type for the Mecha10 framework
#[derive(Error, Debug)]
pub enum Mecha10Error {
    #[error("Node initialization failed: {0}")]
    NodeInit(String),

    #[error("Node '{node_name}' failed: {message}")]
    NodeFailed { node_name: String, message: String },

    #[error("Node '{0}' not found")]
    NodeNotFound(String),

    #[error("Configuration error in field '{field}': {message}\n  Expected: {expected}\n  Got: {got}\n  Suggestion: {suggestion}")]
    ConfigValidation {
        field: String,
        message: String,
        expected: String,
        got: String,
        suggestion: String,
    },

    #[error(
        "Configuration file not found: {path}\n  Suggestion: Create the file or set environment variable {env_var}"
    )]
    ConfigNotFound { path: PathBuf, env_var: String },

    #[error("Invalid configuration format: {0}\n  Suggestion: Validate against schema at {1}")]
    ConfigInvalid(String, String),

    #[error("Configuration error: {0}")]
    Configuration(String),

    #[error("{message}\n  Suggestion: {suggestion}")]
    MessagingError { message: String, suggestion: String },
    #[error("Failed to connect to message broker at {url}\n  → Tried: {url}\n  → Suggestion: {suggestion}")]
    MessagingConnectionFailed { url: String, suggestion: String },

    #[error("Failed to publish message to topic '{topic}': {reason}")]
    PublishFailed { topic: String, reason: String },

    #[error("Failed to subscribe to topic '{topic}': {reason}")]
    SubscribeFailed { topic: String, reason: String },

    #[error("Message deserialization failed for topic '{topic}'\n  Expected type: {expected_type}\n  Error: {error}")]
    DeserializationFailed {
        topic: String,
        expected_type: String,
        error: String,
    },

    #[error("Message serialization failed: {0}")]
    SerializationFailed(String),

    #[error("Serialization error: {message}\n  Suggestion: {suggestion}")]
    SerializationError { message: String, suggestion: String },

    #[error("Circuit breaker open for service '{service}': {message}")]
    CircuitBreakerOpen { service: String, message: String },

    #[error("Failed to load model from '{uri}': {reason}\n  Suggestion: {suggestion}")]
    ModelLoadFailed {
        uri: String,
        reason: String,
        suggestion: String,
    },

    #[error("Model inference failed: {0}")]
    InferenceFailed(String),

    #[error(
        "Model not found in MLflow registry: {model_name}/{version}\n  Suggestion: Check MLflow UI at {mlflow_url}"
    )]
    ModelNotFound {
        model_name: String,
        version: String,
        mlflow_url: String,
    },

    #[error("Model checksum mismatch\n  Expected: {expected}\n  Got: {got}\n  Suggestion: Re-download the model")]
    ModelChecksumMismatch { expected: String, got: String },

    #[error("Build failed for node '{node_name}':\n{stderr}\n  Suggestion: Check the error messages above")]
    BuildFailed { node_name: String, stderr: String },

    #[error("Hot-reload failed: {0}")]
    HotReloadFailed(String),

    #[error("Runtime error: {0}")]
    Runtime(String),

    #[error("Timeout after {timeout_ms}ms waiting for: {operation}")]
    Timeout { timeout_ms: u64, operation: String },

    #[error("Shutdown error: {0}")]
    Shutdown(String),

    #[error("File not found: {path}\n  Suggestion: {suggestion}")]
    FileNotFound { path: PathBuf, suggestion: String },

    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("IO error: {message}\n  Suggestion: {suggestion}")]
    IoError { message: String, suggestion: String },

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("YAML error: {0}")]
    Yaml(String),

    #[error("Redis error: {0}")]
    Redis(String),

    #[error("Database error: {0}")]
    Database(String),

    #[error("HTTP error: {0}")]
    Http(String),

    #[error("{0}")]
    Other(String),
}

/// Result type using Mecha10Error
pub type Result<T> = std::result::Result<T, Mecha10Error>;

impl Mecha10Error {
    /// Create a connection error with helpful suggestions
    pub fn connection_failed(url: impl Into<String>) -> Self {
        let url = url.into();

        let suggestion = if url.contains("localhost") || url.contains("127.0.0.1") {
            "Start Redis with `redis-server`\n  → Or set REDIS_URL environment variable".to_string()
        } else {
            format!("Check network connection and ensure Redis is running at {}", url)
        };

        Self::MessagingConnectionFailed { url, suggestion }
    }

    /// Create a config validation error with clear expectations
    pub fn config_validation(
        field: impl Into<String>,
        message: impl Into<String>,
        expected: impl Into<String>,
        got: impl Into<String>,
        suggestion: impl Into<String>,
    ) -> Self {
        Self::ConfigValidation {
            field: field.into(),
            message: message.into(),
            expected: expected.into(),
            got: got.into(),
            suggestion: suggestion.into(),
        }
    }

    /// Create a model load error with helpful suggestions
    pub fn model_load_failed(uri: impl Into<String>, reason: impl Into<String>) -> Self {
        let uri_str = uri.into();
        let reason = reason.into();

        let suggestion = if uri_str.starts_with("mlflow://") {
            "Check MLflow server is running with `mecha10 mlflow start`\n  → Or verify MLFLOW_TRACKING_URI environment variable".to_string()
        } else if uri_str.starts_with("http://") || uri_str.starts_with("https://") {
            "Check network connection and model URL".to_string()
        } else {
            format!("Check file exists: {}", uri_str)
        };

        Self::ModelLoadFailed {
            uri: uri_str,
            reason,
            suggestion,
        }
    }

    // duplicate: false positive — `messaging_error`/`file_not_found`/`config_not_found` match
    // `LlmMessage::new` (packages/ai-llm) and `JaegerExporter::new`/`ZipkinExporter::new`
    // (tracing_otel.rs, see the note there) on AST shape only (each just moves 2
    // `impl Into<_>` args into a struct/enum variant via `.into()`); these are three unrelated
    // `Mecha10Error` variant constructors, not copy-paste drift between each other or the
    // unrelated types they happen to structurally match.
    /// Create a messaging error with a helpful suggestion
    pub fn messaging_error(message: impl Into<String>, suggestion: impl Into<String>) -> Self {
        Self::MessagingError {
            message: message.into(),
            suggestion: suggestion.into(),
        }
    }

    /// Create a configuration error with context
    pub fn configuration(message: impl Into<String>) -> Self {
        Self::Configuration(message.into())
    }

    /// Create a file not found error with a helpful suggestion
    pub fn file_not_found(path: impl Into<PathBuf>, suggestion: impl Into<String>) -> Self {
        Self::FileNotFound {
            path: path.into(),
            suggestion: suggestion.into(),
        }
    }

    /// Create a config not found error
    pub fn config_not_found(path: impl Into<PathBuf>, env_var: impl Into<String>) -> Self {
        Self::ConfigNotFound {
            path: path.into(),
            env_var: env_var.into(),
        }
    }
}

/// Node-content preflight check: verify a model file exists on disk,
/// returning a formatted `Mecha10Error::Configuration` with numbered
/// remediation steps if it doesn't.
///
/// Shared by node configs that resolve an ONNX model path before the node
/// starts (`rl`, `object-detector`), so a missing catalog/registry model
/// fails fast with consistent, actionable guidance instead of deep inside
/// ONNX Runtime session creation.
///
/// # Errors
///
/// Returns an error with remediation steps if `model_path` doesn't exist.
pub fn preflight_model_exists(model_path: &std::path::Path, label: &str, remediation: &[String]) -> Result<()> {
    if model_path.exists() {
        return Ok(());
    }

    let mut message = format!(
        "❌ Pre-flight check failed: {} not found at {}\n\nPossible solutions:\n",
        label,
        model_path.display()
    );
    for (i, step) in remediation.iter().enumerate() {
        message.push_str(&format!("{}. {}\n", i + 1, step));
    }

    Err(Mecha10Error::Configuration(message))
}

impl From<anyhow::Error> for Mecha10Error {
    fn from(err: anyhow::Error) -> Self {
        Self::Other(err.to_string())
    }
}