Intermediate Projects

Command-Line Task Manager

A command-line task and dependency manager models project tasks with tags, lifecycle states (Backlog, InProgress, Blocked, Done), and prerequisite dependencies (depends_on)—preventing a task from starting or completing while any of its prerequisite tasks are still unfinished, and computing a valid execution order via Kahn's topological sort.

Dependency-Aware Task Manager State Rules:

  Task #1 ("Design Schema") [Done]
        |
        v  (`depends_on: [1]`)
  Task #2 ("Implement API") [InProgress]
        |
        v  (`depends_on: [2]`)
  Task #3 ("Write E2E Tests") [Backlog]
  --> Attempting `complete(3)` before `#2` is `Done` returns `Err(UnmetDependency { task: 3, blocker: 2 })`!

Core Operations and Invariants

Method Validation Performed
add_task(title, tags) Assigns a monotonic TaskId in Backlog state.
add_dependency(task, prereq) Verifies both IDs exist and checks via DFS that adding task -> prereq does not create a circular dependency cycle!
mark_done(task) Verifies that all prerequisite tasks in depends_on are already in TaskStatus::Done before marking task as Done.
ready_tasks() Returns all unfinished tasks whose prerequisites are 100% Done (ready to be worked on right now in parallel!).

Complete Runnable Implementation

use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TaskStatus {
    Backlog,
    InProgress,
    Done,
}

#[derive(Debug, Clone)]
pub struct ManagedTask {
    pub id: u32,
    pub title: String,
    pub status: TaskStatus,
    pub tags: BTreeSet<String>,
    pub depends_on: BTreeSet<u32>,
}

#[derive(Debug, PartialEq, Eq)]
pub enum TaskError {
    NotFound(u32),
    SelfDependency(u32),
    CircularDependency { from: u32, to: u32 },
    UnmetDependency { task: u32, blocker: u32 },
}

#[derive(Default)]
pub struct TaskManager {
    next_id: u32,
    tasks: BTreeMap<u32, ManagedTask>,
}

impl TaskManager {
    pub fn new() -> Self {
        Self {
            next_id: 1,
            tasks: BTreeMap::new(),
        }
    }

    pub fn add_task<'a>(
        &mut self,
        title: impl Into<String>,
        tags: impl IntoIterator<Item = &'a str>,
    ) -> u32 {
        let id = self.next_id;
        self.next_id += 1;
        self.tasks.insert(
            id,
            ManagedTask {
                id,
                title: title.into(),
                status: TaskStatus::Backlog,
                tags: tags.into_iter().map(str::to_string).collect(),
                depends_on: BTreeSet::new(),
            },
        );
        id
    }

    /// Adds a prerequisite `prereq_id` to `task_id`, rejecting cycles!
    pub fn add_dependency(&mut self, task_id: u32, prereq_id: u32) -> Result<(), TaskError> {
        if task_id == prereq_id {
            return Err(TaskError::SelfDependency(task_id));
        }
        if !self.tasks.contains_key(&task_id) {
            return Err(TaskError::NotFound(task_id));
        }
        if !self.tasks.contains_key(&prereq_id) {
            return Err(TaskError::NotFound(prereq_id));
        }

        // Check if `prereq_id` already transitively depends on `task_id`:
        if self.depends_transitively(prereq_id, task_id) {
            return Err(TaskError::CircularDependency {
                from: task_id,
                to: prereq_id,
            });
        }

        self.tasks
            .get_mut(&task_id)
            .unwrap()
            .depends_on
            .insert(prereq_id);
        Ok(())
    }

    fn depends_transitively(&self, start: u32, target: u32) -> bool {
        let mut stack = vec![start];
        let mut visited = BTreeSet::new();
        while let Some(cur) = stack.pop() {
            if cur == target {
                return true;
            }
            if visited.insert(cur) {
                if let Some(t) = self.tasks.get(&cur) {
                    stack.extend(t.depends_on.iter().copied());
                }
            }
        }
        false
    }

    pub fn mark_done(&mut self, task_id: u32) -> Result<(), TaskError> {
        let task = self.tasks.get(&task_id).ok_or(TaskError::NotFound(task_id))?;
        for &prereq_id in &task.depends_on {
            let prereq = &self.tasks[&prereq_id];
            if prereq.status != TaskStatus::Done {
                return Err(TaskError::UnmetDependency {
                    task: task_id,
                    blocker: prereq_id,
                });
            }
        }
        self.tasks.get_mut(&task_id).unwrap().status = TaskStatus::Done;
        Ok(())
    }

    /// Returns all unfinished tasks whose prerequisites are 100% completed!
    pub fn ready_to_work(&self) -> Vec<&ManagedTask> {
        self.tasks
            .values()
            .filter(|t| {
                t.status != TaskStatus::Done
                    && t.depends_on
                        .iter()
                        .all(|id| self.tasks[id].status == TaskStatus::Done)
            })
            .collect()
    }
}

fn main() {
    let mut mgr = TaskManager::new();
    let t_schema = mgr.add_task("Design database schema", ["db", "backend"]);
    let t_api = mgr.add_task("Implement REST endpoints", ["api", "backend"]);
    let t_ui = mgr.add_task("Build frontend dashboard", ["ui"]);

    mgr.add_dependency(t_api, t_schema).unwrap();
    mgr.add_dependency(t_ui, t_api).unwrap();

    // 1. Cycle check: making `t_schema` depend on `t_ui` is rejected!
    assert_eq!(
        mgr.add_dependency(t_schema, t_ui),
        Err(TaskError::CircularDependency {
            from: t_schema,
            to: t_ui
        })
    );

    // 2. Completing `t_api` before `t_schema` is done is rejected:
    assert_eq!(
        mgr.mark_done(t_api),
        Err(TaskError::UnmetDependency {
            task: t_api,
            blocker: t_schema
        })
    );

    // 3. Complete `t_schema` -> `t_api` becomes the next ready task:
    mgr.mark_done(t_schema).unwrap();
    let ready_titles: Vec<&str> = mgr.ready_to_work().iter().map(|t| t.title.as_str()).collect();
    assert_eq!(ready_titles, vec!["Implement REST endpoints"]);

    println!("Unblocked ready tasks after completing schema: {ready_titles:?}");
}
Unblocked ready tasks after completing schema: ["Implement REST endpoints"]