Skip to main content

ralph/queue/graph/
algorithms.rs

1//! Graph algorithms over `DependencyGraph`.
2//!
3//! Responsibilities:
4//! - Topological sort (Kahn's algorithm) with cycle detection.
5//! - Critical path discovery (longest dependency chain) for DAGs.
6//! - Queries for runnable and blocked tasks based on dependency completion.
7//!
8//! Not handled here:
9//! - Graph construction (see `build`).
10//! - Traversal utilities intended for UI exploration (see `traversal`).
11//!
12//! Invariants/assumptions:
13//! - Normal operation is DAG; cycle detection is still supported for robustness.
14
15use super::types::{CriticalPathResult, DependencyGraph};
16use crate::contracts::TaskStatus;
17use anyhow::{Result, bail};
18use std::collections::{HashMap, HashSet, VecDeque};
19
20pub fn topological_sort(graph: &DependencyGraph) -> Result<Vec<String>> {
21    let mut in_degree: HashMap<String, usize> = HashMap::new();
22    let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
23
24    for task_id in graph.task_ids() {
25        in_degree.entry(task_id.clone()).or_insert(0);
26    }
27
28    for task_id in graph.task_ids() {
29        let Some(node) = graph.get(task_id) else {
30            continue;
31        };
32        for dep_id in &node.dependencies {
33            adjacency
34                .entry(dep_id.clone())
35                .or_default()
36                .push(task_id.clone());
37            *in_degree.entry(task_id.clone()).or_insert(0) += 1;
38        }
39    }
40
41    let mut queue: VecDeque<String> = in_degree
42        .iter()
43        .filter(|&(_, &deg)| deg == 0)
44        .map(|(id, _)| id.clone())
45        .collect();
46
47    let mut result = Vec::new();
48
49    while let Some(current) = queue.pop_front() {
50        result.push(current.clone());
51
52        if let Some(neighbors) = adjacency.get(&current) {
53            for neighbor in neighbors {
54                if let Some(deg) = in_degree.get_mut(neighbor) {
55                    *deg = deg.saturating_sub(1);
56                    if *deg == 0 {
57                        queue.push_back(neighbor.clone());
58                    }
59                }
60            }
61        }
62    }
63
64    if result.len() != graph.len() {
65        bail!("Cycle detected in dependency graph");
66    }
67
68    Ok(result)
69}
70
71fn longest_path_from(
72    task_id: &str,
73    graph: &DependencyGraph,
74    memo: &mut HashMap<String, Vec<String>>,
75    visited: &mut HashSet<String>,
76) -> Vec<String> {
77    if let Some(path) = memo.get(task_id) {
78        return path.clone();
79    }
80
81    if visited.contains(task_id) {
82        return vec![task_id.to_string()];
83    }
84    visited.insert(task_id.to_string());
85
86    let mut longest = vec![task_id.to_string()];
87
88    if let Some(node) = graph.get(task_id) {
89        for dep_id in &node.dependencies {
90            let dep_path = longest_path_from(dep_id, graph, memo, visited);
91            if dep_path.len() + 1 > longest.len() {
92                longest = vec![task_id.to_string()];
93                longest.extend(dep_path);
94            }
95        }
96    }
97
98    visited.remove(task_id);
99    memo.insert(task_id.to_string(), longest.clone());
100    longest
101}
102
103pub fn find_critical_paths(graph: &DependencyGraph) -> Vec<CriticalPathResult> {
104    if graph.is_empty() {
105        return Vec::new();
106    }
107
108    let mut memo: HashMap<String, Vec<String>> = HashMap::new();
109    let mut all_paths: Vec<CriticalPathResult> = Vec::new();
110    let mut max_length = 0;
111
112    for root_id in graph.roots() {
113        let mut visited = HashSet::new();
114        let path = longest_path_from(root_id, graph, &mut memo, &mut visited);
115
116        if path.len() > max_length {
117            max_length = path.len();
118            all_paths.clear();
119        }
120
121        if path.len() == max_length && max_length > 0 {
122            let is_blocked = path.iter().any(|id| !graph.is_task_completed(id));
123            all_paths.push(CriticalPathResult {
124                path,
125                length: max_length,
126                is_blocked,
127            });
128        }
129    }
130
131    if all_paths.is_empty() && !graph.is_empty() {
132        for task_id in graph.task_ids() {
133            let mut visited = HashSet::new();
134            let path = longest_path_from(task_id, graph, &mut memo, &mut visited);
135
136            if path.len() > max_length {
137                max_length = path.len();
138                all_paths.clear();
139            }
140
141            if path.len() == max_length {
142                let is_blocked = path.iter().any(|id| !graph.is_task_completed(id));
143                all_paths.push(CriticalPathResult {
144                    path,
145                    length: max_length,
146                    is_blocked,
147                });
148            }
149        }
150    }
151
152    all_paths
153}
154
155pub fn find_critical_path_from(
156    graph: &DependencyGraph,
157    start_task_id: &str,
158) -> Option<CriticalPathResult> {
159    if !graph.contains(start_task_id) {
160        return None;
161    }
162
163    let mut memo: HashMap<String, Vec<String>> = HashMap::new();
164    let mut visited = HashSet::new();
165
166    let path = longest_path_from(start_task_id, graph, &mut memo, &mut visited);
167    let is_blocked = path.iter().any(|id| !graph.is_task_completed(id));
168
169    Some(CriticalPathResult {
170        length: path.len(),
171        path,
172        is_blocked,
173    })
174}
175
176pub fn get_runnable_tasks(graph: &DependencyGraph) -> Vec<String> {
177    graph
178        .values()
179        .filter(|n| {
180            n.task.status == TaskStatus::Todo
181                && n.dependencies
182                    .iter()
183                    .all(|dep_id| graph.is_task_completed(dep_id))
184        })
185        .map(|n| n.task.id.clone())
186        .collect()
187}
188
189pub fn get_blocked_tasks(graph: &DependencyGraph) -> Vec<String> {
190    graph
191        .values()
192        .filter(|n| {
193            matches!(n.task.status, TaskStatus::Todo | TaskStatus::Doing)
194                && n.dependencies
195                    .iter()
196                    .any(|dep_id| !graph.is_task_completed(dep_id))
197        })
198        .map(|n| n.task.id.clone())
199        .collect()
200}