use std::sync::Arc;
use anyhow::{Result, anyhow, ensure};
use chrono::{DateTime, Utc};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
const PLAN_UPDATE_PROGRESS: &str = "Plan updated. Continue working through TODOs.";
const PLAN_UPDATE_COMPLETE: &str = "Plan completed. All TODOs are done.";
const PLAN_UPDATE_CLEARED: &str = "Plan cleared. Start a new TODO list.";
const MAX_PLAN_STEPS: usize = 12;
const MIN_PLAN_STEPS: usize = 1;
const CHECKBOX_PENDING: &str = "[ ]";
const CHECKBOX_IN_PROGRESS: &str = "[ ]";
const CHECKBOX_COMPLETED: &str = "[x]";
const IN_PROGRESS_NOTE: &str = " _(in progress)_";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StepStatus {
Pending,
InProgress,
Completed,
}
impl StepStatus {
pub fn label(&self) -> &'static str {
match self {
StepStatus::Pending => "pending",
StepStatus::InProgress => "in_progress",
StepStatus::Completed => "completed",
}
}
pub fn checkbox(&self) -> &'static str {
match self {
StepStatus::Pending => CHECKBOX_PENDING,
StepStatus::InProgress => CHECKBOX_IN_PROGRESS,
StepStatus::Completed => CHECKBOX_COMPLETED,
}
}
pub fn status_note(&self) -> Option<&'static str> {
match self {
StepStatus::InProgress => Some(IN_PROGRESS_NOTE),
StepStatus::Pending | StepStatus::Completed => None,
}
}
pub fn is_complete(&self) -> bool {
matches!(self, StepStatus::Completed)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlanStep {
pub step: String,
pub status: StepStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum PlanCompletionState {
Empty,
InProgress,
Done,
}
impl PlanCompletionState {
pub fn label(&self) -> &'static str {
match self {
PlanCompletionState::Empty => "no_todos",
PlanCompletionState::InProgress => "todos_remaining",
PlanCompletionState::Done => "done",
}
}
pub fn description(&self) -> &'static str {
match self {
PlanCompletionState::Empty => "No TODOs recorded in the current plan.",
PlanCompletionState::InProgress => "TODOs remain in the current plan.",
PlanCompletionState::Done => "All TODOs have been completed.",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlanSummary {
pub total_steps: usize,
pub completed_steps: usize,
pub status: PlanCompletionState,
}
impl Default for PlanSummary {
fn default() -> Self {
Self {
total_steps: 0,
completed_steps: 0,
status: PlanCompletionState::Empty,
}
}
}
impl PlanSummary {
pub fn from_steps(steps: &[PlanStep]) -> Self {
if steps.is_empty() {
return Self::default();
}
let total_steps = steps.len();
let completed_steps = steps
.iter()
.filter(|step| step.status.is_complete())
.count();
let status = if completed_steps == total_steps {
PlanCompletionState::Done
} else {
PlanCompletionState::InProgress
};
Self {
total_steps,
completed_steps,
status,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskPlan {
#[serde(skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
pub steps: Vec<PlanStep>,
pub summary: PlanSummary,
pub version: u64,
pub updated_at: DateTime<Utc>,
}
impl Default for TaskPlan {
fn default() -> Self {
Self {
explanation: None,
steps: Vec::new(),
summary: PlanSummary::default(),
version: 0,
updated_at: Utc::now(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct UpdatePlanArgs {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
pub plan: Vec<PlanStep>,
}
#[derive(Debug, Clone, Serialize)]
pub struct PlanUpdateResult {
pub success: bool,
pub message: String,
pub plan: TaskPlan,
}
impl PlanUpdateResult {
pub fn success(plan: TaskPlan) -> Self {
let message = match plan.summary.status {
PlanCompletionState::Done => PLAN_UPDATE_COMPLETE.to_string(),
PlanCompletionState::InProgress => PLAN_UPDATE_PROGRESS.to_string(),
PlanCompletionState::Empty => PLAN_UPDATE_CLEARED.to_string(),
};
Self {
success: true,
message,
plan,
}
}
}
#[derive(Debug, Clone)]
pub struct PlanManager {
inner: Arc<RwLock<TaskPlan>>,
}
impl Default for PlanManager {
fn default() -> Self {
Self {
inner: Arc::new(RwLock::new(TaskPlan::default())),
}
}
}
impl PlanManager {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> TaskPlan {
self.inner.read().clone()
}
pub fn update_plan(&self, update: UpdatePlanArgs) -> Result<TaskPlan> {
validate_plan(&update)?;
let sanitized_explanation = update
.explanation
.as_ref()
.map(|text| text.trim().to_string())
.filter(|text| !text.is_empty());
let mut in_progress_count = 0usize;
let mut sanitized_steps: Vec<PlanStep> = Vec::with_capacity(update.plan.len());
for (index, mut step) in update.plan.into_iter().enumerate() {
let trimmed = step.step.trim();
if trimmed.is_empty() {
return Err(anyhow!("Plan step {} cannot be empty", index + 1));
}
if matches!(step.status, StepStatus::InProgress) {
in_progress_count += 1;
}
step.step = trimmed.to_string();
sanitized_steps.push(step);
}
ensure!(
in_progress_count <= 1,
"At most one plan step can be in_progress"
);
let mut guard = self.inner.write();
let version = guard.version.saturating_add(1);
let summary = PlanSummary::from_steps(&sanitized_steps);
let updated_plan = TaskPlan {
explanation: sanitized_explanation,
steps: sanitized_steps,
summary,
version,
updated_at: Utc::now(),
};
*guard = updated_plan.clone();
Ok(updated_plan)
}
}
fn validate_plan(update: &UpdatePlanArgs) -> Result<()> {
let step_count = update.plan.len();
ensure!(
step_count >= MIN_PLAN_STEPS,
"Plan must contain at least {} step(s)",
MIN_PLAN_STEPS
);
ensure!(
step_count <= MAX_PLAN_STEPS,
"Plan must not exceed {} steps",
MAX_PLAN_STEPS
);
for (index, step) in update.plan.iter().enumerate() {
ensure!(
!step.step.trim().is_empty(),
"Plan step {} cannot be empty",
index + 1
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn initializes_with_default_state() {
let manager = PlanManager::new();
let snapshot = manager.snapshot();
assert_eq!(snapshot.steps.len(), 0);
assert_eq!(snapshot.version, 0);
assert_eq!(snapshot.summary.status, PlanCompletionState::Empty);
assert_eq!(snapshot.summary.total_steps, 0);
}
#[test]
fn rejects_empty_plan() {
let manager = PlanManager::new();
let args = UpdatePlanArgs {
explanation: None,
plan: Vec::new(),
};
assert!(manager.update_plan(args).is_err());
}
#[test]
fn rejects_multiple_in_progress_steps() {
let manager = PlanManager::new();
let args = UpdatePlanArgs {
explanation: None,
plan: vec![
PlanStep {
step: "Step one".to_string(),
status: StepStatus::InProgress,
},
PlanStep {
step: "Step two".to_string(),
status: StepStatus::InProgress,
},
],
};
assert!(manager.update_plan(args).is_err());
}
#[test]
fn updates_plan_successfully() {
let manager = PlanManager::new();
let args = UpdatePlanArgs {
explanation: Some("Focus on API layer".to_string()),
plan: vec![
PlanStep {
step: "Audit handlers".to_string(),
status: StepStatus::Pending,
},
PlanStep {
step: "Add tests".to_string(),
status: StepStatus::Pending,
},
],
};
let result = manager.update_plan(args).expect("plan should update");
assert_eq!(result.steps.len(), 2);
assert_eq!(result.version, 1);
assert_eq!(result.steps[0].status, StepStatus::Pending);
assert_eq!(result.summary.total_steps, 2);
assert_eq!(result.summary.completed_steps, 0);
assert_eq!(result.summary.status, PlanCompletionState::InProgress);
}
#[test]
fn marks_plan_done_when_all_completed() {
let manager = PlanManager::new();
let args = UpdatePlanArgs {
explanation: None,
plan: vec![PlanStep {
step: "Finalize deployment".to_string(),
status: StepStatus::Completed,
}],
};
let result = manager.update_plan(args).expect("plan should update");
assert_eq!(result.summary.total_steps, 1);
assert_eq!(result.summary.completed_steps, 1);
assert_eq!(result.summary.status, PlanCompletionState::Done);
}
}