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
+37 -15
View File
@@ -76,12 +76,12 @@ extension ItemContainer {
// MARK: - The trash's two kinds, in one order
/// One row of the trash column a card, or a trashed lane's opaque unit (03-board-ui.md § Trash,
/// re-ruled 2026-07-29: "Lane rows and cards interleave in the one trash column purely by trash
/// rank").
/// re-ruled 2026-07-31: "Lane rows and cards interleave in the one trash column by `modified`
/// descending").
///
/// **It exists so the interleave is written once.** The container's two kinds are two arrays on the
/// snapshot, for the reason `BoardModel.trash` states a trashed card is an ordinary card every
/// card-shaped surface already reads, a trashed lane is an opaque row none of them may but *rank
/// card-shaped surface already reads, a trashed lane is an opaque row none of them may but *column
/// order* is a question about the container as a whole, and it is asked by the column that draws the
/// rows, by the grammar that ranges and navigates over them, and by the path resolver that batches
/// them. Three merges would be three chances to disagree about what "the row below this one" is.
@@ -100,8 +100,28 @@ public enum TrashEntry: Identifiable, Sendable, Equatable {
}
}
/// The rank that decides where this row sits among the others the one field both kinds carry
/// for the same purpose.
/// **The stamp that decides where this row sits among the others** the trash sorts by
/// `modified` descending (01-storage-format.md § Deletion, re-ruled 2026-07-31), and the trash
/// move is what writes it. `nil` for an entry whose mover skipped the restamp, which sorts below
/// every dated sibling (`Ranks.isOrderedForTrash`).
public var modified: Date? {
switch self {
case let .card(card): card.modified.value
case let .lane(lane): lane.modified.value
}
}
/// The first tie-break's key the title as the row draws it, `nil` when the entry has none.
public var title: String? {
switch self {
case let .card(card): card.title.value
case let .lane(lane): lane.title.value
}
}
/// The rank the entry is *carrying*, untouched by the trash move its position among the lane
/// (or board) siblings it left, which a restore returns it to. Deliberately **not** what orders
/// this row: the trash is sorted by `modified` (above).
public var order: Double {
switch self {
case let .card(card): card.order
@@ -131,15 +151,17 @@ public enum TrashEntry: Identifiable, Sendable, Equatable {
extension BoardModel {
/// The trash's rows, top to bottom **the container's one order**, both kinds interleaved by
/// rank (03-board-ui.md § Trash).
/// `modified` descending (03-board-ui.md § Trash, re-ruled 2026-07-31: "The merged order is one
/// derivation a second implementation of 'the row below this one' is a bug by definition").
///
/// The tie-break is the folder name's, `Ranks.sortedForDisplay`'s own, which is what the loader
/// already applied within each kind: two rows minted the same rank by two writers order the same
/// way twice.
/// The tail is `Ranks.isOrderedForTrash`'s title case-insensitively, then folder name which
/// is what the loader already applied within each kind, so the merge of two sorted arrays and
/// each array alone agree everywhere they overlap.
public var trashEntries: [TrashEntry] {
Ranks.sortedForDisplay(
Ranks.sortedForTrash(
trash.map(TrashEntry.card) + trashedLanes.map(TrashEntry.lane),
order: \.order,
modified: \.modified,
title: \.title,
name: { $0.id.rawValue }
)
}
@@ -230,10 +252,10 @@ extension ItemPath {
/// fails partway must fail the same way twice (`BoardStore.styleSubjects` makes the same choice
/// for the same reason).
///
/// **The trash's order interleaves its two kinds by rank** (03-board-ui.md § Trash: "lane rows
/// and cards interleave in the one trash column purely by trash rank"), which is why the walk is
/// `trashEntries` rather than the two arrays concatenated: the column's order is the batch's
/// order, and it is stated in exactly one place.
/// **The trash's order interleaves its two kinds by `modified`** (03-board-ui.md § Trash: "lane
/// rows and cards interleave in the one trash column by `modified` descending"), which is why the
/// walk is `trashEntries` rather than the two arrays concatenated: the column's order is the
/// batch's order, and it is stated in exactly one place.
///
/// Ids the container does not hold are simply absent, which is every caller's standing posture:
/// a selection the next reload will drop writes nothing rather than being refused.