todo_read — querying the list

todo_read is the read side of the todo system: list tasks (with optional filters) or fetch one task by id. It never mutates anything:

pub fn todo_read(list: &TodoList, action: &TodoReadAction)
    -> Result<TodoReadOutput, PlanError>
use cosh_tools::plan::TodoReadAction;

// Everything.
todo_read(&list, &TodoReadAction::List { group: None, status: None })?;

// Only pending tasks in the "Database" group.
todo_read(&list, &TodoReadAction::List {
    group: Some("Database".into()),
    status: Some(TodoStatus::Pending),
})?;

// One task.
todo_read(&list, &TodoReadAction::Get { id: "task-3".into() })?;

The wrapper method is Plan::todo_read(&self, action) (note: &self — reads do not need a mutable borrow).


List — filtering

List returns a copy of every group, optionally narrowed:

Field Effect
group: Some(title) Only the group with that exact title is returned.
status: Some(s) Only tasks with that status are returned.

Filters compose: group + status returns one group containing only the matching tasks. A group that ends up with zero matching items is still included (as an empty group) — filtering narrows items, it does not hide groups.

Get — one task

Get returns a single TaskGroup containing only the requested task, so the shape of the output is identical to List — always groups[0].items[0] for the task. A nonexistent id is the one error on this page: "Task 'x' not found."


The verification nags

List (and only List) attaches nags from the verification contract: every group whose tasks are all terminal but whose tests_verified flag is still false produces

Group 'Database' is complete but tests have not been confirmed.
Use VerifyGroup to confirm tests passed.

This is the module’s gentle enforcement: the list is returned normally, the nag reminds the model to run tests and call VerifyGroup before declaring the group done. Once verified, the nag disappears. Get does not attach these nags (you asked for one task, not a status report).


Example

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

let mut list = TodoList::default();
list = todo_write(&list, &TodoWriteAction::Add {
    group: "Backend".into(),
    description: "Design schema".into(),
    depends_on: None,
})?.list;
list = todo_write(&list, &TodoWriteAction::Add {
    group: "Backend".into(),
    description: "Write migrations".into(),
    depends_on: None,
})?.list;

// All tasks are now terminal → List nags about the unverified group.
list = todo_cross_off(&list, &TodoCrossOff::Complete { id: "task-1".into() })?.list;
list = todo_cross_off(&list, &TodoCrossOff::Complete { id: "task-2".into() })?.list;
let out = todo_read(&list, &TodoReadAction::List { group: None, status: None })?;
assert!(out.nags.iter().any(|n| n.message.contains("tests have not been confirmed")));

// Verify the group → the nag is gone.
list = todo_write(&list, &TodoWriteAction::VerifyGroup { group: "Backend".into() })?.list;
let out = todo_read(&list, &TodoReadAction::List { group: None, status: None })?;
assert!(out.nags.is_empty());