1use std::collections::{HashMap, HashSet, VecDeque};
7
8use chrono::{DateTime, Utc};
9use pe_core::PeError;
10use serde::{Deserialize, Serialize};
11
12use crate::task::{Task, TaskId, TaskStatus};
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
25pub struct TaskDependency {
26 pub task_id: TaskId,
28 pub depends_on_id: TaskId,
30 pub dependency_type: DependencyType,
32 pub created_at: DateTime<Utc>,
34}
35
36impl TaskDependency {
37 #[must_use]
39 pub fn new(
40 task_id: impl Into<TaskId>,
41 depends_on_id: impl Into<TaskId>,
42 dependency_type: DependencyType,
43 ) -> Self {
44 Self {
45 task_id: task_id.into(),
46 depends_on_id: depends_on_id.into(),
47 dependency_type,
48 created_at: Utc::now(),
49 }
50 }
51}
52
53#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[non_exhaustive]
56pub enum DependencyType {
57 Blocks,
59 Requires,
61 Related,
63}
64
65#[must_use]
78pub fn would_create_cycle(
79 task_id: &str,
80 depends_on_id: &str,
81 existing_deps: &[TaskDependency],
82) -> bool {
83 if task_id == depends_on_id {
84 return true; }
86
87 let mut depends_on: HashMap<&str, Vec<&str>> = HashMap::new();
91 for dep in existing_deps {
92 depends_on
93 .entry(dep.task_id.as_str())
94 .or_default()
95 .push(dep.depends_on_id.as_str());
96 }
97
98 let mut visited = HashSet::new();
102 let mut queue = VecDeque::new();
103 queue.push_back(depends_on_id);
104 visited.insert(depends_on_id);
105
106 while let Some(current) = queue.pop_front() {
107 if let Some(parents) = depends_on.get(current) {
108 for &parent in parents {
109 if parent == task_id {
110 return true; }
112 if visited.insert(parent) {
113 queue.push_back(parent);
114 }
115 }
116 }
117 }
118
119 false
120}
121
122pub(crate) fn validate_parent_link(candidate: &Task, tasks: &[Task]) -> Result<(), PeError> {
127 let Some(parent_id) = candidate.parent_task_id.as_deref() else {
128 return Ok(());
129 };
130
131 if parent_id == candidate.id {
132 return Err(PeError::InvalidUpdate {
133 details: format!("Task {} cannot be its own parent", candidate.id),
134 });
135 }
136
137 active_task(parent_id, tasks).ok_or_else(|| PeError::NodeNotFound {
138 node: parent_id.to_string(),
139 })?;
140
141 let mut seen = HashSet::new();
142 let mut current_id = Some(parent_id);
143 while let Some(task_id) = current_id {
144 if task_id == candidate.id {
145 return Err(PeError::InvalidUpdate {
146 details: format!(
147 "Parent link {} -> {} would create a task hierarchy cycle",
148 candidate.id, parent_id
149 ),
150 });
151 }
152 if !seen.insert(task_id.to_string()) {
153 return Err(PeError::InvalidUpdate {
154 details: format!(
155 "Parent chain for task {} already contains a cycle",
156 candidate.id
157 ),
158 });
159 }
160 current_id = active_task(task_id, tasks).and_then(|task| task.parent_task_id.as_deref());
161 }
162
163 Ok(())
164}
165
166pub(crate) fn validate_dependency_endpoints(
167 dep: &TaskDependency,
168 tasks: &[Task],
169) -> Result<(), PeError> {
170 active_task(&dep.task_id, tasks).ok_or_else(|| PeError::NodeNotFound {
171 node: dep.task_id.clone(),
172 })?;
173 active_task(&dep.depends_on_id, tasks).ok_or_else(|| PeError::NodeNotFound {
174 node: dep.depends_on_id.clone(),
175 })?;
176 Ok(())
177}
178
179pub(crate) fn validate_dependency_unique(
180 dep: &TaskDependency,
181 existing_deps: &[TaskDependency],
182) -> Result<(), PeError> {
183 if existing_deps.iter().any(|existing| {
184 existing.task_id == dep.task_id && existing.depends_on_id == dep.depends_on_id
185 }) {
186 return Err(PeError::InvalidUpdate {
187 details: format!(
188 "Dependency {} -> {} already exists",
189 dep.task_id, dep.depends_on_id
190 ),
191 });
192 }
193 Ok(())
194}
195
196fn active_task<'a>(task_id: &str, tasks: &'a [Task]) -> Option<&'a Task> {
197 tasks
198 .iter()
199 .find(|task| task.id == task_id && task.deleted_at.is_none())
200}
201
202#[must_use]
203pub fn find_ready_tasks(
204 task_ids: &[TaskId],
205 deps: &[TaskDependency],
206 task_statuses: &HashMap<TaskId, TaskStatus>,
207) -> Vec<TaskId> {
208 task_ids
209 .iter()
210 .filter(|tid| {
211 let blockers: Vec<&TaskDependency> = deps
212 .iter()
213 .filter(|d| d.task_id == **tid && d.dependency_type == DependencyType::Blocks)
214 .collect();
215
216 blockers.iter().all(|b| {
217 task_statuses
218 .get(&b.depends_on_id)
219 .is_some_and(|s| *s == TaskStatus::Completed)
220 })
221 })
222 .cloned()
223 .collect()
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn test_no_cycle_simple() {
232 let deps = vec![TaskDependency::new("B", "A", DependencyType::Blocks)];
233 assert!(!would_create_cycle("C", "B", &deps));
235 }
236
237 #[test]
238 fn test_self_loop_detected() {
239 assert!(would_create_cycle("A", "A", &[]));
240 }
241
242 #[test]
243 fn test_direct_cycle_detected() {
244 let deps = vec![TaskDependency::new("B", "A", DependencyType::Blocks)];
245 assert!(would_create_cycle("A", "B", &deps));
247 }
248
249 #[test]
250 fn test_transitive_cycle_detected() {
251 let deps = vec![
252 TaskDependency::new("B", "A", DependencyType::Blocks),
253 TaskDependency::new("C", "B", DependencyType::Blocks),
254 ];
255 assert!(would_create_cycle("A", "C", &deps));
257 }
258
259 #[test]
260 fn test_no_cycle_parallel() {
261 let deps = vec![
262 TaskDependency::new("C", "A", DependencyType::Blocks),
263 TaskDependency::new("C", "B", DependencyType::Blocks),
264 ];
265 assert!(!would_create_cycle("B", "A", &deps));
267 }
268
269 #[test]
270 fn test_ready_tasks() {
271 let deps = vec![
272 TaskDependency::new("B", "A", DependencyType::Blocks),
273 TaskDependency::new("C", "B", DependencyType::Blocks),
274 ];
275 let mut statuses = HashMap::new();
276 statuses.insert("A".into(), TaskStatus::Completed);
277 statuses.insert("B".into(), TaskStatus::Pending);
278 statuses.insert("C".into(), TaskStatus::Pending);
279
280 let ready = find_ready_tasks(&["A".into(), "B".into(), "C".into()], &deps, &statuses);
281 assert!(ready.contains(&"A".to_string()));
283 assert!(ready.contains(&"B".to_string()));
284 assert!(!ready.contains(&"C".to_string()));
285 }
286
287 #[test]
288 fn test_related_deps_dont_block() {
289 let deps = vec![TaskDependency::new("B", "A", DependencyType::Related)];
290 let mut statuses = HashMap::new();
291 statuses.insert("A".into(), TaskStatus::Pending); statuses.insert("B".into(), TaskStatus::Pending);
293
294 let ready = find_ready_tasks(&["B".into()], &deps, &statuses);
295 assert!(ready.contains(&"B".to_string()));
297 }
298}