Skip to main content

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}