Materialize the trash — faces, menus, and grammar
Phase 3 finishes the pivot at the surface. One card face serves two containers: CardFaceView extracted with a role — board or trash — so stripe, tint, chip, selection stroke, cut dim, marquee registration, and drag are shared by construction, the trash side differing only in its absences: no Open, no rename, no Style, no file-hover highlight, and a Delete that goes through the confirmation host. The column rewrote around the lanes' own single-column masonry so drag reflow reads as positional slides; chrome stays the hatched header, symbol, and count — 11 gives Empty Trash to the File menu alone. Two real grammar bugs die here: plain Backspace on a trash selection purged without the confirmation the menu raises, and the context menu's Delete resolved against the standing selection, so right-clicking a trash card under a board selection silently did nothing — it now stages the clicked set explicitly. Open, Rename, Style, and Empty Trash validation became testable store seams; the column is one named accessibility container of ordinary card elements. The tombstone era is swept: deleteItem, restoreItem, stripTombstonedChildren — dead since lane copies stopped nesting trash — the restore verb, the unreachable put-back banner row, and every quasi-lane doc comment. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -316,7 +316,7 @@ private final class DuplicateCancellation {
|
||||
/// to a command — the thing 04's contract forbids.
|
||||
///
|
||||
/// **The board scope reveals either side of the trash boundary and ignores every lock.** Reveal "is
|
||||
/// not edit-shaped and stays enabled on tombstoned selections" (04 ▸ The trash), and inspection is a
|
||||
/// not edit-shaped and stays enabled on trash selections" (04 ▸ The trash), and inspection is a
|
||||
/// read, so neither the read-only lock nor the focused-editor rule applies — the same posture the
|
||||
/// trash row's own Reveal takes. A selection whose ids resolve to no folders (one the next reload
|
||||
/// will drop) disables rather than falling back to the root: revealing the wrong thing is worse
|
||||
|
||||
@@ -626,19 +626,15 @@ public final class AppModel {
|
||||
/// The lane and card counts stamped into the registry at close — **live items only** (02
|
||||
/// § Per-board app state, settled).
|
||||
///
|
||||
/// > tombstoned lanes and cards — and cards hidden beneath a tombstoned lane — don't count; the
|
||||
/// > row advertises the board's working size, and the trash is an errand, not inventory.
|
||||
///
|
||||
/// The nesting is the ancestor walk: a tombstoned lane is skipped whole, so its cards are never
|
||||
/// reached whatever their own flags say. `Lane.isDeleted`/`Card.isDeleted` are presence-of-key,
|
||||
/// not validity, so a malformed `deleted:` counts as deleted here exactly as it does everywhere
|
||||
/// else.
|
||||
/// > deleted lanes and cards don't count; the row advertises the board's working size, and the
|
||||
/// > trash is an errand, not inventory.
|
||||
///
|
||||
/// **`.trash/` is excluded by construction** (02-architecture.md § Per-board app state,
|
||||
/// re-grounded 2026-07-28 for the materialized trash): this walks `snapshot.lanes`, and the
|
||||
/// trash is `snapshot.trash` — a sibling container, never a lane — so no filter is needed and
|
||||
/// none could be forgotten. The flag walk above is the retiring half of the same rule, kept
|
||||
/// while boards written by older versions still carry `deleted:` keys.
|
||||
/// none could be forgotten. The tombstone era's ancestor walk over `deleted:` flags is gone with
|
||||
/// the flag; a board an older version wrote counts its unmigrated cards until the migration moves
|
||||
/// them, which is the safe direction and lasts exactly one write.
|
||||
///
|
||||
/// Static and pure: it is a fact about a snapshot, and the close flush is the wrong place to
|
||||
/// discover a counting bug.
|
||||
|
||||
@@ -9,10 +9,10 @@ import Foundation
|
||||
/// board, where identities could collide; a whole-board copy is a new namespace, and Duplicate's
|
||||
/// fork-keeps-history guarantee requires it (copied `.git` history must keep naming the paths it
|
||||
/// describes)".
|
||||
/// - **Tombstoned items are carried too** (03, settled): "Duplicate is a full fork, trash included —
|
||||
/// dropping them would leave the copy's working tree disagreeing with its own copied HEAD". This
|
||||
/// file does nothing to achieve that: a tombstone is a `deleted:` key inside a file, so a copy
|
||||
/// carries it by declining to be clever.
|
||||
/// - **`.trash/` is carried too** (03, settled, re-grounded 2026-07-28 for the materialized trash):
|
||||
/// "Duplicate is a full fork, `.trash/` included — dropping it would leave the copy's working tree
|
||||
/// disagreeing with its own copied HEAD". This file does nothing to achieve that: the trash is an
|
||||
/// ordinary folder under the root, so the tree walk carries it by declining to be clever.
|
||||
/// - **`.git` comes along** — a duplicate of a git board is a fork of its history — with only its
|
||||
/// remote configuration stripped, which is m7's.
|
||||
/// - Timestamps, unknown keys, strays, `CLAUDE.user.md`, attachments: verbatim, for the same reason.
|
||||
|
||||
@@ -16,7 +16,7 @@ import Foundation
|
||||
// Application Support, each folder loaded through `BoardLoader` — `name`/`blurb`/`icon` off the
|
||||
// template board's own `index.md`, order off its `template.order`, an unloadable user template still
|
||||
// listed (by folder name, marked unloadable, carrying the loader's specifics) but not instantiable.
|
||||
// `laneTitles` stops existing at that point: instantiation becomes a tree copy that skips tombstones,
|
||||
// `laneTitles` stops existing at that point: instantiation becomes a tree copy that skips `.trash/`,
|
||||
// mints fresh GUIDs, and stamps `created`/`modified` fresh (`BoardWriter.CopyStamps.born`), never
|
||||
// copying `.git`. The chooser's mini preview renders from the loaded `BoardModel` rather than from
|
||||
// these strings.
|
||||
|
||||
@@ -21,7 +21,7 @@ public struct CardPlacement: Equatable {
|
||||
/// A named decision rather than a scattering of `if`s, because 05-card-window.md ▸ Deletion &
|
||||
/// lifecycle and 02-architecture.md § Live-reload resilience state the same rule from two directions
|
||||
/// and both have to be true of one piece of code. Making it a value also makes it a *pure* function
|
||||
/// of a snapshot, which is the only way the tombstoned-lane case gets tested without a window.
|
||||
/// of a snapshot, which is the only way the deleted-lane case gets tested without a window.
|
||||
public enum CardWindowFate: Equatable {
|
||||
case shows(CardPlacement)
|
||||
case dismisses
|
||||
@@ -38,10 +38,10 @@ public enum CardWindowFate: Equatable {
|
||||
/// nothing the second time, which is what makes those two paths safe to both exist.
|
||||
///
|
||||
/// **What ending means now**: the Edit buffer's debounce is cancelled and its text written — the
|
||||
/// "window close" third of 05's flush rule, and on a dismissal caused by a tombstone the surgical
|
||||
/// body write 05 ▸ Deletion & lifecycle promises ("a dirty Edit buffer flushes into the tombstoned
|
||||
/// card's folder before the window dismisses ... so the keystrokes survive Put Back").
|
||||
/// `BoardStore.writeCardBody` resolves tombstoned cards on purpose for exactly this.
|
||||
/// "window close" third of 05's flush rule, and on a dismissal caused by a delete the surgical body
|
||||
/// write 05 ▸ Deletion & lifecycle promises ("a dirty Edit buffer flushes into the card's folder at
|
||||
/// its new `.trash/` location before the window dismisses ... so the keystrokes survive a later
|
||||
/// restore"). `BoardStore.writeCardBody` resolves trash cards on purpose for exactly this.
|
||||
///
|
||||
/// A *failing* close flush is not this object's problem to solve: it is `DirtyBufferGuard`'s modal
|
||||
/// moment, which the host runs earlier, on `windowShouldClose`, while there is still a window to
|
||||
@@ -100,10 +100,11 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
/// ### Its whole identity is `(board, card)`
|
||||
///
|
||||
/// Which is why this host is mostly a set of dismissal rules. The window follows its card between
|
||||
/// lanes for free — the key names neither — and it dismisses in the three cases where the key stops
|
||||
/// naming anything: the card is tombstoned, its *lane* is tombstoned (effective liveness is
|
||||
/// ancestor-walked, 02 § Live-reload resilience), or the card is simply not in this board's snapshot
|
||||
/// any more, which is what a cross-board move looks like from here.
|
||||
/// lanes for free — the key names neither — and it dismisses whenever the key stops naming a card
|
||||
/// **on the board**: the card moved into `.trash/` ("entering the trash counts as deleted" —
|
||||
/// 05-card-window.md ▸ Deletion & lifecycle, resettled 2026-07-28), its *lane* was deleted and took
|
||||
/// it along, or the card is simply not in this board's snapshot any more, which is what a cross-board
|
||||
/// move looks like from here.
|
||||
///
|
||||
/// ### It can never outlive its board window
|
||||
///
|
||||
@@ -164,10 +165,12 @@ struct CardWindowHost: View {
|
||||
|
||||
/// Whether a card window keyed on `cardID` still has a card, given this board's snapshot.
|
||||
///
|
||||
/// The three dismissal cases collapse into two lines: a card that is not in the snapshot is gone
|
||||
/// (deleted outright, or moved to another board — the board half of the key no longer names it),
|
||||
/// and a card whose **effective** liveness is trashed renders nowhere, whether the tombstone is
|
||||
/// its own or its lane's. Only a live card in a live lane keeps its window.
|
||||
/// **One walk over the lanes is the whole rule** (05-card-window.md ▸ Deletion & lifecycle,
|
||||
/// resettled 2026-07-28 — the materialized trash): deletion is a *move*, so a trashed card has
|
||||
/// physically left its lane and answers `.dismisses` by simply not being found — "entering the
|
||||
/// trash counts as deleted", with no liveness flag to read and no ancestor walk to run. A card
|
||||
/// whose lane was deleted, one purged outright and one moved to another board all fall out of the
|
||||
/// same absence. Only a card in one of this board's lanes keeps its window.
|
||||
///
|
||||
/// Takes the id as the ref stores it — a raw folder name — and compares it as an `ItemID`, so two
|
||||
/// case-spellings of one UUID are one card here exactly as they are everywhere else.
|
||||
|
||||
@@ -29,7 +29,7 @@ extension UTType {
|
||||
///
|
||||
/// `kind` and `container` are the selection's own vocabulary (`SelectionKind`, `ItemContainer`) rather than
|
||||
/// near-copies of it: a clipboard payload is a selection that was copied, and the cards-XOR-lanes and
|
||||
/// live-XOR-tombstoned invariants are exactly the ones those two types already carry. Their raw
|
||||
/// board-XOR-trash invariants are exactly the ones those two types already carry. Their raw
|
||||
/// spellings are pasteboard API — a manifest written before a quit is decoded after the relaunch.
|
||||
///
|
||||
/// `entries` are in the order the copy read them — flatten order on the live side ("lane `order`,
|
||||
@@ -83,10 +83,11 @@ public struct ClipboardManifest: Codable, Sendable, Equatable {
|
||||
/// A **lane** entry's cards, index text and all — "a lane entry embeds its cards' too,
|
||||
/// attachment-less". Empty for a card entry.
|
||||
///
|
||||
/// **Live cards only**, which is not a shortcut: a lane *copy* strips tombstoned cards
|
||||
/// (04-interactions.md ▸ Clipboard, ▸ The trash), and the fallback only ever materializes a
|
||||
/// copy — a cut's move carries the real folder whole and never comes near this array. So the
|
||||
/// embedded set is exactly what a fallback paste should produce.
|
||||
/// **Exactly the lane's cards**, which needs no filter: "a lane carries exactly its cards —
|
||||
/// the trash is board-level, so there is nothing lane-nested to strip or carry"
|
||||
/// (04-interactions.md ▸ Drag and drop, resettled 2026-07-28), and the fallback only ever
|
||||
/// materializes a copy — a cut's move carries the real folder whole and never comes near this
|
||||
/// array. So the embedded set is exactly what a fallback paste should produce.
|
||||
public var cards: [Card]
|
||||
|
||||
/// One card inside a copied lane.
|
||||
|
||||
@@ -38,7 +38,7 @@ import os
|
||||
/// ⌘X stages, writes the pasteboard, and arms the source board's `transient.pendingCut` — the items
|
||||
/// dim in place. The cut is **armed** while the pasteboard still holds its `copyID`, the source store
|
||||
/// is still open, and the pending cut still names something; "deletion voids per item" needs no code
|
||||
/// here at all, because `TransientBoardState.resolve` already ejects a tombstoned or vanished member
|
||||
/// here at all, because `TransientBoardState.resolve` already ejects a member that moved to the trash or vanished
|
||||
/// on every reload, so "paste moves only the survivors" is the reload rule read at paste time.
|
||||
/// Voiding undims and downgrades the paste to a copy from staging.
|
||||
///
|
||||
|
||||
@@ -42,7 +42,7 @@ struct FutureCommand: View {
|
||||
///
|
||||
// m9-templates: copies the open board into the user templates store, close-flushed first exactly as
|
||||
// Duplicate is (09 ▸ Save as Template: "The copy is preceded by the close flush"), `.git` stripped,
|
||||
// tombstones dropped, a `template:` key stamped. Validation will be `acceptsBoardMutations` plus 09's
|
||||
// `.trash/` excluded, a `template:` key stamped. Validation will be `acceptsBoardMutations` plus 09's
|
||||
// one carve-out from the read-only lock — live under the unwritable-location state unless an open
|
||||
// Edit/raw-source session holds unsaved content — so it cannot simply borrow
|
||||
// `DuplicateBoardCommand`'s predicate outright.
|
||||
|
||||
Reference in New Issue
Block a user