weavatrix-refactor-plan 0.1.1

Evidence metadata, validation profiles, and canonical fingerprints for Weavatrix refactor plans
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;
use weavatrix_edit::{EditError, ErrorCode as EditErrorCode};

/// Stable categories for refactor-plan profile failures.
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum PlanErrorCode {
    BasePlanInvalid,
    SchemaMismatch,
    InvalidPlan,
    EvidenceMalformed,
    EvidenceMissing,
    EvidenceInvalid,
    EvidenceTooLarge,
    PlanTooLarge,
    UnsafePath,
    OperationConflict,
    ExtensionConflict,
    UnsafeNumber,
    JsonEncoding,
    NotTextOnly,
}

impl PlanErrorCode {
    /// Returns the wire-compatible code.
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::BasePlanInvalid => "BASE_PLAN_INVALID",
            Self::SchemaMismatch => "SCHEMA_MISMATCH",
            Self::InvalidPlan => "INVALID_PLAN",
            Self::EvidenceMalformed => "EVIDENCE_MALFORMED",
            Self::EvidenceMissing => "EVIDENCE_MISSING",
            Self::EvidenceInvalid => "EVIDENCE_INVALID",
            Self::EvidenceTooLarge => "EVIDENCE_TOO_LARGE",
            Self::PlanTooLarge => "PLAN_TOO_LARGE",
            Self::UnsafePath => "UNSAFE_PATH",
            Self::OperationConflict => "OPERATION_CONFLICT",
            Self::ExtensionConflict => "EXTENSION_CONFLICT",
            Self::UnsafeNumber => "UNSAFE_NUMBER",
            Self::JsonEncoding => "JSON_ENCODING",
            Self::NotTextOnly => "NOT_TEXT_ONLY",
        }
    }
}

impl fmt::Display for PlanErrorCode {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str(self.as_str())
    }
}

/// A bounded, fail-closed plan profile error.
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PlanError {
    code: PlanErrorCode,
    message: Box<str>,
    #[serde(skip_serializing_if = "Option::is_none")]
    field: Option<Box<str>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    operation_index: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    edit_index: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    related_edit_index: Option<usize>,
    #[serde(skip_serializing_if = "Option::is_none")]
    path: Option<Box<str>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    base_code: Option<EditErrorCode>,
}

impl PlanError {
    pub(crate) fn new(code: PlanErrorCode, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into().into_boxed_str(),
            field: None,
            operation_index: None,
            edit_index: None,
            related_edit_index: None,
            path: None,
            base_code: None,
        }
    }

    pub(crate) fn at_field(mut self, field: impl Into<String>) -> Self {
        self.field = Some(field.into().into_boxed_str());
        self
    }

    pub(crate) const fn at_operation(mut self, operation_index: usize) -> Self {
        self.operation_index = Some(operation_index);
        self
    }

    pub(crate) fn at_path(mut self, path: impl Into<String>) -> Self {
        self.path = Some(path.into().into_boxed_str());
        self
    }

    pub(crate) const fn with_code(mut self, code: PlanErrorCode) -> Self {
        self.code = code;
        self
    }

    pub(crate) fn from_edit(error: &EditError) -> Self {
        Self {
            code: PlanErrorCode::BasePlanInvalid,
            message: error.message().into(),
            field: None,
            operation_index: None,
            edit_index: error.edit_index(),
            related_edit_index: error.related_edit_index(),
            path: None,
            base_code: Some(error.code()),
        }
    }

    /// Returns the stable plan-profile error code.
    #[must_use]
    pub const fn code(&self) -> PlanErrorCode {
        self.code
    }

    /// Returns the human-readable explanation.
    #[must_use]
    pub fn message(&self) -> &str {
        &self.message
    }

    /// Returns the annotation field associated with the failure, when available.
    #[must_use]
    pub fn field(&self) -> Option<&str> {
        self.field.as_deref()
    }

    /// Returns the logical operation index associated with the failure.
    #[must_use]
    pub const fn operation_index(&self) -> Option<usize> {
        self.operation_index
    }

    /// Returns the edit index within the logical operation, when available.
    #[must_use]
    pub const fn edit_index(&self) -> Option<usize> {
        self.edit_index
    }

    /// Returns the conflicting edit index within the logical operation, when available.
    #[must_use]
    pub const fn related_edit_index(&self) -> Option<usize> {
        self.related_edit_index
    }

    /// Returns the operation path associated with the failure.
    #[must_use]
    pub fn path(&self) -> Option<&str> {
        self.path.as_deref()
    }

    /// Returns the underlying `weavatrix-edit` code for a base-plan failure.
    #[must_use]
    pub const fn base_code(&self) -> Option<EditErrorCode> {
        self.base_code
    }
}

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

impl std::error::Error for PlanError {}