1use std::collections::HashMap;
14use std::path::{Path, PathBuf};
15
16use serde::{Deserialize, Serialize};
17
18use super::agents::{AgentRegistry, RegistryConfig, SerializedRegistry};
19use super::file_table::{FileId, IndexedFile, IndexedSymbol};
20use super::graph::DepGraph;
21use super::trigram::TrigramIndex;
22use super::versions::VersionLog;
23use super::words::WordIndex;
24use super::IndexState;
25
26pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct SnapshotMeta {
34 pub format_version: u32,
37 pub workspace_root: String,
39 pub git_head: Option<String>,
41 pub indexed_at_ms: i64,
43 pub file_count: usize,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct SnapshotSymbol {
50 pub name: String,
52 pub kind: String,
54 pub start_line: u32,
56 pub end_line: u32,
58 pub signature: String,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct SnapshotFile {
65 pub id: FileId,
67 pub relative_path: String,
69 pub language: String,
71 pub size_bytes: u64,
73 pub line_count: u32,
75 pub content_hash: u64,
77 pub mtime_ms: i64,
79 pub symbols: Vec<SnapshotSymbol>,
81 pub imports: Vec<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct TrigramPosting {
88 pub trigram: u32,
90 pub files: Vec<FileId>,
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct WordPosting {
97 pub word: String,
99 pub hits: Vec<(FileId, u32)>,
101}
102
103#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct DepRow {
106 pub from: FileId,
108 pub to: Vec<FileId>,
110 #[serde(default)]
112 pub unresolved: Vec<String>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct CodeIndexSnapshot {
118 pub meta: SnapshotMeta,
120 pub next_file_id: FileId,
123 pub files: Vec<SnapshotFile>,
125 pub trigrams: Vec<TrigramPosting>,
127 pub words: Vec<WordPosting>,
129 pub deps: Vec<DepRow>,
131 pub versions: VersionLog,
133 pub agents: SerializedRegistry,
135}
136
137impl CodeIndexSnapshot {
138 pub fn path_for(workspace_root: &Path) -> PathBuf {
140 workspace_root
141 .join(".burin")
142 .join("index")
143 .join("snapshot.json")
144 }
145
146 pub fn save(&self, workspace_root: &Path) -> std::io::Result<()> {
149 let path = Self::path_for(workspace_root);
150 if let Some(parent) = path.parent() {
151 std::fs::create_dir_all(parent)?;
152 }
153 let tmp = path.with_extension("json.tmp");
154 let bytes = serde_json::to_vec(self).map_err(std::io::Error::other)?;
155 std::fs::write(&tmp, bytes)?;
156 std::fs::rename(&tmp, &path)?;
157 Ok(())
158 }
159
160 pub fn load(workspace_root: &Path) -> std::io::Result<Option<Self>> {
165 let path = Self::path_for(workspace_root);
166 if !path.exists() {
167 return Ok(None);
168 }
169 let bytes = std::fs::read(&path)?;
170 let snap: CodeIndexSnapshot =
171 serde_json::from_slice(&bytes).map_err(std::io::Error::other)?;
172 if snap.meta.format_version != SNAPSHOT_FORMAT_VERSION {
173 return Ok(None);
174 }
175 Ok(Some(snap))
176 }
177}
178
179impl IndexState {
180 pub fn snapshot(&self) -> CodeIndexSnapshot {
182 let files: Vec<SnapshotFile> = self
183 .files
184 .values()
185 .map(|f| SnapshotFile {
186 id: f.id,
187 relative_path: f.relative_path.clone(),
188 language: f.language.clone(),
189 size_bytes: f.size_bytes,
190 line_count: f.line_count,
191 content_hash: f.content_hash,
192 mtime_ms: f.mtime_ms,
193 symbols: f
194 .symbols
195 .iter()
196 .map(|s| SnapshotSymbol {
197 name: s.name.clone(),
198 kind: s.kind.clone(),
199 start_line: s.start_line,
200 end_line: s.end_line,
201 signature: s.signature.clone(),
202 })
203 .collect(),
204 imports: f.imports.clone(),
205 })
206 .collect();
207
208 let trigrams = self.trigrams.snapshot_postings();
209 let words = self.words.snapshot_postings();
210 let deps = self.deps.snapshot_rows();
211
212 CodeIndexSnapshot {
213 meta: SnapshotMeta {
214 format_version: SNAPSHOT_FORMAT_VERSION,
215 workspace_root: self.root.to_string_lossy().into_owned(),
216 git_head: self.git_head.clone(),
217 indexed_at_ms: self.last_built_unix_ms,
218 file_count: self.files.len(),
219 },
220 next_file_id: self.next_file_id_internal(),
221 files,
222 trigrams,
223 words,
224 deps,
225 versions: self.versions.clone(),
226 agents: self.agents.snapshot(),
227 }
228 }
229
230 pub fn from_snapshot(snap: CodeIndexSnapshot) -> Self {
234 let root = PathBuf::from(snap.meta.workspace_root);
235 let mut files: HashMap<FileId, IndexedFile> = HashMap::with_capacity(snap.files.len());
236 let mut path_to_id: HashMap<String, FileId> = HashMap::with_capacity(snap.files.len());
237 for f in snap.files {
238 let indexed = IndexedFile {
239 id: f.id,
240 relative_path: f.relative_path.clone(),
241 language: f.language,
242 size_bytes: f.size_bytes,
243 line_count: f.line_count,
244 content_hash: f.content_hash,
245 mtime_ms: f.mtime_ms,
246 symbols: f
247 .symbols
248 .into_iter()
249 .map(|s| IndexedSymbol {
250 name: s.name,
251 kind: s.kind,
252 start_line: s.start_line,
253 end_line: s.end_line,
254 signature: s.signature,
255 })
256 .collect(),
257 imports: f.imports,
258 };
259 path_to_id.insert(f.relative_path, f.id);
260 files.insert(f.id, indexed);
261 }
262 let trigrams = TrigramIndex::from_postings(snap.trigrams);
263 let words = WordIndex::from_postings(snap.words);
264 let deps = DepGraph::from_rows(snap.deps);
265 let agents = AgentRegistry::from_snapshot(RegistryConfig::default(), snap.agents);
266
267 let mut state = Self::empty(root);
268 state.files = files;
269 state.path_to_id = path_to_id;
270 state.trigrams = trigrams;
271 state.words = words;
272 state.deps = deps;
273 state.versions = snap.versions;
274 state.agents = agents;
275 state.last_built_unix_ms = snap.meta.indexed_at_ms;
276 state.git_head = snap.meta.git_head;
277 state.set_next_file_id(snap.next_file_id);
278 state
279 }
280
281 pub fn reap_after_recovery(&mut self, now_ms: i64) {
285 self.agents.reap(now_ms);
286 }
287}