use std::fmt;
use thiserror::Error;
use uuid::Uuid;
pub type RuntimeResult<T> = Result<T, RuntimeError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GuardedWriteFailure {
pub entry_index: Option<usize>,
pub missing_source: Option<Uuid>,
pub missing_target: Option<Uuid>,
}
impl fmt::Display for GuardedWriteFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut missing = Vec::new();
if let Some(source) = self.missing_source {
missing.push(format!("source {source}"));
}
if let Some(target) = self.missing_target {
missing.push(format!("target {target}"));
}
let missing = if missing.is_empty() {
"endpoint(s)".to_string()
} else {
missing.join(" and ")
};
match self.entry_index {
Some(index) => write!(
f,
"batch entry {index}: {missing} no longer exist at write time"
),
None => write!(f, "{missing} no longer exist at write time"),
}
}
}
impl std::error::Error for GuardedWriteFailure {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingPackDependency {
pub from: String,
pub requires: String,
}
impl fmt::Display for MissingPackDependency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"pack '{}' requires '{}', but '{}' is not in the loaded pack set",
self.from, self.requires, self.requires
)
}
}
impl std::error::Error for MissingPackDependency {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MissingPackDependencies {
pub missing: Vec<MissingPackDependency>,
}
impl fmt::Display for MissingPackDependencies {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parts: Vec<String> = self.missing.iter().map(ToString::to_string).collect();
write!(f, "{}", parts.join("; "))
}
}
impl std::error::Error for MissingPackDependencies {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CircularPackDependency {
pub cycle: Vec<String>,
}
impl fmt::Display for CircularPackDependency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"circular dependency detected among packs: {}",
self.cycle.join(" -> ")
)
}
}
impl std::error::Error for CircularPackDependency {}
#[derive(Debug, Error)]
pub enum RuntimeError {
#[error("storage: {0}")]
Storage(#[from] khive_storage::StorageError),
#[error("sqlite: {0}")]
Sqlite(#[from] khive_db::SqliteError),
#[error("query: {0}")]
Query(#[from] khive_query::QueryError),
#[error("not found: {0}")]
NotFound(String),
#[error("invalid input: {0}")]
InvalidInput(String),
#[error("unconfigured: {0} is not set")]
Unconfigured(String),
#[error("unknown embedding model: {0}")]
UnknownModel(String),
#[error("embedding: {0}")]
Embedding(#[from] lattice_embed::EmbedError),
#[error("ambiguous: {0}")]
Ambiguous(String),
#[error("fusion: {0}")]
Fusion(#[from] khive_fusion::FuseError),
#[error("internal: {0}")]
Internal(String),
#[error("guarded edge write refused: {0}")]
GuardedWriteFailed(GuardedWriteFailure),
#[error("missing pack dependency: {0}")]
MissingPackDependency(MissingPackDependency),
#[error("missing pack dependencies: {0}")]
MissingPackDependencies(MissingPackDependencies),
#[error("{0}")]
CircularPackDependency(CircularPackDependency),
#[error("pack '{name}' registered twice (indices {first_idx} and {second_idx})")]
PackRedeclared {
name: String,
first_idx: usize,
second_idx: usize,
},
#[error(
"verb collision: verb {verb:?} declared by both pack {first_pack:?} and pack \
{second_pack:?}; rename one handler or use Visibility::Subhandler for internal verbs"
)]
VerbCollision {
verb: String,
first_pack: String,
second_pack: String,
},
#[error("permission denied for verb {verb:?}: {reason}")]
PermissionDenied { verb: String, reason: String },
#[error("{0}")]
Khive(khive_types::KhiveError),
#[error("not found in this namespace")]
NamespaceMismatch { id: uuid::Uuid },
#[error("ambiguous prefix {prefix:?}: matches {}", format_uuid_list(matches))]
AmbiguousPrefix {
prefix: String,
matches: Vec<uuid::Uuid>,
},
#[error(
"cross-backend merge is not supported: \
into_id {into_id} is on backend '{into_backend}', \
from_id {from_id} is on backend '{from_backend}'. \
Both entities must be on the same backend to merge."
)]
CrossBackendMergeUnsupported {
into_id: uuid::Uuid,
from_id: uuid::Uuid,
into_backend: String,
from_backend: String,
},
#[error("unknown remote: {name:?}")]
UnknownRemote { name: String },
#[error("remote cache missing for remote={remote:?} namespace={namespace:?}")]
RemoteCacheMissing { remote: String, namespace: String },
#[error("ambiguous id {id:?}: matched {count} records")]
AmbiguousId { id: String, count: usize },
#[error("cross-namespace write denied: cannot write to remote namespace {namespace:?}")]
CrossNamespaceWrite { namespace: String },
#[error("remote fetch error for remote={remote:?}: {message}")]
RemoteFetchError { remote: String, message: String },
#[error(
"write budget exceeded: max_new_entries={max_new_entries}, \
attempted_new_entries={attempted_new_entries}"
)]
WriteBudgetExceeded {
max_new_entries: u64,
attempted_new_entries: u64,
},
#[error("write blocked: {0}")]
SecretDetected(crate::secret_gate::SecretMatch),
#[error("{operation} exceeded its {budget_ms}ms deadline (elapsed {elapsed_ms}ms)")]
DeadlineExceeded {
operation: String,
budget_ms: u64,
elapsed_ms: u64,
},
}
pub fn fts_text_leg_or_err<T>(
result: Result<Vec<T>, RuntimeError>,
context: &'static str,
query: &str,
) -> RuntimeResult<Vec<T>> {
match result {
Ok(hits) => Ok(hits),
Err(RuntimeError::Storage(se)) if se.is_fts5_syntax_error() => {
tracing::warn!(
error = %se,
query = %query,
context,
"FTS text leg failed on a parser syntax error; failing loud (#569)"
);
Err(RuntimeError::InvalidInput(format!(
"{context}: FTS query could not be parsed: {se}"
)))
}
Err(e) => Err(e),
}
}
fn format_uuid_list(uuids: &[uuid::Uuid]) -> String {
let shorts: Vec<String> = uuids
.iter()
.map(|u| u.to_string()[..8].to_string())
.collect();
shorts.join(", ")
}
impl From<khive_types::EntityTypeError> for RuntimeError {
fn from(e: khive_types::EntityTypeError) -> Self {
Self::InvalidInput(e.to_string())
}
}
impl From<khive_types::KhiveError> for RuntimeError {
fn from(e: khive_types::KhiveError) -> Self {
Self::Khive(e)
}
}