1use 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)] 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}