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}
141
142/// Errors from [`crate::sign`] - commit / operation signing + verification.
143#[derive(Debug, Error)]
144#[non_exhaustive]
145pub enum SignError {
146 /// Object carries no `signature` field; verification requires one.
147 #[error("no signature attached")]
148 NoSignature,
149 /// Signature field uses an algorithm this implementation doesn't support.
150 #[error("unsupported signature algorithm: {got}")]
151 WrongAlgorithm {
152 /// Algorithm tag found on the object.
153 got: String,
154 },
155 /// Public key is not the expected 32 bytes or fails Ed25519 decode.
156 #[error("malformed public key")]
157 MalformedKey,
158 /// Signature bytes are not the expected 64 bytes.
159 #[error("malformed signature")]
160 MalformedSignature,
161 /// Ed25519 verify rejected the signature.
162 #[error("signature verification failed")]
163 InvalidSignature,
164 /// Re-canonicalising the object for verification failed.
165 #[error("encoding: {0}")]
166 Encoding(String),
167 /// The signing key is present in the revocation list and the object's
168 /// timestamp is strictly after the revocation (SPEC §9.2).
169 #[error("key revoked at {revoked_at} µs (object time: {time} µs)")]
170 RevokedKey {
171 /// Microseconds-since-epoch moment the key was revoked.
172 revoked_at: u64,
173 /// The signed object's `time`.
174 time: u64,
175 },
176}
177
178/// Errors from [`crate::repo`] - repository lifecycle operations.
179#[derive(Debug, Error)]
180#[non_exhaustive]
181pub enum RepoError {
182 /// `open` was called on an op-heads store with no heads. Call
183 /// [`crate::repo::ReadonlyRepo::init`] first.
184 #[error("repository not initialized (op-heads store is empty)")]
185 Uninitialized,
186 /// A CAS or linearized operation observed state that no longer
187 /// matches the caller's expectations (SPEC §6.4 / §6.5). Retry
188 /// against a fresh `ReadonlyRepo`.
189 #[error("stale: observed state is no longer current")]
190 Stale,
191 /// Op-DAG is malformed - heads do not share any common ancestor.
192 /// Cannot happen in a well-formed repository (all heads descend
193 /// from the root op); indicates corruption or partial import.
194 #[error("op-DAG has no common ancestor across the current heads")]
195 NoCommonAncestor,
196 /// `Query::one` (or a similar precondition API) found zero matches.
197 #[error("query found zero matches")]
198 NotFound,
199 /// `Query::one` (or a similar precondition API) found multiple
200 /// matches where exactly one was required.
201 #[error("query found multiple matches where exactly one was required")]
202 AmbiguousMatch,
203 /// A secondary index pointed at a block that is missing, malformed,
204 /// or whose contents contradict the index (wrong label, wrong prop
205 /// value). Indicates corruption or a partial import; does not
206 /// trigger on a simple "no such key" miss.
207 #[error("index corruption: {context} (cid = {cid})")]
208 IndexCorrupt {
209 /// Short description of which index + which key was involved.
210 context: String,
211 /// The CID the index pointed at.
212 cid: crate::id::Cid,
213 },
214 /// A vector-search query's dimension did not match the index
215 /// dimension. Each vector index binds to one model + dim at build
216 /// time; agents must pass a query vector of the exact same shape.
217 #[error("vector dim mismatch: index dim is {index_dim}, query dim is {query_dim}")]
218 VectorDimMismatch {
219 /// Dimension the index was built at.
220 index_dim: u32,
221 /// Dimension of the query vector the caller passed.
222 query_dim: usize,
223 },
224 /// A [`crate::retrieve::Retriever`] was executed without any
225 /// filters or rankers configured. Retrieval needs at least one
226 /// label / prop / text / vector input to produce a useful result.
227 ///
228 /// audit-2026-04-25 P2-1 / P1-3: the error now spells out the
229 /// common remediation path (text query needs an embedder) so CLI
230 /// and MCP callers do not have to guess.
231 #[error(
232 "retrieve: no filters or rankers configured. \
233 A text query requires an embedder: \
234 `mnem config set embed.provider ollama && \
235 mnem config set embed.model nomic-embed-text` \
236 (or pass `--where K=V` / `--label L` for a pure filter query)."
237 )]
238 RetrievalEmpty,
239}
240
241/// Errors from [`crate::objects`] - Node/Edge/Tree/... validation and decode.
242#[derive(Debug, Error)]
243#[non_exhaustive]
244pub enum ObjectError {
245 /// The `_kind` discriminator on the wire doesn't match the expected
246 /// Rust type. For example, decoding an Edge as a Node.
247 #[error("wrong kind: expected '{expected}', got '{got}'")]
248 WrongKind {
249 /// The `_kind` value the Rust type expects.
250 expected: &'static str,
251 /// The `_kind` value found in the encoded bytes.
252 got: String,
253 },
254 /// An [`crate::objects::Embedding`]'s `vector` length does not match
255 /// `dim × bytes_per_dtype(dtype)` (SPEC §4.1).
256 #[error("embedding size mismatch: expected {expected} bytes, got {got}")]
257 EmbeddingSizeMismatch {
258 /// Required vector length: `dim × bytes_per_dtype(dtype)`.
259 expected: usize,
260 /// Actual vector length.
261 got: usize,
262 },
263}