klieo-spec 3.5.0

Generic decompose / critique / refine traits and a quality loop runner.
Documentation
//! Crate-level error type.

use thiserror::Error;

/// Errors raised by `klieo-spec` traits and the loop runner.
///
/// Marked `#[non_exhaustive]` so additional variants can be introduced
/// without a major-version bump on impl crates that match on the enum.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SpecError {
    /// A downstream `Decomposer`/`Critic`/`Refiner` impl failed.
    ///
    /// Carries a context message plus the impl's original error as
    /// `#[source]` so `e.source()` traversal preserves the cause chain.
    /// Covers any downstream trait-impl failure that does not fit the
    /// typed variants.
    #[error("downstream error: {message}")]
    Downstream {
        /// Human-readable context describing the wrap site.
        message: String,
        /// Underlying error preserved for `std::error::Error::source()`.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
    },
    /// A misconfiguration in the loop (e.g. zero max iterations).
    #[error("config error: {0}")]
    Config(String),
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::error::Error as StdError;

    #[test]
    fn downstream_message_surfaces_through_display() {
        let e = SpecError::Downstream {
            message: "refiner: empty result".into(),
            source: None,
        };
        assert_eq!(e.to_string(), "downstream error: refiner: empty result");
    }

    #[test]
    fn downstream_preserves_source_chain() {
        let inner = std::io::Error::other("network down");
        let e = SpecError::Downstream {
            message: "critic call failed".into(),
            source: Some(Box::new(inner)),
        };
        let src = e.source().expect("source must be preserved");
        assert_eq!(src.to_string(), "network down");
    }

    #[test]
    fn downstream_without_source_returns_none() {
        let e = SpecError::Downstream {
            message: "no inner".into(),
            source: None,
        };
        assert!(e.source().is_none());
    }
}