Realign code with the 2026-07-31 rulings

The trash sorts by modified descending — the arrival rank mint retires
(Ranks.isOrderedForTrash one comparator, loader + merged order agree;
the legacy deleted: migration stamps modified from the tombstone
timestamp where parseable; delete undo steps validate existence-only;
agent guide v8). Trash selection goes kind-blind — ranges, marquee,
Select All, and the successor walk sweep both kinds; the guard moves to
the exits (mixed-payload drop refusal, copy/cut validation). The copy
stamping preflight widens back to comment depth (load-scoped posture —
the board always loads, the gesture refuses whole). Fixes a latent
no-op: trashed-lane drag restore never fired (DragSession.beginLanes
hard-coded the board container).

2403 tests in 413 suites green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-07-31 18:35:07 -04:00
parent 542ab169a3
commit bec75e4282
37 changed files with 1200 additions and 551 deletions
+21 -12
View File
@@ -118,16 +118,17 @@ public struct BoardModel: Sendable, Equatable {
/// **The container's other kind is `trashedLanes`** (re-ruled 2026-07-29 lanes trash too).
/// The two are separate arrays rather than one list of a sum type because they are separate
/// *things*: a trashed card is an ordinary card that every card-shaped surface already reads,
/// and a trashed lane is an opaque row that none of them may. Interleaving the two by trash
/// rank is a rendering question (03-board-ui.md § Trash: "lane rows and cards interleave in the
/// one trash column purely by trash rank"), and both arrays carry the `order` that answers it.
/// and a trashed lane is an opaque row that none of them may. Interleaving the two is a
/// rendering question (03-board-ui.md § Trash: "lane rows and cards interleave in the one trash
/// column by `modified` descending"), and both arrays carry the stamp that answers it
/// `BoardModel.trashEntries` is the one merge.
///
/// **Display order is `order` ascending, like any lane's cards** `Ranks.sortedForDisplay`,
/// same folder-name tie-break. Newest-first falls out of ordinary ranks rather than a
/// timestamp sort: every arrival mints a rank *above* the current topmost
/// (`Ranks.insertAtHead`), so the trash needs no sort rule of its own. There is deliberately
/// no `deleted:` key on anything in here a trashed card is an ordinary card in a special
/// place.
/// **Display order is `modified` descending** `Ranks.sortedForTrash`, tie-broken by title then
/// folder name (re-ruled 2026-07-31, retiring the arrival rank mint). The trash is the one
/// container in the app whose sequence is not a rank sequence: the delete move stamps, and that
/// stamp *is* the position, with each entry's `order` riding along untouched for its restore.
/// There is deliberately no `deleted:` key on anything in here a trashed card is an ordinary
/// card in a special place.
///
/// Empty when `.trash/` is absent (the overwhelmingly common case the folder is minted by
/// the first delete), and empty when it holds nothing the loader recognizes as a card.
@@ -281,9 +282,17 @@ public struct TrashedLane: Identifiable, Sendable, Equatable {
public let schema: Int
public let title: FieldValue<String>
/// Rank within the trash, ascending = top to bottom the same required, strictly validated
/// field a live lane carries (`Lane.order`), and what interleaves this row among the trash's
/// cards. Newest-first falls out of it: every arrival mints a rank above the current topmost.
/// **When the row entered the trash** and therefore *where it sits*: the trash sorts by
/// `modified` descending (01-storage-format.md § Deletion, re-ruled 2026-07-31), and the trash
/// move is the container-changing write that stamps it. Missing on a foreign mover that skipped
/// the restamp, which sorts it below every dated sibling (`Ranks.isOrderedForTrash`).
public let modified: FieldValue<Date>
/// The lane's rank **among the board's lanes**, riding along untouched the trash move rewrites
/// no `order` at all, so this is still the strip position a restore would want and the value the
/// undo of a restore puts back. It is deliberately *not* what orders this row in the column
/// (`modified` is), and the same required, strictly validated field a live lane carries
/// (`Lane.order`).
public let order: Double
/// **How many cards the lane is holding** the row's whole other half ("Doing 5 cards").