summaryrefslogblamecommitdiff
path: root/makima/src/llm/phase_guidance.rs
blob: 0d4bb3d12728a16a7537f635fc5be33cb7ad1c07 (plain) (tree)






















































































































































































































































































































































































                                                                                                                                                                                                     












                                                                                                                             


























































































































































































































                                                                                                                                   
//! Phase guidance and deliverables tracking for contract management.
//!
//! This module provides structured guidance for each contract phase, tracking
//! expected deliverables and completion criteria.

use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;

/// Priority level for recommended deliverables
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum FilePriority {
    /// Must exist before advancing phase
    Required,
    /// Strongly suggested for phase completion
    Recommended,
    /// Nice to have, not blocking
    Optional,
}

/// A recommended file for a phase
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecommendedFile {
    /// Template ID to create from
    pub template_id: String,
    /// Suggested file name
    pub name_suggestion: String,
    /// Priority level
    pub priority: FilePriority,
    /// Brief description of purpose
    pub description: String,
}

/// Expected deliverables for a phase
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseDeliverables {
    /// Phase name
    pub phase: String,
    /// Recommended files to create
    pub recommended_files: Vec<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
pub fn get_phase_deliverables(phase: &str) -> PhaseDeliverables {
    match phase {
        "research" => PhaseDeliverables {
            phase: "research".to_string(),
            recommended_files: vec![
                RecommendedFile {
                    template_id: "research-notes".to_string(),
                    name_suggestion: "Research Notes".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Document findings and insights during research".to_string(),
                },
                RecommendedFile {
                    template_id: "competitor-analysis".to_string(),
                    name_suggestion: "Competitor Analysis".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Analyze competitors and market positioning".to_string(),
                },
                RecommendedFile {
                    template_id: "user-research".to_string(),
                    name_suggestion: "User Research".to_string(),
                    priority: FilePriority::Optional,
                    description: "Document user interviews and persona insights".to_string(),
                },
            ],
            requires_repository: false,
            requires_tasks: false,
            guidance: "Focus on understanding the problem space, gathering information, and documenting findings. Create at least one research document before moving to Specify phase.".to_string(),
        },
        "specify" => PhaseDeliverables {
            phase: "specify".to_string(),
            recommended_files: vec![
                RecommendedFile {
                    template_id: "requirements".to_string(),
                    name_suggestion: "Requirements Document".to_string(),
                    priority: FilePriority::Required,
                    description: "Define functional and non-functional requirements".to_string(),
                },
                RecommendedFile {
                    template_id: "user-stories".to_string(),
                    name_suggestion: "User Stories".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Define features from the user's perspective".to_string(),
                },
                RecommendedFile {
                    template_id: "acceptance-criteria".to_string(),
                    name_suggestion: "Acceptance Criteria".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Define testable conditions for completion".to_string(),
                },
            ],
            requires_repository: false,
            requires_tasks: false,
            guidance: "Define what needs to be built with clear requirements and acceptance criteria. Ensure specifications are detailed enough for planning.".to_string(),
        },
        "plan" => PhaseDeliverables {
            phase: "plan".to_string(),
            recommended_files: vec![
                RecommendedFile {
                    template_id: "architecture".to_string(),
                    name_suggestion: "Architecture Document".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Document system architecture and design decisions".to_string(),
                },
                RecommendedFile {
                    template_id: "task-breakdown".to_string(),
                    name_suggestion: "Task Breakdown".to_string(),
                    priority: FilePriority::Required,
                    description: "Break down work into implementable tasks".to_string(),
                },
                RecommendedFile {
                    template_id: "technical-design".to_string(),
                    name_suggestion: "Technical Design".to_string(),
                    priority: FilePriority::Optional,
                    description: "Detailed technical specification".to_string(),
                },
            ],
            requires_repository: true,
            requires_tasks: false,
            guidance: "Design the solution and break down work into tasks. A repository must be configured before moving to Execute phase.".to_string(),
        },
        "execute" => PhaseDeliverables {
            phase: "execute".to_string(),
            recommended_files: vec![
                RecommendedFile {
                    template_id: "dev-notes".to_string(),
                    name_suggestion: "Development Notes".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Track implementation details and decisions".to_string(),
                },
                RecommendedFile {
                    template_id: "test-plan".to_string(),
                    name_suggestion: "Test Plan".to_string(),
                    priority: FilePriority::Optional,
                    description: "Document testing strategy and test cases".to_string(),
                },
                RecommendedFile {
                    template_id: "implementation-log".to_string(),
                    name_suggestion: "Implementation Log".to_string(),
                    priority: FilePriority::Optional,
                    description: "Chronological log of implementation progress".to_string(),
                },
            ],
            requires_repository: true,
            requires_tasks: true,
            guidance: "Execute the planned tasks, implement features, and track progress. Complete all tasks before moving to Review phase.".to_string(),
        },
        "review" => PhaseDeliverables {
            phase: "review".to_string(),
            recommended_files: vec![
                RecommendedFile {
                    template_id: "release-notes".to_string(),
                    name_suggestion: "Release Notes".to_string(),
                    priority: FilePriority::Required,
                    description: "Document changes for release communication".to_string(),
                },
                RecommendedFile {
                    template_id: "review-checklist".to_string(),
                    name_suggestion: "Review Checklist".to_string(),
                    priority: FilePriority::Recommended,
                    description: "Comprehensive checklist for code and feature review".to_string(),
                },
                RecommendedFile {
                    template_id: "retrospective".to_string(),
                    name_suggestion: "Retrospective".to_string(),
                    priority: FilePriority::Optional,
                    description: "Reflect on the project and capture learnings".to_string(),
                },
            ],
            requires_repository: false,
            requires_tasks: false,
            guidance: "Review completed work, document the release, and conduct a retrospective. The contract can be completed after review.".to_string(),
        },
        _ => PhaseDeliverables {
            phase: phase.to_string(),
            recommended_files: vec![],
            requires_repository: false,
            requires_tasks: false,
            guidance: "Unknown phase".to_string(),
        },
    }
}

/// Build a phase checklist comparing expected vs actual deliverables
pub fn get_phase_checklist(
    phase: &str,
    files: &[FileInfo],
    tasks: &[TaskInfo],
    has_repository: bool,
) -> PhaseChecklist {
    let deliverables = get_phase_deliverables(phase);

    // Match files to expected deliverables
    let file_deliverables: Vec<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
pub fn check_phase_completion(
    phase: &str,
    files: &[FileInfo],
    tasks: &[TaskInfo],
    has_repository: bool,
) -> bool {
    let checklist = get_phase_checklist(phase, files, tasks, has_repository);

    // Check required files are complete
    let required_files_complete = checklist.file_deliverables.iter()
        .filter(|d| d.priority == FilePriority::Required)
        .all(|d| d.completed);

    // Check repository if required
    let repository_ok = !checklist.repository_required || checklist.has_repository;

    // Check tasks if in execute phase
    let tasks_ok = if let Some(stats) = &checklist.task_stats {
        stats.total > 0 && stats.done == stats.total
    } else {
        true
    };

    required_files_complete && repository_ok && tasks_ok
}

/// Format checklist as markdown for LLM context
pub fn format_checklist_markdown(checklist: &PhaseChecklist) -> String {
    let mut md = format!("## Phase Progress ({} Phase)\n\n", capitalize(&checklist.phase));

    // File deliverables
    md.push_str("### Deliverables\n");
    for status in &checklist.file_deliverables {
        let check = if status.completed { "+" } else { "-" };
        let priority_label = match status.priority {
            FilePriority::Required => " (required)",
            FilePriority::Recommended => " (recommended)",
            FilePriority::Optional => " (optional)",
        };

        if status.completed {
            md.push_str(&format!("[{}] {} - \"{}\"\n", check, status.name, status.actual_name.as_deref().unwrap_or("created")));
        } else {
            md.push_str(&format!("[{}] {}{}\n", check, status.name, priority_label));
        }
    }

    // Repository status
    if checklist.repository_required {
        let check = if checklist.has_repository { "+" } else { "-" };
        md.push_str(&format!("[{}] Repository configured (required)\n", check));
    }

    // Task stats for execute phase
    if let Some(ref stats) = checklist.task_stats {
        md.push_str(&format!("\n### Task Progress\n"));
        md.push_str(&format!("- Total: {}\n", stats.total));
        md.push_str(&format!("- Done: {}\n", stats.done));
        if stats.pending > 0 {
            md.push_str(&format!("- Pending: {}\n", stats.pending));
        }
        if stats.running > 0 {
            md.push_str(&format!("- Running: {}\n", stats.running));
        }
        if stats.failed > 0 {
            md.push_str(&format!("- Failed: {}\n", stats.failed));
        }
    }

    // Summary
    md.push_str(&format!("\n**Status**: {} ({}% complete)\n", checklist.summary, checklist.completion_percentage));

    // Suggestions
    if !checklist.suggestions.is_empty() {
        md.push_str("\n**Next Steps**:\n");
        for suggestion in &checklist.suggestions {
            md.push_str(&format!("- {}\n", suggestion));
        }
    }

    md
}

fn capitalize(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        None => String::new(),
        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_get_phase_deliverables() {
        let research = get_phase_deliverables("research");
        assert_eq!(research.phase, "research");
        assert!(!research.requires_repository);
        assert_eq!(research.recommended_files.len(), 3);

        let plan = get_phase_deliverables("plan");
        assert!(plan.requires_repository);
        assert!(plan.recommended_files.iter().any(|f| f.template_id == "task-breakdown"));
    }

    #[test]
    fn test_phase_checklist_empty() {
        let checklist = get_phase_checklist("research", &[], &[], false);
        assert_eq!(checklist.completion_percentage, 0);
        assert!(!checklist.suggestions.is_empty());
    }

    #[test]
    fn test_check_phase_completion() {
        let files = vec![
            FileInfo {
                id: Uuid::new_v4(),
                name: "Requirements Document".to_string(),
                contract_phase: Some("specify".to_string()),
            },
        ];

        // Specify phase has required file
        let complete = check_phase_completion("specify", &files, &[], false);
        assert!(complete);
    }
}