From 11981a2a9902e5684031b021bb3b48fbdef5a85c Mon Sep 17 00:00:00 2001 From: rzen Date: Fri, 31 Jul 2026 19:01:20 -0400 Subject: [PATCH] Make order and schema optional below the board root Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY --- DESIGN/01-storage-format.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/DESIGN/01-storage-format.md b/DESIGN/01-storage-format.md index 8706c33..ae5a097 100644 --- a/DESIGN/01-storage-format.md +++ b/DESIGN/01-storage-format.md @@ -44,7 +44,7 @@ MyBoard.kanban/ ← board = the document | Field | Type | Required | Notes | |---|---|---|---| -| `schema` | integer | yes | `1` for this design | +| `schema` | integer | root only | `1` for this design. **Required at the board root only** (re-ruled 2026-07-31): below the root a missing `schema` reads as 1 — coerce-tier, logged, bytes preserved — because the walk validating the file against schema 1 is exactly what makes the reading reliable; the root's stamp stays required as the board's identity claim. The app itself always writes the key at every level. | | `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 **that rewrites this `index.md`** — see below | @@ -72,7 +72,7 @@ Body: board description (free Markdown). | Field | Type | Required | Notes | |---|---|---|---| -| `order` | number | yes | Rank among lanes, ascending = left-to-right | +| `order` | number | no | Rank among lanes, ascending = left-to-right. Optional (re-ruled 2026-07-31) — missing or unusable reads as append-at-end (Ordering below) | | `width` | integer | no | Width multiplier ≥ 1 (default 1); no cap | Body: lane description / WIP policy / notes. @@ -81,11 +81,11 @@ Body: lane description / WIP policy / notes. | Field | Type | Required | Notes | |---|---|---|---| -| `order` | number | yes | Rank within its lane, ascending = top-to-bottom | +| `order` | number | no | Rank within its lane, ascending = top-to-bottom. Optional (re-ruled 2026-07-31) — missing or unusable reads as append-at-end (Ordering below) | 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. **A no-sensible-reading fallback logs** (ruled 2026-07-29 — the family posture: every silent recovery leaves a trace): field, path, and raw text, carried as coerce-tier entries in the integrity service's Defect stream (Validation and healing below) — the one place where an observed-in-the-wild shape can later be promoted to a heuristic heal or a notice; no banner, no behavior change. **The unified log is the stream's consumer** (ruled 2026-07-30): the structured log line — subsystem-filterable in Console.app — is the whole surface this version, and promotion is deliberately manual, investigation-driven: a reported oddity leads to the log, the log names the shape, the shape informs the next heuristic. A no-telemetry app has no other observer to build for; an in-app diagnostics surface listing what a load coerced or tolerated is WISHLIST #11, and the typed stream is the plumbing it would read. **Ranges are part of the sensible reading** (settled): an integer `width` below 1 reads as 1 — the table's ≥ 1 is a validity bound, and 03-board-ui.md's width division must never see a zero or negative unit — while a non-finite `order` (`.nan`, `.inf`) is the same loud malformed-input rejection as a non-numeric one: fail-fast's `order` guard covers unusable values, not merely non-numeric ones (NaN has no place in the total order the tie-break and midpoint math assume). The hostile-YAML suite gains both shapes (`width: 0`, `order: .nan`). Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One legacy nuance: a *present* `deleted:` key is migration input, not live state — the item relocates per the Deletion section's legacy rule, whatever the timestamp's condition. **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 *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. Last-wins resolves *before* strictness: a duplicated `order` reads as its final occurrence and only that value faces the malformed-order check — duplication itself is never a fail-fast. 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. **Unquoted colons in a value read as the string they meant** (ratified — the third member of the rescue family, same posture as last-wins and coercion): the classic slip `title: Fix: the thing` — invalid YAML ("mapping values are not allowed in this context") — recovers when, and only when, the scanner error's mark lands on a line that *starts a top-level key* and whose remainder is a **plain unquoted scalar**: everything after the first `: ` reads as the value. A remainder opening with a quote or flow character (`"`, `'`, `[`, `{`) is a different breakage and stays fail-fast; nested/indented shapes never match the guard and stay errors. Mechanism mirrors the duplicate-key rescue: compose retries on a modified copy (the offending value swapped for a same-line placeholder), so one recovered line leaves every other line's diagnostics intact — a second, genuinely broken line still fails loudly with its own error. Read-side only, bytes preserved verbatim; the span editor keys by the line's key prefix, so the file stays fully editable, and the first app write of that key emits the value properly quoted — the slip heals on first touch. Recovery is **silent with a log line** (the family's posture: the recovered value is exactly what the writer meant, so there is nothing actionable to surface). The agent guide keeps teaching quoting — the rescue is a net, not a license. Test obligation: the hostile-YAML suite gains the `key: value: more` shape (none covers it today). 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. **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys, or frontmatter whose YAML resolves to an *empty* mapping while still carrying text (`{}`, `null`, `~` between the delimiters: zero keys load fine, but appending a key after that text would produce unparseable YAML; truly blank or comment-only frontmatter stays editable) — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection. **Compound operations are transactions against this rule** (ruled 2026-07-29): every copy flow that rewrites descendants' `index.md` (paste, ⌥-drag duplicate, the cross-board copy, template instantiation — the stamp and the `modified-by` clear are part of the copy contract, not optional bookkeeping) **preflights the entire subtree** and refuses whole, loudly, naming the offending item — never a partial copy, never a silently unstamped descendant; the finest-grain precedent covers identity collisions, not skipped contract work. **The preflight reaches comment depth** (ruled 2026-07-31): a comment whose frontmatter cannot take the stamp refuses the copy exactly like a card or lane — never-refuse (Enhanced schema below) is a *load* posture, and a user-initiated copy is a transaction, not a load; the refusal names the comment's card so the fix — repair or delete the annotation — is one gesture away, and no copy ever carries a live `remote` claim or stale `modified-by` the sever rule exists to prevent. A mid-flight failure after a clean preflight (disk error) removes the partial result wholesale — construct-then-clean (09-templates.md's atomicity). Whole-board forks (Duplicate, Save as Template) stamp nothing and are exempt. +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. **A no-sensible-reading fallback logs** (ruled 2026-07-29 — the family posture: every silent recovery leaves a trace): field, path, and raw text, carried as coerce-tier entries in the integrity service's Defect stream (Validation and healing below) — the one place where an observed-in-the-wild shape can later be promoted to a heuristic heal or a notice; no banner, no behavior change. **The unified log is the stream's consumer** (ruled 2026-07-30): the structured log line — subsystem-filterable in Console.app — is the whole surface this version, and promotion is deliberately manual, investigation-driven: a reported oddity leads to the log, the log names the shape, the shape informs the next heuristic. A no-telemetry app has no other observer to build for; an in-app diagnostics surface listing what a load coerced or tolerated is WISHLIST #11, and the typed stream is the plumbing it would read. **Ranges are part of the sensible reading** (settled): an integer `width` below 1 reads as 1 — the table's ≥ 1 is a validity bound, and 03-board-ui.md's width division must never see a zero or negative unit — while a non-finite `order` (`.nan`, `.inf`) is unusable exactly like a non-numeric one and takes the same reading — append-at-end (re-ruled 2026-07-31, Ordering below; NaN has no place in the total order the tie-break and midpoint math assume, so an unusable value never enters it — it sorts by the default instead). The hostile-YAML suite keeps both shapes (`width: 0`, `order: .nan`), now as coercion cases. Coercion is read-side only; the bytes on disk are **preserved verbatim, never rewritten**. One legacy nuance: a *present* `deleted:` key is migration input, not live state — the item relocates per the Deletion section's legacy rule, whatever the timestamp's condition. **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 *top-level* key appearing twice reads as its last occurrence, earlier occurrences preserved verbatim on disk and invisible. A duplicate inside a nested mapping value remains a YAML error — the rescue applies where the editor's slip actually happens, the top level. Last-wins resolves *before* strictness: a duplicated `order` reads as its final occurrence and only that value faces the usability reading — duplication itself never degrades anything. 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. **Unquoted colons in a value read as the string they meant** (ratified — the third member of the rescue family, same posture as last-wins and coercion): the classic slip `title: Fix: the thing` — invalid YAML ("mapping values are not allowed in this context") — recovers when, and only when, the scanner error's mark lands on a line that *starts a top-level key* and whose remainder is a **plain unquoted scalar**: everything after the first `: ` reads as the value. A remainder opening with a quote or flow character (`"`, `'`, `[`, `{`) is a different breakage and stays fail-fast; nested/indented shapes never match the guard and stay errors. Mechanism mirrors the duplicate-key rescue: compose retries on a modified copy (the offending value swapped for a same-line placeholder), so one recovered line leaves every other line's diagnostics intact — a second, genuinely broken line still fails loudly with its own error. Read-side only, bytes preserved verbatim; the span editor keys by the line's key prefix, so the file stays fully editable, and the first app write of that key emits the value properly quoted — the slip heals on first touch. Recovery is **silent with a log line** (the family's posture: the recovered value is exactly what the writer meant, so there is nothing actionable to surface). The agent guide keeps teaching quoting — the rescue is a net, not a license. Test obligation: the hostile-YAML suite gains the `key: value: more` shape (none covers it today). Fail-fast remains reserved for structure with no sensible reading: YAML validity, a `schema` newer than the app, and the board root's requirements (re-ruled 2026-07-31 — below the root, `order` and `schema` are optional and default instead; a present-but-unusable `order` takes the same append-at-end reading as a missing one, Ordering below). **Readable-but-uneditable shapes load; writes to them refuse** (settled): frontmatter the surgical editor can't key by spans — a whole-frontmatter flow mapping, non-scalar keys, or frontmatter whose YAML resolves to an *empty* mapping while still carrying text (`{}`, `null`, `~` between the delimiters: zero keys load fine, but appending a key after that text would produce unparseable YAML; truly blank or comment-only frontmatter stays editable) — reads and renders normally, and any app write to that file fails loudly through the per-file write-failure banner (02-architecture.md ▸ Write-failure surfacing) naming the shape, never a silent corruption and never a load rejection. **Compound operations are transactions against this rule** (ruled 2026-07-29): every copy flow that rewrites descendants' `index.md` (paste, ⌥-drag duplicate, the cross-board copy, template instantiation — the stamp and the `modified-by` clear are part of the copy contract, not optional bookkeeping) **preflights the entire subtree** and refuses whole, loudly, naming the offending item — never a partial copy, never a silently unstamped descendant; the finest-grain precedent covers identity collisions, not skipped contract work. **The preflight reaches comment depth** (ruled 2026-07-31): a comment whose frontmatter cannot take the stamp refuses the copy exactly like a card or lane — never-refuse (Enhanced schema below) is a *load* posture, and a user-initiated copy is a transaction, not a load; the refusal names the comment's card so the fix — repair or delete the annotation — is one gesture away, and no copy ever carries a live `remote` claim or stale `modified-by` the sever rule exists to prevent. A mid-flight failure after a clean preflight (disk error) removes the partial result wholesale — construct-then-clean (09-templates.md's atomicity). Whole-board forks (Duplicate, Save as Template) stamp nothing and are exempt. ## Enhanced schema (reserved; comments storage specified 2026-07-29, feature out of scope) @@ -100,6 +100,7 @@ Future integrations extend the core schema; this version reserves their names Carried over unchanged — gapped fractional ranks: - New items append at `max + 1024`; insert at head = `min − 1024`; insert between two = midpoint. +- **Missing or unusable `order` reads as append-at-end** (re-ruled 2026-07-31 — below the root the key is optional, Frontmatter above): order-less items sort after every ordered sibling, among themselves by the folder-name tie-break — deterministic with zero sibling reads, which is what makes the minimum agent card legal (`mkdir` + one `index.md` write, no 200-file lane scan; 08-agent-integration.md's masterplan requirement — "filing a card must need nothing but the schema" — demands exactly this). The reading is coerce-tier (logged, bytes preserved); the app still always writes the key. **The rank materializes on touch**: the first Writer rewrite of the file stamps a real rank (on-touch heal, Validation and healing below), and placement math that must rank an item *relative to* an order-less sibling stamps that sibling inline, inside the gesture's bracket and commit (the midpoint-renumber precedent). Accepted cost: two order-less siblings sort by UUID rather than by intent until touched — logged, healed on first touch. - 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 — order-only rewrites, so no `modified` stamp and no `modified-by` clear (reorders-don't-stamp, `modified`'s scope above). 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. The batch is atomic per file, not per lane — an interrupted renumber (crash, volume error mid-lane) leaves some siblings rescaled and some not, which is accepted the same way: every order still a valid float, display order still deterministic via the tie-break, and the next renumber completes the job. 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, **comparing the canonical lowercase spelling** — settled: ordering follows the value-based identity model, never an uppercase folder's ASCII accident) for deterministic rendering. The same canonicalization applies to every folder-name tie-break in the corpus (the trash sort included — 03-board-ui.md). @@ -123,17 +124,16 @@ The tombstone model is **retired** — no `deleted:` key, no in-place flags, no ## 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 partial load is the decision surface's user-chosen Skip (below), and 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. +Loud, specific error (path + what's wrong) for: unparseable YAML, `schema` newer than the app, board root without `index.md`, and the root's own missing `schema` (re-ruled 2026-07-31 — below the root `order` and `schema` are optional and default instead, append-at-end and schema-1, coerce-tier; the root keeps its requirements as the this-really-is-a-board gate). No partial loads — the only partial load is the decision surface's user-chosen Skip (below), and 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): it describes what the hand-editor actually did — started the key and never gave it a value — so the field's missing reading applies: the default everywhere the key is optional, the rejection on the root's `schema`. 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. **The decision surface (settled 2026-07-31).** Blocking means one aggregated surface hosted by the pre-snapshot loading window (02-architecture.md ▸ the loading state) — the loading content transforms in place, never a sheet over a spinner. It appears on **attended opens only** (welcome click, File ▸ Open…, Finder): restoration failures keep the retire-to-welcome-row landing, and the row's retry click is the attended open that then shows the surface — repair is an attended act, and launch never chains dialogs. The loader collects every fail-fast defect in the walk rather than stopping at the first. The surface groups defects **by class**: each class section states the defect once, lists the affected files (path + specifics, each with Reveal in Finder and Open in Editor), and carries one class-level choice preselected to its default, with per-item override behind a disclosure. The vocabulary offers only honest choices per class: - *Unparseable YAML* — no app-minted rewrite (the app would be guessing at content): Open in Editor + Re-check, or Skip. -- *Missing/null/malformed `order`* — minted lossless repair, the default: place at the bottom of its container (max + 1024). Order is app-owned; nothing user-authored is lost. -- *Missing `schema`* — stamp `schema: 1`, the default: reliable exactly because the walk just validated the rest of the file against schema 1. - *`schema` newer than the app* — no honest fix (downgrading risks silent loss): an unfixable row ("made by a newer Lanework — update the app") offering only Skip; on the board root it blocks the whole board (Cancel is the only exit). - *Board root without `index.md`* — minted repair, the default: create a board index (folder-name title, `schema: 1`) — the user just opened this folder as a board, and the mint is content-lossless. +- *Board root missing `schema`* — stamp `schema: 1`, the default: reliable exactly because the walk just validated the file against schema 1. (Below the root, missing `order`/`schema` never reach this surface — they default at read time, re-ruled 2026-07-31: Frontmatter and Ordering above.) **Skip is user-consented tolerance, loudly marked** (ruled 2026-07-31): a skipped item loads the board without it — the file stays on disk untouched, tolerated-invisible like strays — and the opened board carries a warning-tone notice naming the skipped items, each with Reveal in Finder. Skips are **per-open decisions, never persisted**: the next open of a still-broken board presents the surface again — the notice is the honest residue of this open, not a stored preference. @@ -149,7 +149,7 @@ Every rule in this document that refuses, tolerates, recovers, or repairs is an - **Coerce** — a sensible reading exists (the coercion rulebook, last-wins, null-as-missing, the rescue family): silent, read-side only, bytes preserved — the recovered value is exactly what the writer meant. - **Heal** — an app-owned invariant is violated *and* a lossless canonical repair exists: the only verdict that writes. -Heals classify on two axes. **Consent**: *app-initiated* covers every **reliably healable** repair — semantically unambiguous and content-lossless (loose-file relocation, legacy card migration, `kind` backfill, the agent guide, and — re-ruled 2026-07-29 — the duplicate-id remint: Lanework owns the board and re-mints identity at will, Fractal layout above); *user-gated* is reserved for fixes that would genuinely choose interpretation — a class with **no current members** since the remint went silent, kept as the boundary line. The silent posture is safe because heals are auditable: on Pro boards every heal commits separately (below) — the durable trail; on base it simply happens quietly — and a heal that changes user content announces itself with a warning-tone notice (the relocation's precedent), never a consent gate. **Timing**, three modes: *inline* when the triggering gesture cannot proceed without it (the midpoint-exhaustion renumber, the import-boundary remint — each stays inside its gesture's write bracket and commit); *on-touch* when the defect is latent — the fix folds into a write already rewriting that file (`kind` backfill, duplicate-key twin removal, the colon rescue's quote-on-first-write); *scheduled* when the defect degrades behavior while it stands (loose files invisible to the attachment surface, `deleted:` cards rendering by a retired model, a stale agent guide actively misleading agents, a wrong-typed node squatting a claimed name at any level — deletion, or a card's attachment surface, broken while it stands) — run at the reload tail and at board open, uniformly. The timing principle: **proactive when the defect is load-bearing now, on-touch when it's latent.** +Heals classify on two axes. **Consent**: *app-initiated* covers every **reliably healable** repair — semantically unambiguous and content-lossless (loose-file relocation, legacy card migration, `kind` backfill, the agent guide, and — re-ruled 2026-07-29 — the duplicate-id remint: Lanework owns the board and re-mints identity at will, Fractal layout above); *user-gated* is reserved for fixes that would genuinely choose interpretation — a class with **no current members** since the remint went silent, kept as the boundary line. The silent posture is safe because heals are auditable: on Pro boards every heal commits separately (below) — the durable trail; on base it simply happens quietly — and a heal that changes user content announces itself with a warning-tone notice (the relocation's precedent), never a consent gate. **Timing**, three modes: *inline* when the triggering gesture cannot proceed without it (the midpoint-exhaustion renumber, the import-boundary remint — each stays inside its gesture's write bracket and commit); *on-touch* when the defect is latent — the fix folds into a write already rewriting that file (`kind` backfill, duplicate-key twin removal, the colon rescue's quote-on-first-write, the rank stamp on an order-less file — Ordering above); *scheduled* when the defect degrades behavior while it stands (loose files invisible to the attachment surface, `deleted:` cards rendering by a retired model, a stale agent guide actively misleading agents, a wrong-typed node squatting a claimed name at any level — deletion, or a card's attachment surface, broken while it stands) — run at the reload tail and at board open, uniformly. The timing principle: **proactive when the defect is load-bearing now, on-touch when it's latent.** **Write discipline needs no rule of its own** (settled — the existing stamp rule decides every case): a heal that rewrites an `index.md` is an ordinary app write — stamps `modified`, clears `modified-by`; an on-touch heal rides its host write's stamp — while a heal that only renames or relocates folders and files never opens `index.md` and stamps nothing (the relocation, the remint — identity repairs, not edits). Atomicity is per file (temp + rename); interrupted batches are accepted per the renumber precedent — every intermediate state is valid. **Every scheduled heal re-verifies its defect against disk at write time** and no-ops when it is gone: losing the race to a foreign fix — an agent mid-edit, another repair — is success, never an error (the remint-races-a-vanished-duplicate precedent, generalized). Scheduled heals are memo-guarded against hot loops — armed before the attempt, cleared explicitly on success, re-armed only by a changed defect signature — and **defer, never abandon**, under any read-only lock.