1use std::fmt;
4
5use thiserror::Error;
6use uuid::Uuid;
7
8pub type RuntimeResult<T> = Result<T, RuntimeError>;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct GuardedWriteFailure {
17 pub entry_index: Option<usize>,
20 pub missing_source: Option<Uuid>,
23 pub missing_target: Option<Uuid>,
26}
27
28impl fmt::Display for GuardedWriteFailure {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 let mut missing = Vec::new();
31 if let Some(source) = self.missing_source {
32 missing.push(format!("source {source}"));
33 }
34 if let Some(target) = self.missing_target {
35 missing.push(format!("target {target}"));
36 }
37 let missing = if missing.is_empty() {
38 "endpoint(s)".to_string()
39 } else {
40 missing.join(" and ")
41 };
42 match self.entry_index {
43 Some(index) => write!(
44 f,
45 "batch entry {index}: {missing} no longer exist at write time"
46 ),
47 None => write!(f, "{missing} no longer exist at write time"),
48 }
49 }
50}
51
52impl std::error::Error for GuardedWriteFailure {}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct MissingPackDependency {
57 pub from: String,
58 pub requires: String,
59}
60
61impl fmt::Display for MissingPackDependency {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(
64 f,
65 "pack '{}' requires '{}', but '{}' is not in the loaded pack set",
66 self.from, self.requires, self.requires
67 )
68 }
69}
70
71impl std::error::Error for MissingPackDependency {}
72
73#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct MissingPackDependencies {
76 pub missing: Vec<MissingPackDependency>,
77}
78
79impl fmt::Display for MissingPackDependencies {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 let parts: Vec<String> = self.missing.iter().map(ToString::to_string).collect();
82 write!(f, "{}", parts.join("; "))
83 }
84}
85
86impl std::error::Error for MissingPackDependencies {}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct CircularPackDependency {
91 pub cycle: Vec<String>,
92}
93
94impl fmt::Display for CircularPackDependency {
95 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 write!(
97 f,
98 "circular dependency detected among packs: {}",
99 self.cycle.join(" -> ")
100 )
101 }
102}
103
104impl std::error::Error for CircularPackDependency {}
105
106#[derive(Debug, Error)]
112pub enum RuntimeError {
113 #[error("storage: {0}")]
114 Storage(#[from] khive_storage::StorageError),
115
116 #[error("sqlite: {0}")]
117 Sqlite(#[from] khive_db::SqliteError),
118
119 #[error("query: {0}")]
120 Query(#[from] khive_query::QueryError),
121
122 #[error("not found: {0}")]
123 NotFound(String),
124
125 #[error("invalid input: {0}")]
126 InvalidInput(String),
127
128 #[error("unconfigured: {0} is not set")]
129 Unconfigured(String),
130
131 #[error("unknown embedding model: {0}")]
132 UnknownModel(String),
133
134 #[error("embedding: {0}")]
135 Embedding(#[from] lattice_embed::EmbedError),
136
137 #[error("ambiguous: {0}")]
138 Ambiguous(String),
139
140 #[error("fusion: {0}")]
141 Fusion(#[from] khive_fusion::FuseError),
142
143 #[error("internal: {0}")]
144 Internal(String),
145
146 #[error("guarded edge write refused: {0}")]
147 GuardedWriteFailed(GuardedWriteFailure),
148
149 #[error("missing pack dependency: {0}")]
150 MissingPackDependency(MissingPackDependency),
151
152 #[error("missing pack dependencies: {0}")]
153 MissingPackDependencies(MissingPackDependencies),
154
155 #[error("{0}")]
156 CircularPackDependency(CircularPackDependency),
157
158 #[error("pack '{name}' registered twice (indices {first_idx} and {second_idx})")]
159 PackRedeclared {
160 name: String,
161 first_idx: usize,
162 second_idx: usize,
163 },
164
165 #[error(
169 "verb collision: verb {verb:?} declared by both pack {first_pack:?} and pack \
170 {second_pack:?}; rename one handler or use Visibility::Subhandler for internal verbs"
171 )]
172 VerbCollision {
173 verb: String,
174 first_pack: String,
175 second_pack: String,
176 },
177
178 #[error("permission denied for verb {verb:?}: {reason}")]
184 PermissionDenied { verb: String, reason: String },
185
186 #[error("{0}")]
190 Khive(khive_types::KhiveError),
191
192 #[error("not found in this namespace")]
197 NamespaceMismatch { id: uuid::Uuid },
198
199 #[error("ambiguous prefix {prefix:?}: matches {}", format_uuid_list(matches))]
205 AmbiguousPrefix {
206 prefix: String,
207 matches: Vec<uuid::Uuid>,
208 },
209
210 #[error(
215 "cross-backend merge is not supported: \
216 into_id {into_id} is on backend '{into_backend}', \
217 from_id {from_id} is on backend '{from_backend}'. \
218 Both entities must be on the same backend to merge."
219 )]
220 CrossBackendMergeUnsupported {
221 into_id: uuid::Uuid,
222 from_id: uuid::Uuid,
223 into_backend: String,
224 from_backend: String,
225 },
226
227 #[error("unknown remote: {name:?}")]
230 UnknownRemote { name: String },
231
232 #[error("remote cache missing for remote={remote:?} namespace={namespace:?}")]
234 RemoteCacheMissing { remote: String, namespace: String },
235
236 #[error("ambiguous id {id:?}: matched {count} records")]
238 AmbiguousId { id: String, count: usize },
239
240 #[error("cross-namespace write denied: cannot write to remote namespace {namespace:?}")]
242 CrossNamespaceWrite { namespace: String },
243
244 #[error("remote fetch error for remote={remote:?}: {message}")]
246 RemoteFetchError { remote: String, message: String },
247
248 #[error(
254 "write budget exceeded: max_new_entries={max_new_entries}, \
255 attempted_new_entries={attempted_new_entries}"
256 )]
257 WriteBudgetExceeded {
258 max_new_entries: u64,
259 attempted_new_entries: u64,
260 },
261
262 #[error("write blocked: {0}")]
268 SecretDetected(crate::secret_gate::SecretMatch),
269}
270
271pub fn fts_text_leg_or_err<T>(
278 result: Result<Vec<T>, RuntimeError>,
279 context: &'static str,
280 query: &str,
281) -> RuntimeResult<Vec<T>> {
282 match result {
283 Ok(hits) => Ok(hits),
284 Err(RuntimeError::Storage(se)) if se.is_fts5_syntax_error() => {
285 tracing::warn!(
286 error = %se,
287 query = %query,
288 context,
289 "FTS text leg failed on a parser syntax error; failing loud (#569)"
290 );
291 Err(RuntimeError::InvalidInput(format!(
292 "{context}: FTS query could not be parsed: {se}"
293 )))
294 }
295 Err(e) => Err(e),
296 }
297}
298
299fn format_uuid_list(uuids: &[uuid::Uuid]) -> String {
300 let shorts: Vec<String> = uuids
301 .iter()
302 .map(|u| u.to_string()[..8].to_string())
303 .collect();
304 shorts.join(", ")
305}
306
307impl From<khive_types::EntityTypeError> for RuntimeError {
311 fn from(e: khive_types::EntityTypeError) -> Self {
312 Self::InvalidInput(e.to_string())
313 }
314}
315
316impl From<khive_types::KhiveError> for RuntimeError {
317 fn from(e: khive_types::KhiveError) -> Self {
318 Self::Khive(e)
319 }
320}