1use thiserror::Error;
7
8#[non_exhaustive]
10#[derive(Error, Debug)]
11pub enum NounVerbError {
12 #[error("Command '{noun}' not found{suggestion}")]
14 CommandNotFound { noun: String, suggestion: String },
15
16 #[error("Verb '{verb}' not found for noun '{noun}'{suggestion}")]
18 VerbNotFound { noun: String, verb: String, suggestion: String },
19
20 #[error("Invalid command structure: {message}")]
22 InvalidStructure { message: String },
23
24 #[error("Command execution failed: {message}")]
26 ExecutionError { message: String },
27
28 #[error("Argument parsing failed: {message}")]
30 ArgumentError { message: String },
31
32 #[error("Plugin error: {0}")]
34 PluginError(String),
35
36 #[error("Validation failed: {0}")]
38 ValidationFailed(String),
39
40 #[error("Middleware error: {0}")]
42 MiddlewareError(String),
43
44 #[error("Telemetry error: {0}")]
46 TelemetryError(String),
47
48 #[error("Error: {0}")]
50 Generic(String),
51}
52
53fn levenshtein_distance(a: &str, b: &str) -> usize {
55 let a_chars: Vec<char> = a.chars().collect();
56 let b_chars: Vec<char> = b.chars().collect();
57 let a_len = a_chars.len();
58 let b_len = b_chars.len();
59
60 if a_len == 0 {
61 return b_len;
62 }
63 if b_len == 0 {
64 return a_len;
65 }
66
67 let mut cache: Vec<usize> = (1..=b_len).collect();
68 let mut dist = 0;
69
70 for (i, &ca) in a_chars.iter().enumerate() {
71 let mut result = i;
72 dist = i + 1;
73 for (j, &cb) in b_chars.iter().enumerate() {
74 let temp = result;
75 result = cache[j];
76 dist =
77 if ca == cb { temp } else { std::cmp::min(std::cmp::min(result, dist), temp) + 1 };
78 cache[j] = dist;
79 }
80 }
81
82 dist
83}
84
85pub fn find_best_matches<'a>(input: &str, candidates: &[&'a str]) -> Vec<&'a str> {
87 let mut with_distances: Vec<(&str, usize)> = candidates
88 .iter()
89 .map(|&c| (c, levenshtein_distance(input, c)))
90 .filter(|&(_, dist)| dist <= 3 && dist < input.len())
91 .collect();
92
93 with_distances.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(b.0)));
94 with_distances.into_iter().map(|(c, _)| c).collect()
95}
96
97impl NounVerbError {
98 pub fn with_recovery_suggestions(self) -> String {
104 self.to_string()
106 }
107
108 pub fn command_not_found(noun: impl Into<String>) -> Self {
110 Self::CommandNotFound { noun: noun.into(), suggestion: String::new() }
111 }
112
113 pub fn command_not_found_with_candidates(noun: impl Into<String>, candidates: &[&str]) -> Self {
115 let noun_str = noun.into();
116 let matches = find_best_matches(&noun_str, candidates);
117 let suggestion = if matches.is_empty() {
118 String::new()
119 } else {
120 let suggestions_str = matches
121 .iter()
122 .map(|s| format!("\x1b[1m\x1b[33m{}\x1b[0m", s))
123 .collect::<Vec<_>>()
124 .join(", ");
125 format!(". Did you mean: {}?", suggestions_str)
126 };
127 Self::CommandNotFound { noun: noun_str, suggestion }
128 }
129
130 pub fn verb_not_found(noun: impl Into<String>, verb: impl Into<String>) -> Self {
132 Self::VerbNotFound { noun: noun.into(), verb: verb.into(), suggestion: String::new() }
133 }
134
135 pub fn verb_not_found_with_candidates(
137 noun: impl Into<String>,
138 verb: impl Into<String>,
139 candidates: &[&str],
140 ) -> Self {
141 let verb_str = verb.into();
142 let matches = find_best_matches(&verb_str, candidates);
143 let suggestion = if matches.is_empty() {
144 String::new()
145 } else {
146 let suggestions_str = matches
147 .iter()
148 .map(|s| format!("\x1b[1m\x1b[33m{}\x1b[0m", s))
149 .collect::<Vec<_>>()
150 .join(", ");
151 format!(". Did you mean: {}?", suggestions_str)
152 };
153 Self::VerbNotFound { noun: noun.into(), verb: verb_str, suggestion }
154 }
155
156 pub fn invalid_structure(message: impl Into<String>) -> Self {
158 Self::InvalidStructure { message: message.into() }
159 }
160
161 pub fn execution_error(message: impl Into<String>) -> Self {
163 Self::ExecutionError { message: message.into() }
164 }
165
166 pub fn argument_error(message: impl Into<String>) -> Self {
168 Self::ArgumentError { message: message.into() }
169 }
170
171 pub fn missing_argument(name: impl Into<String>) -> Self {
173 Self::ArgumentError { message: format!("Required argument '{}' is missing", name.into()) }
174 }
175
176 pub fn validation_error(
178 name: impl Into<String>,
179 value: impl Into<String>,
180 constraints: Option<&str>,
181 ) -> Self {
182 let name = name.into();
183 let value = value.into();
184 if let Some(constraints) = constraints {
185 Self::ArgumentError {
186 message: format!(
187 "Invalid value '{}' for argument '{}'. {}",
188 value, name, constraints
189 ),
190 }
191 } else {
192 Self::ArgumentError {
193 message: format!("Invalid value '{}' for argument '{}'", value, name),
194 }
195 }
196 }
197
198 pub fn validation_range_error(
200 name: impl Into<String>,
201 value: impl Into<String>,
202 min: Option<&str>,
203 max: Option<&str>,
204 ) -> Self {
205 let name = name.into();
206 let value = value.into();
207 let constraint_msg = match (min, max) {
208 (Some(min), Some(max)) => format!("Must be between {} and {}", min, max),
209 (Some(min), None) => format!("Must be >= {}", min),
210 (None, Some(max)) => format!("Must be <= {}", max),
211 (None, None) => "Invalid value".to_string(),
212 };
213 Self::validation_error(name, value, Some(&constraint_msg))
214 }
215
216 pub fn validation_length_error(
218 name: impl Into<String>,
219 value: impl Into<String>,
220 min: Option<usize>,
221 max: Option<usize>,
222 ) -> Self {
223 let name = name.into();
224 let value = value.into();
225 let constraint_msg = match (min, max) {
226 (Some(min), Some(max)) => {
227 format!("Length must be between {} and {} characters", min, max)
228 }
229 (Some(min), None) => format!("Length must be at least {} characters", min),
230 (None, Some(max)) => format!("Length must be at most {} characters", max),
231 (None, None) => "Invalid length".to_string(),
232 };
233 Self::validation_error(name, value, Some(&constraint_msg))
234 }
235}
236
237impl From<std::io::Error> for NounVerbError {
238 fn from(err: std::io::Error) -> Self {
239 Self::ExecutionError { message: err.to_string() }
240 }
241}
242
243pub type Result<T> = std::result::Result<T, NounVerbError>;
245
246#[non_exhaustive]
248#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
249pub enum ErrorKind {
250 InvalidInput,
252 PermissionDenied,
254 InvariantBreach,
256 DeadlineExceeded,
258 GuardExceeded,
260 CommandNotFound,
262 VerbNotFound,
264 ExecutionError,
266 InternalError,
268}
269
270#[non_exhaustive]
272#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
273pub enum Severity {
274 Warning,
276 Error,
278 Critical,
280}
281
282#[non_exhaustive]
284#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
285#[serde(untagged)]
286pub enum ActionTemplate {
287 TimeoutAdjustment {
289 suggested_timeout_ms: u64,
291 reason: String,
293 },
294 CommandFix {
296 suggested_command: String,
298 reason: String,
300 },
301}
302
303#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
305pub struct StructuredError {
306 pub kind: ErrorKind,
308 pub severity: Severity,
310 pub message: String,
312 pub details: std::collections::HashMap<String, serde_json::Value>,
314 pub action_templates: Vec<ActionTemplate>,
316}
317
318impl StructuredError {
319 pub fn deadline_exceeded(deadline_ms: u64, actual_ms: u64) -> Self {
321 let mut details = std::collections::HashMap::new();
322 details.insert("deadline_ms".to_string(), serde_json::json!(deadline_ms));
323 details.insert("actual_ms".to_string(), serde_json::json!(actual_ms));
324
325 Self {
326 kind: ErrorKind::DeadlineExceeded,
327 severity: Severity::Critical,
328 message: format!("Deadline {}ms exceeded, took {}ms", deadline_ms, actual_ms),
329 details,
330 action_templates: vec![ActionTemplate::TimeoutAdjustment {
331 suggested_timeout_ms: actual_ms + 100,
332 reason: "Increase deadline budget to match observed latency".to_string(),
333 }],
334 }
335 }
336
337 pub fn from_error(err: &NounVerbError) -> Self {
339 let mut details = std::collections::HashMap::new();
340 let mut action_templates = Vec::new();
341 let mut severity = Severity::Error;
342
343 let kind = match err {
344 NounVerbError::CommandNotFound { noun, suggestion } => {
345 details.insert("noun".to_string(), serde_json::Value::String(noun.clone()));
346 if !suggestion.is_empty() {
347 details.insert(
348 "suggestion".to_string(),
349 serde_json::Value::String(suggestion.clone()),
350 );
351 let clean = suggestion
352 .replace("\x1b[1m\x1b[33m", "")
353 .replace("\x1b[0m", "")
354 .replace(". Did you mean: ", "")
355 .replace("?", "");
356 let candidates: Vec<&str> = clean.split(", ").collect();
357 if let Some(first) = candidates.first() {
358 if !first.is_empty() {
359 action_templates.push(ActionTemplate::CommandFix {
360 suggested_command: first.to_string(),
361 reason: format!(
362 "Suggested correction for misspelled command '{}'",
363 noun
364 ),
365 });
366 }
367 }
368 }
369 ErrorKind::CommandNotFound
370 }
371 NounVerbError::VerbNotFound { noun, verb, suggestion } => {
372 details.insert("noun".to_string(), serde_json::Value::String(noun.clone()));
373 details.insert("verb".to_string(), serde_json::Value::String(verb.clone()));
374 if !suggestion.is_empty() {
375 details.insert(
376 "suggestion".to_string(),
377 serde_json::Value::String(suggestion.clone()),
378 );
379 let clean = suggestion
380 .replace("\x1b[1m\x1b[33m", "")
381 .replace("\x1b[0m", "")
382 .replace(". Did you mean: ", "")
383 .replace("?", "");
384 let candidates: Vec<&str> = clean.split(", ").collect();
385 if let Some(first) = candidates.first() {
386 if !first.is_empty() {
387 action_templates.push(ActionTemplate::CommandFix {
388 suggested_command: format!("{} {}", noun, first),
389 reason: format!(
390 "Suggested correction for misspelled verb '{}'",
391 verb
392 ),
393 });
394 }
395 }
396 }
397 ErrorKind::VerbNotFound
398 }
399 NounVerbError::InvalidStructure { message } => {
400 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
401 ErrorKind::InvalidInput
402 }
403 NounVerbError::ExecutionError { message } => {
404 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
405 if message.to_lowercase().contains("deadline")
406 || message.to_lowercase().contains("timeout")
407 || message.to_lowercase().contains("budget exceeded")
408 {
409 severity = Severity::Critical;
410 action_templates.push(ActionTemplate::TimeoutAdjustment {
411 suggested_timeout_ms: 1000,
412 reason: "Increase deadline budget due to execution timeout".to_string(),
413 });
414 ErrorKind::DeadlineExceeded
415 } else {
416 ErrorKind::ExecutionError
417 }
418 }
419 NounVerbError::ArgumentError { message } => {
420 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
421 ErrorKind::InvalidInput
422 }
423 NounVerbError::PluginError(message) => {
424 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
425 ErrorKind::InternalError
426 }
427 NounVerbError::ValidationFailed(message) => {
428 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
429 ErrorKind::InvariantBreach
430 }
431 NounVerbError::MiddlewareError(message) => {
432 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
433 ErrorKind::InternalError
434 }
435 NounVerbError::TelemetryError(message) => {
436 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
437 ErrorKind::InternalError
438 }
439 NounVerbError::Generic(message) => {
440 details.insert("message".to_string(), serde_json::Value::String(message.clone()));
441 ErrorKind::InternalError
442 }
443 };
444
445 StructuredError { kind, severity, message: err.to_string(), details, action_templates }
446 }
447}