Skip to main content

hivemind/core/
error.rs

1//! Structured error types.
2//!
3//! Errors must be classifiable, attributable, and actionable.
4//! Every error answers: What failed? Why? What can be done next?
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Error category for classification.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub enum ErrorCategory {
13    /// System-level errors (IO, network, etc.)
14    System,
15    /// Runtime adapter errors
16    Runtime,
17    /// Agent execution errors
18    Agent,
19    /// Scope violation errors
20    Scope,
21    /// Verification check failures
22    Verification,
23    /// Git operation errors
24    Git,
25    /// User input errors
26    User,
27    /// Policy violation errors
28    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/// Structured error with full context.
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub struct HivemindError {
49    /// Error category for classification.
50    pub category: ErrorCategory,
51    /// Unique error code within category.
52    pub code: String,
53    /// Human-readable error message.
54    pub message: String,
55    /// Component and identifier that originated the error.
56    pub origin: String,
57    /// Whether this error is potentially recoverable.
58    pub recoverable: bool,
59    /// Hint for recovery action.
60    pub recovery_hint: Option<String>,
61    /// Additional context key-value pairs.
62    pub context: HashMap<String, String>,
63}
64
65impl HivemindError {
66    /// Creates a new error with the given parameters.
67    #[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    /// Sets whether the error is recoverable.
86    #[must_use]
87    pub fn recoverable(mut self, recoverable: bool) -> Self {
88        self.recoverable = recoverable;
89        self
90    }
91
92    /// Sets the recovery hint.
93    #[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    /// Adds context to the error.
100    #[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    /// Creates a system error.
107    #[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    /// Creates a user input error.
117    #[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    /// Creates a git error.
127    #[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    /// Creates a runtime adapter error.
137    #[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    /// Creates an agent execution error.
147    #[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    /// Creates a scope violation error.
157    #[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    /// Creates a verification error.
167    #[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    /// Creates a policy violation error.
177    #[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
195/// Result type using `HivemindError`.
196pub type Result<T> = std::result::Result<T, HivemindError>;
197
198/// Exit codes for CLI.
199#[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}