sdd-layer 0.14.0

Spec-Driven Development CLI and agent harness
use crate::runtime::redaction::redact_text;
use std::error::Error;
use std::fmt;

pub type SddResult<T> = std::result::Result<T, SddError>;

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum SddError {
    Config { message: String },
    Runtime { message: String },
    Provider { message: String },
}

impl SddError {
    pub fn config(message: impl Into<String>) -> Self {
        Self::Config {
            message: message.into(),
        }
    }

    pub fn runtime(message: impl Into<String>) -> Self {
        Self::Runtime {
            message: message.into(),
        }
    }

    pub fn provider(message: impl Into<String>) -> Self {
        Self::Provider {
            message: message.into(),
        }
    }

    fn kind(&self) -> &'static str {
        match self {
            Self::Config { .. } => "config",
            Self::Runtime { .. } => "runtime",
            Self::Provider { .. } => "provider",
        }
    }

    fn message(&self) -> &str {
        match self {
            Self::Config { message } | Self::Runtime { message } | Self::Provider { message } => {
                message
            }
        }
    }
}

impl fmt::Display for SddError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "sdd {} error: {}",
            self.kind(),
            redact_text(self.message())
        )
    }
}

impl Error for SddError {}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::redaction::Secret;

    #[test]
    fn display_redacts_secret_values_embedded_in_errors() {
        let secret = Secret::new("sk-test-secret");
        let error = SddError::provider(format!("provider auth failed with {secret}"));

        let rendered = error.to_string();

        assert!(rendered.contains("[REDACTED]"));
        assert!(!rendered.contains("sk-test-secret"));
    }

    #[test]
    fn result_alias_uses_sdd_error() {
        let result: SddResult<()> = Err(SddError::runtime("OPENAI_API_KEY=sk-test-secret"));

        let rendered = match result {
            Ok(()) => unreachable!("esperado Err do alias SddResult"),
            Err(error) => error.to_string(),
        };

        assert!(rendered.contains("OPENAI_API_KEY=[REDACTED]"));
        assert!(!rendered.contains("sk-test-secret"));
    }
}