todo_edit — editing task metadata
todo_edit changes the metadata of an existing task: its description, its
group, and/or its dependency list. The edit is partial — every field on
TodoEdit is Option, and only the
fields you provide are touched:
pub fn todo_edit(list: &TodoList, edit: &TodoEdit)
-> Result<TodoWriteOutput, PlanError>
let edit = TodoEdit {
id: "task-1".into(),
description: Some("Design the schema (v2)".into()), // rewrite
group: Some("Backend".into()), // move to another group
depends_on: None, // leave unchanged
};
The wrapper method is Plan::todo_edit(&mut self, edit).
What each field does
| Field | Effect |
|---|---|
description: Some(text) |
Replaces the description. Empty/whitespace text is rejected ("Description must be non-empty text."). |
group: Some(title) |
Moves the task to that group, creating it if needed (new groups start tests_verified: false). A group equal to the current one is a no-op. |
depends_on: Some(ids) |
Replaces the whole dependency list. |
Fields left None are untouched — that is what makes TodoEdit safe to
reuse: one struct can describe a rename, a move, or both without needing to
know the task’s current values.
Dependency problems nag (and are still applied)
Like Add, an edited depends_on list is
stored as given, and problems only produce nags:
| Problem | Nag |
|---|---|
depends_on contains the task’s own id |
"Dependency 'x' is a self-reference." |
depends_on references a nonexistent task |
"Dependency 'x' does not exist in the task list." |
| The dependency would create a cycle | "Dependency 'x' would create a circular dependency chain." |
The cycle check is transitive: it walks the dependency graph from the new dependency to see whether the edited task is already reachable from it (A → B → C, then editing A to depend on C is caught). Despite the nag, the list is updated — the module warns, it does not police.
Errors
- Nonexistent task id →
"Task 'x' does not exist. Use List to see available tasks." - Empty new description →
"Description must be non-empty text."
Both reject the whole edit: nothing changes.
Example
use cosh_tools::plan::{TodoList, TodoWriteAction, TodoEdit, todo_write, todo_edit};
let mut list = TodoList::default();
list = todo_write(&list, &TodoWriteAction::Add {
group: "Database".into(),
description: "Design schema".into(),
depends_on: None,
})?.list;
// Rename the task and move it to the "Backend" group in one edit.
let out = todo_edit(&list, &TodoEdit {
id: "task-1".into(),
description: Some("Design schema (v2)".into()),
group: Some("Backend".into()),
depends_on: None,
})?;
list = out.list;
// A circular dependency nags — task-1 now depends on task-2, and task-2
// depends on task-1 — but the edit still lands.
let list2 = todo_write(&list, &TodoWriteAction::Add {
group: "Backend".into(),
description: "Migrations".into(),
depends_on: Some(vec!["task-1".into()]),
})?.list;
let out = todo_edit(&list2, &TodoEdit {
id: "task-1".into(),
description: None,
group: None,
depends_on: Some(vec!["task-2".into()]),
})?;
assert!(out.nags.iter().any(|n| n.message.contains("circular")));