//! Lifecycle command handlers for knowledge deprecation and migration tracking. use std::process::ExitCode; use chrono::{NaiveDate, Utc}; use uuid::Uuid; use aphoria::{ learning_store_dir, AphoriaConfig, DeprecatedUsage, KnowledgeStatus, LifecycleStore, LocalPatternStore, MigrationProgress, MigrationStore, PatternStore, StatusTransition, }; use crate::cli::{LifecycleCommands, MigrationCommands}; /// Handle lifecycle subcommands. pub async fn handle_lifecycle_command( command: LifecycleCommands, config: &AphoriaConfig, ) -> ExitCode { match command { LifecycleCommands::Deprecate { pattern_id, reason, superseded_by, sunset_date, migration_guide, } => { handle_deprecate( &pattern_id, &reason, superseded_by.as_deref(), sunset_date.as_deref(), migration_guide, config, ) .await } LifecycleCommands::Archive { pattern_id, reason } => { handle_archive(&pattern_id, &reason, config).await } LifecycleCommands::Reactivate { pattern_id, reason } => { handle_reactivate(&pattern_id, &reason, config).await } LifecycleCommands::History { pattern_id, format } => { handle_history(&pattern_id, &format, config).await } LifecycleCommands::List { status, overdue, format } => { handle_list(status.as_deref(), overdue, &format, config).await } } } /// Handle migrations subcommands. pub async fn handle_migrations_command( command: MigrationCommands, config: &AphoriaConfig, ) -> ExitCode { match command { MigrationCommands::Status { pattern, scope, format } => { handle_migration_status(pattern.as_deref(), scope.as_deref(), &format, config).await } MigrationCommands::Export { output, format, include_resolved } => { handle_migration_export(&output, &format, include_resolved, config).await } MigrationCommands::Blockers { pattern_id, scope } => { handle_migration_blockers(&pattern_id, scope.as_deref(), config).await } } } /// Deprecate a pattern. async fn handle_deprecate( pattern_id: &str, reason: &str, superseded_by: Option<&str>, sunset_date: Option<&str>, migration_guide: Option, _config: &AphoriaConfig, ) -> ExitCode { // Parse pattern ID let id = match Uuid::parse_str(pattern_id) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id, e); return ExitCode::from(1); } }; // Parse superseded_by if provided let superseded_by_id = if let Some(s) = superseded_by { match Uuid::parse_str(s) { Ok(id) => Some(id), Err(e) => { eprintln!("Invalid superseded-by ID '{}': {}", s, e); return ExitCode::from(1); } } } else { None }; // Parse sunset date if provided let sunset_datetime = if let Some(date_str) = sunset_date { match NaiveDate::parse_from_str(date_str, "%Y-%m-%d") { Ok(date) => { let datetime = date.and_hms_opt(23, 59, 59); datetime.map(|dt| chrono::TimeZone::from_utc_datetime(&Utc, &dt)) } Err(e) => { eprintln!("Invalid sunset date '{}': {}. Use YYYY-MM-DD format.", date_str, e); return ExitCode::from(1); } } } else { None }; // Load pattern store to verify pattern exists let pattern_store = match LocalPatternStore::new(&learning_store_dir()) { Ok(s) => s, Err(e) => { eprintln!("Failed to open pattern store: {}", e); return ExitCode::from(1); } }; let pattern = match pattern_store.get_pattern_by_id(&id) { Some(p) => p, None => { eprintln!("Pattern '{}' not found", pattern_id); return ExitCode::from(1); } }; // Create lifecycle store let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; // Get current status let current_status = lifecycle_store.get_current_status(&id).unwrap_or(KnowledgeStatus::Active); // Create new deprecated status let new_status = KnowledgeStatus::Deprecated { reason: reason.to_string(), superseded_by: superseded_by_id, sunset_date: sunset_datetime, migration_guide, }; // Record transition let transition = StatusTransition::new( id, current_status, new_status.clone(), whoami::username(), Some(format!("Deprecated: {}", reason)), ); if let Err(e) = lifecycle_store.record_transition(transition) { eprintln!("Failed to record transition: {}", e); return ExitCode::from(1); } // Display result println!("Pattern deprecated successfully"); println!(); println!(" Pattern ID: {}", id); println!(" Pattern Name: {}", pattern.claim_template.predicate); println!(" Reason: {}", reason); if let Some(s) = superseded_by_id { println!(" Superseded By: {}", s); } if let Some(date) = sunset_datetime { println!(" Sunset Date: {}", date.format("%Y-%m-%d")); } println!(); println!("Scans will now FLAG this pattern with migration guidance."); ExitCode::SUCCESS } /// Archive a pattern. async fn handle_archive(pattern_id: &str, reason: &str, _config: &AphoriaConfig) -> ExitCode { let id = match Uuid::parse_str(pattern_id) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id, e); return ExitCode::from(1); } }; // Load pattern store to verify pattern exists let pattern_store = match LocalPatternStore::new(&learning_store_dir()) { Ok(s) => s, Err(e) => { eprintln!("Failed to open pattern store: {}", e); return ExitCode::from(1); } }; if pattern_store.get_pattern_by_id(&id).is_none() { eprintln!("Pattern '{}' not found", pattern_id); return ExitCode::from(1); } // Create lifecycle store let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let current_status = lifecycle_store.get_current_status(&id).unwrap_or(KnowledgeStatus::Active); let new_status = KnowledgeStatus::Archived { archived_at: Utc::now(), reason: reason.to_string() }; let transition = StatusTransition::new( id, current_status, new_status, whoami::username(), Some(format!("Archived: {}", reason)), ); if let Err(e) = lifecycle_store.record_transition(transition) { eprintln!("Failed to record transition: {}", e); return ExitCode::from(1); } println!("Pattern archived successfully"); println!(); println!(" Pattern ID: {}", id); println!(" Reason: {}", reason); println!(); println!("Pattern will no longer match during scans."); ExitCode::SUCCESS } /// Reactivate a deprecated pattern. async fn handle_reactivate(pattern_id: &str, reason: &str, _config: &AphoriaConfig) -> ExitCode { let id = match Uuid::parse_str(pattern_id) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id, e); return ExitCode::from(1); } }; // Load pattern store let pattern_store = match LocalPatternStore::new(&learning_store_dir()) { Ok(s) => s, Err(e) => { eprintln!("Failed to open pattern store: {}", e); return ExitCode::from(1); } }; if pattern_store.get_pattern_by_id(&id).is_none() { eprintln!("Pattern '{}' not found", pattern_id); return ExitCode::from(1); } // Create lifecycle store let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let current_status = match lifecycle_store.get_current_status(&id) { Some(s) => s, None => { eprintln!("Pattern has no lifecycle history (already active)"); return ExitCode::from(1); } }; if !current_status.is_deprecated() { eprintln!("Pattern is not deprecated (status: {})", current_status.status_name()); return ExitCode::from(1); } let transition = StatusTransition::new( id, current_status, KnowledgeStatus::Active, whoami::username(), Some(format!("Reactivated: {}", reason)), ); if let Err(e) = lifecycle_store.record_transition(transition) { eprintln!("Failed to record transition: {}", e); return ExitCode::from(1); } println!("Pattern reactivated successfully"); println!(); println!(" Pattern ID: {}", id); println!(" Reason: {}", reason); println!(); println!("Pattern is now active and will match without deprecation warnings."); ExitCode::SUCCESS } /// Show lifecycle history for a pattern. async fn handle_history(pattern_id: &str, format: &str, _config: &AphoriaConfig) -> ExitCode { let id = match Uuid::parse_str(pattern_id) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id, e); return ExitCode::from(1); } }; let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let history = lifecycle_store.get_history(&id); if history.is_empty() { println!("No lifecycle history for pattern {}", pattern_id); println!(); println!("Pattern is in Active status (default)."); return ExitCode::SUCCESS; } match format { "json" => { let json = serde_json::to_string_pretty(&history).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { println!("Lifecycle History for {}", pattern_id); println!("{}", "=".repeat(60)); println!(); for transition in &history { let arrow = "→"; println!( "{} {} {} → {}", transition.timestamp.format("%Y-%m-%d %H:%M"), arrow, transition.from_status.status_name(), transition.to_status.status_name() ); println!(" By: {}", transition.initiated_by); if let Some(ref comment) = transition.comment { println!(" Comment: {}", comment); } println!(); } } } ExitCode::SUCCESS } /// List patterns by lifecycle status. async fn handle_list( status_filter: Option<&str>, overdue: bool, format: &str, _config: &AphoriaConfig, ) -> ExitCode { let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let pattern_store = match LocalPatternStore::new(&learning_store_dir()) { Ok(s) => s, Err(e) => { eprintln!("Failed to open pattern store: {}", e); return ExitCode::from(1); } }; // Collect patterns with their status let mut results: Vec<(Uuid, String, KnowledgeStatus)> = Vec::new(); // Get all patterns and their statuses for pattern in pattern_store.get_all_patterns() { let status = lifecycle_store.get_current_status(&pattern.id).unwrap_or(KnowledgeStatus::Active); // Apply filters if let Some(filter) = status_filter { if status.status_name() != filter { continue; } } if overdue && !status.is_past_sunset() { continue; } results.push((pattern.id, pattern.claim_template.predicate.clone(), status)); } if results.is_empty() { println!("No patterns found matching criteria."); return ExitCode::SUCCESS; } match format { "json" => { let json_data: Vec = results .iter() .map(|(id, name, status)| { serde_json::json!({ "id": id.to_string(), "name": name, "status": status.status_name(), "days_until_sunset": status.days_until_sunset(), }) }) .collect(); let json = serde_json::to_string_pretty(&json_data).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { println!("Patterns by Lifecycle Status"); println!("{}", "=".repeat(60)); println!(); for (id, name, status) in &results { let sunset_info = status .days_until_sunset() .map(|d| { if d < 0 { format!(" (OVERDUE by {} days)", -d) } else { format!(" ({} days until sunset)", d) } }) .unwrap_or_default(); println!("{:<40} {:<12} {}", name, status.status_name(), sunset_info); println!(" {}", id); } println!(); println!("Total: {} patterns", results.len()); } } ExitCode::SUCCESS } /// Show migration status for deprecated patterns. async fn handle_migration_status( pattern_filter: Option<&str>, _scope_filter: Option<&str>, format: &str, _config: &AphoriaConfig, ) -> ExitCode { let migration_store = match MigrationStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open migration store: {}", e); return ExitCode::from(1); } }; let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let pattern_store = match LocalPatternStore::new(&learning_store_dir()) { Ok(s) => s, Err(e) => { eprintln!("Failed to open pattern store: {}", e); return ExitCode::from(1); } }; // Get deprecated patterns let deprecated = lifecycle_store.get_deprecated_patterns(); if deprecated.is_empty() { println!("No deprecated patterns found."); return ExitCode::SUCCESS; } let mut progress_list: Vec = Vec::new(); for (pattern_id, _status) in &deprecated { // Apply pattern filter if specified if let Some(filter) = pattern_filter { if let Ok(filter_id) = Uuid::parse_str(filter) { if pattern_id != &filter_id { continue; } } } // Get pattern name let pattern_name = pattern_store .get_pattern_by_id(pattern_id) .map(|p| p.claim_template.predicate.clone()) .unwrap_or_else(|| "Unknown".to_string()); let progress = migration_store.get_progress(pattern_id, &pattern_name); progress_list.push(progress); } if progress_list.is_empty() { println!("No migration data found."); return ExitCode::SUCCESS; } match format { "json" => { let json = serde_json::to_string_pretty(&progress_list).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { println!("Migration Status"); println!("{}", "=".repeat(70)); println!(); println!("{:<30} {:>8} {:>8} {:>10}", "Pattern", "Total", "Resolved", "Progress"); println!("{}", "-".repeat(70)); for progress in &progress_list { println!( "{:<30} {:>8} {:>8} {:>9.1}%", truncate(&progress.pattern_name, 30), progress.total_usages, progress.resolved_usages, progress.completion_percent() ); } println!(); let total_usages: usize = progress_list.iter().map(|p| p.total_usages).sum(); let total_resolved: usize = progress_list.iter().map(|p| p.resolved_usages).sum(); let overall_percent = if total_usages > 0 { (total_resolved as f32 / total_usages as f32) * 100.0 } else { 100.0 }; println!( "Overall: {} of {} usages resolved ({:.1}%)", total_resolved, total_usages, overall_percent ); } } ExitCode::SUCCESS } /// Export migration data. async fn handle_migration_export( output: &std::path::Path, format: &str, include_resolved: bool, _config: &AphoriaConfig, ) -> ExitCode { let migration_store = match MigrationStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open migration store: {}", e); return ExitCode::from(1); } }; let content = match format { "csv" => migration_store.export_csv(include_resolved), "json" => { // For JSON, we need to manually build the data let lifecycle_store = match LifecycleStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open lifecycle store: {}", e); return ExitCode::from(1); } }; let deprecated = lifecycle_store.get_deprecated_patterns(); let mut all_usages: Vec = Vec::new(); for (pattern_id, _) in deprecated { let usages = if include_resolved { migration_store.get_usages(&pattern_id) } else { migration_store.get_pending_usages(&pattern_id) }; all_usages.extend(usages); } serde_json::to_string_pretty(&all_usages).unwrap_or_else(|_| "[]".to_string()) } _ => { eprintln!("Unknown format '{}'. Use 'csv' or 'json'.", format); return ExitCode::from(1); } }; if let Err(e) = std::fs::write(output, content) { eprintln!("Failed to write to {}: {}", output.display(), e); return ExitCode::from(1); } println!("Exported migration data to {}", output.display()); ExitCode::SUCCESS } /// Show migration blockers for a pattern. async fn handle_migration_blockers( pattern_id: &str, _scope_filter: Option<&str>, _config: &AphoriaConfig, ) -> ExitCode { let id = match Uuid::parse_str(pattern_id) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id, e); return ExitCode::from(1); } }; let migration_store = match MigrationStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open migration store: {}", e); return ExitCode::from(1); } }; let pending = migration_store.get_pending_usages(&id); if pending.is_empty() { println!("No pending usages found for pattern {}", pattern_id); println!(); println!("Migration is complete."); return ExitCode::SUCCESS; } println!("Migration Blockers for {}", pattern_id); println!("{}", "=".repeat(70)); println!(); for usage in &pending { println!("{}:{}", usage.file_path, usage.line); println!(" Project: {}", &usage.project_hash[..8.min(usage.project_hash.len())]); println!(" First seen: {}", usage.first_detected.format("%Y-%m-%d")); println!(" Last seen: {}", usage.last_detected.format("%Y-%m-%d")); println!(); } println!("Total blockers: {}", pending.len()); ExitCode::SUCCESS } /// Truncate a string to a maximum length. fn truncate(s: &str, max: usize) -> String { if s.len() <= max { s.to_string() } else { format!("{}...", &s[..max - 3]) } }