1use super::task::Task;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
6#[serde(rename_all = "lowercase")]
7pub enum IdFormat {
8 #[default]
10 Sequential,
11 Uuid,
13}
14
15impl IdFormat {
16 pub fn as_str(&self) -> &'static str {
18 match self {
19 IdFormat::Sequential => "sequential",
20 IdFormat::Uuid => "uuid",
21 }
22 }
23
24 pub fn parse(s: &str) -> Self {
26 match s.to_lowercase().as_str() {
27 "uuid" => IdFormat::Uuid,
28 _ => IdFormat::Sequential,
29 }
30 }
31}
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct Phase {
35 pub name: String,
36 pub tasks: Vec<Task>,
37 #[serde(default)]
39 pub id_format: IdFormat,
40}
41
42impl Phase {
43 pub fn new(name: String) -> Self {
44 Phase {
45 name,
46 tasks: Vec::new(),
47 id_format: IdFormat::default(),
48 }
49 }
50
51 pub fn add_task(&mut self, task: Task) {
52 self.tasks.push(task);
53 }
54
55 pub fn get_task(&self, task_id: &str) -> Option<&Task> {
56 self.tasks.iter().find(|t| t.id == task_id)
57 }
58
59 pub fn get_task_mut(&mut self, task_id: &str) -> Option<&mut Task> {
60 self.tasks.iter_mut().find(|t| t.id == task_id)
61 }
62
63 pub fn remove_task(&mut self, task_id: &str) -> Option<Task> {
64 self.tasks
65 .iter()
66 .position(|t| t.id == task_id)
67 .map(|idx| self.tasks.remove(idx))
68 }
69
70 pub fn get_stats(&self) -> PhaseStats {
71 let mut total = 0;
72 let mut pending = 0;
73 let mut in_progress = 0;
74 let mut done = 0;
75 let mut blocked = 0;
76 let mut expanded = 0;
77 let mut total_complexity = 0;
78
79 for task in &self.tasks {
80 if task.is_subtask() {
82 continue;
83 }
84
85 total += 1;
86
87 if !task.is_expanded() {
90 total_complexity += task.complexity;
91 }
92
93 match task.status {
94 super::task::TaskStatus::Pending => pending += 1,
95 super::task::TaskStatus::InProgress => in_progress += 1,
96 super::task::TaskStatus::Done => done += 1,
97 super::task::TaskStatus::Blocked => blocked += 1,
98 super::task::TaskStatus::Expanded => {
99 let all_subtasks_done = task.subtasks.iter().all(|subtask_id| {
101 self.get_task(subtask_id)
102 .map(|st| st.status == super::task::TaskStatus::Done)
103 .unwrap_or(false)
104 });
105 if all_subtasks_done && !task.subtasks.is_empty() {
106 done += 1;
107 } else {
108 expanded += 1;
109 }
110 }
111 _ => {}
112 }
113 }
114
115 PhaseStats {
116 total,
117 pending,
118 in_progress,
119 done,
120 blocked,
121 expanded,
122 total_complexity,
123 }
124 }
125
126 pub fn get_actionable_tasks(&self) -> Vec<&Task> {
128 self.tasks
129 .iter()
130 .filter(|t| {
131 if t.is_expanded() {
133 return false;
134 }
135 if let Some(ref parent_id) = t.parent_id {
137 self.get_task(parent_id)
139 .map(|p| p.is_expanded())
140 .unwrap_or(false)
141 } else {
142 true
144 }
145 })
146 .collect()
147 }
148
149 pub fn find_next_task(&self) -> Option<&Task> {
152 self.tasks.iter().find(|task| {
153 task.status == super::task::TaskStatus::Pending
154 && task.has_dependencies_met(&self.tasks)
155 })
156 }
157
158 pub fn find_next_task_cross_tag<'a>(&'a self, all_tasks: &[&Task]) -> Option<&'a Task> {
161 self.tasks.iter().find(|task| {
162 task.status == super::task::TaskStatus::Pending
163 && task.has_dependencies_met_refs(all_tasks)
164 })
165 }
166
167 pub fn get_tasks_needing_expansion(&self) -> Vec<&Task> {
168 self.tasks.iter().filter(|t| t.needs_expansion()).collect()
169 }
170}
171
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct PhaseStats {
174 pub total: usize,
175 pub pending: usize,
176 pub in_progress: usize,
177 pub done: usize,
178 pub blocked: usize,
179 pub expanded: usize,
180 pub total_complexity: u32,
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use crate::models::task::{Task, TaskStatus};
187
188 #[test]
189 fn test_phase_creation() {
190 let phase = Phase::new("phase-1-auth".to_string());
191
192 assert_eq!(phase.name, "phase-1-auth");
193 assert!(phase.tasks.is_empty());
194 }
195
196 #[test]
197 fn test_add_task() {
198 let mut phase = Phase::new("phase-1".to_string());
199 let task = Task::new(
200 "TASK-1".to_string(),
201 "Test Task".to_string(),
202 "Description".to_string(),
203 );
204
205 phase.add_task(task.clone());
206
207 assert_eq!(phase.tasks.len(), 1);
208 assert_eq!(phase.tasks[0].id, "TASK-1");
209 }
210
211 #[test]
212 fn test_get_task() {
213 let mut phase = Phase::new("phase-1".to_string());
214 let task = Task::new(
215 "TASK-1".to_string(),
216 "Test Task".to_string(),
217 "Description".to_string(),
218 );
219 phase.add_task(task);
220
221 let retrieved = phase.get_task("TASK-1");
222 assert!(retrieved.is_some());
223 assert_eq!(retrieved.unwrap().id, "TASK-1");
224
225 let missing = phase.get_task("TASK-99");
226 assert!(missing.is_none());
227 }
228
229 #[test]
230 fn test_get_task_mut() {
231 let mut phase = Phase::new("phase-1".to_string());
232 let task = Task::new(
233 "TASK-1".to_string(),
234 "Test Task".to_string(),
235 "Description".to_string(),
236 );
237 phase.add_task(task);
238
239 {
240 let task_mut = phase.get_task_mut("TASK-1").unwrap();
241 task_mut.set_status(TaskStatus::InProgress);
242 }
243
244 assert_eq!(
245 phase.get_task("TASK-1").unwrap().status,
246 TaskStatus::InProgress
247 );
248 }
249
250 #[test]
251 fn test_remove_task() {
252 let mut phase = Phase::new("phase-1".to_string());
253 let task1 = Task::new(
254 "TASK-1".to_string(),
255 "Task 1".to_string(),
256 "Desc".to_string(),
257 );
258 let task2 = Task::new(
259 "TASK-2".to_string(),
260 "Task 2".to_string(),
261 "Desc".to_string(),
262 );
263 phase.add_task(task1);
264 phase.add_task(task2);
265
266 let removed = phase.remove_task("TASK-1");
267 assert!(removed.is_some());
268 assert_eq!(removed.unwrap().id, "TASK-1");
269 assert_eq!(phase.tasks.len(), 1);
270 assert_eq!(phase.tasks[0].id, "TASK-2");
271
272 let missing = phase.remove_task("TASK-99");
273 assert!(missing.is_none());
274 }
275
276 #[test]
277 fn test_get_stats_empty_phase() {
278 let phase = Phase::new("phase-1".to_string());
279 let stats = phase.get_stats();
280
281 assert_eq!(stats.total, 0);
282 assert_eq!(stats.pending, 0);
283 assert_eq!(stats.in_progress, 0);
284 assert_eq!(stats.done, 0);
285 assert_eq!(stats.blocked, 0);
286 assert_eq!(stats.total_complexity, 0);
287 }
288
289 #[test]
290 fn test_get_stats_with_tasks() {
291 let mut phase = Phase::new("phase-1".to_string());
292
293 let mut task1 = Task::new(
294 "TASK-1".to_string(),
295 "Task 1".to_string(),
296 "Desc".to_string(),
297 );
298 task1.complexity = 3;
299 task1.set_status(TaskStatus::Done);
300
301 let mut task2 = Task::new(
302 "TASK-2".to_string(),
303 "Task 2".to_string(),
304 "Desc".to_string(),
305 );
306 task2.complexity = 5;
307 task2.set_status(TaskStatus::InProgress);
308
309 let mut task3 = Task::new(
310 "TASK-3".to_string(),
311 "Task 3".to_string(),
312 "Desc".to_string(),
313 );
314 task3.complexity = 8;
315 let mut task4 = Task::new(
318 "TASK-4".to_string(),
319 "Task 4".to_string(),
320 "Desc".to_string(),
321 );
322 task4.complexity = 2;
323 task4.set_status(TaskStatus::Blocked);
324
325 phase.add_task(task1);
326 phase.add_task(task2);
327 phase.add_task(task3);
328 phase.add_task(task4);
329
330 let stats = phase.get_stats();
331
332 assert_eq!(stats.total, 4);
333 assert_eq!(stats.pending, 1);
334 assert_eq!(stats.in_progress, 1);
335 assert_eq!(stats.done, 1);
336 assert_eq!(stats.blocked, 1);
337 assert_eq!(stats.total_complexity, 18); }
339
340 #[test]
341 fn test_find_next_task_no_dependencies() {
342 let mut phase = Phase::new("phase-1".to_string());
343
344 let mut task1 = Task::new(
345 "TASK-1".to_string(),
346 "Task 1".to_string(),
347 "Desc".to_string(),
348 );
349 task1.set_status(TaskStatus::Done);
350
351 let task2 = Task::new(
352 "TASK-2".to_string(),
353 "Task 2".to_string(),
354 "Desc".to_string(),
355 );
356 let task3 = Task::new(
359 "TASK-3".to_string(),
360 "Task 3".to_string(),
361 "Desc".to_string(),
362 );
363 phase.add_task(task1);
366 phase.add_task(task2);
367 phase.add_task(task3);
368
369 let next = phase.find_next_task();
370 assert!(next.is_some());
371 assert_eq!(next.unwrap().id, "TASK-2"); }
373
374 #[test]
375 fn test_find_next_task_with_dependencies() {
376 let mut phase = Phase::new("phase-1".to_string());
377
378 let mut task1 = Task::new(
379 "TASK-1".to_string(),
380 "Task 1".to_string(),
381 "Desc".to_string(),
382 );
383 task1.set_status(TaskStatus::Done);
384
385 let task2 = Task::new(
386 "TASK-2".to_string(),
387 "Task 2".to_string(),
388 "Desc".to_string(),
389 );
390 let mut task3 = Task::new(
393 "TASK-3".to_string(),
394 "Task 3".to_string(),
395 "Desc".to_string(),
396 );
397 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
398 phase.add_task(task1);
401 phase.add_task(task2);
402 phase.add_task(task3);
403
404 let next = phase.find_next_task();
405 assert!(next.is_some());
406 assert_eq!(next.unwrap().id, "TASK-2"); }
408
409 #[test]
410 fn test_find_next_task_dependencies_met() {
411 let mut phase = Phase::new("phase-1".to_string());
412
413 let mut task1 = Task::new(
414 "TASK-1".to_string(),
415 "Task 1".to_string(),
416 "Desc".to_string(),
417 );
418 task1.set_status(TaskStatus::Done);
419
420 let mut task2 = Task::new(
421 "TASK-2".to_string(),
422 "Task 2".to_string(),
423 "Desc".to_string(),
424 );
425 task2.set_status(TaskStatus::Done);
426
427 let mut task3 = Task::new(
428 "TASK-3".to_string(),
429 "Task 3".to_string(),
430 "Desc".to_string(),
431 );
432 task3.dependencies = vec!["TASK-1".to_string(), "TASK-2".to_string()];
433 phase.add_task(task1);
436 phase.add_task(task2);
437 phase.add_task(task3);
438
439 let next = phase.find_next_task();
440 assert!(next.is_some());
441 assert_eq!(next.unwrap().id, "TASK-3"); }
443
444 #[test]
445 fn test_find_next_task_none_available() {
446 let mut phase = Phase::new("phase-1".to_string());
447
448 let mut task1 = Task::new(
449 "TASK-1".to_string(),
450 "Task 1".to_string(),
451 "Desc".to_string(),
452 );
453 task1.set_status(TaskStatus::Done);
454
455 let mut task2 = Task::new(
456 "TASK-2".to_string(),
457 "Task 2".to_string(),
458 "Desc".to_string(),
459 );
460 task2.set_status(TaskStatus::InProgress);
461
462 phase.add_task(task1);
463 phase.add_task(task2);
464
465 let next = phase.find_next_task();
466 assert!(next.is_none()); }
468
469 #[test]
470 fn test_get_tasks_needing_expansion() {
471 let mut phase = Phase::new("phase-1".to_string());
472
473 let mut task1 = Task::new(
474 "TASK-1".to_string(),
475 "Small Task".to_string(),
476 "Desc".to_string(),
477 );
478 task1.complexity = 5;
479
480 let mut task2 = Task::new(
481 "TASK-2".to_string(),
482 "Medium Task".to_string(),
483 "Desc".to_string(),
484 );
485 task2.complexity = 13;
486
487 let mut task3 = Task::new(
488 "TASK-3".to_string(),
489 "Large Task".to_string(),
490 "Desc".to_string(),
491 );
492 task3.complexity = 21;
493
494 let mut task4 = Task::new(
495 "TASK-4".to_string(),
496 "Huge Task".to_string(),
497 "Desc".to_string(),
498 );
499 task4.complexity = 34;
500
501 phase.add_task(task1);
502 phase.add_task(task2);
503 phase.add_task(task3);
504 phase.add_task(task4);
505
506 let needing_expansion = phase.get_tasks_needing_expansion();
507
508 assert_eq!(needing_expansion.len(), 4); assert!(needing_expansion.iter().any(|t| t.id == "TASK-1"));
510 assert!(needing_expansion.iter().any(|t| t.id == "TASK-2"));
511 assert!(needing_expansion.iter().any(|t| t.id == "TASK-3"));
512 assert!(needing_expansion.iter().any(|t| t.id == "TASK-4"));
513 }
514
515 #[test]
516 fn test_phase_serialization() {
517 let mut phase = Phase::new("phase-1".to_string());
518 let task = Task::new(
519 "TASK-1".to_string(),
520 "Test Task".to_string(),
521 "Description".to_string(),
522 );
523 phase.add_task(task);
524
525 let json = serde_json::to_string(&phase).unwrap();
526 let deserialized: Phase = serde_json::from_str(&json).unwrap();
527
528 assert_eq!(phase.name, deserialized.name);
529 assert_eq!(phase.tasks.len(), deserialized.tasks.len());
530 assert_eq!(phase.tasks[0].id, deserialized.tasks[0].id);
531 }
532
533 #[test]
534 fn test_get_stats_expanded_with_all_subtasks_done() {
535 let mut phase = Phase::new("phase-1".to_string());
536
537 let mut parent = Task::new(
539 "TASK-1".to_string(),
540 "Parent Task".to_string(),
541 "Desc".to_string(),
542 );
543 parent.complexity = 8;
544 parent.set_status(TaskStatus::Expanded);
545 parent.subtasks = vec!["TASK-1.1".to_string(), "TASK-1.2".to_string()];
546
547 let mut subtask1 = Task::new(
549 "TASK-1.1".to_string(),
550 "Subtask 1".to_string(),
551 "Desc".to_string(),
552 );
553 subtask1.parent_id = Some("TASK-1".to_string());
554 subtask1.set_status(TaskStatus::Done);
555
556 let mut subtask2 = Task::new(
557 "TASK-1.2".to_string(),
558 "Subtask 2".to_string(),
559 "Desc".to_string(),
560 );
561 subtask2.parent_id = Some("TASK-1".to_string());
562 subtask2.set_status(TaskStatus::Done);
563
564 let mut task2 = Task::new(
566 "TASK-2".to_string(),
567 "Regular Task".to_string(),
568 "Desc".to_string(),
569 );
570 task2.complexity = 3;
571 task2.set_status(TaskStatus::Done);
572
573 phase.add_task(parent);
574 phase.add_task(subtask1);
575 phase.add_task(subtask2);
576 phase.add_task(task2);
577
578 let stats = phase.get_stats();
579
580 assert_eq!(stats.total, 2);
582 assert_eq!(stats.done, 2);
584 assert_eq!(stats.expanded, 0);
586 }
587
588 #[test]
589 fn test_get_stats_expanded_with_incomplete_subtasks() {
590 let mut phase = Phase::new("phase-1".to_string());
591
592 let mut parent = Task::new(
594 "TASK-1".to_string(),
595 "Parent Task".to_string(),
596 "Desc".to_string(),
597 );
598 parent.complexity = 8;
599 parent.set_status(TaskStatus::Expanded);
600 parent.subtasks = vec!["TASK-1.1".to_string(), "TASK-1.2".to_string()];
601
602 let mut subtask1 = Task::new(
604 "TASK-1.1".to_string(),
605 "Subtask 1".to_string(),
606 "Desc".to_string(),
607 );
608 subtask1.parent_id = Some("TASK-1".to_string());
609 subtask1.set_status(TaskStatus::Done);
610
611 let mut subtask2 = Task::new(
612 "TASK-1.2".to_string(),
613 "Subtask 2".to_string(),
614 "Desc".to_string(),
615 );
616 subtask2.parent_id = Some("TASK-1".to_string());
617 phase.add_task(parent);
620 phase.add_task(subtask1);
621 phase.add_task(subtask2);
622
623 let stats = phase.get_stats();
624
625 assert_eq!(stats.total, 1);
627 assert_eq!(stats.expanded, 1);
629 assert_eq!(stats.done, 0);
630 }
631}