A liveness flip is a vanish — selection re-resolves by UUID and liveness side

Resolves the 02-vs-04 contradiction: foreign edits that flip deleted:
on a selected card eject it from the selection (and pending cut, which
already behaved this way), so 04's homogeneous-by-liveness invariant
holds across reloads and menu validation never sees a mixed selection.

Claude-Session: https://claude.ai/code/session_01HJ7PhFNmQ19bvy9RMD6GSb
This commit is contained in:
2026-07-26 15:50:10 -04:00
parent 4b20f8499b
commit 0871b96789
2 changed files with 2 additions and 2 deletions
+1 -1
View File
@@ -41,7 +41,7 @@ The **one named exception** is transient UI state rendering things that don't ex
- **A failed reload never replaces a good snapshot.** Fail-fast (01-storage-format.md) is the *initial-load* contract, where there is nothing to fall back on. Once a board is open, a watcher-triggered reload that fails (unparseable YAML, missing required fields — typically a non-atomic external write caught mid-flight) keeps the last good snapshot on screen and raises a **non-modal banner** carrying fail-fast's specifics (offending path + what's wrong). The watcher keeps watching; the next successful reload clears the banner automatically — transient breakage self-heals without the user losing the board, persistent breakage stays loudly visible. Editing is not locked out: writes go through the Writer as usual (the breakage is per-file and localized), and the reload debounce already absorbs most momentary invalid states before they surface.
- **App-initiated git churn is bracketed.** Operations the app runs itself (pull-rebase, branch switch, undo restore — 06-history-undo.md, 07-sync-collab.md) suspend watcher reloads for their duration and finish with one full reload — half-checked-out trees are never rendered. External git activity (the user running git in a terminal) can't be bracketed: the debounce coalesces its churn, and a transiently inconsistent but parseable tree may render briefly and heals on the next event — accepted.
- **Selection survives reloads by UUID.** Selection — and every transient state that references items (drag state, pending cut) — is a set of UUIDs over the snapshot, re-resolved when a reload swaps it: items still present stay selected; items that vanished leave the selection silently, no substitute invented — the search filter's hidden-cards-leave-the-selection rule (04-interactions.md) applied to external change. The search filter is deliberately absent from that list: the query string is transient state, but its result set is *derived* — the predicate re-runs against each new snapshot (04's live filter), so a card an agent files mid-search appears the moment the reload lands, and a card edited to no longer match animates out. Kin rules elsewhere: card windows dismiss when their card is deleted (05-card-window.md), the placeholder is discarded when its lane vanishes (above), and VoiceOver announces a vanished focused card and recovers focus to its lane (10-accessibility.md). App-mediated deletion is deliberately different — an act, not a surprise: ⌫ selects the successor sibling (04-interactions.md ▸ The map).
- **Selection survives reloads by UUID.** Selection — and every transient state that references items (drag state, pending cut) — is a set of UUIDs over the snapshot, re-resolved when a reload swaps it: items still present stay selected; items that vanished leave the selection silently, no substitute invented — the search filter's hidden-cards-leave-the-selection rule (04-interactions.md) applied to external change. **A liveness flip is a vanish for this purpose**: re-resolution matches UUID *and* liveness side, so a foreign edit that tombstones a selected live card — or restores a selected tombstoned one — ejects it from the selection (and from the pending cut, which 04-interactions.md ▸ Clipboard already states), keeping 04's homogeneous-by-liveness invariant true across reloads. The search filter is deliberately absent from that list: the query string is transient state, but its result set is *derived* — the predicate re-runs against each new snapshot (04's live filter), so a card an agent files mid-search appears the moment the reload lands, and a card edited to no longer match animates out. Kin rules elsewhere: card windows dismiss when their card is deleted (05-card-window.md), the placeholder is discarded when its lane vanishes (above), and VoiceOver announces a vanished focused card and recovers focus to its lane (10-accessibility.md). App-mediated deletion is deliberately different — an act, not a surprise: ⌫ selects the successor sibling (04-interactions.md ▸ The map).
- **A failed reload after a bracketed operation locks the board read-only** — the exception to "editing is not locked out" above. Ordinary watcher breakage is per-file: the snapshot still describes the tree, so editing around the broken file is safe. But a bracketed git operation changed the tree *wholesale*: if its final reload fails, the last-good snapshot on screen describes the pre-operation state (after a branch switch, a different branch entirely — 06-history-undo.md), and writes derived from it would land nonsense on the new tree. The banner carries the same fail-fast specifics plus the read-only state; the next successful reload (typically after the offending file is fixed) clears both. **The lock's scope** (shared with the vanished-root case below): every mutating command disables via menu validation — creation, delete and Put Back, paste, Move/Style/rename, trash operations, the popover's git controls — and the board refuses drops, including drags arriving from another board's window; selection, navigation, search, ⌘C copy-out, and Reveal in Finder stay live (reading the last-good snapshot is the point of keeping it).
### Write-failure surfacing
+1 -1
View File
@@ -58,7 +58,7 @@ The trash quasi-lane (03-board-ui.md ▸ Trash) speaks the same keyboard languag
- **Navigation**: the shown trash is the **last container for card navigation** — arrows walk into and out of it, and ⌥→ jumps to it. The quasi-lane itself is never selectable *as a lane* (no lane op applies to it): with a lane selected, ←/→ and ⌥→ stop at the last real lane.
- **Moves are inert across the boundary**: no move or paste ever targets the trash (deleting is ⌫/⌘⌫), and ⌥⌘↑/⌥⌘↓ are inert *on* tombstoned cards (moving out is Put Back or drag-to-restore).
- **Selection is homogeneous by liveness** (extending the homogeneous-selection rule): a selection never mixes live and tombstoned cards. Select All selects visible live cards only; a rubber-band stays on the side of the boundary it started on. Menu validation stays binary — Delete for live selections, Put Back / Delete Immediately for tombstoned ones.
- **Selection is homogeneous by liveness** (extending the homogeneous-selection rule): a selection never mixes live and tombstoned cards. Select All selects visible live cards only; a rubber-band stays on the side of the boundary it started on. Menu validation stays binary — Delete for live selections, Put Back / Delete Immediately for tombstoned ones. External liveness flips can't breach the invariant: a reload that flips `deleted:` on a selected card ejects it from the selection (02-architecture.md's reload-survival rule — a flip is a vanish from its side of the boundary), so validation never sees a mixed selection.
- **Tombstoned lane entries are full keyboard citizens, homogeneous by kind**: arrows walk every trash entry in its sorted order — card and lane entries alike (a lane's single restorable entry, 03-board-ui.md ▸ Trash) — and the board's cards-XOR-lanes rule extends into the trash: a selection never mixes card entries and lane entries (on top of never mixing live and tombstoned). Put Back (⌘⌫) and Delete Immediately (⌥⌘⌫) apply to lane entries exactly as to cards — a put-back lane returns whole, cards and all. A lane entry is not draggable (its entry is a compact row, not the lane); its copy-out is ⌘C only, and its move-out is Put Back.
- **Clipboard: copy out only.** ⌘C (cards and lane entries), ⌥-drag, and the cross-board drag default (cards) always yield *live* copies — `deleted:` is stripped on paste/duplicate/drop, like copying a file out of Finder's Trash; a lane entry's copy additionally strips its tombstoned interior cards (the lane-copy rule — copies transfer content, and trash isn't content). ⌘X is disabled: the move-out vocabulary is Put Back or drag-to-restore, nothing else. The *cross-board restore-move* (⌘-drag below) needs no command of its own — its keyboard equivalent is the composition Put Back → ⌘X → ⌘V in the destination: same folder, same identity.
- **Everything edit-shaped is disabled** on tombstoned selections — Open Card, Rename, Style… (File ▸ Duplicate is untouched: it duplicates the board, never the selection — 11-command-nexus.md; card copies out of the trash are ⌘C or ⌥-drag, which name a live destination). Finder file drops (attachment import) on tombstoned cards are inert — 03-board-ui.md's no-editing-in-the-trash.