impl AgentContextIndex {
pub fn save(&self, index_path: &Path) -> Result<(), String> {
fs::create_dir_all(index_path)
.map_err(|e| format!("Failed to create index directory: {e}"))?;
let manifest_json = serde_json::to_string_pretty(&self.manifest)
.map_err(|e| format!("Failed to serialize manifest: {e}"))?;
fs::write(index_path.join("manifest.json"), manifest_json)
.map_err(|e| format!("Failed to write manifest: {e}"))?;
let db_path = index_path.with_extension("db");
super::sqlite_backend::save_to_sqlite(
&db_path,
&self.functions,
&self.calls,
&self.graph_metrics,
&self.manifest,
&self.coverage_off_files,
)?;
Ok(())
}
pub fn load(index_path: &Path) -> Result<Self, String> {
let db_candidate = index_path.with_extension("db");
if db_candidate.exists() {
let schema_ok = super::sqlite_backend::open_db(&db_candidate)
.map(|conn| super::sqlite_backend::has_valid_schema(&conn))
.unwrap_or(false);
if schema_ok {
match Self::load_from_sqlite(&db_candidate) {
Ok(index) => return Ok(index),
Err(e) => {
eprintln!(" Warning: SQLite load failed, falling back to blob: {e}");
}
}
} else {
let _ = std::fs::remove_file(&db_candidate);
}
}
Self::load_from_blob(index_path)
}
fn load_from_sqlite(db_path: &Path) -> Result<Self, String> {
use super::sqlite_backend::{
load_functions_lightweight, load_graph_metrics, load_metadata, open_db,
};
let conn = open_db(db_path)?;
let manifest = load_metadata(&conn)?;
let functions = load_functions_lightweight(&conn)?;
let graph_metrics = load_graph_metrics(&conn)?;
let calls = HashMap::new();
let called_by = HashMap::new();
let indices = build_indices_without_corpus(&functions);
let name_frequency = compute_name_frequency(&indices.name_index, functions.len());
let project_root = PathBuf::from(&manifest.project_root);
let coverage_off_files = load_coverage_off_files(&conn);
Ok(Self {
functions,
name_index: indices.name_index,
file_index: indices.file_index,
corpus: Vec::new(),
corpus_lower: Vec::new(),
name_frequency,
calls,
called_by,
graph_metrics,
project_root,
manifest,
db_path: Some(db_path.to_path_buf()),
coverage_off_files,
})
}
fn load_from_blob(index_path: &Path) -> Result<Self, String> {
let manifest_str = fs::read_to_string(index_path.join("manifest.json"))
.map_err(|e| format!("Failed to read manifest: {e}"))?;
let manifest: IndexManifest = serde_json::from_str(&manifest_str)
.map_err(|e| format!("Failed to parse manifest: {e}"))?;
let compressed = fs::read(index_path.join("functions.lz4"))
.map_err(|e| format!("Failed to read functions: {e}"))?;
let decompressed = lz4_flex::decompress_size_prepended(&compressed)
.map_err(|e| format!("Failed to decompress functions: {e}"))?;
let payload: IndexPayload = bincode::deserialize(&decompressed)
.map_err(|e| format!("Failed to parse payload: {e}"))?;
let functions = payload.functions;
let corpus = payload.corpus;
let calls = payload.calls;
let called_by = payload.called_by;
let has_cached_indices = !payload.name_index.is_empty();
let (name_index, file_index, graph_metrics, corpus_lower, name_frequency) =
if has_cached_indices {
let corpus_lower = if payload.corpus_lower.is_empty() {
corpus.iter().map(|d| d.to_lowercase()).collect()
} else {
payload.corpus_lower
};
(
payload.name_index,
payload.file_index,
payload.graph_metrics,
corpus_lower,
payload.name_frequency,
)
} else {
let is_legacy =
manifest.version.starts_with("1.0") || manifest.version.starts_with("1.1");
let indices = build_indices(&functions);
let (calls_rebuilt, called_by_rebuilt) = if is_legacy && calls.is_empty() {
build_call_graph(&functions, &indices.name_index)
} else {
(calls.clone(), called_by.clone())
};
let graph_metrics =
compute_graph_metrics(functions.len(), &calls_rebuilt, &called_by_rebuilt);
let name_frequency = compute_name_frequency(&indices.name_index, functions.len());
let corpus_lower: Vec<String> = corpus.iter().map(|d| d.to_lowercase()).collect();
(
indices.name_index,
indices.file_index,
graph_metrics,
corpus_lower,
name_frequency,
)
};
let project_root = PathBuf::from(&manifest.project_root);
let db_candidate = index_path.with_extension("db");
let db_path = if db_candidate.exists() {
Some(db_candidate)
} else {
None
};
Ok(Self {
functions,
name_index,
file_index,
corpus,
corpus_lower,
name_frequency,
calls,
called_by,
graph_metrics,
project_root,
manifest,
db_path,
coverage_off_files: HashSet::new(), })
}
}