Skip to main content

ctx/
error.rs

1//! Unified error type for ctx operations.
2//!
3//! This module provides a single error type that consolidates all error handling
4//! across the codebase, replacing module-specific error types like `EmbeddingError`,
5//! `SmartError`, `DiffError`, and `AuditError`.
6
7use thiserror::Error;
8
9/// Unified error type for all ctx operations.
10#[derive(Error, Debug)]
11pub enum CtxError {
12    // ========== Database Errors ==========
13    /// SQLite database error.
14    #[error("Database error: {0}")]
15    Database(#[from] rusqlite::Error),
16
17    /// DuckDB analytics error.
18    #[cfg(feature = "duckdb")]
19    #[error("Analytics error: {0}")]
20    Analytics(#[from] duckdb::Error),
21
22    // ========== IO Errors ==========
23    /// File system or IO error.
24    #[error("IO error: {0}")]
25    Io(#[from] std::io::Error),
26
27    /// Readline/shell input error.
28    #[error("Readline error: {0}")]
29    Readline(#[from] rustyline::error::ReadlineError),
30
31    // ========== Serialization Errors ==========
32    /// JSON serialization/deserialization error.
33    #[error("Serialization error: {0}")]
34    Serialization(#[from] serde_json::Error),
35
36    // ========== Network Errors ==========
37    /// HTTP request error.
38    #[error("Network error: {0}")]
39    Network(#[from] reqwest::Error),
40
41    // ========== Embedding Errors ==========
42    /// Generic embedding operation error.
43    #[error("Embedding error: {0}")]
44    Embedding(String),
45
46    /// API rate limit exceeded.
47    #[error("Rate limited: retry after {0} seconds")]
48    RateLimited(u64),
49
50    /// Invalid or missing API key.
51    #[error("Invalid API key")]
52    InvalidApiKey,
53
54    /// Embedding model not found.
55    #[error("Model not found: {0}")]
56    ModelNotFound(String),
57
58    /// Vector dimension mismatch.
59    #[error("Dimension mismatch: expected {expected}, got {actual}")]
60    DimensionMismatch { expected: usize, actual: usize },
61
62    // ========== Git/Diff Errors ==========
63    /// Git command or operation error.
64    #[error("Git error: {0}")]
65    Git(String),
66
67    /// Not inside a git repository.
68    #[error("Not a git repository")]
69    NotGitRepo,
70
71    /// Invalid git revision or reference.
72    #[error("Invalid revision: {0}")]
73    InvalidRevision(String),
74
75    /// No changes found in diff.
76    #[error("No changes found")]
77    NoChanges,
78
79    // ========== Business Logic Errors ==========
80    /// No relevant files found for smart context.
81    #[error("No relevant files found")]
82    NoMatches,
83
84    /// Index database not found.
85    #[error("Index not found: {0}")]
86    #[allow(dead_code)] // Part of public API for future use
87    IndexNotFound(String),
88
89    /// Parse error during code analysis.
90    #[error("Parse error: {0}")]
91    #[allow(dead_code)] // Part of public API for future use
92    ParseError(String),
93
94    /// Token counting error.
95    #[error("Token counting error: {0}")]
96    #[allow(dead_code)] // Part of public API for future use
97    TokenCount(String),
98
99    /// File read error with path context.
100    #[error("Failed to read file '{path}': {message}")]
101    #[allow(dead_code)] // Part of public API for future use
102    FileRead { path: String, message: String },
103
104    // ========== Generic Errors ==========
105    /// Generic error for cases not covered above.
106    #[error("{0}")]
107    Other(String),
108}
109
110/// Result type alias using CtxError.
111pub type Result<T> = std::result::Result<T, CtxError>;
112
113impl CtxError {
114    /// Create an embedding error from a string message.
115    pub fn embedding(msg: impl Into<String>) -> Self {
116        CtxError::Embedding(msg.into())
117    }
118
119    /// Create a git error from a string message.
120    pub fn git(msg: impl Into<String>) -> Self {
121        CtxError::Git(msg.into())
122    }
123
124    /// Create a parse error from a string message.
125    #[allow(dead_code)] // Part of public API for future use
126    pub fn parse(msg: impl Into<String>) -> Self {
127        CtxError::ParseError(msg.into())
128    }
129
130    /// Create a token count error from a string message.
131    #[allow(dead_code)] // Part of public API for future use
132    pub fn token_count(msg: impl Into<String>) -> Self {
133        CtxError::TokenCount(msg.into())
134    }
135
136    /// Create a file read error with path context.
137    #[allow(dead_code)] // Part of public API for future use
138    pub fn file_read(path: impl Into<String>, msg: impl Into<String>) -> Self {
139        CtxError::FileRead {
140            path: path.into(),
141            message: msg.into(),
142        }
143    }
144
145    /// Create an "other" error from a string message.
146    #[allow(dead_code)] // Part of public API for future use
147    pub fn other(msg: impl Into<String>) -> Self {
148        CtxError::Other(msg.into())
149    }
150}
151
152// ========== Convenience Conversions ==========
153
154impl From<String> for CtxError {
155    fn from(s: String) -> Self {
156        CtxError::Other(s)
157    }
158}
159
160impl From<&str> for CtxError {
161    fn from(s: &str) -> Self {
162        CtxError::Other(s.to_string())
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_error_display() {
172        let err = CtxError::Database(rusqlite::Error::InvalidQuery);
173        assert!(err.to_string().contains("Database error"));
174
175        let err = CtxError::DimensionMismatch {
176            expected: 1536,
177            actual: 384,
178        };
179        assert_eq!(
180            err.to_string(),
181            "Dimension mismatch: expected 1536, got 384"
182        );
183
184        let err = CtxError::RateLimited(60);
185        assert_eq!(err.to_string(), "Rate limited: retry after 60 seconds");
186    }
187
188    #[test]
189    fn test_error_constructors() {
190        let err = CtxError::embedding("test error");
191        assert!(matches!(err, CtxError::Embedding(_)));
192
193        let err = CtxError::git("not a repo");
194        assert!(matches!(err, CtxError::Git(_)));
195
196        let err = CtxError::file_read("/path/to/file", "permission denied");
197        assert!(matches!(err, CtxError::FileRead { .. }));
198    }
199
200    #[test]
201    fn test_from_string() {
202        let err: CtxError = "some error".into();
203        assert!(matches!(err, CtxError::Other(_)));
204
205        let err: CtxError = String::from("another error").into();
206        assert!(matches!(err, CtxError::Other(_)));
207    }
208}