1use anyhow::Result;
2use colored::Colorize;
3use std::collections::{HashMap, HashSet};
4use std::path::PathBuf;
5
6use crate::models::task::{Task, TaskStatus};
7use crate::storage::Storage;
8
9#[derive(Debug)]
10pub struct Wave {
11 pub number: usize,
12 pub tasks: Vec<String>,
13}
14
15pub fn run(
16 project_root: Option<PathBuf>,
17 tag: Option<&str>,
18 max_parallel: usize,
19 all_tags: bool,
20) -> Result<()> {
21 if max_parallel == 0 {
23 anyhow::bail!("--max-parallel must be at least 1");
24 }
25
26 let storage = Storage::new(project_root);
27 let all_tasks = storage.load_tasks()?;
28
29 let phase_tags: Vec<String> = if all_tags {
31 all_tasks.keys().cloned().collect()
32 } else if let Some(t) = tag {
33 if !all_tasks.contains_key(t) {
34 anyhow::bail!("Phase '{}' not found. Run: scud tags", t);
35 }
36 vec![t.to_string()]
37 } else {
38 let active = storage.get_active_group()?;
40 match active {
41 Some(t) => vec![t],
42 None => anyhow::bail!("No active task group. Use --tag <phase-tag> or run: scud tags"),
43 }
44 };
45
46 let mut actionable: Vec<&Task> = Vec::new();
48 for tag in &phase_tags {
49 if let Some(phase) = all_tasks.get(tag) {
50 for task in &phase.tasks {
51 if task.status != TaskStatus::Done
53 && task.status != TaskStatus::Expanded
54 && task.status != TaskStatus::Cancelled
55 {
56 if let Some(ref parent_id) = task.parent_id {
58 let parent_expanded = phase
59 .get_task(parent_id)
60 .map(|p| p.is_expanded())
61 .unwrap_or(false);
62 if parent_expanded {
63 actionable.push(task);
64 }
65 } else {
66 actionable.push(task);
68 }
69 }
70 }
71 }
72 }
73
74 if actionable.is_empty() {
75 println!("{}", "No actionable tasks found.".yellow());
76 println!("All tasks may be completed, expanded, or cancelled.");
77 return Ok(());
78 }
79
80 let waves = compute_waves(&actionable, max_parallel);
82
83 println!(
85 "\n{} {}",
86 "Execution Waves".blue().bold(),
87 format!("(max {} parallel)", max_parallel).dimmed()
88 );
89 println!("{}", "=".repeat(50).blue());
90 println!();
91
92 let mut total_tasks = 0;
93 for wave in &waves {
94 total_tasks += wave.tasks.len();
95
96 let batch_info = if wave.tasks.len() > max_parallel {
97 format!(
98 " (batched into {} rounds)",
99 wave.tasks.len().div_ceil(max_parallel)
100 )
101 } else {
102 String::new()
103 };
104
105 println!(
106 "{} {} task{}{}",
107 format!("Wave {}:", wave.number).yellow().bold(),
108 wave.tasks.len(),
109 if wave.tasks.len() == 1 { "" } else { "s" },
110 batch_info.dimmed()
111 );
112
113 for (round_idx, chunk) in wave.tasks.chunks(max_parallel).enumerate() {
114 if wave.tasks.len() > max_parallel {
115 println!(" {} {}", "Round".dimmed(), round_idx + 1);
116 }
117
118 for task_id in chunk {
119 if let Some(task) = actionable.iter().find(|t| &t.id == task_id) {
121 let status_indicator = match task.status {
122 TaskStatus::Pending => "○".white(),
123 TaskStatus::InProgress => "●".cyan(),
124 TaskStatus::Blocked => "✗".red(),
125 _ => "?".dimmed(),
126 };
127
128 let deps = if task.dependencies.is_empty() {
129 String::new()
130 } else {
131 format!(" <- {}", task.dependencies.join(", "))
132 .dimmed()
133 .to_string()
134 };
135
136 let complexity = if task.complexity > 0 {
137 format!(" [{}]", task.complexity).dimmed().to_string()
138 } else {
139 String::new()
140 };
141
142 println!(
143 " {} {} {}{}{}",
144 status_indicator,
145 task_id.cyan(),
146 task.title,
147 complexity,
148 deps
149 );
150 }
151 }
152 }
153 println!();
154 }
155
156 println!("{}", "Summary".blue().bold());
158 println!("{}", "-".repeat(30).blue());
159
160 let total_waves = waves.len();
161 let total_rounds: usize = waves
162 .iter()
163 .map(|w| w.tasks.len().div_ceil(max_parallel))
164 .sum();
165
166 println!(" Total tasks: {}", total_tasks);
167 println!(" Total waves: {}", total_waves);
168 println!(" Total rounds: {}", total_rounds);
169
170 if total_tasks > 0 && total_rounds > 0 {
171 let speedup = total_tasks as f64 / total_rounds as f64;
172 println!(" Speedup: {}", format!("{:.1}x", speedup).green());
173 println!(
174 " {}",
175 format!(
176 "(from {} sequential to {} parallel rounds)",
177 total_tasks, total_rounds
178 )
179 .dimmed()
180 );
181 }
182
183 let blocked: Vec<_> = actionable
185 .iter()
186 .filter(|t| t.status == TaskStatus::Blocked)
187 .collect();
188 if !blocked.is_empty() {
189 println!();
190 println!("{}", "Blocked Tasks:".red().bold());
191 for task in blocked {
192 println!(" {} {}", task.id.red(), task.title);
193 }
194 }
195
196 println!();
197
198 Ok(())
199}
200
201fn compute_waves(tasks: &[&Task], _max_parallel: usize) -> Vec<Wave> {
204 let task_ids: HashSet<String> = tasks.iter().map(|t| t.id.clone()).collect();
207
208 let mut in_degree: HashMap<String, usize> = HashMap::new();
210 let mut dependents: HashMap<String, Vec<String>> = HashMap::new();
211
212 for task in tasks {
213 in_degree.entry(task.id.clone()).or_insert(0);
214
215 for dep in &task.dependencies {
216 if task_ids.contains(dep) {
218 *in_degree.entry(task.id.clone()).or_insert(0) += 1;
219 dependents
220 .entry(dep.clone())
221 .or_default()
222 .push(task.id.clone());
223 }
224 }
225 }
226
227 let mut waves: Vec<Wave> = Vec::new();
229 let mut remaining = in_degree.clone();
230 let mut wave_number = 1;
231
232 while !remaining.is_empty() {
233 let ready: Vec<String> = remaining
235 .iter()
236 .filter(|(_, °)| deg == 0)
237 .map(|(id, _)| id.clone())
238 .collect();
239
240 if ready.is_empty() {
241 println!("{}", "Warning: Circular dependency detected!".red().bold());
243 println!("The following tasks have unresolved dependencies:");
244 for id in remaining.keys() {
245 if let Some(task) = tasks.iter().find(|t| &t.id == id) {
246 let unmet_deps: Vec<_> = task
247 .dependencies
248 .iter()
249 .filter(|d| remaining.contains_key(*d))
250 .collect();
251 println!(" {} depends on {:?}", id, unmet_deps);
252 }
253 }
254 break;
255 }
256
257 for task_id in &ready {
259 remaining.remove(task_id);
260
261 if let Some(deps) = dependents.get(task_id) {
262 for dep_id in deps {
263 if let Some(deg) = remaining.get_mut(dep_id) {
264 *deg = deg.saturating_sub(1);
265 }
266 }
267 }
268 }
269
270 waves.push(Wave {
271 number: wave_number,
272 tasks: ready,
273 });
274 wave_number += 1;
275 }
276
277 waves
278}