summaryrefslogblamecommitdiff
path: root/makima/src/llm/task_output.rs
blob: c7f699095d3c1fa8e450a68aec0b0ad437385355 (plain) (tree)




















































































                                                           



                                      






































                                                                                    
                                                                                                         


































































































                                                                                                                                                        
                                                                         









                                                                          




















                                                                                     
































































































































































































































                                                                                                    
//! Task output processing and task derivation utilities.
//!
//! This module provides utilities for:
//! - Parsing task lists from markdown documents
//! - Analyzing completed task outputs
//! - Suggesting follow-up actions based on task results

use regex::Regex;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// A parsed task from a markdown document
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParsedTask {
    /// Task name/title
    pub name: String,
    /// Task description or plan
    pub description: Option<String>,
    /// Group/phase this task belongs to
    pub group: Option<String>,
    /// Order within the group (0-indexed)
    pub order: usize,
    /// Whether this task was marked as completed in source
    pub completed: bool,
    /// Dependencies (names of other tasks)
    pub dependencies: Vec<String>,
}

/// Result of parsing tasks from a document
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskParseResult {
    /// Successfully parsed tasks
    pub tasks: Vec<ParsedTask>,
    /// Groups/phases found
    pub groups: Vec<String>,
    /// Total tasks found
    pub total: usize,
    /// Any parsing warnings
    pub warnings: Vec<String>,
}

/// Impact on contract phase
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhaseImpact {
    /// Current phase
    pub phase: String,
    /// Whether phase targets are now met
    pub targets_met: bool,
    /// Tasks remaining in phase
    pub tasks_remaining: usize,
    /// Suggestion for phase transition
    pub transition_suggestion: Option<String>,
}

/// Suggested action based on task output
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SuggestedAction {
    /// Create a follow-up task
    CreateTask {
        name: String,
        plan: String,
        chain_from: Option<Uuid>,
    },
    /// Create a new file from template
    CreateFile {
        template_id: String,
        name: String,
        seed_content: Option<String>,
    },
    /// Update an existing file
    UpdateFile {
        file_id: Uuid,
        file_name: String,
        additions: String,
    },
    /// Advance to next phase
    AdvancePhase {
        to_phase: String,
    },
    /// Run the next chained task
    RunNextTask {
        task_id: Uuid,
        task_name: String,
    },
    /// Mark the contract as completed
    MarkContractComplete {
        contract_id: Uuid,
    },
}

/// Analysis of a completed task's output
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskOutputAnalysis {
    /// Summary of what was accomplished
    pub summary: String,
    /// Files that were created/modified (from diff)
    pub files_affected: Vec<String>,
    /// Suggested next actions
    pub next_steps: Vec<SuggestedAction>,
    /// Impact on contract phase
    pub phase_impact: Option<PhaseImpact>,
}

/// Parse tasks from a markdown task breakdown document
///
/// Supports formats like:
/// - `[ ] Task name`
/// - `[x] Completed task`
/// - `1. Task name`
/// - `- Task name`
///
/// Groups are detected from `## Phase/Section` headings.
pub fn parse_tasks_from_breakdown(content: &str) -> TaskParseResult {
    let mut tasks = Vec::new();
    let mut groups = Vec::new();
    let mut warnings = Vec::new();
    let mut current_group: Option<String> = None;
    let mut task_order = 0;

    // Patterns for task items
    let checkbox_pattern = Regex::new(r"^\s*[-*]\s*\[([ xX])\]\s*(.+)$").unwrap();
    let numbered_checkbox = Regex::new(r"^\s*\d+\.\s*\[([ xX])\]\s*(.+)$").unwrap();
    let numbered_pattern = Regex::new(r"^\s*\d+\.\s+(.+)$").unwrap();
    let bullet_pattern = Regex::new(r"^\s*[-*]\s+(.+)$").unwrap();
    let heading_pattern = Regex::new(r"^##\s+(?:Phase\s*\d*:?\s*)?(.+)$").unwrap();

    // Patterns for dependencies (inline)
    let depends_pattern = Regex::new(r"(?i)\(?\s*(?:depends on|after|requires):?\s*([^)]+)\)?").unwrap();

    for line in content.lines() {
        let trimmed = line.trim();

        // Skip empty lines
        if trimmed.is_empty() {
            continue;
        }

        // Check for section headings
        if let Some(caps) = heading_pattern.captures(trimmed) {
            let group_name = caps[1].trim().to_string();
            if !groups.contains(&group_name) {
                groups.push(group_name.clone());
            }
            current_group = Some(group_name);
            task_order = 0;
            continue;
        }

        // Try to parse as a task
        let mut task_name: Option<String> = None;
        let mut completed = false;

        // Try checkbox patterns first (more specific)
        if let Some(caps) = checkbox_pattern.captures(trimmed) {
            completed = &caps[1] != " ";
            task_name = Some(caps[2].trim().to_string());
        } else if let Some(caps) = numbered_checkbox.captures(trimmed) {
            completed = &caps[1] != " ";
            task_name = Some(caps[2].trim().to_string());
        } else if let Some(caps) = numbered_pattern.captures(trimmed) {
            task_name = Some(caps[1].trim().to_string());
        } else if let Some(caps) = bullet_pattern.captures(trimmed) {
            // Only treat as task if it looks like a task (has actionable verbs)
            let text = caps[1].trim();
            if looks_like_task(text) {
                task_name = Some(text.to_string());
            }
        }

        if let Some(name) = task_name {
            // Skip items that are clearly not tasks
            if name.to_lowercase().starts_with("note:") ||
               name.to_lowercase().starts_with("todo:") && name.len() < 10 ||
               name.starts_with('#') {
                continue;
            }

            // Extract dependencies if present
            let dependencies = if let Some(dep_caps) = depends_pattern.captures(&name) {
                dep_caps[1]
                    .split(',')
                    .map(|s| s.trim().to_string())
                    .filter(|s| !s.is_empty())
                    .collect()
            } else {
                Vec::new()
            };

            // Clean task name (remove dependency info)
            let clean_name = depends_pattern.replace(&name, "").trim().to_string();

            // Extract description if there's a colon
            let (final_name, description) = if let Some(idx) = clean_name.find(':') {
                let (n, d) = clean_name.split_at(idx);
                (n.trim().to_string(), Some(d[1..].trim().to_string()))
            } else {
                (clean_name, None)
            };

            tasks.push(ParsedTask {
                name: final_name,
                description,
                group: current_group.clone(),
                order: task_order,
                completed,
                dependencies,
            });

            task_order += 1;
        }
    }

    let total = tasks.len();

    // Add warnings
    if tasks.is_empty() {
        warnings.push("No tasks found in document. Ensure tasks are formatted as checkbox items (- [ ] Task) or numbered lists (1. Task).".to_string());
    }

    TaskParseResult {
        tasks,
        groups,
        total,
        warnings,
    }
}

/// Check if text looks like a task (has action verbs at word boundaries)
fn looks_like_task(text: &str) -> bool {
    let lower = text.to_lowercase();
    let action_verbs = [
        "add", "create", "implement", "build", "write", "fix", "update",
        "refactor", "test", "configure", "set up", "setup", "deploy",
        "integrate", "migrate", "design", "review", "document", "remove",
        "delete", "modify", "change", "improve", "optimize", "enable",
        "disable", "install", "initialize", "define", "extend", "extract",
    ];

    // Check if text starts with an action verb (followed by space or end)
    for verb in &action_verbs {
        if lower.starts_with(verb) {
            // Check for word boundary after verb
            let after = &lower[verb.len()..];
            if after.is_empty() || after.starts_with(' ') || after.starts_with('_') {
                return true;
            }
        }
        // Check if verb appears after space with word boundary
        let pattern = format!(" {} ", verb);
        let pattern_end = format!(" {}", verb);
        if lower.contains(&pattern) {
            return true;
        }
        // Check if verb is at the end of string after a space
        if lower.ends_with(&pattern_end) && lower.len() > pattern_end.len() {
            return true;
        }
    }
    false
}

/// Analyze a completed task's output to suggest next actions
pub fn analyze_task_output(
    _task_id: Uuid,
    task_name: &str,
    task_result: Option<&str>,
    task_diff: Option<&str>,
    contract_phase: &str,
    total_tasks: usize,
    completed_tasks: usize,
    next_task: Option<(Uuid, String)>,
    dev_notes_file: Option<(Uuid, String)>,
) -> TaskOutputAnalysis {
    let mut next_steps = Vec::new();
    let mut files_affected = Vec::new();

    // Parse files from diff if available
    if let Some(diff) = task_diff {
        files_affected = extract_files_from_diff(diff);
    }

    // Generate summary
    let summary = if let Some(result) = task_result {
        if result.len() > 200 {
            format!("{}...", &result[..200])
        } else {
            result.to_string()
        }
    } else {
        format!("Task '{}' completed", task_name)
    };

    // If there's a next chained task, suggest running it
    if let Some((next_id, next_name)) = next_task {
        next_steps.push(SuggestedAction::RunNextTask {
            task_id: next_id,
            task_name: next_name,
        });
    }

    // Suggest updating Dev Notes if in execute phase and file exists
    if contract_phase == "execute" {
        if let Some((file_id, file_name)) = dev_notes_file {
            let additions = format!(
                "\n## Task: {}\n\n{}\n\n### Files Modified\n{}\n",
                task_name,
                summary,
                files_affected.iter()
                    .map(|f| format!("- {}", f))
                    .collect::<Vec<_>>()
                    .join("\n")
            );

            next_steps.push(SuggestedAction::UpdateFile {
                file_id,
                file_name,
                additions,
            });
        } else {
            // Suggest creating Dev Notes
            next_steps.push(SuggestedAction::CreateFile {
                template_id: "dev-notes".to_string(),
                name: "Development Notes".to_string(),
                seed_content: Some(format!(
                    "# Development Notes\n\n## Task: {}\n\n{}\n",
                    task_name, summary
                )),
            });
        }
    }

    // Calculate phase impact
    let new_completed = completed_tasks + 1;
    let targets_met = new_completed >= total_tasks && total_tasks > 0;
    let tasks_remaining = total_tasks.saturating_sub(new_completed);

    let transition_suggestion = if targets_met && contract_phase == "execute" {
        Some("All tasks complete. Ready to advance to Review phase.".to_string())
    } else {
        None
    };

    // If targets are met, suggest phase transition
    if targets_met && contract_phase == "execute" {
        next_steps.push(SuggestedAction::AdvancePhase {
            to_phase: "review".to_string(),
        });
    }

    let phase_impact = Some(PhaseImpact {
        phase: contract_phase.to_string(),
        targets_met,
        tasks_remaining,
        transition_suggestion,
    });

    TaskOutputAnalysis {
        summary,
        files_affected,
        next_steps,
        phase_impact,
    }
}

/// Extract file paths from a git diff
fn extract_files_from_diff(diff: &str) -> Vec<String> {
    let mut files = Vec::new();
    let file_pattern = Regex::new(r"^(?:diff --git a/|[+]{3} b/|[-]{3} a/)(.+)$").unwrap();

    for line in diff.lines() {
        if let Some(caps) = file_pattern.captures(line) {
            let path = caps[1].trim().to_string();
            // Skip /dev/null and duplicates
            if path != "/dev/null" && !files.contains(&path) {
                // Clean up path (remove a/ or b/ prefix from git diff)
                let clean_path = path.trim_start_matches("a/").trim_start_matches("b/").to_string();
                if !files.contains(&clean_path) {
                    files.push(clean_path);
                }
            }
        }
    }

    files
}

/// Format parsed tasks for display
pub fn format_parsed_tasks(result: &TaskParseResult) -> String {
    let mut output = String::new();

    if result.tasks.is_empty() {
        output.push_str("No tasks found in the document.\n");
        for warning in &result.warnings {
            output.push_str(&format!("Warning: {}\n", warning));
        }
        return output;
    }

    output.push_str(&format!("Found {} task(s)", result.total));
    if !result.groups.is_empty() {
        output.push_str(&format!(" in {} group(s)", result.groups.len()));
    }
    output.push_str(":\n\n");

    let mut current_group: Option<&str> = None;
    for (i, task) in result.tasks.iter().enumerate() {
        // Print group header if changed
        if task.group.as_deref() != current_group {
            current_group = task.group.as_deref();
            if let Some(group) = current_group {
                output.push_str(&format!("**{}**\n", group));
            }
        }

        let status = if task.completed { "[x]" } else { "[ ]" };
        output.push_str(&format!("{}. {} {}", i + 1, status, task.name));

        if !task.dependencies.is_empty() {
            output.push_str(&format!(" (depends on: {})", task.dependencies.join(", ")));
        }

        output.push('\n');
    }

    output
}

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

    #[test]
    fn test_parse_checkbox_tasks() {
        let content = r#"
## Phase 1: Setup
- [ ] Set up project structure
- [x] Configure dev environment

## Phase 2: Features
1. [ ] Implement authentication
2. [ ] Add user dashboard
"#;

        let result = parse_tasks_from_breakdown(content);
        assert_eq!(result.total, 4);
        assert_eq!(result.groups.len(), 2);
        assert!(!result.tasks[0].completed);
        assert!(result.tasks[1].completed);
    }

    #[test]
    fn test_parse_with_dependencies() {
        let content = r#"
- [ ] Task A
- [ ] Task B (depends on: Task A)
"#;

        let result = parse_tasks_from_breakdown(content);
        assert_eq!(result.tasks[1].dependencies, vec!["Task A"]);
    }

    #[test]
    fn test_extract_files_from_diff() {
        let diff = r#"
diff --git a/src/main.rs b/src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,4 @@
+fn new_function() {}
"#;

        let files = extract_files_from_diff(diff);
        assert!(files.contains(&"src/main.rs".to_string()));
    }

    #[test]
    fn test_looks_like_task() {
        assert!(looks_like_task("Add authentication"));
        assert!(looks_like_task("Create user model"));
        assert!(looks_like_task("implement feature X"));
        assert!(!looks_like_task("This is a note"));
        assert!(!looks_like_task("Summary of changes"));
    }
}