//! Phase guidance and deliverables tracking for contract management.
//!
//! This module provides structured guidance for each contract phase, tracking
//! expected deliverables and completion criteria.
//!
//! ## Contract Types
//!
//! ### Simple
//! - **Plan phase**: One required deliverable: "Plan"
//! - **Execute phase**: One required deliverable: "PR"
//!
//! ### Specification
//! - **Research phase**: One required deliverable: "Research Notes"
//! - **Specify phase**: One required deliverable: "Requirements Document"
//! - **Plan phase**: One required deliverable: "Plan"
//! - **Execute phase**: One required deliverable: "PR"
//! - **Review phase**: One required deliverable: "Release Notes"
//!
//! ### Execute
//! - **Execute phase only**: No deliverables at all
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Priority level for deliverables
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum DeliverablePriority {
/// Must be completed before advancing phase
Required,
/// Strongly suggested for phase completion
Recommended,
/// Nice to have, not blocking
Optional,
}
/// 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: DeliverablePriority,
/// Brief description of purpose
pub description: String,
}
/// Expected deliverables for a phase
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PhaseDeliverables {
/// Phase name
pub phase: String,
/// Deliverables for this phase
pub deliverables: Vec<Deliverable>,
/// Whether a repository is required for this phase
pub requires_repository: bool,
/// Whether tasks should be completed in this phase
pub requires_tasks: bool,
/// Guidance text for this phase
pub guidance: String,
}
/// Status of a deliverable
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeliverableStatus {
/// Deliverable ID
pub id: String,
/// Display name
pub name: String,
/// Priority
pub priority: DeliverablePriority,
/// Whether it has been completed
pub completed: bool,
}
/// Checklist for phase completion
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct PhaseChecklist {
/// Current phase
pub phase: String,
/// Deliverable status list
pub deliverables: Vec<DeliverableStatus>,
/// 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<TaskStats>,
/// Overall completion percentage (0-100)
pub completion_percentage: u8,
/// Summary message
pub summary: String,
/// Suggestions for next actions
pub suggestions: Vec<String>,
}
/// 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 task info for checklist building
pub struct TaskInfo {
pub name: String,
pub status: String,
}
use crate::db::models::PhaseConfig;
/// Get phase deliverables configuration (legacy, defaults to "simple" contract type)
pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables {
get_phase_deliverables_for_type(phase, "simple")
}
/// Get phase deliverables configuration for a specific contract type
pub fn get_phase_deliverables_for_type(phase: &str, contract_type: &str) -> PhaseDeliverables {
match contract_type {
"execute" => get_execute_type_deliverables(phase),
"specification" => get_specification_type_deliverables(phase),
"simple" | _ => get_simple_type_deliverables(phase),
}
}
/// Get phase deliverables from a custom PhaseConfig
/// This is used for contracts with custom templates
pub fn get_phase_deliverables_from_config(phase: &str, config: &PhaseConfig) -> PhaseDeliverables {
// Check if this phase exists in the config
let phase_exists = config.phases.iter().any(|p| p.id == phase);
if !phase_exists {
return PhaseDeliverables {
phase: phase.to_string(),
deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: format!("Phase '{}' is not defined in this contract template", phase),
};
}
// Get deliverables for this phase from the config
let deliverables: Vec<Deliverable> = config
.deliverables
.get(phase)
.map(|defs| {
defs.iter()
.map(|d| Deliverable {
id: d.id.clone(),
name: d.name.clone(),
priority: match d.priority.as_str() {
"recommended" => DeliverablePriority::Recommended,
"optional" => DeliverablePriority::Optional,
_ => DeliverablePriority::Required,
},
description: format!("{} deliverable", d.name),
})
.collect()
})
.unwrap_or_default();
// Determine if repository is required (typically for execute-like phases)
let requires_repository = phase == "execute" || phase == "plan";
// Determine if tasks are required (typically for execute phase)
let requires_tasks = phase == "execute";
// Find the phase name for better guidance
let phase_name = config
.phases
.iter()
.find(|p| p.id == phase)
.map(|p| p.name.clone())
.unwrap_or_else(|| phase.to_string());
let guidance = if deliverables.is_empty() {
format!("Complete the {} phase. No specific deliverables are required.", phase_name)
} else {
let deliverable_names: Vec<_> = deliverables.iter().map(|d| d.name.clone()).collect();
format!(
"Complete the {} phase by producing the following deliverables: {}",
phase_name,
deliverable_names.join(", ")
)
};
PhaseDeliverables {
phase: phase.to_string(),
deliverables,
requires_repository,
requires_tasks,
guidance,
}
}
/// Get phase deliverables, checking custom config first, then falling back to built-in types
pub fn get_phase_deliverables_with_config(
phase: &str,
contract_type: &str,
phase_config: Option<&PhaseConfig>,
) -> PhaseDeliverables {
// If we have a custom phase config, use it
if let Some(config) = phase_config {
return get_phase_deliverables_from_config(phase, config);
}
// Otherwise, fall back to built-in contract types
get_phase_deliverables_for_type(phase, contract_type)
}
/// Get deliverables for 'simple' contract type
fn get_simple_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"plan" => PhaseDeliverables {
phase: "plan".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(),
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(),
deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for simple contract type".to_string(),
},
}
}
/// Get deliverables for 'specification' contract type
fn get_specification_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"research" => PhaseDeliverables {
phase: "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(),
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(),
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(),
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(),
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(),
deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for specification contract type".to_string(),
},
}
}
/// Get deliverables for 'execute' contract type
fn get_execute_type_deliverables(phase: &str) -> PhaseDeliverables {
match phase {
"execute" => PhaseDeliverables {
phase: "execute".to_string(),
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(),
deliverables: vec![],
requires_repository: false,
requires_tasks: false,
guidance: "The 'execute' contract type only supports the 'execute' phase.".to_string(),
},
}
}
/// Build a phase checklist comparing expected vs actual deliverables
pub fn get_phase_checklist_for_type(
phase: &str,
completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
contract_type: &str,
) -> PhaseChecklist {
let phase_config = get_phase_deliverables_for_type(phase, contract_type);
// Build deliverable status list
let deliverables: Vec<DeliverableStatus> = phase_config
.deliverables
.iter()
.map(|d| DeliverableStatus {
id: d.id.clone(),
name: d.name.clone(),
priority: d.priority,
completed: completed_deliverables.contains(&d.id),
})
.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 deliverables (not optional)
for status in &deliverables {
if status.priority != DeliverablePriority::Optional {
total_items += 1;
if status.completed {
completed_items += 1;
}
}
}
// Count repository if required
if phase_config.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 deliverables
for status in &deliverables {
if !status.completed {
match status.priority {
DeliverablePriority::Required => {
suggestions.push(format!(
"Mark '{}' as complete using mark_deliverable_complete (required)",
status.name
));
}
DeliverablePriority::Recommended => {
suggestions.push(format!(
"Consider completing '{}' (recommended)",
status.name
));
}
DeliverablePriority::Optional => {}
}
}
}
// Suggest repository if needed
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 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
));
} else if stats.failed > 0 {
suggestions.push(format!("Address {} failed task(s)", stats.failed));
} else if stats.done == stats.total && stats.total > 0 {
suggestions.push("All tasks complete. Mark deliverables and advance phase.".to_string());
}
}
// Generate summary
let summary = generate_phase_summary(
phase,
&deliverables,
has_repository,
&task_stats,
completion_percentage,
);
PhaseChecklist {
phase: phase.to_string(),
deliverables,
has_repository,
repository_required: phase_config.requires_repository,
task_stats,
completion_percentage,
summary,
suggestions,
}
}
fn generate_phase_summary(
phase: &str,
deliverables: &[DeliverableStatus],
has_repository: bool,
task_stats: &Option<TaskStats>,
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. Mark deliverables complete when ready."
.to_string()
} else {
format!(
"{}/{} deliverables complete. Ready to transition to Specify phase.",
completed_count, total_count
)
}
}
"specify" => {
let has_required = deliverables
.iter()
.filter(|d| d.priority == DeliverablePriority::Required)
.all(|d| d.completed);
if !has_required {
"Specify phase requires completing the Requirements Document deliverable."
.to_string()
} else {
"Specifications ready. Consider transitioning to Plan phase.".to_string()
}
}
"plan" => {
let has_required = deliverables
.iter()
.filter(|d| d.priority == DeliverablePriority::Required)
.all(|d| d.completed);
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()
} 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 to implement the plan.".to_string()
} else if stats.done == stats.total {
"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
}
)
}
} else {
"Execute phase in progress.".to_string()
}
}
"review" => {
let has_required = deliverables
.iter()
.filter(|d| d.priority == DeliverablePriority::Required)
.all(|d| d.completed);
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()
}
}
_ => format!("Phase {} - {}% complete", phase, completion_percentage),
}
}
/// Result of checking if deliverables are met for the current phase
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct DeliverableCheckResult {
/// Whether all required deliverables are met
pub deliverables_met: bool,
/// Whether the phase is ready to advance (includes all readiness checks)
pub ready_to_advance: bool,
/// Current phase
pub phase: String,
/// Next phase (if available)
pub next_phase: Option<String>,
/// List of required deliverables and their status
pub required_deliverables: Vec<DeliverableItem>,
/// List of what's missing (if any)
pub missing: Vec<String>,
/// Human-readable summary
pub summary: String,
/// Whether auto-progress is recommended
pub auto_progress_recommended: bool,
}
/// 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: "deliverable", "repository", "tasks"
pub deliverable_type: String,
/// Whether it's met
pub met: bool,
/// Additional details
pub details: Option<String>,
}
/// Check if all required deliverables for the current phase are met
pub fn check_deliverables_met(
phase: &str,
contract_type: &str,
completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
) -> DeliverableCheckResult {
let mut required_items = Vec::new();
let mut missing = Vec::new();
// Get the deliverables for this contract type and phase
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 !is_complete {
missing.push(format!(
"Mark '{}' as complete (required)",
deliverable.name
));
}
}
}
// Check repository for phases that require it
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,
details: if has_repository {
Some("Repository configured".to_string())
} else {
None
},
});
if !has_repository {
missing.push("Configure a repository".to_string());
}
}
// Check tasks for execute phase
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_items.push(DeliverableItem {
id: "tasks".to_string(),
name: "Tasks Completed".to_string(),
deliverable_type: "tasks".to_string(),
met: tasks_complete,
details: Some(format!("{}/{} tasks done", done_tasks, total_tasks)),
});
if !tasks_complete {
if total_tasks == 0 {
missing.push("Create and complete tasks".to_string());
} else {
missing.push(format!(
"Complete remaining {} task(s)",
total_tasks - done_tasks
));
}
}
}
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
)
} else {
format!(
"All deliverables met for {} phase. This is the final phase.",
phase
)
}
} else {
format!(
"{} deliverable(s) still needed for {} phase.",
missing.len(),
phase
)
};
DeliverableCheckResult {
deliverables_met,
ready_to_advance,
phase: phase.to_string(),
next_phase,
required_deliverables: required_items,
missing,
summary,
auto_progress_recommended: deliverables_met && ready_to_advance,
}
}
/// Get the next phase based on contract type
pub fn get_next_phase_for_contract(contract_type: &str, current_phase: &str) -> Option<String> {
match contract_type {
"simple" => match current_phase {
"plan" => Some("execute".to_string()),
"execute" => None, // Terminal phase for simple contracts
_ => None,
},
"execute" => None, // Execute-only contracts don't have phase transitions
"specification" | _ => match current_phase {
"research" => Some("specify".to_string()),
"specify" => Some("plan".to_string()),
"plan" => Some("execute".to_string()),
"execute" => Some("review".to_string()),
"review" => None, // Final phase
_ => None,
},
}
}
/// Determine if the contract should auto-progress to the next phase
pub fn should_auto_progress(
phase: &str,
contract_type: &str,
completed_deliverables: &[String],
tasks: &[TaskInfo],
has_repository: bool,
autonomous_loop: bool,
) -> AutoProgressDecision {
let check = check_deliverables_met(
phase,
contract_type,
completed_deliverables,
tasks,
has_repository,
);
if !check.deliverables_met {
return AutoProgressDecision {
should_progress: false,
next_phase: None,
reason: format!("Deliverables not met: {}", check.missing.join(", ")),
action: AutoProgressAction::WaitForDeliverables,
};
}
if check.next_phase.is_none() {
return AutoProgressDecision {
should_progress: false,
next_phase: None,
reason: "This is the terminal phase. Contract can be completed.".to_string(),
action: AutoProgressAction::CompleteContract,
};
}
if autonomous_loop {
AutoProgressDecision {
should_progress: true,
next_phase: check.next_phase,
reason: "All deliverables met and autonomous_loop is enabled.".to_string(),
action: AutoProgressAction::AdvancePhase,
}
} else {
AutoProgressDecision {
should_progress: false,
next_phase: check.next_phase,
reason: "All deliverables met. Suggest advancing to next phase.".to_string(),
action: AutoProgressAction::SuggestAdvance,
}
}
}
/// Result of auto-progress decision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AutoProgressDecision {
/// Whether to automatically progress
pub should_progress: bool,
/// The next phase to progress to
pub next_phase: Option<String>,
/// Reason for the decision
pub reason: String,
/// Recommended action
pub action: AutoProgressAction,
}
/// Actions that can be taken based on auto-progress decision
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AutoProgressAction {
/// Wait for required deliverables
WaitForDeliverables,
/// Automatically advance to next phase
AdvancePhase,
/// Suggest user to advance (when not autonomous)
SuggestAdvance,
/// Contract is complete, mark as done
CompleteContract,
}
/// Generate enhanced prompt guidance for deliverable checking
pub fn generate_deliverable_prompt_guidance(
phase: &str,
contract_type: &str,
check_result: &DeliverableCheckResult,
) -> String {
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
));
// 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
));
}
// Show status and next actions
guidance.push_str("\n### Status\n");
if check_result.deliverables_met {
guidance.push_str("**All deliverables are met.**\n\n");
if let Some(ref next) = check_result.next_phase {
guidance.push_str(&format!("Ready to advance to **{}** phase.\n", next));
if check_result.auto_progress_recommended {
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",
);
}
} else {
guidance.push_str("**Deliverables not yet met.**\n\n");
guidance.push_str("Missing:\n");
for item in &check_result.missing {
guidance.push_str(&format!("- {}\n", item));
}
guidance.push_str(
"\nUse `mark_deliverable_complete` to mark deliverables as complete when ready.\n",
);
}
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)
);
// Deliverables
md.push_str("### Deliverables\n");
for status in &checklist.deliverables {
let check = if status.completed { "+" } else { "-" };
let priority_label = match status.priority {
DeliverablePriority::Required => " (required)",
DeliverablePriority::Recommended => " (recommended)",
DeliverablePriority::Optional => " (optional)",
};
if status.completed {
md.push_str(&format!("[{}] {} - completed\n", check, status.name));
} 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("\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::<String>() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_phase_deliverables_simple() {
let plan = get_phase_deliverables_for_type("plan", "simple");
assert_eq!(plan.phase, "plan");
assert!(plan.requires_repository);
assert_eq!(plan.deliverables.len(), 1);
assert_eq!(plan.deliverables[0].id, "plan-document");
assert_eq!(plan.deliverables[0].priority, DeliverablePriority::Required);
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.deliverables.len(), 1);
assert_eq!(execute.deliverables[0].id, "pull-request");
}
#[test]
fn test_get_phase_deliverables_specification() {
let research = get_phase_deliverables_for_type("research", "specification");
assert_eq!(research.deliverables.len(), 1);
assert_eq!(research.deliverables[0].id, "research-notes");
let specify = get_phase_deliverables_for_type("specify", "specification");
assert_eq!(specify.deliverables.len(), 1);
assert_eq!(specify.deliverables[0].id, "requirements-document");
let review = get_phase_deliverables_for_type("review", "specification");
assert_eq!(review.deliverables.len(), 1);
assert_eq!(review.deliverables[0].id, "release-notes");
}
#[test]
fn test_get_phase_deliverables_execute_type() {
let execute = get_phase_deliverables_for_type("execute", "execute");
assert!(execute.deliverables.is_empty());
assert!(execute.requires_repository);
assert!(execute.requires_tasks);
}
#[test]
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());
// 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_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);
}
}