1use std::collections::HashMap;
9
10#[derive(Debug, Clone)]
12pub struct TileVersion {
13 pub tile_id: String,
14 pub version: u32,
15 pub parent_id: Option<String>,
16 pub content: String,
17 pub question: String,
18 pub answer: String,
19 pub confidence: f32,
20 pub dependencies: Vec<String>,
21 pub counterpoint_ids: Vec<String>,
22 pub tags: Vec<String>,
23 pub invalidated: bool,
24 pub created_at: u64,
25}
26
27#[derive(Debug, Clone)]
29pub struct CascadeEvent {
30 pub source_tile_id: String,
31 pub source_version: u32,
32 pub affected_tile_ids: Vec<String>,
33 pub reason: String,
34 pub timestamp: u64,
35}
36
37#[derive(Debug, Clone)]
39pub struct Dependency {
40 pub dependent_id: String, pub dependency_id: String, pub strength: f32, }
44
45pub struct TileStore {
47 versions: HashMap<String, Vec<TileVersion>>,
49 dependencies: Vec<Dependency>,
51 cascade_log: Vec<CascadeEvent>,
53 current_versions: HashMap<String, u32>,
55}
56
57impl TileStore {
58 pub fn new() -> Self {
59 Self {
60 versions: HashMap::new(),
61 dependencies: Vec::new(),
62 cascade_log: Vec::new(),
63 current_versions: HashMap::new(),
64 }
65 }
66
67 pub fn insert(&mut self, version: TileVersion) {
69 let id = version.tile_id.clone();
70 let ver = version.version;
71 self.current_versions.insert(id.clone(), ver);
72 self.versions.entry(id).or_default().push(version);
73 }
74
75 pub fn insert_version(&mut self, version: TileVersion) -> Result<(), String> {
77 let id = version.tile_id.clone();
78 if let Some(existing) = self.versions.get(&id) {
79 if !existing.iter().any(|v| v.version == version.version) {
80 self.current_versions.insert(id.clone(), version.version);
81 self.versions.get_mut(&id).unwrap().push(version);
82 return Ok(());
83 }
84 Err(format!("Version {} already exists for tile {}", version.version, id))
85 } else {
86 self.insert(version);
87 Ok(())
88 }
89 }
90
91 pub fn get_latest(&self, tile_id: &str) -> Option<&TileVersion> {
93 self.versions.get(tile_id)
94 .and_then(|versions| versions.last())
95 }
96
97 pub fn get_version(&self, tile_id: &str, version: u32) -> Option<&TileVersion> {
99 self.versions.get(tile_id)
100 .and_then(|versions| versions.iter().find(|v| v.version == version))
101 }
102
103 pub fn get_history(&self, tile_id: &str) -> Vec<&TileVersion> {
105 match self.versions.get(tile_id) {
106 Some(versions) => versions.iter().rev().collect(),
107 None => Vec::new(),
108 }
109 }
110
111 pub fn rollback(&mut self, tile_id: &str, target_version: u32, now: u64) -> Result<TileVersion, String> {
114 let old = self.get_version(tile_id, target_version)
115 .ok_or_else(|| format!("Version {} not found for tile {}", target_version, tile_id))?
116 .clone();
117
118 let current_ver = self.current_versions.get(tile_id)
119 .ok_or_else(|| format!("Tile {} not found", tile_id))?;
120
121 let rolled_back = TileVersion {
122 tile_id: tile_id.to_string(),
123 version: current_ver + 1,
124 parent_id: Some(format!("{}:v{}", tile_id, target_version)),
125 content: old.content.clone(),
126 question: old.question.clone(),
127 answer: old.answer.clone(),
128 confidence: old.confidence * 0.95, dependencies: old.dependencies.clone(),
130 counterpoint_ids: old.counterpoint_ids.clone(),
131 tags: old.tags.clone(),
132 invalidated: false,
133 created_at: now,
134 };
135
136 self.insert_version(rolled_back.clone())?;
137 Ok(rolled_back)
138 }
139
140 pub fn add_dependency(&mut self, dependent_id: &str, dependency_id: &str, strength: f32) {
142 self.dependencies.push(Dependency {
143 dependent_id: dependent_id.to_string(),
144 dependency_id: dependency_id.to_string(),
145 strength: strength.min(1.0).max(0.0),
146 });
147 }
148
149 pub fn get_dependents(&self, tile_id: &str) -> Vec<&Dependency> {
151 self.dependencies.iter()
152 .filter(|d| d.dependency_id == tile_id)
153 .collect()
154 }
155
156 pub fn get_dependencies_of(&self, tile_id: &str) -> Vec<&Dependency> {
158 self.dependencies.iter()
159 .filter(|d| d.dependent_id == tile_id)
160 .collect()
161 }
162
163 pub fn invalidate(&mut self, tile_id: &str, reason: &str, now: u64) -> CascadeEvent {
165 if let Some(versions) = self.versions.get_mut(tile_id) {
167 if let Some(latest) = versions.last_mut() {
168 latest.invalidated = true;
169 }
170 }
171
172 let dependents: Vec<String> = self.get_dependents(tile_id)
174 .iter()
175 .map(|d| d.dependent_id.clone())
176 .collect();
177
178 for dep_id in &dependents {
180 if let Some(versions) = self.versions.get_mut(dep_id) {
181 if let Some(latest) = versions.last_mut() {
182 latest.confidence *= 0.8; }
184 }
185 }
186
187 let event = CascadeEvent {
188 source_tile_id: tile_id.to_string(),
189 source_version: self.current_versions.get(tile_id).copied().unwrap_or(0),
190 affected_tile_ids: dependents.clone(),
191 reason: reason.to_string(),
192 timestamp: now,
193 };
194
195 self.cascade_log.push(event.clone());
196 event
197 }
198
199 pub fn restore(&mut self, tile_id: &str, now: u64) -> Result<(), String> {
201 if let Some(versions) = self.versions.get_mut(tile_id) {
202 if let Some(latest) = versions.last_mut() {
203 if latest.invalidated {
204 latest.invalidated = false;
205 latest.confidence = (latest.confidence / 0.8).min(1.0);
206 return Ok(());
207 }
208 }
209 }
210 Err(format!("Tile {} not found or not invalidated", tile_id))
211 }
212
213 pub fn search_by_tag(&self, tag: &str) -> Vec<&TileVersion> {
215 self.versions.values()
216 .filter_map(|versions| versions.last())
217 .filter(|v| v.tags.iter().any(|t| t == tag))
218 .collect()
219 }
220
221 pub fn get_cascade_log(&self) -> &[CascadeEvent] {
223 &self.cascade_log
224 }
225
226 pub fn tile_count(&self) -> usize {
228 self.versions.len()
229 }
230
231 pub fn total_versions(&self) -> usize {
233 self.versions.values().map(|v| v.len()).sum()
234 }
235
236 pub fn tile_ids(&self) -> Vec<String> {
238 self.versions.keys().cloned().collect()
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 fn make_version(id: &str, ver: u32, content: &str, conf: f32) -> TileVersion {
247 TileVersion {
248 tile_id: id.to_string(),
249 version: ver,
250 parent_id: if ver > 1 { Some(format!("{}:v{}", id, ver - 1)) } else { None },
251 content: content.to_string(),
252 question: format!("Q{}", ver),
253 answer: content.to_string(),
254 confidence: conf,
255 dependencies: Vec::new(),
256 counterpoint_ids: Vec::new(),
257 tags: vec!["test".to_string()],
258 invalidated: false,
259 created_at: (ver as u64) * 1000,
260 }
261 }
262
263 #[test]
264 fn test_insert_and_get_latest() {
265 let mut store = TileStore::new();
266 store.insert(make_version("t1", 1, "content v1", 0.9));
267 let latest = store.get_latest("t1").unwrap();
268 assert_eq!(latest.content, "content v1");
269 assert_eq!(latest.version, 1);
270 }
271
272 #[test]
273 fn test_insert_version() {
274 let mut store = TileStore::new();
275 store.insert(make_version("t1", 1, "v1", 0.8));
276 store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
277 let latest = store.get_latest("t1").unwrap();
278 assert_eq!(latest.version, 2);
279 assert_eq!(latest.content, "v2");
280 }
281
282 #[test]
283 fn test_duplicate_version_rejected() {
284 let mut store = TileStore::new();
285 store.insert(make_version("t1", 1, "v1", 0.8));
286 let result = store.insert_version(make_version("t1", 1, "v1-dup", 0.9));
287 assert!(result.is_err());
288 }
289
290 #[test]
291 fn test_get_specific_version() {
292 let mut store = TileStore::new();
293 store.insert(make_version("t1", 1, "v1", 0.8));
294 store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
295 store.insert_version(make_version("t1", 3, "v3", 0.95)).unwrap();
296 let v2 = store.get_version("t1", 2).unwrap();
297 assert_eq!(v2.content, "v2");
298 }
299
300 #[test]
301 fn test_get_history() {
302 let mut store = TileStore::new();
303 store.insert(make_version("t1", 1, "v1", 0.8));
304 store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
305 store.insert_version(make_version("t1", 3, "v3", 0.95)).unwrap();
306 let history = store.get_history("t1");
307 assert_eq!(history.len(), 3);
308 assert_eq!(history[0].version, 3); assert_eq!(history[2].version, 1);
310 }
311
312 #[test]
313 fn test_rollback() {
314 let mut store = TileStore::new();
315 store.insert(make_version("t1", 1, "v1", 0.9));
316 store.insert_version(make_version("t1", 2, "v2-bad", 0.7)).unwrap();
317 let rolled = store.rollback("t1", 1, 3000).unwrap();
318 assert_eq!(rolled.content, "v1");
319 assert_eq!(rolled.version, 3);
320 assert!((rolled.confidence - 0.855).abs() < 0.01); let latest = store.get_latest("t1").unwrap();
322 assert_eq!(latest.content, "v1");
323 }
324
325 #[test]
326 fn test_rollback_nonexistent_version() {
327 let mut store = TileStore::new();
328 store.insert(make_version("t1", 1, "v1", 0.9));
329 let result = store.rollback("t1", 99, 3000);
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn test_add_dependency() {
335 let mut store = TileStore::new();
336 store.insert(make_version("t1", 1, "v1", 0.9));
337 store.insert(make_version("t2", 1, "v2", 0.8));
338 store.add_dependency("t2", "t1", 1.0);
339 let deps = store.get_dependents("t1");
340 assert_eq!(deps.len(), 1);
341 assert_eq!(deps[0].dependent_id, "t2");
342 }
343
344 #[test]
345 fn test_invalidate_cascade() {
346 let mut store = TileStore::new();
347 store.insert(make_version("t1", 1, "v1", 0.9));
348 store.insert(make_version("t2", 1, "v2", 0.8));
349 store.insert(make_version("t3", 1, "v3", 0.7));
350 store.add_dependency("t2", "t1", 1.0);
351 store.add_dependency("t3", "t1", 0.5);
352
353 let event = store.invalidate("t1", "found to be incorrect", 5000);
354 assert!(store.get_latest("t1").unwrap().invalidated);
355 assert_eq!(event.affected_tile_ids.len(), 2);
356 assert!(event.affected_tile_ids.contains(&"t2".to_string()));
357 assert!(event.affected_tile_ids.contains(&"t3".to_string()));
358 assert!(store.get_latest("t2").unwrap().confidence < 0.8);
360 assert!(store.get_latest("t3").unwrap().confidence < 0.7);
361 }
362
363 #[test]
364 fn test_restore_tile() {
365 let mut store = TileStore::new();
366 store.insert(make_version("t1", 1, "v1", 0.8));
367 store.invalidate("t1", "temporary", 5000);
368 assert!(store.get_latest("t1").unwrap().invalidated);
369 store.restore("t1", 6000).unwrap();
370 assert!(!store.get_latest("t1").unwrap().invalidated);
371 }
372
373 #[test]
374 fn test_restore_nonexistent() {
375 let mut store = TileStore::new();
376 assert!(store.restore("t99", 5000).is_err());
377 }
378
379 #[test]
380 fn test_search_by_tag() {
381 let mut store = TileStore::new();
382 let mut v1 = make_version("t1", 1, "rust content", 0.9);
383 v1.tags = vec!["rust".to_string(), "systems".to_string()];
384 let mut v2 = make_version("t2", 1, "python content", 0.8);
385 v2.tags = vec!["python".to_string()];
386 let mut v3 = make_version("t3", 1, "more rust", 0.7);
387 v3.tags = vec!["rust".to_string()];
388 store.insert(v1);
389 store.insert(v2);
390 store.insert(v3);
391 let rust_tiles = store.search_by_tag("rust");
392 assert_eq!(rust_tiles.len(), 2);
393 }
394
395 #[test]
396 fn test_cascade_log() {
397 let mut store = TileStore::new();
398 store.insert(make_version("t1", 1, "v1", 0.9));
399 store.insert(make_version("t2", 1, "v2", 0.8));
400 store.add_dependency("t2", "t1", 1.0);
401 store.invalidate("t1", "test", 5000);
402 assert_eq!(store.get_cascade_log().len(), 1);
403 assert_eq!(store.get_cascade_log()[0].reason, "test");
404 }
405
406 #[test]
407 fn test_tile_count_and_ids() {
408 let mut store = TileStore::new();
409 store.insert(make_version("t1", 1, "v1", 0.9));
410 store.insert(make_version("t2", 1, "v2", 0.8));
411 assert_eq!(store.tile_count(), 2);
412 assert_eq!(store.total_versions(), 2);
413 let ids = store.tile_ids();
414 assert!(ids.contains(&"t1".to_string()));
415 assert!(ids.contains(&"t2".to_string()));
416 }
417
418 #[test]
419 fn test_dependency_strength_clamped() {
420 let mut store = TileStore::new();
421 store.add_dependency("t2", "t1", 1.5);
422 let deps = store.get_dependents("t1");
423 assert_eq!(deps[0].strength, 1.0);
424 store.add_dependency("t3", "t1", -0.5);
425 let deps2 = store.get_dependents("t1");
426 assert_eq!(deps2[1].strength, 0.0);
427 }
428
429 #[test]
430 fn test_get_dependencies_of() {
431 let mut store = TileStore::new();
432 store.insert(make_version("t1", 1, "v1", 0.9));
433 store.insert(make_version("t2", 1, "v2", 0.8));
434 store.insert(make_version("t3", 1, "v3", 0.7));
435 store.add_dependency("t1", "t2", 1.0);
436 store.add_dependency("t1", "t3", 0.5);
437 let deps = store.get_dependencies_of("t1");
438 assert_eq!(deps.len(), 2);
439 }
440
441 #[test]
442 fn test_immutability_old_versions_preserved() {
443 let mut store = TileStore::new();
444 store.insert(make_version("t1", 1, "v1", 0.9));
445 store.insert_version(make_version("t1", 2, "v2", 0.8)).unwrap();
446 store.insert_version(make_version("t1", 3, "v3", 0.7)).unwrap();
447 assert_eq!(store.get_version("t1", 1).unwrap().content, "v1");
449 assert_eq!(store.get_version("t1", 2).unwrap().content, "v2");
450 }
451}