Materialize the trash — store, undo, and the container universe

Phase 2 swaps every consumer: Liveness and its ancestor walk are gone,
replaced by ItemContainer — a UUID set plus the container side it
lives on, presence the whole test, one selection boundary instead of
the old liveness law. Deletion stages by place: board cards move to
the trash at a store-minted head rank, trash-side delete is permanent
behind its confirmation, Delete Immediately skips the trash from
anywhere, lane delete captures the subtree and removes the folder.
Restore has no method at all — moveCards resolves members in either
container, so drag-out and cut-paste are the ordinary moves 13 calls
them, registering ordinary Move steps. The delete inverse moves the
card back to its captured lane and rank; redo replays the captured
trash rank, a value the gesture actually wrote; lane undo recreates
the subtree byte-faithfully in session. Purges register nothing —
where 13's trash section contradicts its own Rules on that, Rules
wins, filed for ruling. Staleness collapsed to present-or-absent: a
container is a path, so a foreign restore fails the delete step's
expectation structurally. Legacy tombstones migrate on the loose-file
tail hook, cards oldest-first so minting above top reproduces the
retired newest-first column, lanes returning live, one folded loss
row naming both directions. Put Back, restoreByDrag,
receiveRestoredCards, TrashEntry, and the kind machinery are deleted;
the trash column renders the container correctly with its full face
rework left to phase 3.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 17:47:56 -04:00
parent 16c10d61c3
commit 53bc71f7fb
53 changed files with 3459 additions and 3655 deletions
+84 -93
View File
@@ -61,9 +61,9 @@ struct PasteFromStagingTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
@@ -85,9 +85,9 @@ struct PasteFromStagingTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
@@ -107,9 +107,9 @@ struct PasteFromStagingTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(clipboardLane1, in: harness.fixture) == ["First", "Second"])
@@ -123,13 +123,13 @@ struct PasteFromStagingTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
target.handleWatcherEvent(.treeChanged(.appMediated))
await target.awaitQuiescence()
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let ids = try pastedIDs(destinationLane, in: destination)
@@ -142,7 +142,7 @@ struct PasteFromStagingTests {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: harness.store)?.value
@@ -159,15 +159,15 @@ struct PasteFromStagingTests {
@Suite("Paste ▸ lanes")
struct PasteLaneTests {
@Test("A pasted lane copy takes fresh GUIDs throughout and strips tombstoned cards")
func laneCopyStripsTombstones() async throws {
@Test("A pasted lane copy takes fresh GUIDs throughout and carries exactly its cards")
func laneCopyRemintsThroughout() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
@@ -176,12 +176,11 @@ struct PasteLaneTests {
let arrived = try #require(model.lanes.last)
#expect(arrived.id.rawValue != Ident.lane1)
#expect(arrived.title.value == "Todo")
// The tombstoned card is gone "the copy transfers content, and trash isn't content".
// "A lane carries exactly its cards the trash is board-level, so there is nothing
// lane-nested to strip or carry" (04 Drag and drop, resettled 2026-07-28).
#expect(arrived.cards.count == 2)
#expect(arrived.cards.allSatisfy { !$0.isDeleted })
#expect(arrived.cards.map(\.id.rawValue).allSatisfy { $0 != Ident.card1 && $0 != Ident.card2 })
// The tombstoned original is still recoverable where it always was.
#expect(try lane(clipboardLane1, in: harness.fixture)?.cards.count == 3)
#expect(try pasted(harness.fixture).trash.count == 1, "and the source's trash is untouched")
}
@Test("A lane paste with nothing selected lands at the board's right end")
@@ -192,7 +191,7 @@ struct PasteLaneTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
@@ -208,30 +207,30 @@ struct PasteLaneTests {
try empty.item("", Item.board)
let target = try BoardStore(rootURL: empty.root)
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
#expect(try pasted(empty).lanes.compactMap(\.title.value) == ["Todo"])
}
@Test("A lane cut-move carries its tombstoned cards whole")
func laneCutMoveCarriesTombstones() async throws {
@Test("A lane cut-move carries its cards whole, identity and all")
func laneCutMoveCarriesItsCards() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.cut(from: harness.store)
await harness.clipboard.paste(into: target)?.value
let model = try pasted(destination)
let arrived = try #require(model.lanes.first { $0.id == clipboardLane1 })
// Identity travelled, and the tombstone landed in the destination's trash.
#expect(arrived.cards.count == 3)
#expect(arrived.cards.contains { $0.isDeleted })
// Identity travelled, and every card came with it nothing to strip.
#expect(arrived.cards.count == 2)
#expect(model.trash.isEmpty, "the source board's trash is board-level and stays there")
// The lane left the source board entirely.
#expect(try pasted(harness.fixture).lanes.map(\.id) == [clipboardLane2])
}
@@ -241,7 +240,7 @@ struct PasteLaneTests {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: harness.store)?.value
@@ -259,8 +258,8 @@ struct PasteLaneTests {
@Suite("Paste ▸ from the trash")
struct PasteFromTrashTests {
@Test("A card copied out of the trash arrives live")
func cardArrivesLive() async throws {
@Test("A card copied out of the trash is an ordinary copy of an ordinary card")
func cardCopiesOutOrdinarily() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
@@ -268,46 +267,39 @@ struct PasteFromTrashTests {
let target = try BoardStore(rootURL: destination.root)
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardCard3], liveness: .trashed)
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Trashed"])
// And the tombstoned original stays in the source trash copy-out, never a move.
#expect(try lane(clipboardLane1, in: harness.fixture)?
.cards.first { $0.id == clipboardCard3 }?.isDeleted == true)
// The original stays in the source trash a copy is a copy (04 The trash: "C copies a
// trash card; a live copy lands wherever pasted, like copying out of Finder's Trash").
#expect(try pasted(harness.fixture).trash.map(\.id) == [clipboardCard3])
// And nothing had to be stripped on arrival: a trashed card carries no key at all.
let landed = try #require(try lane(destinationLane, in: destination)?.cards.last)
#expect(landed.deleted.isMissing)
}
@Test("A lane entry copied out of the trash arrives live, its tombstoned interior cards stripped")
func laneEntryStripsBothWays() async throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
// A tombstoned lane holding one plain card and one that carries its own tombstone.
try fixture.item(Ident.lane1, tombstonedItem(order: "1024", title: "Archive"))
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "Kept"))
try fixture.item("\(Ident.lane1)/\(Ident.card2)", tombstonedItem(order: "2048", title: "Gone"))
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
let harness = try ClipboardHarness(fixture: fixture)
defer { try? FileManager.default.removeItem(at: harness.staging) }
let destination = try makeDestination()
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
/// **Cut in the trash, paste into a lane, is the keyboard-native restore** (04-interactions.md
/// The trash, resettled 2026-07-28) "an ordinary folder move", which is exactly what the
/// armed cut already does.
@Test("Cut in the trash and paste into a lane is the keyboard restore — the folder moves")
func cutFromTheTrashIsTheKeyboardRestore() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardLane1], liveness: .trashed)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.paste(into: target)?.value
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.cut(from: harness.store)
harness.store.select([clipboardLane2], in: .board)
await harness.clipboard.paste(into: harness.store)?.value
let model = try pasted(destination)
let arrived = try #require(model.lanes.last)
#expect(arrived.isDeleted == false)
#expect(arrived.title.value == "Archive")
#expect(arrived.cards.count == 1)
#expect(arrived.cards.first?.title.value == "Kept")
#expect(arrived.cards.first?.isDeleted == false)
let model = try pasted(harness.fixture)
#expect(model.trash.isEmpty, "the folder left the trash")
let lane = try #require(model.lanes.first { $0.id == clipboardLane2 })
#expect(lane.cards.map(\.id).contains(clipboardCard3), "identity travels — it is a move")
#expect(harness.store.transient.pendingCut.isEmpty, "the cut was consumed")
}
}
@@ -337,9 +329,9 @@ struct PasteSearchAndStalenessTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
// "First" would be hidden by this query exactly the card that must not arrive invisible.
target.searchQuery = "resident"
await harness.clipboard.paste(into: target)?.value
@@ -356,7 +348,7 @@ struct PasteSearchAndStalenessTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane2], liveness: .live)
harness.store.select([clipboardLane2], in: .board)
harness.clipboard.copy(from: harness.store)
target.searchQuery = "resident"
await harness.clipboard.paste(into: target)?.value
@@ -373,10 +365,10 @@ struct PasteSearchAndStalenessTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
harness.pasteboard.takeOver()
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
target.searchQuery = "resident"
// `refresh()` at the front of the paste sees the moved changeCount, so there is no payload
@@ -395,9 +387,9 @@ struct PasteSearchAndStalenessTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
target.searchQuery = "resident"
// The gesture passed validation; the takeover lands while the task is still waiting on the
@@ -427,9 +419,9 @@ struct PasteCutTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// Identity travelled.
@@ -450,13 +442,13 @@ struct PasteCutTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
target.handleWatcherEvent(.treeChanged(.appMediated))
await target.awaitQuiescence()
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let ids = try pastedIDs(destinationLane, in: destination)
@@ -474,12 +466,12 @@ struct PasteCutTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
// The source board closes its store goes, and with it the cut's arming.
harness.store.transient.pendingCut = .empty
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// A copy: a fresh identity at the destination, and the original still at home.
@@ -496,22 +488,21 @@ struct PasteCutTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1, clipboardCard2], liveness: .live)
harness.store.select([clipboardCard1, clipboardCard2], in: .board)
harness.clipboard.cut(from: harness.store)
// One of the two is tombstoned before the paste: the reload ejects it from the pending cut.
// One of the two is deleted before the paste: the reload ejects it from the pending cut.
harness.store.delete([clipboardCard1])
harness.store.handleWatcherEvent(.treeChanged(.appMediated))
await harness.store.awaitQuiescence()
#expect(harness.store.transient.pendingCut.ids == [clipboardCard2])
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedIDs(destinationLane, in: destination) == [Ident.indexless, Ident.card2])
// The tombstoned one stayed behind, in the source board's trash.
#expect(try lane(clipboardLane1, in: harness.fixture)?
.cards.first { $0.id == clipboardCard1 }?.isDeleted == true)
// The deleted one stayed behind, in the source board's trash.
#expect(try pasted(harness.fixture).trash.map(\.id).contains(clipboardCard1))
}
@Test("A cut emptied down to nothing is simply void — a paste copies instead")
@@ -522,14 +513,14 @@ struct PasteCutTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.cut(from: harness.store)
harness.store.delete([clipboardCard1])
harness.store.handleWatcherEvent(.treeChanged(.appMediated))
await harness.store.awaitQuiescence()
#expect(harness.store.transient.pendingCut.isEmpty)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
// The staged snapshot is still there, so the paste is a copy content intact.
@@ -551,7 +542,7 @@ struct PasteFallbackTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
// The snapshot goes a swept tree, a full disk, an unreadable container.
@@ -560,7 +551,7 @@ struct PasteFallbackTests {
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
let arrived = try #require(try pastedIDs(destinationLane, in: destination).last)
@@ -585,7 +576,7 @@ struct PasteFallbackTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardCard1], liveness: .live)
harness.store.select([clipboardCard1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
@@ -593,7 +584,7 @@ struct PasteFallbackTests {
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(target.banners.losses.map(\.message) == ["Pasted 'First' without its 2 attachments"])
@@ -608,7 +599,7 @@ struct PasteFallbackTests {
let target = try BoardStore(rootURL: destination.root)
// `card2` has no attachments, so a fallback loses nothing at all.
harness.store.select([clipboardCard2], liveness: .live)
harness.store.select([clipboardCard2], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
@@ -616,7 +607,7 @@ struct PasteFallbackTests {
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Second"])
@@ -631,7 +622,7 @@ struct PasteFallbackTests {
defer { destination.tearDown() }
let target = try BoardStore(rootURL: destination.root)
harness.store.select([clipboardLane1], liveness: .live)
harness.store.select([clipboardLane1], in: .board)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
@@ -643,14 +634,14 @@ struct PasteFallbackTests {
let arrived = try #require(try pasted(destination).lanes.last)
#expect(arrived.title.value == "Todo")
// The two live cards, and not the tombstoned third.
// Both of the lane's cards.
#expect(arrived.cards.count == 2)
#expect(Set(arrived.cards.compactMap(\.title.value)) == ["First", "Second"])
#expect(target.banners.losses.map(\.message) == ["Pasted 'Todo' without its 2 attachments"])
}
@Test("A trash-sourced fallback still strips `deleted:` at materialization")
func trashedFallbackStripsDeleted() async throws {
@Test("A trash-sourced fallback materializes an ordinary card — there is no key to strip")
func trashedFallbackIsOrdinary() async throws {
let harness = try makeClipboardHarness()
defer { harness.tearDown() }
let destination = try makeDestination()
@@ -658,7 +649,7 @@ struct PasteFallbackTests {
let target = try BoardStore(rootURL: destination.root)
harness.store.transient.isTrashVisible = true
harness.store.select([clipboardCard3], liveness: .trashed)
harness.store.select([clipboardCard3], in: .trash)
harness.clipboard.copy(from: harness.store)
await harness.clipboard.stagingSettled()
let copyID = try #require(harness.clipboard.payload?.copyID)
@@ -666,7 +657,7 @@ struct PasteFallbackTests {
at: harness.staging.appendingPathComponent(copyID, isDirectory: true)
)
target.select([destinationLane], liveness: .live)
target.select([destinationLane], in: .board)
await harness.clipboard.paste(into: target)?.value
#expect(try pastedTitles(destinationLane, in: destination) == ["Resident", "Trashed"])