@@ -1,9 +1,20 @@
11# SPEC — a tree-shaped plain-text notes and task manager for the terminal
22
3−Status: draft 0.39 · 2026-09-26
3+Status: draft 0.40 · 2026-09-26
44Working name: not chosen yet. This document uses `notes` as the binary name; rename freely.
55Language: Rust · TUI: ratatui · Sync: Syncthing · History: file versioning on one node
66
7+Changes in 0.40: one spelling of task state. A block's state is the checkbox on its own
8+title line, like any node's; `todo:` is gone (read on legacy files, migrated on the next
9+toggle or `check --fix`), so `rg '\[ \]'` is the whole open-task list (§4.5, §8). Block
10+files keep their names when titles change; `check --fix` renames on request (§6.4). A block
11+file's later column-0 lines are adopted by its root instead of making it read-only (§4.9).
12+Verbs track nodes by key — titles plus ordinals among same-titled siblings — never by title
13+path (§3.4). Undo records exactly the files an operation touched and refuses if one changed
14+since (§10.11). Text after a child node is always preceded by a blank line (§4.2), and a
15+heading embed's level is checked against its position (§4.7). §3.1 states what the ordering
16+rule costs.
17+
718Changes in 0.39: a node's children are an ordered list of **text children** and **child
819nodes**, interleaved as they are in the file; the body is just the leading text (§3.1, §3.3).
920Children obey one ordering rule, `(text | item)* section*`, because Markdown cannot close a
@@ -237,7 +248,7 @@ Principles, in priority order:
237248| Section | A node spelled as an ATX heading `#`, `##`, … (no upper bound). |
238249| Item | A node spelled as a `- ` bullet. |
239250| Spelling | Whether a node is written as a heading or a bullet. Presentation only (§3.1). |
240−| Task | A non-block node with a checkbox after its marker, or a block with a `todo` key. |
251+| Task | A node with a checkbox after its marker — block or not. |
241252| Block | A node with its own identity and file. Only blocks have properties; every edit changes exactly one block. |
242253| Id | A random four-word phonemic name (`racfer-hattes-dozzod-binwes`) in a block's `id:` frontmatter; its stable identity. |
243254| Name | The slug of a block's title. With a prefix of the id in front, its filename. Decoration, not identity. |
@@ -292,6 +303,17 @@ Rules:
292303 before its new parent's first section child, and a section no earlier than just after its
293304 last item child. The *boundary* of a node's children is the position between its last
294305 item or text child and its first section child.
306+
307+ **What the rule costs.** A node cannot have items or text after a section child: under
308+ `# Trip` with a `## Budget` subsection, there is no way to put another bullet or paragraph
309+ back under Trip after Budget. Everything written there is Budget's. Plain Markdown cannot
310+ express it either, so the format accepts the limit rather than invent a closing syntax
311+ no other tool would read. The practical consequences are that `>`, `<`, paste, refile and
312+ capture sometimes place an item earlier than asked (before the first section), `J` / `K`
313+ refuse to move an item past a section, and `~` moves the node it respells. The TUI says
314+ so in the status line whenever the rule, not the user, chose the position; none of these
315+ is a bug. The workaround is the obvious one:
316+ put the later items in a section of their own (`## Also`), or keep sections last.
295317- **Text children are content, not outline.** They are not rows in the outline pane, not
296318 targets, never blocks or tasks, and are never moved on their own. A verb that moves,
297319 deletes or refiles a node moves exactly that node's own lines; the text children around it
@@ -307,8 +329,10 @@ Rules:
307329 moves to its parent's boundary, so its siblings keep their parent.
308330- Sibling order is block order and is meaningful.
309331- Any node may be a **block** (the root of its own file). Its file starts with the node
310− as it is spelled — a heading or a bullet — so the file is the only place its spelling
311− lives (§4.9).
332+ as it is spelled — a heading or a bullet — and that is where its spelling is defined
333+ (§4.9). The embed repeats it in its form (§4.7) so the parent file parses into the right
334+ shape on its own; the app keeps the two in step (`~` rewrites both), and where they
335+ disagree the file wins and `notes check` reports the embed.
312336
313337### 3.2 Properties
314338
@@ -332,7 +356,7 @@ Reserved keys (the app assigns semantics; users may still read/write them by han
332356| Key | Value | Meaning |
333357|---|---|---|
334358| `id` | four `@p` words | The block's identity (§3.4). Present on every managed file; never on `root.md`. |
335−| `todo` | `open` \| `done` | Makes the block a task and holds its state (§4.5). Absent means not a task. |
359+| `todo` | `open` \| `done` | **Legacy.** Before 0.40 it held a block's task state. Read only when the block's title line has no checkbox, reported by `notes check`, and removed on the next toggle or `check --fix`, which write the checkbox instead (§4.5). The app never writes it. |
336360| `due` | `YYYY-MM-DD` | Task due date; shown in the outline. |
337361| `done` | `YYYY-MM-DD` | Written when a task block is checked; removed when unchecked. |
338362| `conflict` | `<device> <timestamp>` | Marks a "theirs" copy written by the merge engine (§12.4). |
@@ -361,8 +385,8 @@ A text line belongs to the deepest open node whose region it reaches:
361385
362386So a line at column 0 after `- b` under `# Trip` is Trip's text, not b's: it ends the list.
363387(CommonMark would read an unindented line right after a list item with no blank line between
364−as a lazy continuation of that item; this format does not. Write a blank line before it and
365−both agree.)
388+as a lazy continuation of that item; this format does not. Canonical form therefore always
389+writes a blank line there (§4.2), and then both agree.)
366390
367391A blank line belongs to the text run it is in, or — between nodes — to the deepest open node
368392before it: the blank line after `- a` is `a`'s trailing separator. Blank lines are therefore
@@ -382,7 +406,19 @@ when the block is created and stored as the `id` key of its frontmatter. Its fil
382406directory uses as its prefix — usually one — then the slug of its title
383407(`racfer~order-new-switch.md`). The prefix alone identifies the file; the name is for humans. The filename is decoration that any tool may change and the
384408app will repair (§6.4). The one exception is `root.md`, which has no
385−id. A non-block node's identity is its **path**.
409+id. A non-block node has no stored identity. Two things stand in for one, and they are
410+different on purpose:
411+
412+- **Typed targets** (below) name a node by titles. Titles are what a person can type, and a
413+ title that matches twice is an ambiguity error, never a guess.
414+- **Tracking** — re-attaching the cursor, folds and a verb's result across a re-parse —
415+ uses the node's **key**: the block whose file holds it (none for `root.md`), then one step
416+ per level down from that file's root, each step the title *and the node's ordinal among
417+ same-titled siblings*. Two siblings with one title, or two empty new nodes, have
418+ different keys. Keys are never shown and never typed.
419+
420+Verbs never find a node again by its title path: a verb that re-parses finds its result by
421+key, by id, or by position.
386422
387423Embeds reference the id alone (`![[racfer-hattes-dozzod-binwes]]`), so nothing in the vault
388424depends on a filename and a rename touches only the renamed file.
@@ -399,7 +435,8 @@ Resolution order for a command target (the *refile*, *go to* and *make block* pr
399435Ambiguous or missing targets are an error for the command that named them; broken or
400436duplicate embeds are diagnostics in `notes check` (§15.7).
401437
402−Renaming a block's title renames its file (§6.4). Nothing else references the filename.
438+Renaming a block's title does not rename its file (§6.4); a stale name is harmless, since
439+nothing references the filename.
403440
404441Titles may not contain `/` (the path separator). `[` and `]` are discouraged.
405442
@@ -465,6 +502,8 @@ The app **reads** any CommonMark it can make sense of and **writes** this subset
465502 or bullets alike. `[X]` is accepted on read; `[-]` is read as `[x]`.
466503- Exactly one blank line between a node's body and its first child, and between sibling
467504 sections. Loose/tight lists are preserved as found.
505+- A blank line before any text child that follows a child node (§3.3), so that CommonMark
506+ readers agree the text is not part of that node.
468507
469508Canonicalization is **lazy**: a node is rewritten in canonical form only when it is touched
470509(edited, spliced, moved). `notes check --fix` (or *canonicalize* in the palette) rewrites
@@ -508,21 +547,31 @@ was designed for an external editor and is set aside with it, §17.)
508547
509548### 4.5 Tasks
510549
511−Any node can be a task. Where the state is written depends on whether the node is a file:
550+Any node can be a task, and there is one way to write it: a **checkbox after the marker on
551+its title line**. `- [ ]` and `## [ ]` are both open tasks; `[x]` is done. A block is no
552+different — its checkbox is on its own title line, in its file:
512553
513−- A node that is **not** a block is a task iff it has a checkbox after its marker.
514− `- [ ]` and `## [ ]` are both open tasks; `[x]` is done. That is all the state it has.
515−- A **block** is a task iff its frontmatter has `todo:`, whose value is the state:
516− `open` or `done`. Its title line in the file carries no checkbox. Checking writes
517− `todo: done` and `done: <date>`; reverting sets `todo: open` and removes the date.
554+```markdown
555+---
556+id: racfer-hattes-mislup-nodrys
557+due: 2026-09-20
558+---
559+
560+- [ ] Order new switch
561+```
562+
563+What a block adds is a completion record: checking it writes `done: <date>` to its
564+frontmatter, and unchecking removes the date. A plain task has no frontmatter and so no
565+date; that is the whole difference. Because the state is always on the title line,
566+`rg '\[ \]'` over the vault is the complete open-task list.
518567
519568There is no cancelled state. A task that will never be done is either checked off or
520569deleted.
521570
522−Embeds carry no state. Cutting a checkbox task turns the checkbox into `todo:` (§6.1). A
523−checkbox on a block's own title line is
524−accepted on read as `todo` and rewritten on the next touch. A task section has its own
525−state *and* the derived count of its subtree. See §8.
571+Embeds carry no state: the checkbox travels with the title line into the block's file when
572+it is made (§6.1). A legacy `todo:` key (§3.2) is read as the state when the title line has
573+no checkbox; if both are present the checkbox wins. A task section has its own state *and*
574+the derived count of its subtree. See §8.
526575
527576### 4.6 Links
528577
@@ -554,11 +603,19 @@ there, belong to that section (§3.1). Either form is read anywhere; an embed wh
554603does not match its block's spelling is a diagnostic (§15.7) that `notes check --fix`
555604rewrites, and it is placed where its form puts it.
556605
557−The embed names the block by id and nothing else; title, checkbox or `todo`, properties,
558−text and children all live in the file. The embed's form repeats the block's spelling so
559−that the parent file parses into the right shape without opening the block. An embed line
560−has no children in the parent file; lines indented under a bare embed, or nested under a
561−heading embed, are a diagnostic.
606+The embed names the block by id and nothing else; title, checkbox, properties, text and
607+children all live in the file. The embed's form repeats the block's spelling so that the
608+parent file parses into the right shape without opening the block. An embed line has no
609+children in the parent file; lines indented under a bare embed, or nested under a heading
610+embed, are a diagnostic.
611+
612+A heading embed's **level** is read like any heading's: honoured as written, and it
613+decides the embed's parent the same way. The level its position gives is `1 +` its section
614+ancestors (§3.1), and that is the level the app writes. A written level deeper than that —
615+left by another editor, or by moving text around by hand — still parses under the same
616+parent (a shallower one would parse elsewhere and is simply a different position), so
617+`notes check` reports it and `check --fix` rewrites it to the positional level; the
618+structure does not change.
562619
563620`![[` anywhere else is body text. The app does not implement general transclusion.
564621
@@ -572,7 +629,7 @@ for a later query language (§17); leave such blocks alone and they will start w
572629A block's file is exactly `render(node, 1, resolve_blocks = false)`:
573630
574631```
575−[frontmatter, always beginning with id; todo: if the block is a task]
632+[frontmatter, always beginning with id; done: if the block is a checked task]
576633[the node's title line, as spelled: "# Title" or "- Title"]
577634[its children: text and nodes, in order]
578635```
@@ -580,11 +637,18 @@ A block's file is exactly `render(node, 1, resolve_blocks = false)`:
580637- The root is written as it is spelled in the tree. A block that is a heading is a `#` at column 0; one
581638 that is a bullet is a `- ` at column 0, with its body and children indented under it exactly
582639 as they would be in the parent. The file is the only place the spelling is recorded.
583−- The block's task state is `todo:` in its frontmatter, with a `done:` date beside it
584− when done; there is no checkbox in the file. Toggling the node — from the outline,
585− or the embed — writes to the file.
586−- A file with text before the root other than frontmatter, or with more than one node at
587− column 0, is a diagnostic; the app treats the file as read-only until fixed.
640+- The block's task state is the checkbox on its title line, with a `done:` date in the
641+ frontmatter when it is checked. Toggling the node — from the outline, or the embed —
642+ writes to the file.
643+- The first node at column 0 is the block's root. **Anything after it at column 0 — more
644+ nodes, or text — is adopted as the root's children**, in order, as if it were indented
645+ under it. That is what a phone editor produces by appending `- new thing` to a block
646+ spelled as a bullet, and it must never make the block unusable. Adopted lines are
647+ non-canonical (`notes check` notes them); the next write of the block, or `check --fix`,
648+ writes them in place under the root (indented under a bullet root, re-levelled under a
649+ heading root).
650+- Text before the root other than frontmatter, or an embed before it, is a diagnostic; the
651+ app treats that file as read-only until fixed, since it cannot tell what the block is.
588652- `root.md` is the file of the implicit Root node; its frontmatter holds vault-level
589653 properties (rarely needed) and never an `id`.
590654
@@ -640,19 +704,18 @@ Mirrored pairs, no raidz. Snapshots hourly via sanoid.
640704```markdown
641705---
642706id: racfer-hattes-mislup-nodrys
643−todo: open
644707due: 2026-09-20
645708---
646709
647−- Order new switch
710+- [ ] Order new switch
648711 Two options, noted under Networking.
649712```
650713
651714Note that "Replace the flaky switch" is a one-line task with no file; "ZFS layout" is a
652715section block, so its embed is a heading at the level it has under NAS, while "Order new
653716switch" is an item block and is embedded bare. "Order new switch" became a block the moment
654−it got a due date, its checkbox became `todo: open`, and its
655−file starts with a bullet because that is what it is. "Options" is
717+it got a due date; its checkbox moved into the file with its title line, and its file starts
718+with a bullet because that is what it is. "Options" is
656719a section nested under an item, and "Snapshot policy" is a section that is itself a task.
657720
658721---
@@ -725,7 +788,7 @@ each block is written by its own splice, and the law holds per block.
725788Edge cases follow from the tags, not from rules:
726789
727790- Renaming a nested block is editing its title line; it is that block's line, so its file
728− is rewritten and renamed (§6.4). No second step.
791+ is rewritten (and keeps its name, §6.4). No second step.
729792- Deleting a nested block's title line deletes the block: the file goes to trash, and any
730793 lines it still owned are re-tagged to the enclosing block — they become plain text of the
731794 parent, which is what deleting a heading does to its content in any editor. This is the
@@ -762,7 +825,7 @@ demotes anything. The level-overflow rule of earlier drafts is gone.
7628253. Replace the node's span in its parent file with an embed at the node's position: a
763826 heading embed at its level for a section, a bare `![[id]]` at its indent for an item
764827 (§4.7). The text children around the node stay in the parent. A checkbox on the node
765− becomes `todo: <state>` in the frontmatter (§4.5).
828+ stays on its title line, now in the block's file (§4.5).
7668294. Update the index.
767830
768831There is no inverse. A block stays a file until it is deleted (`d`, *clear done*, or
@@ -777,7 +840,8 @@ or on `s` / *make block*. There is no other way.
777840
778841- Every block is embedded from **exactly one** embed. A second embed for the same id is
779842 a diagnostic and renders as broken. Cycles are a diagnostic.
780−- A block file has exactly one node at column 0 (§4.9) and its frontmatter has a valid
843+- A block file has one root, its first node at column 0; later column-0 lines are adopted
844+ under it (§4.9). Its frontmatter has a valid
781845 `id`.
782846- Blocks may be nested: a block's file may itself contain embeds.
783847- Embed lines have no children in the parent file.
@@ -834,11 +898,14 @@ the same way (§12.2). The filename carries no identity: a file is parsed becaus
834898check` asks for the prefix) while `racfer~order-new-switch.md` without one is ignored.
835899Safe on APFS, ext4, Android storage and Windows; case-insensitive-safe because lowercase.
836900
837−Renaming a block's title renames the file to the new name (temp-and-rename). Nothing
838−else in the vault references the filename, so nothing else is rewritten. A file renamed or
839−titled by another tool is still found by its id; `notes check` reports filenames whose
840−prefix is not a leading run of the id or whose name no longer matches the title, and
841−`notes check --fix` fixes them.
901+**Names are set once.** The name is the slug of the title when the block is made, and a
902+later title change does not rename the file. Under Syncthing a rename arrives on other
903+devices as a delete plus a create; a phone that edits the old name meanwhile turns that
904+into a sync conflict or an orphan, for nothing — the prefix alone identifies the file and
905+nothing references the filename. A file renamed or titled by another tool is still found by
906+its id. `notes check` reports filenames whose prefix is not a leading run of the id (a
907+problem) and names that no longer match the title (stale, harmless); `notes check --fix`
908+renames both, which is the one moment the app renames a file.
842909
843910### 6.5 Refile
844911
@@ -891,7 +958,7 @@ delete it. Nothing in the archive is hidden or dimmed.
891958| Node | open | done |
892959|---|---|---|
893960| not a block | `- [ ] Title` / `## [ ] Title` | `[x]` |
894−| block | `todo: open` | `todo: done` + `done: <date>` |
961+| block | `- [ ] Title` / `## [ ] Title` | `[x]` + `done: <date>` in frontmatter |
895962
896963Any node can be a task. A checkbox task has exactly this much state and nothing else. Its
897964completion date is not recorded; if you need one, make the task a block so toggling
@@ -900,9 +967,9 @@ writes `done:` (§8.2).
900967### 8.2 Task blocks
901968
902969A task that needs a date, a property, a completion record, or a body with structure becomes
903−a block (§6.1). Its checkbox becomes `todo:` in the frontmatter; the embed in the parent
904−is `![[id]]` and the title line in the file is plain. Toggling from anywhere
905−(outline or embed) rewrites `todo:` and stamps or removes `done:`.
970+a block (§6.1). Its checkbox moves into the block's file with its title line; the embed in
971+the parent is `![[id]]`. Toggling from anywhere (outline or embed) rewrites that checkbox and
972+stamps or removes `done:`.
906973The reading pane renders a resolved task embed on one line:
907974
908975```
@@ -1009,8 +1076,8 @@ aside (§17, open decision 16); the ```` ```query ```` fence is reserved for it
10091076| `n` / `N` | new sibling after cursor / new last child, spelled like the cursor node / like the last child node (a section if the parent has section children): inserts an empty node and opens it with `e` |
10101077| `e` | edit the subtree's Markdown in the built-in editor; saves as you go (§10.6) |
10111078| `a` | property editor: a form over the node's properties (§10.6); first property on a plain node makes it a block |
1012−| `x` | toggle task open / done (checkbox, or `todo:` plus `done:` on a block) |
1013−| `t` | toggle task-ness: adds or removes the checkbox, or the `todo` key on a block |
1079+| `x` | toggle task open / done: the checkbox, plus `done:` on a block |
1080+| `t` | toggle task-ness: adds or removes the checkbox (and a block's `done:`) |
10141081| `s` | make the node a block |
10151082| `y` / `d` | yank / delete subtree into the register (delete goes to trash too) |
10161083| `p` / `P` | paste register after / before cursor as sibling, clamped by the ordering rule (§3.1) |
@@ -1114,8 +1181,15 @@ Every mutation is an operation with an inverse (span edits, file create/move/del
11141181including making blocks). The session op log powers `u` / `U`. Outline verbs write
11151182immediately and atomically; the built-in editor writes each dirty block within 750 ms of the
11161183last keystroke, or sooner when the cursor leaves it (§10.6), so the only unsaved text at any
1117−moment is under a second of typing in one block. Undo after an external change re-checks
1118−span hashes and refuses with a message if the target moved.
1184+moment is under a second of typing in one block.
1185+
1186+An op-log entry records **exactly the files the operation touched**: for each, its text
1187+before and after, where *absent* stands for a file the operation created or deleted (making
1188+a block, trashing one). An operation that changed nothing leaves no entry, so it does not
1189+clear the redo stack. Undo checks every touched file against its *after* text, and redo
1190+against its *before* text; if any differs — another editor or a sync changed it since — it
1191+refuses with a message naming the file, writes nothing, and keeps the entry. Files the
1192+operation did not touch are never written by undo, so an external edit to them survives.
11191193
11201194---
11211195
@@ -1138,7 +1212,7 @@ span hashes and refuses with a message if the target moved.
11381212§11.4.
11391213
11401214- A changed file that the user is not editing is re-parsed; the cursor is re-attached by
1141− id, then path, then nearest surviving ancestor.
1215+ id, then key (§3.4), then the deepest step of the key that still exists.
11421216- A changed file with a built-in edit in progress: the editor saves its dirty blocks first;
11431217 a block whose span hash no longer matches is merged two-way (§5.2 step 5), then the file
11441218 is re-parsed and the buffer re-rendered around the cursor.
@@ -1216,7 +1290,7 @@ never deletes, so this is the safe failure; delete the copy you don't want.
12161290
12171291**Per matched node**, for each of title, checkbox state, text (all its text children, with
12181292their positions among its child nodes, as one field), and — for the file's block — each
1219−frontmatter key independently (`todo` included): `O == T → take O`, otherwise **conflict**. A conflict pair is raised for every field that differs, even when
1293+frontmatter key independently: `O == T → take O`, otherwise **conflict**. A conflict pair is raised for every field that differs, even when
12201294only one side touched it; the cost of having no per-device state is paid here, in
12211295resolution clicks, never in lost text.
12221296
@@ -1324,7 +1398,7 @@ pub enum TaskState { Open, Done }
13241398pub struct Node {
13251399 kind: Kind,
13261400 title: String,
1327− task: Option<TaskState>, // any node; from the checkbox, or `todo` for a block
1401+ task: Option<TaskState>, // any node, block or not: the checkbox on its title line
13281402 content: Vec<Content>, // ordered children: text runs and nodes (§3.1)
13291403 parent: Option<NodeId>,
13301404 file: FileId,
@@ -1334,7 +1408,7 @@ pub struct Node {
13341408
13351409pub struct Block {
13361410 id: Option<Id>, // None only for root.md
1337− name: String, // slug of the title at last rename
1411+ name: String, // slug of the title when made (or at the last `check --fix`)
13381412 path: RelPath, // e.g. "racfer~order-new-switch.md"
13391413 props: IndexMap<String, String>, // top-level `key: value` lines, order-preserving; includes `id`
13401414 frontmatter_raw: String, // verbatim, for lossless rewrite
@@ -1347,6 +1421,11 @@ pub struct Span { start: usize, end: usize }
13471421
13481422pub struct Tree { nodes: Vec<Node>, root: NodeId, files: Vec<FileState> }
13491423pub struct Cursor { path: Vec<NodeId> } // zipper-style focus; all outline verbs are cursor ops
1424+pub enum NodeKey { // survives re-parses (§3.4)
1425+ Root,
1426+ Id(Id),
1427+ Path { block: Option<Id>, steps: Vec<(String, usize)> }, // title + ordinal among same-titled siblings
1428+}
13501429```
13511430
13521431Levels and indents are computed from ancestry, never stored.
@@ -1409,7 +1488,9 @@ of real-world Markdown (Obsidian, Logseq, FSNotes exports) that must parse witho
14091488
14101489`notes check` reports, with `ariadne`-rendered source spans: non-canonical syntax, empty
14111490titles, titles containing `/`, broken, duplicate or cyclic embeds, embeds with children in the
1412−parent file, embeds whose form (bare or heading) does not match their block's spelling, malformed block files, invalid or duplicate `id` keys, ignored `.md` files,
1491+parent file, embeds whose form (bare or heading) does not match their block's spelling,
1492+heading embeds whose level is not the level of their position, text directly after a child
1493+node with no blank line, column-0 lines adopted by a block's root, malformed block files, invalid or duplicate `id` keys, ignored `.md` files,
14131494filenames whose prefix or name no longer match their id or title, frontmatter outside
14141495byte 0, `due` or `done` values that are not ISO dates, and unresolved conflict blocks.
14151496The TUI
@@ -1424,8 +1505,8 @@ gets wrong. What other tools see:
14241505
14251506| Tool | What it sees | Notes |
14261507|---|---|---|
1427−| `rg`, `fd`, Helix | plain text | `rg '\[ \]'` and `rg '^todo: open'` together are the open task list; `rg '^done: 2026-09'` the blocks finished this month; `rg '^due:'` the dated ones |
1428−| Any phone editor | plain text | capture by appending a `- ` line to the inbox block's file; the app adds nothing to it |
1508+| `rg`, `fd`, Helix | plain text | `rg '\[ \]'` is the open task list; `rg '^done: 2026-09'` the blocks finished this month; `rg '^due:'` the dated ones |
1509+| Any phone editor | plain text | capture by appending a `- ` line to the inbox block's file, at any indent: a column-0 line is adopted by the block's root (§4.9); the app adds nothing to it |
14291510| Obsidian, Logseq, FSNotes | mostly readable, structurally wrong | frontmatter as properties and `- [ ]` bullets render; `![[id]]` shows as a broken embed (a heading embed as a heading holding one), `## [ ]` as a literal heading, `#######` as a paragraph, indented headings as code. Fine for reading a file; do not edit structure there |
14301511
14311512---
@@ -1451,7 +1532,7 @@ task state; wiki-links between nodes and backlinks
14511532| M0 | `notes-core`: parser incl. frontmatter, tree, render/splice, canonicalize, ids and names | round-trip laws pass under proptest; `notes check` works |
14521533| M1 | TUI: outline + reading panes, zoom, fold, move, promote/demote, refile, filter box, subtree editing (`e`), undo | daily-drivable on a single `root.md` |
14531534| M2 | Blocks and properties: `s`, property-triggered blocks, task blocks | a task can be given a due date in the editor and its file round-trips |
1454−| M3 | Tasks: toggling on any node, `todo:` and `done:` stamping, dates, clear-done, filter box | replaces a TaskPaper file |
1535+| M3 | Tasks: toggling on any node, `done:` stamping, dates, clear-done, filter box | replaces a TaskPaper file |
14551536| M4 | Inbox and capture; watcher and reload | safe to edit in Helix and the TUI at once |
14561537| M5 | Sync: conflict detection, two-way node-level merge, `conflict:` blocks, conflict view, `notes merge` | phone + laptop + homelab over Syncthing for two weeks without data loss |
14571538| M6 | CLI polish, diagnostics, docs | 1.0 |
@@ -1464,10 +1545,9 @@ task state; wiki-links between nodes and backlinks
146415452. **Dated inbox, ISO titles, fixed.** Capture creates `## YYYY-MM-DD` under `Inbox`. There
14651546 is no format choice and no way to turn it off except `--to`. Days append oldest-first; a
14661547 long-lived inbox is a long section, which is what `s` (make block) and *clear done* are for.
1467−3. **Filenames follow titles, always.** A title change is a file rename (Syncthing handles
1468− renames by block reuse, so cheap but visible) and nothing else, since the id lives in the
1469− file. The id prefix stays in front through renames, so it is the one part of a filename
1470− that never moves.
1548+3. **Filenames are named once.** A title change does not rename the file: renames sync as
1549+ delete-plus-create and race with edits on other devices. The id prefix identifies the
1550+ file; the name is a hint that `notes check --fix` refreshes on request.
147115513b. **Id format.** Four-word `@p` names (64 bits). Two words (32 bits) were enough for
14721552 identity but left the one-word filename prefix with only 16 bits of room to grow; four
14731553 words make the id collision-free in practice and give the prefix three words of slack.
@@ -1475,10 +1555,10 @@ task state; wiki-links between nodes and backlinks
14751555 syllable tables; nobody is expected to type all four.
147615564. **Task sections.** Any node can be a task, so a project heading can be checked off. The
14771557 derived count is still shown beside it; there is no roll-up in either direction (§8.3).
1478−4b. **Two spellings of task state.** A checkbox for nodes that have no file, `todo:` for
1479− nodes that do. One mechanism was considered twice: all-frontmatter would make every task
1480− a file, all-checkbox puts structure on a title line the frontmatter already describes.
1481− The split costs one conversion when a block is made and nothing else.
1558+4b. **One spelling of task state.** The checkbox, for every node. Until 0.39 blocks used
1559+ `todo:` in frontmatter instead; that hid block tasks from `rg '\[ \]'` and cost a
1560+ conversion every time a task became a block. All-frontmatter was never an option (it
1561+ would make every task a file). A block keeps one thing a plain task lacks: `done:`.
148215625. **Two task states.** Open and done. Cancelled was removed: it was a third value in every
14831563 table, a second date key, and a key binding, for a distinction between "done" and
14841564 "won't do" that a deleted line or a done line expresses well enough. `[-]` is read as