use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct TileVersion {
pub tile_id: String,
pub version: u32,
pub parent_id: Option<String>,
pub content: String,
pub question: String,
pub answer: String,
pub confidence: f32,
pub dependencies: Vec<String>,
pub counterpoint_ids: Vec<String>,
pub tags: Vec<String>,
pub invalidated: bool,
pub created_at: u64,
}
#[derive(Debug, Clone)]
pub struct CascadeEvent {
pub source_tile_id: String,
pub source_version: u32,
pub affected_tile_ids: Vec<String>,
pub reason: String,
pub timestamp: u64,
}
#[derive(Debug, Clone)]
pub struct Dependency {
pub dependent_id: String, pub dependency_id: String, pub strength: f32, }
pub struct TileStore {
versions: HashMap<String, Vec<TileVersion>>,
dependencies: Vec<Dependency>,
cascade_log: Vec<CascadeEvent>,
current_versions: HashMap<String, u32>,
}
impl TileStore {
pub fn new() -> Self {
Self {
versions: HashMap::new(),
dependencies: Vec::new(),
cascade_log: Vec::new(),
current_versions: HashMap::new(),
}
}
pub fn insert(&mut self, version: TileVersion) {
let id = version.tile_id.clone();
let ver = version.version;
self.current_versions.insert(id.clone(), ver);
self.versions.entry(id).or_default().push(version);
}
pub fn insert_version(&mut self, version: TileVersion) -> Result<(), String> {
let id = version.tile_id.clone();
if let Some(existing) = self.versions.get(&id) {
if !existing.iter().any(|v| v.version == version.version) {
self.current_versions.insert(id.clone(), version.version);
self.versions.get_mut(&id).unwrap().push(version);
return Ok(());
}
Err(format!("Version {} already exists for tile {}", version.version, id))
} else {
self.insert(version);
Ok(())
}
}
pub fn get_latest(&self, tile_id: &str) -> Option<&TileVersion> {
self.versions.get(tile_id)
.and_then(|versions| versions.last())
}
pub fn get_version(&self, tile_id: &str, version: u32) -> Option<&TileVersion> {
self.versions.get(tile_id)
.and_then(|versions| versions.iter().find(|v| v.version == version))
}
pub fn get_history(&self, tile_id: &str) -> Vec<&TileVersion> {
match self.versions.get(tile_id) {
Some(versions) => versions.iter().rev().collect(),
None => Vec::new(),
}
}
pub fn rollback(&mut self, tile_id: &str, target_version: u32, now: u64) -> Result<TileVersion, String> {
let old = self.get_version(tile_id, target_version)
.ok_or_else(|| format!("Version {} not found for tile {}", target_version, tile_id))?
.clone();
let current_ver = self.current_versions.get(tile_id)
.ok_or_else(|| format!("Tile {} not found", tile_id))?;
let rolled_back = TileVersion {
tile_id: tile_id.to_string(),
version: current_ver + 1,
parent_id: Some(format!("{}:v{}", tile_id, target_version)),
content: old.content.clone(),
question: old.question.clone(),
answer: old.answer.clone(),
confidence: old.confidence * 0.95, dependencies: old.dependencies.clone(),
counterpoint_ids: old.counterpoint_ids.clone(),
tags: old.tags.clone(),
invalidated: false,
created_at: now,
};
self.insert_version(rolled_back.clone())?;
Ok(rolled_back)
}
pub fn add_dependency(&mut self, dependent_id: &str, dependency_id: &str, strength: f32) {
self.dependencies.push(Dependency {
dependent_id: dependent_id.to_string(),
dependency_id: dependency_id.to_string(),
strength: strength.min(1.0).max(0.0),
});
}
pub fn get_dependents(&self, tile_id: &str) -> Vec<&Dependency> {
self.dependencies.iter()
.filter(|d| d.dependency_id == tile_id)
.collect()
}
pub fn get_dependencies_of(&self, tile_id: &str) -> Vec<&Dependency> {
self.dependencies.iter()
.filter(|d| d.dependent_id == tile_id)
.collect()
}
pub fn invalidate(&mut self, tile_id: &str, reason: &str, now: u64) -> CascadeEvent {
if let Some(versions) = self.versions.get_mut(tile_id) {
if let Some(latest) = versions.last_mut() {
latest.invalidated = true;
}
}
let dependents: Vec<String> = self.get_dependents(tile_id)
.iter()
.map(|d| d.dependent_id.clone())
.collect();
for dep_id in &dependents {
if let Some(versions) = self.versions.get_mut(dep_id) {
if let Some(latest) = versions.last_mut() {
latest.confidence *= 0.8; }
}
}
let event = CascadeEvent {
source_tile_id: tile_id.to_string(),
source_version: self.current_versions.get(tile_id).copied().unwrap_or(0),
affected_tile_ids: dependents.clone(),
reason: reason.to_string(),
timestamp: now,
};
self.cascade_log.push(event.clone());
event
}
pub fn restore(&mut self, tile_id: &str, now: u64) -> Result<(), String> {
if let Some(versions) = self.versions.get_mut(tile_id) {
if let Some(latest) = versions.last_mut() {
if latest.invalidated {
latest.invalidated = false;
latest.confidence = (latest.confidence / 0.8).min(1.0);
return Ok(());
}
}
}
Err(format!("Tile {} not found or not invalidated", tile_id))
}
pub fn search_by_tag(&self, tag: &str) -> Vec<&TileVersion> {
self.versions.values()
.filter_map(|versions| versions.last())
.filter(|v| v.tags.iter().any(|t| t == tag))
.collect()
}
pub fn get_cascade_log(&self) -> &[CascadeEvent] {
&self.cascade_log
}
pub fn tile_count(&self) -> usize {
self.versions.len()
}
pub fn total_versions(&self) -> usize {
self.versions.values().map(|v| v.len()).sum()
}
pub fn tile_ids(&self) -> Vec<String> {
self.versions.keys().cloned().collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_version(id: &str, ver: u32, content: &str, conf: f32) -> TileVersion {
TileVersion {
tile_id: id.to_string(),
version: ver,
parent_id: if ver > 1 { Some(format!("{}:v{}", id, ver - 1)) } else { None },
content: content.to_string(),
question: format!("Q{}", ver),
answer: content.to_string(),
confidence: conf,
dependencies: Vec::new(),
counterpoint_ids: Vec::new(),
tags: vec!["test".to_string()],
invalidated: false,
created_at: (ver as u64) * 1000,
}
}
#[test]
fn test_insert_and_get_latest() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "content v1", 0.9));
let latest = store.get_latest("t1").unwrap();
assert_eq!(latest.content, "content v1");
assert_eq!(latest.version, 1);
}
#[test]
fn test_insert_version() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.8));
store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
let latest = store.get_latest("t1").unwrap();
assert_eq!(latest.version, 2);
assert_eq!(latest.content, "v2");
}
#[test]
fn test_duplicate_version_rejected() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.8));
let result = store.insert_version(make_version("t1", 1, "v1-dup", 0.9));
assert!(result.is_err());
}
#[test]
fn test_get_specific_version() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.8));
store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
store.insert_version(make_version("t1", 3, "v3", 0.95)).unwrap();
let v2 = store.get_version("t1", 2).unwrap();
assert_eq!(v2.content, "v2");
}
#[test]
fn test_get_history() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.8));
store.insert_version(make_version("t1", 2, "v2", 0.9)).unwrap();
store.insert_version(make_version("t1", 3, "v3", 0.95)).unwrap();
let history = store.get_history("t1");
assert_eq!(history.len(), 3);
assert_eq!(history[0].version, 3); assert_eq!(history[2].version, 1);
}
#[test]
fn test_rollback() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert_version(make_version("t1", 2, "v2-bad", 0.7)).unwrap();
let rolled = store.rollback("t1", 1, 3000).unwrap();
assert_eq!(rolled.content, "v1");
assert_eq!(rolled.version, 3);
assert!((rolled.confidence - 0.855).abs() < 0.01); let latest = store.get_latest("t1").unwrap();
assert_eq!(latest.content, "v1");
}
#[test]
fn test_rollback_nonexistent_version() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
let result = store.rollback("t1", 99, 3000);
assert!(result.is_err());
}
#[test]
fn test_add_dependency() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert(make_version("t2", 1, "v2", 0.8));
store.add_dependency("t2", "t1", 1.0);
let deps = store.get_dependents("t1");
assert_eq!(deps.len(), 1);
assert_eq!(deps[0].dependent_id, "t2");
}
#[test]
fn test_invalidate_cascade() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert(make_version("t2", 1, "v2", 0.8));
store.insert(make_version("t3", 1, "v3", 0.7));
store.add_dependency("t2", "t1", 1.0);
store.add_dependency("t3", "t1", 0.5);
let event = store.invalidate("t1", "found to be incorrect", 5000);
assert!(store.get_latest("t1").unwrap().invalidated);
assert_eq!(event.affected_tile_ids.len(), 2);
assert!(event.affected_tile_ids.contains(&"t2".to_string()));
assert!(event.affected_tile_ids.contains(&"t3".to_string()));
assert!(store.get_latest("t2").unwrap().confidence < 0.8);
assert!(store.get_latest("t3").unwrap().confidence < 0.7);
}
#[test]
fn test_restore_tile() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.8));
store.invalidate("t1", "temporary", 5000);
assert!(store.get_latest("t1").unwrap().invalidated);
store.restore("t1", 6000).unwrap();
assert!(!store.get_latest("t1").unwrap().invalidated);
}
#[test]
fn test_restore_nonexistent() {
let mut store = TileStore::new();
assert!(store.restore("t99", 5000).is_err());
}
#[test]
fn test_search_by_tag() {
let mut store = TileStore::new();
let mut v1 = make_version("t1", 1, "rust content", 0.9);
v1.tags = vec!["rust".to_string(), "systems".to_string()];
let mut v2 = make_version("t2", 1, "python content", 0.8);
v2.tags = vec!["python".to_string()];
let mut v3 = make_version("t3", 1, "more rust", 0.7);
v3.tags = vec!["rust".to_string()];
store.insert(v1);
store.insert(v2);
store.insert(v3);
let rust_tiles = store.search_by_tag("rust");
assert_eq!(rust_tiles.len(), 2);
}
#[test]
fn test_cascade_log() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert(make_version("t2", 1, "v2", 0.8));
store.add_dependency("t2", "t1", 1.0);
store.invalidate("t1", "test", 5000);
assert_eq!(store.get_cascade_log().len(), 1);
assert_eq!(store.get_cascade_log()[0].reason, "test");
}
#[test]
fn test_tile_count_and_ids() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert(make_version("t2", 1, "v2", 0.8));
assert_eq!(store.tile_count(), 2);
assert_eq!(store.total_versions(), 2);
let ids = store.tile_ids();
assert!(ids.contains(&"t1".to_string()));
assert!(ids.contains(&"t2".to_string()));
}
#[test]
fn test_dependency_strength_clamped() {
let mut store = TileStore::new();
store.add_dependency("t2", "t1", 1.5);
let deps = store.get_dependents("t1");
assert_eq!(deps[0].strength, 1.0);
store.add_dependency("t3", "t1", -0.5);
let deps2 = store.get_dependents("t1");
assert_eq!(deps2[1].strength, 0.0);
}
#[test]
fn test_get_dependencies_of() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert(make_version("t2", 1, "v2", 0.8));
store.insert(make_version("t3", 1, "v3", 0.7));
store.add_dependency("t1", "t2", 1.0);
store.add_dependency("t1", "t3", 0.5);
let deps = store.get_dependencies_of("t1");
assert_eq!(deps.len(), 2);
}
#[test]
fn test_immutability_old_versions_preserved() {
let mut store = TileStore::new();
store.insert(make_version("t1", 1, "v1", 0.9));
store.insert_version(make_version("t1", 2, "v2", 0.8)).unwrap();
store.insert_version(make_version("t1", 3, "v3", 0.7)).unwrap();
assert_eq!(store.get_version("t1", 1).unwrap().content, "v1");
assert_eq!(store.get_version("t1", 2).unwrap().content, "v2");
}
}