todo_cross_off — completing and cancelling tasks

todo_cross_off moves a task into a terminal state — Completed or Cancelled — in one step:

pub fn todo_cross_off(list: &TodoList, action: &TodoCrossOff)
    -> Result<TodoWriteOutput, PlanError>
use cosh_tools::plan::TodoCrossOff;

// Mark a task done.
todo_cross_off(&list, &TodoCrossOff::Complete { id: "task-1".into() })?;

// Give up on a task that will not happen.
todo_cross_off(&list, &TodoCrossOff::Cancel { id: "task-2".into() })?;

The wrapper method is Plan::todo_cross_off(&mut self, action).


Terminal states are sticky

A task may only reach Completed or Cancelled once. Cross-off on a task that is already in a terminal state is rejected:

  • "Task 'task-1' is already completed."
  • "Task 'task-1' is already cancelled."

The same task cannot be cancelled after being completed (or vice versa). There is no “undo” in the todo system — un-completing a task is not a supported operation, by design: the terminal states mark decisions, and a mistake should be corrected by editing the task’s description or removing it with todo_write.

Any non-terminal task (Pending or InProgress) can be crossed off. Note that crossing off an InProgress task also frees the “one task at a time” slot for a new Start.


Dependents are warned

When a task is crossed off, every other task that lists it in depends_on — and is not itself terminal — gets a nag naming the new state:

'task-3' depends on 'task-1' which is now Completed.

The nag is advisory: the dependent task stays as it is. It is the model’s job to read the nag and decide whether the dependent still makes sense (perhaps cancel or re-plan it).


Errors

  • Nonexistent task id → "Task 'x' does not exist. Use List to see available tasks."
  • Task already terminal → the “already completed/cancelled” errors above.

Example

use cosh_tools::plan::{TodoList, TodoWriteAction, TodoCrossOff, todo_write, todo_cross_off};

let mut list = TodoList::default();
list = todo_write(&list, &TodoWriteAction::Add {
    group: "API".into(),
    description: "User endpoints".into(),
    depends_on: None,
})?.list;
list = todo_write(&list, &TodoWriteAction::Add {
    group: "API".into(),
    description: "Health check".into(),
    depends_on: Some(vec!["task-1".into()]),
})?.list;

// Completing task-1 nags about task-2, which depends on it.
let out = todo_cross_off(&list, &TodoCrossOff::Complete { id: "task-1".into() })?;
assert!(out.nags.iter().any(|n| n.message.contains("depends on 'task-1'")));

// Cross-off is one-way: completing task-1 again is an error.
assert!(todo_cross_off(&out.list, &TodoCrossOff::Complete { id: "task-1".into() }).is_err());