1use std::{error::Error, fmt, io, path::Path};
5
6use crate::object::{ContentHash, StateId, TreeError, TreeStreamError};
7
8#[derive(Debug, Clone, PartialEq)]
10pub struct RecoveryDetails {
11 pub kind: &'static str,
12 pub error: String,
13 pub hint: String,
14 pub unsafe_condition: String,
15 pub would_change: String,
16 pub preserved: String,
17 pub recovery_commands: Option<Vec<String>>,
22}
23
24impl RecoveryDetails {
25 pub fn safety_refusal(
26 kind: &'static str,
27 error: impl Into<String>,
28 hint: impl Into<String>,
29 unsafe_condition: impl Into<String>,
30 would_change: impl Into<String>,
31 already_preserved: impl Into<String>,
32 ) -> Self {
33 Self {
34 kind,
35 error: error.into(),
36 hint: hint.into(),
37 unsafe_condition: unsafe_condition.into(),
38 would_change: would_change.into(),
39 preserved: already_preserved.into(),
40 recovery_commands: None,
41 }
42 }
43
44 #[must_use]
48 pub fn with_recovery_commands(mut self, commands: Vec<String>) -> Self {
49 self.recovery_commands = Some(commands);
50 self
51 }
52
53 pub fn invalid_usage(
54 kind: &'static str,
55 error: impl Into<String>,
56 hint: impl Into<String>,
57 ) -> Self {
58 Self::safety_refusal(
59 kind,
60 error,
61 hint,
62 "the command arguments do not describe a valid operation",
63 "running with ambiguous or invalid arguments could target the wrong repository state or metadata",
64 "no repository objects, refs, metadata, or worktree files were changed",
65 )
66 }
67
68 pub fn feature_unavailable(command: &str, feature: &str) -> Self {
69 Self::safety_refusal(
70 "feature_unavailable",
71 format!("{command} requires building heddle with --features {feature}"),
72 format!(
73 "Use a binary built with the `{feature}` feature, or rerun without the feature-specific flag."
74 ),
75 format!("this heddle binary was built without the `{feature}` feature"),
76 format!("{command} cannot run because the requested analysis engine is unavailable"),
77 "repository state, refs, and worktree files were left unchanged",
78 )
79 }
80
81 pub fn serialization_error(detail: impl fmt::Display) -> Self {
82 Self::safety_refusal(
83 "state_corrupted",
84 "Repository state is corrupted or unreadable",
85 "Inspect repository integrity before attempting repair.",
86 format!("a stored repository object failed to decode: {detail}"),
87 "continuing would read or write through repository state Heddle cannot decode",
88 "the command stopped before mutating repository state; intact objects were left unchanged",
89 )
90 }
91
92 pub fn repository_integrity_error(error: impl Into<String>) -> Self {
93 Self::safety_refusal(
94 "repository_integrity_error",
95 error,
96 "Inspect repository integrity, then restore or repair the reported object/ref.",
97 "repository object or ref integrity did not pass validation",
98 "continuing could compound corruption or hide the missing object",
99 "the command stopped before applying the requested mutation",
100 )
101 }
102
103 pub fn repository_not_found(path: &Path) -> Self {
104 Self::safety_refusal(
105 "repository_not_found",
106 format!("repository not found at {}", path.display()),
107 "Initialize the requested repository before running repository commands.",
108 format!("no Heddle repository was found at '{}'", path.display()),
109 "the command cannot inspect or change repository state until initialization",
110 "no repository objects, refs, metadata, or worktree files were changed",
111 )
112 }
113
114 pub fn state_not_found(state_id: impl fmt::Display) -> Self {
115 Self::safety_refusal(
116 "state_not_found",
117 format!("State not found: {state_id}"),
118 "List recent states with `heddle log`, then choose an existing state id.",
119 "the requested state id does not exist in this repository",
120 "continuing with a guessed state could target the wrong history point",
121 "repository state, refs, metadata, and worktree files were left unchanged",
122 )
123 }
124}
125
126impl fmt::Display for RecoveryDetails {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 write!(
129 f,
130 "{}. Unsafe: {}. Would change: {}. Preserved: {}.",
131 self.error, self.unsafe_condition, self.would_change, self.preserved
132 )?;
133 Ok(())
134 }
135}
136
137impl Error for RecoveryDetails {}
138
139#[derive(Debug, thiserror::Error)]
145pub enum LockError {
146 #[error("failed to acquire lock: {0}")]
147 Acquire(#[source] io::Error),
148 #[error("lock file not accessible: {0}")]
149 Io(#[source] io::Error),
150}
151
152#[derive(Debug, thiserror::Error)]
154pub enum HeddleError {
155 #[error("{0}")]
156 Recovery(Box<RecoveryDetails>),
157 #[error("object not found: {0}")]
158 NotFound(String),
159 #[error("No merge in progress")]
160 NoMergeInProgress,
161 #[error("no worktree changes to capture")]
162 NoChanges,
163 #[error("state not found: {0}")]
164 StateNotFound(StateId),
165 #[error("invalid object: {0}")]
166 InvalidObject(String),
167 #[error("repository not found at {0}")]
168 RepositoryNotFound(std::path::PathBuf),
169 #[error("repository already exists at {0}")]
170 RepositoryExists(std::path::PathBuf),
171 #[error("repository clone at {0} is incomplete and must be repaired from its origin")]
172 IncompleteClone(std::path::PathBuf),
173 #[error(
174 "repository config at {path} uses repository format {found} but this binary supports {supported}; upgrade Heddle before opening it"
175 )]
176 RepositoryFormatTooNew {
177 path: std::path::PathBuf,
178 found: u32,
179 supported: u32,
180 },
181 #[error(
182 "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"
183 )]
184 RepositoryFormatTooOld {
185 path: std::path::PathBuf,
186 found: u32,
187 required: u32,
188 },
189 #[error(
190 "{storage} uses format {found}, but this binary supports {supported}; upgrade Heddle before opening it"
191 )]
192 StorageFormatTooNew {
193 storage: String,
194 found: u32,
195 supported: u32,
196 },
197 #[error(
198 "{storage} predates required format {required} (found {found}); recreate the repository or re-adopt its Git history with this Heddle version"
199 )]
200 StorageFormatTooOld {
201 storage: String,
202 found: u32,
203 required: u32,
204 },
205 #[error("io error: {0}")]
206 Io(#[from] std::io::Error),
207 #[error("repository lock unavailable: {0}")]
208 Lock(#[from] LockError),
209 #[error("serialization error: {0}")]
210 Serialization(String),
211 #[error("configuration error: {0}")]
212 Config(String),
213 #[error("native Thread '{thread}' owner signing key is unavailable: {reason}")]
217 NativeSourceSignerUnavailable { thread: String, reason: String },
218 #[error("configuration parse error at {path}: {source}")]
219 ConfigParse {
220 path: std::path::PathBuf,
221 #[source]
226 source: toml::de::Error,
227 },
228 #[error(
229 "invalid {key}: '{value}' — valid values are {} (in {path})",
230 valid_values.join(" or ")
231 )]
232 ConfigInvalidValue {
233 path: std::path::PathBuf,
234 key: String,
235 value: String,
236 valid_values: Vec<String>,
237 },
238 #[error("conflict: {0}")]
239 Conflict(String),
240 #[error("compression error: {0}")]
241 Compression(String),
242 #[error("invalid ref name: {0}")]
243 InvalidRefName(String),
244 #[error("file too large: {0} bytes")]
245 InvalidFileSize(u64),
246 #[error(
247 "symlink target escapes repository: {} -> {}",
248 path.display(),
249 target.display()
250 )]
251 InvalidSymlinkTarget {
252 path: std::path::PathBuf,
253 target: std::path::PathBuf,
254 },
255 #[error("object corruption: expected {expected}, found {found}")]
256 Corruption {
257 expected: ContentHash,
258 found: ContentHash,
259 },
260 #[error(
261 "missing {object_type} object: {id} (run `heddle maintenance fsck --full` to inspect store integrity)"
262 )]
263 MissingObject { object_type: String, id: String },
264 #[error("invalid tree entry: {0}")]
265 InvalidTreeEntry(#[from] TreeError),
266 #[error("tree stream error: {0}")]
267 TreeStream(TreeStreamError),
268 #[error("redacted tree: {0}")]
276 RedactedTree(String),
277}
278
279impl From<TreeStreamError> for HeddleError {
280 fn from(error: TreeStreamError) -> Self {
281 match error {
282 TreeStreamError::Invalid(error) => Self::InvalidTreeEntry(error),
283 other => Self::TreeStream(other),
284 }
285 }
286}
287
288impl HeddleError {
289 pub fn recovery(details: RecoveryDetails) -> Self {
290 HeddleError::Recovery(Box::new(details))
291 }
292}
293
294impl From<rmp_serde::encode::Error> for HeddleError {
295 fn from(e: rmp_serde::encode::Error) -> Self {
296 HeddleError::Serialization(e.to_string())
297 }
298}
299
300impl From<rmp_serde::decode::Error> for HeddleError {
301 fn from(e: rmp_serde::decode::Error) -> Self {
302 HeddleError::Serialization(e.to_string())
303 }
304}
305
306impl From<crate::object::SemanticIndexError> for HeddleError {
307 fn from(e: crate::object::SemanticIndexError) -> Self {
308 HeddleError::InvalidObject(e.to_string())
309 }
310}
311
312impl From<toml::de::Error> for HeddleError {
313 fn from(e: toml::de::Error) -> Self {
314 HeddleError::Config(e.to_string())
315 }
316}
317
318impl From<toml::ser::Error> for HeddleError {
319 fn from(e: toml::ser::Error) -> Self {
320 HeddleError::Config(e.to_string())
321 }
322}
323
324impl From<serde_json::Error> for HeddleError {
325 fn from(e: serde_json::Error) -> Self {
326 HeddleError::Serialization(e.to_string())
327 }
328}
329
330impl From<heddle_format::compression::CompressionError> for HeddleError {
331 fn from(e: heddle_format::compression::CompressionError) -> Self {
332 HeddleError::Compression(e.to_string())
333 }
334}
335
336pub type Result<T> = std::result::Result<T, HeddleError>;
338
339#[cfg(test)]
340mod tests {
341 use super::{HeddleError, RecoveryDetails};
342
343 #[test]
344 fn safety_refusal_formats_domain_details() {
345 let details = RecoveryDetails::safety_refusal(
346 "example",
347 "error",
348 "hint",
349 "unsafe",
350 "would change",
351 "preserved",
352 );
353
354 assert_eq!(
355 details.to_string(),
356 "error. Unsafe: unsafe. Would change: would change. Preserved: preserved."
357 );
358 }
359
360 #[test]
361 fn recovery_error_displays_structured_error_copy() {
362 let err = HeddleError::recovery(RecoveryDetails::serialization_error("bad marker"));
363
364 assert!(err.to_string().contains("Repository state is corrupted"));
365 assert!(!err.to_string().contains("heddle maintenance fsck --full"));
366 }
367}