anya_core/tools/
source_of_truth_registry.rs

1// Canonical Source of Truth Registry Implementation
2// This module implements the PRD requirements for work item tracking
3// and duplication elimination enforcement
4
5use blake3;
6use dashmap::DashMap; // Removed unused DashSet import
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{
10    atomic::{AtomicU32, AtomicU64, Ordering},
11    Arc,
12};
13use std::time::{SystemTime, UNIX_EPOCH};
14use thiserror::Error;
15use tokio::fs;
16use tokio::io::{AsyncReadExt, AsyncWriteExt};
17
18/// Errors related to Source of Truth Registry operations
19#[derive(Debug, Error)]
20pub enum SourceOfTruthError {
21    #[error("Work item not found: {0}")]
22    WorkItemNotFound(String),
23
24    #[error("Duplication detected: {0}")]
25    DuplicationDetected(String),
26
27    #[error("Invalid work item ID format: {0}")]
28    InvalidWorkItemId(String),
29
30    #[error("Canonical document conflict: {0}")]
31    CanonicalConflict(String),
32
33    #[error("Registry corruption detected: {0}")]
34    RegistryCorruption(String),
35
36    #[error("IO error: {0}")]
37    IoError(#[from] std::io::Error),
38
39    #[error("Serialization error: {0}")]
40    SerializationError(#[from] serde_json::Error),
41}
42
43/// Work item status tracking
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
45pub enum WorkStatus {
46    Planning,
47    InProgress,
48    CodeReview,
49    Testing,
50    Completed,
51    Blocked(String), // Reason for blocking
52}
53
54/// Duplication check status
55#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
56pub enum DuplicationCheckStatus {
57    Passed,
58    Failed(String), // Duplication details
59    NotChecked,
60}
61
62/// Canonical document status
63#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
64pub enum CanonicalStatus {
65    Draft,
66    Review,
67    Verified,
68    Deprecated,
69}
70
71/// Work item tracking structure
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct WorkItem {
74    pub id: String,
75    pub title: String,
76    pub status: WorkStatus,
77    pub component: String,
78    pub files_modified: Vec<String>,
79    pub duplication_check: DuplicationCheckStatus,
80    pub source_of_truth_updated: bool,
81    pub verification_hash: [u8; 32],
82    pub completion_timestamp: Option<u64>,
83    pub evidence_link: String,
84    pub dependencies: Vec<String>,
85    pub blockers: Vec<String>,
86    pub created: u64,
87    pub last_updated: u64,
88}
89
90/// Canonical document entry
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct CanonicalDocument {
93    pub file_path: String,
94    pub work_item_id: String,
95    pub verification_hash: [u8; 32],
96    pub last_updated: u64,
97    pub canonical_status: CanonicalStatus,
98    pub authority_level: u8, // 1-10, 10 being highest authority
99}
100
101/// Source of Truth Registry entry
102#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct SourceOfTruthEntry {
104    pub file_path: String,
105    pub work_item_id: String,
106    pub verification_hash: [u8; 32],
107    pub last_updated: u64,
108    pub canonical_status: CanonicalStatus,
109}
110
111/// Duplication detection entry
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct DuplicationEntry {
114    pub content_hash: [u8; 32],
115    pub file_path: String,
116    pub function_signature: Option<String>,
117    pub first_occurrence: u64, // timestamp
118}
119
120/// Function signature for duplication detection
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct FunctionSignature {
123    pub name: String,
124    pub parameters: Vec<String>,
125    pub return_type: Option<String>,
126    pub visibility: String,
127    pub file_path: String,
128    pub line_number: u32,
129}
130
131/// Code fingerprint for advanced duplication detection
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct CodeFingerprint {
134    pub content_hash: [u8; 32],
135    pub normalized_hash: [u8; 32], // Hash after removing whitespace/comments
136    pub function_count: u32,
137    pub line_count: u32,
138    pub file_path: String,
139}
140
141/// Documentation entry for duplication checking
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct DocumentationEntry {
144    pub content_hash: [u8; 32],
145    pub title: String,
146    pub file_path: String,
147    pub section: String,
148}
149
150/// Main Source of Truth Registry
151#[derive(Debug)]
152pub struct SourceOfTruthRegistry {
153    /// Registry of all canonical documents
154    canonical_documents: DashMap<String, CanonicalDocument>,
155    /// Work item tracking
156    work_items: DashMap<String, WorkItem>,
157    /// Duplication prevention index
158    duplication_index: DashMap<String, DuplicationEntry>,
159    /// Function signature index
160    function_signatures: DashMap<String, FunctionSignature>,
161    /// Code fingerprint index
162    #[allow(dead_code)]
163    code_fingerprints: DashMap<String, CodeFingerprint>,
164    /// Documentation content index
165    #[allow(dead_code)]
166    documentation_index: DashMap<String, DocumentationEntry>,
167    /// Last registry update
168    last_updated: AtomicU64,
169    /// Registry version
170    version: AtomicU32,
171    /// Registry file path
172    registry_path: String,
173}
174
175impl SourceOfTruthRegistry {
176    /// Create new registry instance
177    pub async fn new(registry_path: String) -> Result<Self, SourceOfTruthError> {
178        let registry = Self {
179            canonical_documents: DashMap::new(),
180            work_items: DashMap::new(),
181            duplication_index: DashMap::new(),
182            function_signatures: DashMap::new(),
183            code_fingerprints: DashMap::new(),
184            documentation_index: DashMap::new(),
185            last_updated: AtomicU64::new(Self::current_timestamp()),
186            version: AtomicU32::new(1),
187            registry_path,
188        };
189
190        // Create registry directory if it doesn't exist
191        if let Some(parent) = std::path::Path::new(&registry.registry_path).parent() {
192            fs::create_dir_all(parent).await?;
193        }
194
195        // Load existing registry if it exists
196        if registry.load_from_disk().await.is_err() {
197            // If loading fails, initialize with empty registry
198            registry.save_to_disk().await?;
199        }
200
201        Ok(registry)
202    }
203
204    /// Generate unique work item ID
205    pub fn generate_work_item_id(&self) -> String {
206        let now = chrono::Utc::now();
207        let date_prefix = now.format("%Y-%m-%d").to_string();
208
209        // Find next sequence number for today
210        let mut sequence = 1;
211        loop {
212            let candidate_id = format!("WI-{date_prefix}-{sequence}");
213            if !self.work_items.contains_key(&candidate_id) {
214                return candidate_id;
215            }
216            sequence += 1;
217        }
218    }
219
220    /// Create new work item with comprehensive validation
221    pub async fn create_work_item(
222        &self,
223        title: String,
224        component: String,
225    ) -> Result<WorkItem, SourceOfTruthError> {
226        // 1. Generate unique ID
227        let work_id = self.generate_work_item_id();
228
229        // 2. Run pre-work duplication check
230        let duplication_status = self.check_work_item_duplication(&title, &component).await?;
231        if matches!(duplication_status, DuplicationCheckStatus::Failed(_)) {
232            return Err(SourceOfTruthError::DuplicationDetected(format!(
233                "Work item title or component already exists: {title}"
234            )));
235        }
236
237        // 3. Create work item
238        let work_item = WorkItem {
239            id: work_id.clone(),
240            title,
241            status: WorkStatus::Planning,
242            component,
243            files_modified: Vec::new(),
244            duplication_check: duplication_status,
245            source_of_truth_updated: false,
246            verification_hash: [0u8; 32],
247            completion_timestamp: None,
248            evidence_link: String::new(),
249            dependencies: Vec::new(),
250            blockers: Vec::new(),
251            created: Self::current_timestamp(),
252            last_updated: Self::current_timestamp(),
253        };
254
255        // 4. Register in registry
256        self.work_items.insert(work_id.clone(), work_item.clone());
257        self.update_last_modified();
258
259        // 5. Save to disk
260        self.save_to_disk().await?;
261
262        Ok(work_item)
263    }
264
265    /// Update work item status with validation
266    pub async fn update_work_item_status(
267        &self,
268        work_id: &str,
269        new_status: WorkStatus,
270    ) -> Result<(), SourceOfTruthError> {
271        use log::debug;
272        debug!("update_work_item_status called for work_id: {work_id}, new_status: {new_status:?}");
273        // 1. Validate work item exists
274        let mut work_item = match self.work_items.get_mut(work_id) {
275            Some(item) => item,
276            None => {
277                debug!("Work item not found: {work_id}");
278                return Err(SourceOfTruthError::WorkItemNotFound(work_id.to_string()));
279            }
280        };
281
282        // 2. Validate status transition
283        if let Err(e) = self.validate_status_transition(&work_item.status, &new_status) {
284            debug!("Invalid status transition: {e:?}");
285            return Err(e);
286        }
287
288        // 3. Update work item
289        work_item.status = new_status.clone();
290        work_item.last_updated = Self::current_timestamp();
291
292        // 4. Handle completion
293        if matches!(new_status, WorkStatus::Completed) {
294            work_item.completion_timestamp = Some(Self::current_timestamp());
295            work_item.verification_hash = self.generate_verification_hash(&work_item).await?;
296            work_item.source_of_truth_updated = true;
297        }
298
299        self.update_last_modified();
300        self.save_to_disk().await?;
301
302        debug!("update_work_item_status completed for work_id: {work_id}");
303        Ok(())
304    }
305
306    /// Comprehensive duplication check for code files
307    pub async fn check_code_duplication(
308        &self,
309        file_path: &str,
310        content: &str,
311    ) -> Result<DuplicationCheckStatus, SourceOfTruthError> {
312        // 1. Generate content fingerprint
313        let content_hash = blake3::hash(content.as_bytes()).into();
314
315        // 2. Check for exact content duplication
316        for entry in self.duplication_index.iter() {
317            if entry.value().content_hash == content_hash && entry.value().file_path != file_path {
318                return Ok(DuplicationCheckStatus::Failed(format!(
319                    "Exact content duplication found in {}",
320                    entry.value().file_path
321                )));
322            }
323        }
324
325        // 3. Check function signature duplication
326        let functions = self.extract_rust_functions(content)?;
327        for function in functions {
328            let signature_key = format!("{}::{}", function.name, function.parameters.join(","));
329            if let Some(existing) = self.function_signatures.get(&signature_key) {
330                if existing.file_path != file_path {
331                    return Ok(DuplicationCheckStatus::Failed(format!(
332                        "Function signature duplication: {} in {}",
333                        signature_key, existing.file_path
334                    )));
335                }
336            }
337        }
338
339        // 4. Update indexes
340        self.duplication_index.insert(
341            file_path.to_string(),
342            DuplicationEntry {
343                content_hash,
344                file_path: file_path.to_string(),
345                function_signature: None,
346                first_occurrence: Self::current_timestamp(),
347            },
348        );
349
350        Ok(DuplicationCheckStatus::Passed)
351    }
352
353    /// Check for work item title/component duplication
354    async fn check_work_item_duplication(
355        &self,
356        title: &str,
357        component: &str,
358    ) -> Result<DuplicationCheckStatus, SourceOfTruthError> {
359        for item in self.work_items.iter() {
360            let work_item = item.value();
361            if work_item.title == title && work_item.component == component {
362                return Ok(DuplicationCheckStatus::Failed(format!(
363                    "Duplicate work item: {title} in {component}"
364                )));
365            }
366        }
367        Ok(DuplicationCheckStatus::Passed)
368    }
369
370    /// Extract Rust function signatures from source code
371    fn extract_rust_functions(
372        &self,
373        content: &str,
374    ) -> Result<Vec<FunctionSignature>, SourceOfTruthError> {
375        let mut functions = Vec::new();
376        let lines: Vec<&str> = content.lines().collect();
377
378        for (line_num, line) in lines.iter().enumerate() {
379            if let Some(func) = self.parse_rust_function_signature(line, line_num as u32 + 1) {
380                functions.push(func);
381            }
382        }
383
384        Ok(functions)
385    }
386
387    /// Parse a single Rust function signature
388    fn parse_rust_function_signature(
389        &self,
390        line: &str,
391        line_number: u32,
392    ) -> Option<FunctionSignature> {
393        let trimmed = line.trim();
394
395        // Simple regex-like parsing for Rust functions
396        if trimmed.starts_with("pub fn ") || trimmed.starts_with("fn ") {
397            let visibility = if trimmed.starts_with("pub ") {
398                "pub"
399            } else {
400                "private"
401            };
402
403            // Extract function name and parameters (simplified)
404            if let Some(paren_start) = trimmed.find('(') {
405                if let Some(fn_start) = trimmed.find("fn ") {
406                    let name_start = fn_start + 3;
407                    let name = trimmed[name_start..paren_start].trim().to_string();
408
409                    // Extract parameters (simplified - just parameter names)
410                    if let Some(paren_end) = trimmed.find(')') {
411                        let param_str = &trimmed[paren_start + 1..paren_end];
412                        let parameters: Vec<String> = param_str
413                            .split(',')
414                            .map(|p| p.trim().split(':').next().unwrap_or("").trim().to_string())
415                            .filter(|p| !p.is_empty())
416                            .collect();
417
418                        // Extract return type
419                        let return_type = trimmed.find("->").map(|arrow_pos| {
420                            trimmed[arrow_pos + 2..]
421                                .split_whitespace()
422                                .next()
423                                .unwrap_or("")
424                                .to_string()
425                        });
426
427                        return Some(FunctionSignature {
428                            name,
429                            parameters,
430                            return_type,
431                            visibility: visibility.to_string(),
432                            file_path: String::new(), // Set by caller
433                            line_number,
434                        });
435                    }
436                }
437            }
438        }
439
440        None
441    }
442
443    /// Validate status transition is allowed
444    fn validate_status_transition(
445        &self,
446        current: &WorkStatus,
447        new: &WorkStatus,
448    ) -> Result<(), SourceOfTruthError> {
449        use log::debug;
450        match (current, new) {
451            (WorkStatus::Planning, WorkStatus::InProgress) => Ok(()),
452            (WorkStatus::InProgress, WorkStatus::CodeReview) => Ok(()),
453            (WorkStatus::CodeReview, WorkStatus::Testing) => Ok(()),
454            (WorkStatus::Testing, WorkStatus::Completed) => Ok(()),
455            (_, WorkStatus::Blocked(_)) => Ok(()), // Can always be blocked
456            (WorkStatus::Blocked(_), _) => Ok(()), // Can transition from blocked to any state
457            _ => {
458                debug!("Invalid status transition from {current:?} to {new:?}");
459                Err(SourceOfTruthError::InvalidWorkItemId(format!(
460                    "Invalid status transition from {current:?} to {new:?}"
461                )))
462            }
463        }
464    }
465
466    /// Generate verification hash for completed work item
467    async fn generate_verification_hash(
468        &self,
469        work_item: &WorkItem,
470    ) -> Result<[u8; 32], SourceOfTruthError> {
471        let mut hasher = blake3::Hasher::new();
472        hasher.update(work_item.id.as_bytes());
473        hasher.update(work_item.title.as_bytes());
474        hasher.update(&work_item.completion_timestamp.unwrap_or(0).to_le_bytes());
475
476        // Include hash of all modified files
477        for file_path in &work_item.files_modified {
478            if let Ok(content) = fs::read_to_string(file_path).await {
479                hasher.update(content.as_bytes());
480            }
481        }
482
483        Ok(hasher.finalize().into())
484    }
485
486    /// Get current timestamp in nanoseconds
487    fn current_timestamp() -> u64 {
488        SystemTime::now()
489            .duration_since(UNIX_EPOCH)
490            .unwrap()
491            .as_nanos() as u64
492    }
493
494    /// Update last modified timestamp
495    fn update_last_modified(&self) {
496        self.last_updated
497            .store(Self::current_timestamp(), Ordering::Relaxed);
498        self.version.fetch_add(1, Ordering::Relaxed);
499    }
500
501    /// Save registry to disk
502    async fn save_to_disk(&self) -> Result<(), SourceOfTruthError> {
503        // Create a serializable version of the registry
504        let registry_data = RegistryData {
505            canonical_documents: self
506                .canonical_documents
507                .iter()
508                .map(|entry| (entry.key().clone(), entry.value().clone()))
509                .collect(),
510            work_items: self
511                .work_items
512                .iter()
513                .map(|entry| (entry.key().clone(), entry.value().clone()))
514                .collect(),
515            duplication_index: self
516                .duplication_index
517                .iter()
518                .map(|entry| (entry.key().clone(), entry.value().clone()))
519                .collect(),
520            last_updated: self.last_updated.load(Ordering::Relaxed),
521            version: self.version.load(Ordering::Relaxed),
522        };
523
524        let json_data = serde_json::to_string_pretty(&registry_data)?;
525        let mut file = fs::File::create(&self.registry_path).await?;
526        file.write_all(json_data.as_bytes()).await?;
527
528        Ok(())
529    }
530
531    /// Load registry from disk
532    async fn load_from_disk(&self) -> Result<(), SourceOfTruthError> {
533        let mut file = fs::File::open(&self.registry_path).await?;
534        let mut contents = String::new();
535        file.read_to_string(&mut contents).await?;
536
537        let registry_data: RegistryData = serde_json::from_str(&contents)?;
538
539        // Clear existing data
540        self.canonical_documents.clear();
541        self.work_items.clear();
542        self.duplication_index.clear();
543
544        // Load data
545        for (key, value) in registry_data.canonical_documents {
546            self.canonical_documents.insert(key, value);
547        }
548        for (key, value) in registry_data.work_items {
549            self.work_items.insert(key, value);
550        }
551        for (key, value) in registry_data.duplication_index {
552            self.duplication_index.insert(key, value);
553        }
554
555        self.last_updated
556            .store(registry_data.last_updated, Ordering::Relaxed);
557        self.version.store(registry_data.version, Ordering::Relaxed);
558
559        Ok(())
560    }
561}
562
563/// Serializable registry data structure
564#[derive(Debug, Serialize, Deserialize)]
565struct RegistryData {
566    canonical_documents: HashMap<String, CanonicalDocument>,
567    work_items: HashMap<String, WorkItem>,
568    duplication_index: HashMap<String, DuplicationEntry>,
569    last_updated: u64,
570    version: u32,
571}
572
573/// Global registry instance
574static GLOBAL_REGISTRY: once_cell::sync::Lazy<
575    Arc<tokio::sync::RwLock<Option<SourceOfTruthRegistry>>>,
576> = once_cell::sync::Lazy::new(|| Arc::new(tokio::sync::RwLock::new(None)));
577
578/// Initialize global registry
579pub async fn initialize_global_registry(registry_path: String) -> Result<(), SourceOfTruthError> {
580    let registry = SourceOfTruthRegistry::new(registry_path).await?;
581    let mut global = GLOBAL_REGISTRY.write().await;
582    *global = Some(registry);
583    Ok(())
584}
585
586/// Get reference to global registry
587pub async fn get_global_registry() -> Arc<tokio::sync::RwLock<Option<SourceOfTruthRegistry>>> {
588    GLOBAL_REGISTRY.clone()
589}
590
591#[cfg(test)]
592mod tests {
593    use super::*;
594    use tempfile::tempdir;
595
596    #[tokio::test]
597    async fn test_work_item_creation() {
598        let temp_dir = tempdir().unwrap();
599        let registry_path = temp_dir
600            .path()
601            .join("registry.json")
602            .to_string_lossy()
603            .to_string();
604
605        let registry = SourceOfTruthRegistry::new(registry_path).await.unwrap();
606
607        let work_item = registry
608            .create_work_item("Test work item".to_string(), "test_component".to_string())
609            .await
610            .unwrap();
611
612        assert!(work_item.id.starts_with("WI-"));
613        assert_eq!(work_item.status, WorkStatus::Planning);
614        assert_eq!(work_item.duplication_check, DuplicationCheckStatus::Passed);
615    }
616
617    #[tokio::test]
618    async fn test_duplication_detection() {
619        let temp_dir = tempdir().unwrap();
620        let registry_path = temp_dir
621            .path()
622            .join("registry.json")
623            .to_string_lossy()
624            .to_string();
625
626        let registry = SourceOfTruthRegistry::new(registry_path).await.unwrap();
627
628        // First work item should pass
629        let _work_item1 = registry
630            .create_work_item("Unique title".to_string(), "component1".to_string())
631            .await
632            .unwrap();
633
634        // Duplicate should fail
635        let result = registry
636            .create_work_item("Unique title".to_string(), "component1".to_string())
637            .await;
638
639        assert!(result.is_err());
640        assert!(matches!(
641            result.unwrap_err(),
642            SourceOfTruthError::DuplicationDetected(_)
643        ));
644    }
645
646    #[tokio::test]
647    async fn test_status_transition_valid() {
648        use tokio::time::{timeout, Duration};
649        use log::debug;
650        let temp_dir = tempdir().unwrap();
651        let registry_path = temp_dir
652            .path()
653            .join("registry_valid.json")
654            .to_string_lossy()
655            .to_string();
656
657        let registry = SourceOfTruthRegistry::new(registry_path).await.unwrap();
658        let work_item = registry
659            .create_work_item("Status test valid".to_string(), "test_component".to_string())
660            .await
661            .unwrap();
662
663        let valid = timeout(Duration::from_secs(10), registry.update_work_item_status(&work_item.id, WorkStatus::InProgress)).await;
664        match valid {
665            Ok(Ok(_)) => debug!("Valid status transition succeeded"),
666            Ok(Err(e)) => panic!("Valid status transition failed: {e:?}"),
667            Err(_) => panic!("Timeout on valid status transition"),
668        }
669    }
670
671    #[tokio::test]
672    async fn test_status_transition_invalid() {
673        use tokio::time::{timeout, Duration};
674        use log::debug;
675        let temp_dir = tempdir().unwrap();
676        let registry_path = temp_dir
677            .path()
678            .join("registry_invalid.json")
679            .to_string_lossy()
680            .to_string();
681
682        let registry = SourceOfTruthRegistry::new(registry_path).await.unwrap();
683        let work_item = registry
684            .create_work_item("Status test invalid".to_string(), "test_component".to_string())
685            .await
686            .unwrap();
687
688        // Try invalid transition: Planning -> Completed (should fail)
689        let invalid = timeout(Duration::from_secs(10), registry.update_work_item_status(&work_item.id, WorkStatus::Completed)).await;
690        match invalid {
691            Ok(Ok(_)) => panic!("Invalid status transition unexpectedly succeeded"),
692            Ok(Err(_)) => debug!("Invalid status transition correctly failed"),
693            Err(_) => panic!("Timeout on invalid status transition"),
694        }
695    }
696}