sara_core/error.rs
1//! Error types for the sara-core library.
2//!
3//! This module defines a unified error type for all SARA operations.
4//! All errors are consolidated into the single [`SaraError`] enum with
5//! clear variants for each error category.
6//!
7//! # Error Categories
8//!
9//! - **File Operations**: Reading and writing files
10//! - **Parsing**: Markdown and YAML frontmatter parsing
11//! - **Validation**: Graph structure and item validation
12//! - **Configuration**: Loading and validating configuration
13//! - **Queries**: Item lookup and graph traversal
14//! - **Git Operations**: Repository access and version control
15//! - **Editing**: Item modification operations
16//!
17//! # Examples
18//!
19//! ```
20//! use sara_core::error::SaraError;
21//! use std::path::PathBuf;
22//!
23//! # fn example() -> Result<(), SaraError> {
24//! // Validation errors use explicit variants
25//! let err = SaraError::BrokenReference {
26//! from: sara_core::model::ItemId::new_unchecked("UC-001"),
27//! to: sara_core::model::ItemId::new_unchecked("SOL-999"),
28//! };
29//!
30//! // File operations with context
31//! let err = SaraError::InvalidFrontmatter {
32//! file: PathBuf::from("doc.md"),
33//! reason: "Missing required 'id' field".to_string(),
34//! };
35//! # Ok(())
36//! # }
37//! ```
38
39use std::path::PathBuf;
40use thiserror::Error;
41
42use crate::model::{ItemId, ItemType, RelationshipType};
43
44/// Main error type for sara-core operations.
45///
46/// Consolidates all error categories into a single type with clear variants.
47/// Uses `thiserror` for automatic `Display` and `Error` trait implementations.
48///
49/// # Errors
50///
51/// This enum categorizes all possible errors that can occur during SARA operations.
52/// Each variant includes contextual information to help diagnose the issue.
53///
54/// # Examples
55///
56/// ```
57/// use sara_core::error::SaraError;
58/// use std::path::PathBuf;
59///
60/// # fn example() -> Result<(), SaraError> {
61/// // File not found
62/// let err = SaraError::FileNotFound { path: PathBuf::from("missing.md") };
63///
64/// // Parse error with context
65/// let err = SaraError::InvalidFrontmatter {
66/// file: PathBuf::from("doc.md"),
67/// reason: "Missing 'id' field".to_string(),
68/// };
69/// # Ok(())
70/// # }
71/// ```
72#[derive(Debug, Error, serde::Serialize)]
73#[serde(tag = "error_type", content = "details")]
74pub enum SaraError {
75 // ==================== File Operations ====================
76 /// Failed to read a file from the filesystem.
77 #[error("Failed to read file '{path}': {source}")]
78 FileRead {
79 /// Path to the file that couldn't be read.
80 path: PathBuf,
81 /// Underlying I/O error.
82 #[serde(skip)]
83 #[source]
84 source: std::io::Error,
85 },
86
87 /// File was not found at the specified path.
88 #[error("File not found: {path}")]
89 FileNotFound {
90 /// Path to the missing file.
91 path: PathBuf,
92 },
93
94 /// Failed to write a file to the filesystem.
95 #[error("Failed to write file '{path}': {source}")]
96 FileWrite {
97 /// Path to the file that couldn't be written.
98 path: PathBuf,
99 /// Underlying I/O error.
100 #[serde(skip)]
101 #[source]
102 source: std::io::Error,
103 },
104
105 // ==================== Parsing ====================
106 /// Invalid frontmatter in a Markdown file.
107 #[error("Invalid frontmatter in {file}: {reason}")]
108 InvalidFrontmatter {
109 /// Path to the file with invalid frontmatter.
110 file: PathBuf,
111 /// Description of what's wrong with the frontmatter.
112 reason: String,
113 },
114
115 /// File is missing required frontmatter section.
116 #[error("Missing frontmatter in {file}")]
117 MissingFrontmatter {
118 /// Path to the file without frontmatter.
119 file: PathBuf,
120 },
121
122 /// Invalid YAML syntax in frontmatter.
123 #[error("Invalid YAML in {file}: {reason}")]
124 InvalidYaml {
125 /// Path to the file with invalid YAML.
126 file: PathBuf,
127 /// YAML parsing error details.
128 reason: String,
129 },
130
131 /// Invalid item type value in frontmatter.
132 #[error("Invalid item type '{value}' in {file}")]
133 InvalidItemType {
134 /// Path to the file with the invalid type.
135 file: PathBuf,
136 /// The invalid type value encountered.
137 value: String,
138 },
139
140 /// Missing required field in frontmatter.
141 #[error("Missing required field '{field}' in {file}")]
142 MissingField {
143 /// The field that was missing.
144 field: String,
145 /// Path to the file.
146 file: PathBuf,
147 },
148
149 // ==================== Validation ====================
150 /// Invalid item ID format.
151 #[error("Invalid item ID '{id}': {reason}")]
152 InvalidId {
153 /// The invalid ID.
154 id: String,
155 /// Why the ID is invalid.
156 reason: String,
157 },
158
159 /// Broken reference to non-existent item.
160 #[error("Broken reference: {from} references non-existent item {to}")]
161 BrokenReference {
162 /// The item with the broken reference.
163 from: ItemId,
164 /// The non-existent item being referenced.
165 to: ItemId,
166 },
167
168 /// Orphan item with no upstream parent.
169 #[error("Orphan item: {id} ({item_type}) has no upstream parent")]
170 OrphanItem {
171 /// The orphaned item ID.
172 id: ItemId,
173 /// The item type.
174 item_type: ItemType,
175 },
176
177 /// Duplicate identifier found in multiple files.
178 #[error("Duplicate identifier: {id} defined in multiple files")]
179 DuplicateIdentifier {
180 /// The duplicated ID.
181 id: ItemId,
182 },
183
184 /// Circular reference detected in the graph.
185 #[error("Circular reference detected: {cycle}")]
186 CircularReference {
187 /// Description of the cycle.
188 cycle: String,
189 },
190
191 /// Invalid relationship between item types.
192 #[error("Invalid relationship: {from_id} ({from_type}) cannot {rel_type} {to_id} ({to_type})")]
193 InvalidRelationship {
194 /// Source item ID.
195 from_id: ItemId,
196 /// Target item ID.
197 to_id: ItemId,
198 /// Source item type.
199 from_type: ItemType,
200 /// Target item type.
201 to_type: ItemType,
202 /// Relationship type attempted.
203 rel_type: RelationshipType,
204 },
205
206 /// Invalid metadata in item.
207 #[error("Invalid metadata in {file}: {reason}")]
208 InvalidMetadata {
209 /// File containing the invalid metadata.
210 file: String,
211 /// Description of the metadata issue.
212 reason: String,
213 },
214
215 /// Unrecognized field in frontmatter.
216 #[error("Unrecognized field '{field}' in {file}")]
217 UnrecognizedField {
218 /// The unrecognized field name.
219 field: String,
220 /// File containing the field.
221 file: String,
222 },
223
224 /// Redundant relationship declared on both sides.
225 #[error(
226 "Redundant relationship: {from_id} and {to_id} both declare the relationship (only one is needed)"
227 )]
228 RedundantRelationship {
229 /// First item ID.
230 from_id: ItemId,
231 /// Second item ID.
232 to_id: ItemId,
233 },
234
235 // ==================== Configuration ====================
236 /// Configuration file could not be read.
237 #[error("Failed to read config file {path}: {reason}")]
238 ConfigRead {
239 /// Path to the config file.
240 path: PathBuf,
241 /// Reason for the failure.
242 reason: String,
243 },
244
245 /// Configuration file has invalid content.
246 #[error("Invalid config file {path}: {reason}")]
247 InvalidConfig {
248 /// Path to the config file.
249 path: PathBuf,
250 /// Description of the configuration error.
251 reason: String,
252 },
253
254 /// Repository path does not exist or is not a directory.
255 #[error("Repository not found: {path}")]
256 RepositoryNotFound {
257 /// Path that was expected to be a repository.
258 path: PathBuf,
259 },
260
261 // ==================== Queries ====================
262 /// No parent items exist for the given item type.
263 #[error(
264 "Cannot create {item_type}: no {parent_type} items exist. Create a {parent_type} first."
265 )]
266 MissingParent {
267 /// The item type that requires a parent.
268 item_type: String,
269 /// The parent type that is missing.
270 parent_type: String,
271 },
272
273 /// Item was not found in the knowledge graph.
274 #[error("Item not found: {id}")]
275 ItemNotFound {
276 /// The item ID that wasn't found.
277 id: String,
278 /// Suggested similar item IDs (fuzzy matches).
279 suggestions: Vec<String>,
280 },
281
282 /// Query syntax or parameters are invalid.
283 #[error("Invalid query: {reason}")]
284 InvalidQuery {
285 /// Description of what's wrong with the query.
286 reason: String,
287 },
288
289 // ==================== Git Operations ====================
290 /// Failed to open a Git repository.
291 #[error("Failed to open repository {path}: {reason}")]
292 GitOpenRepository {
293 /// Path to the repository.
294 path: PathBuf,
295 /// Error from git2.
296 reason: String,
297 },
298
299 /// Git reference (branch, tag, commit) is invalid.
300 #[error("Invalid Git reference: {reference}")]
301 InvalidGitReference {
302 /// The invalid reference string.
303 reference: String,
304 },
305
306 /// Failed to read a file from a Git reference.
307 #[error("Failed to read file {path} at {reference}: {reason}")]
308 GitReadFile {
309 /// Path to the file in the repository.
310 path: PathBuf,
311 /// Git reference (commit, branch, tag).
312 reference: String,
313 /// Error details.
314 reason: String,
315 },
316
317 /// Generic Git operation error.
318 #[error("Git operation failed: {0}")]
319 Git(String),
320
321 // ==================== Edit Operations ====================
322 /// Interactive terminal required but not available.
323 #[error(
324 "Interactive mode requires a terminal. Use modification flags (--name, --description, etc.) to edit non-interactively."
325 )]
326 NonInteractiveTerminal,
327
328 /// User cancelled the operation.
329 #[error("User cancelled")]
330 Cancelled,
331
332 /// Traceability link points to non-existent item.
333 #[error("Invalid traceability link: {id} does not exist")]
334 InvalidLink {
335 /// The invalid item ID.
336 id: String,
337 },
338
339 /// Edit operation failed with custom error message.
340 #[error("Edit failed: {0}")]
341 EditFailed(String),
342
343 // ==================== Wrapped Errors ====================
344 /// Standard I/O error.
345 #[error("I/O error: {0}")]
346 Io(
347 #[serde(skip)]
348 #[from]
349 std::io::Error,
350 ),
351
352 /// Git2 library error.
353 #[error("Git error: {0}")]
354 Git2(
355 #[serde(skip)]
356 #[from]
357 git2::Error,
358 ),
359}
360
361impl SaraError {
362 /// Formats suggestions as a user-friendly message.
363 ///
364 /// Returns `None` if this is not an `ItemNotFound` error or if there are no suggestions.
365 pub fn format_suggestions(&self) -> Option<String> {
366 match self {
367 Self::ItemNotFound { suggestions, .. } if !suggestions.is_empty() => {
368 Some(format!("Did you mean: {}?", suggestions.join(", ")))
369 }
370 _ => None,
371 }
372 }
373}
374
375/// Result type for sara-core operations.
376///
377/// This is a convenience alias for `Result<T, SaraError>`.
378///
379/// # Examples
380///
381/// ```
382/// use sara_core::error::Result;
383///
384/// fn parse_file() -> Result<String> {
385/// Ok("parsed content".to_string())
386/// }
387/// ```
388pub type Result<T> = std::result::Result<T, SaraError>;
389
390// Rust guideline compliant 2026-02-06