ralph/queue/graph/
algorithms.rs1use 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 == 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(¤t) {
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}