Skip to main content

ralph/queue/graph/
build.rs

1//! Graph construction from queue files.
2//!
3//! Responsibilities:
4//! - Build a `DependencyGraph` from the active queue and optional done queue.
5//! - Normalize task IDs for graph keys using `trim()`.
6//! - Populate forward and reverse relationship edges.
7//!
8//! Not handled here:
9//! - Validating DAG-ness / cycle detection (algorithms can detect; queue validation is elsewhere).
10//! - Algorithms (see `algorithms`) and traversal helpers (see `traversal`).
11//!
12//! Invariants/assumptions:
13//! - Task IDs are unique across active+done (enforced elsewhere).
14//! - Relationship IDs may contain whitespace and are normalized via `trim()`.
15
16use crate::contracts::QueueFile;
17use std::collections::HashMap;
18
19use super::types::{DependencyGraph, TaskNode};
20
21pub fn build_graph(active: &QueueFile, done: Option<&QueueFile>) -> DependencyGraph {
22    let mut nodes: HashMap<String, TaskNode> = HashMap::new();
23
24    let mut dependents_map: HashMap<String, Vec<String>> = HashMap::new();
25    let mut blocked_by_map: HashMap<String, Vec<String>> = HashMap::new();
26    let mut related_by_map: HashMap<String, Vec<String>> = HashMap::new();
27    let mut duplicated_by_map: HashMap<String, Vec<String>> = HashMap::new();
28
29    let all_tasks = active
30        .tasks
31        .iter()
32        .chain(done.iter().flat_map(|d| d.tasks.iter()));
33
34    for task in all_tasks {
35        let task_id = task.id.trim().to_string();
36
37        for dep_id in &task.depends_on {
38            let dep_id = dep_id.trim().to_string();
39            if dep_id.is_empty() {
40                continue;
41            }
42            dependents_map
43                .entry(dep_id)
44                .or_default()
45                .push(task_id.clone());
46        }
47
48        for blocked_id in &task.blocks {
49            let blocked_id = blocked_id.trim().to_string();
50            if blocked_id.is_empty() {
51                continue;
52            }
53            blocked_by_map
54                .entry(blocked_id)
55                .or_default()
56                .push(task_id.clone());
57        }
58
59        for related_id in &task.relates_to {
60            let related_id = related_id.trim().to_string();
61            if related_id.is_empty() {
62                continue;
63            }
64            related_by_map
65                .entry(related_id)
66                .or_default()
67                .push(task_id.clone());
68        }
69
70        if let Some(duplicates_id) = &task.duplicates {
71            let duplicates_id = duplicates_id.trim().to_string();
72            if duplicates_id.is_empty() {
73                continue;
74            }
75            duplicated_by_map
76                .entry(duplicates_id)
77                .or_default()
78                .push(task_id.clone());
79        }
80    }
81
82    let all_tasks = active
83        .tasks
84        .iter()
85        .chain(done.iter().flat_map(|d| d.tasks.iter()));
86
87    for task in all_tasks {
88        let task_id = task.id.trim().to_string();
89
90        let dependencies: Vec<String> = task
91            .depends_on
92            .iter()
93            .map(|s| s.trim().to_string())
94            .filter(|s| !s.is_empty())
95            .collect();
96
97        let blocks: Vec<String> = task
98            .blocks
99            .iter()
100            .map(|s| s.trim().to_string())
101            .filter(|s| !s.is_empty())
102            .collect();
103
104        let relates_to: Vec<String> = task
105            .relates_to
106            .iter()
107            .map(|s| s.trim().to_string())
108            .filter(|s| !s.is_empty())
109            .collect();
110
111        let duplicates = task
112            .duplicates
113            .as_ref()
114            .map(|s| s.trim().to_string())
115            .filter(|s| !s.is_empty());
116
117        nodes.insert(
118            task_id.clone(),
119            TaskNode {
120                task: task.clone(),
121                dependencies,
122                dependents: dependents_map.get(&task_id).cloned().unwrap_or_default(),
123                blocks,
124                blocked_by: blocked_by_map.get(&task_id).cloned().unwrap_or_default(),
125                relates_to,
126                related_by: related_by_map.get(&task_id).cloned().unwrap_or_default(),
127                duplicates,
128                duplicated_by: duplicated_by_map.get(&task_id).cloned().unwrap_or_default(),
129            },
130        );
131    }
132
133    let roots: Vec<String> = nodes
134        .iter()
135        .filter(|(_, n)| n.dependents.is_empty())
136        .map(|(id, _)| id.clone())
137        .collect();
138
139    let leaves: Vec<String> = nodes
140        .iter()
141        .filter(|(_, n)| n.dependencies.is_empty())
142        .map(|(id, _)| id.clone())
143        .collect();
144
145    DependencyGraph::from_parts(nodes, roots, leaves)
146}