//! Governance command handlers for approval workflows and audit trails. use std::path::Path; use std::process::ExitCode; use chrono::{NaiveDate, TimeZone, Utc}; use uuid::Uuid; use aphoria::{ learning_store_dir, AphoriaConfig, ApprovalRequest, ApprovalStatus, AuditTrail, ExportFormat, GovernanceStateMachine, GovernanceStore, LocalPatternStore, PatternStore, }; use crate::cli::{AuditCommands, GovernanceCommands}; /// Handle governance subcommands. pub async fn handle_governance_command( command: GovernanceCommands, config: &AphoriaConfig, ) -> ExitCode { if !config.governance.enabled && !matches!(command, GovernanceCommands::Status { .. }) { eprintln!("Governance is not enabled. Add [governance] enabled = true to aphoria.toml"); return ExitCode::from(1); } match command { GovernanceCommands::Pending { workflow, format } => { handle_pending(workflow.as_deref(), &format, config).await } GovernanceCommands::Approve { id, comment } => handle_approve(&id, comment, config).await, GovernanceCommands::Reject { id, reason } => handle_reject(&id, &reason, config).await, GovernanceCommands::Escalate { id } => handle_escalate(&id, config).await, GovernanceCommands::Status { pattern, all, format } => { handle_status(pattern.as_deref(), all, &format, config).await } GovernanceCommands::CheckTimeouts => handle_check_timeouts(config).await, GovernanceCommands::Create { pattern_id, workflow } => { handle_create(&pattern_id, workflow.as_deref(), config).await } } } /// Handle audit subcommands. pub async fn handle_audit_command(command: AuditCommands, config: &AphoriaConfig) -> ExitCode { match command { AuditCommands::Trail { pattern, format } => handle_trail(&pattern, &format, config).await, AuditCommands::Export { output, format, date_range } => { handle_export(&output, &format, date_range.as_deref(), config).await } AuditCommands::Summary { format } => handle_summary(&format, config).await, } } /// List pending approval requests. async fn handle_pending( workflow_filter: Option<&str>, format: &str, config: &AphoriaConfig, ) -> ExitCode { let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; // Check timeouts if configured if config.governance.check_timeouts_on_access { let _ = sm.check_timeouts(); } let pending = match sm.list_pending() { Ok(p) => p, Err(e) => { eprintln!("Failed to list pending requests: {}", e); return ExitCode::from(1); } }; // Filter by workflow if specified let filtered: Vec<_> = if let Some(wf) = workflow_filter { pending.into_iter().filter(|r| r.workflow_name == wf).collect() } else { pending }; if filtered.is_empty() { println!("No pending approval requests."); return ExitCode::SUCCESS; } match format { "json" => { let json = serde_json::to_string_pretty(&filtered).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { println!("Pending Approval Requests"); println!("{}", "=".repeat(80)); println!(); println!("{:<36} {:<20} {:<15} {:<10}", "Request ID", "Pattern", "Stage", "Days"); println!("{}", "-".repeat(80)); for request in &filtered { let stage = request.status.current_stage().unwrap_or("-"); let days = (Utc::now() - request.created_at).num_days(); let pattern_name = truncate(&request.pattern_name, 20); println!("{:<36} {:<20} {:<15} {:<10}", request.id, pattern_name, stage, days); } println!(); println!("Total: {} pending requests", filtered.len()); } } ExitCode::SUCCESS } /// Approve a pending request. async fn handle_approve(id: &str, comment: Option, config: &AphoriaConfig) -> ExitCode { let request_id = match Uuid::parse_str(id) { Ok(id) => id, Err(e) => { eprintln!("Invalid request ID '{}': {}", id, e); return ExitCode::from(1); } }; let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let approver = whoami::username(); match sm.approve(request_id, &approver, comment) { Ok(request) => { println!("Approved successfully"); println!(); println!(" Request ID: {}", request.id); println!(" Pattern: {}", request.pattern_name); println!(" Status: {}", request.status); if request.status.is_approved() { println!(); println!("Workflow complete. Pattern can now be promoted."); } ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to approve: {}", e); ExitCode::from(1) } } } /// Reject a pending request. async fn handle_reject(id: &str, reason: &str, config: &AphoriaConfig) -> ExitCode { let request_id = match Uuid::parse_str(id) { Ok(id) => id, Err(e) => { eprintln!("Invalid request ID '{}': {}", id, e); return ExitCode::from(1); } }; let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let approver = whoami::username(); match sm.reject(request_id, &approver, reason.to_string()) { Ok(request) => { println!("Request rejected"); println!(); println!(" Request ID: {}", request.id); println!(" Pattern: {}", request.pattern_name); println!(" Reason: {}", reason); println!(); println!("Pattern promotion blocked. Create a new request to try again."); ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to reject: {}", e); ExitCode::from(1) } } } /// Escalate a pending request. async fn handle_escalate(id: &str, config: &AphoriaConfig) -> ExitCode { let request_id = match Uuid::parse_str(id) { Ok(id) => id, Err(e) => { eprintln!("Invalid request ID '{}': {}", id, e); return ExitCode::from(1); } }; let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let escalator = whoami::username(); match sm.escalate(request_id, &escalator) { Ok(request) => { println!("Request escalated"); println!(); println!(" Request ID: {}", request.id); println!(" Pattern: {}", request.pattern_name); println!(" Status: {}", request.status); ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to escalate: {}", e); ExitCode::from(1) } } } /// Show request status. async fn handle_status( pattern_filter: Option<&str>, show_all: bool, format: &str, _config: &AphoriaConfig, ) -> ExitCode { let store = match GovernanceStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let requests = if let Some(pattern_str) = pattern_filter { match Uuid::parse_str(pattern_str) { Ok(pattern_id) => match store.get_request_by_pattern(&pattern_id) { Ok(Some(r)) => vec![r], Ok(None) => { println!("No approval request found for pattern {}", pattern_str); return ExitCode::SUCCESS; } Err(e) => { eprintln!("Failed to get request: {}", e); return ExitCode::from(1); } }, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_str, e); return ExitCode::from(1); } } } else if show_all { match store.list_all() { Ok(r) => r, Err(e) => { eprintln!("Failed to list requests: {}", e); return ExitCode::from(1); } } } else { match store.list_pending() { Ok(r) => r, Err(e) => { eprintln!("Failed to list pending: {}", e); return ExitCode::from(1); } } }; if requests.is_empty() { println!("No approval requests found."); return ExitCode::SUCCESS; } match format { "json" => { let json = serde_json::to_string_pretty(&requests).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { for request in &requests { print_request_details(request); println!(); } } } ExitCode::SUCCESS } /// Print detailed request information. fn print_request_details(request: &ApprovalRequest) { println!("Request: {}", request.id); println!("{}", "=".repeat(60)); println!(" Pattern: {} ({})", request.pattern_name, request.pattern_id); println!(" Workflow: {}", request.workflow_name); println!(" Status: {}", request.status); println!( " Created: {} by {}", request.created_at.format("%Y-%m-%d %H:%M"), request.created_by ); println!(" Updated: {}", request.updated_at.format("%Y-%m-%d %H:%M")); if let Some(deadline) = request.stage_deadline { let remaining = deadline - Utc::now(); if remaining.num_seconds() > 0 { println!( " Deadline: {} ({} hours remaining)", deadline.format("%Y-%m-%d %H:%M"), remaining.num_hours() ); } else { println!(" Deadline: {} (OVERDUE)", deadline.format("%Y-%m-%d %H:%M")); } } if let Some(ref evidence) = request.evidence_summary { println!(" Evidence: {}", evidence); } if !request.decisions.is_empty() { println!(); println!(" Decisions:"); for decision in &request.decisions { let comment = decision.comment.as_deref().unwrap_or("-"); println!( " {} {} by {} at {} ({})", decision.timestamp.format("%Y-%m-%d %H:%M"), decision.decision, decision.approver, decision.stage, comment ); } } } /// Check and process timed-out requests. async fn handle_check_timeouts(config: &AphoriaConfig) -> ExitCode { let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; match sm.check_timeouts() { Ok(processed) => { if processed.is_empty() { println!("No timed-out requests found."); } else { println!("Processed {} timed-out requests:", processed.len()); println!(); for request in &processed { println!(" {} - {} - {}", request.id, request.pattern_name, request.status); } } ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to check timeouts: {}", e); ExitCode::from(1) } } } /// Create an approval request for a pattern. async fn handle_create( pattern_id_str: &str, workflow_name: Option<&str>, config: &AphoriaConfig, ) -> ExitCode { let pattern_id = match Uuid::parse_str(pattern_id_str) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id_str, 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); } }; let pattern = match pattern_store.get_pattern_by_id(&pattern_id) { Some(p) => p, None => { eprintln!("Pattern '{}' not found", pattern_id_str); return ExitCode::from(1); } }; // Get workflow let workflow = if let Some(name) = workflow_name { match config.governance.get_workflow(name) { Some(w) => w.clone(), None => { eprintln!("Workflow '{}' not found", name); return ExitCode::from(1); } } } else { match config.governance.get_default_workflow() { Some(w) => w.clone(), None => { eprintln!("No default workflow configured"); return ExitCode::from(1); } } }; let sm = match GovernanceStateMachine::open_default(config.governance.clone()) { Ok(sm) => sm, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let creator = whoami::username(); match sm.create_request(&pattern, &workflow, &creator) { Ok(request) => { println!("Approval request created"); println!(); println!(" Request ID: {}", request.id); println!(" Pattern: {}", request.pattern_name); println!(" Workflow: {}", request.workflow_name); println!(" Status: {}", request.status); ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to create request: {}", e); ExitCode::from(1) } } } /// Show audit trail for a pattern. async fn handle_trail(pattern_id_str: &str, format: &str, config: &AphoriaConfig) -> ExitCode { let _ = config; // Unused but kept for API consistency let pattern_id = match Uuid::parse_str(pattern_id_str) { Ok(id) => id, Err(e) => { eprintln!("Invalid pattern ID '{}': {}", pattern_id_str, e); return ExitCode::from(1); } }; let trail = match AuditTrail::open_default() { Ok(t) => t, Err(e) => { eprintln!("Failed to open audit trail: {}", e); return ExitCode::from(1); } }; let events = match trail.get_pattern_timeline(&pattern_id) { Ok(e) => e, Err(e) => { eprintln!("Failed to get timeline: {}", e); return ExitCode::from(1); } }; if events.is_empty() { println!("No audit events for pattern {}", pattern_id_str); return ExitCode::SUCCESS; } match format { "json" => { let json = serde_json::to_string_pretty(&events).unwrap_or_else(|_| "[]".to_string()); println!("{}", json); } _ => { println!("Audit Trail for {}", pattern_id_str); println!("{}", "=".repeat(70)); println!(); println!("{:<20} {:<20} {:<15} {:<15}", "Timestamp", "Event", "Actor", "Request"); println!("{}", "-".repeat(70)); for event in &events { println!( "{:<20} {:<20} {:<15} {:<15}", event.timestamp.format("%Y-%m-%d %H:%M"), event.event_type.to_string(), truncate(&event.actor, 15), &event.request_id.to_string()[..8], ); } println!(); println!("Total: {} events", events.len()); } } ExitCode::SUCCESS } /// Export audit data. async fn handle_export( output: &Path, format_str: &str, date_range: Option<&str>, _config: &AphoriaConfig, ) -> ExitCode { let format = match format_str.parse::() { Ok(f) => f, Err(e) => { eprintln!("Invalid format: {}", e); return ExitCode::from(1); } }; let trail = match AuditTrail::open_default() { Ok(t) => t, Err(e) => { eprintln!("Failed to open audit trail: {}", e); return ExitCode::from(1); } }; let result = if let Some(range) = date_range { // Parse date range let parts: Vec<&str> = range.split("..").collect(); if parts.len() != 2 { eprintln!("Invalid date range format. Use: YYYY-MM-DD..YYYY-MM-DD"); return ExitCode::from(1); } let start = match NaiveDate::parse_from_str(parts[0], "%Y-%m-%d") { Ok(d) => Utc.from_utc_datetime(&d.and_hms_opt(0, 0, 0).unwrap_or_default()), Err(e) => { eprintln!("Invalid start date: {}", e); return ExitCode::from(1); } }; let end = match NaiveDate::parse_from_str(parts[1], "%Y-%m-%d") { Ok(d) => Utc.from_utc_datetime(&d.and_hms_opt(23, 59, 59).unwrap_or_default()), Err(e) => { eprintln!("Invalid end date: {}", e); return ExitCode::from(1); } }; trail.export_date_range(format, output, start, end) } else { trail.export(format, output) }; match result { Ok(()) => { println!("Exported audit data to {}", output.display()); ExitCode::SUCCESS } Err(e) => { eprintln!("Failed to export: {}", e); ExitCode::from(1) } } } /// Show audit summary. async fn handle_summary(format: &str, _config: &AphoriaConfig) -> ExitCode { let store = match GovernanceStore::open_default() { Ok(s) => s, Err(e) => { eprintln!("Failed to open governance store: {}", e); return ExitCode::from(1); } }; let trail = match AuditTrail::open_default() { Ok(t) => t, Err(e) => { eprintln!("Failed to open audit trail: {}", e); return ExitCode::from(1); } }; let requests = match store.list_all() { Ok(r) => r, Err(e) => { eprintln!("Failed to list requests: {}", e); return ExitCode::from(1); } }; let events = match trail.get_all_events() { Ok(e) => e, Err(e) => { eprintln!("Failed to get events: {}", e); return ExitCode::from(1); } }; // Calculate statistics let total_requests = requests.len(); let approved = requests.iter().filter(|r| r.status.is_approved()).count(); let rejected = requests.iter().filter(|r| r.status.is_rejected()).count(); let pending = requests.iter().filter(|r| r.status.is_pending()).count(); let expired = requests.iter().filter(|r| matches!(r.status, ApprovalStatus::Expired)).count(); let approval_rate = if total_requests > 0 { (approved as f32 / total_requests as f32) * 100.0 } else { 0.0 }; // Calculate average approval time for completed requests let avg_approval_days: Option = { let completed: Vec<_> = requests.iter().filter(|r| r.status.is_approved()).collect(); if completed.is_empty() { None } else { let total_days: i64 = completed.iter().map(|r| (r.updated_at - r.created_at).num_days()).sum(); Some(total_days as f32 / completed.len() as f32) } }; match format { "json" => { let summary = serde_json::json!({ "total_requests": total_requests, "approved": approved, "rejected": rejected, "pending": pending, "expired": expired, "approval_rate_percent": approval_rate, "avg_approval_days": avg_approval_days, "total_events": events.len(), }); let json = serde_json::to_string_pretty(&summary).unwrap_or_else(|_| "{}".to_string()); println!("{}", json); } _ => { println!("Governance Audit Summary"); println!("{}", "=".repeat(50)); println!(); println!("Requests:"); println!(" Total: {}", total_requests); println!(" Approved: {} ({:.1}%)", approved, approval_rate); println!(" Rejected: {}", rejected); println!(" Pending: {}", pending); println!(" Expired: {}", expired); println!(); if let Some(days) = avg_approval_days { println!("Average approval time: {:.1} days", days); } println!("Total audit events: {}", events.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.saturating_sub(3)]) } }