Skip to main content

ralph/queue/graph/
traversal.rs

1//! Chain traversal and relationship queries for `DependencyGraph`.
2//!
3//! Responsibilities:
4//! - Provide transitive "chain" traversal helpers over relationship edges.
5//! - Provide bounded variants for UI rendering (limit + truncated flag).
6//! - Provide immediate relationship accessors.
7//!
8//! Not handled here:
9//! - Graph construction (see `build`).
10//! - Topological sort / critical path algorithms (see `algorithms`).
11//!
12//! Invariants/assumptions:
13//! - Traversals are best-effort and tolerate missing task IDs by returning partial results.
14//! - Chains may include duplicates if the graph contains converging paths (preserves prior behavior).
15
16use super::types::{BoundedChainResult, DependencyGraph, TaskNode};
17use std::collections::HashSet;
18
19fn collect_chain_unbounded(
20    graph: &DependencyGraph,
21    start: &str,
22    mut neighbors: impl FnMut(&TaskNode) -> Vec<String>,
23) -> Vec<String> {
24    let mut chain = Vec::new();
25    let mut visited = HashSet::new();
26    let mut stack = vec![start.to_string()];
27
28    while let Some(current_id) = stack.pop() {
29        if visited.contains(&current_id) {
30            continue;
31        }
32        visited.insert(current_id.clone());
33
34        if let Some(node) = graph.get(&current_id) {
35            for next_id in neighbors(node) {
36                if !visited.contains(&next_id) {
37                    chain.push(next_id.clone());
38                    stack.push(next_id);
39                }
40            }
41        }
42    }
43
44    chain
45}
46
47fn collect_chain_bounded(
48    graph: &DependencyGraph,
49    start: &str,
50    limit: usize,
51    mut neighbors: impl FnMut(&TaskNode) -> Vec<String>,
52) -> BoundedChainResult {
53    if limit == 0 {
54        let mut visited = HashSet::new();
55        let mut stack = vec![start.to_string()];
56
57        while let Some(current_id) = stack.pop() {
58            if visited.contains(&current_id) {
59                continue;
60            }
61            visited.insert(current_id.clone());
62
63            if let Some(node) = graph.get(&current_id) {
64                for next_id in neighbors(node) {
65                    if !visited.contains(&next_id) {
66                        return BoundedChainResult {
67                            task_ids: Vec::new(),
68                            truncated: true,
69                        };
70                    }
71                }
72            }
73        }
74
75        return BoundedChainResult {
76            task_ids: Vec::new(),
77            truncated: false,
78        };
79    }
80
81    let mut result = BoundedChainResult {
82        task_ids: Vec::new(),
83        truncated: false,
84    };
85
86    let mut visited = HashSet::new();
87    let mut stack = vec![start.to_string()];
88
89    while let Some(current_id) = stack.pop() {
90        if visited.contains(&current_id) {
91            continue;
92        }
93        visited.insert(current_id.clone());
94
95        if let Some(node) = graph.get(&current_id) {
96            for next_id in neighbors(node) {
97                if !visited.contains(&next_id) {
98                    if result.task_ids.len() < limit {
99                        result.task_ids.push(next_id.clone());
100                    } else {
101                        result.truncated = true;
102                    }
103                    stack.push(next_id);
104                }
105            }
106        }
107    }
108
109    result
110}
111
112impl DependencyGraph {
113    pub fn get_blocking_chain(&self, task_id: &str) -> Vec<String> {
114        collect_chain_unbounded(self, task_id, |n| n.dependencies.clone())
115    }
116
117    pub fn get_blocked_chain(&self, task_id: &str) -> Vec<String> {
118        collect_chain_unbounded(self, task_id, |n| n.dependents.clone())
119    }
120
121    pub fn get_blocking_chain_bounded(&self, task_id: &str, limit: usize) -> BoundedChainResult {
122        collect_chain_bounded(self, task_id, limit, |n| n.dependencies.clone())
123    }
124
125    pub fn get_blocked_chain_bounded(&self, task_id: &str, limit: usize) -> BoundedChainResult {
126        collect_chain_bounded(self, task_id, limit, |n| n.dependents.clone())
127    }
128
129    pub fn get_blocks_chain(&self, task_id: &str) -> Vec<String> {
130        collect_chain_unbounded(self, task_id, |n| n.blocks.clone())
131    }
132
133    pub fn get_blocked_by_chain(&self, task_id: &str) -> Vec<String> {
134        collect_chain_unbounded(self, task_id, |n| n.blocked_by.clone())
135    }
136
137    pub fn get_related_chain(&self, task_id: &str) -> Vec<String> {
138        collect_chain_unbounded(self, task_id, |n| {
139            let mut out = Vec::with_capacity(n.relates_to.len() + n.related_by.len());
140            out.extend(n.relates_to.iter().cloned());
141            out.extend(n.related_by.iter().cloned());
142            out
143        })
144    }
145
146    pub fn get_duplicate_chain(&self, task_id: &str) -> Vec<String> {
147        let mut chain = Vec::new();
148        let mut visited = HashSet::new();
149        let mut current = task_id.to_string();
150
151        loop {
152            if visited.contains(&current) {
153                break;
154            }
155            visited.insert(current.clone());
156
157            if let Some(node) = self.get(&current)
158                && let Some(duplicates) = &node.duplicates
159                && !visited.contains(duplicates)
160            {
161                chain.push(duplicates.clone());
162                current = duplicates.clone();
163                continue;
164            }
165            break;
166        }
167
168        if let Some(node) = self.get(task_id) {
169            for dupe_id in &node.duplicated_by {
170                if !visited.contains(dupe_id) {
171                    chain.push(dupe_id.clone());
172                }
173            }
174        }
175
176        chain
177    }
178
179    pub fn get_immediate_dependencies(&self, task_id: &str) -> Vec<String> {
180        self.get(task_id)
181            .map(|n| n.dependencies.clone())
182            .unwrap_or_default()
183    }
184
185    pub fn get_immediate_dependents(&self, task_id: &str) -> Vec<String> {
186        self.get(task_id)
187            .map(|n| n.dependents.clone())
188            .unwrap_or_default()
189    }
190
191    pub fn get_immediate_blocks(&self, task_id: &str) -> Vec<String> {
192        self.get(task_id)
193            .map(|n| n.blocks.clone())
194            .unwrap_or_default()
195    }
196
197    pub fn get_immediate_blocked_by(&self, task_id: &str) -> Vec<String> {
198        self.get(task_id)
199            .map(|n| n.blocked_by.clone())
200            .unwrap_or_default()
201    }
202
203    pub fn get_immediate_relates_to(&self, task_id: &str) -> Vec<String> {
204        self.get(task_id)
205            .map(|n| n.relates_to.clone())
206            .unwrap_or_default()
207    }
208
209    pub fn get_immediate_duplicated_by(&self, task_id: &str) -> Vec<String> {
210        self.get(task_id)
211            .map(|n| n.duplicated_by.clone())
212            .unwrap_or_default()
213    }
214}