From 1d1796a7f14213133984453a80bf06f477ae7dcb Mon Sep 17 00:00:00 2001 From: rzen Date: Wed, 29 Jul 2026 16:45:45 -0400 Subject: [PATCH] =?UTF-8?q?Moves=20don't=20stamp=20=E2=80=94=20order-only?= =?UTF-8?q?=20rewrites=20leave=20modified=20and=20modified-by=20alone?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rule 01 ▸ Deletion and 03 ▸ Trash cited is now stated (ruled 2026-07-29): order is logically the container's property, stored as an attribute in each member's file for implementation convenience, so reorders, cross-lane moves, and renumber rescales rewrite index.md without touching content — no modified stamp, no modified-by clear. The trash move is the genuine exception: deletion is an edit to the item's story, stamping and clearing both, feeding the future age-based auto-purge. Considered and declined: stamping the container's own modified on reorder — fair reading, pragmatically risky to enforce, nothing reads it. Board: Contradictions card 30c8f207 → Resolved. Code realign (Writer stamp suppression, agent guide v7) queues for the session-end realign card. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 --- DESIGN/01-storage-format.md | 8 ++++---- DESIGN/03-board-ui.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/DESIGN/01-storage-format.md b/DESIGN/01-storage-format.md index 0d4be3d..ba59371 100644 --- a/DESIGN/01-storage-format.md +++ b/DESIGN/01-storage-format.md @@ -54,9 +54,9 @@ MyBoard.kanban/ ← board = the document | `iconColor` | string | no | Palette name or hex | | `kind` | string | no | The object's kind — `board`, `lane`, `card` (`comment` reserved with the enhanced schema). **Written at creation of every object** (re-ruled 2026-07-29 — consistency across the schema, even where position already answers). The value — never the key's mere presence — names the kind, and consumers that consult it trust the value outright: no stripping, no corroboration machinery. Only consequential inside `.trash/` today, where position can't answer (Deletion below); everywhere else it is redundant with position (level is position) and carried for uniformity. Missing on an older object, it **backfills on touch** — the integrity service's on-touch heal (Validation and healing below), never a scheduled sweep. | -**`modified`'s scope — content provenance, not file activity (settled).** The stamp updates on every app write that rewrites the item's `index.md`, and only those — it describes the item's *content* (title, body, frontmatter), never its files. Two designed app writes therefore don't bump it, deliberately: **raw-source Apply** writes the validated buffer byte-for-byte (05-card-window.md — the verbatim contract outranks stamping, the same reason it doesn't clear `modified-by` below), and **attachment operations** never touch `index.md` at all — load-bearing, not an oversight: Add Attachment stays enabled during an open raw-source edit precisely because it can't fight the buffer (03-board-ui.md ▸ toolbar). Accepted consequence: the card window's "Modified ⟨date⟩" line (05-card-window.md) can read older than the card's latest attachment activity — attachment history lives where files do (the sidebar shows them; on git boards the Attach/Remove commits are the activity trail — 06-history-undo.md). +**`modified`'s scope — content provenance, not file activity (settled).** The stamp updates on every app write that rewrites the item's `index.md`, and only those — it describes the item's *content* (title, body, frontmatter), never its files. Two designed app writes therefore don't bump it, deliberately: **raw-source Apply** writes the validated buffer byte-for-byte (05-card-window.md — the verbatim contract outranks stamping, the same reason it doesn't clear `modified-by` below), and **attachment operations** never touch `index.md` at all — load-bearing, not an oversight: Add Attachment stays enabled during an open raw-source edit precisely because it can't fight the buffer (03-board-ui.md ▸ toolbar). The third is a class, not a write: **order-only rewrites don't stamp — the moves-don't-stamp rule** (ruled 2026-07-29; the rule Deletion below and 03 ▸ Trash already cited). `order` is logically the *container's* property — a relationship among the lane's members that the format happens to store as an attribute inside each member's file — so a reorder, a cross-lane move, and a renumber's whole-lane rescale (Ordering below) rewrite `index.md` without touching content: no stamp, and no `modified-by` clear (the pairing below — attribution can't change when content didn't). The trash move is the one deliberate exception (Deletion below): deletion is an edit to the item's story, so it stamps and clears both. Considered and declined: stamping the container's own `modified` on reorder (the lane's ordering did change) is a fair reading, declined pragmatically — it fans one member's write out to the container's file and is hard to enforce without adding risk, for a signal nothing reads. Accepted consequence: the card window's "Modified ⟨date⟩" line (05-card-window.md) can read older than the card's latest attachment activity — attachment history lives where files do (the sidebar shows them; on git boards the Attach/Remove commits are the activity trail — 06-history-undo.md). -**`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. +**`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. Two carve-outs: **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; and **order-only rewrites** (the moves-don't-stamp rule above) — position changes touch no content, so a standing stamp survives a reorder or move, while the trash move, stamping `modified`, clears here too. ### Board (`/index.md`) @@ -101,7 +101,7 @@ 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. 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. +- 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 (moves-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). ## Attachments @@ -114,7 +114,7 @@ Carried over unchanged — gapped fractional ranks: The tombstone model is **retired** — no `deleted:` key, no in-place flags, no ancestor-walked hiding. Deletion is a **move**: -- **Deleting a card or a lane moves its folder into `/.trash/`** (lanes joined 2026-07-29 — retiring the design's sole destructive delete) — a reserved, app-claimed name at board root (joining `CLAUDE.md` and the seeded `.gitignore` on the claimed list; agents and hand-editors should treat it as the app's). `.trash/` holds card and lane folders interleaved directly, no `index.md` of its own. A non-directory node squatting the name — a file, a symlink — is an invalid artifact healed by displacement (the claimed-names rule above): until the heal lands, the loader reads the board as having an empty trash and a delete's directory mint fails as an ordinary one-shot — a window measured in one reload, not a standing state. Every arrival lands at the **top** regardless of kind: the move mints an `order` rank above the current topmost, so the trash sorts newest-first by ordinary ranks — no timestamp sort, no tie-break rules. The move **stamps `modified`** (deletion is an edit to the item's story — the deliberate exception to moves-don't-stamp), which is what a future age-based auto-purge will read (deliberate follow-up, not in the initial release). A trashed lane keeps its subtree untouched — its cards ride along and come back with it. +- **Deleting a card or a lane moves its folder into `/.trash/`** (lanes joined 2026-07-29 — retiring the design's sole destructive delete) — a reserved, app-claimed name at board root (joining `CLAUDE.md` and the seeded `.gitignore` on the claimed list; agents and hand-editors should treat it as the app's). `.trash/` holds card and lane folders interleaved directly, no `index.md` of its own. A non-directory node squatting the name — a file, a symlink — is an invalid artifact healed by displacement (the claimed-names rule above): until the heal lands, the loader reads the board as having an empty trash and a delete's directory mint fails as an ordinary one-shot — a window measured in one reload, not a standing state. Every arrival lands at the **top** regardless of kind: the move mints an `order` rank above the current topmost, so the trash sorts newest-first by ordinary ranks — no timestamp sort, no tie-break rules. The move **stamps `modified` and clears `modified-by`** (deletion is an edit to the item's story — the deliberate exception to the moves-don't-stamp rule, `modified`'s scope above), which is what a future age-based auto-purge will read (deliberate follow-up, not in the initial release). A trashed lane keeps its subtree untouched — its cards ride along and come back with it. - **`kind:` discriminates inside `.trash/`** (ruled 2026-07-29): depth defines meaning *on the live board*, but the trash is flat, and an empty lane folder is shape-identical to a card folder (`/index.md` both). Every object carries `kind` from creation (re-ruled 2026-07-29 — the common-schema row, Frontmatter above); inside `.trash/` it is where the key earns its keep. A lane's `kind: lane` is **backfilled on touch when absent** (the integrity service's on-touch heal, Validation and healing below: any Writer rewrite of that lane's `index.md`, the trash move's rank mint included, stamps the missing key — **on-touch only, never a scheduled backfill sweep**) and never stripped (restore stays a pure move; the key is inert on the live board where position wins). The trash reader's rule **trusts the value** (re-ruled 2026-07-29 — the value-names-the-kind posture): `kind: lane` → lane; `kind: card` → card; an unrecognized value or no key falls through to shape — UUID-shaped children with their own `index.md` → lane (the key backfills on the next touch), else card. Trusting the value means an external writer's `kind: lane` on what looks card-shaped is honored, never policed. Honest limit of on-touch-only backfill, accepted: a kind-less trashed lane emptied of its children before any touch becomes indistinguishable from a card. - **Restore is a move out**: any move of a card folder from `.trash/` into a lane, or of a lane folder to board level — drag, or cut+paste — is an ordinary move; there is no restore-specific machinery and no Put Back. Position (and, for cards, lane) are chosen at restore time. - A folder that disappears entirely (Finder deletion) is also a delete; the app reflects it — such items are gone, they never enter the trash. diff --git a/DESIGN/03-board-ui.md b/DESIGN/03-board-ui.md index ba89c90..fb941d9 100644 --- a/DESIGN/03-board-ui.md +++ b/DESIGN/03-board-ui.md @@ -62,7 +62,7 @@ The window-title widget opens the **board popover** — the one board-level surf **Resettled 2026-07-28 — the materialized trash.** The tombstone model (a `deleted:` flag on items left in place, rendered by a pure-view quasi-lane) is **retired**: it generated a standing tax of nesting rules — ancestor walks, effective liveness, entry-vs-universe splits, kind-homogeneous selection — that this design replaces wholesale. Deletion is now a **move**: deleting a card moves its folder into **`/.trash/`**, a reserved, materialized container (01-storage-format.md). A trashed card is an ordinary card in a special place — search, selection, rendering, styling, and clipboard all treat it exactly like any other card, and `.trash/` is self-describing in Finder and to agents. - **Lanes trash too** (re-ruled 2026-07-29, retiring "cards only" and with it the design's sole destructive delete): deleting a lane moves its folder — subtree intact — into `.trash/`, exactly as a card moves; `kind: lane` in its frontmatter is what tells a trashed lane from a card in the flat container (01-storage-format.md ▸ Deletion), stamped on the way in when absent. The no-dialog posture survives for a better reason: the move is recoverable, so nothing needs confirming. Native undo's inverse is the ordinary move back (13-native-undo.md — the capture/recreate machinery retires). **A trashed lane is an opaque unit**: one distinct dimmed row showing its title and held-card count ("Doing — 5 cards"), no styling accents, never expandable; its cards are invisible to search and not individually addressable — it restores whole or purges whole. The row matches the search filter by lane title only. Lane rows and cards interleave in the one trash column purely by trash rank. -- **Entry is always at the top** (settled): every arrival, card or lane — ⌫/⌘⌫ delete and drag-to-trash alike — lands at the trash's topmost position, minting an `order` rank above the current top. Newest-first ordering falls out of ordinary ranks by construction: **there is no `deleted:` key and no timestamp sort** — the trash sorts by `order` like any lane. The move **stamps `modified`** (the one exception to moves-don't-stamp — deletion is an edit to the card's story), which is what a future age-based auto-purge will read (deferred, 01-storage-format.md). +- **Entry is always at the top** (settled): every arrival, card or lane — ⌫/⌘⌫ delete and drag-to-trash alike — lands at the trash's topmost position, minting an `order` rank above the current top. Newest-first ordering falls out of ordinary ranks by construction: **there is no `deleted:` key and no timestamp sort** — the trash sorts by `order` like any lane. The move **stamps `modified`** (the one exception to moves-don't-stamp, now stated in 01's `modified` scope — deletion is an edit to the card's story), which is what a future age-based auto-purge will read (deferred, 01-storage-format.md). - **Rendering**: trailing (rightmost) position when shown, visually distinct — dimmed/hatched header, trash SF Symbol, count badge; no new-card button; not draggable, not resizable, excluded from lane reordering. Fixed one width unit, consumed only while shown; Show/Hide Trash is a re-divide trigger (Layout above). **Visibility**: hidden by default; View ▸ Show Trash toggles (no default chord — ⇧⌘T belongs to the system's Show Tab Bar, 11-command-nexus.md); per-open transient state, resets to hidden, never persisted. Hidden, the trash is invisible to every gesture and to search; shown, its cards participate in the filter **exactly like any other card** — the point of the pivot. - **No Put Back** (settled): the valuable item is the card (or lane); where it goes on the way out is the user's cheap decision. Restoring is an ordinary move out: **drag** a trash card into any lane at any position — or a trashed lane row to a lane-strip slot — or **⌘X in the trash, ⌘V** — into a lane for cards, after the anchor lane for a trashed lane (04-interactions.md's lane-paste rule verbatim); the clipboard works on trash items like on any item, which is also the keyboard-native restore path (10-accessibility.md). Dropped on another board it follows the drag locality model (04-interactions.md). - **No editing in the trash**: trash cards don't open — double-click stops at selection; move it out first — and a trashed lane row never expands. Moving a card to the trash dismisses its open card window (05-card-window.md), and an external move-in observed by reload does the same; a lane entering the trash dismisses the open card windows of every card it carries (they entered the trash with it).