diff options
Diffstat (limited to 'makima/src/llm/phase_guidance.rs')
| -rw-r--r-- | makima/src/llm/phase_guidance.rs | 688 |
1 files changed, 282 insertions, 406 deletions
diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs index 03f7c76..379bdca 100644 --- a/makima/src/llm/phase_guidance.rs +++ b/makima/src/llm/phase_guidance.rs @@ -21,13 +21,12 @@ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use uuid::Uuid; -/// Priority level for recommended deliverables +/// Priority level for deliverables #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "lowercase")] -pub enum FilePriority { - /// Must exist before advancing phase +pub enum DeliverablePriority { + /// Must be completed before advancing phase Required, /// Strongly suggested for phase completion Recommended, @@ -35,49 +34,45 @@ pub enum FilePriority { 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, +/// A deliverable for a phase +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Deliverable { + /// Unique identifier for the deliverable + pub id: String, + /// Display name + pub name: String, /// Priority level - pub priority: FilePriority, + pub priority: DeliverablePriority, /// Brief description of purpose pub description: String, } /// Expected deliverables for a phase -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct PhaseDeliverables { /// Phase name pub phase: String, - /// Recommended files to create - pub recommended_files: Vec<RecommendedFile>, + /// Deliverables for this phase + pub deliverables: Vec<Deliverable>, /// Whether a repository is required for this phase pub requires_repository: bool, - /// Whether tasks should exist in this phase + /// Whether tasks should be completed in this phase pub requires_tasks: bool, /// Guidance text for this phase pub guidance: String, } -/// Status of a deliverable item +/// Status of a deliverable #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableStatus { - /// Template ID - pub template_id: String, - /// Expected name + /// Deliverable ID + pub id: String, + /// Display name pub name: String, /// Priority - pub priority: FilePriority, - /// Whether it has been created + pub priority: DeliverablePriority, + /// Whether it has been completed pub completed: bool, - /// File ID if created - pub file_id: Option<Uuid>, - /// Actual file name if created - pub actual_name: Option<String>, } /// Checklist for phase completion @@ -85,8 +80,8 @@ pub struct DeliverableStatus { pub struct PhaseChecklist { /// Current phase pub phase: String, - /// File deliverables status - pub file_deliverables: Vec<DeliverableStatus>, + /// Deliverable status list + pub deliverables: Vec<DeliverableStatus>, /// Whether repository is configured pub has_repository: bool, /// Whether repository was required @@ -111,16 +106,8 @@ pub struct TaskStats { pub failed: usize, } -/// Minimal file info for checklist building -pub struct FileInfo { - pub id: Uuid, - pub name: String, - pub contract_phase: Option<String>, -} - /// Minimal task info for checklist building pub struct TaskInfo { - pub id: Uuid, pub name: String, pub status: String, } @@ -131,22 +118,6 @@ pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables { } /// Get phase deliverables configuration for a specific contract type -/// -/// ## Contract Types -/// -/// ### Simple -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// -/// ### Specification -/// - Research: Only "Research Notes" deliverable (required) -/// - Specify: Only "Requirements Document" deliverable (required) -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// - Review: Only "Release Notes" deliverable (required) -/// -/// ### Execute -/// - Execute: No deliverables at all pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> PhaseDeliverables { match contract_type { "execute" => get_execute_type_deliverables(phase), @@ -156,41 +127,35 @@ pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> Phas } /// Get deliverables for 'simple' contract type -/// - Plan phase: Only "Plan" deliverable (required) -/// - Execute phase: Only "PR" deliverable (required) fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables { match phase { "plan" => PhaseDeliverables { phase: "plan".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "plan".to_string(), - name_suggestion: "Plan".to_string(), - priority: FilePriority::Required, - description: "Implementation plan detailing the approach and tasks".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "plan-document".to_string(), + name: "Plan".to_string(), + priority: DeliverablePriority::Required, + description: "Implementation plan detailing the approach and tasks".to_string(), + }], requires_repository: true, requires_tasks: false, guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(), }, "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "pr".to_string(), - name_suggestion: "PR".to_string(), - priority: FilePriority::Required, - description: "Pull request with the implemented changes".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "pull-request".to_string(), + name: "Pull Request".to_string(), + priority: DeliverablePriority::Required, + description: "Pull request with the implemented changes".to_string(), + }], requires_repository: true, requires_tasks: true, guidance: "Execute the plan and create a PR with the implemented changes. Complete all tasks to finish the contract.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "Unknown phase for simple contract type".to_string(), @@ -199,86 +164,71 @@ fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables { } /// Get deliverables for 'specification' contract type -/// - Research: Only "Research Notes" deliverable (required) -/// - Specify: Only "Requirements Document" deliverable (required) -/// - Plan: Only "Plan" deliverable (required) -/// - Execute: Only "PR" deliverable (required) -/// - Review: Only "Release Notes" deliverable (required) fn get_specification_type_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::Required, - description: "Document findings and insights during research".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "research-notes".to_string(), + name: "Research Notes".to_string(), + priority: DeliverablePriority::Required, + description: "Document findings and insights during research".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Focus on understanding the problem space and document your findings in the Research Notes 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(), - }, - ], + deliverables: vec![Deliverable { + id: "requirements-document".to_string(), + name: "Requirements Document".to_string(), + priority: DeliverablePriority::Required, + description: "Define functional and non-functional requirements".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Define what needs to be built with clear requirements in the Requirements Document. Ensure specifications are detailed enough for planning.".to_string(), }, "plan" => PhaseDeliverables { phase: "plan".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "plan".to_string(), - name_suggestion: "Plan".to_string(), - priority: FilePriority::Required, - description: "Implementation plan detailing the approach and tasks".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "plan-document".to_string(), + name: "Plan".to_string(), + priority: DeliverablePriority::Required, + description: "Implementation plan detailing the approach and tasks".to_string(), + }], requires_repository: true, requires_tasks: false, guidance: "Create a plan document that outlines the implementation approach. A repository must be configured before moving to Execute phase.".to_string(), }, "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![ - RecommendedFile { - template_id: "pr".to_string(), - name_suggestion: "PR".to_string(), - priority: FilePriority::Required, - description: "Pull request with the implemented changes".to_string(), - }, - ], + deliverables: vec![Deliverable { + id: "pull-request".to_string(), + name: "Pull Request".to_string(), + priority: DeliverablePriority::Required, + description: "Pull request with the implemented changes".to_string(), + }], requires_repository: true, requires_tasks: true, guidance: "Execute the plan and create a PR with the implemented changes. 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(), - }, - ], + deliverables: vec![Deliverable { + id: "release-notes".to_string(), + name: "Release Notes".to_string(), + priority: DeliverablePriority::Required, + description: "Document changes for release communication".to_string(), + }], requires_repository: false, requires_tasks: false, guidance: "Review completed work and document the release in the Release Notes. The contract can be completed after review.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "Unknown phase for specification contract type".to_string(), @@ -287,19 +237,18 @@ fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables { } /// Get deliverables for 'execute' contract type -/// - Execute phase only: No deliverables at all fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables { match phase { "execute" => PhaseDeliverables { phase: "execute".to_string(), - recommended_files: vec![], // No deliverables for execute-only contract type + deliverables: vec![], // No deliverables for execute-only contract type requires_repository: true, requires_tasks: true, guidance: "Execute the tasks directly. No deliverable documents are required for this contract type.".to_string(), }, _ => PhaseDeliverables { phase: phase.to_string(), - recommended_files: vec![], + deliverables: vec![], requires_repository: false, requires_tasks: false, guidance: "The 'execute' contract type only supports the 'execute' phase.".to_string(), @@ -307,49 +256,25 @@ fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables { } } -/// Build a phase checklist comparing expected vs actual deliverables (legacy, defaults to "simple") -pub fn get_phase_checklist( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, -) -> PhaseChecklist { - get_phase_checklist_for_type(phase, files, tasks, has_repository, "simple") -} - -/// Build a phase checklist comparing expected vs actual deliverables for a specific contract type +/// Build a phase checklist comparing expected vs actual deliverables pub fn get_phase_checklist_for_type( phase: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, contract_type: &str, ) -> PhaseChecklist { - let deliverables = get_phase_deliverables_for_type(phase, contract_type); + let phase_config = get_phase_deliverables_for_type(phase, contract_type); - // Match files to expected deliverables - let file_deliverables: Vec<DeliverableStatus> = deliverables - .recommended_files + // Build deliverable status list + let deliverables: Vec<DeliverableStatus> = phase_config + .deliverables .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()), - } + .map(|d| DeliverableStatus { + id: d.id.clone(), + name: d.name.clone(), + priority: d.priority, + completed: completed_deliverables.contains(&d.id), }) .collect(); @@ -359,9 +284,18 @@ pub fn get_phase_checklist_for_type( 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 }) + let failed = tasks + .iter() + .filter(|t| t.status == "failed" || t.status == "error") + .count(); + + Some(TaskStats { + total, + pending, + running, + done, + failed, + }) } else { None }; @@ -370,9 +304,9 @@ pub fn get_phase_checklist_for_type( 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 { + // Count required and recommended deliverables (not optional) + for status in &deliverables { + if status.priority != DeliverablePriority::Optional { total_items += 1; if status.completed { completed_items += 1; @@ -381,7 +315,7 @@ pub fn get_phase_checklist_for_type( } // Count repository if required - if deliverables.requires_repository { + if phase_config.requires_repository { total_items += 1; if has_repository { completed_items += 1; @@ -407,62 +341,64 @@ pub fn get_phase_checklist_for_type( // Generate suggestions let mut suggestions = Vec::new(); - // Suggest missing required files - for status in &file_deliverables { + // Suggest missing deliverables + for status in &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)); + DeliverablePriority::Required => { + suggestions.push(format!( + "Mark '{}' as complete using mark_deliverable_complete (required)", + status.name + )); } - FilePriority::Optional => { - // Don't suggest optional items + DeliverablePriority::Recommended => { + suggestions.push(format!( + "Consider completing '{}' (recommended)", + status.name + )); } + DeliverablePriority::Optional => {} } } } // Suggest repository if needed - if deliverables.requires_repository && !has_repository { + if phase_config.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()); + suggestions.push("Create tasks to implement the plan".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)); + 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)); } else if stats.done == stats.total && stats.total > 0 { - // All tasks complete - for simple contracts, this is the terminal phase - // For specification contracts, they should advance to review phase - suggestions.push("Mark the contract as completed (for simple contracts) or advance to Review phase".to_string()); - } - } - - // Suggest completion for review phase (terminal for specification contracts) - if phase == "review" { - let has_release_notes = file_deliverables.iter() - .any(|d| d.template_id == "release-notes" && d.completed); - if has_release_notes { - suggestions.push("Mark the contract as completed".to_string()); + suggestions.push("All tasks complete. Mark deliverables and advance phase.".to_string()); } } // Generate summary - let summary = generate_phase_summary(phase, &file_deliverables, has_repository, &task_stats, completion_percentage); + let summary = generate_phase_summary( + phase, + &deliverables, + has_repository, + &task_stats, + completion_percentage, + ); PhaseChecklist { phase: phase.to_string(), - file_deliverables, + deliverables, has_repository, - repository_required: deliverables.requires_repository, + repository_required: phase_config.requires_repository, task_stats, completion_percentage, summary, @@ -483,32 +419,39 @@ fn generate_phase_summary( match phase { "research" => { if completed_count == 0 { - "Research phase needs documentation. Create research notes or competitor analysis.".to_string() + "Research phase needs documentation. Mark deliverables complete when ready." + .to_string() } else { - format!("{}/{} research documents created. Consider transitioning to Specify phase.", completed_count, total_count) + format!( + "{}/{} deliverables complete. Ready to transition to Specify phase.", + completed_count, total_count + ) } } "specify" => { - let has_required = deliverables.iter() - .filter(|d| d.priority == FilePriority::Required) + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::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() + "Specify phase requires completing the Requirements Document deliverable." + .to_string() } else { - format!("{}/{} specification documents created.", completed_count, total_count) + "Specifications ready. Consider transitioning to Plan phase.".to_string() } } "plan" => { - let has_task_breakdown = deliverables.iter() - .any(|d| d.template_id == "task-breakdown" && d.completed); + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::Required) + .all(|d| d.completed); - if !has_task_breakdown { - "Plan phase requires a Task Breakdown document.".to_string() + if !has_required { + "Plan phase requires completing the Plan deliverable.".to_string() } else if !has_repository { - "Repository not configured. Configure a repository before Execute phase.".to_string() + "Repository not configured. Configure a repository before Execute phase." + .to_string() } else { "Planning complete. Ready to transition to Execute phase.".to_string() } @@ -516,23 +459,33 @@ fn generate_phase_summary( "execute" => { if let Some(stats) = task_stats { if stats.total == 0 { - "No tasks created. Create tasks from the Task Breakdown document.".to_string() + "No tasks created. Create tasks to implement the plan.".to_string() } else if stats.done == stats.total { - "All tasks complete! Ready for Review phase.".to_string() + "All tasks complete! Mark deliverables and advance to Review phase (or complete contract).".to_string() } else { - format!("{}/{} tasks completed ({}% done)", stats.done, stats.total, - if stats.total > 0 { (stats.done * 100) / stats.total } else { 0 }) + 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); + let has_required = deliverables + .iter() + .filter(|d| d.priority == DeliverablePriority::Required) + .all(|d| d.completed); - if !has_release_notes { - "Review phase requires Release Notes before completion.".to_string() + if !has_required { + "Review phase requires completing the Release Notes deliverable.".to_string() } else { "Review documentation complete. Contract can be marked as done.".to_string() } @@ -541,44 +494,6 @@ fn generate_phase_summary( } } -/// Check if phase targets are met for transition (legacy, defaults to "simple") -pub fn check_phase_completion( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, -) -> bool { - check_phase_completion_for_type(phase, files, tasks, has_repository, "simple") -} - -/// Check if phase targets are met for transition for a specific contract type -pub fn check_phase_completion_for_type( - phase: &str, - files: &[FileInfo], - tasks: &[TaskInfo], - has_repository: bool, - contract_type: &str, -) -> bool { - let checklist = get_phase_checklist_for_type(phase, files, tasks, has_repository, contract_type); - - // 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 -} - /// Result of checking if deliverables are met for the current phase #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableCheckResult { @@ -603,9 +518,11 @@ pub struct DeliverableCheckResult { /// A single deliverable item status #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] pub struct DeliverableItem { + /// ID of the deliverable + pub id: String, /// Name of the deliverable pub name: String, - /// Type: "file", "repository", "pr", "tasks" + /// Type: "deliverable", "repository", "tasks" pub deliverable_type: String, /// Whether it's met pub met: bool, @@ -614,51 +531,49 @@ pub struct DeliverableItem { } /// Check if all required deliverables for the current phase are met -/// This is used for both prompts and the check_deliverables_met tool pub fn check_deliverables_met( phase: &str, contract_type: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, - pr_url: Option<&str>, ) -> DeliverableCheckResult { - let mut required_deliverables = Vec::new(); + let mut required_items = Vec::new(); let mut missing = Vec::new(); // Get the deliverables for this contract type and phase - let deliverables = get_phase_deliverables_for_type(phase, contract_type); - - // Check required files for this phase - for rec in &deliverables.recommended_files { - if rec.priority == FilePriority::Required { - let matched = files.iter().any(|f| { - f.contract_phase.as_deref() == Some(phase) && - (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("-", " "))) - }); - - required_deliverables.push(DeliverableItem { - name: rec.name_suggestion.clone(), - deliverable_type: "file".to_string(), - met: matched, - details: if matched { - Some("Document exists".to_string()) + let phase_config = get_phase_deliverables_for_type(phase, contract_type); + + // Check required deliverables for this phase + for deliverable in &phase_config.deliverables { + if deliverable.priority == DeliverablePriority::Required { + let is_complete = completed_deliverables.contains(&deliverable.id); + + required_items.push(DeliverableItem { + id: deliverable.id.clone(), + name: deliverable.name.clone(), + deliverable_type: "deliverable".to_string(), + met: is_complete, + details: if is_complete { + Some("Marked complete".to_string()) } else { None }, }); - if !matched { - missing.push(format!("Create {} (required)", rec.name_suggestion)); + if !is_complete { + missing.push(format!( + "Mark '{}' as complete (required)", + deliverable.name + )); } } } // Check repository for phases that require it - if deliverables.requires_repository { - required_deliverables.push(DeliverableItem { + if phase_config.requires_repository { + required_items.push(DeliverableItem { + id: "repository".to_string(), name: "Repository".to_string(), deliverable_type: "repository".to_string(), met: has_repository, @@ -675,12 +590,13 @@ pub fn check_deliverables_met( } // Check tasks for execute phase - if deliverables.requires_tasks { + if phase_config.requires_tasks { let total_tasks = tasks.len(); let done_tasks = tasks.iter().filter(|t| t.status == "done").count(); let tasks_complete = total_tasks > 0 && done_tasks == total_tasks; - required_deliverables.push(DeliverableItem { + required_items.push(DeliverableItem { + id: "tasks".to_string(), name: "Tasks Completed".to_string(), deliverable_type: "tasks".to_string(), met: tasks_complete, @@ -691,38 +607,36 @@ pub fn check_deliverables_met( if total_tasks == 0 { missing.push("Create and complete tasks".to_string()); } else { - missing.push(format!("Complete remaining {} task(s)", total_tasks - done_tasks)); + missing.push(format!( + "Complete remaining {} task(s)", + total_tasks - done_tasks + )); } } } - // For simple/specification contracts in execute phase, PR is a key deliverable - if (contract_type == "simple" || contract_type == "specification") && phase == "execute" { - let has_pr = pr_url.is_some() && !pr_url.unwrap_or("").is_empty(); - required_deliverables.push(DeliverableItem { - name: "Pull Request".to_string(), - deliverable_type: "pr".to_string(), - met: has_pr, - details: pr_url.map(|u| format!("PR: {}", u)), - }); - - if !has_pr { - missing.push("Create a Pull Request for the completed work".to_string()); - } - } - - let deliverables_met = required_deliverables.iter().all(|d| d.met); + let deliverables_met = required_items.iter().all(|d| d.met); let next_phase = get_next_phase_for_contract(contract_type, phase); let ready_to_advance = deliverables_met && next_phase.is_some(); let summary = if deliverables_met { if let Some(ref next) = next_phase { - format!("All deliverables met for {} phase. Ready to advance to {} phase.", phase, next) + format!( + "All deliverables met for {} phase. Ready to advance to {} phase.", + phase, next + ) } else { - format!("All deliverables met for {} phase. This is the final phase.", phase) + format!( + "All deliverables met for {} phase. This is the final phase.", + phase + ) } } else { - format!("{} deliverable(s) still needed for {} phase.", missing.len(), phase) + format!( + "{} deliverable(s) still needed for {} phase.", + missing.len(), + phase + ) }; DeliverableCheckResult { @@ -730,7 +644,7 @@ pub fn check_deliverables_met( ready_to_advance, phase: phase.to_string(), next_phase, - required_deliverables, + required_deliverables: required_items, missing, summary, auto_progress_recommended: deliverables_met && ready_to_advance, @@ -758,17 +672,21 @@ pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) -> } /// Determine if the contract should auto-progress to the next phase -/// This is called when deliverables are met and autonomous_loop is enabled pub fn should_auto_progress( phase: &str, contract_type: &str, - files: &[FileInfo], + completed_deliverables: &[String], tasks: &[TaskInfo], has_repository: bool, - pr_url: Option<&str>, autonomous_loop: bool, ) -> AutoProgressDecision { - let check = check_deliverables_met(phase, contract_type, files, tasks, has_repository, pr_url); + let check = check_deliverables_met( + phase, + contract_type, + completed_deliverables, + tasks, + has_repository, + ); if !check.deliverables_met { return AutoProgressDecision { @@ -840,15 +758,25 @@ pub fn generate_deliverable_prompt_guidance( let mut guidance = String::new(); guidance.push_str("\n## Phase Deliverables Status\n\n"); - guidance.push_str(&format!("**Current Phase**: {} | **Contract Type**: {}\n\n", - capitalize(phase), contract_type)); + guidance.push_str(&format!( + "**Current Phase**: {} | **Contract Type**: {}\n\n", + capitalize(phase), + contract_type + )); // Show required deliverables checklist guidance.push_str("### Required Deliverables Checklist\n"); for item in &check_result.required_deliverables { let status = if item.met { "[x]" } else { "[ ]" }; - let details = item.details.as_ref().map(|d| format!(" - {}", d)).unwrap_or_default(); - guidance.push_str(&format!("{} **{}** ({}){}\n", status, item.name, item.deliverable_type, details)); + let details = item + .details + .as_ref() + .map(|d| format!(" - {}", d)) + .unwrap_or_default(); + guidance.push_str(&format!( + "{} **{}** ({}){}\n", + status, item.name, item.deliverable_type, details + )); } // Show status and next actions @@ -861,7 +789,9 @@ pub fn generate_deliverable_prompt_guidance( guidance.push_str(&format!("\n**ACTION REQUIRED**: Since all deliverables are met, you should call `advance_phase` with `new_phase=\"{}\"` to progress the contract.\n", next)); } } else { - guidance.push_str("This is the terminal phase. The contract can be marked as completed.\n"); + guidance.push_str( + "This is the terminal phase. The contract can be marked as completed.\n", + ); } } else { guidance.push_str("**Deliverables not yet met.**\n\n"); @@ -869,7 +799,9 @@ pub fn generate_deliverable_prompt_guidance( for item in &check_result.missing { guidance.push_str(&format!("- {}\n", item)); } - guidance.push_str("\nComplete the missing deliverables before advancing to the next phase.\n"); + guidance.push_str( + "\nUse `mark_deliverable_complete` to mark deliverables as complete when ready.\n", + ); } guidance @@ -877,20 +809,23 @@ pub fn generate_deliverable_prompt_guidance( /// 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)); + let mut md = format!( + "## Phase Progress ({} Phase)\n\n", + capitalize(&checklist.phase) + ); - // File deliverables + // Deliverables md.push_str("### Deliverables\n"); - for status in &checklist.file_deliverables { + for status in &checklist.deliverables { let check = if status.completed { "+" } else { "-" }; let priority_label = match status.priority { - FilePriority::Required => " (required)", - FilePriority::Recommended => " (recommended)", - FilePriority::Optional => " (optional)", + DeliverablePriority::Required => " (required)", + DeliverablePriority::Recommended => " (recommended)", + DeliverablePriority::Optional => " (optional)", }; if status.completed { - md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created"))); + md.push_str(&format!("[{}] {} - completed\n", check, status.name)); } else { md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label)); } @@ -904,7 +839,7 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { // Task stats for execute phase if let Some(ref stats) = checklist.task_stats { - md.push_str(&format!("\n### Task Progress\n")); + md.push_str("\n### Task Progress\n"); md.push_str(&format!("- Total: {}\n", stats.total)); md.push_str(&format!("- Done: {}\n", stats.done)); if stats.pending > 0 { @@ -919,7 +854,10 @@ pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String { } // Summary - md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage)); + md.push_str(&format!( + "\n**Status**: {} ({}% complete)\n", + checklist.summary, checklist.completion_percentage + )); // Suggestions if !checklist.suggestions.is_empty() { @@ -946,125 +884,63 @@ mod tests { #[test] fn test_get_phase_deliverables_simple() { - // Simple contract type: Plan phase has only "Plan" deliverable let plan = get_phase_deliverables_for_type("plan", "simple"); assert_eq!(plan.phase, "plan"); assert!(plan.requires_repository); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); - assert_eq!(plan.recommended_files[0].priority, FilePriority::Required); + assert_eq!(plan.deliverables.len(), 1); + assert_eq!(plan.deliverables[0].id, "plan-document"); + assert_eq!(plan.deliverables[0].priority, DeliverablePriority::Required); - // Simple contract type: Execute phase has only "PR" deliverable let execute = get_phase_deliverables_for_type("execute", "simple"); assert_eq!(execute.phase, "execute"); assert!(execute.requires_repository); assert!(execute.requires_tasks); - assert_eq!(execute.recommended_files.len(), 1); - assert_eq!(execute.recommended_files[0].template_id, "pr"); - assert_eq!(execute.recommended_files[0].priority, FilePriority::Required); + assert_eq!(execute.deliverables.len(), 1); + assert_eq!(execute.deliverables[0].id, "pull-request"); } #[test] fn test_get_phase_deliverables_specification() { - // Specification: Research phase has only "Research Notes" deliverable let research = get_phase_deliverables_for_type("research", "specification"); - assert_eq!(research.phase, "research"); - assert!(!research.requires_repository); - assert_eq!(research.recommended_files.len(), 1); - assert_eq!(research.recommended_files[0].template_id, "research-notes"); - assert_eq!(research.recommended_files[0].priority, FilePriority::Required); + assert_eq!(research.deliverables.len(), 1); + assert_eq!(research.deliverables[0].id, "research-notes"); - // Specification: Specify phase has only "Requirements Document" deliverable let specify = get_phase_deliverables_for_type("specify", "specification"); - assert_eq!(specify.phase, "specify"); - assert_eq!(specify.recommended_files.len(), 1); - assert_eq!(specify.recommended_files[0].template_id, "requirements"); - assert_eq!(specify.recommended_files[0].priority, FilePriority::Required); - - // Specification: Plan phase has only "Plan" deliverable - let plan = get_phase_deliverables_for_type("plan", "specification"); - assert_eq!(plan.phase, "plan"); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); + assert_eq!(specify.deliverables.len(), 1); + assert_eq!(specify.deliverables[0].id, "requirements-document"); - // Specification: Execute phase has only "PR" deliverable - let execute = get_phase_deliverables_for_type("execute", "specification"); - assert_eq!(execute.phase, "execute"); - assert_eq!(execute.recommended_files.len(), 1); - assert_eq!(execute.recommended_files[0].template_id, "pr"); - - // Specification: Review phase has only "Release Notes" deliverable let review = get_phase_deliverables_for_type("review", "specification"); - assert_eq!(review.phase, "review"); - assert_eq!(review.recommended_files.len(), 1); - assert_eq!(review.recommended_files[0].template_id, "release-notes"); - assert_eq!(review.recommended_files[0].priority, FilePriority::Required); + assert_eq!(review.deliverables.len(), 1); + assert_eq!(review.deliverables[0].id, "release-notes"); } #[test] fn test_get_phase_deliverables_execute_type() { - // Execute contract type: Only execute phase, NO deliverables let execute = get_phase_deliverables_for_type("execute", "execute"); - assert_eq!(execute.phase, "execute"); + assert!(execute.deliverables.is_empty()); assert!(execute.requires_repository); assert!(execute.requires_tasks); - assert!(execute.recommended_files.is_empty()); // NO deliverables - - // Execute contract type: Other phases should return empty deliverables - let plan = get_phase_deliverables_for_type("plan", "execute"); - assert!(plan.recommended_files.is_empty()); } #[test] - fn test_phase_checklist_empty_simple() { - let checklist = get_phase_checklist_for_type("plan", &[], &[], false, "simple"); - assert_eq!(checklist.completion_percentage, 0); - assert!(!checklist.suggestions.is_empty()); - } - - #[test] - fn test_phase_checklist_execute_type_no_deliverables() { - // Execute contract type with no file deliverables - let checklist = get_phase_checklist_for_type("execute", &[], &[], true, "execute"); - // Should have no file deliverables - assert!(checklist.file_deliverables.is_empty()); - } - - #[test] - fn test_check_phase_completion_specification() { - let files = vec![ - FileInfo { - id: Uuid::new_v4(), - name: "Requirements Document".to_string(), - contract_phase: Some("specify".to_string()), - }, - ]; - - // Specify phase has required file for specification contract type - let complete = check_phase_completion_for_type("specify", &files, &[], false, "specification"); - assert!(complete); - } - - #[test] - fn test_check_phase_completion_simple() { - let files = vec![ - FileInfo { - id: Uuid::new_v4(), - name: "Plan".to_string(), - contract_phase: Some("plan".to_string()), - }, - ]; + fn test_check_deliverables_met() { + // No deliverables marked complete + let result = check_deliverables_met("plan", "simple", &[], &[], true); + assert!(!result.deliverables_met); + assert!(!result.missing.is_empty()); - // Plan phase has required "Plan" file for simple contract type - let complete = check_phase_completion_for_type("plan", &files, &[], true, "simple"); - assert!(complete); + // Deliverable marked complete + let completed = vec!["plan-document".to_string()]; + let result = check_deliverables_met("plan", "simple", &completed, &[], true); + assert!(result.deliverables_met); + assert!(result.ready_to_advance); } #[test] - fn test_legacy_functions_default_to_simple() { - // Legacy get_phase_deliverables defaults to simple - let plan = get_phase_deliverables("plan"); - assert_eq!(plan.recommended_files.len(), 1); - assert_eq!(plan.recommended_files[0].template_id, "plan"); + fn test_phase_checklist() { + let completed = vec!["plan-document".to_string()]; + let checklist = get_phase_checklist_for_type("plan", &completed, &[], true, "simple"); + assert_eq!(checklist.completion_percentage, 100); + assert!(checklist.deliverables[0].completed); } } |
