1use anyhow::Result;
2use colored::Colorize;
3use dialoguer::Select;
4use std::collections::HashMap;
5
6use crate::models::phase::Phase;
7use crate::models::task::{Task, TaskStatus};
8use crate::storage::Storage;
9
10pub fn flatten_all_tasks(all_phases: &HashMap<String, Phase>) -> Vec<&Task> {
12 all_phases
13 .values()
14 .flat_map(|phase| phase.tasks.iter())
15 .collect()
16}
17
18pub fn is_interactive() -> bool {
20 atty::is(atty::Stream::Stdin) && atty::is(atty::Stream::Stdout)
21}
22
23pub fn find_next_task(
27 storage: &Storage,
28 tag: Option<&str>,
29 all_tags: bool,
30) -> Option<(Task, String)> {
31 let tasks = storage.load_tasks().ok()?;
32 let all_tasks_flat = flatten_all_tasks(&tasks);
33
34 if all_tags {
35 for (phase_tag, phase) in &tasks {
36 for task in &phase.tasks {
37 if is_task_ready(task, phase, &all_tasks_flat) {
38 return Some((task.clone(), phase_tag.clone()));
39 }
40 }
41 }
42 None
43 } else {
44 let phase_tag = tag
45 .map(String::from)
46 .or_else(|| storage.get_active_group().ok().flatten())?;
47 let phase = tasks.get(&phase_tag)?;
48
49 for task in &phase.tasks {
50 if is_task_ready(task, phase, &all_tasks_flat) {
51 return Some((task.clone(), phase_tag.clone()));
52 }
53 }
54 None
55 }
56}
57
58pub fn is_task_spawnable(task: &Task, phase: &Phase) -> bool {
64 if task.status != TaskStatus::Pending {
65 return false;
66 }
67 if task.is_expanded() {
68 return false;
69 }
70 if let Some(ref parent_id) = task.parent_id {
71 let parent_expanded = phase
72 .get_task(parent_id)
73 .map(|p| p.is_expanded())
74 .unwrap_or(false);
75 if !parent_expanded {
76 return false;
77 }
78 }
79 true
80}
81
82pub fn is_task_ready(task: &Task, phase: &Phase, all_tasks: &[&Task]) -> bool {
84 is_task_spawnable(task, phase) && task.has_dependencies_met_refs(all_tasks)
85}
86
87pub fn resolve_group_tag(
95 storage: &Storage,
96 explicit_tag: Option<&str>,
97 allow_interactive: bool,
98) -> Result<String> {
99 if let Some(tag) = explicit_tag {
101 let tasks = storage.load_tasks()?;
102 if !tasks.contains_key(tag) {
103 anyhow::bail!("Task group '{}' not found. Run: scud tags", tag);
104 }
105 return Ok(tag.to_string());
106 }
107
108 if let Some(active) = storage.get_active_group()? {
110 return Ok(active);
111 }
112
113 if allow_interactive && is_interactive() {
115 let tasks = storage.load_tasks()?;
116 if tasks.is_empty() {
117 anyhow::bail!(
118 "No task groups found. Create one with: scud parse-prd <file> --tag <tag>"
119 );
120 }
121
122 let mut tags: Vec<&String> = tasks.keys().collect();
123 tags.sort();
124
125 println!("{}", "No active task group set.".yellow());
127 let selection = Select::new()
128 .with_prompt("Select a task group")
129 .items(&tags)
130 .default(0)
131 .interact()?;
132
133 let selected = tags[selection].clone();
134
135 storage.set_active_group(&selected)?;
137 println!("{} {}", "Active group set to:".green(), selected.green());
138
139 return Ok(selected);
140 }
141
142 anyhow::bail!("No active task group. Use --tag <tag> or run: scud tags <tag>")
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149 use crate::models::phase::Phase;
150 use crate::models::task::Task;
151
152 #[test]
155 fn spawnable_pending_task() {
156 let mut phase = Phase::new("t".into());
157 phase.add_task(Task::new("1".into(), "A".into(), "d".into()));
158 assert!(is_task_spawnable(&phase.tasks[0], &phase));
159 }
160
161 #[test]
162 fn not_spawnable_in_progress() {
163 let mut phase = Phase::new("t".into());
164 let mut task = Task::new("1".into(), "A".into(), "d".into());
165 task.set_status(TaskStatus::InProgress);
166 phase.add_task(task);
167 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
168 }
169
170 #[test]
171 fn not_spawnable_done() {
172 let mut phase = Phase::new("t".into());
173 let mut task = Task::new("1".into(), "A".into(), "d".into());
174 task.set_status(TaskStatus::Done);
175 phase.add_task(task);
176 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
177 }
178
179 #[test]
180 fn not_spawnable_failed() {
181 let mut phase = Phase::new("t".into());
182 let mut task = Task::new("1".into(), "A".into(), "d".into());
183 task.set_status(TaskStatus::Failed);
184 phase.add_task(task);
185 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
186 }
187
188 #[test]
189 fn not_spawnable_expanded_status() {
190 let mut phase = Phase::new("t".into());
191 let mut task = Task::new("1".into(), "A".into(), "d".into());
192 task.set_status(TaskStatus::Expanded);
193 phase.add_task(task);
194 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
195 }
196
197 #[test]
198 fn not_spawnable_has_subtasks() {
199 let mut phase = Phase::new("t".into());
200 let mut parent = Task::new("1".into(), "Parent".into(), "d".into());
201 parent.subtasks = vec!["1.1".into()];
202 phase.add_task(parent);
203 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
205 }
206
207 #[test]
208 fn spawnable_subtask_of_expanded_parent() {
209 let mut phase = Phase::new("t".into());
210
211 let mut parent = Task::new("1".into(), "Parent".into(), "d".into());
212 parent.subtasks = vec!["1.1".into()];
213 phase.add_task(parent);
214
215 let mut child = Task::new("1.1".into(), "Child".into(), "d".into());
216 child.parent_id = Some("1".into());
217 phase.add_task(child);
218
219 assert!(is_task_spawnable(&phase.tasks[1], &phase));
221 }
222
223 #[test]
224 fn not_spawnable_subtask_of_unexpanded_parent() {
225 let mut phase = Phase::new("t".into());
226
227 let parent = Task::new("1".into(), "Parent".into(), "d".into());
229 phase.add_task(parent);
230
231 let mut child = Task::new("1.1".into(), "Child".into(), "d".into());
232 child.parent_id = Some("1".into());
233 phase.add_task(child);
234
235 assert!(!is_task_spawnable(&phase.tasks[1], &phase));
237 }
238
239 #[test]
240 fn spawnable_subtask_with_missing_parent() {
241 let mut phase = Phase::new("t".into());
243 let mut orphan = Task::new("1.1".into(), "Orphan".into(), "d".into());
244 orphan.parent_id = Some("nonexistent".into());
245 phase.add_task(orphan);
246
247 assert!(!is_task_spawnable(&phase.tasks[0], &phase));
249 }
250
251 #[test]
252 fn spawnable_task_no_parent() {
253 let mut phase = Phase::new("t".into());
255 let task = Task::new("1".into(), "Top".into(), "d".into());
256 phase.add_task(task);
257 assert!(is_task_spawnable(&phase.tasks[0], &phase));
258 }
259
260 #[test]
263 fn ready_no_deps() {
264 let mut phase = Phase::new("t".into());
265 phase.add_task(Task::new("1".into(), "A".into(), "d".into()));
266 let all: Vec<&Task> = phase.tasks.iter().collect();
267 assert!(is_task_ready(&phase.tasks[0], &phase, &all));
268 }
269
270 #[test]
271 fn not_ready_dep_pending() {
272 let mut phase = Phase::new("t".into());
273 phase.add_task(Task::new("1".into(), "First".into(), "d".into()));
274 let mut t2 = Task::new("2".into(), "Second".into(), "d".into());
275 t2.dependencies = vec!["1".into()];
276 phase.add_task(t2);
277 let all: Vec<&Task> = phase.tasks.iter().collect();
278
279 assert!(is_task_ready(&phase.tasks[0], &phase, &all));
280 assert!(!is_task_ready(&phase.tasks[1], &phase, &all));
281 }
282
283 #[test]
284 fn ready_dep_done() {
285 let mut phase = Phase::new("t".into());
286 let mut t1 = Task::new("1".into(), "First".into(), "d".into());
287 t1.set_status(TaskStatus::Done);
288 phase.add_task(t1);
289
290 let mut t2 = Task::new("2".into(), "Second".into(), "d".into());
291 t2.dependencies = vec!["1".into()];
292 phase.add_task(t2);
293 let all: Vec<&Task> = phase.tasks.iter().collect();
294
295 assert!(!is_task_ready(&phase.tasks[0], &phase, &all));
297 assert!(is_task_ready(&phase.tasks[1], &phase, &all));
299 }
300
301 #[test]
302 fn not_ready_expanded_even_with_deps_met() {
303 let mut phase = Phase::new("t".into());
304 let mut t1 = Task::new("1".into(), "First".into(), "d".into());
305 t1.set_status(TaskStatus::Done);
306 phase.add_task(t1);
307
308 let mut t2 = Task::new("2".into(), "Second".into(), "d".into());
309 t2.dependencies = vec!["1".into()];
310 t2.subtasks = vec!["2.1".into()]; phase.add_task(t2);
312 let all: Vec<&Task> = phase.tasks.iter().collect();
313
314 assert!(!is_task_ready(&phase.tasks[1], &phase, &all));
316 }
317
318 #[test]
319 fn not_ready_subtask_parent_unexpanded_deps_met() {
320 let mut phase = Phase::new("t".into());
321 phase.add_task(Task::new("1".into(), "Parent".into(), "d".into()));
323
324 let mut child = Task::new("1.1".into(), "Child".into(), "d".into());
325 child.parent_id = Some("1".into());
326 phase.add_task(child);
328 let all: Vec<&Task> = phase.tasks.iter().collect();
329
330 assert!(!is_task_ready(&phase.tasks[1], &phase, &all));
332 }
333
334 #[test]
335 fn ready_chain_of_three() {
336 let mut phase = Phase::new("t".into());
337 let mut t1 = Task::new("1".into(), "A".into(), "d".into());
338 t1.set_status(TaskStatus::Done);
339 phase.add_task(t1);
340
341 let mut t2 = Task::new("2".into(), "B".into(), "d".into());
342 t2.dependencies = vec!["1".into()];
343 t2.set_status(TaskStatus::Done);
344 phase.add_task(t2);
345
346 let mut t3 = Task::new("3".into(), "C".into(), "d".into());
347 t3.dependencies = vec!["2".into()];
348 phase.add_task(t3);
349
350 let all: Vec<&Task> = phase.tasks.iter().collect();
351
352 assert!(!is_task_ready(&phase.tasks[0], &phase, &all)); assert!(!is_task_ready(&phase.tasks[1], &phase, &all)); assert!(is_task_ready(&phase.tasks[2], &phase, &all)); }
356
357 #[test]
358 fn not_ready_multiple_deps_one_unmet() {
359 let mut phase = Phase::new("t".into());
360 let mut t1 = Task::new("1".into(), "A".into(), "d".into());
361 t1.set_status(TaskStatus::Done);
362 phase.add_task(t1);
363
364 phase.add_task(Task::new("2".into(), "B".into(), "d".into())); let mut t3 = Task::new("3".into(), "C".into(), "d".into());
367 t3.dependencies = vec!["1".into(), "2".into()];
368 phase.add_task(t3);
369
370 let all: Vec<&Task> = phase.tasks.iter().collect();
371
372 assert!(!is_task_ready(&phase.tasks[2], &phase, &all));
374 }
375
376 #[test]
377 fn ready_multiple_deps_all_met() {
378 let mut phase = Phase::new("t".into());
379 let mut t1 = Task::new("1".into(), "A".into(), "d".into());
380 t1.set_status(TaskStatus::Done);
381 phase.add_task(t1);
382
383 let mut t2 = Task::new("2".into(), "B".into(), "d".into());
384 t2.set_status(TaskStatus::Done);
385 phase.add_task(t2);
386
387 let mut t3 = Task::new("3".into(), "C".into(), "d".into());
388 t3.dependencies = vec!["1".into(), "2".into()];
389 phase.add_task(t3);
390
391 let all: Vec<&Task> = phase.tasks.iter().collect();
392 assert!(is_task_ready(&phase.tasks[2], &phase, &all));
393 }
394
395 #[test]
398 fn flatten_empty() {
399 let phases: HashMap<String, Phase> = HashMap::new();
400 assert!(flatten_all_tasks(&phases).is_empty());
401 }
402
403 #[test]
404 fn flatten_multiple_phases() {
405 let mut phases = HashMap::new();
406
407 let mut p1 = Phase::new("a".into());
408 p1.add_task(Task::new("1".into(), "X".into(), "d".into()));
409 phases.insert("a".into(), p1);
410
411 let mut p2 = Phase::new("b".into());
412 p2.add_task(Task::new("2".into(), "Y".into(), "d".into()));
413 p2.add_task(Task::new("3".into(), "Z".into(), "d".into()));
414 phases.insert("b".into(), p2);
415
416 let flat = flatten_all_tasks(&phases);
417 assert_eq!(flat.len(), 3);
418 }
419}