1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ErrorCategory {
13 System,
15 Runtime,
17 Agent,
19 Scope,
21 Verification,
23 Git,
25 User,
27 Policy,
29}
30
31impl std::fmt::Display for ErrorCategory {
32 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 match self {
34 Self::System => write!(f, "system"),
35 Self::Runtime => write!(f, "runtime"),
36 Self::Agent => write!(f, "agent"),
37 Self::Scope => write!(f, "scope"),
38 Self::Verification => write!(f, "verification"),
39 Self::Git => write!(f, "git"),
40 Self::User => write!(f, "user"),
41 Self::Policy => write!(f, "policy"),
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct HivemindError {
49 pub category: ErrorCategory,
51 pub code: String,
53 pub message: String,
55 pub origin: String,
57 pub recoverable: bool,
59 pub recovery_hint: Option<String>,
61 pub context: HashMap<String, String>,
63}
64
65impl HivemindError {
66 #[must_use]
68 pub fn new(
69 category: ErrorCategory,
70 code: impl Into<String>,
71 message: impl Into<String>,
72 origin: impl Into<String>,
73 ) -> Self {
74 Self {
75 category,
76 code: code.into(),
77 message: message.into(),
78 origin: origin.into(),
79 recoverable: false,
80 recovery_hint: None,
81 context: HashMap::new(),
82 }
83 }
84
85 #[must_use]
87 pub fn recoverable(mut self, recoverable: bool) -> Self {
88 self.recoverable = recoverable;
89 self
90 }
91
92 #[must_use]
94 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
95 self.recovery_hint = Some(hint.into());
96 self
97 }
98
99 #[must_use]
101 pub fn with_context(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
102 self.context.insert(key.into(), value.into());
103 self
104 }
105
106 #[must_use]
108 pub fn system(
109 code: impl Into<String>,
110 message: impl Into<String>,
111 origin: impl Into<String>,
112 ) -> Self {
113 Self::new(ErrorCategory::System, code, message, origin)
114 }
115
116 #[must_use]
118 pub fn user(
119 code: impl Into<String>,
120 message: impl Into<String>,
121 origin: impl Into<String>,
122 ) -> Self {
123 Self::new(ErrorCategory::User, code, message, origin).recoverable(true)
124 }
125
126 #[must_use]
128 pub fn git(
129 code: impl Into<String>,
130 message: impl Into<String>,
131 origin: impl Into<String>,
132 ) -> Self {
133 Self::new(ErrorCategory::Git, code, message, origin)
134 }
135
136 #[must_use]
138 pub fn runtime(
139 code: impl Into<String>,
140 message: impl Into<String>,
141 origin: impl Into<String>,
142 ) -> Self {
143 Self::new(ErrorCategory::Runtime, code, message, origin).recoverable(true)
144 }
145
146 #[must_use]
148 pub fn agent(
149 code: impl Into<String>,
150 message: impl Into<String>,
151 origin: impl Into<String>,
152 ) -> Self {
153 Self::new(ErrorCategory::Agent, code, message, origin).recoverable(true)
154 }
155
156 #[must_use]
158 pub fn scope(
159 code: impl Into<String>,
160 message: impl Into<String>,
161 origin: impl Into<String>,
162 ) -> Self {
163 Self::new(ErrorCategory::Scope, code, message, origin)
164 }
165
166 #[must_use]
168 pub fn verification(
169 code: impl Into<String>,
170 message: impl Into<String>,
171 origin: impl Into<String>,
172 ) -> Self {
173 Self::new(ErrorCategory::Verification, code, message, origin).recoverable(true)
174 }
175
176 #[must_use]
178 pub fn policy(
179 code: impl Into<String>,
180 message: impl Into<String>,
181 origin: impl Into<String>,
182 ) -> Self {
183 Self::new(ErrorCategory::Policy, code, message, origin)
184 }
185}
186
187impl std::fmt::Display for HivemindError {
188 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189 write!(f, "[{}:{}] {}", self.category, self.code, self.message)
190 }
191}
192
193impl std::error::Error for HivemindError {}
194
195pub type Result<T> = std::result::Result<T, HivemindError>;
197
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum ExitCode {
201 Success = 0,
202 Error = 1,
203 NotFound = 2,
204 Conflict = 3,
205 PermissionDenied = 4,
206}
207
208impl From<ExitCode> for i32 {
209 fn from(code: ExitCode) -> Self {
210 code as Self
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn error_display() {
220 let err = HivemindError::system("io_error", "Failed to read file", "storage:event_store");
221 assert!(err.to_string().contains("system"));
222 assert!(err.to_string().contains("io_error"));
223 }
224
225 #[test]
226 fn error_with_context() {
227 let err = HivemindError::user(
228 "invalid_name",
229 "Project name cannot be empty",
230 "cli:project",
231 )
232 .with_context("field", "name")
233 .with_hint("Provide a non-empty project name");
234
235 assert_eq!(err.context.get("field"), Some(&"name".to_string()));
236 assert!(err.recovery_hint.is_some());
237 assert!(err.recoverable);
238 }
239
240 #[test]
241 fn error_serialization() {
242 let err = HivemindError::git("clone_failed", "Failed to clone repository", "git:clone")
243 .with_context("repo", "https://github.com/example/repo");
244
245 let json = serde_json::to_string(&err).expect("serialize");
246 let restored: HivemindError = serde_json::from_str(&json).expect("deserialize");
247
248 assert_eq!(restored.category, ErrorCategory::Git);
249 assert_eq!(restored.code, "clone_failed");
250 }
251}