Skip to main content

scud_task_core/models/
phase.rs

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/// ID format for task generation
9#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
10#[serde(rename_all = "lowercase")]
11pub enum IdFormat {
12    /// Sequential numeric IDs: "1", "2", "3", subtasks: "1.1", "1.2"
13    #[default]
14    Sequential,
15    /// UUID v4 IDs: 32-character hex strings
16    Uuid,
17}
18
19impl IdFormat {
20    /// Convert to string representation for SCG format
21    pub fn as_str(&self) -> &'static str {
22        match self {
23            IdFormat::Sequential => "sequential",
24            IdFormat::Uuid => "uuid",
25        }
26    }
27
28    /// Parse from string representation
29    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    /// ID format used for this phase (default: Sequential for backwards compatibility)
42    #[serde(default)]
43    pub id_format: IdFormat,
44    /// B-thread definitions from @weave section.
45    #[serde(default, skip_serializing_if = "Vec::is_empty")]
46    pub weave_threads: Vec<BThread>,
47    /// Agent role definitions from @roles section.
48    #[serde(default, skip_serializing_if = "Vec::is_empty")]
49    pub roles: Vec<Role>,
50    /// Partition definitions from @partitions section.
51    #[serde(default, skip_serializing_if = "Vec::is_empty")]
52    pub partitions: Vec<PartitionDef>,
53    /// Active mutex locks from @locks section.
54    #[serde(default, skip_serializing_if = "Vec::is_empty")]
55    pub locks: Vec<ActiveLock>,
56    /// Behavioral edges (~~, >>, !=) from @edges section.
57    #[serde(default, skip_serializing_if = "Vec::is_empty")]
58    pub weave_edges: Vec<WeaveEdge>,
59    /// Node annotations (role=, scope=) from extended @nodes.
60    #[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            // Don't count subtasks in total (they're part of parent)
109            if task.is_subtask() {
110                continue;
111            }
112
113            total += 1;
114
115            // Only count complexity for non-expanded tasks
116            // (expanded tasks have their work represented by subtasks)
117            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                    // Check if all subtasks are done - if so, count as done
128                    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    /// Get actionable tasks (not expanded, not subtasks of incomplete parents)
155    pub fn get_actionable_tasks(&self) -> Vec<&Task> {
156        self.tasks
157            .iter()
158            .filter(|t| {
159                // Exclude expanded parents (work on subtasks instead)
160                if t.is_expanded() {
161                    return false;
162                }
163                // Include subtasks only if they're actionable
164                if let Some(ref parent_id) = t.parent_id {
165                    // Parent must be expanded
166                    self.get_task(parent_id)
167                        .map(|p| p.is_expanded())
168                        .unwrap_or(false)
169                } else {
170                    // Top-level task
171                    true
172                }
173            })
174            .collect()
175    }
176
177    /// Find next task using only local phase tasks for dependency checking.
178    /// For cross-tag dependency support, use find_next_task_cross_tag instead.
179    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    /// Find next task with cross-tag dependency support.
186    /// Pass flattened tasks from all phases for proper dependency resolution.
187    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        // Pending by default
342
343        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); // 3 + 5 + 8 + 2
364    }
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        // Pending, no dependencies
383
384        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"); // First pending task
390    }
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        // Pending, no dependencies
409
410        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        // Pending, but depends on TASK-2 which is not done
417
418        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"); // TASK-3 blocked by dependencies
425    }
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()); // No pending tasks
450    }
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); // Default
476    }
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}