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
+10 -13
View File
@@ -184,27 +184,25 @@ struct ImportAttachmentsToCardTests {
#expect(store.banners.oneShots.isEmpty)
}
@Test("A tombstoned, ancestor-tombstoned, or vanished target writes nothing")
@Test("A trashed or vanished target writes nothing")
func inertTargets() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let sources = try DropSources()
defer { sources.tearDown() }
// A tombstoned card, and a live card under a tombstoned lane effective liveness is
// ancestor-walked, so both render nowhere and both are inert.
try fixture.item("\(Ident.lane1)/\(Ident.card2)", tombstoned(order: "2048", title: "Second"))
try fixture.item(Ident.lane3, tombstoned(order: "3072", title: "Gone"))
try fixture.item("\(Ident.lane3)/\(Ident.card4)", Item.rich(order: "1024", title: "Buried"))
// "Finder file drops on trash cards are inert" (04 The trash), and a card whose lane was
// deleted is simply not there.
try fixture.move("\(Ident.lane1)/\(Ident.card2)", toTrash: Ident.card2)
let store = try BoardStore(rootURL: fixture.root)
let shot = try sources.file("shot.png")
store.importAttachments([shot], toCard: card2) // tombstoned card
store.importAttachments([shot], toCard: ItemID(rawValue: Ident.card4)) // under a tombstoned lane
store.importAttachments([shot], toCard: card2) // a trash card
store.importAttachments([shot], toCard: ItemID(rawValue: Ident.card4)) // its lane is gone
store.importAttachments([shot], toCard: ItemID(rawValue: Ident.indexless)) // no such card
store.importAttachments([shot], toCard: lane1) // a lane, not a card
store.importAttachments([], toCard: card1) // nothing dropped
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card2)/attachments"))
#expect(!fixture.exists(".trash/\(Ident.card2)/attachments"))
#expect(!fixture.exists("\(Ident.lane3)/\(Ident.card4)/attachments"))
#expect(!fixture.exists("\(Ident.lane1)/attachments"))
#expect(!fixture.exists("\(Ident.lane1)/\(Ident.card1)/attachments"))
@@ -387,21 +385,20 @@ struct CreateCardsFromFilesTests {
#expect(store.banners.oneShots.isEmpty)
}
@Test("A tombstoned, vanished, or empty destination writes nothing")
@Test("A deleted, vanished, or empty destination writes nothing")
func inertDestinations() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let sources = try DropSources()
defer { sources.tearDown() }
try fixture.item(Ident.lane3, tombstoned(order: "3072", title: "Gone"))
let store = try BoardStore(rootURL: fixture.root)
let shot = try sources.file("shot.png")
store.createCards(fromFiles: [shot], inLane: lane3, at: 0) // tombstoned
store.createCards(fromFiles: [shot], inLane: lane3, at: 0) // no such lane
store.createCards(fromFiles: [shot], inLane: ItemID(rawValue: Ident.indexless), at: 0) // no such lane
store.createCards(fromFiles: [], inLane: lane1, at: 0) // nothing dropped
#expect(try fixture.entryNames(Ident.lane3) == ["index.md"])
#expect(!fixture.exists(Ident.lane3))
#expect(try titles(lane1, in: fixture) == ["First", "Second", "Third"])
#expect(store.banners.oneShots.isEmpty)
}