use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(from = "TaskDepDef", into = "TaskDepDef")]
pub enum TaskDep {
Index(usize),
Id(Uuid),
}
impl TaskDep {
pub fn to_uuid(&self, order_to_id: &std::collections::HashMap<usize, Uuid>) -> Option<Uuid> {
match self {
TaskDep::Id(uuid) => Some(*uuid),
TaskDep::Index(idx) => order_to_id.get(idx).copied(),
}
}
pub fn is_uuid(&self) -> bool {
matches!(self, TaskDep::Id(_))
}
pub fn as_uuid(&self) -> Option<Uuid> {
match self {
TaskDep::Id(uuid) => Some(*uuid),
TaskDep::Index(_) => None,
}
}
}
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum TaskDepDef {
Uuid(Uuid),
Index(usize),
}
impl From<TaskDepDef> for TaskDep {
fn from(def: TaskDepDef) -> Self {
match def {
TaskDepDef::Uuid(uuid) => TaskDep::Id(uuid),
TaskDepDef::Index(idx) => TaskDep::Index(idx),
}
}
}
impl From<TaskDep> for TaskDepDef {
fn from(dep: TaskDep) -> Self {
match dep {
TaskDep::Id(uuid) => TaskDepDef::Uuid(uuid),
TaskDep::Index(idx) => TaskDepDef::Index(idx),
}
}
}
pub fn deserialize_task_deps<'de, D>(deserializer: D) -> Result<Vec<TaskDep>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct TaskDepVisitor;
impl<'de> serde::de::Visitor<'de> for TaskDepVisitor {
type Value = Vec<TaskDep>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an array of task indices (1-based integers) or UUID strings")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let mut deps = Vec::new();
while let Some(val) = seq.next_element::<serde_json::Value>()? {
match val {
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
if i < 1 {
return Err(serde::de::Error::custom("task indices start at 1"));
}
deps.push(TaskDep::Index(i as usize));
} else {
return Err(serde::de::Error::custom(
"task index must be a positive integer",
));
}
}
serde_json::Value::String(s) => match Uuid::parse_str(&s) {
Ok(uuid) => deps.push(TaskDep::Id(uuid)),
Err(_) => {
return Err(serde::de::Error::custom(format!("invalid UUID: {}", s)));
}
},
_ => {
return Err(serde::de::Error::custom(
"task dependency must be an integer index or UUID string",
));
}
}
}
Ok(deps)
}
}
deserializer.deserialize_seq(TaskDepVisitor)
}
pub fn serialize_task_type<S>(task_type: &TaskType, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let s = match task_type {
TaskType::Research => "research",
TaskType::Edit => "edit",
TaskType::Create => "create",
TaskType::Delete => "delete",
TaskType::Test => "test",
TaskType::Refactor => "refactor",
TaskType::Documentation => "documentation",
TaskType::Configuration => "configuration",
TaskType::Build => "build",
TaskType::Other(s) => s.as_str(),
};
serializer.serialize_str(s)
}
pub fn deserialize_task_type<'de, D>(deserializer: D) -> Result<TaskType, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.to_lowercase().as_str() {
"research" => Ok(TaskType::Research),
"edit" => Ok(TaskType::Edit),
"create" => Ok(TaskType::Create),
"delete" => Ok(TaskType::Delete),
"test" => Ok(TaskType::Test),
"refactor" => Ok(TaskType::Refactor),
"documentation" => Ok(TaskType::Documentation),
"configuration" => Ok(TaskType::Configuration),
"build" => Ok(TaskType::Build),
other => Ok(TaskType::Other(other.to_string())),
}
}
fn default_uuid() -> Uuid {
Uuid::new_v4()
}
fn default_now() -> DateTime<Utc> {
Utc::now()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanDocument {
#[serde(default = "default_uuid")]
pub id: Uuid,
#[serde(default = "default_uuid")]
pub session_id: Uuid,
pub title: String,
pub description: String,
pub tasks: Vec<PlanTask>,
#[serde(default)]
pub context: String,
#[serde(default)]
pub risks: Vec<String>,
#[serde(default)]
pub test_strategy: String,
#[serde(default)]
pub technical_stack: Vec<String>,
#[serde(default)]
pub status: PlanStatus,
#[serde(default = "default_now")]
pub created_at: DateTime<Utc>,
#[serde(default = "default_now")]
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub approved_at: Option<DateTime<Utc>>,
}
impl PlanDocument {
pub fn new(session_id: Uuid, title: String, description: String) -> Self {
Self {
id: Uuid::new_v4(),
session_id,
title,
description,
tasks: Vec::new(),
context: String::new(),
risks: Vec::new(),
test_strategy: String::new(),
technical_stack: Vec::new(),
status: PlanStatus::Draft,
created_at: Utc::now(),
updated_at: Utc::now(),
approved_at: None,
}
}
pub fn add_task(&mut self, task: PlanTask) {
self.tasks.push(task);
self.updated_at = Utc::now();
}
pub fn resolve_index_deps(&mut self) {
use std::collections::HashMap;
let mut order_to_id: HashMap<usize, Uuid> = HashMap::new();
for (idx, task) in self.tasks.iter().enumerate() {
let order = if task.order > 0 { task.order } else { idx + 1 };
order_to_id.insert(order, task.id);
}
for task in &mut self.tasks {
let resolved: Vec<TaskDep> = task
.dependencies
.iter()
.map(|dep| {
match dep {
TaskDep::Index(idx) => {
if let Some(uuid) = order_to_id.get(idx) {
TaskDep::Id(*uuid)
} else {
TaskDep::Index(*idx)
}
}
TaskDep::Id(_) => dep.clone(),
}
})
.collect();
task.dependencies = resolved;
}
}
pub fn tasks_in_order(&self) -> Option<Vec<&PlanTask>> {
use std::collections::{HashMap, VecDeque};
let mut in_degree: HashMap<Uuid, usize> = HashMap::new();
let mut dependents: HashMap<Uuid, Vec<Uuid>> = HashMap::new();
for task in &self.tasks {
let uuid_deps: Vec<Uuid> = task
.dependencies
.iter()
.filter_map(|d| d.as_uuid())
.collect();
in_degree.insert(task.id, uuid_deps.len());
for dep_id in &uuid_deps {
dependents.entry(*dep_id).or_default().push(task.id);
}
}
let mut queue: VecDeque<Uuid> = VecDeque::new();
for task in &self.tasks {
if task.dependencies.is_empty() {
queue.push_back(task.id);
}
}
let mut sorted_ids = Vec::new();
while let Some(task_id) = queue.pop_front() {
sorted_ids.push(task_id);
if let Some(deps) = dependents.get(&task_id) {
for &dependent_id in deps {
if let Some(degree) = in_degree.get_mut(&dependent_id) {
*degree -= 1;
if *degree == 0 {
queue.push_back(dependent_id);
}
}
}
}
}
if sorted_ids.len() != self.tasks.len() {
return None; }
let task_map: HashMap<Uuid, &PlanTask> = self.tasks.iter().map(|t| (t.id, t)).collect();
Some(
sorted_ids
.iter()
.filter_map(|id| task_map.get(id).copied())
.collect(),
)
}
pub fn get_task(&self, task_id: &Uuid) -> Option<&PlanTask> {
self.tasks.iter().find(|t| t.id == *task_id)
}
pub fn get_task_mut(&mut self, task_id: &Uuid) -> Option<&mut PlanTask> {
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|t| t.id == *task_id)
}
pub fn count_by_status(&self, status: TaskStatus) -> usize {
self.tasks.iter().filter(|t| t.status == status).count()
}
pub fn progress_percentage(&self) -> f32 {
if self.tasks.is_empty() {
return 0.0;
}
let completed = self.count_by_status(TaskStatus::Completed);
(completed as f32 / self.tasks.len() as f32) * 100.0
}
pub fn is_complete(&self) -> bool {
!self.tasks.is_empty()
&& self
.tasks
.iter()
.all(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
}
pub fn approve(&mut self) {
self.status = PlanStatus::Approved;
self.approved_at = Some(Utc::now());
self.updated_at = Utc::now();
}
pub fn reject(&mut self) {
self.status = PlanStatus::Rejected;
self.updated_at = Utc::now();
}
pub fn start_execution(&mut self) {
self.status = PlanStatus::InProgress;
self.updated_at = Utc::now();
}
pub fn complete(&mut self) {
self.status = PlanStatus::Completed;
self.updated_at = Utc::now();
}
pub fn validate_dependencies(&self) -> Result<(), String> {
let task_ids: std::collections::HashSet<Uuid> = self.tasks.iter().map(|t| t.id).collect();
for task in &self.tasks {
for dep in &task.dependencies {
if dep.as_uuid().is_some_and(|id| !task_ids.contains(&id)) {
return Err(format!(
"❌ Invalid Dependency\n\n\
Task '{}' (#{}) depends on a task that doesn't exist.\n\n\
💡 Fix: Remove this dependency or ensure the referenced task is added first.",
task.title, task.order
));
}
}
}
let ordered = self.tasks_in_order();
if ordered.is_none() {
let unprocessed: Vec<&str> = self
.tasks
.iter()
.filter(|task| !task.dependencies.is_empty())
.map(|task| task.title.as_str())
.collect();
return Err(format!(
"❌ Circular Dependency Detected\n\n\
Tasks with dependencies: {}\n\n\
💡 Fix: Review the dependency chain and remove circular references.\n\
Example: If Task A depends on B, B depends on C, and C depends on A,\n\
you need to break one of these dependency links.",
unprocessed.join(", ")
));
}
Ok(())
}
pub fn next_executable_task(&self) -> Option<&PlanTask> {
let completed_ids: std::collections::HashSet<Uuid> = self
.tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
.map(|t| t.id)
.collect();
self.tasks.iter().find(|task| {
matches!(task.status, TaskStatus::Pending)
&& task
.dependencies
.iter()
.all(|dep| dep.as_uuid().is_some_and(|id| completed_ids.contains(&id)))
})
}
pub fn next_executable_task_mut(&mut self) -> Option<&mut PlanTask> {
let completed_ids: std::collections::HashSet<Uuid> = self
.tasks
.iter()
.filter(|t| matches!(t.status, TaskStatus::Completed | TaskStatus::Skipped))
.map(|t| t.id)
.collect();
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|task| {
matches!(task.status, TaskStatus::Pending)
&& task
.dependencies
.iter()
.all(|dep| dep.as_uuid().is_some_and(|id| completed_ids.contains(&id)))
})
}
pub fn get_task_by_order(&self, order: usize) -> Option<&PlanTask> {
self.tasks.iter().find(|t| t.order == order)
}
pub fn get_task_by_order_mut(&mut self, order: usize) -> Option<&mut PlanTask> {
self.updated_at = Utc::now();
self.tasks.iter_mut().find(|t| t.order == order)
}
pub fn dependencies_satisfied(&self, task: &PlanTask) -> bool {
task.dependencies.iter().all(|dep| {
dep.as_uuid()
.and_then(|id| self.get_task(&id))
.map(|dep| matches!(dep.status, TaskStatus::Completed | TaskStatus::Skipped))
.unwrap_or(false)
})
}
pub fn execution_summary(&self) -> ExecutionSummary {
let mut summary = ExecutionSummary::default();
for task in &self.tasks {
summary.total_tasks += 1;
match task.status {
TaskStatus::Completed => summary.completed += 1,
TaskStatus::Failed => summary.failed += 1,
TaskStatus::InProgress => summary.in_progress += 1,
TaskStatus::Pending => summary.pending += 1,
TaskStatus::Skipped => summary.skipped += 1,
TaskStatus::Blocked(_) => summary.blocked += 1,
}
summary.total_retries += task.retry_count as usize;
}
summary
}
pub fn ready_tasks(&self) -> Vec<&PlanTask> {
self.tasks
.iter()
.filter(|task| {
matches!(task.status, TaskStatus::Pending) && self.dependencies_satisfied(task)
})
.collect()
}
pub fn retriable_tasks(&self) -> Vec<&PlanTask> {
self.tasks.iter().filter(|task| task.can_retry()).collect()
}
pub fn get_validation_warnings(&self) -> Vec<String> {
let mut warnings = Vec::new();
for task in &self.tasks {
if task.complexity >= 5 {
warnings.push(format!(
"⚠️ Task '{}' has maximum complexity ({}★) - consider breaking it down",
task.title, task.complexity
));
}
if task.description.len() < 50 {
warnings.push(format!(
"💡 Task '{}' has a brief description ({} chars) - add more detail",
task.title,
task.description.len()
));
}
if task.acceptance_criteria.is_empty() {
warnings.push(format!(
"💡 Task '{}' has no acceptance criteria - define success criteria",
task.title
));
}
}
if self.tasks.len() > 20 {
warnings.push(format!(
"⚠️ Plan has {} tasks (>20) - consider splitting into smaller plans",
self.tasks.len()
));
}
if self.context.is_empty() {
warnings
.push("💡 Plan has no context - add environment info or constraints".to_string());
}
if self.risks.is_empty() {
warnings
.push("💡 Plan has no identified risks - document potential issues".to_string());
}
if self.test_strategy.is_empty() {
warnings
.push("💡 Plan has no test strategy - define how to verify success".to_string());
}
warnings
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ExecutionSummary {
pub total_tasks: usize,
pub completed: usize,
pub failed: usize,
pub in_progress: usize,
pub pending: usize,
pub skipped: usize,
pub blocked: usize,
pub total_retries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum PlanStatus {
#[default]
Draft,
PendingApproval,
Approved,
Rejected,
InProgress,
Completed,
Cancelled,
}
impl std::fmt::Display for PlanStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanStatus::Draft => write!(f, "Draft"),
PlanStatus::PendingApproval => write!(f, "Pending Approval"),
PlanStatus::Approved => write!(f, "Approved"),
PlanStatus::Rejected => write!(f, "Rejected"),
PlanStatus::InProgress => write!(f, "In Progress"),
PlanStatus::Completed => write!(f, "Completed"),
PlanStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanTask {
#[serde(default = "default_uuid")]
pub id: Uuid,
#[serde(default)]
pub order: usize,
pub title: String,
pub description: String,
#[serde(
deserialize_with = "deserialize_task_type",
serialize_with = "serialize_task_type"
)]
pub task_type: TaskType,
#[serde(default, deserialize_with = "deserialize_task_deps")]
pub dependencies: Vec<TaskDep>,
#[serde(default)]
pub complexity: u8,
#[serde(default)]
pub acceptance_criteria: Vec<String>,
#[serde(default)]
pub status: TaskStatus,
#[serde(default)]
pub notes: Option<String>,
#[serde(default)]
pub completed_at: Option<DateTime<Utc>>,
#[serde(default)]
pub retry_count: u8,
#[serde(default = "default_max_retries")]
pub max_retries: u8,
#[serde(default)]
pub artifacts: Vec<String>,
}
fn default_max_retries() -> u8 {
3
}
impl PlanTask {
pub fn new(order: usize, title: String, description: String, task_type: TaskType) -> Self {
Self {
id: Uuid::new_v4(),
order,
title,
description,
task_type,
dependencies: Vec::new(),
complexity: 3, acceptance_criteria: Vec::new(),
status: TaskStatus::Pending,
notes: None,
completed_at: None,
retry_count: 0,
max_retries: 3,
artifacts: Vec::new(),
}
}
pub fn start(&mut self) {
self.status = TaskStatus::InProgress;
}
pub fn add_artifact(&mut self, artifact: String) {
self.artifacts.push(artifact);
}
pub fn can_retry(&self) -> bool {
self.retry_count < self.max_retries
&& matches!(self.status, TaskStatus::Pending | TaskStatus::Failed)
}
pub fn complete(&mut self, notes: Option<String>) {
self.status = TaskStatus::Completed;
self.notes = notes;
self.completed_at = Some(Utc::now());
}
pub fn fail(&mut self, reason: String) {
self.status = TaskStatus::Failed;
self.notes = Some(reason);
}
pub fn block(&mut self, reason: String) {
self.status = TaskStatus::Blocked(reason);
}
pub fn skip(&mut self, reason: Option<String>) {
self.status = TaskStatus::Skipped;
if let Some(r) = reason {
self.notes = Some(r);
}
}
pub fn complexity_stars(&self) -> String {
let filled = self.complexity.min(5);
let empty = 5 - filled;
"★".repeat(filled as usize) + &"☆".repeat(empty as usize)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum TaskType {
Research,
Edit,
Create,
Delete,
Test,
Refactor,
Documentation,
Configuration,
Build,
Other(String),
}
impl std::fmt::Display for TaskType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskType::Research => write!(f, "Research"),
TaskType::Edit => write!(f, "Edit"),
TaskType::Create => write!(f, "Create"),
TaskType::Delete => write!(f, "Delete"),
TaskType::Test => write!(f, "Test"),
TaskType::Refactor => write!(f, "Refactor"),
TaskType::Documentation => write!(f, "Documentation"),
TaskType::Configuration => write!(f, "Configuration"),
TaskType::Build => write!(f, "Build"),
TaskType::Other(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub enum TaskStatus {
#[default]
Pending,
InProgress,
Completed,
Skipped,
Failed,
Blocked(String),
}
impl std::fmt::Display for TaskStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TaskStatus::Pending => write!(f, "Pending"),
TaskStatus::InProgress => write!(f, "In Progress"),
TaskStatus::Completed => write!(f, "Completed"),
TaskStatus::Skipped => write!(f, "Skipped"),
TaskStatus::Failed => write!(f, "Failed"),
TaskStatus::Blocked(reason) => write!(f, "Blocked: {}", reason),
}
}
}
impl TaskStatus {
pub fn icon(&self) -> &str {
match self {
TaskStatus::Pending => "⏸️",
TaskStatus::InProgress => "▶️",
TaskStatus::Completed => "✅",
TaskStatus::Skipped => "⏭️",
TaskStatus::Failed => "❌",
TaskStatus::Blocked(_) => "🚫",
}
}
}