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
+41 -11
View File
@@ -60,12 +60,11 @@ enum TrashDrop {
/// The row the shadow takes, always: **the topmost**.
///
/// Not arbitrary, and the ranks are what make it honest: "every trash arrival mints a rank
/// above the current top" (04-interactions.md The trash), so a fresh delete genuinely lands on
/// top. The drop
/// therefore still lands exactly where the shadow shows the one positional promise every other
/// drop in this app makes while being the only proposal on the board the *pointer* does not
/// choose.
/// Not arbitrary, and the stamp is what makes it honest: "every trash arrival stamps `modified`
/// and the trash sorts newest-first by that stamp" (04-interactions.md The trash, re-ruled
/// 2026-07-31), so a fresh delete genuinely lands on top. The drop therefore still lands exactly
/// where the shadow shows the one positional promise every other drop in this app makes
/// while being the only proposal on the board the *pointer* does not choose.
static let landingIndex = 0
/// Whether the shown trash takes this session the whole of the gate, and every clause is a
@@ -284,6 +283,18 @@ final class DragSession {
/// and that is the whole of what makes it a restore (04-interactions.md The trash).
private(set) var container: ItemContainer = .board
/// **Whether the selection this drag was picked up from spanned both kinds** only ever true in
/// the trash, whose selection went kind-blind (04-interactions.md The trash, ruled 2026-07-31).
///
/// A session carries items of exactly one `kind` the pasteboard type is per-kind and a
/// `DragPayload` names one so a mixed selection dragged from a trash row starts a session over
/// *its* kind and silently leaves the other behind. That silence is what this flag exists to
/// prevent: "pickup is allowed the selection is legal but every out-of-trash drop target
/// refuses the mixed payload, and the release surfaces a notice explaining the rule". The refusal
/// is at the commit (`BoardDropContext.commitDrop`), where every drop out of the trash funnels,
/// rather than at hover 04 puts the explanation at the release.
private(set) var mixesKinds = false
/// The dragged items in **flatten order** the order they will land in.
private(set) var members: [ItemID] = []
@@ -496,16 +507,33 @@ final class DragSession {
folders: [URL],
heights: [CGFloat],
container: ItemContainer,
source: BoardStore
source: BoardStore,
mixesKinds: Bool = false
) {
begin(kind: .cards, members: members, folders: folders, container: container, source: source)
begin(
kind: .cards, members: members, folders: folders,
container: container, source: source, mixesKinds: mixesKinds
)
cardHeights = heights
laneUnits = []
}
/// Begins a lane session.
func beginLanes(_ members: [ItemID], folders: [URL], units: [Int], source: BoardStore) {
begin(kind: .lanes, members: members, folders: folders, container: .board, source: source)
///
/// `container` is a parameter because a **trashed lane row**'s drag is a `.lanes` session in
/// `.trash` the restore at the lane level (04-interactions.md The trash Drag-to-restore).
func beginLanes(
_ members: [ItemID],
folders: [URL],
units: [Int],
container: ItemContainer = .board,
source: BoardStore,
mixesKinds: Bool = false
) {
begin(
kind: .lanes, members: members, folders: folders,
container: container, source: source, mixesKinds: mixesKinds
)
laneUnits = units
cardHeights = []
}
@@ -515,7 +543,8 @@ final class DragSession {
members: [ItemID],
folders: [URL],
container: ItemContainer,
source: BoardStore
source: BoardStore,
mixesKinds: Bool
) {
endHold()
self.kind = kind
@@ -523,6 +552,7 @@ final class DragSession {
self.memberSet = Set(members)
self.folders = folders
self.container = container
self.mixesKinds = mixesKinds
self.sourceStore = source
self.sourceRoot = source.rootURL
self.proposal = nil