//! Dependency analysis for task execution ordering. //! //! This module provides functionality to: //! - Check if a task's dependencies are satisfied before it can start //! - Auto-detect dependency patterns based on file patterns //! - Warn about potential dependency violations use std::collections::HashSet; use uuid::Uuid; /// Dependency ordering configuration. #[derive(Debug, Clone)] pub struct DependencyOrderingConfig { /// Enable dependency ordering checks. pub enabled: bool, /// Auto-detect dependencies from file patterns. pub auto_detect: bool, /// Warn on detected dependency violations. pub warn_on_violation: bool, } impl Default for DependencyOrderingConfig { fn default() -> Self { Self { enabled: true, auto_detect: true, warn_on_violation: true, } } } /// Result of a dependency check. #[derive(Debug, Clone)] pub struct DependencyCheckResult { /// Whether the task can start (all dependencies satisfied). pub can_start: bool, /// IDs of tasks that are still pending/running. pub unmet_dependencies: Vec, /// Warnings about potential dependency issues. pub warnings: Vec, } impl DependencyCheckResult { /// Create a result indicating the task can start. pub fn can_start() -> Self { Self { can_start: true, unmet_dependencies: Vec::new(), warnings: Vec::new(), } } /// Create a result indicating the task cannot start due to unmet dependencies. pub fn blocked(unmet: Vec) -> Self { Self { can_start: false, unmet_dependencies: unmet, warnings: Vec::new(), } } /// Add a warning to the result. pub fn with_warning(mut self, warning: String) -> Self { self.warnings.push(warning); self } } /// File pattern categories for dependency detection. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum FileCategory { /// Schema/migration files (must run first). Schema, /// Type/model definitions. Types, /// Backend/API code. Backend, /// UI components (must run last). Ui, /// Unknown/uncategorized files. Unknown, } impl FileCategory { /// Determine the category of a file based on its path. pub fn from_path(path: &str) -> Self { let path_lower = path.to_lowercase(); // Schema/migration patterns if path_lower.contains("migration") || path_lower.contains("schema") || path_lower.ends_with(".sql") || path_lower.contains("prisma/schema") || path_lower.contains("drizzle") { return FileCategory::Schema; } // Type/model patterns if path_lower.contains("/types") || path_lower.contains("/models") || path_lower.contains("/entities") || path_lower.ends_with(".d.ts") || path_lower.ends_with("types.ts") || path_lower.ends_with("types.rs") || path_lower.ends_with("models.rs") { return FileCategory::Types; } // UI patterns if path_lower.contains("/components") || path_lower.contains("/views") || path_lower.contains("/pages") || path_lower.contains("/ui/") || path_lower.ends_with(".tsx") || path_lower.ends_with(".jsx") || path_lower.ends_with(".vue") || path_lower.ends_with(".svelte") { return FileCategory::Ui; } // Backend patterns if path_lower.contains("/api") || path_lower.contains("/server") || path_lower.contains("/handlers") || path_lower.contains("/services") || path_lower.contains("/controllers") || path_lower.contains("/routes") || path_lower.ends_with(".rs") || path_lower.ends_with(".go") || path_lower.ends_with(".py") { return FileCategory::Backend; } FileCategory::Unknown } /// Get the expected execution order (lower = earlier). pub fn execution_order(&self) -> u8 { match self { FileCategory::Schema => 0, FileCategory::Types => 1, FileCategory::Backend => 2, FileCategory::Ui => 3, FileCategory::Unknown => 2, // Treat unknown as backend-level } } } /// Analyze a task's plan to determine what file categories it affects. pub fn analyze_task_files(plan: &str) -> HashSet { let mut categories = HashSet::new(); // Simple heuristic: look for file patterns in the plan text let patterns_schema = [ "migration", "schema", ".sql", "prisma", "drizzle", "database", "ALTER TABLE", "CREATE TABLE", "DROP TABLE", ]; let patterns_types = [ "type ", "interface ", "struct ", "model ", "entity", "types.ts", "types.rs", "models.rs", ".d.ts", ]; let patterns_backend = [ "api", "endpoint", "handler", "controller", "route", "service", "server", "backend", "REST", "GraphQL", ]; let patterns_ui = [ "component", "view", "page", "ui ", "frontend", "react", ".tsx", ".jsx", ".vue", ".svelte", "button", "form", ]; let plan_lower = plan.to_lowercase(); for pattern in patterns_schema { if plan_lower.contains(&pattern.to_lowercase()) { categories.insert(FileCategory::Schema); break; } } for pattern in patterns_types { if plan_lower.contains(&pattern.to_lowercase()) { categories.insert(FileCategory::Types); break; } } for pattern in patterns_backend { if plan_lower.contains(&pattern.to_lowercase()) { categories.insert(FileCategory::Backend); break; } } for pattern in patterns_ui { if plan_lower.contains(&pattern.to_lowercase()) { categories.insert(FileCategory::Ui); break; } } categories } /// Information about a task's status for dependency checking. #[derive(Debug, Clone)] pub struct TaskDependencyInfo { pub id: Uuid, pub status: String, pub plan: String, } impl TaskDependencyInfo { /// Check if this task is considered "complete" for dependency purposes. pub fn is_complete(&self) -> bool { matches!(self.status.as_str(), "done" | "merged") } } /// Check if a task can start based on its explicit dependencies. /// /// Returns a `DependencyCheckResult` indicating whether the task can start /// and listing any unmet dependencies. pub fn can_start_task( depends_on: &[Uuid], dependency_tasks: &[TaskDependencyInfo], _config: &DependencyOrderingConfig, ) -> DependencyCheckResult { // If no explicit dependencies, the task can start if depends_on.is_empty() { return DependencyCheckResult::can_start(); } let complete_task_ids: HashSet = dependency_tasks .iter() .filter(|t| t.is_complete()) .map(|t| t.id) .collect(); let unmet: Vec = depends_on .iter() .filter(|dep_id| !complete_task_ids.contains(dep_id)) .copied() .collect(); if unmet.is_empty() { DependencyCheckResult::can_start() } else { DependencyCheckResult::blocked(unmet) } } /// Auto-detect potential dependency violations based on file patterns. /// /// This analyzes the task's plan and compares it with sibling tasks to warn /// if execution order might be problematic. pub fn detect_dependency_violations( task_plan: &str, sibling_tasks: &[TaskDependencyInfo], config: &DependencyOrderingConfig, ) -> Vec { if !config.auto_detect || !config.warn_on_violation { return Vec::new(); } let mut warnings = Vec::new(); let task_categories = analyze_task_files(task_plan); // Find the minimum execution order this task touches let task_min_order = task_categories .iter() .map(|c| c.execution_order()) .min() .unwrap_or(u8::MAX); // Check if any pending/running sibling tasks should run first for sibling in sibling_tasks { if sibling.is_complete() { continue; } let sibling_categories = analyze_task_files(&sibling.plan); let sibling_min_order = sibling_categories .iter() .map(|c| c.execution_order()) .min() .unwrap_or(u8::MAX); // Warn if this task touches "earlier" categories while sibling touches "later" ones // and the sibling is still pending if sibling_min_order < task_min_order { let sibling_cat_names: Vec<&str> = sibling_categories .iter() .map(|c| match c { FileCategory::Schema => "schema/migrations", FileCategory::Types => "types/models", FileCategory::Backend => "backend/API", FileCategory::Ui => "UI components", FileCategory::Unknown => "other", }) .collect(); let task_cat_names: Vec<&str> = task_categories .iter() .map(|c| match c { FileCategory::Schema => "schema/migrations", FileCategory::Types => "types/models", FileCategory::Backend => "backend/API", FileCategory::Ui => "UI components", FileCategory::Unknown => "other", }) .collect(); warnings.push(format!( "Task may depend on sibling task {} which affects {} (this task affects {})", sibling.id, sibling_cat_names.join(", "), task_cat_names.join(", ") )); } } warnings } /// Suggest automatic dependencies based on file pattern analysis. /// /// This analyzes tasks in a contract and suggests which tasks should depend on others /// based on the Ralph pattern: schema -> types -> backend -> UI. pub fn suggest_dependencies( task_plan: &str, sibling_tasks: &[TaskDependencyInfo], ) -> Vec { let mut suggested = Vec::new(); let task_categories = analyze_task_files(task_plan); // Get this task's minimum execution order let task_min_order = task_categories .iter() .map(|c| c.execution_order()) .min() .unwrap_or(u8::MAX); // Suggest dependencies on tasks that touch "earlier" categories for sibling in sibling_tasks { let sibling_categories = analyze_task_files(&sibling.plan); let sibling_max_order = sibling_categories .iter() .map(|c| c.execution_order()) .max() .unwrap_or(0); // If sibling's work is at an earlier stage, suggest it as a dependency if sibling_max_order < task_min_order { suggested.push(sibling.id); } } suggested } #[cfg(test)] mod tests { use super::*; #[test] fn test_file_category_from_path() { assert_eq!( FileCategory::from_path("migrations/001_create_users.sql"), FileCategory::Schema ); assert_eq!( FileCategory::from_path("src/types/user.ts"), FileCategory::Types ); assert_eq!( FileCategory::from_path("src/api/users.rs"), FileCategory::Backend ); assert_eq!( FileCategory::from_path("src/components/UserProfile.tsx"), FileCategory::Ui ); } #[test] fn test_execution_order() { assert!(FileCategory::Schema.execution_order() < FileCategory::Types.execution_order()); assert!(FileCategory::Types.execution_order() < FileCategory::Backend.execution_order()); assert!(FileCategory::Backend.execution_order() < FileCategory::Ui.execution_order()); } #[test] fn test_analyze_task_files() { let plan = "Create a new migration to add the users table"; let categories = analyze_task_files(plan); assert!(categories.contains(&FileCategory::Schema)); let plan = "Implement the UserProfile component with React"; let categories = analyze_task_files(plan); assert!(categories.contains(&FileCategory::Ui)); } #[test] fn test_can_start_task_no_deps() { let result = can_start_task(&[], &[], &DependencyOrderingConfig::default()); assert!(result.can_start); assert!(result.unmet_dependencies.is_empty()); } #[test] fn test_can_start_task_with_complete_deps() { let dep_id = Uuid::new_v4(); let dep_task = TaskDependencyInfo { id: dep_id, status: "done".to_string(), plan: "Some completed task".to_string(), }; let result = can_start_task( &[dep_id], &[dep_task], &DependencyOrderingConfig::default(), ); assert!(result.can_start); assert!(result.unmet_dependencies.is_empty()); } #[test] fn test_can_start_task_with_pending_deps() { let dep_id = Uuid::new_v4(); let dep_task = TaskDependencyInfo { id: dep_id, status: "running".to_string(), plan: "Some running task".to_string(), }; let result = can_start_task( &[dep_id], &[dep_task], &DependencyOrderingConfig::default(), ); assert!(!result.can_start); assert_eq!(result.unmet_dependencies, vec![dep_id]); } #[test] fn test_detect_dependency_violations() { let ui_task_plan = "Create the UserProfile component"; let schema_sibling = TaskDependencyInfo { id: Uuid::new_v4(), status: "pending".to_string(), plan: "Add migration to create users table".to_string(), }; let warnings = detect_dependency_violations( ui_task_plan, &[schema_sibling], &DependencyOrderingConfig::default(), ); // Should warn that UI task might depend on schema task assert!(!warnings.is_empty()); } #[test] fn test_suggest_dependencies() { let schema_task = TaskDependencyInfo { id: Uuid::new_v4(), status: "pending".to_string(), plan: "Add migration to create users table".to_string(), }; let ui_task_plan = "Create the UserProfile component"; let suggested = suggest_dependencies(ui_task_plan, &[schema_task.clone()]); // Should suggest schema task as a dependency for UI task assert!(suggested.contains(&schema_task.id)); } }