//! Phase guidance and deliverables tracking for contract management. //! //! This module provides structured guidance for each contract phase, tracking //! expected deliverables and completion criteria. use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use uuid::Uuid; /// Priority level for recommended deliverables #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] pub enum FilePriority { /// Must exist before advancing phase Required, /// Strongly suggested for phase completion Recommended, /// Nice to have, not blocking Optional, } /// A recommended file for a phase #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RecommendedFile { /// Template ID to create from pub template_id: String, /// Suggested file name pub name_suggestion: String, /// Priority level pub priority: FilePriority, /// Brief description of purpose pub description: String, } /// Expected deliverables for a phase #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PhaseDeliverables { /// Phase name pub phase: String, /// Recommended files to create pub recommended_files: Vec, /// Whether a repository is required for this phase pub requires_repository: bool, /// Whether tasks should exist in this phase pub requires_tasks: bool, /// Guidance text for this phase pub guidance: String, } /// Status of a deliverable item #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableStatus { /// Template ID pub template_id: String, /// Expected name pub name: String, /// Priority pub priority: FilePriority, /// Whether it has been created pub completed: bool, /// File ID if created pub file_id: Option, /// Actual file name if created pub actual_name: Option, } /// Checklist for phase completion #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PhaseChecklist { /// Current phase pub phase: String, /// File deliverables status pub file_deliverables: Vec, /// Whether repository is configured pub has_repository: bool, /// Whether repository was required pub repository_required: bool, /// Task statistics (for execute phase) pub task_stats: Option, /// Overall completion percentage (0-100) pub completion_percentage: u8, /// Summary message pub summary: String, /// Suggestions for next actions pub suggestions: Vec, } /// Task statistics for execute phase #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct TaskStats { pub total: usize, pub pending: usize, pub running: usize, pub done: usize, pub failed: usize, } /// Minimal file info for checklist building pub struct FileInfo { pub id: Uuid, pub name: String, pub contract_phase: Option, } /// Minimal task info for checklist building pub struct TaskInfo { pub id: Uuid, pub name: String, pub status: String, } /// Get phase deliverables configuration pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables { match phase { "research" => PhaseDeliverables { phase: "research".to_string(), recommended_files: vec![ RecommendedFile { template_id: "research-notes".to_string(), name_suggestion: "Research Notes".to_string(), priority: FilePriority::Recommended, description: "Document findings and insights during research".to_string(), }, RecommendedFile { template_id: "competitor-analysis".to_string(), name_suggestion: "Competitor Analysis".to_string(), priority: FilePriority::Recommended, description: "Analyze competitors and market positioning".to_string(), }, RecommendedFile { template_id: "user-research".to_string(), name_suggestion: "User Research".to_string(), priority: FilePriority::Optional, description: "Document user interviews and persona insights".to_string(), }, ], requires_repository: false, requires_tasks: false, guidance: "Focus on understanding the problem space, gathering information, and documenting findings. Create at least one research document before moving to Specify phase.".to_string(), }, "specify" => PhaseDeliverables { phase: "specify".to_string(), recommended_files: vec![ RecommendedFile { template_id: "requirements".to_string(), name_suggestion: "Requirements Document".to_string(), priority: FilePriority::Required, description: "Define functional and non-functional requirements".to_string(), }, RecommendedFile { template_id: "user-stories".to_string(), name_suggestion: "User Stories".to_string(), priority: FilePriority::Recommended, description: "Define features from the user's perspective".to_string(), }, RecommendedFile { template_id: "acceptance-criteria".to_string(), name_suggestion: "Acceptance Criteria".to_string(), priority: FilePriority::Recommended, description: "Define testable conditions for completion".to_string(), }, ], requires_repository: false, requires_tasks: false, guidance: "Define what needs to be built with clear requirements and acceptance criteria. Ensure specifications are detailed enough for planning.".to_string(), }, "plan" => PhaseDeliverables { phase: "plan".to_string(), recommended_files: vec![ RecommendedFile { template_id: "architecture".to_string(), name_suggestion: "Architecture Document".to_string(), priority: FilePriority::Recommended, description: "Document system architecture and design decisions".to_string(), }, RecommendedFile { template_id: "task-breakdown".to_string(), name_suggestion: "Task Breakdown".to_string(), priority: FilePriority::Required, description: "Break down work into implementable tasks".to_string(), }, RecommendedFile { template_id: "technical-design".to_string(), name_suggestion: "Technical Design".to_string(), priority: FilePriority::Optional, description: "Detailed technical specification".to_string(), }, ], requires_repository: true, requires_tasks: false, guidance: "Design the solution and break down work into tasks. A repository must be configured before moving to Execute phase.".to_string(), }, "execute" => PhaseDeliverables { phase: "execute".to_string(), recommended_files: vec![ RecommendedFile { template_id: "dev-notes".to_string(), name_suggestion: "Development Notes".to_string(), priority: FilePriority::Recommended, description: "Track implementation details and decisions".to_string(), }, RecommendedFile { template_id: "test-plan".to_string(), name_suggestion: "Test Plan".to_string(), priority: FilePriority::Optional, description: "Document testing strategy and test cases".to_string(), }, RecommendedFile { template_id: "implementation-log".to_string(), name_suggestion: "Implementation Log".to_string(), priority: FilePriority::Optional, description: "Chronological log of implementation progress".to_string(), }, ], requires_repository: true, requires_tasks: true, guidance: "Execute the planned tasks, implement features, and track progress. Complete all tasks before moving to Review phase.".to_string(), }, "review" => PhaseDeliverables { phase: "review".to_string(), recommended_files: vec![ RecommendedFile { template_id: "release-notes".to_string(), name_suggestion: "Release Notes".to_string(), priority: FilePriority::Required, description: "Document changes for release communication".to_string(), }, RecommendedFile { template_id: "review-checklist".to_string(), name_suggestion: "Review Checklist".to_string(), priority: FilePriority::Recommended, description: "Comprehensive checklist for code and feature review".to_string(), }, RecommendedFile { template_id: "retrospective".to_string(), name_suggestion: "Retrospective".to_string(), priority: FilePriority::Optional, description: "Reflect on the project and capture learnings".to_string(), }, ], requires_repository: false, requires_tasks: false, guidance: "Review completed work, document the release, and conduct a retrospective. The contract can be completed after review.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), recommended_files: vec![], requires_repository: false, requires_tasks: false, guidance: "Unknown phase".to_string(), }, } } /// Build a phase checklist comparing expected vs actual deliverables pub fn get_phase_checklist( phase: &str, files: &[FileInfo], tasks: &[TaskInfo], has_repository: bool, ) -> PhaseChecklist { let deliverables = get_phase_deliverables(phase); // Match files to expected deliverables let file_deliverables: Vec = deliverables .recommended_files .iter() .map(|rec| { // Check if a file with matching template ID or similar name exists let matched_file = files.iter().find(|f| { // Match by phase first f.contract_phase.as_deref() == Some(phase) && // Then by name similarity (case-insensitive contains) (f.name.to_lowercase().contains(&rec.name_suggestion.to_lowercase()) || rec.name_suggestion.to_lowercase().contains(&f.name.to_lowercase()) || f.name.to_lowercase().contains(&rec.template_id.replace("-", " "))) }); DeliverableStatus { template_id: rec.template_id.clone(), name: rec.name_suggestion.clone(), priority: rec.priority, completed: matched_file.is_some(), file_id: matched_file.map(|f| f.id), actual_name: matched_file.map(|f| f.name.clone()), } }) .collect(); // Calculate task stats for execute phase let task_stats = if phase == "execute" { let total = tasks.len(); let pending = tasks.iter().filter(|t| t.status == "pending").count(); let running = tasks.iter().filter(|t| t.status == "running").count(); let done = tasks.iter().filter(|t| t.status == "done").count(); let failed = tasks.iter().filter(|t| t.status == "failed" || t.status == "error").count(); Some(TaskStats { total, pending, running, done, failed }) } else { None }; // Calculate completion percentage let mut completed_items = 0; let mut total_items = 0; // Count required and recommended files (not optional) for status in &file_deliverables { if status.priority != FilePriority::Optional { total_items += 1; if status.completed { completed_items += 1; } } } // Count repository if required if deliverables.requires_repository { total_items += 1; if has_repository { completed_items += 1; } } // Count tasks if in execute phase if let Some(ref stats) = task_stats { if stats.total > 0 { total_items += 1; if stats.done == stats.total && stats.total > 0 { completed_items += 1; } } } let completion_percentage = if total_items > 0 { ((completed_items as f64 / total_items as f64) * 100.0) as u8 } else { 100 // No requirements means complete }; // Generate suggestions let mut suggestions = Vec::new(); // Suggest missing required files for status in &file_deliverables { if !status.completed { match status.priority { FilePriority::Required => { suggestions.push(format!("Create {} (required)", status.name)); } FilePriority::Recommended => { suggestions.push(format!("Consider creating {} (recommended)", status.name)); } FilePriority::Optional => { // Don't suggest optional items } } } } // Suggest repository if needed if deliverables.requires_repository && !has_repository { suggestions.push("Configure a repository for task execution".to_string()); } // Suggest task actions for execute phase if let Some(ref stats) = task_stats { if stats.total == 0 { suggestions.push("Create tasks from the Task Breakdown document".to_string()); } else if stats.pending > 0 { suggestions.push(format!("Run {} pending task(s)", stats.pending)); } else if stats.running > 0 { suggestions.push(format!("Wait for {} running task(s) to complete", stats.running)); } else if stats.failed > 0 { suggestions.push(format!("Address {} failed task(s)", stats.failed)); } } // Generate summary let summary = generate_phase_summary(phase, &file_deliverables, has_repository, &task_stats, completion_percentage); PhaseChecklist { phase: phase.to_string(), file_deliverables, has_repository, repository_required: deliverables.requires_repository, task_stats, completion_percentage, summary, suggestions, } } fn generate_phase_summary( phase: &str, deliverables: &[DeliverableStatus], has_repository: bool, task_stats: &Option, completion_percentage: u8, ) -> String { let completed_count = deliverables.iter().filter(|d| d.completed).count(); let total_count = deliverables.len(); match phase { "research" => { if completed_count == 0 { "Research phase needs documentation. Create research notes or competitor analysis.".to_string() } else { format!("{}/{} research documents created. Consider transitioning to Specify phase.", completed_count, total_count) } } "specify" => { let has_required = deliverables.iter() .filter(|d| d.priority == FilePriority::Required) .all(|d| d.completed); if !has_required { "Specify phase requires a Requirements Document before transitioning.".to_string() } else if completion_percentage >= 66 { "Specifications are ready. Consider transitioning to Plan phase.".to_string() } else { format!("{}/{} specification documents created.", completed_count, total_count) } } "plan" => { let has_task_breakdown = deliverables.iter() .any(|d| d.template_id == "task-breakdown" && d.completed); if !has_task_breakdown { "Plan phase requires a Task Breakdown document.".to_string() } else if !has_repository { "Repository not configured. Configure a repository before Execute phase.".to_string() } else { "Planning complete. Ready to transition to Execute phase.".to_string() } } "execute" => { if let Some(stats) = task_stats { if stats.total == 0 { "No tasks created. Create tasks from the Task Breakdown document.".to_string() } else if stats.done == stats.total { "All tasks complete! Ready for Review phase.".to_string() } else { format!("{}/{} tasks completed ({}% done)", stats.done, stats.total, if stats.total > 0 { (stats.done * 100) / stats.total } else { 0 }) } } else { "Execute phase in progress.".to_string() } } "review" => { let has_release_notes = deliverables.iter() .any(|d| d.template_id == "release-notes" && d.completed); if !has_release_notes { "Review phase requires Release Notes before completion.".to_string() } else { "Review documentation complete. Contract can be marked as done.".to_string() } } _ => format!("Phase {} - {}% complete", phase, completion_percentage), } } /// Check if phase targets are met for transition pub fn check_phase_completion( phase: &str, files: &[FileInfo], tasks: &[TaskInfo], has_repository: bool, ) -> bool { let checklist = get_phase_checklist(phase, files, tasks, has_repository); // Check required files are complete let required_files_complete = checklist.file_deliverables.iter() .filter(|d| d.priority == FilePriority::Required) .all(|d| d.completed); // Check repository if required let repository_ok = !checklist.repository_required || checklist.has_repository; // Check tasks if in execute phase let tasks_ok = if let Some(stats) = &checklist.task_stats { stats.total > 0 && stats.done == stats.total } else { true }; required_files_complete && repository_ok && tasks_ok } /// Format checklist as markdown for LLM context pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { let mut md = format!("## Phase Progress ({} Phase)\n\n", capitalize(&checklist.phase)); // File deliverables md.push_str("### Deliverables\n"); for status in &checklist.file_deliverables { let check = if status.completed { "+" } else { "-" }; let priority_label = match status.priority { FilePriority::Required => " (required)", FilePriority::Recommended => " (recommended)", FilePriority::Optional => " (optional)", }; if status.completed { md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created"))); } else { md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label)); } } // Repository status if checklist.repository_required { let check = if checklist.has_repository { "+" } else { "-" }; md.push_str(&format!("[{}] Repository configured (required)\n", check)); } // Task stats for execute phase if let Some(ref stats) = checklist.task_stats { md.push_str(&format!("\n### Task Progress\n")); md.push_str(&format!("- Total: {}\n", stats.total)); md.push_str(&format!("- Done: {}\n", stats.done)); if stats.pending > 0 { md.push_str(&format!("- Pending: {}\n", stats.pending)); } if stats.running > 0 { md.push_str(&format!("- Running: {}\n", stats.running)); } if stats.failed > 0 { md.push_str(&format!("- Failed: {}\n", stats.failed)); } } // Summary md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage)); // Suggestions if !checklist.suggestions.is_empty() { md.push_str("\n**Next Steps**:\n"); for suggestion in &checklist.suggestions { md.push_str(&format!("- {}\n", suggestion)); } } md } fn capitalize(s: &str) -> String { let mut chars = s.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::() + chars.as_str(), } } #[cfg(test)] mod tests { use super::*; #[test] fn test_get_phase_deliverables() { let research = get_phase_deliverables("research"); assert_eq!(research.phase, "research"); assert!(!research.requires_repository); assert_eq!(research.recommended_files.len(), 3); let plan = get_phase_deliverables("plan"); assert!(plan.requires_repository); assert!(plan.recommended_files.iter().any(|f| f.template_id == "task-breakdown")); } #[test] fn test_phase_checklist_empty() { let checklist = get_phase_checklist("research", &[], &[], false); assert_eq!(checklist.completion_percentage, 0); assert!(!checklist.suggestions.is_empty()); } #[test] fn test_check_phase_completion() { let files = vec![ FileInfo { id: Uuid::new_v4(), name: "Requirements Document".to_string(), contract_phase: Some("specify".to_string()), }, ]; // Specify phase has required file let complete = check_phase_completion("specify", &files, &[], false); assert!(complete); } }