1use super::task::{Task, TaskStatus};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use crate::weave::bthread::{BThread, NodeAnnotation, PartitionDef, Role, WeaveEdge};
6use crate::weave::coordinator::ActiveLock;
7
8#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum IdFormat {
12 #[default]
14 Sequential,
15 Uuid,
17}
18
19impl IdFormat {
20 pub fn as_str(&self) -> &'static str {
22 match self {
23 IdFormat::Sequential => "sequential",
24 IdFormat::Uuid => "uuid",
25 }
26 }
27
28 pub fn parse(s: &str) -> Self {
30 match s.to_lowercase().as_str() {
31 "uuid" => IdFormat::Uuid,
32 _ => IdFormat::Sequential,
33 }
34 }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Phase {
39 pub name: String,
40 pub tasks: Vec<Task>,
41 #[serde(default)]
43 pub id_format: IdFormat,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
46 pub weave_threads: Vec<BThread>,
47 #[serde(default, skip_serializing_if = "Vec::is_empty")]
49 pub roles: Vec<Role>,
50 #[serde(default, skip_serializing_if = "Vec::is_empty")]
52 pub partitions: Vec<PartitionDef>,
53 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub locks: Vec<ActiveLock>,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
58 pub weave_edges: Vec<WeaveEdge>,
59 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
61 pub node_annotations: HashMap<String, NodeAnnotation>,
62}
63
64impl Phase {
65 pub fn new(name: String) -> Self {
66 Phase {
67 name,
68 tasks: Vec::new(),
69 id_format: IdFormat::default(),
70 weave_threads: Vec::new(),
71 roles: Vec::new(),
72 partitions: Vec::new(),
73 locks: Vec::new(),
74 weave_edges: Vec::new(),
75 node_annotations: HashMap::new(),
76 }
77 }
78
79 pub fn add_task(&mut self, task: Task) {
80 self.tasks.push(task);
81 }
82
83 pub fn get_task(&self, task_id: &str) -> Option<&Task> {
84 self.tasks.iter().find(|t| t.id == task_id)
85 }
86
87 pub fn get_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
88 self.tasks.iter_mut().find(|t| t.id == task_id)
89 }
90
91 pub fn remove_task(&mut self, task_id: &str) -> Option<Task> {
92 self.tasks
93 .iter()
94 .position(|t| t.id == task_id)
95 .map(|idx| self.tasks.remove(idx))
96 }
97
98 pub fn get_stats(&self) -> PhaseStats {
99 let mut total = 0;
100 let mut pending = 0;
101 let mut in_progress = 0;
102 let mut done = 0;
103 let mut blocked = 0;
104 let mut expanded = 0;
105 let mut total_complexity = 0;
106
107 for task in &self.tasks {
108 if task.is_subtask() {
110 continue;
111 }
112
113 total += 1;
114
115 if !task.is_expanded() {
118 total_complexity += task.complexity;
119 }
120
121 match task.status {
122 TaskStatus::Pending => pending += 1,
123 TaskStatus::InProgress => in_progress += 1,
124 TaskStatus::Done => done += 1,
125 TaskStatus::Blocked => blocked += 1,
126 TaskStatus::Expanded => {
127 let all_subtasks_done = task.subtasks.iter().all(|subtask_id| {
129 self.get_task(subtask_id)
130 .map(|st| st.status == TaskStatus::Done)
131 .unwrap_or(false)
132 });
133 if all_subtasks_done && !task.subtasks.is_empty() {
134 done += 1;
135 } else {
136 expanded += 1;
137 }
138 }
139 _ => {}
140 }
141 }
142
143 PhaseStats {
144 total,
145 pending,
146 in_progress,
147 done,
148 blocked,
149 expanded,
150 total_complexity,
151 }
152 }
153
154 pub fn get_actionable_tasks(&self) -> Vec<&Task> {
156 self.tasks
157 .iter()
158 .filter(|t| {
159 if t.is_expanded() {
161 return false;
162 }
163 if let Some(ref parent_id) = t.parent_id {
165 self.get_task(parent_id)
167 .map(|p| p.is_expanded())
168 .unwrap_or(false)
169 } else {
170 true
172 }
173 })
174 .collect()
175 }
176
177 pub fn find_next_task(&self) -> Option<&Task> {
180 self.tasks.iter().find(|task| {
181 task.status == TaskStatus::Pending && task.has_dependencies_met(&self.tasks)
182 })
183 }
184
185 pub fn find_next_task_cross_tag<'a>(&'a self, all_tasks: &[&Task]) -> Option<&'a Task> {
188 self.tasks.iter().find(|task| {
189 task.status == TaskStatus::Pending && task.has_dependencies_met_refs(all_tasks)
190 })
191 }
192
193 pub fn get_tasks_needing_expansion(&self) -> Vec<&Task> {
194 self.tasks.iter().filter(|t| t.needs_expansion()).collect()
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct PhaseStats {
200 pub total: usize,
201 pub pending: usize,
202 pub in_progress: usize,
203 pub done: usize,
204 pub blocked: usize,
205 pub expanded: usize,
206 pub total_complexity: u32,
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::models::task::{Task, TaskStatus};
213
214 #[test]
215 fn test_phase_creation() {
216 let phase = Phase::new("phase-1-auth".to_string());
217
218 assert_eq!(phase.name, "phase-1-auth");
219 assert!(phase.tasks.is_empty());
220 }
221
222 #[test]
223 fn test_add_task() {
224 let mut phase = Phase::new("phase-1".to_string());
225 let task = Task::new(
226 "TASK-1".to_string(),
227 "Test Task".to_string(),
228 "Description".to_string(),
229 );
230
231 phase.add_task(task.clone());
232
233 assert_eq!(phase.tasks.len(), 1);
234 assert_eq!(phase.tasks[0].id, "TASK-1");
235 }
236
237 #[test]
238 fn test_get_task() {
239 let mut phase = Phase::new("phase-1".to_string());
240 let task = Task::new(
241 "TASK-1".to_string(),
242 "Test Task".to_string(),
243 "Description".to_string(),
244 );
245 phase.add_task(task);
246
247 let retrieved = phase.get_task("TASK-1");
248 assert!(retrieved.is_some());
249 assert_eq!(retrieved.unwrap().id, "TASK-1");
250
251 let missing = phase.get_task("TASK-99");
252 assert!(missing.is_none());
253 }
254
255 #[test]
256 fn test_get_task_mut() {
257 let mut phase = Phase::new("phase-1".to_string());
258 let task = Task::new(
259 "TASK-1".to_string(),
260 "Test Task".to_string(),
261 "Description".to_string(),
262 );
263 phase.add_task(task);
264
265 {
266 let task_mut = phase.get_task_mut("TASK-1").unwrap();
267 task_mut.set_status(TaskStatus::InProgress);
268 }
269
270 assert_eq!(
271 phase.get_task("TASK-1").unwrap().status,
272 TaskStatus::InProgress
273 );
274 }
275
276 #[test]
277 fn test_remove_task() {
278 let mut phase = Phase::new("phase-1".to_string());
279 let task1 = Task::new(
280 "TASK-1".to_string(),
281 "Task 1".to_string(),
282 "Desc".to_string(),
283 );
284 let task2 = Task::new(
285 "TASK-2".to_string(),
286 "Task 2".to_string(),
287 "Desc".to_string(),
288 );
289 phase.add_task(task1);
290 phase.add_task(task2);
291
292 let removed = phase.remove_task("TASK-1");
293 assert!(removed.is_some());
294 assert_eq!(removed.unwrap().id, "TASK-1");
295 assert_eq!(phase.tasks.len(), 1);
296 assert_eq!(phase.tasks[0].id, "TASK-2");
297
298 let missing = phase.remove_task("TASK-99");
299 assert!(missing.is_none());
300 }
301
302 #[test]
303 fn test_get_stats_empty_phase() {
304 let phase = Phase::new("phase-1".to_string());
305 let stats = phase.get_stats();
306
307 assert_eq!(stats.total, 0);
308 assert_eq!(stats.pending, 0);
309 assert_eq!(stats.in_progress, 0);
310 assert_eq!(stats.done, 0);
311 assert_eq!(stats.blocked, 0);
312 assert_eq!(stats.total_complexity, 0);
313 }
314
315 #[test]
316 fn test_get_stats_with_tasks() {
317 let mut phase = Phase::new("phase-1".to_string());
318
319 let mut task1 = Task::new(
320 "TASK-1".to_string(),
321 "Task 1".to_string(),
322 "Desc".to_string(),
323 );
324 task1.complexity = 3;
325 task1.set_status(TaskStatus::Done);
326
327 let mut task2 = Task::new(
328 "TASK-2".to_string(),
329 "Task 2".to_string(),
330 "Desc".to_string(),
331 );
332 task2.complexity = 5;
333 task2.set_status(TaskStatus::InProgress);
334
335 let mut task3 = Task::new(
336 "TASK-3".to_string(),
337 "Task 3".to_string(),
338 "Desc".to_string(),
339 );
340 task3.complexity = 8;
341 let mut task4 = Task::new(
344 "TASK-4".to_string(),
345 "Task 4".to_string(),
346 "Desc".to_string(),
347 );
348 task4.complexity = 2;
349 task4.set_status(TaskStatus::Blocked);
350
351 phase.add_task(task1);
352 phase.add_task(task2);
353 phase.add_task(task3);
354 phase.add_task(task4);
355
356 let stats = phase.get_stats();
357
358 assert_eq!(stats.total, 4);
359 assert_eq!(stats.pending, 1);
360 assert_eq!(stats.in_progress, 1);
361 assert_eq!(stats.done, 1);
362 assert_eq!(stats.blocked, 1);
363 assert_eq!(stats.total_complexity, 18); }
365
366 #[test]
367 fn test_find_next_task_no_dependencies() {
368 let mut phase = Phase::new("phase-1".to_string());
369
370 let mut task1 = Task::new(
371 "TASK-1".to_string(),
372 "Task 1".to_string(),
373 "Desc".to_string(),
374 );
375 task1.set_status(TaskStatus::Done);
376
377 let task2 = Task::new(
378 "TASK-2".to_string(),
379 "Task 2".to_string(),
380 "Desc".to_string(),
381 );
382 phase.add_task(task1);
385 phase.add_task(task2);
386
387 let next = phase.find_next_task();
388 assert!(next.is_some());
389 assert_eq!(next.unwrap().id, "TASK-2"); }
391
392 #[test]
393 fn test_find_next_task_with_dependencies() {
394 let mut phase = Phase::new("phase-1".to_string());
395
396 let mut task1 = Task::new(
397 "TASK-1".to_string(),
398 "Task 1".to_string(),
399 "Desc".to_string(),
400 );
401 task1.set_status(TaskStatus::Done);
402
403 let task2 = Task::new(
404 "TASK-2".to_string(),
405 "Task 2".to_string(),
406 "Desc".to_string(),
407 );
408 let mut task3 = Task::new(
411 "TASK-3".to_string(),
412 "Task 3".to_string(),
413 "Desc".to_string(),
414 );
415 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
416 phase.add_task(task1);
419 phase.add_task(task2);
420 phase.add_task(task3);
421
422 let next = phase.find_next_task();
423 assert!(next.is_some());
424 assert_eq!(next.unwrap().id, "TASK-2"); }
426
427 #[test]
428 fn test_find_next_task_none_available() {
429 let mut phase = Phase::new("phase-1".to_string());
430
431 let mut task1 = Task::new(
432 "TASK-1".to_string(),
433 "Task 1".to_string(),
434 "Desc".to_string(),
435 );
436 task1.set_status(TaskStatus::Done);
437
438 let mut task2 = Task::new(
439 "TASK-2".to_string(),
440 "Task 2".to_string(),
441 "Desc".to_string(),
442 );
443 task2.set_status(TaskStatus::InProgress);
444
445 phase.add_task(task1);
446 phase.add_task(task2);
447
448 let next = phase.find_next_task();
449 assert!(next.is_none()); }
451
452 #[test]
453 fn test_phase_serialization() {
454 let mut phase = Phase::new("phase-1".to_string());
455 let task = Task::new(
456 "TASK-1".to_string(),
457 "Test Task".to_string(),
458 "Description".to_string(),
459 );
460 phase.add_task(task);
461
462 let json = serde_json::to_string(&phase).unwrap();
463 let deserialized: Phase = serde_json::from_str(&json).unwrap();
464
465 assert_eq!(phase.name, deserialized.name);
466 assert_eq!(phase.tasks.len(), deserialized.tasks.len());
467 assert_eq!(phase.tasks[0].id, deserialized.tasks[0].id);
468 }
469
470 #[test]
471 fn test_id_format_parse() {
472 assert_eq!(IdFormat::parse("sequential"), IdFormat::Sequential);
473 assert_eq!(IdFormat::parse("uuid"), IdFormat::Uuid);
474 assert_eq!(IdFormat::parse("UUID"), IdFormat::Uuid);
475 assert_eq!(IdFormat::parse("unknown"), IdFormat::Sequential); }
477
478 #[test]
479 fn test_id_format_as_str() {
480 assert_eq!(IdFormat::Sequential.as_str(), "sequential");
481 assert_eq!(IdFormat::Uuid.as_str(), "uuid");
482 }
483}