use std::fs;
use std::path::{Path, PathBuf};
pub type Result<T> = std::result::Result<T, LeIndexError>;
#[derive(Debug)]
pub enum LeIndexError {
Parse {
message: String,
file_path: Option<PathBuf>,
suggestion: Option<String>,
},
Index {
message: String,
recoverable: bool,
},
Storage {
message: String,
recoverable: bool,
},
Search {
message: String,
},
Config {
message: String,
suggestion: Option<String>,
},
Io {
context: String,
path: Option<PathBuf>,
source: std::io::Error,
},
Memory {
message: String,
suggestion: Option<String>,
},
Validation {
message: String,
suggestion: Option<String>,
},
}
impl LeIndexError {
pub fn parse_error(message: impl Into<String>, file_path: impl Into<PathBuf>) -> Self {
LeIndexError::Parse {
message: message.into(),
file_path: Some(file_path.into()),
suggestion: None,
}
}
pub fn index_error(message: impl Into<String>, recoverable: bool) -> Self {
LeIndexError::Index {
message: message.into(),
recoverable,
}
}
pub fn storage_error(message: impl Into<String>, recoverable: bool) -> Self {
LeIndexError::Storage {
message: message.into(),
recoverable,
}
}
pub fn search_error(message: impl Into<String>) -> Self {
LeIndexError::Search {
message: message.into(),
}
}
pub fn config_error(message: impl Into<String>, suggestion: Option<String>) -> Self {
LeIndexError::Config {
message: message.into(),
suggestion,
}
}
pub fn memory_error(message: impl Into<String>, suggestion: Option<String>) -> Self {
LeIndexError::Memory {
message: message.into(),
suggestion,
}
}
pub fn validation_error(message: impl Into<String>, suggestion: Option<String>) -> Self {
LeIndexError::Validation {
message: message.into(),
suggestion,
}
}
pub fn is_recoverable(&self) -> bool {
match self {
LeIndexError::Index { recoverable, .. } => *recoverable,
LeIndexError::Storage { recoverable, .. } => *recoverable,
LeIndexError::Parse { .. } => true, _ => false,
}
}
pub fn suggestion(&self) -> Option<String> {
match self {
LeIndexError::Parse { suggestion, .. } => suggestion.clone(),
LeIndexError::Config { suggestion, .. } => suggestion.clone(),
LeIndexError::Memory { suggestion, .. } => suggestion.clone(),
LeIndexError::Validation { suggestion, .. } => suggestion.clone(),
LeIndexError::Index {
recoverable: true, ..
} => {
Some("Try re-indexing with a smaller token budget or fewer languages.".to_string())
}
LeIndexError::Storage {
recoverable: true, ..
} => Some("Try deleting .leindex directory and re-indexing.".to_string()),
_ => None,
}
}
}
impl std::fmt::Display for LeIndexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LeIndexError::Parse { message, .. } => write!(f, "Parse error: {}", message),
LeIndexError::Index { message, .. } => write!(f, "Indexing error: {}", message),
LeIndexError::Storage { message, .. } => write!(f, "Storage error: {}", message),
LeIndexError::Search { message } => write!(f, "Search error: {}", message),
LeIndexError::Config { message, .. } => write!(f, "Configuration error: {}", message),
LeIndexError::Io { context, .. } => write!(f, "I/O error: {}", context),
LeIndexError::Memory { message, .. } => write!(f, "Memory error: {}", message),
LeIndexError::Validation { message, .. } => write!(f, "Validation error: {}", message),
}
}
}
impl std::error::Error for LeIndexError {}
impl From<std::io::Error> for LeIndexError {
fn from(err: std::io::Error) -> Self {
LeIndexError::Io {
context: err.to_string(),
path: None,
source: err,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RecoveryStrategy {
Skip,
Retry,
Fallback,
Abort,
}
#[derive(Debug)]
pub struct ErrorContext {
pub operation: String,
pub file_path: Option<PathBuf>,
pub error: LeIndexError,
pub error_count: usize,
pub max_errors: usize,
}
impl ErrorContext {
pub fn new(operation: impl Into<String>) -> Self {
Self {
operation: operation.into(),
file_path: None,
error: LeIndexError::validation_error("Unknown error", None),
error_count: 0,
max_errors: 100,
}
}
pub fn with_file_path(mut self, path: impl Into<PathBuf>) -> Self {
self.file_path = Some(path.into());
self
}
pub fn with_error(mut self, error: LeIndexError) -> Self {
self.error = error;
self
}
pub fn with_max_errors(mut self, max: usize) -> Self {
self.max_errors = max;
self
}
pub fn recovery_strategy(&self) -> RecoveryStrategy {
if matches!(self.error, LeIndexError::Parse { .. }) {
return RecoveryStrategy::Skip;
}
if self.error.is_recoverable() && self.error_count < 3 {
return RecoveryStrategy::Retry;
}
if matches!(self.error, LeIndexError::Validation { .. }) {
return RecoveryStrategy::Fallback;
}
if self.error_count >= self.max_errors {
return RecoveryStrategy::Abort;
}
RecoveryStrategy::Skip
}
pub fn should_abort(&self) -> bool {
self.error_count >= self.max_errors || !self.error.is_recoverable()
}
}
#[derive(Debug, Clone)]
pub struct PartialIndexResult {
pub successful_files: usize,
pub failed_files: Vec<PathBuf>,
pub stats: PartialStats,
pub completed: bool,
}
#[derive(Debug, Clone)]
pub struct PartialStats {
pub total_files: usize,
pub parsed_files: usize,
pub total_signatures: usize,
pub indexed_nodes: usize,
}
impl PartialIndexResult {
pub fn new() -> Self {
Self {
successful_files: 0,
failed_files: Vec::new(),
stats: PartialStats {
total_files: 0,
parsed_files: 0,
total_signatures: 0,
indexed_nodes: 0,
},
completed: false,
}
}
pub fn add_failure(&mut self, file_path: PathBuf) {
self.failed_files.push(file_path);
}
pub fn is_usable(&self) -> bool {
if self.stats.total_files == 0 {
return false;
}
let success_rate = self.stats.parsed_files as f64 / self.stats.total_files as f64;
success_rate >= 0.5
}
}
impl Default for PartialIndexResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CorruptionStatus {
Healthy,
Minor {
missing_files: usize,
},
Major {
description: String,
},
Severe {
description: String,
},
}
impl CorruptionStatus {
pub fn is_usable(&self) -> bool {
match self {
CorruptionStatus::Healthy => true,
CorruptionStatus::Minor { .. } => true,
CorruptionStatus::Major { .. } => false,
CorruptionStatus::Severe { .. } => false,
}
}
pub fn message(&self) -> String {
match self {
CorruptionStatus::Healthy => "Index is healthy".to_string(),
CorruptionStatus::Minor { missing_files } => {
format!("Minor corruption: {} files are missing", missing_files)
}
CorruptionStatus::Major { description } => {
format!("Major corruption: {}", description)
}
CorruptionStatus::Severe { description } => {
format!(
"Severe corruption: {}. Index rebuild required.",
description
)
}
}
}
}
pub fn detect_corruption<P: AsRef<Path>>(project_path: P) -> Result<CorruptionStatus> {
let project_path = project_path.as_ref();
let leindex_dir = project_path.join(".leindex");
if !leindex_dir.exists() {
return Ok(CorruptionStatus::Healthy);
}
let db_path = leindex_dir.join("leindex.db");
if !db_path.exists() {
return Ok(CorruptionStatus::Minor { missing_files: 1 });
}
match crate::storage::schema::Storage::open(&db_path) {
Ok(_) => Ok(CorruptionStatus::Healthy),
Err(e) => {
if e.to_string().contains("corrupted") {
Ok(CorruptionStatus::Major {
description: format!("Database corruption detected: {}", e),
})
} else {
Ok(CorruptionStatus::Severe {
description: format!("Cannot access database: {}", e),
})
}
}
}
}
pub fn recover_corruption<P: AsRef<Path>>(project_path: P) -> Result<bool> {
let project_path = project_path.as_ref();
let status = detect_corruption(project_path)?;
match status {
CorruptionStatus::Healthy => Ok(true),
CorruptionStatus::Minor { .. } => {
Ok(false) }
CorruptionStatus::Major { .. } => {
let leindex_dir = project_path.join(".leindex");
fs::remove_dir_all(&leindex_dir)?;
Ok(false) }
CorruptionStatus::Severe { .. } => {
Ok(false)
}
}
}
pub fn format_error(error: &LeIndexError) -> String {
let mut message = format!("Error: {}", error);
if let Some(suggestion) = error.suggestion() {
message.push_str(&format!("\n\nSuggestion: {}", suggestion));
}
if let LeIndexError::Io { path: Some(p), .. } = error {
message.push_str(&format!("\n\nPath: {:?}", p));
}
message
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let error = LeIndexError::parse_error("Test error", "/test/path");
assert!(matches!(error, LeIndexError::Parse { .. }));
}
#[test]
fn test_recoverable_check() {
let error1 = LeIndexError::index_error("Test", true);
assert!(error1.is_recoverable());
let error2 = LeIndexError::index_error("Test", false);
assert!(!error2.is_recoverable());
}
#[test]
fn test_recovery_strategy() {
let ctx =
ErrorContext::new("test").with_error(LeIndexError::parse_error("test", "/test/path"));
let strategy = ctx.recovery_strategy();
assert_eq!(strategy, RecoveryStrategy::Skip);
}
#[test]
fn test_partial_result() {
let mut result = PartialIndexResult::new();
result.stats.total_files = 10;
result.stats.parsed_files = 8;
assert!(result.is_usable());
result.stats.parsed_files = 4;
assert!(!result.is_usable());
}
#[test]
fn test_corruption_status() {
let status = CorruptionStatus::Healthy;
assert!(status.is_usable());
let message = status.message();
assert!(message.contains("healthy"));
}
}