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