package graph import ( "fmt" "sync" ) // Task represents a node in the dependency graph type Task struct { ID string Title string Status string // pending, ready, running, completed, failed DependsOn []string Metadata map[string]interface{} } // DependencyGraph manages task dependencies type DependencyGraph struct { mu sync.RWMutex tasks map[string]*Task adjacencyList map[string][]string // task -> dependent tasks reverseList map[string][]string // task -> dependencies topologicalOrder []string cycleDetected bool status map[string]string // task -> status } // NewDependencyGraph creates a new dependency graph func NewDependencyGraph() *DependencyGraph { return &DependencyGraph{ tasks: make(map[string]*Task), adjacencyList: make(map[string][]string), reverseList: make(map[string][]string), topologicalOrder: make([]string, 0), status: make(map[string]string), } } // AddTask adds a task to the graph func (dg *DependencyGraph) AddTask(task *Task) error { if task == nil || task.ID == "" { return fmt.Errorf("task cannot be nil and must have an ID") } dg.mu.Lock() defer dg.mu.Unlock() if _, exists := dg.tasks[task.ID]; exists { return fmt.Errorf("task already exists: %s", task.ID) } dg.tasks[task.ID] = task dg.status[task.ID] = "pending" // Initialize adjacency lists if _, exists := dg.adjacencyList[task.ID]; !exists { dg.adjacencyList[task.ID] = make([]string, 0) } if _, exists := dg.reverseList[task.ID]; !exists { dg.reverseList[task.ID] = make([]string, 0) } return nil } // AddDependency adds a dependency: dependent depends on prerequisite func (dg *DependencyGraph) AddDependency(dependent, prerequisite string) error { dg.mu.Lock() defer dg.mu.Unlock() if _, exists := dg.tasks[dependent]; !exists { return fmt.Errorf("dependent task not found: %s", dependent) } if _, exists := dg.tasks[prerequisite]; !exists { return fmt.Errorf("prerequisite task not found: %s", prerequisite) } // Check for duplicate for _, dep := range dg.reverseList[dependent] { if dep == prerequisite { return fmt.Errorf("dependency already exists: %s -> %s", dependent, prerequisite) } } dg.reverseList[dependent] = append(dg.reverseList[dependent], prerequisite) dg.adjacencyList[prerequisite] = append(dg.adjacencyList[prerequisite], dependent) return nil } // ValidateGraph checks for cycles and structural integrity func (dg *DependencyGraph) ValidateGraph() error { dg.mu.Lock() defer dg.mu.Unlock() // Check for cycles using DFS visited := make(map[string]bool) recStack := make(map[string]bool) for taskID := range dg.tasks { if !visited[taskID] { if dg.hasCycleLocked(taskID, visited, recStack) { dg.cycleDetected = true return fmt.Errorf("cycle detected in dependency graph") } } } return nil } // hasCycleLocked detects cycles using DFS (must be called with lock held) func (dg *DependencyGraph) hasCycleLocked(node string, visited, recStack map[string]bool) bool { visited[node] = true recStack[node] = true for _, dep := range dg.reverseList[node] { if !visited[dep] { if dg.hasCycleLocked(dep, visited, recStack) { return true } } else if recStack[dep] { return true } } recStack[node] = false return false } // GetTopologicalOrder returns tasks in execution order func (dg *DependencyGraph) GetTopologicalOrder() ([]string, error) { dg.mu.Lock() defer dg.mu.Unlock() if dg.cycleDetected { return nil, fmt.Errorf("graph contains cycles") } // Kahn's algorithm inDegree := make(map[string]int) for taskID := range dg.tasks { inDegree[taskID] = len(dg.reverseList[taskID]) } queue := make([]string, 0) for taskID, degree := range inDegree { if degree == 0 { queue = append(queue, taskID) } } topOrder := make([]string, 0) for len(queue) > 0 { current := queue[0] queue = queue[1:] topOrder = append(topOrder, current) for _, dependent := range dg.adjacencyList[current] { inDegree[dependent]-- if inDegree[dependent] == 0 { queue = append(queue, dependent) } } } if len(topOrder) != len(dg.tasks) { return nil, fmt.Errorf("topological sort failed - graph may have cycles") } dg.topologicalOrder = topOrder return topOrder, nil } // GetReadyTasks returns tasks that have no remaining dependencies func (dg *DependencyGraph) GetReadyTasks() []string { dg.mu.RLock() defer dg.mu.RUnlock() ready := make([]string, 0) for taskID, deps := range dg.reverseList { allDepsComplete := true for _, dep := range deps { if dg.status[dep] != "completed" { allDepsComplete = false break } } if allDepsComplete && dg.status[taskID] == "pending" { ready = append(ready, taskID) } } return ready } // MarkCompleted marks a task as completed and updates dependents func (dg *DependencyGraph) MarkCompleted(taskID string) error { dg.mu.Lock() defer dg.mu.Unlock() if _, exists := dg.tasks[taskID]; !exists { return fmt.Errorf("task not found: %s", taskID) } dg.status[taskID] = "completed" return nil } // MarkFailed marks a task as failed func (dg *DependencyGraph) MarkFailed(taskID string) error { dg.mu.Lock() defer dg.mu.Unlock() if _, exists := dg.tasks[taskID]; !exists { return fmt.Errorf("task not found: %s", taskID) } dg.status[taskID] = "failed" return nil } // GetTaskStatus returns the status of a task func (dg *DependencyGraph) GetTaskStatus(taskID string) (string, error) { dg.mu.RLock() defer dg.mu.RUnlock() status, exists := dg.status[taskID] if !exists { return "", fmt.Errorf("task not found: %s", taskID) } return status, nil } // GetDependencies returns all dependencies of a task func (dg *DependencyGraph) GetDependencies(taskID string) ([]string, error) { dg.mu.RLock() defer dg.mu.RUnlock() deps, exists := dg.reverseList[taskID] if !exists { return nil, fmt.Errorf("task not found: %s", taskID) } result := make([]string, len(deps)) copy(result, deps) return result, nil } // GetDependents returns all tasks that depend on this task func (dg *DependencyGraph) GetDependents(taskID string) ([]string, error) { dg.mu.RLock() defer dg.mu.RUnlock() deps, exists := dg.adjacencyList[taskID] if !exists { return nil, fmt.Errorf("task not found: %s", taskID) } result := make([]string, len(deps)) copy(result, deps) return result, nil } // GetTask returns a task by ID func (dg *DependencyGraph) GetTask(taskID string) (*Task, bool) { dg.mu.RLock() defer dg.mu.RUnlock() task, exists := dg.tasks[taskID] return task, exists } // GetAllTasks returns all tasks func (dg *DependencyGraph) GetAllTasks() map[string]*Task { dg.mu.RLock() defer dg.mu.RUnlock() result := make(map[string]*Task) for id, task := range dg.tasks { result[id] = task } return result } // GetGraphStats returns statistics about the graph func (dg *DependencyGraph) GetGraphStats() map[string]interface{} { dg.mu.RLock() defer dg.mu.RUnlock() pending := 0 completed := 0 failed := 0 for _, status := range dg.status { switch status { case "pending": pending++ case "completed": completed++ case "failed": failed++ } } return map[string]interface{}{ "total_tasks": len(dg.tasks), "pending_tasks": pending, "completed_tasks": completed, "failed_tasks": failed, "cycle_detected": dg.cycleDetected, "total_edges": dg.countEdgesLocked(), } } // countEdgesLocked counts total dependencies (must be called with lock held) func (dg *DependencyGraph) countEdgesLocked() int { count := 0 for _, deps := range dg.reverseList { count += len(deps) } return count } // Clear clears all tasks and dependencies func (dg *DependencyGraph) Clear() { dg.mu.Lock() defer dg.mu.Unlock() dg.tasks = make(map[string]*Task) dg.adjacencyList = make(map[string][]string) dg.reverseList = make(map[string][]string) dg.topologicalOrder = make([]string, 0) dg.status = make(map[string]string) dg.cycleDetected = false } // CanExecuteTask checks if a task can be executed (all deps complete) func (dg *DependencyGraph) CanExecuteTask(taskID string) bool { dg.mu.RLock() defer dg.mu.RUnlock() deps, exists := dg.reverseList[taskID] if !exists { return false } for _, dep := range deps { if dg.status[dep] != "completed" { return false } } return true } // GetCriticalPath returns the longest path through the graph func (dg *DependencyGraph) GetCriticalPath() []string { dg.mu.RLock() defer dg.mu.RUnlock() // Use longest path algorithm distances := make(map[string]int) parent := make(map[string]string) for taskID := range dg.tasks { distances[taskID] = 0 } // Process in topological order for _, taskID := range dg.topologicalOrder { for _, dependent := range dg.adjacencyList[taskID] { if distances[dependent] < distances[taskID]+1 { distances[dependent] = distances[taskID] + 1 parent[dependent] = taskID } } } // Find task with maximum distance maxDist := 0 endTask := "" for taskID, dist := range distances { if dist > maxDist { maxDist = dist endTask = taskID } } // Reconstruct path path := make([]string, 0) current := endTask for current != "" { path = append([]string{current}, path...) current = parent[current] } return path }