Skip to main content

khive_runtime/
error.rs

1//! Runtime error types.
2
3use std::fmt;
4
5use thiserror::Error;
6
7pub type RuntimeResult<T> = Result<T, RuntimeError>;
8
9/// A single missing pack dependency (ADR-037).
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct MissingPackDependency {
12    pub from: String,
13    pub requires: String,
14}
15
16impl fmt::Display for MissingPackDependency {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(
19            f,
20            "pack '{}' requires '{}', but '{}' is not in the loaded pack set",
21            self.from, self.requires, self.requires
22        )
23    }
24}
25
26impl std::error::Error for MissingPackDependency {}
27
28/// Multiple missing pack dependencies collected into one error (ADR-037).
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct MissingPackDependencies {
31    pub missing: Vec<MissingPackDependency>,
32}
33
34impl fmt::Display for MissingPackDependencies {
35    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36        let parts: Vec<String> = self.missing.iter().map(ToString::to_string).collect();
37        write!(f, "{}", parts.join("; "))
38    }
39}
40
41impl std::error::Error for MissingPackDependencies {}
42
43/// Circular pack dependency detected during topological sort (ADR-037).
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct CircularPackDependency {
46    pub cycle: Vec<String>,
47}
48
49impl fmt::Display for CircularPackDependency {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(
52            f,
53            "circular dependency detected among packs: {}",
54            self.cycle.join(" -> ")
55        )
56    }
57}
58
59impl std::error::Error for CircularPackDependency {}
60
61#[derive(Debug, Error)]
62pub enum RuntimeError {
63    #[error("storage: {0}")]
64    Storage(#[from] khive_storage::StorageError),
65
66    #[error("sqlite: {0}")]
67    Sqlite(#[from] khive_db::SqliteError),
68
69    #[error("query: {0}")]
70    Query(#[from] khive_query::QueryError),
71
72    #[error("not found: {0}")]
73    NotFound(String),
74
75    #[error("invalid input: {0}")]
76    InvalidInput(String),
77
78    #[error("unconfigured: {0} is not set")]
79    Unconfigured(String),
80
81    #[error("unknown embedding model: {0}")]
82    UnknownModel(String),
83
84    #[error("embedding: {0}")]
85    Embedding(#[from] lattice_embed::EmbedError),
86
87    #[error("ambiguous: {0}")]
88    Ambiguous(String),
89
90    #[error("internal: {0}")]
91    Internal(String),
92
93    #[error("missing pack dependency: {0}")]
94    MissingPackDependency(MissingPackDependency),
95
96    #[error("missing pack dependencies: {0}")]
97    MissingPackDependencies(MissingPackDependencies),
98
99    #[error("{0}")]
100    CircularPackDependency(CircularPackDependency),
101
102    #[error("pack '{name}' registered twice (indices {first_idx} and {second_idx})")]
103    PackRedeclared {
104        name: String,
105        first_idx: usize,
106        second_idx: usize,
107    },
108
109    /// Two packs declared the same `Visibility::Verb` handler name (ADR-017
110    /// §Boot-time collision checks). `Visibility::Subhandler` entries are
111    /// pack-prefixed and do not participate in cross-pack collision checks.
112    #[error(
113        "verb collision: verb {verb:?} declared by both pack {first_pack:?} and pack \
114         {second_pack:?}; rename one handler or use Visibility::Subhandler for internal verbs"
115    )]
116    VerbCollision {
117        verb: String,
118        first_pack: String,
119        second_pack: String,
120    },
121
122    /// Gate denied this verb invocation (ADR-035).
123    ///
124    /// Returned by `VerbRegistry::dispatch` when the configured `Gate` returns
125    /// `GateDecision::Deny`. The pack is never invoked. The `reason` field
126    /// carries the deny message produced by the gate implementation.
127    #[error("permission denied for verb {verb:?}: {reason}")]
128    PermissionDenied { verb: String, reason: String },
129
130    /// A structured [`khive_types::KhiveError`] converted into the runtime
131    /// layer. The full structured error is preserved so callers can inspect
132    /// `kind`, `code`, `details`, and `retry_hint` without information loss.
133    #[error("{0}")]
134    Khive(khive_types::KhiveError),
135
136    /// Record exists but belongs to a different namespace than the provided token.
137    ///
138    /// Externally reported as "not found in this namespace" to avoid leaking
139    /// cross-namespace existence information (ADR-007 timing-oracle mitigation).
140    #[error("not found in this namespace")]
141    NamespaceMismatch { id: uuid::Uuid },
142
143    /// A short-prefix lookup matched more than one record (ADR-016 §UUID arguments).
144    ///
145    /// `prefix` is the 8+ hex-char prefix supplied by the caller.
146    /// `matches` holds the full UUIDs of all matching records (at most 2 are
147    /// reported to bound the scan — callers must supply the full UUID to disambiguate).
148    #[error("ambiguous prefix {prefix:?}: matches {}", format_uuid_list(matches))]
149    AmbiguousPrefix {
150        prefix: String,
151        matches: Vec<uuid::Uuid>,
152    },
153
154    /// Cross-backend `merge_entity` is unsupported in v1 (ADR-009 §cross-backend-merge).
155    ///
156    /// Both entities must reside on the same backend. To merge entities on different
157    /// backends, manually export `from_id`, delete it, and re-import on `into_id`'s backend.
158    #[error(
159        "cross-backend merge is not supported: \
160         into_id {into_id} is on backend '{into_backend}', \
161         from_id {from_id} is on backend '{from_backend}'. \
162         Both entities must be on the same backend to merge."
163    )]
164    CrossBackendMergeUnsupported {
165        into_id: uuid::Uuid,
166        from_id: uuid::Uuid,
167        into_backend: String,
168        from_backend: String,
169    },
170
171    // ── ADR-037: Remote Resolution and Content-Hash Verification ─────────────
172    /// A `kg://` ref names a remote not declared in `schema.yaml`.
173    #[error("unknown remote: {name:?}")]
174    UnknownRemote { name: String },
175
176    /// A remote cache entry is absent and `--fetch` was not requested.
177    #[error("remote cache missing for remote={remote:?} namespace={namespace:?}")]
178    RemoteCacheMissing { remote: String, namespace: String },
179
180    /// A short ID matches multiple entities in the same namespace or remote cache.
181    #[error("ambiguous id {id:?}: matched {count} records")]
182    AmbiguousId { id: String, count: usize },
183
184    /// A write operation targeted a remote namespace, which is read-only.
185    #[error("cross-namespace write denied: cannot write to remote namespace {namespace:?}")]
186    CrossNamespaceWrite { namespace: String },
187
188    /// A remote fetch failed (network error, authentication failure, etc.).
189    #[error("remote fetch error for remote={remote:?}: {message}")]
190    RemoteFetchError { remote: String, message: String },
191}
192
193fn format_uuid_list(uuids: &[uuid::Uuid]) -> String {
194    let shorts: Vec<String> = uuids
195        .iter()
196        .map(|u| u.to_string()[..8].to_string())
197        .collect();
198    shorts.join(", ")
199}
200
201impl From<khive_types::KhiveError> for RuntimeError {
202    fn from(e: khive_types::KhiveError) -> Self {
203        Self::Khive(e)
204    }
205}