use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::agents::{AgentRegistry, RegistryConfig, SerializedRegistry};
use super::file_table::{FileId, IndexedFile, IndexedSymbol};
use super::graph::DepGraph;
use super::trigram::TrigramIndex;
use super::versions::VersionLog;
use super::words::WordIndex;
use super::IndexState;
pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotMeta {
pub format_version: u32,
pub workspace_root: String,
pub git_head: Option<String>,
pub indexed_at_ms: i64,
pub file_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotSymbol {
pub name: String,
pub kind: String,
pub start_line: u32,
pub end_line: u32,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotFile {
pub id: FileId,
pub relative_path: String,
pub language: String,
pub size_bytes: u64,
pub line_count: u32,
pub content_hash: u64,
pub mtime_ms: i64,
pub symbols: Vec<SnapshotSymbol>,
pub imports: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrigramPosting {
pub trigram: u32,
pub files: Vec<FileId>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WordPosting {
pub word: String,
pub hits: Vec<(FileId, u32)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DepRow {
pub from: FileId,
pub to: Vec<FileId>,
#[serde(default)]
pub unresolved: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CodeIndexSnapshot {
pub meta: SnapshotMeta,
pub next_file_id: FileId,
pub files: Vec<SnapshotFile>,
pub trigrams: Vec<TrigramPosting>,
pub words: Vec<WordPosting>,
pub deps: Vec<DepRow>,
pub versions: VersionLog,
pub agents: SerializedRegistry,
}
impl CodeIndexSnapshot {
pub fn path_for(workspace_root: &Path) -> PathBuf {
workspace_root
.join(".burin")
.join("index")
.join("snapshot.json")
}
pub fn save(&self, workspace_root: &Path) -> std::io::Result<()> {
let path = Self::path_for(workspace_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let tmp = path.with_extension("json.tmp");
let bytes = serde_json::to_vec(self).map_err(std::io::Error::other)?;
std::fs::write(&tmp, bytes)?;
std::fs::rename(&tmp, &path)?;
Ok(())
}
pub fn load(workspace_root: &Path) -> std::io::Result<Option<Self>> {
let path = Self::path_for(workspace_root);
if !path.exists() {
return Ok(None);
}
let bytes = std::fs::read(&path)?;
let snap: CodeIndexSnapshot =
serde_json::from_slice(&bytes).map_err(std::io::Error::other)?;
if snap.meta.format_version != SNAPSHOT_FORMAT_VERSION {
return Ok(None);
}
Ok(Some(snap))
}
}
impl IndexState {
pub fn snapshot(&self) -> CodeIndexSnapshot {
let files: Vec<SnapshotFile> = self
.files
.values()
.map(|f| SnapshotFile {
id: f.id,
relative_path: f.relative_path.clone(),
language: f.language.clone(),
size_bytes: f.size_bytes,
line_count: f.line_count,
content_hash: f.content_hash,
mtime_ms: f.mtime_ms,
symbols: f
.symbols
.iter()
.map(|s| SnapshotSymbol {
name: s.name.clone(),
kind: s.kind.clone(),
start_line: s.start_line,
end_line: s.end_line,
signature: s.signature.clone(),
})
.collect(),
imports: f.imports.clone(),
})
.collect();
let trigrams = self.trigrams.snapshot_postings();
let words = self.words.snapshot_postings();
let deps = self.deps.snapshot_rows();
CodeIndexSnapshot {
meta: SnapshotMeta {
format_version: SNAPSHOT_FORMAT_VERSION,
workspace_root: self.root.to_string_lossy().into_owned(),
git_head: self.git_head.clone(),
indexed_at_ms: self.last_built_unix_ms,
file_count: self.files.len(),
},
next_file_id: self.next_file_id_internal(),
files,
trigrams,
words,
deps,
versions: self.versions.clone(),
agents: self.agents.snapshot(),
}
}
pub fn from_snapshot(snap: CodeIndexSnapshot) -> Self {
let root = PathBuf::from(snap.meta.workspace_root);
let mut files: HashMap<FileId, IndexedFile> = HashMap::with_capacity(snap.files.len());
let mut path_to_id: HashMap<String, FileId> = HashMap::with_capacity(snap.files.len());
for f in snap.files {
let indexed = IndexedFile {
id: f.id,
relative_path: f.relative_path.clone(),
language: f.language,
size_bytes: f.size_bytes,
line_count: f.line_count,
content_hash: f.content_hash,
mtime_ms: f.mtime_ms,
symbols: f
.symbols
.into_iter()
.map(|s| IndexedSymbol {
name: s.name,
kind: s.kind,
start_line: s.start_line,
end_line: s.end_line,
signature: s.signature,
})
.collect(),
imports: f.imports,
};
path_to_id.insert(f.relative_path, f.id);
files.insert(f.id, indexed);
}
let trigrams = TrigramIndex::from_postings(snap.trigrams);
let words = WordIndex::from_postings(snap.words);
let deps = DepGraph::from_rows(snap.deps);
let agents = AgentRegistry::from_snapshot(RegistryConfig::default(), snap.agents);
let mut state = Self::empty(root);
state.files = files;
state.path_to_id = path_to_id;
state.trigrams = trigrams;
state.words = words;
state.deps = deps;
state.versions = snap.versions;
state.agents = agents;
state.last_built_unix_ms = snap.meta.indexed_at_ms;
state.git_head = snap.meta.git_head;
state.set_next_file_id(snap.next_file_id);
state
}
pub fn reap_after_recovery(&mut self, now_ms: i64) {
self.agents.reap(now_ms);
}
}