use std::{error::Error, fmt, io, path::Path};
use crate::object::{ContentHash, StateId, TreeError, TreeStreamError};
#[derive(Debug, Clone, PartialEq)]
pub struct RecoveryDetails {
pub kind: &'static str,
pub error: String,
pub hint: String,
pub unsafe_condition: String,
pub would_change: String,
pub preserved: String,
pub recovery_commands: Option<Vec<String>>,
}
impl RecoveryDetails {
pub fn safety_refusal(
kind: &'static str,
error: impl Into<String>,
hint: impl Into<String>,
unsafe_condition: impl Into<String>,
would_change: impl Into<String>,
already_preserved: impl Into<String>,
) -> Self {
Self {
kind,
error: error.into(),
hint: hint.into(),
unsafe_condition: unsafe_condition.into(),
would_change: would_change.into(),
preserved: already_preserved.into(),
recovery_commands: None,
}
}
#[must_use]
pub fn with_recovery_commands(mut self, commands: Vec<String>) -> Self {
self.recovery_commands = Some(commands);
self
}
pub fn invalid_usage(
kind: &'static str,
error: impl Into<String>,
hint: impl Into<String>,
) -> Self {
Self::safety_refusal(
kind,
error,
hint,
"the command arguments do not describe a valid operation",
"running with ambiguous or invalid arguments could target the wrong repository state or metadata",
"no repository objects, refs, metadata, or worktree files were changed",
)
}
pub fn feature_unavailable(command: &str, feature: &str) -> Self {
Self::safety_refusal(
"feature_unavailable",
format!("{command} requires building heddle with --features {feature}"),
format!(
"Use a binary built with the `{feature}` feature, or rerun without the feature-specific flag."
),
format!("this heddle binary was built without the `{feature}` feature"),
format!("{command} cannot run because the requested analysis engine is unavailable"),
"repository state, refs, and worktree files were left unchanged",
)
}
pub fn serialization_error(detail: impl fmt::Display) -> Self {
Self::safety_refusal(
"state_corrupted",
"Repository state is corrupted or unreadable",
"Inspect repository integrity before attempting repair.",
format!("a stored repository object failed to decode: {detail}"),
"continuing would read or write through repository state Heddle cannot decode",
"the command stopped before mutating repository state; intact objects were left unchanged",
)
}
pub fn repository_integrity_error(error: impl Into<String>) -> Self {
Self::safety_refusal(
"repository_integrity_error",
error,
"Inspect repository integrity, then restore or repair the reported object/ref.",
"repository object or ref integrity did not pass validation",
"continuing could compound corruption or hide the missing object",
"the command stopped before applying the requested mutation",
)
}
pub fn repository_not_found(path: &Path) -> Self {
Self::safety_refusal(
"repository_not_found",
format!("repository not found at {}", path.display()),
"Initialize the requested repository before running repository commands.",
format!("no Heddle repository was found at '{}'", path.display()),
"the command cannot inspect or change repository state until initialization",
"no repository objects, refs, metadata, or worktree files were changed",
)
}
pub fn state_not_found(state_id: impl fmt::Display) -> Self {
Self::safety_refusal(
"state_not_found",
format!("State not found: {state_id}"),
"List recent states with `heddle log`, then choose an existing state id.",
"the requested state id does not exist in this repository",
"continuing with a guessed state could target the wrong history point",
"repository state, refs, metadata, and worktree files were left unchanged",
)
}
}
impl fmt::Display for RecoveryDetails {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}. Unsafe: {}. Would change: {}. Preserved: {}.",
self.error, self.unsafe_condition, self.would_change, self.preserved
)?;
Ok(())
}
}
impl Error for RecoveryDetails {}
#[derive(Debug, thiserror::Error)]
pub enum LockError {
#[error("failed to acquire lock: {0}")]
Acquire(#[source] io::Error),
#[error("lock file not accessible: {0}")]
Io(#[source] io::Error),
}
#[derive(Debug, thiserror::Error)]
pub enum HeddleError {
#[error("{0}")]
Recovery(Box<RecoveryDetails>),
#[error("object not found: {0}")]
NotFound(String),
#[error("No merge in progress")]
NoMergeInProgress,
#[error("no worktree changes to capture")]
NoChanges,
#[error("state not found: {0}")]
StateNotFound(StateId),
#[error("invalid object: {0}")]
InvalidObject(String),
#[error("repository not found at {0}")]
RepositoryNotFound(std::path::PathBuf),
#[error("repository already exists at {0}")]
RepositoryExists(std::path::PathBuf),
#[error("repository clone at {0} is incomplete and must be repaired from its origin")]
IncompleteClone(std::path::PathBuf),
#[error(
"repository config at {path} uses repository format {found} but this binary supports {supported}; upgrade Heddle before opening it"
)]
RepositoryFormatTooNew {
path: std::path::PathBuf,
found: u32,
supported: u32,
},
#[error(
"repository at {path} predates format v{required} (found v{found}); re-clone or re-import into a new directory. First copy uncommitted and untracked work from every checkout, and back up the entire .heddle directory (including local threads, discussions, context, coordination and operation history). Keep the old repository until recovery is verified; do not edit its format version"
)]
RepositoryFormatTooOld {
path: std::path::PathBuf,
found: u32,
required: u32,
},
#[error(
"{storage} uses format {found}, but this binary supports {supported}; upgrade Heddle before opening it"
)]
StorageFormatTooNew {
storage: String,
found: u32,
supported: u32,
},
#[error(
"{storage} predates required format {required} (found {found}); recreate the repository or re-adopt its Git history with this Heddle version"
)]
StorageFormatTooOld {
storage: String,
found: u32,
required: u32,
},
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("repository lock unavailable: {0}")]
Lock(#[from] LockError),
#[error("serialization error: {0}")]
Serialization(String),
#[error("configuration error: {0}")]
Config(String),
#[error("native Thread '{thread}' owner signing key is unavailable: {reason}")]
NativeSourceSignerUnavailable { thread: String, reason: String },
#[error("configuration parse error at {path}: {source}")]
ConfigParse {
path: std::path::PathBuf,
#[source]
source: toml::de::Error,
},
#[error(
"invalid {key}: '{value}' — valid values are {} (in {path})",
valid_values.join(" or ")
)]
ConfigInvalidValue {
path: std::path::PathBuf,
key: String,
value: String,
valid_values: Vec<String>,
},
#[error("conflict: {0}")]
Conflict(String),
#[error("compression error: {0}")]
Compression(String),
#[error("invalid ref name: {0}")]
InvalidRefName(String),
#[error("file too large: {0} bytes")]
InvalidFileSize(u64),
#[error(
"symlink target escapes repository: {} -> {}",
path.display(),
target.display()
)]
InvalidSymlinkTarget {
path: std::path::PathBuf,
target: std::path::PathBuf,
},
#[error("object corruption: expected {expected}, found {found}")]
Corruption {
expected: ContentHash,
found: ContentHash,
},
#[error(
"missing {object_type} object: {id} (run `heddle maintenance fsck --full` to inspect store integrity)"
)]
MissingObject { object_type: String, id: String },
#[error("invalid tree entry: {0}")]
InvalidTreeEntry(#[from] TreeError),
#[error("tree stream error: {0}")]
TreeStream(TreeStreamError),
#[error("redacted tree: {0}")]
RedactedTree(String),
}
impl From<TreeStreamError> for HeddleError {
fn from(error: TreeStreamError) -> Self {
match error {
TreeStreamError::Invalid(error) => Self::InvalidTreeEntry(error),
other => Self::TreeStream(other),
}
}
}
impl HeddleError {
pub fn recovery(details: RecoveryDetails) -> Self {
HeddleError::Recovery(Box::new(details))
}
}
impl From<rmp_serde::encode::Error> for HeddleError {
fn from(e: rmp_serde::encode::Error) -> Self {
HeddleError::Serialization(e.to_string())
}
}
impl From<rmp_serde::decode::Error> for HeddleError {
fn from(e: rmp_serde::decode::Error) -> Self {
HeddleError::Serialization(e.to_string())
}
}
impl From<crate::object::SemanticIndexError> for HeddleError {
fn from(e: crate::object::SemanticIndexError) -> Self {
HeddleError::InvalidObject(e.to_string())
}
}
impl From<toml::de::Error> for HeddleError {
fn from(e: toml::de::Error) -> Self {
HeddleError::Config(e.to_string())
}
}
impl From<toml::ser::Error> for HeddleError {
fn from(e: toml::ser::Error) -> Self {
HeddleError::Config(e.to_string())
}
}
impl From<serde_json::Error> for HeddleError {
fn from(e: serde_json::Error) -> Self {
HeddleError::Serialization(e.to_string())
}
}
impl From<heddle_format::compression::CompressionError> for HeddleError {
fn from(e: heddle_format::compression::CompressionError) -> Self {
HeddleError::Compression(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, HeddleError>;
#[cfg(test)]
mod tests {
use super::{HeddleError, RecoveryDetails};
#[test]
fn safety_refusal_formats_domain_details() {
let details = RecoveryDetails::safety_refusal(
"example",
"error",
"hint",
"unsafe",
"would change",
"preserved",
);
assert_eq!(
details.to_string(),
"error. Unsafe: unsafe. Would change: would change. Preserved: preserved."
);
}
#[test]
fn recovery_error_displays_structured_error_copy() {
let err = HeddleError::recovery(RecoveryDetails::serialization_error("bad marker"));
assert!(err.to_string().contains("Repository state is corrupted"));
assert!(!err.to_string().contains("heddle maintenance fsck --full"));
}
}