1use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(tag = "type", rename_all = "snake_case")]
22pub enum TaskKind {
23 BreakCycle {
28 cycle: Vec<String>,
30 },
31 ImplementSpec {
33 spec_id: String,
35 },
36 Custom {
38 description: String,
40 },
41}
42
43impl std::fmt::Display for TaskKind {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::BreakCycle { .. } => write!(f, "CYCLE"),
47 Self::ImplementSpec { spec_id, .. } => write!(f, "SPEC-{spec_id}"),
48 Self::Custom { .. } => write!(f, "CUSTOM"),
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
55#[serde(rename_all = "snake_case")]
56pub enum TaskStatus {
57 #[default]
59 Pending,
60 Ready,
62 InProgress,
64 Completed,
66 Blocked,
68}
69
70impl std::fmt::Display for TaskStatus {
71 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72 match self {
73 Self::Pending => write!(f, "pending"),
74 Self::Ready => write!(f, "ready"),
75 Self::InProgress => write!(f, "in_progress"),
76 Self::Completed => write!(f, "completed"),
77 Self::Blocked => write!(f, "blocked"),
78 }
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct Task {
85 pub id: String,
87 pub name: String,
89 pub kind: TaskKind,
91 pub effort_hours: f32,
93 pub dependencies: Vec<String>,
95 pub status: TaskStatus,
97 pub earliest_start: f32,
99 pub earliest_finish: f32,
101 pub latest_start: f32,
103 pub latest_finish: f32,
105 pub float: f32,
107 pub is_critical: bool,
109 pub affected_files: Vec<String>,
111}
112
113impl Default for Task {
114 fn default() -> Self {
115 Self {
116 id: String::new(),
117 name: String::new(),
118 kind: TaskKind::Custom {
119 description: String::new(),
120 },
121 effort_hours: 0.0,
122 dependencies: Vec::new(),
123 status: TaskStatus::Pending,
124 earliest_start: 0.0,
125 earliest_finish: 0.0,
126 latest_start: 0.0,
127 latest_finish: 0.0,
128 float: 0.0,
129 is_critical: false,
130 affected_files: Vec::new(),
131 }
132 }
133}
134
135impl Task {
136 pub fn new(
138 id: impl Into<String>,
139 name: impl Into<String>,
140 kind: TaskKind,
141 effort_hours: f32,
142 ) -> Self {
143 Self {
144 id: id.into(),
145 name: name.into(),
146 kind,
147 effort_hours,
148 ..Default::default()
149 }
150 }
151
152 #[must_use]
154 pub fn depends_on(mut self, task_id: impl Into<String>) -> Self {
155 self.dependencies.push(task_id.into());
156 self
157 }
158
159 #[must_use]
161 pub fn affects_file(mut self, file: impl Into<String>) -> Self {
162 self.affected_files.push(file.into());
163 self
164 }
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct TaskBatch {
170 pub id: String,
172 pub tasks: Vec<String>,
174 pub total_effort_hours: f32,
176 pub duration_hours: f32,
178 pub start_time: f32,
180}
181
182impl Default for TaskBatch {
183 fn default() -> Self {
184 Self {
185 id: String::new(),
186 tasks: Vec::new(),
187 total_effort_hours: 0.0,
188 duration_hours: 0.0,
189 start_time: 0.0,
190 }
191 }
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct Bottleneck {
197 pub task_id: String,
199 pub task_name: String,
201 pub blocks_count: usize,
203 pub blocked_hours: f32,
205 pub roi: f32,
207 pub effort_hours: f32,
209}
210
211#[derive(Debug, Clone, Default, Serialize, Deserialize)]
213pub struct CriticalPathResult {
214 pub total_tasks: usize,
216 pub critical_path: Vec<String>,
218 pub critical_path_duration: f32,
220 pub total_duration_sequential: f32,
222 pub optimal_duration_parallel: f32,
224 pub speedup_factor: f32,
226 pub parallelizable_batches: Vec<TaskBatch>,
228 pub bottlenecks: Vec<Bottleneck>,
230 pub tasks: Vec<Task>,
232 pub unscheduled: Vec<String>,
239}
240
241impl CriticalPathResult {
242 #[must_use]
244 pub fn get_task(&self, id: &str) -> Option<&Task> {
245 self.tasks.iter().find(|t| t.id == id)
246 }
247}
248
249#[cfg(test)]
250#[allow(clippy::float_cmp)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn test_task_creation() {
256 let task = Task::new(
257 "SPEC-001",
258 "Implement login flow",
259 TaskKind::ImplementSpec {
260 spec_id: "auth.login".to_string(),
261 },
262 2.0,
263 )
264 .depends_on("SPEC-000")
265 .affects_file("src/auth/login.rs")
266 .affects_file("src/auth/mod.rs");
267
268 assert_eq!(task.id, "SPEC-001");
269 assert_eq!(task.effort_hours, 2.0);
270 assert_eq!(task.dependencies, vec!["SPEC-000"]);
271 assert_eq!(
272 task.affected_files,
273 vec!["src/auth/login.rs", "src/auth/mod.rs"]
274 );
275 }
276
277 #[test]
278 fn test_task_kind_display() {
279 assert_eq!(
280 TaskKind::ImplementSpec {
281 spec_id: "auth.login".to_string()
282 }
283 .to_string(),
284 "SPEC-auth.login"
285 );
286
287 assert_eq!(
288 TaskKind::BreakCycle {
289 cycle: vec!["a".to_string(), "b".to_string()]
290 }
291 .to_string(),
292 "CYCLE"
293 );
294
295 assert_eq!(
296 TaskKind::Custom {
297 description: "anything".to_string()
298 }
299 .to_string(),
300 "CUSTOM"
301 );
302 }
303
304 #[test]
305 fn test_get_task() {
306 let result = CriticalPathResult {
307 total_tasks: 2,
308 tasks: vec![
309 Task {
310 id: "T1".to_string(),
311 effort_hours: 5.0,
312 ..Default::default()
313 },
314 Task {
315 id: "T2".to_string(),
316 effort_hours: 3.0,
317 ..Default::default()
318 },
319 ],
320 ..Default::default()
321 };
322
323 assert_eq!(result.get_task("T2").expect("T2 present").effort_hours, 3.0);
324 assert!(result.get_task("missing").is_none());
325 }
326}