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("embedding: {0}")]
82    Embedding(#[from] lattice_embed::EmbedError),
83
84    #[error("ambiguous: {0}")]
85    Ambiguous(String),
86
87    #[error("internal: {0}")]
88    Internal(String),
89
90    #[error("missing pack dependency: {0}")]
91    MissingPackDependency(MissingPackDependency),
92
93    #[error("missing pack dependencies: {0}")]
94    MissingPackDependencies(MissingPackDependencies),
95
96    #[error("{0}")]
97    CircularPackDependency(CircularPackDependency),
98
99    #[error("pack '{name}' registered twice (indices {first_idx} and {second_idx})")]
100    PackRedeclared {
101        name: String,
102        first_idx: usize,
103        second_idx: usize,
104    },
105
106    /// Two packs declared the same `Visibility::Verb` handler name (ADR-017
107    /// §Boot-time collision checks). `Visibility::Subhandler` entries are
108    /// pack-prefixed and do not participate in cross-pack collision checks.
109    #[error(
110        "verb collision: verb {verb:?} declared by both pack {first_pack:?} and pack \
111         {second_pack:?}; rename one handler or use Visibility::Subhandler for internal verbs"
112    )]
113    VerbCollision {
114        verb: String,
115        first_pack: String,
116        second_pack: String,
117    },
118
119    /// Gate denied this verb invocation (ADR-035).
120    ///
121    /// Returned by `VerbRegistry::dispatch` when the configured `Gate` returns
122    /// `GateDecision::Deny`. The pack is never invoked. The `reason` field
123    /// carries the deny message produced by the gate implementation.
124    #[error("permission denied for verb {verb:?}: {reason}")]
125    PermissionDenied { verb: String, reason: String },
126
127    /// A structured [`khive_types::KhiveError`] converted into the runtime
128    /// layer. The full structured error is preserved so callers can inspect
129    /// `kind`, `code`, `details`, and `retry_hint` without information loss.
130    #[error("{0}")]
131    Khive(khive_types::KhiveError),
132
133    /// Record exists but belongs to a different namespace than the provided token.
134    ///
135    /// Externally reported as "not found in this namespace" to avoid leaking
136    /// cross-namespace existence information (ADR-007 timing-oracle mitigation).
137    #[error("not found in this namespace")]
138    NamespaceMismatch { id: uuid::Uuid },
139
140    /// A short-prefix lookup matched more than one record (ADR-016 §UUID arguments).
141    ///
142    /// `prefix` is the 8+ hex-char prefix supplied by the caller.
143    /// `matches` holds the full UUIDs of all matching records (at most 2 are
144    /// reported to bound the scan — callers must supply the full UUID to disambiguate).
145    #[error("ambiguous prefix {prefix:?}: matches {}", format_uuid_list(matches))]
146    AmbiguousPrefix {
147        prefix: String,
148        matches: Vec<uuid::Uuid>,
149    },
150
151    /// Cross-backend `merge_entity` is unsupported in v1 (ADR-009 §cross-backend-merge).
152    ///
153    /// Both entities must reside on the same backend. To merge entities on different
154    /// backends, manually export `from_id`, delete it, and re-import on `into_id`'s backend.
155    #[error(
156        "cross-backend merge is not supported: \
157         into_id {into_id} is on backend '{into_backend}', \
158         from_id {from_id} is on backend '{from_backend}'. \
159         Both entities must be on the same backend to merge."
160    )]
161    CrossBackendMergeUnsupported {
162        into_id: uuid::Uuid,
163        from_id: uuid::Uuid,
164        into_backend: String,
165        from_backend: String,
166    },
167
168    // ── ADR-037: Remote Resolution and Content-Hash Verification ─────────────
169    /// A `kg://` ref names a remote not declared in `schema.yaml`.
170    #[error("unknown remote: {name:?}")]
171    UnknownRemote { name: String },
172
173    /// A remote cache entry is absent and `--fetch` was not requested.
174    #[error("remote cache missing for remote={remote:?} namespace={namespace:?}")]
175    RemoteCacheMissing { remote: String, namespace: String },
176
177    /// A short ID matches multiple entities in the same namespace or remote cache.
178    #[error("ambiguous id {id:?}: matched {count} records")]
179    AmbiguousId { id: String, count: usize },
180
181    /// A write operation targeted a remote namespace, which is read-only.
182    #[error("cross-namespace write denied: cannot write to remote namespace {namespace:?}")]
183    CrossNamespaceWrite { namespace: String },
184
185    /// A remote fetch failed (network error, authentication failure, etc.).
186    #[error("remote fetch error for remote={remote:?}: {message}")]
187    RemoteFetchError { remote: String, message: String },
188}
189
190fn format_uuid_list(uuids: &[uuid::Uuid]) -> String {
191    let shorts: Vec<String> = uuids
192        .iter()
193        .map(|u| u.to_string()[..8].to_string())
194        .collect();
195    shorts.join(", ")
196}
197
198impl From<khive_types::KhiveError> for RuntimeError {
199    fn from(e: khive_types::KhiveError) -> Self {
200        Self::Khive(e)
201    }
202}