use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TaskKind {
BreakCycle {
cycle: Vec<String>,
},
ImplementSpec {
spec_id: String,
},
Custom {
description: String,
},
}
impl std::fmt::Display for TaskKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::BreakCycle { .. } => write!(f, "CYCLE"),
Self::ImplementSpec { spec_id, .. } => write!(f, "SPEC-{spec_id}"),
Self::Custom { .. } => write!(f, "CUSTOM"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskStatus {
#[default]
Pending,
Ready,
InProgress,
Completed,
Blocked,
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Pending => write!(f, "pending"),
Self::Ready => write!(f, "ready"),
Self::InProgress => write!(f, "in_progress"),
Self::Completed => write!(f, "completed"),
Self::Blocked => write!(f, "blocked"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub name: String,
pub kind: TaskKind,
pub effort_hours: f32,
pub dependencies: Vec<String>,
pub status: TaskStatus,
pub earliest_start: f32,
pub earliest_finish: f32,
pub latest_start: f32,
pub latest_finish: f32,
pub float: f32,
pub is_critical: bool,
pub affected_files: Vec<String>,
}
impl Default for Task {
fn default() -> Self {
Self {
id: String::new(),
name: String::new(),
kind: TaskKind::Custom {
description: String::new(),
},
effort_hours: 0.0,
dependencies: Vec::new(),
status: TaskStatus::Pending,
earliest_start: 0.0,
earliest_finish: 0.0,
latest_start: 0.0,
latest_finish: 0.0,
float: 0.0,
is_critical: false,
affected_files: Vec::new(),
}
}
}
impl Task {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
kind: TaskKind,
effort_hours: f32,
) -> Self {
Self {
id: id.into(),
name: name.into(),
kind,
effort_hours,
..Default::default()
}
}
#[must_use]
pub fn depends_on(mut self, task_id: impl Into<String>) -> Self {
self.dependencies.push(task_id.into());
self
}
#[must_use]
pub fn affects_file(mut self, file: impl Into<String>) -> Self {
self.affected_files.push(file.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskBatch {
pub id: String,
pub tasks: Vec<String>,
pub total_effort_hours: f32,
pub duration_hours: f32,
pub start_time: f32,
}
impl Default for TaskBatch {
fn default() -> Self {
Self {
id: String::new(),
tasks: Vec::new(),
total_effort_hours: 0.0,
duration_hours: 0.0,
start_time: 0.0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Bottleneck {
pub task_id: String,
pub task_name: String,
pub blocks_count: usize,
pub blocked_hours: f32,
pub roi: f32,
pub effort_hours: f32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CriticalPathResult {
pub total_tasks: usize,
pub critical_path: Vec<String>,
pub critical_path_duration: f32,
pub total_duration_sequential: f32,
pub optimal_duration_parallel: f32,
pub speedup_factor: f32,
pub parallelizable_batches: Vec<TaskBatch>,
pub bottlenecks: Vec<Bottleneck>,
pub tasks: Vec<Task>,
pub unscheduled: Vec<String>,
}
impl CriticalPathResult {
#[must_use]
pub fn get_task(&self, id: &str) -> Option<&Task> {
self.tasks.iter().find(|t| t.id == id)
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn test_task_creation() {
let task = Task::new(
"SPEC-001",
"Implement login flow",
TaskKind::ImplementSpec {
spec_id: "auth.login".to_string(),
},
2.0,
)
.depends_on("SPEC-000")
.affects_file("src/auth/login.rs")
.affects_file("src/auth/mod.rs");
assert_eq!(task.id, "SPEC-001");
assert_eq!(task.effort_hours, 2.0);
assert_eq!(task.dependencies, vec!["SPEC-000"]);
assert_eq!(
task.affected_files,
vec!["src/auth/login.rs", "src/auth/mod.rs"]
);
}
#[test]
fn test_task_kind_display() {
assert_eq!(
TaskKind::ImplementSpec {
spec_id: "auth.login".to_string()
}
.to_string(),
"SPEC-auth.login"
);
assert_eq!(
TaskKind::BreakCycle {
cycle: vec!["a".to_string(), "b".to_string()]
}
.to_string(),
"CYCLE"
);
assert_eq!(
TaskKind::Custom {
description: "anything".to_string()
}
.to_string(),
"CUSTOM"
);
}
#[test]
fn test_get_task() {
let result = CriticalPathResult {
total_tasks: 2,
tasks: vec![
Task {
id: "T1".to_string(),
effort_hours: 5.0,
..Default::default()
},
Task {
id: "T2".to_string(),
effort_hours: 3.0,
..Default::default()
},
],
..Default::default()
};
assert_eq!(result.get_task("T2").expect("T2 present").effort_hours, 3.0);
assert!(result.get_task("missing").is_none());
}
}