Skip to main content

mempill_core/
error.rs

1//! MemError — top-level error type for the mempill engine.
2//!
3//! Every invariant violation surfaces as a typed variant — never silently swallowed.
4//! Uses `thiserror` for ergonomic `Display` + `Error` implementations.
5
6use thiserror::Error;
7use mempill_types::{AgentId, ClaimRef};
8
9/// Top-level error type for the mempill engine.
10/// Every invariant violation surfaces as a typed variant here — never silently swallowed.
11#[derive(Debug, Error)]
12pub enum MemError {
13    // ── STRUCTURAL WRITE REJECTIONS (Disposition::Rejected) ──────────────────
14    /// Provenance label is absent on the write request.
15    #[error("Missing or untyped provenance on write: claim cannot be committed without a provenance label")]
16    MissingProvenance,
17
18    /// The caller does not hold write authority for the specified `agent_id`.
19    #[error("Caller does not hold write authority for agent_id {agent_id:?}")]
20    WriteAuthorityViolation {
21        /// The agent ID that the caller attempted to write on behalf of.
22        agent_id: AgentId,
23    },
24
25    /// The fact payload is structurally invalid (empty subject, invalid JSON, etc.).
26    #[error("Malformed fact: {reason}")]
27    MalformedFact {
28        /// Human-readable description of the malformation.
29        reason: String,
30    },
31
32    /// The supplied `agent_id` is not recognised by the engine.
33    #[error("Unknown or invalid agent_id: {agent_id:?}")]
34    UnknownAgentId {
35        /// The unrecognised agent ID.
36        agent_id: AgentId,
37    },
38
39    /// The referenced claim does not exist in the store.
40    #[error("Claim not found: {claim_ref:?}")]
41    ClaimNotFound {
42        /// The claim reference that was not found.
43        claim_ref: ClaimRef,
44    },
45
46    // ── CONCURRENCY ───────────────────────────────────────────────────────────
47    /// Single-writer-per-agent-id invariant violated: the write lock is already held.
48    #[error("Write lock for agent_id {agent_id:?} is already held (single-writer-per-agent-id violation)")]
49    WriteLockContention {
50        /// The agent ID whose write lock is already held.
51        agent_id: AgentId,
52    },
53
54    // ── ASYNC / SPAWN_BLOCKING BRIDGE ─────────────────────────────────────────
55    /// Returned when a `tokio::task::spawn_blocking` call fails to join at the EngineHandle async boundary.
56    #[error("spawn_blocking task failed: {reason}")]
57    SpawnBlocking {
58        /// Description of the join error.
59        reason: String,
60    },
61
62    // ── INVARIANT VIOLATIONS (bugs — should never occur in correct impl) ──────
63    /// Partial write detected — atomic commit unit invariant violated.
64    #[error("Atomic commit unit violated: partial write detected for agent_id {agent_id:?}")]
65    AtomicCommitViolation {
66        /// The agent ID for which the partial write was detected.
67        agent_id: AgentId,
68    },
69
70    /// Belief changed between reads without an intervening write — fixed-history monotonicity violated.
71    #[error(
72        "Fixed-history monotonicity violated: belief changed between reads without an intervening write \
73         for agent_id {agent_id:?}"
74    )]
75    MonotonicityViolation {
76        /// The agent ID for which monotonicity was violated.
77        agent_id: AgentId,
78    },
79
80    /// Materialized belief cache disagrees with the canonical fold result.
81    #[error(
82        "Belief cache inconsistency: materialized belief cache disagrees with canonical fold \
83         (cache must be subordinate)"
84    )]
85    BeliefCacheInconsistency,
86
87    // ── TEMPORAL COHERENCE ────────────────────────────────────────────────────
88    /// `valid_time_start` is after `valid_time_end`.
89    #[error(
90        "Temporal coherence failure: valid_time_start ({start}) is after valid_time_end ({end})"
91    )]
92    IncoherentTemporalWindow {
93        /// The valid-time start (RFC3339).
94        start: String,
95        /// The valid-time end (RFC3339).
96        end: String,
97    },
98
99    // ── PERSISTENCE ───────────────────────────────────────────────────────────
100    /// Underlying persistence adapter returned an error.
101    #[error("Persistence error: {source}")]
102    Persistence {
103        /// The wrapped persistence error.
104        #[from]
105        source: Box<dyn std::error::Error + Send + Sync + 'static>,
106    },
107
108    /// SQLite PRAGMA initialization failed (WAL, FULL sync, foreign keys).
109    #[error("SQLite PRAGMA initialization failed: {reason}")]
110    PragmaInitFailed {
111        /// Description of the PRAGMA failure.
112        reason: String,
113    },
114
115    // ── ORACLE PORT ───────────────────────────────────────────────────────────
116    /// Oracle port returned an error during `request_adjudication` or another oracle call site.
117    /// Use `OracleError { reason: e.to_string() }` — string-reason convention is consistent
118    /// across all non-persistence, non-internal error variants.
119    #[error("Oracle port error: {reason}")]
120    OracleError {
121        /// Description of the oracle error.
122        reason: String,
123    },
124
125    /// Pending-adjudication store error from insert_pending or mark_resolved.
126    #[error("Pending-adjudication store error: {source}")]
127    PendingStore {
128        /// The wrapped pending-store error.
129        source: Box<dyn std::error::Error + Send + Sync + 'static>,
130    },
131
132    /// The adjudication handle is unknown, expired, or has already been resolved.
133    #[error("Adjudication handle not found: {handle_id}")]
134    AdjudicationHandleNotFound {
135        /// The handle UUID that was not found.
136        handle_id: uuid::Uuid,
137    },
138
139    // ── CONFIGURATION ─────────────────────────────────────────────────────────
140    /// An engine calibration parameter has an invalid value.
141    #[error("Engine calibration parameter invalid: {param} = {value}: {reason}")]
142    ConfigurationError {
143        /// The parameter name.
144        param: String,
145        /// The invalid value.
146        value: String,
147        /// Why the value is invalid.
148        reason: String,
149    },
150}
151
152/// Write surface result — returned synchronously.
153/// For heavy-path ops, disposition = QueuedForAdjudication;
154/// final state arrives asynchronously via the oracle callback.
155pub type WriteResult = Result<mempill_types::WriteOutcome, MemError>;
156
157/// Belief projection result — returned from query_memory.
158pub type BeliefResult = Result<mempill_types::BeliefProjection, MemError>;
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use mempill_types::AgentId;
164
165    #[test]
166    fn mem_error_missing_provenance_display() {
167        let e = MemError::MissingProvenance;
168        let s = e.to_string();
169        assert!(s.contains("provenance"));
170    }
171
172    #[test]
173    fn mem_error_malformed_fact_carries_reason() {
174        let e = MemError::MalformedFact { reason: "empty subject".into() };
175        assert!(e.to_string().contains("empty subject"));
176    }
177
178    #[test]
179    fn mem_error_spawn_blocking_present_and_displays() {
180        let e = MemError::SpawnBlocking { reason: "task panicked".into() };
181        let s = e.to_string();
182        assert!(s.contains("spawn_blocking"));
183        assert!(s.contains("task panicked"));
184    }
185
186    #[test]
187    fn mem_error_write_authority_violation_displays_agent_id() {
188        let e = MemError::WriteAuthorityViolation {
189            agent_id: AgentId("agent-42".into()),
190        };
191        assert!(e.to_string().contains("agent-42"));
192    }
193
194    #[test]
195    fn mem_error_claim_not_found_displays_claim_ref() {
196        let id = uuid::Uuid::new_v4();
197        let e = MemError::ClaimNotFound {
198            claim_ref: mempill_types::ClaimRef(id),
199        };
200        assert!(e.to_string().contains(&id.to_string()));
201    }
202
203    #[test]
204    fn mem_error_atomic_commit_violation_displays_agent_id() {
205        let e = MemError::AtomicCommitViolation {
206            agent_id: AgentId("agent-99".into()),
207        };
208        assert!(e.to_string().contains("agent-99"));
209    }
210
211    #[test]
212    fn mem_error_incoherent_temporal_window_displays_times() {
213        let e = MemError::IncoherentTemporalWindow {
214            start: "2025-01-02T00:00:00Z".into(),
215            end: "2025-01-01T00:00:00Z".into(),
216        };
217        let s = e.to_string();
218        assert!(s.contains("2025-01-02"));
219        assert!(s.contains("2025-01-01"));
220    }
221
222    #[test]
223    fn mem_error_oracle_error_carries_reason() {
224        let e = MemError::OracleError { reason: "timeout".into() };
225        assert!(e.to_string().contains("timeout"));
226    }
227
228    #[test]
229    fn mem_error_configuration_error_displays_all_fields() {
230        let e = MemError::ConfigurationError {
231            param: "valid_time_confidence_threshold".into(),
232            value: "-0.1".into(),
233            reason: "must be in [0.0, 1.0]".into(),
234        };
235        let s = e.to_string();
236        assert!(s.contains("valid_time_confidence_threshold"));
237        assert!(s.contains("-0.1"));
238        assert!(s.contains("must be in [0.0, 1.0]"));
239    }
240
241    #[test]
242    fn mem_error_adjudication_handle_not_found() {
243        let id = uuid::Uuid::new_v4();
244        let e = MemError::AdjudicationHandleNotFound { handle_id: id };
245        assert!(e.to_string().contains(&id.to_string()));
246    }
247
248    #[test]
249    fn mem_error_pragma_init_failed() {
250        let e = MemError::PragmaInitFailed { reason: "WAL failed".into() };
251        assert!(e.to_string().contains("WAL failed"));
252    }
253
254    #[test]
255    fn mem_error_is_debug() {
256        let e = MemError::MissingProvenance;
257        let s = format!("{e:?}");
258        assert!(s.contains("MissingProvenance"));
259    }
260}