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
+151 -108
View File
@@ -18,7 +18,10 @@ import Testing
// MARK: - Fixtures
private func tombstoned(order: String, title: String, deleted: String = "2026-03-03T09:00:00Z") -> String {
/// A card sitting in `<root>/.trash/` an ordinary card in a special place (03-board-ui.md §
/// Trash), with the unknown-key overlay every other fixture card carries so an inverse's
/// verbatim-preservation claim has something to preserve.
private func trashResident(order: String, title: String) -> String {
"""
---
schema: 1
@@ -26,7 +29,6 @@ private func tombstoned(order: String, title: String, deleted: String = "2026-03
order: \(order)
project: lanework # agent overlay
created: 2026-01-01T09:00:00Z
deleted: \(deleted)
---
\(title) body.
@@ -47,8 +49,8 @@ Styled body.
"""
/// Two live lanes the first with two live cards, a styled one and a tombstoned one; the second
/// with one card. Enough for every inverse in this file.
/// Two lanes the first with two plain cards and a styled one, the second with one card plus one
/// card already sitting in the board's `.trash/`. Enough for every inverse in this file.
@MainActor
private func makeBoard() throws -> WriterFixture {
let fixture = try WriterFixture()
@@ -57,9 +59,9 @@ private func makeBoard() throws -> WriterFixture {
try fixture.item("\(Ident.lane1)/\(Ident.card1)", Item.rich(order: "1024", title: "First"))
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
try fixture.item("\(Ident.lane1)/\(Ident.card3)", styledCard)
try fixture.item("\(Ident.lane1)/\(Ident.card4)", tombstoned(order: "4096", title: "Trashed"))
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
try fixture.item("\(Ident.lane2)/\(Ident.indexless)", Item.rich(order: "1024", title: "Elsewhere"))
try fixture.item(".trash/\(Ident.card4)", trashResident(order: "1024", title: "Trashed"))
return fixture
}
@@ -74,7 +76,7 @@ private let elsewhere = ItemID(rawValue: Ident.indexless)
private let card1Path = "\(Ident.lane1)/\(Ident.card1)"
private let card2Path = "\(Ident.lane1)/\(Ident.card2)"
private let card3Path = "\(Ident.lane1)/\(Ident.card3)"
private let trashedPath = "\(Ident.lane1)/\(Ident.card4)"
private let trashedPath = ".trash/\(Ident.card4)"
/// A store with a stack behind it. The provider is returned because `BoardStore.history` is **weak**
/// the session owns the stack in the app, and a test that dropped it would watch its own steps
@@ -488,7 +490,7 @@ struct MoveUndoTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.select([card1], liveness: .live)
store.select([card1], in: .board)
store.sortSelection(.down)
@@ -506,13 +508,13 @@ struct MoveUndoTests {
}
}
// MARK: - The trash pair
// MARK: - Delete
@MainActor
@Suite("Undo ▸ the trash pair")
@Suite("Undo ▸ delete")
struct TrashUndoTests {
@Test("Undoing a delete is Put Back — the key goes, and nothing else moves")
@Test("Undoing a card delete moves it back out of the trash, to its lane and its rank")
func deleteRoundTrip() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
@@ -520,20 +522,42 @@ struct TrashUndoTests {
let before = try fixture.indexText(card1Path)
store.delete([card1])
#expect(try document(fixture, card1Path).deleted.value != nil)
#expect(!fixture.exists(card1Path), "the folder physically left its lane")
#expect(fixture.exists(".trash/\(Ident.card1)"))
#expect(history.undoActionName == "Delete Card")
history.undo()
#expect(fixture.exists(card1Path), "13 ▸ Interaction with the trash: the undo is the move back")
#expect(!fixture.exists(".trash/\(Ident.card1)"))
let undone = try fixture.indexText(card1Path)
#expect(try FrontmatterDocument.parse(undone).deleted.isMissing)
// Byte-identical but for the stamp: the tombstone and its inverse are one key each.
#expect(untouchedLines(undone).filter { !$0.hasPrefix("deleted:") } == untouchedLines(before))
#expect(try FrontmatterDocument.parse(undone).order.value == 1024, "at its original rank")
// Byte-identical but for the stamps every app write owns no `deleted:` key was ever
// written, so there is none to come back and none to remove.
#expect(untouchedLines(undone) == untouchedLines(before))
history.redo()
#expect(try document(fixture, card1Path).deleted.value != nil)
#expect(fixture.exists(".trash/\(Ident.card1)"))
#expect(!fixture.exists(card1Path))
}
@Test("A multi-item delete is one step with a plural title")
@Test("The redo files the card under the rank the delete minted, not a fresh one")
func redoUsesTheCapturedTrashRank() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.delete([card1])
let minted = try #require(try document(fixture, ".trash/\(Ident.card1)").order.value)
#expect(minted < 1024, "entry is at the top: a rank above the current topmost (order 1024)")
history.undo()
history.redo()
#expect(try document(fixture, ".trash/\(Ident.card1)").order.value == minted,
"the redo replays the write's own captured rank")
}
@Test("A multi-card delete is one step with a plural title, and lands newest-last on top")
func batchDeleteIsOneStep() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
@@ -542,69 +566,74 @@ struct TrashUndoTests {
store.delete([card1, card2])
#expect(history.undoActionName == "Delete 2 Cards")
let first = try #require(try document(fixture, ".trash/\(Ident.card1)").order.value)
let second = try #require(try document(fixture, ".trash/\(Ident.card2)").order.value)
#expect(second < first, "each arrival in the run mints a rank above the one before it")
history.undo()
#expect(try document(fixture, card1Path).deleted.isMissing)
#expect(try document(fixture, card2Path).deleted.isMissing)
#expect(fixture.exists(card1Path))
#expect(fixture.exists(card2Path))
#expect(try loadedTrash(fixture).map(\.id) == [trashed], "only the board's own resident is left")
#expect(history.canUndo == false)
}
@Test("A lane delete is named for the lane")
func laneDeleteIsNamedForTheLane() throws {
@Test("A lane delete is physical, and its undo recreates the folder byte for byte")
func laneDeleteRecreatesTheSubtree() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
let laneText = try fixture.indexText(Ident.lane2)
let cardText = try fixture.indexText("\(Ident.lane2)/\(Ident.indexless)")
store.delete([lane2])
#expect(!fixture.exists(Ident.lane2), "03 ▸ Trash: deleting a lane deletes it, physically")
#expect(try loadedTrash(fixture).map(\.id) == [trashed], "lanes are never trashed")
#expect(history.undoActionName == "Delete Lane")
history.undo()
#expect(try document(fixture, Ident.lane2).deleted.isMissing)
#expect(fixture.exists(Ident.lane2))
#expect(try fixture.indexText(Ident.lane2) == laneText, "the capture replays bytes, it does not edit")
#expect(try fixture.indexText("\(Ident.lane2)/\(Ident.indexless)") == cardText,
"and the whole subtree comes back with it — nested cards included")
history.redo()
#expect(!fixture.exists(Ident.lane2))
}
@Test("Undoing a Put Back re-tombstones with the timestamp the row was filed under")
func putBackRoundTrip() throws {
@Test("A restore-by-move-out registers as an ordinary Move, with the ordinary move inverse")
func restoreIsAnOrdinaryMove() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
let filedUnder = try #require(try document(fixture, trashedPath).deleted.value)
let trashRank = try #require(try document(fixture, trashedPath).order.value)
store.putBack([trashed])
#expect(try document(fixture, trashedPath).deleted.isMissing)
#expect(history.undoActionName == "Restore Card")
store.moveCards([trashed], toLane: lane2, at: 0)
history.undo()
#expect(try document(fixture, trashedPath).deleted.value == filedUnder,
"the trash sorts by this — a fresh stamp would reorder a list the user was reading")
history.redo()
#expect(try document(fixture, trashedPath).deleted.isMissing)
}
@Test("Drag-to-restore undoes the position half too")
func restoreByDragRoundTrip() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
let filedUnder = try #require(try document(fixture, trashedPath).deleted.value)
store.restoreByDrag(cardID: trashed, intoLane: lane2, at: 0)
#expect(fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
#expect(try document(fixture, "\(Ident.lane2)/\(Ident.card4)").deleted.isMissing)
#expect(history.undoActionName == "Restore Card")
#expect(!fixture.exists(trashedPath))
#expect(history.undoActionName == "Move Card",
"13: a restore is an ordinary move between containers, named as one")
history.undo()
#expect(fixture.exists(trashedPath), "back in the lane it was trashed in")
#expect(fixture.exists("\(Ident.lane2)/\(Ident.card4)") == false)
let undone = try document(fixture, trashedPath)
#expect(undone.deleted.value == filedUnder)
#expect(undone.order.value == 4096, "at the rank it was trashed holding")
#expect(fixture.exists(trashedPath), "back in the trash it came out of")
#expect(!fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
#expect(try document(fixture, trashedPath).order.value == trashRank,
"at the rank it was filed under")
history.redo()
#expect(fixture.exists("\(Ident.lane2)/\(Ident.card4)"))
#expect(try document(fixture, "\(Ident.lane2)/\(Ident.card4)").deleted.isMissing)
}
}
/// The board's trash as the loader reads it never the store's snapshot, which a write deliberately
/// does not touch.
@MainActor
private func loadedTrash(_ fixture: WriterFixture) throws -> [Card] {
try BoardLoader.load(boardRoot: fixture.root).model.trash
}
// MARK: - The Edit session
@MainActor
@@ -703,6 +732,7 @@ struct NotUndoableTests {
store.delete([card1])
let armed = try #require(history.undoActionName)
store.select([trashed], in: .trash)
store.deleteImmediately([trashed])
#expect(fixture.exists(trashedPath) == false)
@@ -758,7 +788,7 @@ struct NotUndoableTests {
store.delete([card1])
store.setLaneWidth(lane1, units: 2)
#expect(try document(fixture, card1Path).deleted.value != nil)
#expect(fixture.exists(".trash/\(Ident.card1)"))
#expect(try document(fixture, Ident.lane1).width.value == 2)
#expect(store.banners.oneShots.isEmpty)
}
@@ -803,9 +833,9 @@ struct CrossingIsAWriteTests {
store.delete([card1])
for _ in 0 ..< 3 {
history.undo()
#expect(try document(fixture, card1Path).deleted.isMissing)
#expect(fixture.exists(card1Path))
history.redo()
#expect(try document(fixture, card1Path).deleted.value != nil)
#expect(fixture.exists(".trash/\(Ident.card1)"))
}
}
}
@@ -840,8 +870,15 @@ private enum Foreign {
_ = try BoardWriter.writeBody(inItemFolder: fixture.url(path), body: body)
}
static func delete(_ fixture: WriterFixture, _ path: String) throws {
try BoardWriter.deleteItem(at: fixture.url(path))
/// A foreign delete of a **card** the shape a delete has on disk now: the folder moves into
/// `.trash/`. `FileManager` and nothing else, so no rank is minted and no stamp is written.
static func trash(_ fixture: WriterFixture, _ path: String, id: String) throws {
try fixture.move(path, toTrash: id)
}
/// A foreign delete of a **lane** physical, since lanes are never trashed.
static func removeLane(_ fixture: WriterFixture, _ path: String) throws {
try FileManager.default.removeItem(at: fixture.url(path))
}
static func purge(_ fixture: WriterFixture, _ path: String) throws {
@@ -886,12 +923,13 @@ struct StaleStepTests {
store.transient.updateRenameDraft("Second!")
store.commitRename()
try Foreign.delete(fixture, card2Path)
try Foreign.trash(fixture, card2Path, id: Ident.card2)
history.undo()
// The top step's card is in the trash now, so its rename is not ours to walk back; the one
// below it is untouched and applies in the same Z.
#expect(try document(fixture, card2Path).title.value == "Second!", "the foreign writer's board, left alone")
#expect(try document(fixture, ".trash/\(Ident.card2)").title.value == "Second!",
"the foreign writer's board, left alone")
#expect(try document(fixture, card1Path).title.value == "First", "⌘Z fell through and did something")
#expect(store.banners.signposts.map(\.message)
== ["Undo skipped — 'Second!' changed outside Lanework"])
@@ -906,29 +944,33 @@ struct StaleStepTests {
let (store, history) = try makeStore(fixture)
store.delete([card1])
try Foreign.purge(fixture, card1Path)
try Foreign.purge(fixture, ".trash/\(Ident.card1)")
history.undo()
#expect(fixture.exists(card1Path) == false)
#expect(!fixture.exists(".trash/\(Ident.card1)"))
#expect(store.banners.signposts.count == 1)
#expect(store.banners.oneShots.isEmpty, "a skip is not a write failure — no error row")
#expect(history.canUndo == false)
#expect(history.canRedo == false, "a skipped step leaves nothing behind")
}
@Test("A foreign Put Back skips the delete's undo — the item is not on the side we left it")
/// The container check, and it needs no field of its own: a delete step's undo expects its card
/// at `<root>/.trash/<id>`, and a foreign restore leaves that path empty (`HistoryStaleness`).
@Test("A foreign restore skips the delete's undo — the card is not in the container we left it")
func aForeignRestoreSkipsTheDeleteStep() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.delete([card1])
try BoardWriter.restoreItem(at: fixture.url(card1Path))
try fixture.move(".trash/\(Ident.card1)", toLane: Ident.lane2, card: Ident.card1)
history.undo()
#expect(try document(fixture, card1Path).deleted.isMissing, "still live, as they left it")
#expect(fixture.exists("\(Ident.lane2)/\(Ident.card1)"), "where the foreign writer put it")
#expect(!fixture.exists(card1Path), "and not moved back on top of them")
#expect(store.banners.signposts.map(\.message) == ["Undo skipped — 'First' changed outside Lanework"])
}
@@ -949,23 +991,25 @@ struct StaleStepTests {
#expect(history.canUndo == false)
}
@Test("A tombstoned card is stale for a field edit, even with the field itself untouched")
func aTombstonedTargetIsStaleForAFieldEdit() throws {
@Test("A foreign lane delete is stale for a field edit — there is nothing at the path")
func aRemovedLaneIsStaleForAFieldEdit() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
store.setLaneWidth(lane1, units: 3)
try Foreign.delete(fixture, Ident.lane1)
try Foreign.removeLane(fixture, Ident.lane1)
history.undo()
#expect(try document(fixture, Ident.lane1).width.value == 3, "not resized inside the trash")
#expect(!fixture.exists(Ident.lane1), "not conjured back to be resized")
#expect(store.banners.signposts.count == 1)
}
@Test("A card under a foreign-tombstoned lane is stale too — liveness is ancestor-walked")
func anAncestorTombstoneIsStale() throws {
/// The card's own path is the check: a lane delete is physical, so a card under it is simply not
/// there any more no ancestor walk, which is what materializing the trash bought.
@Test("A card whose lane a foreign writer deleted is stale too")
func aCardUnderARemovedLaneIsStale() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
@@ -973,11 +1017,11 @@ struct StaleStepTests {
store.transient.beginRename(of: card1, currentTitle: "First")
store.transient.updateRenameDraft("Renamed")
store.commitRename()
try Foreign.delete(fixture, Ident.lane1)
try Foreign.removeLane(fixture, Ident.lane1)
history.undo()
#expect(try document(fixture, card1Path).title.value == "Renamed", "the card renders nowhere; nothing was written")
#expect(!fixture.exists(card1Path), "the card is gone; nothing was written")
#expect(store.banners.signposts.count == 1)
}
@@ -1014,7 +1058,7 @@ struct StaleStepTests {
store.commitRename()
try Foreign.rename(fixture, card2Path, to: "Theirs")
try Foreign.delete(fixture, card3Path)
try Foreign.trash(fixture, card3Path, id: Ident.card3)
history.undo()
#expect(try document(fixture, card1Path).title.value == "First", "the step applied")
@@ -1108,7 +1152,7 @@ struct StaleStepTests {
let (store, history) = try makeStore(fixture)
store.delete([card1])
try Foreign.purge(fixture, card1Path)
try Foreign.purge(fixture, ".trash/\(Ident.card1)")
history.undo()
let row = try #require(store.bannerRows.last)
@@ -1219,7 +1263,7 @@ struct FailedCrossingTests {
try FileManager.default.setAttributes([.posixPermissions: 0o500], ofItemAtPath: fixture.url(card1Path).path)
history.undo()
#expect(try document(fixture, card2Path).deleted.value != nil,
#expect(fixture.exists(".trash/\(Ident.card2)"),
"the step below was never reached — a refused disk is not a reason to attempt more")
#expect(history.undoActionName == "Rename Card")
}
@@ -1322,34 +1366,36 @@ struct HistoryPhraseTests {
/// that the two doors reach one state rather than two similar ones, and that alternating between
/// them leaves a stack that crosses cleanly in both directions.
@MainActor
@Suite("Undo ▸ the trash's two doors")
@Suite("Undo ▸ the trash and the stack are the same folder moves")
struct TrashInterplayTests {
@Test("Undoing a delete lands exactly where Put Back would have — the same bytes, not a near miss")
/// 13-native-undo.md Interaction with the trash: "The stack and the trash never conflict they
/// are the same folder moves addressed by recency instead of by selection."
@Test("Undoing a delete lands exactly where a manual move-out would — the same bytes")
func theTwoDoorsReachOneState() async throws {
// Two identical boards, one per door: the claim is about a *state*, so the honest comparison
// is the whole board read off disk, not the one field each path happens to write.
let byUndo = try makeBoard()
defer { byUndo.tearDown() }
let byPutBack = try makeBoard()
defer { byPutBack.tearDown() }
let byMove = try makeBoard()
defer { byMove.tearDown() }
let origin = try boardTexts(byUndo)
let (undoStore, history) = try makeStore(byUndo)
undoStore.delete([card1])
history.undo()
let (putBackStore, _) = try makeStore(byPutBack)
putBackStore.delete([card1])
// Put Back reads the trashed side of the snapshot, so it has to see the tombstone first
// which is the one-way flow, not a test artefact.
await reload(putBackStore)
putBackStore.putBack([card1])
let (moveStore, _) = try makeStore(byMove)
moveStore.delete([card1])
// The move-out reads the trash side of the snapshot, so it has to see the card arrive there
// first which is the one-way flow, not a test artefact.
await reload(moveStore)
moveStore.moveCards([card1], toLane: lane1, at: 0)
expectSameBoard(try boardTexts(byUndo), try boardTexts(byPutBack), "the two doors")
expectSameBoard(try boardTexts(byUndo), try boardTexts(byMove), "the two doors")
expectSameBoard(try boardTexts(byUndo), origin, "undo against the board it started from")
#expect(try document(byUndo, card1Path).deleted.isMissing)
#expect(try document(byPutBack, card1Path).deleted.isMissing)
#expect(byUndo.exists(card1Path))
#expect(byMove.exists(card1Path))
}
@Test("Undoing a delete is position-preserving — the card comes back where it was, not at an end")
@@ -1358,25 +1404,25 @@ struct TrashInterplayTests {
defer { fixture.tearDown() }
let (store, history) = try makeStore(fixture)
/// The lane's live cards in display order, read through the loader the order the board
/// actually renders, rather than the ranks it is derived from.
func liveCards() throws -> [String] {
/// The lane's cards in display order, read through the loader the order the board actually
/// renders, rather than the ranks it is derived from.
func laneCards() throws -> [String] {
let result = try BoardLoader.load(boardRoot: fixture.root)
let lane = try #require(result.model.lanes.first { $0.id == lane1 })
return lane.cards.filter { !$0.isDeleted }.map(\.id.rawValue)
return lane.cards.map(\.id.rawValue)
}
let before = try liveCards()
let before = try laneCards()
#expect(before == [Ident.card1, Ident.card2, Ident.card3], "the middle card is genuinely in the middle")
// The middle of three: an implementation that restored by appending would pass on a first or
// last card and fail here.
store.delete([card2])
await reload(store)
#expect(try liveCards() == [Ident.card1, Ident.card3])
#expect(try laneCards() == [Ident.card1, Ident.card3])
history.undo()
#expect(try liveCards() == before, "01's deletion bullet: restore is position-perfect")
#expect(try laneCards() == before, "13: the undo returns the card to its source lane and rank")
#expect(try document(fixture, card2Path).order.value == 2048, "the rank it held all along")
}
@@ -1387,30 +1433,27 @@ struct TrashInterplayTests {
let (store, history) = try makeStore(fixture)
let live = try boardTexts(fixture)
// Three gestures over one card, alternating the doors: , Put Back, .
// Three gestures over one card, alternating the doors: , move back out, .
store.delete([card1])
await reload(store)
let filedUnder = try #require(try document(fixture, card1Path).deleted.value)
#expect(fixture.exists(".trash/\(Ident.card1)"))
store.putBack([card1])
store.moveCards([card1], toLane: lane1, at: 0)
await reload(store)
#expect(try document(fixture, card1Path).deleted.isMissing)
#expect(fixture.exists(card1Path))
store.delete([card1])
await reload(store)
#expect(history.undoActionName == "Delete Card")
let trashed = try boardTexts(fixture)
let trashedBoard = try boardTexts(fixture)
// Back up the stack: delete restore tombstone restore. The middle step is Put Back's
// inverse, which has to re-file the row under the stamp it was filed under rather than under
// now the trash sorts by it.
// Back up the stack: delete move delete, each crossed in reverse.
history.undo()
#expect(try document(fixture, card1Path).deleted.isMissing)
#expect(history.undoActionName == "Restore Card")
#expect(fixture.exists(card1Path))
#expect(history.undoActionName == "Move Card")
history.undo()
#expect(try document(fixture, card1Path).deleted.value == filedUnder,
"re-tombstoned where it was filed, not where a fresh stamp would put it")
#expect(fixture.exists(".trash/\(Ident.card1)"), "back in the trash the move took it out of")
#expect(history.undoActionName == "Delete Card")
history.undo()
@@ -1419,12 +1462,12 @@ struct TrashInterplayTests {
// And forward again, the same three steps in the other direction.
history.redo()
#expect(try document(fixture, card1Path).deleted.value != nil)
#expect(fixture.exists(".trash/\(Ident.card1)"))
history.redo()
#expect(try document(fixture, card1Path).deleted.isMissing)
#expect(fixture.exists(card1Path))
history.redo()
#expect(history.canRedo == false)
expectSameBoard(try boardTexts(fixture), trashed, "three doors forward")
expectSameBoard(try boardTexts(fixture), trashedBoard, "three doors forward")
#expect(store.banners.signposts.isEmpty, "nothing was stale: the doors never collided")
#expect(store.banners.oneShots.isEmpty)
}