1use std::collections::HashMap;
15use std::path::{Path, PathBuf};
16
17use serde::{Deserialize, Serialize};
18
19use super::agents::{AgentRegistry, RegistryConfig, SerializedRegistry};
20use super::file_table::{FileId, IndexedFile, IndexedSymbol};
21use super::graph::DepGraph;
22use super::trigram::TrigramIndex;
23use super::versions::VersionLog;
24use super::words::WordIndex;
25use super::IndexState;
26
27pub const SNAPSHOT_FORMAT_VERSION: u32 = 1;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SnapshotMeta {
35 pub format_version: u32,
38 pub workspace_root: String,
40 pub git_head: Option<String>,
42 pub indexed_at_ms: i64,
44 pub file_count: usize,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SnapshotSymbol {
51 pub name: String,
53 pub kind: String,
55 pub start_line: u32,
57 pub end_line: u32,
59 pub signature: String,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct SnapshotFile {
66 pub id: FileId,
68 pub relative_path: String,
70 pub language: String,
72 pub size_bytes: u64,
74 pub line_count: u32,
76 pub content_hash: u64,
78 pub mtime_ms: i64,
80 pub symbols: Vec<SnapshotSymbol>,
82 pub imports: Vec<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct TrigramPosting {
89 pub trigram: u32,
91 pub files: Vec<FileId>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct WordPosting {
98 pub word: String,
100 pub hits: Vec<(FileId, u32)>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct DepRow {
107 pub from: FileId,
109 pub to: Vec<FileId>,
111 #[serde(default)]
113 pub unresolved: Vec<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct CodeIndexSnapshot {
119 pub meta: SnapshotMeta,
121 pub next_file_id: FileId,
124 pub files: Vec<SnapshotFile>,
126 pub trigrams: Vec<TrigramPosting>,
128 pub words: Vec<WordPosting>,
130 pub deps: Vec<DepRow>,
132 pub versions: VersionLog,
134 pub agents: SerializedRegistry,
136}
137
138impl CodeIndexSnapshot {
139 pub fn path_for(workspace_root: &Path) -> PathBuf {
141 workspace_root
142 .join(".burin")
143 .join("index")
144 .join("snapshot.json")
145 }
146
147 pub fn save(&self, workspace_root: &Path) -> std::io::Result<()> {
150 let path = Self::path_for(workspace_root);
151 if let Some(parent) = path.parent() {
152 std::fs::create_dir_all(parent)?;
153 }
154 let tmp = path.with_extension("json.tmp");
155 let bytes = serde_json::to_vec(self).map_err(std::io::Error::other)?;
156 std::fs::write(&tmp, bytes)?;
157 std::fs::rename(&tmp, &path)?;
158 Ok(())
159 }
160
161 pub fn load(workspace_root: &Path) -> std::io::Result<Option<Self>> {
166 let path = Self::path_for(workspace_root);
167 if !path.exists() {
168 return Ok(None);
169 }
170 let bytes = std::fs::read(&path)?;
171 let snap: CodeIndexSnapshot =
172 serde_json::from_slice(&bytes).map_err(std::io::Error::other)?;
173 if snap.meta.format_version != SNAPSHOT_FORMAT_VERSION {
174 return Ok(None);
175 }
176 Ok(Some(snap))
177 }
178}
179
180impl IndexState {
181 pub fn snapshot(&self) -> CodeIndexSnapshot {
183 let files: Vec<SnapshotFile> = self
184 .files
185 .values()
186 .map(|f| SnapshotFile {
187 id: f.id,
188 relative_path: f.relative_path.clone(),
189 language: f.language.clone(),
190 size_bytes: f.size_bytes,
191 line_count: f.line_count,
192 content_hash: f.content_hash,
193 mtime_ms: f.mtime_ms,
194 symbols: f
195 .symbols
196 .iter()
197 .map(|s| SnapshotSymbol {
198 name: s.name.clone(),
199 kind: s.kind.clone(),
200 start_line: s.start_line,
201 end_line: s.end_line,
202 signature: s.signature.clone(),
203 })
204 .collect(),
205 imports: f.imports.clone(),
206 })
207 .collect();
208
209 let trigrams = self.trigrams.snapshot_postings();
210 let words = self.words.snapshot_postings();
211 let deps = self.deps.snapshot_rows();
212
213 CodeIndexSnapshot {
214 meta: SnapshotMeta {
215 format_version: SNAPSHOT_FORMAT_VERSION,
216 workspace_root: self.root.to_string_lossy().into_owned(),
217 git_head: self.git_head.clone(),
218 indexed_at_ms: self.last_built_unix_ms,
219 file_count: self.files.len(),
220 },
221 next_file_id: self.next_file_id_internal(),
222 files,
223 trigrams,
224 words,
225 deps,
226 versions: self.versions.clone(),
227 agents: self.agents.snapshot(),
228 }
229 }
230
231 pub fn from_snapshot(snap: CodeIndexSnapshot) -> Self {
235 let root = PathBuf::from(snap.meta.workspace_root);
236 let mut files: HashMap<FileId, IndexedFile> = HashMap::with_capacity(snap.files.len());
237 let mut path_to_id: HashMap<String, FileId> = HashMap::with_capacity(snap.files.len());
238 for f in snap.files {
239 let indexed = IndexedFile {
240 id: f.id,
241 relative_path: f.relative_path.clone(),
242 language: f.language,
243 size_bytes: f.size_bytes,
244 line_count: f.line_count,
245 content_hash: f.content_hash,
246 mtime_ms: f.mtime_ms,
247 symbols: f
248 .symbols
249 .into_iter()
250 .map(|s| IndexedSymbol {
251 name: s.name,
252 kind: s.kind,
253 start_line: s.start_line,
254 end_line: s.end_line,
255 signature: s.signature,
256 })
257 .collect(),
258 imports: f.imports,
259 };
260 path_to_id.insert(f.relative_path, f.id);
261 files.insert(f.id, indexed);
262 }
263 let trigrams = TrigramIndex::from_postings(snap.trigrams);
264 let words = WordIndex::from_postings(snap.words);
265 let deps = DepGraph::from_rows(snap.deps);
266 let agents = AgentRegistry::from_snapshot(RegistryConfig::default(), snap.agents);
267
268 let mut state = Self::empty(root);
269 state.files = files;
270 state.path_to_id = path_to_id;
271 state.trigrams = trigrams;
272 state.words = words;
273 state.deps = deps;
274 state.versions = snap.versions;
275 state.agents = agents;
276 state.last_built_unix_ms = snap.meta.indexed_at_ms;
277 state.git_head = snap.meta.git_head;
278 state.set_next_file_id(snap.next_file_id);
279 state
280 }
281
282 pub fn reap_after_recovery(&mut self, now_ms: i64) {
286 self.agents.reap(now_ms);
287 }
288}