Skip to main content

ggen_cli_lib/
error.rs

1//! Semantic exit codes and error handling for ggen CLI
2//!
3//! Provides deterministic, agent-friendly error handling with semantic exit codes
4//! that enable agents to understand why a command failed and respond appropriately.
5//!
6//! # Exit Codes
7//! - 0: Success - operation completed successfully
8//! - 1: ValidationError - RDF/SHACL/type validation failed
9//! - 2: SparqlError - SPARQL query syntax/termination error
10//! - 3: TemplateError - Template rendering failed
11//! - 4: OutputInvalid - Generated code failed validation (not valid Rust)
12//! - 5: Timeout - Operation exceeded time limit
13//! - 6: FileError - File system operation failed
14//! - 7: NetworkError - Network operation failed
15//! - 8: ConfigError - Configuration validation failed
16//! - 127: Unknown - Unexpected error
17
18use thiserror::Error;
19
20/// Semantic error types for ggen CLI operations
21#[derive(Error, Debug)]
22pub enum GgenError {
23    /// RDF parsing, SHACL validation, or type consistency error
24    #[error("Validation error: {0}")]
25    ValidationError(String),
26
27    /// SPARQL query syntax error, termination issue, or execution error
28    #[error("SPARQL error: {0}")]
29    SparqlError(String),
30
31    /// Template rendering error (Tera error, context mismatch)
32    #[error("Template error: {0}")]
33    TemplateError(String),
34
35    /// Generated code failed validation (syntax, compilation, safety checks)
36    #[error("Output validation error: {0}")]
37    OutputInvalid(String),
38
39    /// Operation exceeded time limit (>5s for SPARQL, >10s for code gen)
40    #[error("Operation timeout: {0}")]
41    Timeout(String),
42
43    /// File I/O error
44    #[error("File error: {0}")]
45    FileError(String),
46
47    /// File I/O error with path context
48    #[error("File error in {path}: {message}")]
49    FileErrorWithPath { path: String, message: String },
50
51    /// Network operation error (HTTP, API calls, etc.)
52    #[error("Network error: {0}")]
53    NetworkError(String),
54
55    /// JSON serialization/deserialization error
56    #[error("JSON error: {0}")]
57    JsonError(String),
58
59    /// Configuration file parsing or validation error
60    #[error("Configuration error: {0}")]
61    ConfigError(String),
62
63    /// clap noun-verb command execution error
64    #[error("Command execution error: {0}")]
65    CommandError(String),
66
67    /// External tool/dependency error (LLM, marketplace, etc.)
68    #[error("External service error: {0}")]
69    ExternalServiceError(String),
70
71    /// Internal error (should be avoided - use specific variants above)
72    #[error("Internal error: {0}")]
73    Internal(String),
74
75    /// PaaS operation error (converted from PaasError)
76    #[error("PaaS error: {0}")]
77    PaasError(String),
78
79    /// Pack receipt error (converted from PackReceiptError)
80    #[error("Pack receipt error: {0}")]
81    PackReceiptError(String),
82
83    /// Validation error (converted from ValidationError)
84    #[error("Validation error: {0}")]
85    InvalidInput(String),
86}
87
88impl From<std::io::Error> for GgenError {
89    fn from(err: std::io::Error) -> Self {
90        GgenError::FileError(err.to_string())
91    }
92}
93
94impl From<serde_json::error::Error> for GgenError {
95    fn from(err: serde_json::error::Error) -> Self {
96        GgenError::JsonError(err.to_string())
97    }
98}
99
100impl From<GgenError> for ggen_core::utils::Error {
101    fn from(err: GgenError) -> Self {
102        ggen_core::utils::Error::new(&err.to_string())
103    }
104}
105
106impl From<GgenError> for clap_noun_verb::NounVerbError {
107    fn from(err: GgenError) -> Self {
108        clap_noun_verb::NounVerbError::execution_error(&err.to_string())
109    }
110}
111
112impl GgenError {
113    /// Convert from clap noun-verb error
114    pub fn from_clap_error(err: clap_noun_verb::NounVerbError) -> Self {
115        GgenError::CommandError(err.to_string())
116    }
117
118    /// Convert from PaasError
119    pub fn from_paas_error(err: impl std::fmt::Display) -> Self {
120        GgenError::PaasError(err.to_string())
121    }
122
123    /// Convert from PackReceiptError
124    pub fn from_pack_receipt_error(err: impl std::fmt::Display) -> Self {
125        GgenError::PackReceiptError(err.to_string())
126    }
127
128    /// Convert from ValidationError
129    pub fn from_validation_error(err: impl std::fmt::Display) -> Self {
130        GgenError::InvalidInput(err.to_string())
131    }
132
133    /// Create a file error with path context
134    pub fn file_error(path: &str, message: &str) -> Self {
135        GgenError::FileErrorWithPath {
136            path: path.to_string(),
137            message: message.to_string(),
138        }
139    }
140
141    /// Create a network error
142    pub fn network_error(message: &str) -> Self {
143        GgenError::NetworkError(message.to_string())
144    }
145
146    /// Create an external service error
147    pub fn external_service_error(message: &str) -> Self {
148        GgenError::ExternalServiceError(message.to_string())
149    }
150}
151
152impl GgenError {
153    /// Get semantic exit code for this error
154    pub fn exit_code(&self) -> i32 {
155        match self {
156            GgenError::ValidationError(_) => 1,
157            GgenError::SparqlError(_) => 2,
158            GgenError::TemplateError(_) => 3,
159            GgenError::OutputInvalid(_) => 4,
160            GgenError::Timeout(_) => 5,
161            GgenError::FileError(_) => 127,
162            GgenError::JsonError(_) => 127,
163            GgenError::Internal(_) => 127,
164            _ => 1,
165        }
166    }
167
168    /// Human-readable error category for agent decisions
169    pub fn category(&self) -> &'static str {
170        match self {
171            GgenError::ValidationError(_) => "validation",
172            GgenError::SparqlError(_) => "sparql",
173            GgenError::TemplateError(_) => "template",
174            GgenError::OutputInvalid(_) => "output",
175            GgenError::Timeout(_) => "timeout",
176            GgenError::FileError(_) => "file",
177            GgenError::JsonError(_) => "json",
178            GgenError::Internal(_) => "internal",
179            _ => "unknown",
180        }
181    }
182}
183
184/// Result type for ggen CLI operations
185pub type Result<T> = std::result::Result<T, GgenError>;
186
187/// Extension trait for easy error conversion
188pub trait GgenResultExt<T> {
189    /// Convert any result to GgenError using appropriate conversion
190    fn to_ggen_result(self) -> Result<T>;
191}
192
193impl<T, E> GgenResultExt<T> for std::result::Result<T, E>
194where
195    E: ToString,
196{
197    fn to_ggen_result(self) -> Result<T> {
198        self.map_err(|e| GgenError::Internal(e.to_string()))
199    }
200}
201
202/// Macro for easy error conversion in CLI commands
203#[macro_export]
204macro_rules! map_err {
205    ($expr:expr, $error_type:ident) => {
206        $expr.map_err(|e| $crate::error::GgenError::$error_type(e.to_string()))?
207    };
208    ($expr:expr, $error_type:ident, $message:expr) => {
209        $expr.map_err(|e| $crate::error::GgenError::$error_type(format!("{}: {}", $message, e)))?
210    };
211}
212
213/// Audit trail for code generation (enables agent verification)
214#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
215pub struct AuditTrail {
216    /// SHA256 hash of input ontology
217    pub input_ontology_hash: String,
218
219    /// SPARQL query executed
220    pub sparql_query: String,
221
222    /// Template name used
223    pub template_name: String,
224
225    /// Generated code
226    pub output_code: String,
227
228    /// Validation passed (true = safe to commit, false = review needed)
229    pub validation_passed: bool,
230
231    /// Exit code from generation
232    pub exit_code: i32,
233
234    /// Generation time in milliseconds
235    pub duration_ms: u64,
236
237    /// Validation errors (if any)
238    pub validation_errors: Vec<String>,
239}
240
241impl AuditTrail {
242    /// Create new audit trail
243    pub fn new(
244        input_ontology_hash: String, sparql_query: String, template_name: String,
245        output_code: String,
246    ) -> Self {
247        Self {
248            input_ontology_hash,
249            sparql_query,
250            template_name,
251            output_code,
252            validation_passed: false,
253            exit_code: 0,
254            duration_ms: 0,
255            validation_errors: Vec::new(),
256        }
257    }
258
259    /// Mark validation as passed
260    pub fn mark_valid(mut self) -> Self {
261        self.validation_passed = true;
262        self.exit_code = 0;
263        self
264    }
265
266    /// Add validation error
267    pub fn add_error(mut self, error: String) -> Self {
268        self.validation_errors.push(error);
269        self.validation_passed = false;
270        self.exit_code = 4; // OutputInvalid
271        self
272    }
273
274    /// Set generation duration
275    pub fn with_duration(mut self, duration_ms: u64) -> Self {
276        self.duration_ms = duration_ms;
277        self
278    }
279}
280
281#[cfg(test)]
282mod tests {
283    use super::*;
284
285    #[test]
286    fn test_exit_codes() {
287        assert_eq!(
288            GgenError::ValidationError("test".to_string()).exit_code(),
289            1
290        );
291        assert_eq!(GgenError::SparqlError("test".to_string()).exit_code(), 2);
292        assert_eq!(GgenError::TemplateError("test".to_string()).exit_code(), 3);
293        assert_eq!(GgenError::OutputInvalid("test".to_string()).exit_code(), 4);
294        assert_eq!(GgenError::Timeout("test".to_string()).exit_code(), 5);
295    }
296
297    #[test]
298    fn test_audit_trail() {
299        let audit = AuditTrail::new(
300            "abc123".to_string(),
301            "SELECT ?x WHERE { ?x a rdfs:Class }".to_string(),
302            "rust-service".to_string(),
303            "struct User { id: Uuid }".to_string(),
304        )
305        .mark_valid()
306        .with_duration(42);
307
308        assert!(audit.validation_passed);
309        assert_eq!(audit.exit_code, 0);
310        assert_eq!(audit.duration_ms, 42);
311    }
312}