/// Community domain model for temporal graph-RAG. /// Single Responsibility: Community (cluster) storage and metadata. /// Open/Closed: Algorithm field extensible for new clustering methods. use serde::{Deserialize, Serialize}; use time::OffsetDateTime; /// Community: Cluster of related entities with summary. /// Dependency Inversion: Depends on abstractions (String for id, OffsetDateTime for time). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Community { pub id: String, // UUID as string pub project_id: String, // Identity pub name: String, pub name_embedding: Option>, pub keywords: Vec, pub summary: Option, pub summary_embedding: Option>, // Temporal #[serde(with = "time::serde::rfc3339")] pub t_created: OffsetDateTime, #[serde(with = "time::serde::rfc3339::option")] pub t_refreshed: Option, // Stats pub member_count: i32, pub edge_count: i32, // Algorithm metadata pub algorithm: String, pub version: i32, } impl Community { /// Create new community with minimal fields. pub fn new(project_id: &str, name: &str) -> Self { Self { id: uuid::Uuid::new_v4().to_string(), project_id: project_id.to_string(), name: name.to_string(), name_embedding: None, keywords: vec![], summary: None, summary_embedding: None, t_created: OffsetDateTime::now_utc(), t_refreshed: None, member_count: 0, edge_count: 0, algorithm: "label_propagation".to_string(), version: 1, } } /// Does this community need refresh (exceeds max age)? /// Used in T3.4 cronjob: weekly refresh. pub fn needs_refresh(&self, max_age_hours: i64) -> bool { match self.t_refreshed { Some(t) => { let duration = std::time::Duration::from_secs((max_age_hours * 3600) as u64); OffsetDateTime::now_utc() - t > duration } None => true, // Never refreshed } } /// Builder pattern: Set summary. pub fn with_summary(mut self, summary: &str) -> Self { self.summary = Some(summary.to_string()); self } /// Builder pattern: Set keywords. pub fn with_keywords(mut self, keywords: Vec) -> Self { self.keywords = keywords; self } /// Builder pattern: Set embeddings. pub fn with_name_embedding(mut self, embedding: Vec) -> Self { self.name_embedding = Some(embedding); self } pub fn with_summary_embedding(mut self, embedding: Vec) -> Self { self.summary_embedding = Some(embedding); self } /// Builder pattern: Set algorithm. pub fn with_algorithm(mut self, algorithm: &str) -> Self { self.algorithm = algorithm.to_string(); self } /// DRY: Normalized name for deduplication. pub fn name_normalized(&self) -> String { self.name.to_lowercase().trim().to_string() } /// Update member count (called from T3.1-T3.2 compaction). pub fn set_member_count(&mut self, count: i32) { self.member_count = count; } /// Update edge count (called from T3.1-T3.2 compaction). pub fn set_edge_count(&mut self, count: i32) { self.edge_count = count; } /// Mark community as refreshed (called from T4.2-T4.3). pub fn mark_refreshed(&mut self) { self.t_refreshed = Some(OffsetDateTime::now_utc()); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_community_creation() { let community = Community::new("proj1", "Kubernetes Experts"); assert_eq!(community.name, "Kubernetes Experts"); assert_eq!(community.algorithm, "label_propagation"); assert_eq!(community.member_count, 0); } #[test] fn test_community_needs_refresh() { let mut community = Community::new("proj1", "Test"); // Never refreshed should return true assert!(community.needs_refresh(24)); // Mark as refreshed community.mark_refreshed(); // Should not need refresh immediately assert!(!community.needs_refresh(24)); } #[test] fn test_community_builder_pattern() { let community = Community::new("proj1", "Cloud Native") .with_summary("Entities related to cloud-native technologies") .with_keywords(vec![ "kubernetes".to_string(), "docker".to_string(), ]) .with_algorithm("louvain"); assert_eq!( community.summary, Some("Entities related to cloud-native technologies".to_string()) ); assert_eq!(community.keywords.len(), 2); assert_eq!(community.algorithm, "louvain"); } #[test] fn test_community_normalized_name() { let community = Community::new("proj1", " Kubernetes EXPERTS "); assert_eq!(community.name_normalized(), "kubernetes experts"); } #[test] fn test_community_stats_update() { let mut community = Community::new("proj1", "Test"); community.set_member_count(42); community.set_edge_count(156); assert_eq!(community.member_count, 42); assert_eq!(community.edge_count, 156); } #[test] fn test_community_serialization() { let community = Community::new("proj1", "Test") .with_keywords(vec!["k8s".to_string()]); let json = serde_json::to_string(&community).unwrap(); let deserialized: Community = serde_json::from_str(&json).unwrap(); assert_eq!(community.name, deserialized.name); assert_eq!(community.keywords, deserialized.keywords); } }