on master help sign in
Rename project from "notes" to "fold" (#4)

Co-authored-by: Claude <noreply@anthropic.com>

commit2de42a9280fb4714f92c925cf4f5726ca189251c authoriko <ilyakooo0@gmail.com> dateFri 2 Oct 2026, 04:20 (1 month ago) parentc3c872f
1 file changed+43−43
SPEC.md+43−43
SPEC.md+43−43
@@ -1,7 +1,7 @@
11# SPEC — a tree-shaped plain-text notes and task manager for the terminal
22
33Status: draft 0.40 · 2026-09-26
4−Working name: not chosen yet. This document uses `notes` as the binary name; rename freely.
4+Name: fold. The binary is `fold`; the crates are `fold-*` (§15.1).
55Language: Rust · TUI: ratatui · Sync: Syncthing · History: file versioning on one node
66
77---
@@ -135,7 +135,7 @@ Rules:
135135 as it is spelled — a heading or a bullet — and that is where its spelling is defined
136136 (§4.9). The embed repeats it in its form (§4.7) so the parent file parses into the right
137137 shape on its own; the app keeps the two in step (`~` rewrites both), and where they
138− disagree the file wins and `notes check` reports the embed.
138+ disagree the file wins and `fold check` reports the embed.
139139
140140### 3.2 Properties
141141
@@ -235,7 +235,7 @@ Resolution order for a command target (the *refile*, *go to* and *make block* pr
235235 (`2026-09-10`) resolve this way; there is nothing special about them.
236236
237237Ambiguous or missing targets are an error for the command that named them; broken or
238−duplicate embeds are diagnostics in `notes check` (§15.7).
238+duplicate embeds are diagnostics in `fold check` (§15.7).
239239
240240Renaming a block's title does not rename its file (§6.4); a stale name is harmless, since
241241nothing references the filename.
@@ -266,7 +266,7 @@ The vault is flat: every block sits beside `root.md`. Only `root.md` and `.md` f
266266whose frontmatter carries a valid `id` (§6.4) are parsed. Everything else — subdirectories,
267267non-Markdown files, `.md` files without an id — is ignored: never parsed, never written,
268268never deleted. There is no import: to bring a foreign `.md` file in, paste its text into
269−a node (or `notes capture < file.md`) and let the app assign ids as it makes blocks. `notes check`
269+a node (or `fold capture < file.md`) and let the app assign ids as it makes blocks. `fold check`
270270lists ignored `.md` files so nothing is forgotten in the vault.
271271
272272There is no configuration and no configuration file. The inbox is the section titled
@@ -309,7 +309,7 @@ The app **reads** any CommonMark it can make sense of and **writes** this subset
309309 readers agree the text is not part of that node.
310310
311311Canonicalization is **lazy**: a node is rewritten in canonical form only when it is touched
312−(edited, spliced, moved). `notes check --fix` (or *canonicalize* in the palette) rewrites
312+(edited, spliced, moved). `fold check --fix` (or *canonicalize* in the palette) rewrites
313313everything at once.
314314
315315**What is ours.** A standard Markdown renderer will mishandle exactly these four things:
@@ -328,7 +328,7 @@ title := any text not starting with whitespace; may not contain "/"
328328
329329There is no attribute syntax. An `@word` or `key:: value` in a title or body is text, and
330330so is `[[…]]` (reserved, §4.6).
331−A title line with an empty title is invalid; `notes check` reports it, the TUI won't create it.
331+A title line with an empty title is invalid; `fold check` reports it, the TUI won't create it.
332332
333333### 4.4 Frontmatter
334334
@@ -401,7 +401,7 @@ The app writes the form that matches the block's spelling: a block whose root is
401401is embedded with a heading embed, a block whose root is an item with a bare one. The
402402heading form exists because a bare embed after a section sibling would, like any line
403403there, belong to that section (§3.1). Either form is read anywhere; an embed whose form
404−does not match its block's spelling is a diagnostic (§15.7) that `notes check --fix`
404+does not match its block's spelling is a diagnostic (§15.7) that `fold check --fix`
405405rewrites, and it is placed where its form puts it.
406406
407407The embed names the block by id and nothing else; title, checkbox, properties, text and
@@ -415,7 +415,7 @@ decides the embed's parent the same way. The level its position gives is `1 +` i
415415ancestors (§3.1), and that is the level the app writes. A written level deeper than that —
416416left by another editor, or by moving text around by hand — still parses under the same
417417parent (a shallower one would parse elsewhere and is simply a different position), so
418−`notes check` reports it and `check --fix` rewrites it to the positional level; the
418+`fold check` reports it and `check --fix` rewrites it to the positional level; the
419419structure does not change.
420420
421421`![[` anywhere else is body text. The app does not implement general transclusion.
@@ -445,7 +445,7 @@ A block's file is exactly `render(node, 1, resolve_blocks = false)`:
445445 nodes, or text — is adopted as the root's children**, in order, as if it were indented
446446 under it. That is what a phone editor produces by appending `- new thing` to a block
447447 spelled as a bullet, and it must never make the block unusable. Adopted lines are
448− non-canonical (`notes check` notes them); the next write of the block, or `check --fix`,
448+ non-canonical (`fold check` notes them); the next write of the block, or `check --fix`,
449449 writes them in place under the root (indented under a bullet root, re-levelled under a
450450 heading root).
451451- Text before the root other than frontmatter, or an embed before it, is a diagnostic; the
@@ -669,7 +669,7 @@ syllables (vowels `e u y`) — each word `prefix suffix`, words joined by `-`:
669669`racfer-hattes-dozzod-binwes`. No `~` sigil and no scrambling step, since the bits are random
670670rather than sequential. Never derived from content or time. Stored as the `id` frontmatter
671671key and validated against the syllable tables on read; a file whose `id` does not validate
672−is ignored, and `notes check` says why.
672+is ignored, and `fold check` says why.
673673
674674Collisions: 2⁶⁴ values, so two ids minted independently are never expected to collide; the
675675index still checks and redraws locally, and the merge engine still reports an id collision
@@ -696,7 +696,7 @@ splits without knowing how many words the prefix has. Because the prefix alone i
696696can never collide with anything, and `racfer~` already tells you which file you are looking
697697at before you read the rest. Cross-device collisions are found at merge time and resolved
698698the same way (§12.2). The filename carries no identity: a file is parsed because of its
699−`id` key, not its name, so `python-basics.md` with an `id:` is a block (and `notes
699+`id` key, not its name, so `python-basics.md` with an `id:` is a block (and `fold
700700check` asks for the prefix) while `racfer~order-new-switch.md` without one is ignored.
701701Safe on APFS, ext4, Android storage and Windows; case-insensitive-safe because lowercase.
702702
@@ -705,8 +705,8 @@ later title change does not rename the file. Under Syncthing a rename arrives on
705705devices as a delete plus a create; a phone that edits the old name meanwhile turns that
706706into a sync conflict or an orphan, for nothing — the prefix alone identifies the file and
707707nothing references the filename. A file renamed or titled by another tool is still found by
708−its id. `notes check` reports filenames whose prefix is not a leading run of the id (a
709−problem) and names that no longer match the title (stale, harmless); `notes check --fix`
708+its id. `fold check` reports filenames whose prefix is not a leading run of the id (a
709+problem) and names that no longer match the title (stale, harmless); `fold check --fix`
710710renames both, which is the one moment the app renames a file.
711711
712712### 6.5 Refile
@@ -735,8 +735,8 @@ delete it. Nothing in the archive is hidden or dimmed.
735735
736736- The inbox is the top-level section titled `Inbox` (case-insensitive). If none exists,
737737 capture creates `# Inbox` as the last top-level section of `root.md`. If several exist,
738− the first in `root.md` order wins and `notes check` reports the rest.
739−- **Capture** (`c` in the TUI, `notes capture`, a global hotkey via the CLI) appends an item
738+ the first in `root.md` order wins and `fold check` reports the rest.
739+- **Capture** (`c` in the TUI, `fold capture`, a global hotkey via the CLI) appends an item
740740 under today's day: the child section of `Inbox` titled with today's date, `YYYY-MM-DD`,
741741 created on demand as the last child. `--task` or a leading `[ ]` makes it a task.
742742 `--to <target>` captures under any node instead. A captured item is clamped like any
@@ -1232,7 +1232,7 @@ they stay apart, as undoing them as one would drop that change.
12321232
12331233### 11.1 Writes
12341234
1235−- **Atomic**: write to `.<name>.notes-tmp` in the same directory, `fsync`, `rename`.
1235+- **Atomic**: write to `.<name>.fold-tmp` in the same directory, `fsync`, `rename`.
12361236 Never truncate in place.
12371237- **Span-preserving**: an edit replaces exactly the bytes of the affected node(s) — for a
12381238 property change, exactly the frontmatter lines that changed. Every other byte is copied
@@ -1287,16 +1287,16 @@ during atomic writes.
12871287### 11.4 Ignore patterns
12881288
12891289Never parsed, never written, never deleted: dotfiles and dot-directories (`.git`, `.jj`,
1290−`.stfolder`, `.stversions`, `.stignore`, `.obsidian`), `.syncthing.*.tmp`, `*.notes-tmp`,
1290+`.stfolder`, `.stversions`, `.stignore`, `.obsidian`), `.syncthing.*.tmp`, `*.fold-tmp`,
12911291`*.tmp`, and any file not ending in `.md`. `*.sync-conflict-*.md` is parsed only by the merge
12921292engine.
12931293
12941294### 11.5 Trash
12951295
12961296Deleted subtrees and resolved `conflict:` blocks are written to
1297−`$XDG_STATE_HOME/notes/trash/<timestamp>-<id-or-name>.md` (device-local, never synced) before
1297+`$XDG_STATE_HOME/fold/trash/<timestamp>-<id-or-name>.md` (device-local, never synced) before
12981298removal; editor text no save could take, to `<timestamp>-unsaved-<title>.md` (§10.6).
1299−`notes trash list|restore` manages it. `restore` moves an entry back into the vault, but an
1299+`fold trash list|restore` manages it. `restore` moves an entry back into the vault, but an
13001300`unsaved-` entry has no id, so the vault would ignore it as a file (§6.4): it is printed
13011301instead, and stays in the trash. The content decides, not the name: a block file renamed
13021302`unsaved-….md` (§3.4) keeps its id and is moved back. The app never deletes user content
@@ -1312,11 +1312,11 @@ without a trash copy.
13121312 winner plus a `name.sync-conflict-<date>-<time>-<device>.md` copy of the loser.
13131313- Therefore: keep concurrently-edited things in different files where possible (a
13141314 `Inbox` as a block; blocks), and make the app the thing that understands conflict files.
1315−- The vault's `.stignore` should contain at minimum: `.git`, `.jj`, `*.notes-tmp`, `*.tmp`.
1315+- The vault's `.stignore` should contain at minimum: `.git`, `.jj`, `*.fold-tmp`, `*.tmp`.
13161316
13171317### 12.2 Detection
13181318
1319−The watcher (or `notes merge`, or the startup scan) finds `X.sync-conflict-*.md` next to
1319+The watcher (or `fold merge`, or the startup scan) finds `X.sync-conflict-*.md` next to
13201320`X.md`, as the files are then: an `X.md` that came in with its copy is merged into. Several
13211321conflict files for the same `X.md` are merged oldest first.
13221322
@@ -1404,16 +1404,16 @@ notices it.
14041404
14051405## 13. CLI
14061406
1407−All commands take `--vault PATH` (default: `$NOTES_VAULT`, else the nearest ancestor of
1408−`$PWD` containing `root.md`, else `~/notes`).
1407+All commands take `--vault PATH` (default: `$FOLD_VAULT`, else the nearest ancestor of
1408+`$PWD` containing `root.md`, else `~/fold`).
14091409
14101410| Command | Purpose |
14111411|---|---|
1412−| `notes [--keys normal\|vim\|helix]` | open the TUI; creates `root.md` if the directory is empty (§4.1.1); `--keys` picks the editor's keymap (§10.6) |
1413−| `notes capture [TEXT] [--to TARGET] [--task]` | append to the inbox (stdin if no TEXT) |
1414−| `notes check [--fix]` | diagnostics with source spans (§15.7); `--fix` rewrites the vault in canonical form (§4.2) and repairs filenames (§6.4) |
1415−| `notes merge [--dry-run]` | process sync-conflict files non-interactively; list leftovers |
1416−| `notes trash list \| restore ID` | trash management |
1412+| `fold [--keys normal\|vim\|helix]` | open the TUI; creates `root.md` if the directory is empty (§4.1.1); `--keys` picks the editor's keymap (§10.6) |
1413+| `fold capture [TEXT] [--to TARGET] [--task]` | append to the inbox (stdin if no TEXT) |
1414+| `fold check [--fix]` | diagnostics with source spans (§15.7); `--fix` rewrites the vault in canonical form (§4.2) and repairs filenames (§6.4) |
1415+| `fold merge [--dry-run]` | process sync-conflict files non-interactively; list leftovers |
1416+| `fold trash list \| restore ID` | trash management |
14171417
14181418That is the whole CLI: the entry points a shell, a hotkey, a cron job on the homelab, or a
14191419recovery session needs. Everything else is a TUI verb.
@@ -1433,9 +1433,9 @@ contain the same Markdown behave identically.
14331433There are no device settings either. The app reads no file outside the vault. What other
14341434tools keep in configuration is:
14351435
1436−- the vault: `--vault PATH`, else `$NOTES_VAULT`, else the nearest ancestor of `$PWD`
1437− containing `root.md`, else `~/notes`;
1438−- the trash location: `$XDG_STATE_HOME/notes/trash/` (§11.5);
1436+- the vault: `--vault PATH`, else `$FOLD_VAULT`, else the nearest ancestor of `$PWD`
1437+ containing `root.md`, else `~/fold`;
1438+- the trash location: `$XDG_STATE_HOME/fold/trash/` (§11.5);
14391439- the editor's keymap: `--keys`, else `$FOLD_KEYS` (`normal`, `vim`, `helix`), else
14401440 `normal` (§10.6);
14411441- everything visual: fixed (§10.1) or a session toggle (`zd`, `zr`).
@@ -1450,12 +1450,12 @@ The only thing the app keeps outside the vault is the trash.
14501450
14511451```
14521452crates/
1453− notes-core/ parsing, tree, render/splice, index, store, merge — no TUI deps
1454− notes-tui/ ratatui application
1455− notes-cli/ clap binary; depends on both
1453+ fold-core/ parsing, tree, render/splice, index, store, merge — no TUI deps
1454+ fold-tui/ ratatui application
1455+ fold-cli/ clap binary; depends on both
14561456```
14571457
1458−`notes-core` is the stable API a future Android client or Helix extension would use.
1458+`fold-core` is the stable API a future Android client or Helix extension would use.
14591459
14601460### 15.2 Core types (sketch)
14611461
@@ -1510,7 +1510,7 @@ exact spans, lossless round-tripping, and a small canonical subset — not a ful
15101510`pulldown-cmark` may be used later for inline styling in the reading pane only.
15111511
15121512Reading tolerance (setext headings, `*` bullets, tabs, `[X]`, `[-]`) is handled in the lexer and
1513−recorded per node as "non-canonical", which `notes check` reports and touching rewrites.
1513+recorded per node as "non-canonical", which `fold check` reports and touching rewrites.
15141514
15151515### 15.4 Dependencies
15161516
@@ -1519,7 +1519,7 @@ recorded per node as "non-canonical", which `notes check` reports and touching r
15191519(today's date), `clap`, `indexmap`, `nucleo` (fuzzy
15201520filter), `similar` (sequence alignment and diff3 for merge), `blake3`, `tempfile`,
15211521`unicode-normalization`, `unicode-width`, `directories` (XDG),
1522−`getrandom` (64-bit ids; the `@p` syllable tables are a 512-entry constant in `notes-core`),
1522+`getrandom` (64-bit ids; the `@p` syllable tables are a 512-entry constant in `fold-core`),
15231523`ariadne` (diagnostics), `proptest` + `insta` (tests).
15241524
15251525### 15.5 Performance targets
@@ -1556,7 +1556,7 @@ of real-world Markdown (Obsidian, Logseq, FSNotes exports) that must parse witho
15561556
15571557### 15.7 Diagnostics
15581558
1559−`notes check` reports, with `ariadne`-rendered source spans: non-canonical syntax, empty
1559+`fold check` reports, with `ariadne`-rendered source spans: non-canonical syntax, empty
15601560titles, titles containing `/`, broken, duplicate or cyclic embeds, embeds with children in the
15611561parent file, embeds whose form (bare or heading) does not match their block's spelling,
15621562heading embeds whose level is not the level of their position, text directly after a child
@@ -1597,25 +1597,25 @@ task state; wiki-links between nodes and backlinks
15971597
15981598| # | Deliverable | Exit criterion |
15991599|---|---|---|
1600−| M0 | `notes-core`: parser incl. frontmatter, tree, render/splice, canonicalize, ids and names | round-trip laws pass under proptest; `notes check` works |
1600+| M0 | `fold-core`: parser incl. frontmatter, tree, render/splice, canonicalize, ids and names | round-trip laws pass under proptest; `fold check` works |
16011601| M1 | TUI: outline + reading panes, zoom, fold, move, promote/demote, refile, filter box, subtree editing (`e`), undo | daily-drivable on a single `root.md` |
16021602| 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 |
16031603| M3 | Tasks: toggling on any node, `done:` stamping, dates, clear-done, filter box | replaces a TaskPaper file |
16041604| M4 | Inbox and capture; watcher and reload | safe to edit in Helix and the TUI at once |
1605−| 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 |
1605+| M5 | Sync: conflict detection, two-way node-level merge, `conflict:` blocks, conflict view, `fold merge` | phone + laptop + homelab over Syncthing for two weeks without data loss |
16061606| M6 | CLI polish, diagnostics, docs | 1.0 |
16071607
16081608---
16091609
16101610## 19. Decisions
16111611
1612−1. **Name.** Binary `notes` is a placeholder.
1612+1. **Name.** `fold`: the binary, the crate prefix and the state directory.
161316132. **Dated inbox, ISO titles, fixed.** Capture creates `## YYYY-MM-DD` under `Inbox`. There
16141614 is no format choice and no way to turn it off except `--to`. Days append oldest-first; a
16151615 long-lived inbox is a long section, which is what `s` (make block) and *clear done* are for.
161616163. **Filenames are named once.** A title change does not rename the file: renames sync as
16171617 delete-plus-create and race with edits on other devices. The id prefix identifies the
1618− file; the name is a hint that `notes check --fix` refreshes on request.
1618+ file; the name is a hint that `fold check --fix` refreshes on request.
161916194. **Id format.** Four-word `@p` names (64 bits): collision-free in practice, with three words
16201620 of slack for the filename prefix to grow into. Pronounceable, typeable from memory, and
16211621 self-validating against the syllable tables; nobody is expected to type all four.
@@ -1646,7 +1646,7 @@ task state; wiki-links between nodes and backlinks
1646164613. **Inbox by title.** Capture finds the inbox by the title `Inbox`. Renaming it means the
16471647 next capture creates a new one. A hidden marker property would be the only setting.
1648164814. **No config file.** Presentation choices are fixed or session toggles; a config format, a
1649− lookup order and a `notes config` command are not worth it. A real per-device need is
1649+ lookup order and a `fold config` command are not worth it. A real per-device need is
16501650 one environment variable: the editor's keymap is `$FOLD_KEYS` (§10.6), and a
16511651 colour-blind palette, say, would be another.
1652165215. **Windows.** Slugs and atomic writes are Windows-safe by design; nothing else is tested.