Files
lanework/DESIGN/01-storage-format.md
T
rzen 94e795aa89 Renumber commits as the triggering event, not a standalone message
Resolves the 01-vs-06 contradiction: the pure-diff composer cannot emit
'Renumber lane X' (order-only bookkeeping composes nothing), and a
renumber never happens alone — it batches with the insert/move that
exhausted the midpoint, so the commit reads as that event. 06's
bookkeeping rule now says sequence-preserving order changes are the
bookkeeping; repositioning order changes still compose Reorder.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
2026-07-26 15:51:31 -04:00

17 KiB
Raw Blame History

Storage Format (Schema 1)

The on-disk contract between the filesystem (source of truth) and every consumer — the app, hand editors, and agents. This is the successor to the pathfinder's SCHEMA.md; once stable it graduates into a normative SCHEMA.md in the new repo. The schema number stays 1: the pathfinder never shipped, so this design redefines schema 1 rather than bumping it — there is no migration story and no compatibility obligation to pathfinder boards.

The schema splits in two. The core schema — everything specified in this document: boards, lanes, cards, attachments — is what this version implements. The enhanced schema — comments, tracker integration fields — is out of scope for this version; core only reserves its names (see Enhanced schema below) so nothing collides later.

Fractal layout

Every level is a folder containing an index.md. Identity is the folder name; everything else lives in the file.

MyBoard.kanban/                                  ← board = the document
├── index.md                                     ← board settings + description
├── 3f2a9c41-8b7e-4d2a-9c1f-5e6a7b8c9d0e/       ← lane folder
│   ├── index.md                                 ← lane metadata + description
│   ├── 27ca60f1-ef07-44d5-b688-73bf1f77e066/   ← card folder
│   │   ├── index.md                             ← card frontmatter + body
│   │   └── attachments/                         ← card attachments
│   │       └── sketch.png
│   └── 8d3b1f22-…/
│       └── index.md
└── b1e4d7a0-…/
    └── index.md

Rules (carried over from the pathfinder, proven)

  • Level is position. Root = board, depth 1 = lane, depth 2 = card. No type field.
  • Folder names are lowercase UUIDv4, immutable, never renamed. The folder name is the primary key; titles live in frontmatter only. Collisions are impossible by construction.
  • Identity lifecycle: moves keep the UUID, copies mint fresh ones. Every move (between lanes, between boards, cut+paste) is a physical folder move — identity travels with it. Every copy (⌘C/⌘V, ⌥-drag duplicate, the cross-board drag default — 04-interactions.md's locality model — and template instantiation) mints fresh UUIDs for every folder it materializes; a copy keeps created (it's a fork) but is a new identity. Exception: template instantiation stamps created/modified fresh — a board created from a template is born today, not forked from it (09-templates.md); Save as Template, by contrast, is an ordinary fork and keeps timestamps (inert anyway — instantiation restamps them). The app never produces a duplicate UUID: within-board uniqueness is enforced at the import boundary — a move-in (a cross-board ⌘-drag move or a cut+paste — 04-interactions.md) whose UUID already exists anywhere in the destination board is degraded to a copy (fresh UUID minted, content intact; the source is removed as usual for a move). Degradation is per-folder, at the finest grain: in a compound arrival (a lane with its cards, a multi-card drag or paste), only the colliding folders are reminted — everything else keeps its identity and remains a true move, and source removal is uniform regardless (the whole selection leaves the source, as for any move). A lane drag with one colliding nested card thus stays a lane move; just that card arrives reminted. One gesture silently yielding moved originals plus reminted copies is the intended behavior, not an accident — the collision is an identity repair, invisible in content terms. Boards are independent identity namespaces: the same UUID may legitimately exist in two boards (e.g. after a cross-board move-out undone at the source — 06-history-undo.md); such forks are tolerated and only ever meet at an import boundary, where the collision is neutralized. A user hand-copying a card folder in Finder can still produce sibling duplicates — the app doesn't currently detect that, and behavior with duplicate sibling-level UUIDs in one board is unspecified.
  • index.md = YAML frontmatter between --- delimiters, then Markdown body.
  • Reserved child names at card level: attachments/ (specified below) and comments/ (enhanced schema — see below). Never levels themselves, never identity (identity-bearing siblings are UUIDs).
  • A folder below the root lacking index.md is skipped with a log warning, not an error — an interrupted two-step create must not brick the board. Stray tolerance is uniform at every level: non-reserved files and folders at board, lane, or card level are ignored by the loader, preserved verbatim, never rendered (inside a card, only index.md and the reserved child names mean anything — anything else is not the app's business).
  • Atomic writes: temp file, rename over index.md.
  • Unknown frontmatter keys and their order are preserved verbatim on every rewrite. This is load-bearing for agent overlays (project:, sphere:, custom tags) — see 08-agent-integration.md.
  • The app never reformats a body it didn't change (byte-identical round-trip; plain string comparison, no re-serialization).

Frontmatter

Common to all levels

Field Type Required Notes
schema integer yes 1 for this design
title string no Optional at every level — the display name everywhere. Lanes/cards: a missing title shows the untitled placeholder. Boards: a missing title falls back to the folder name (see Board below)
created ISO-8601 no Set at creation, with timezone
modified ISO-8601 no Updated on every app write
modified-by string no Self-reported writer identity, set by external writers only; the app clears it on every write — see below
deleted ISO-8601 no Tombstone — see Deletion
background string no Palette name or #RRGGBB[AA] — see 03-board-ui.md
icon string no SF Symbol name, per-level defaults
iconColor string no Palette name or hex

modified-by — self-reported provenance (settled). An external writer (agent, script) may stamp a file it writes with a short free-form identity (modified-by: claude). The app never sets it and clears it on every app-mediated write — absence means "the board's user, via the app"; the file is being rewritten anyway, so clearing costs no extra write. Consequences compose from existing rules: copies arrive cleared (paste and duplicate are app writes), template instantiation strips it alongside its timestamp restamp (09-templates.md), Save as Template keeps it inertly. The key is schema-owned, not an unknown key — unknown-key preservation doesn't apply to it. Downstream surfaces: the card window's modified line renders it (05-card-window.md), and it refines foreign commit attribution (06-history-undo.md). Honest limit: it is self-reported — a hand edit that leaves an old stamp in place inherits its attribution until the next app write clears it. One carve-out: raw-source Apply (05-card-window.md) writes byte-for-byte and does not clear a stamp the user typed or kept — the outlet exists to write anything, and the validated-then-verbatim contract outranks the clearing rule.

Board (<root>/index.md)

Field Type Required Notes
template mapping no {order: N} — picker position when the board lives in a template store (09-templates.md); future subkeys possible. Carried onto instantiated boards, where it is ignored and preserved like any unknown key.

Body: board description (free Markdown).

Board naming: the schema is uniform at every level — folder name is storage identity, title is display. A board's title is what the window title and welcome recents show; when absent, the UI falls back to the folder name (sans extension). The in-app Board rename (03-board-ui.md) edits title only, never the folder — so the app's display name and the Finder document name can diverge (accepted; renaming the folder is a Finder operation). Template instantiation seeds the new board's title with the user-chosen document name, so the two start out matching (09-templates.md).

Lane (<root>/<guid>/index.md)

Field Type Required Notes
order number yes Rank among lanes, ascending = left-to-right
width integer no Width multiplier ≥ 1 (default 1); no cap

Body: lane description / WIP policy / notes.

Card (<root>/<guid>/<guid>/index.md)

Field Type Required Notes
order number yes Rank within its lane, ascending = top-to-bottom

Body: the card's content — the whole point.

Schema-owned display fields are Lanework's to interpret — coerce where a sensible reading exists, fall back to the field's default where none does (settled). A scalar of the wrong YAML type reads as its source text (title: 2048 displays as "2048", width: "2" reads as 2); where no sensible reading exists — a sequence or mapping where a scalar belongs, a non-integer width, an unparseable timestamp — the field falls back to its default: untitled placeholder, width 1, no color, no icon. Coercion is read-side only; the bytes on disk are preserved verbatim, never rewritten. One tombstone nuance: any present deleted: key tombstones the item — an unusable timestamp still deletes, its date merely unknown (the user's intent to delete outranks the broken date). Duplicate keys: last one wins (settled — the coercing read; strict YAML would reject the file, so this is a deliberate divergence in the editor's favor): a key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. An app write of a duplicated key rewrites the winning (last) occurrence and removes the earlier ones — the app owns the keys it writes, and leaving a stale twin would resurrect it if the winner were later removed; removing a key removes all its occurrences. App rewrites preserve comments (settled): comments on their own lines always survive a rewrite; an inline comment on a rewritten value line is re-spliced after the new value — best-effort, guaranteed for plain scalar lines (the realistic case), dropped only in pathological shapes. Fail-fast remains reserved for structure (schema, order, YAML validity) — and it covers malformed, not just missing: an order that is present but non-numeric is the same loud malformed-input rejection as a missing one.

Enhanced schema (reserved, out of scope)

Future integrations extend the core schema; this version only reserves their names so nothing else squats on them. The app treats reserved keys as ordinary unknown keys (preserved verbatim, invisible in the UI) and reserved folders as ignored non-level content:

  • comments/ card child — a future comment thread: fractal like everything else (folder-per-comment holding index.md + attachments/, author/created frontmatter, chronological). Specified fully when the enhanced schema is designed; no comments functionality in this version.
  • Card metadata keys labels, assignees, due — issue-style metadata belonging to the tracker-integration story (the pathfinder shipped them natively; the rewrite reserves them instead). One deliberate carve-out: the commit-message composer names changes to these three keys ("Relabel card 'X'" — 06-history-undo.md), invisible in the UI though they are.
  • remote key (board, card) and remote-state key (lane) — future tracker connectors (Gitea/GitHub/GitLab/Jira), per 07-sync-collab.md's out-of-scope note.

Ordering

Carried over unchanged — gapped fractional ranks:

  • New items append at max + 1024; insert at head = min 1024; insert between two = midpoint.
  • A reorder rewrites only the moved item's index.md.
  • Midpoint precision exhausted → renumber that one lane's cards (or the board's lanes) to whole multiples of 1024. Rare, local — and the one exception to the only-the-moved-item rule: a renumber rewrites every sibling's index.md in that lane. On a remote-shared board the burst is conflict-prone in principle but benign in practice: each rewritten file changes only its order, the local-wins rebase (07-sync-collab.md) resolves per file, orders stay valid floats, and deterministic tie-breaking keeps rendering stable even if two machines' renumbers interleave. It lands inside the triggering operation's single commit (06-history-undo.md): the renumber batches with the insert or move that exhausted the midpoint, and its sequence-preserving order rewrites are bookkeeping to the message composer — the commit reads as the triggering event ("Move card 'Fix login' to Doing"), never as N separate edits or a standalone renumber.
  • Ties broken by folder name (lexicographic) for deterministic rendering.

Attachments

  • attachments/ under a card folder holds the card's files. The app's attachment surfaces (the card window's sidebar section and import — 05-card-window.md; the face carousel — 03-board-ui.md ▸ Card face) are flat: top-level files only.
  • Import collisions auto-rename, Finder-style (shot.pngshot 2.png): an import never overwrites an existing file and never refuses the drop. Never-refuses is policy, not an I/O guarantee — a failed copy (source unreadable, destination full) surfaces per 02-architecture.md ▸ Write-failure surfacing, with any partial file removed.
  • Subfolders are tolerated, preserved verbatim, never created by the app, and not surfaced in those surfaces. Their contents stay reachable via Reveal in Finder, and body-embedded relative paths into them (![](attachments/sub/x.png)) still render — preview resolves against the card folder.

Deletion — tombstones

  • Deleting writes deleted: <ISO-8601> into frontmatter; tombstoned items (and everything beneath) are hidden from the board and surface in the trash quasi-lane (03-board-ui.md) — a pure view. Tombstoned items stay exactly where they are on disk, which is what makes restore (Put Back = removing the key) position-preserving: the item reappears at its recorded order among its current siblings. One acknowledged edge: a renumber rescales visible siblings only (tombstones are inert to ordering), so a post-renumber Put Back can land among different neighbors than the ones it was deleted between — same lane, deterministic placement, just not always the original gap.
  • Tombstones are inert to ordering: appends and renumbering operate on visible siblings only; a restored item re-enters at its recorded order, ties broken deterministically.
  • A folder that disappears without a tombstone (Finder deletion) is also a delete; the app reflects it (such items are gone entirely — they never enter the trash).
  • A deleted: key at board level is legal per the common frontmatter table but meaningless — a board can't tombstone itself out of its own window. The loader ignores it and logs (the lenient-values philosophy: preserved verbatim, invisible in the UI).
  • Physical removal is the trash's Delete Immediately / Empty Trash (03-board-ui.md). Time-based auto-purge of old tombstones: deliberate follow-up, not in the initial release.

Malformed input — fail fast

Loud, specific error (path + what's wrong) for: unparseable YAML, missing required fields (schema; order where required), schema newer than the app, board root without index.md. No partial loads. The only tolerated absence is a missing index.md below the root (skip + warn, per Rules above). An explicitly null value (order: with nothing after it, order: null) reads as missing (settled): on a required field that is the missing-required-field rejection — it describes what the hand-editor actually did, started the key and never gave it a value; on an optional field, null is simply absent and the default applies.

Fail-fast is the initial-load contract. Once a board is open, a failed live reload does not blank the board: the window keeps the last good snapshot and surfaces the same loud specifics in a non-modal banner — see 02-architecture.md's live-reload resilience.

Document packaging

  • Boards are created as .kanban packages (UTI conforming to com.apple.package) so Finder treats a board as one document.
  • Extension-less board folders still open.

Changes from the pathfinder schema

No migration is implied by these — pathfinder boards have no compatibility claim on the rewrite (a hand-rename of media/attachments/ etc. covers any board worth carrying over).

  • media/attachments/ (clearer, one unambiguous home for card files).
  • The schema is split into core (this version) and enhanced (comments, tracker fields — reserved only, out of scope).

Open questions

None currently — trash-bin behavior is settled: in-place tombstones with the trash as a pure view (Deletion above; UI in 03-board-ui.md).