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
+49
View File
@@ -669,6 +669,55 @@ struct ClipboardAvailabilityTests {
== ItemReferenceSet(ids: [clipboardTrashedLane], container: .trash))
}
/// **The guard the trash's kind-blind selection moved to the exits** (04-interactions.md The
/// trash, ruled 2026-07-31): "the pasteboard's payload types are per-kind, so Cut and Copy grey
/// out via ordinary menu validation while a trash selection mixes kinds no failed gesture, no
/// beep".
///
/// It is also what keeps `ClipboardManifest.kind` honest: the manifest names one payload type,
/// and a set spanning both never reaches the capture.
@Test("A mixed trash selection greys out both Copy and Cut")
func mixedTrashSelectionClosesCopyAndCut() throws {
let harness = try makeTrashedLaneHarness()
defer { harness.tearDown() }
harness.store.transient.isTrashVisible = true
// Each kind alone is fine the selection is legal either way, and so is the gesture.
harness.store.select([clipboardCard3], in: .trash)
#expect(harness.clipboard.canCopy(from: harness.store))
harness.store.select([clipboardTrashedLane], in: .trash)
#expect(harness.clipboard.canCopy(from: harness.store))
// Together a selection the grammar now allows the exits close.
harness.store.select([clipboardCard3, clipboardTrashedLane], in: .trash)
#expect(harness.store.selection.ids.count == 2, "the selection itself is legal")
#expect(harness.clipboard.canCopy(from: harness.store) == false)
#expect(harness.clipboard.canCut(from: harness.store) == false)
// Delete is deliberately *not* gated: it works on a mixed selection, the alert counting both
// kinds (04 The trash).
#expect(TrashModel.canDelete(selection: harness.store.selection, in: harness.store.snapshot))
}
/// The live board's own mixed set cannot be built by any gesture but the predicate answers for
/// it anyway rather than assuming, so a future caller cannot smuggle one past the exits.
@Test("The mixed-kind predicate answers for the live board too")
func mixedKindPredicateCoversTheBoard() throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let snapshot = harness.store.snapshot
let laneID = try #require(snapshot.lanes.first?.id)
#expect(!SelectionGrammar.mixesKinds(
ItemReferenceSet(ids: [clipboardCard1], container: .board), in: snapshot))
#expect(SelectionGrammar.mixesKinds(
ItemReferenceSet(ids: [clipboardCard1, laneID], container: .board), in: snapshot))
// Rows the container no longer holds are ignored a ghost must not grey out a menu item.
#expect(!SelectionGrammar.mixesKinds(
ItemReferenceSet(ids: [clipboardCard1, ItemID(rawValue: Ident.indexless)], container: .board),
in: snapshot))
#expect(!SelectionGrammar.mixesKinds(.empty, in: snapshot))
}
@Test("The read-only lock blocks cut but never copy")
func lockBlocksCutOnly() throws {
let harness = try makeClipboardHarness()