Skip to main content

csm_core_lib/
error.rs

1//! Error types for chaotic semantic memory
2
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum MemoryError {
7    #[error("Database error: {message}")]
8    Database {
9        message: String,
10        #[source]
11        source: Option<Box<dyn std::error::Error + Send + Sync>>,
12    },
13
14    #[error("Invalid input for '{field}': {reason}")]
15    InvalidInput { field: String, reason: String },
16
17    #[error("Invalid vector dimension: expected {expected}, got {actual}")]
18    InvalidDimension { expected: usize, actual: usize },
19
20    #[error("{entity} not found: '{id}'")]
21    NotFound { entity: String, id: String },
22
23    #[error("Unsupported operation: {0}")]
24    UnsupportedOperation(String),
25
26    #[error("Reservoir error: {message}")]
27    Reservoir {
28        message: String,
29        #[source]
30        source: Option<Box<dyn std::error::Error + Send + Sync>>,
31    },
32
33    #[error("Persistence error: {0}")]
34    Persistence(String),
35
36    #[error("External service error: {0}")]
37    External(String),
38
39    #[error("Configuration error: {0}")]
40    Config(String),
41
42    #[error("IO error: {0}")]
43    Io(#[from] std::io::Error),
44
45    #[error("Serialization error: {0}")]
46    Serialization(#[from] serde_json::Error),
47
48    #[error("Observability error: {0}")]
49    Observability(String),
50
51    #[error("Observability feature '{feature}' is not enabled; rebuild with --features {feature}")]
52    ObservabilityFeatureDisabled { feature: &'static str },
53
54    #[error("Observability stack already initialised in this process")]
55    ObservabilityAlreadyInitialised,
56}
57
58impl MemoryError {
59    pub fn database(message: impl Into<String>) -> Self {
60        Self::Database {
61            message: message.into(),
62            source: None,
63        }
64    }
65
66    pub fn database_with_source(
67        message: impl Into<String>,
68        source: impl std::error::Error + Send + Sync + 'static,
69    ) -> Self {
70        Self::Database {
71            message: message.into(),
72            source: Some(Box::new(source)),
73        }
74    }
75
76    pub fn reservoir(message: impl Into<String>) -> Self {
77        Self::Reservoir {
78            message: message.into(),
79            source: None,
80        }
81    }
82
83    pub fn reservoir_with_source(
84        message: impl Into<String>,
85        source: impl std::error::Error + Send + Sync + 'static,
86    ) -> Self {
87        Self::Reservoir {
88            message: message.into(),
89            source: Some(Box::new(source)),
90        }
91    }
92
93    #[allow(clippy::missing_const_for_fn)] // match arms with string refs prevent const
94    pub fn remediation(&self) -> Option<&'static str> {
95        match self {
96            Self::Database { .. } => Some(
97                "Check that the database path is accessible and the schema is up to date. Run with persistence disabled to rule out DB issues.",
98            ),
99            Self::InvalidInput { .. } => Some(
100                "Verify the input format matches the expected type and constraints for the given field.",
101            ),
102            Self::InvalidDimension { .. } => Some(
103                "Ensure vector dimension matches the configured size. Use FrameworkBuilder::with_reservoir_input_size() to adjust.",
104            ),
105            Self::NotFound { .. } => Some(
106                "Check that the concept ID exists before performing this operation. Use get() or probe() to verify.",
107            ),
108            Self::UnsupportedOperation(_) => Some(
109                "This operation is not supported in the current configuration. Check feature flags and configuration.",
110            ),
111            Self::Reservoir { .. } => Some(
112                "Check reservoir configuration (input_size, spectral_radius). Reset with FrameworkBuilder defaults if needed.",
113            ),
114            Self::Persistence(_) => Some(
115                "Verify database connectivity and file system permissions. Check that the database file is not corrupted.",
116            ),
117            Self::External(_) => {
118                Some("Check the external service configuration and network connectivity.")
119            }
120            Self::Config(_) => Some(
121                "Review configuration parameters. Use FrameworkBuilder defaults for a known-good starting point.",
122            ),
123            Self::Io(_) => Some("Check file system permissions and available disk space."),
124            Self::Serialization(_) => Some(
125                "Ensure data is valid JSON/binary format. Use export/import functions for safe serialization.",
126            ),
127            Self::Observability(_) => Some(
128                "Check observability configuration. Ensure endpoints are reachable and feature flags are enabled.",
129            ),
130            Self::ObservabilityFeatureDisabled { .. } => {
131                Some("Rebuild with the required feature flag enabled.")
132            }
133            Self::ObservabilityAlreadyInitialised => Some(
134                "Observability stack can only be initialised once per process. Check for duplicate initialization.",
135            ),
136        }
137    }
138}
139
140pub type Result<T> = std::result::Result<T, MemoryError>;
141
142#[cfg(test)]
143mod tests {
144    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
145    use super::MemoryError;
146
147    #[test]
148    fn database_error_exposes_source_chain() {
149        let io = std::io::Error::other("inner-io");
150        let err = MemoryError::database_with_source("db failed", io);
151        let source = std::error::Error::source(&err).expect("source should exist");
152        assert_eq!(source.to_string(), "inner-io");
153    }
154
155    #[test]
156    fn reservoir_error_exposes_source_chain() {
157        let io = std::io::Error::other("inner-reservoir");
158        let err = MemoryError::reservoir_with_source("reservoir failed", io);
159        let source = std::error::Error::source(&err).expect("source should exist");
160        assert_eq!(source.to_string(), "inner-reservoir");
161    }
162
163    #[test]
164    fn remediation_returns_hints_for_all_variants() {
165        let cases: Vec<(MemoryError, &str)> = vec![
166            (
167                MemoryError::database("db"),
168                "Check that the database path is accessible",
169            ),
170            (
171                MemoryError::InvalidInput {
172                    field: "f".into(),
173                    reason: "r".into(),
174                },
175                "Verify the input format",
176            ),
177            (
178                MemoryError::InvalidDimension {
179                    expected: 128,
180                    actual: 64,
181                },
182                "Ensure vector dimension",
183            ),
184            (
185                MemoryError::NotFound {
186                    entity: "concept".into(),
187                    id: "x".into(),
188                },
189                "Check that the concept ID",
190            ),
191            (
192                MemoryError::UnsupportedOperation("op".into()),
193                "This operation is not supported",
194            ),
195            (
196                MemoryError::reservoir("res"),
197                "Check reservoir configuration",
198            ),
199            (
200                MemoryError::Persistence("p".into()),
201                "Verify database connectivity",
202            ),
203            (
204                MemoryError::External("e".into()),
205                "Check the external service",
206            ),
207            (
208                MemoryError::Config("c".into()),
209                "Review configuration parameters",
210            ),
211            (
212                MemoryError::Io(std::io::Error::other("io")),
213                "Check file system permissions",
214            ),
215            (
216                MemoryError::Serialization(serde_json::from_str::<i32>("bad").unwrap_err()),
217                "Ensure data is valid JSON",
218            ),
219            (
220                MemoryError::Observability("o".into()),
221                "Check observability configuration",
222            ),
223            (
224                MemoryError::ObservabilityFeatureDisabled { feature: "metrics" },
225                "Rebuild with the required feature flag",
226            ),
227            (
228                MemoryError::ObservabilityAlreadyInitialised,
229                "Observability stack can only be initialised once",
230            ),
231        ];
232        for (err, prefix) in cases {
233            let hint = err.remediation().expect("remediation should return Some");
234            assert!(
235                hint.starts_with(prefix),
236                "remediation hint for {err:?} should start with {prefix:?}, got {hint:?}"
237            );
238        }
239    }
240}