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    /// The on-disk index was created with a different schema version.
90    #[error("Index schema version mismatch (found v{found}, expected v{expected}). Run 'ctx index --force' to rebuild the index.")]
91    SchemaVersionMismatch { found: i64, expected: i64 },
92
93    /// Parse error during code analysis.
94    #[error("Parse error: {0}")]
95    #[allow(dead_code)] // Part of public API for future use
96    ParseError(String),
97
98    /// Token counting error.
99    #[error("Token counting error: {0}")]
100    #[allow(dead_code)] // Part of public API for future use
101    TokenCount(String),
102
103    /// File read error with path context.
104    #[error("Failed to read file '{path}': {message}")]
105    #[allow(dead_code)] // Part of public API for future use
106    FileRead { path: String, message: String },
107
108    // ========== Generic Errors ==========
109    /// Generic error for cases not covered above.
110    #[error("{0}")]
111    Other(String),
112}
113
114/// Result type alias using CtxError.
115pub type Result<T> = std::result::Result<T, CtxError>;
116
117impl CtxError {
118    /// Create an embedding error from a string message.
119    pub fn embedding(msg: impl Into<String>) -> Self {
120        CtxError::Embedding(msg.into())
121    }
122
123    /// Create a git error from a string message.
124    pub fn git(msg: impl Into<String>) -> Self {
125        CtxError::Git(msg.into())
126    }
127
128    /// Create a parse error from a string message.
129    #[allow(dead_code)] // Part of public API for future use
130    pub fn parse(msg: impl Into<String>) -> Self {
131        CtxError::ParseError(msg.into())
132    }
133
134    /// Create a token count error from a string message.
135    #[allow(dead_code)] // Part of public API for future use
136    pub fn token_count(msg: impl Into<String>) -> Self {
137        CtxError::TokenCount(msg.into())
138    }
139
140    /// Create a file read error with path context.
141    #[allow(dead_code)] // Part of public API for future use
142    pub fn file_read(path: impl Into<String>, msg: impl Into<String>) -> Self {
143        CtxError::FileRead {
144            path: path.into(),
145            message: msg.into(),
146        }
147    }
148
149    /// Create an "other" error from a string message.
150    #[allow(dead_code)] // Part of public API for future use
151    pub fn other(msg: impl Into<String>) -> Self {
152        CtxError::Other(msg.into())
153    }
154}
155
156// ========== Convenience Conversions ==========
157
158impl From<String> for CtxError {
159    fn from(s: String) -> Self {
160        CtxError::Other(s)
161    }
162}
163
164impl From<&str> for CtxError {
165    fn from(s: &str) -> Self {
166        CtxError::Other(s.to_string())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_error_display() {
176        let err = CtxError::Database(rusqlite::Error::InvalidQuery);
177        assert!(err.to_string().contains("Database error"));
178
179        let err = CtxError::DimensionMismatch {
180            expected: 1536,
181            actual: 384,
182        };
183        assert_eq!(
184            err.to_string(),
185            "Dimension mismatch: expected 1536, got 384"
186        );
187
188        let err = CtxError::RateLimited(60);
189        assert_eq!(err.to_string(), "Rate limited: retry after 60 seconds");
190    }
191
192    #[test]
193    fn test_error_constructors() {
194        let err = CtxError::embedding("test error");
195        assert!(matches!(err, CtxError::Embedding(_)));
196
197        let err = CtxError::git("not a repo");
198        assert!(matches!(err, CtxError::Git(_)));
199
200        let err = CtxError::file_read("/path/to/file", "permission denied");
201        assert!(matches!(err, CtxError::FileRead { .. }));
202    }
203
204    #[test]
205    fn test_from_string() {
206        let err: CtxError = "some error".into();
207        assert!(matches!(err, CtxError::Other(_)));
208
209        let err: CtxError = String::from("another error").into();
210        assert!(matches!(err, CtxError::Other(_)));
211    }
212}