//! 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;
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<RecommendedFile>,
/// 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<Uuid>,
/// Actual file name if created
pub actual_name: Option<String>,
}
/// 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<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 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,
}
/// 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
///
/// ## 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),
"specification" => get_specification_type_deliverables(phase),
"simple" | _ => get_simple_type_deliverables(phase),
}
}
/// 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(),
},
],
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(),
},
],
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![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for simple contract type".to_string(),
},
}
}
/// 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(),
},
],
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(),
},
],
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(),
},
],
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(),
},
],
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(),
},
],
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![],
requires_repository: false,
requires_tasks: false,
guidance: "Unknown phase for specification contract type".to_string(),
},
}
}
/// 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
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![],
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 (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
pub fn get_phase_checklist_for_type(
phase: &str,
files: &[FileInfo],
tasks: &[TaskInfo],
has_repository: bool,
contract_type: &str,
) -> PhaseChecklist {
let deliverables = get_phase_deliverables_for_type(phase, contract_type);
// Match files to expected deliverables
let file_deliverables: Vec<DeliverableStatus> = 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));
} 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());
}
}
// 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<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. 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 (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 {
/// 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 {
/// Name of the deliverable
pub name: String,
/// Type: "file", "repository", "pr", "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
/// This is used for both prompts and the check_deliverables_met tool
pub fn check_deliverables_met(
phase: &str,
contract_type: &str,
files: &[FileInfo],
tasks: &[TaskInfo],
has_repository: bool,
pr_url: Option<&str>,
) -> DeliverableCheckResult {
let mut required_deliverables = 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())
} else {
None
},
});
if !matched {
missing.push(format!("Create {} (required)", rec.name_suggestion));
}
}
}
// Check repository for phases that require it
if deliverables.requires_repository {
required_deliverables.push(DeliverableItem {
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 deliverables.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 {
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));
}
}
}
// 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 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,
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
/// This is called when deliverables are met and autonomous_loop is enabled
pub fn should_auto_progress(
phase: &str,
contract_type: &str,
files: &[FileInfo],
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);
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("\nComplete the missing deliverables before advancing to the next phase.\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));
// 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::<String>() + chars.as_str(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[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);
// 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);
}
#[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);
// 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");
// 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);
}
#[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.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()),
},
];
// Plan phase has required "Plan" file for simple contract type
let complete = check_phase_completion_for_type("plan", &files, &[], true, "simple");
assert!(complete);
}
#[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");
}
}