plan_from_md — parsing a Markdown plan file
plan_load_from_md turns a Markdown plan file into a TodoList,
so a plan can be written by hand (or generated), saved with fs_write, and
then executed through the todo tools:
pub fn plan_from_md(path: &str) -> Result<TodoList, PlanError>
The wrapper method is Plan::load_from_md(&mut self, path) — it parses the
file and replaces the internal list. The one error case is an unreadable
file: "Failed to read plan file: <io error>".
The PLAN_WRITE syntax
The canonical syntax lives in the crate constant plan::PLAN_WRITE. Its
rules are few:
# Plan: <title> ← any title line, ignored
## Group heading ← one per group of tasks
- [ ] Task description ← pending task
- depends: task-1, task-3 ← optional, on a sub-bullet under the task
- [x] Completed task ← completed task
## Another group
- [ ] More tasks
- Groups start with
##(level-2 heading). The text after##becomes the group title. Everything before the first##is ignored. - Tasks are flat checklist items:
- [ ]for pending,- [x]or- [X]for completed. - Dependencies go on a
- depends: a, bsub-bullet immediately after the task; the list is comma-separated and each id is trimmed. - Everything else is ignored: blockquotes, code fences, regular
paragraphs, deeper nesting, and the
# Plan:title line.
IDs are assigned sequentially in the order tasks appear, across all groups:
the first task is task-1, the second task-2, and so on. Completed items
parse with status Completed, everything else Pending; tests_verified
starts false for every group.
The parser’s exact rules
The implementation is deliberately line-based and forgiving — it reads the file line by line and only reacts to lines it recognizes:
| Line | Recognized as |
|---|---|
## <title> |
A new group (pushed, even if it ends up empty). |
- [ ] <desc> |
A pending task in the current group. |
- [x] <desc> / - [X] <desc> |
A completed task in the current group. |
- depends: <ids> |
The dependency list of the last task added. |
| anything else | Ignored. |
Subtleties worth knowing:
- The markers require the trailing space:
- [x]alone (no space) or- [x]Taskis not recognized. Use- [x] Task. - Task lines before any
##heading are dropped (there is no group to put them in yet). - A
- depends:line attaches to the last task of the current group, even if a blank line separates them; an empty id list (- depends:) clears the dependencies. - The title line (
# Plan: …) and any prose are fine anywhere — they are ignored, which is what makes the file readable as normal Markdown.
Example
A plan file, plan.md:
# Plan: Ship the API
## Database
- [ ] Design schema
- [ ] Write migrations
- depends: task-1
## API
- [x] Health check
Parsed:
use cosh_tools::plan::{TodoStatus, plan_from_md};
let list = plan_from_md("plan.md")?;
assert_eq!(list.groups.len(), 2);
assert_eq!(list.groups[0].title, "Database");
assert_eq!(list.groups[0].items[0].id, "task-1");
assert_eq!(list.groups[1].items[0].status, TodoStatus::Completed);
// The "Write migrations" task depends on "Design schema".
assert_eq!(list.groups[0].items[1].depends_on, vec!["task-1"]);
The typical loop is: fs_write the plan file → plan_load_from_md → read
the parsed list with todo_read to verify it → drive the
work with todo_write / todo_cross_off / todo_edit.