Skip to main content

voce_schema/
errors.rs

1//! Unified error taxonomy for the Voce IR pipeline.
2//!
3//! Every error includes: an error code, human-readable message, source location,
4//! and an actionable suggestion for fixing the problem.
5
6use serde::{Deserialize, Serialize};
7
8/// Top-level error type for the Voce IR pipeline.
9#[derive(Debug, thiserror::Error)]
10pub enum VoceError {
11    /// Schema-level errors (parsing, format, version mismatch).
12    #[error("[{code}] Schema error: {message}")]
13    Schema {
14        code: ErrorCode,
15        message: String,
16        suggestion: String,
17    },
18
19    /// Validation errors (rule violations found by validation passes).
20    #[error("[{code}] Validation error at {node_path}: {message}")]
21    Validation {
22        code: ErrorCode,
23        message: String,
24        node_path: String,
25        suggestion: String,
26        severity: ErrorSeverity,
27    },
28
29    /// Compilation errors (failures during IR → output conversion).
30    #[error("[{code}] Compilation error at {node_path}: {message}")]
31    Compilation {
32        code: ErrorCode,
33        message: String,
34        node_path: String,
35        suggestion: String,
36    },
37
38    /// Deployment errors (failures during bundle/upload).
39    #[error("[{code}] Deployment error: {message}")]
40    Deployment {
41        code: ErrorCode,
42        message: String,
43        suggestion: String,
44    },
45
46    /// Pipeline errors (orchestration failures).
47    #[error("[{code}] Pipeline error: {message}")]
48    Pipeline {
49        code: ErrorCode,
50        message: String,
51        suggestion: String,
52    },
53
54    /// AI bridge errors (generation, API, timeout).
55    #[error("[{code}] AI bridge error: {message}")]
56    AiBridge {
57        code: ErrorCode,
58        message: String,
59        suggestion: String,
60    },
61}
62
63/// Typed error codes for every Voce error.
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ErrorCode(pub String);
66
67impl std::fmt::Display for ErrorCode {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{}", self.0)
70    }
71}
72
73impl ErrorCode {
74    // Schema errors
75    pub fn schema_parse() -> Self { Self("S001".to_string()) }
76    pub fn schema_version() -> Self { Self("S002".to_string()) }
77    pub fn schema_missing_root() -> Self { Self("S003".to_string()) }
78
79    // Compilation errors
80    pub fn compile_node_failed() -> Self { Self("C001".to_string()) }
81    pub fn compile_timeout() -> Self { Self("C002".to_string()) }
82    pub fn compile_unsupported_node() -> Self { Self("C003".to_string()) }
83    pub fn compile_asset_failed() -> Self { Self("C004".to_string()) }
84
85    // Deployment errors
86    pub fn deploy_adapter_not_found() -> Self { Self("D001".to_string()) }
87    pub fn deploy_bundle_failed() -> Self { Self("D002".to_string()) }
88    pub fn deploy_upload_failed() -> Self { Self("D003".to_string()) }
89    pub fn deploy_config_invalid() -> Self { Self("D004".to_string()) }
90
91    // Pipeline errors
92    pub fn pipeline_timeout() -> Self { Self("P001".to_string()) }
93    pub fn pipeline_interrupted() -> Self { Self("P002".to_string()) }
94
95    // AI bridge errors
96    pub fn ai_api_error() -> Self { Self("A001".to_string()) }
97    pub fn ai_rate_limited() -> Self { Self("A002".to_string()) }
98    pub fn ai_timeout() -> Self { Self("A003".to_string()) }
99    pub fn ai_incomplete_output() -> Self { Self("A004".to_string()) }
100    pub fn ai_key_invalid() -> Self { Self("A005".to_string()) }
101}
102
103/// Error severity levels.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105pub enum ErrorSeverity {
106    /// Blocks compilation — must be fixed.
107    Error,
108    /// Emits output but flags a potential issue.
109    Warning,
110    /// Informational only.
111    Info,
112}
113
114impl std::fmt::Display for ErrorSeverity {
115    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116        match self {
117            ErrorSeverity::Error => write!(f, "error"),
118            ErrorSeverity::Warning => write!(f, "warning"),
119            ErrorSeverity::Info => write!(f, "info"),
120        }
121    }
122}
123
124/// A structured error report for JSON output.
125#[derive(Debug, Serialize, Deserialize)]
126pub struct ErrorReport {
127    pub code: String,
128    pub severity: String,
129    pub message: String,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub node_path: Option<String>,
132    pub suggestion: String,
133}
134
135impl From<&VoceError> for ErrorReport {
136    fn from(err: &VoceError) -> Self {
137        match err {
138            VoceError::Schema { code, message, suggestion } => ErrorReport {
139                code: code.0.clone(),
140                severity: "error".to_string(),
141                message: message.clone(),
142                node_path: None,
143                suggestion: suggestion.clone(),
144            },
145            VoceError::Validation { code, message, node_path, suggestion, severity } => ErrorReport {
146                code: code.0.clone(),
147                severity: severity.to_string(),
148                message: message.clone(),
149                node_path: Some(node_path.clone()),
150                suggestion: suggestion.clone(),
151            },
152            VoceError::Compilation { code, message, node_path, suggestion } => ErrorReport {
153                code: code.0.clone(),
154                severity: "error".to_string(),
155                message: message.clone(),
156                node_path: Some(node_path.clone()),
157                suggestion: suggestion.clone(),
158            },
159            VoceError::Deployment { code, message, suggestion } => ErrorReport {
160                code: code.0.clone(),
161                severity: "error".to_string(),
162                message: message.clone(),
163                node_path: None,
164                suggestion: suggestion.clone(),
165            },
166            VoceError::Pipeline { code, message, suggestion } => ErrorReport {
167                code: code.0.clone(),
168                severity: "error".to_string(),
169                message: message.clone(),
170                node_path: None,
171                suggestion: suggestion.clone(),
172            },
173            VoceError::AiBridge { code, message, suggestion } => ErrorReport {
174                code: code.0.clone(),
175                severity: "error".to_string(),
176                message: message.clone(),
177                node_path: None,
178                suggestion: suggestion.clone(),
179            },
180        }
181    }
182}
183
184/// CLI exit codes.
185pub mod exit_codes {
186    pub const SUCCESS: i32 = 0;
187    pub const VALIDATION_ERROR: i32 = 1;
188    pub const COMPILATION_ERROR: i32 = 2;
189    pub const DEPLOYMENT_ERROR: i32 = 3;
190    pub const AI_BRIDGE_ERROR: i32 = 4;
191    pub const INTERNAL_ERROR: i32 = 5;
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn error_code_display() {
200        let code = ErrorCode::schema_parse();
201        assert_eq!(code.to_string(), "S001");
202    }
203
204    #[test]
205    fn voce_error_display_includes_code() {
206        let err = VoceError::Schema {
207            code: ErrorCode::schema_parse(),
208            message: "Invalid JSON".to_string(),
209            suggestion: "Check JSON syntax".to_string(),
210        };
211        let msg = format!("{err}");
212        assert!(msg.contains("S001"));
213        assert!(msg.contains("Invalid JSON"));
214    }
215
216    #[test]
217    fn error_report_from_validation_error() {
218        let err = VoceError::Validation {
219            code: ErrorCode("STR001".to_string()),
220            message: "Missing root".to_string(),
221            node_path: "$.root".to_string(),
222            suggestion: "Add a root ViewRoot node".to_string(),
223            severity: ErrorSeverity::Error,
224        };
225        let report = ErrorReport::from(&err);
226        assert_eq!(report.code, "STR001");
227        assert_eq!(report.severity, "error");
228        assert!(report.node_path.is_some());
229    }
230
231    #[test]
232    fn error_report_serializes_to_json() {
233        let report = ErrorReport {
234            code: "C001".to_string(),
235            severity: "error".to_string(),
236            message: "Node failed to compile".to_string(),
237            node_path: Some("root.children[0]".to_string()),
238            suggestion: "Check node structure".to_string(),
239        };
240        let json = serde_json::to_string(&report).unwrap();
241        assert!(json.contains("C001"));
242        assert!(json.contains("root.children[0]"));
243    }
244
245    #[test]
246    fn exit_codes_are_distinct() {
247        let codes = [
248            exit_codes::SUCCESS,
249            exit_codes::VALIDATION_ERROR,
250            exit_codes::COMPILATION_ERROR,
251            exit_codes::DEPLOYMENT_ERROR,
252            exit_codes::AI_BRIDGE_ERROR,
253            exit_codes::INTERNAL_ERROR,
254        ];
255        let unique: std::collections::HashSet<_> = codes.iter().collect();
256        assert_eq!(unique.len(), codes.len());
257    }
258}