mnem_core/error.rs
1//! Top-level error type for `mnem-core`.
2//!
3//! Each module defines its own `thiserror`-based error enum that `From`-converts
4//! into [`enum@Error`]. Public-facing APIs return `Result<T, Error>`.
5//!
6//! `mnem-core` never panics on user input. Invariant violations that are
7//! logically impossible (e.g. a `NodeId` of the wrong length after validated
8//! construction) use `debug_assert!` in debug builds and return `Error` in
9//! release builds.
10
11use thiserror::Error;
12
13/// Top-level error type returned by `mnem-core` public APIs.
14///
15/// Variants are intentionally coarse-grained. Each module's native error type
16/// carries the detail; this top-level enum exists so callers can match on
17/// category without depending on every sub-module's error shape.
18#[derive(Debug, Error)]
19#[non_exhaustive]
20pub enum Error {
21 /// An identity primitive (stable ID, multihash, CID, link) was malformed.
22 #[error("id: {0}")]
23 Id(#[from] IdError),
24 /// A canonical-encoding round-trip failed.
25 #[error("codec: {0}")]
26 Codec(#[from] CodecError),
27 /// A blockstore operation (has/get/put/delete) failed.
28 #[error("store: {0}")]
29 Store(#[from] StoreError),
30 /// An object (Node/Edge/Tree/Commit/...) was malformed or invalid.
31 #[error("object: {0}")]
32 Object(#[from] ObjectError),
33 /// A repository-level operation failed (init, open, commit, ...).
34 #[error("repo: {0}")]
35 Repo(#[from] RepoError),
36 /// A signing or verification operation failed.
37 #[error("sign: {0}")]
38 Sign(#[from] SignError),
39 // Remote, etc. variants land as those modules arrive.
40}
41
42/// Convenient result alias.
43pub type Result<T> = core::result::Result<T, Error>;
44
45impl Error {
46 /// `true` iff this error means "the op-heads store is empty; call
47 /// [`crate::repo::ReadonlyRepo::init`] first". Callers typically
48 /// use it to decide whether to auto-initialise vs. propagate.
49 ///
50 /// Prefer this over stringly-typed `format!("{e}").contains(...)`
51 /// matches: the latter silently breaks on any wording change.
52 #[must_use]
53 pub const fn is_uninitialized(&self) -> bool {
54 matches!(self, Self::Repo(RepoError::Uninitialized))
55 }
56}
57
58/// Errors from [`crate::id`] - stable IDs, multihash, CID, link.
59#[derive(Debug, Error)]
60#[non_exhaustive]
61pub enum IdError {
62 /// A stable-ID byte string has the wrong length (expected 16).
63 #[error("stable id: expected 16 bytes, got {got}")]
64 StableIdLength {
65 /// Length of the input in bytes.
66 got: usize,
67 },
68 /// A stable-ID string was not a valid UUID.
69 #[error("stable id: not a valid uuid: {source}")]
70 StableIdParse {
71 /// Underlying `uuid` crate error.
72 #[source]
73 source: uuid::Error,
74 },
75 /// A multihash could not be constructed or decoded.
76 #[error("multihash: {source}")]
77 Multihash {
78 /// Underlying `multihash` crate error.
79 #[source]
80 source: multihash::Error,
81 },
82 /// A CID could not be constructed or decoded.
83 #[error("cid: {source}")]
84 Cid {
85 /// Underlying `cid` crate error.
86 #[source]
87 source: cid::Error,
88 },
89 /// A link was annotated with a codec that doesn't match the underlying CID.
90 #[error("link: expected codec 0x{expected:x}, got 0x{got:x}")]
91 LinkWrongCodec {
92 /// Codec the caller expected the link to point at.
93 expected: u64,
94 /// Codec the CID actually carries.
95 got: u64,
96 },
97}
98
99/// Errors from [`crate::codec`] - canonical encode/decode.
100#[derive(Debug, Error)]
101#[non_exhaustive]
102pub enum CodecError {
103 /// Encoding a value to canonical DAG-CBOR (or DAG-JSON) failed.
104 #[error("encode: {0}")]
105 Encode(String),
106 /// Decoding canonical bytes back into a value failed.
107 #[error("decode: {0}")]
108 Decode(String),
109 /// The value contained a form forbidden by the mnem canonical rules
110 /// (NaN/Inf float, indefinite-length marker, non-string map key, etc.).
111 #[error("non-canonical form: {0}")]
112 NonCanonical(String),
113}
114
115/// Errors from [`crate::store`] - blockstore operations.
116#[derive(Debug, Error)]
117#[non_exhaustive]
118pub enum StoreError {
119 /// The CID attached to a `put` does not match a hash of the bytes.
120 /// Returned only by backends that choose to verify - the default
121 /// contract trusts the caller (ARCHITECTURE §3.1; see also the
122 /// Phase 1 risk review: re-hashing on every put is a hot-path tax).
123 #[error("cid mismatch: claimed {claimed}, computed {computed}")]
124 CidMismatch {
125 /// CID the caller asserted.
126 claimed: crate::id::Cid,
127 /// CID the backend computed from the bytes.
128 computed: crate::id::Cid,
129 },
130 /// A CID referenced by a tree walk (lookup, cursor, diff, GC, …) is
131 /// not present in the blockstore. Indicates a broken or partial tree.
132 #[error("not found: {cid}")]
133 NotFound {
134 /// The CID that was looked up and missing.
135 cid: crate::id::Cid,
136 },
137 /// Backend-specific I/O failure, translated into a string.
138 #[error("io: {0}")]
139 Io(String),
140 /// On-disk content does not hash to the CID it is stored under.
141 /// Indicates silent disk corruption or a store-level bug (e.g. a
142 /// `put_trusted` caller that violated the safety contract).
143 /// The caller MUST treat this as an unrecoverable integrity failure
144 /// for the affected block.
145 #[error("corruption: block stored under {cid} hashes to a different CID: {detail}")]
146 Corruption {
147 /// The CID that was requested (and used as the storage key).
148 cid: String,
149 /// Human-readable description of the mismatch.
150 detail: String,
151 },
152}
153
154/// Errors from [`crate::sign`] - commit / operation signing + verification.
155#[derive(Debug, Error)]
156#[non_exhaustive]
157pub enum SignError {
158 /// Object carries no `signature` field; verification requires one.
159 #[error("no signature attached")]
160 NoSignature,
161 /// Signature field uses an algorithm this implementation doesn't support.
162 #[error("unsupported signature algorithm: {got}")]
163 WrongAlgorithm {
164 /// Algorithm tag found on the object.
165 got: String,
166 },
167 /// Public key is not the expected 32 bytes or fails Ed25519 decode.
168 #[error("malformed public key")]
169 MalformedKey,
170 /// Signature bytes are not the expected 64 bytes.
171 #[error("malformed signature")]
172 MalformedSignature,
173 /// Ed25519 verify rejected the signature.
174 #[error("signature verification failed")]
175 InvalidSignature,
176 /// Re-canonicalising the object for verification failed.
177 #[error("encoding: {0}")]
178 Encoding(String),
179 /// The signing key is present in the revocation list and the object's
180 /// timestamp is strictly after the revocation (SPEC §9.2).
181 #[error("key revoked at {revoked_at} µs (object time: {time} µs)")]
182 RevokedKey {
183 /// Microseconds-since-epoch moment the key was revoked.
184 revoked_at: u64,
185 /// The signed object's `time`.
186 time: u64,
187 },
188}
189
190/// Errors from [`crate::repo`] - repository lifecycle operations.
191#[derive(Debug, Error)]
192#[non_exhaustive]
193pub enum RepoError {
194 /// `open` was called on an op-heads store with no heads. Call
195 /// [`crate::repo::ReadonlyRepo::init`] first.
196 #[error("repository not initialized (op-heads store is empty)")]
197 Uninitialized,
198 /// A CAS or linearized operation observed state that no longer
199 /// matches the caller's expectations (SPEC §6.4 / §6.5). Retry
200 /// against a fresh `ReadonlyRepo`.
201 #[error("stale: observed state is no longer current")]
202 Stale,
203 /// Op-DAG is malformed - heads do not share any common ancestor.
204 /// Cannot happen in a well-formed repository (all heads descend
205 /// from the root op); indicates corruption or partial import.
206 #[error("op-DAG has no common ancestor across the current heads")]
207 NoCommonAncestor,
208 /// `Query::one` (or a similar precondition API) found zero matches.
209 #[error("query found zero matches")]
210 NotFound,
211 /// `Query::one` (or a similar precondition API) found multiple
212 /// matches where exactly one was required.
213 #[error("query found multiple matches where exactly one was required")]
214 AmbiguousMatch,
215 /// A secondary index pointed at a block that is missing, malformed,
216 /// or whose contents contradict the index (wrong label, wrong prop
217 /// value). Indicates corruption or a partial import; does not
218 /// trigger on a simple "no such key" miss.
219 #[error("index corruption: {context} (cid = {cid})")]
220 IndexCorrupt {
221 /// Short description of which index + which key was involved.
222 context: String,
223 /// The CID the index pointed at.
224 cid: crate::id::Cid,
225 },
226 /// A vector-search query's dimension did not match the index
227 /// dimension. Each vector index binds to one model + dim at build
228 /// time; agents must pass a query vector of the exact same shape.
229 #[error("vector dim mismatch: index dim is {index_dim}, query dim is {query_dim}")]
230 VectorDimMismatch {
231 /// Dimension the index was built at.
232 index_dim: u32,
233 /// Dimension of the query vector the caller passed.
234 query_dim: usize,
235 },
236 /// A [`crate::retrieve::Retriever`] was executed without any
237 /// filters or rankers configured. Retrieval needs at least one
238 /// label / prop / text / vector input to produce a useful result.
239 ///
240 /// audit-2026-04-25 P2-1 / P1-3: the error now spells out the
241 /// common remediation path (text query needs an embedder) so CLI
242 /// and MCP callers do not have to guess.
243 #[error(
244 "retrieve: no filters or rankers configured. \
245 A text query requires an embedder: \
246 `mnem config set embed.provider ollama && \
247 mnem config set embed.model nomic-embed-text` \
248 (or pass `--where K=V` / `--label L` for a pure filter query)."
249 )]
250 RetrievalEmpty,
251 /// C8: `add_edge` was called with an endpoint that does not exist
252 /// in the current view (neither in the base commit's node tree nor
253 /// in nodes staged in this transaction). Committing such an edge
254 /// would produce a dangling reference that retrieval and graph-
255 /// expand cannot resolve.
256 #[error("dangling edge: node {id} ({role}) does not exist in the current view")]
257 DanglingEdge {
258 /// The `NodeId` that was not found.
259 id: crate::id::NodeId,
260 /// `"src"` or `"dst"` - which endpoint is missing.
261 role: &'static str,
262 },
263}
264
265/// Errors from [`crate::objects`] - Node/Edge/Tree/... validation and decode.
266#[derive(Debug, Error)]
267#[non_exhaustive]
268pub enum ObjectError {
269 /// The `_kind` discriminator on the wire doesn't match the expected
270 /// Rust type. For example, decoding an Edge as a Node.
271 #[error("wrong kind: expected '{expected}', got '{got}'")]
272 WrongKind {
273 /// The `_kind` value the Rust type expects.
274 expected: &'static str,
275 /// The `_kind` value found in the encoded bytes.
276 got: String,
277 },
278 /// An [`crate::objects::Embedding`]'s `vector` length does not match
279 /// `dim × bytes_per_dtype(dtype)` (SPEC §4.1).
280 #[error("embedding size mismatch: expected {expected} bytes, got {got}")]
281 EmbeddingSizeMismatch {
282 /// Required vector length: `dim × bytes_per_dtype(dtype)`.
283 expected: usize,
284 /// Actual vector length.
285 got: usize,
286 },
287 /// A caller-supplied value failed a precondition check (e.g. an empty
288 /// model string passed to [`crate::repo::Transaction::set_embedding`]).
289 /// The message describes the failing invariant.
290 #[error("invalid input: {0}")]
291 InvalidInput(String),
292}