1use crate::spec::Spec;
4use anyhow::{anyhow, Result};
5use std::collections::{HashMap, HashSet, VecDeque};
6
7pub fn detect_cycles(specs: &[Spec]) -> Vec<Vec<String>> {
20 let mut cycles = Vec::new();
21 let mut visited = HashSet::new();
22 let mut rec_stack = HashSet::new();
23 let mut path = Vec::new();
24
25 let adj_map = build_adjacency_map(specs);
27
28 for spec in specs {
29 if !visited.contains(&spec.id) {
30 if let Some(cycle) =
31 detect_cycle_dfs(&spec.id, &adj_map, &mut visited, &mut rec_stack, &mut path)
32 {
33 cycles.push(cycle);
34 }
35 }
36 }
37
38 cycles
39}
40
41fn detect_cycle_dfs(
43 node: &str,
44 adj_map: &HashMap<String, Vec<String>>,
45 visited: &mut HashSet<String>,
46 rec_stack: &mut HashSet<String>,
47 path: &mut Vec<String>,
48) -> Option<Vec<String>> {
49 visited.insert(node.to_string());
50 rec_stack.insert(node.to_string());
51 path.push(node.to_string());
52
53 if let Some(deps) = adj_map.get(node) {
54 for dep in deps {
55 if !visited.contains(dep) {
56 if let Some(cycle) = detect_cycle_dfs(dep, adj_map, visited, rec_stack, path) {
57 return Some(cycle);
58 }
59 } else if rec_stack.contains(dep) {
60 let cycle_start = path.iter().position(|id| id == dep).unwrap();
62 let cycle = path[cycle_start..].to_vec();
63 return Some(cycle);
64 }
65 }
66 }
67
68 rec_stack.remove(node);
69 path.pop();
70 None
71}
72
73fn build_adjacency_map(specs: &[Spec]) -> HashMap<String, Vec<String>> {
75 let mut adj_map = HashMap::new();
76
77 for spec in specs {
78 let deps = spec.frontmatter.depends_on.clone().unwrap_or_default();
79 adj_map.insert(spec.id.clone(), deps);
80 }
81
82 adj_map
83}
84
85pub fn topological_sort(specs: &[Spec]) -> Result<Vec<String>> {
100 let cycles = detect_cycles(specs);
102 if !cycles.is_empty() {
103 let cycle_str = cycles[0].join(" -> ");
104 return Err(anyhow!("Circular dependency detected: {}", cycle_str));
105 }
106
107 let spec_ids: HashSet<String> = specs.iter().map(|s| s.id.clone()).collect();
108
109 let mut dependents_of: HashMap<String, Vec<String>> = HashMap::new();
112 let mut in_degree: HashMap<String, usize> = HashMap::new();
113
114 for spec in specs {
116 dependents_of.entry(spec.id.clone()).or_default();
117 in_degree.entry(spec.id.clone()).or_insert(0);
118 }
119
120 for spec in specs {
122 if let Some(deps) = &spec.frontmatter.depends_on {
123 for dep in deps {
124 if spec_ids.contains(dep) {
126 dependents_of
128 .entry(dep.clone())
129 .or_default()
130 .push(spec.id.clone());
131 *in_degree.entry(spec.id.clone()).or_insert(0) += 1;
133 }
134 }
135 }
136 }
137
138 let mut queue = VecDeque::new();
140 let mut result = Vec::new();
141
142 for (id, °ree) in &in_degree {
144 if degree == 0 {
145 queue.push_back(id.clone());
146 }
147 }
148
149 while let Some(node) = queue.pop_front() {
150 result.push(node.clone());
151
152 if let Some(dependents) = dependents_of.get(&node) {
154 for dependent in dependents {
155 if let Some(degree) = in_degree.get_mut(dependent) {
156 *degree -= 1;
157 if *degree == 0 {
158 queue.push_back(dependent.clone());
159 }
160 }
161 }
162 }
163 }
164
165 if result.len() == specs.len() {
167 Ok(result)
168 } else {
169 Err(anyhow!(
170 "Failed to produce topological sort (this shouldn't happen after cycle check)"
171 ))
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::spec::{SpecFrontmatter, SpecStatus};
179
180 fn make_spec(id: &str, depends_on: Option<Vec<String>>) -> Spec {
181 Spec {
182 id: id.to_string(),
183 frontmatter: SpecFrontmatter {
184 status: SpecStatus::Pending,
185 depends_on,
186 ..Default::default()
187 },
188 title: Some(format!("Test {}", id)),
189 body: format!("# Test {}\n\nBody.", id),
190 }
191 }
192
193 #[test]
194 fn test_detect_cycles_no_cycles() {
195 let specs = vec![
196 make_spec("001", None),
197 make_spec("002", Some(vec!["001".to_string()])),
198 make_spec("003", Some(vec!["002".to_string()])),
199 ];
200
201 let cycles = detect_cycles(&specs);
202 assert!(cycles.is_empty());
203 }
204
205 #[test]
206 fn test_detect_cycles_simple_cycle() {
207 let specs = vec![
208 make_spec("001", Some(vec!["002".to_string()])),
209 make_spec("002", Some(vec!["001".to_string()])),
210 ];
211
212 let cycles = detect_cycles(&specs);
213 assert_eq!(cycles.len(), 1);
214 assert!(cycles[0].contains(&"001".to_string()));
215 assert!(cycles[0].contains(&"002".to_string()));
216 }
217
218 #[test]
219 fn test_detect_cycles_self_cycle() {
220 let specs = vec![make_spec("001", Some(vec!["001".to_string()]))];
221
222 let cycles = detect_cycles(&specs);
223 assert_eq!(cycles.len(), 1);
224 assert_eq!(cycles[0], vec!["001"]);
225 }
226
227 #[test]
228 fn test_detect_cycles_linear_chain() {
229 let specs = vec![
230 make_spec("A", Some(vec!["B".to_string()])),
231 make_spec("B", Some(vec!["C".to_string()])),
232 make_spec("C", None),
233 ];
234
235 let cycles = detect_cycles(&specs);
236 assert!(
237 cycles.is_empty(),
238 "Linear chain A->B->C should have no cycles"
239 );
240 }
241
242 #[test]
243 fn test_detect_cycles_simple_cycle_abc() {
244 let specs = vec![
245 make_spec("A", Some(vec!["B".to_string()])),
246 make_spec("B", Some(vec!["A".to_string()])),
247 ];
248
249 let cycles = detect_cycles(&specs);
250 assert_eq!(cycles.len(), 1, "Should detect one cycle");
251 assert!(
252 cycles[0].contains(&"A".to_string()),
253 "Cycle should contain A"
254 );
255 assert!(
256 cycles[0].contains(&"B".to_string()),
257 "Cycle should contain B"
258 );
259 }
260
261 #[test]
262 fn test_detect_cycles_three_node() {
263 let specs = vec![
264 make_spec("A", Some(vec!["B".to_string()])),
265 make_spec("B", Some(vec!["C".to_string()])),
266 make_spec("C", Some(vec!["A".to_string()])),
267 ];
268
269 let cycles = detect_cycles(&specs);
270 assert_eq!(cycles.len(), 1, "Should detect one cycle");
271 assert!(
272 cycles[0].contains(&"A".to_string()),
273 "Cycle should contain A"
274 );
275 assert!(
276 cycles[0].contains(&"B".to_string()),
277 "Cycle should contain B"
278 );
279 assert!(
280 cycles[0].contains(&"C".to_string()),
281 "Cycle should contain C"
282 );
283 }
284
285 #[test]
286 fn test_detect_cycles_self_reference() {
287 let specs = vec![make_spec("A", Some(vec!["A".to_string()]))];
288
289 let cycles = detect_cycles(&specs);
290 assert_eq!(cycles.len(), 1, "Should detect one cycle");
291 assert_eq!(
292 cycles[0],
293 vec!["A"],
294 "Cycle should be just A referencing itself"
295 );
296 }
297
298 #[test]
299 fn test_topological_sort_linear() {
300 let specs = vec![
301 make_spec("001", None),
302 make_spec("002", Some(vec!["001".to_string()])),
303 make_spec("003", Some(vec!["002".to_string()])),
304 ];
305
306 let result = topological_sort(&specs).unwrap();
307 assert_eq!(result.len(), 3);
308
309 let pos_001 = result.iter().position(|id| id == "001").unwrap();
310 let pos_002 = result.iter().position(|id| id == "002").unwrap();
311 let pos_003 = result.iter().position(|id| id == "003").unwrap();
312
313 assert!(pos_001 < pos_002);
314 assert!(pos_002 < pos_003);
315 }
316
317 #[test]
318 fn test_topological_sort_diamond_numeric() {
319 let specs = vec![
320 make_spec("001", None),
321 make_spec("002", Some(vec!["001".to_string()])),
322 make_spec("003", Some(vec!["001".to_string()])),
323 make_spec("004", Some(vec!["002".to_string(), "003".to_string()])),
324 ];
325
326 let result = topological_sort(&specs).unwrap();
327 assert_eq!(result.len(), 4);
328
329 let pos_001 = result.iter().position(|id| id == "001").unwrap();
331 let pos_002 = result.iter().position(|id| id == "002").unwrap();
332 let pos_003 = result.iter().position(|id| id == "003").unwrap();
333 let pos_004 = result.iter().position(|id| id == "004").unwrap();
334
335 assert!(pos_001 < pos_002);
336 assert!(pos_001 < pos_003);
337 assert!(pos_002 < pos_004);
338 assert!(pos_003 < pos_004);
339 }
340
341 #[test]
342 fn test_topological_sort_with_cycle() {
343 let specs = vec![
344 make_spec("001", Some(vec!["002".to_string()])),
345 make_spec("002", Some(vec!["001".to_string()])),
346 ];
347
348 let result = topological_sort(&specs);
349 assert!(result.is_err());
350 assert!(result
351 .unwrap_err()
352 .to_string()
353 .contains("Circular dependency"));
354 }
355
356 #[test]
357 fn test_topological_sort_no_dependencies() {
358 let specs = vec![
359 make_spec("001", None),
360 make_spec("002", None),
361 make_spec("003", None),
362 ];
363
364 let result = topological_sort(&specs).unwrap();
365 assert_eq!(result.len(), 3);
366 }
368
369 #[test]
370 fn test_topological_sort_empty() {
371 let specs: Vec<Spec> = vec![];
372 let result = topological_sort(&specs).unwrap();
373 assert!(result.is_empty());
374 }
375
376 #[test]
377 fn test_topological_sort_single() {
378 let specs = vec![make_spec("A", None)];
379 let result = topological_sort(&specs).unwrap();
380 assert_eq!(result, vec!["A"]);
381 }
382
383 #[test]
384 fn test_topological_sort_linear_chain() {
385 let specs = vec![
386 make_spec("A", Some(vec!["B".to_string()])),
387 make_spec("B", Some(vec!["C".to_string()])),
388 make_spec("C", None),
389 ];
390
391 let result = topological_sort(&specs).unwrap();
392 assert_eq!(result.len(), 3);
393
394 let pos_a = result.iter().position(|id| id == "A").unwrap();
395 let pos_b = result.iter().position(|id| id == "B").unwrap();
396 let pos_c = result.iter().position(|id| id == "C").unwrap();
397
398 assert!(pos_c < pos_b, "C should come before B (dependencies first)");
399 assert!(pos_b < pos_a, "B should come before A (dependencies first)");
400 }
401
402 #[test]
403 fn test_topological_sort_diamond() {
404 let specs = vec![
405 make_spec("A", Some(vec!["B".to_string(), "C".to_string()])),
406 make_spec("B", Some(vec!["D".to_string()])),
407 make_spec("C", Some(vec!["D".to_string()])),
408 make_spec("D", None),
409 ];
410
411 let result = topological_sort(&specs).unwrap();
412 assert_eq!(result.len(), 4);
413
414 let pos_a = result.iter().position(|id| id == "A").unwrap();
415 let pos_b = result.iter().position(|id| id == "B").unwrap();
416 let pos_c = result.iter().position(|id| id == "C").unwrap();
417 let pos_d = result.iter().position(|id| id == "D").unwrap();
418
419 assert!(pos_d < pos_b, "D should come before B");
420 assert!(pos_d < pos_c, "D should come before C");
421 assert!(pos_b < pos_a, "B should come before A");
422 assert!(pos_c < pos_a, "C should come before A");
423 }
424
425 #[test]
426 fn test_topological_sort_cycle_error() {
427 let specs = vec![
428 make_spec("A", Some(vec!["B".to_string()])),
429 make_spec("B", Some(vec!["A".to_string()])),
430 ];
431
432 let result = topological_sort(&specs);
433 assert!(result.is_err());
434 let err = result.unwrap_err();
435 assert!(
436 err.to_string().contains("Circular dependency"),
437 "Error should mention circular dependency"
438 );
439 }
440}