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
+60
View File
@@ -143,6 +143,66 @@ enum Ranks: Sendable {
items.sorted { isOrderedForDisplay($0, before: $1, order: order, name: name) }
}
// MARK: - The trash's own order
/// The trash's order: **`modified` descending**, ties broken by title
/// (case-insensitive), then folder name (01-storage-format.md § Deletion,
/// re-ruled 2026-07-31, retiring the arrival rank mint; 03-board-ui.md §
/// Trash: "the trash sorts by `modified` descending Ties break by title
/// (case-insensitive), then folder name the deterministic tail").
///
/// **`order` is not consulted at all in here**, which is the whole of the
/// ruling: a delete is a pure folder move plus the stamp, and the item's
/// `order` key rides along untouched for its eventual restore. The trash is
/// the one container in the app whose sequence is not a rank sequence.
///
/// **An undated entry sorts after every dated one** the comment thread's
/// own rule for a missing `created` ("ties and missing/malformed sort
/// after dated siblings"), read one container over: a stamp the app always
/// writes is missing only on a hand-made or foreign entry, and a missing
/// value must never outrank a real deletion. The tail then separates them
/// exactly as it separates two equal stamps.
///
/// Titles compare with `localizedCaseInsensitiveCompare`, the same
/// comparison the rest of the app's user-facing sorting uses, and an
/// untitled entry compares as the empty string it sorts first among its
/// stamp-mates, which is deterministic and is all the tail owes.
static func isOrderedForTrash<T>(
_ lhs: T, before rhs: T,
modified: (T) -> Date?, title: (T) -> String?, name: (T) -> String
) -> Bool {
let lhsModified = modified(lhs)
let rhsModified = modified(rhs)
if lhsModified != rhsModified {
// Newest first; an absent stamp is older than every present one.
switch (lhsModified, rhsModified) {
case let (.some(left), .some(right)): return left > right
case (.some, .none): return true
case (.none, .some): return false
case (.none, .none): break
}
}
let lhsTitle = title(lhs) ?? ""
let rhsTitle = title(rhs) ?? ""
let comparison = lhsTitle.localizedCaseInsensitiveCompare(rhsTitle)
guard comparison == .orderedSame else { return comparison == .orderedAscending }
return name(lhs) < name(rhs)
}
/// Sorts the trash's entries into column order `isOrderedForTrash`'s rule,
/// applied by the loader to each kind's array and by `BoardModel.trashEntries`
/// to the merged sequence, so the two can never disagree about what "the row
/// below this one" is (03-board-ui.md § Trash: "The merged order is one
/// derivation").
static func sortedForTrash<T>(
_ items: [T],
modified: (T) -> Date?,
title: (T) -> String?,
name: (T) -> String
) -> [T] {
items.sorted { isOrderedForTrash($0, before: $1, modified: modified, title: title, name: name) }
}
// MARK: - Tombstone exclusion
/// `append(toVisible:)`, ignoring tombstoned siblings. Tombstones are