diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/src/llm/phase_guidance.rs | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/src/llm/phase_guidance.rs')
| -rw-r--r-- | makima/src/llm/phase_guidance.rs | 594 |
1 files changed, 594 insertions, 0 deletions
diff --git a/makima/src/llm/phase_guidance.rs b/makima/src/llm/phase_guidance.rs new file mode 100644 index 0000000..e2d6cd8 --- /dev/null +++ b/makima/src/llm/phase_guidance.rs @@ -0,0 +1,594 @@ +//! 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)); + } + } + + // 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); + } +} |
