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:
@@ -38,7 +38,6 @@ private func makeBoard() throws -> WriterFixture {
|
||||
try fixture.item("\(Ident.lane1)/\(Ident.card2)", Item.rich(order: "2048", title: "Second"))
|
||||
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
|
||||
try fixture.item("\(Ident.lane2)/\(Ident.card3)", Item.rich(order: "1024", title: "Third"))
|
||||
try fixture.item(Ident.lane3, tombstoned(order: "3072", title: "Archive"))
|
||||
return fixture
|
||||
}
|
||||
|
||||
@@ -71,14 +70,14 @@ struct NewCardTargetTests {
|
||||
|
||||
// "With a card selected, the new card is created in that card's lane, immediately after it
|
||||
// (paste-anchor consistency)."
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: card1))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card3], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card3], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane2, anchorCardID: card3))
|
||||
|
||||
// The last card in a lane is still an anchor here — "after the last card" and "at the
|
||||
// bottom" coincide, and it is the commit that notices (`BoardStore.insertionIndex`).
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card2], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card2], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: card2))
|
||||
}
|
||||
|
||||
@@ -89,7 +88,7 @@ struct NewCardTargetTests {
|
||||
let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
|
||||
// "With a lane selected, appended at its bottom (Return consistency)."
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [lane2], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [lane2], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane2, anchorCardID: nil))
|
||||
}
|
||||
|
||||
@@ -107,25 +106,25 @@ struct NewCardTargetTests {
|
||||
#expect(resolve(snapshot) == NewCardTarget.Resolution(laneID: lane1, anchorCardID: nil))
|
||||
#expect(resolve(snapshot, lastActive: ItemID(rawValue: Ident.indexless))
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: nil))
|
||||
// A *tombstoned* lane is not a target either — it renders nowhere, and the trash is never a
|
||||
// creation destination.
|
||||
// A lane that has been deleted is not a target either — a lane delete is physical, so the
|
||||
// memory simply names nothing.
|
||||
#expect(resolve(snapshot, lastActive: lane3)
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: nil))
|
||||
}
|
||||
|
||||
@Test("A tombstoned selection never anchors creation — it behaves as nothing selected")
|
||||
func aTombstonedSelectionNeverAnchors() throws {
|
||||
@Test("A trash selection never anchors creation — it behaves as nothing selected")
|
||||
func aTrashSelectionNeverAnchors() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
|
||||
// Settled in 04 ▸ The map, on the ⌘N rule's own wording: "a **tombstoned** selection, which
|
||||
// never anchors creation". The trash-side lane is a real lane on disk with a live sibling
|
||||
// list — the rule must not let its identity leak in as a target.
|
||||
let trashed = ItemReferenceSet(ids: [lane3], liveness: .trashed)
|
||||
#expect(resolve(snapshot, selection: trashed, lastActive: lane2)
|
||||
// 04 ▸ The map, on the ⌘N rule's own wording: "with nothing selected — or a **trash**
|
||||
// selection, which never anchors creation". A trashed card's board-side lane must not leak
|
||||
// in as a target.
|
||||
let inTrash = ItemReferenceSet(ids: [card1], container: .trash)
|
||||
#expect(resolve(snapshot, selection: inTrash, lastActive: lane2)
|
||||
== NewCardTarget.Resolution(laneID: lane2, anchorCardID: nil))
|
||||
#expect(resolve(snapshot, selection: trashed)
|
||||
#expect(resolve(snapshot, selection: inTrash)
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: nil))
|
||||
}
|
||||
|
||||
@@ -140,21 +139,22 @@ struct NewCardTargetTests {
|
||||
// target — New Card, Return-creation, and Paste with a card payload disable via menu
|
||||
// validation until a lane exists."
|
||||
#expect(resolve(empty) == nil)
|
||||
#expect(resolve(empty, selection: ItemReferenceSet(ids: [card1], liveness: .live)) == nil)
|
||||
#expect(resolve(empty, selection: ItemReferenceSet(ids: [card1], container: .board)) == nil)
|
||||
#expect(resolve(empty, lastActive: lane1) == nil)
|
||||
}
|
||||
|
||||
@Test("A board whose every lane is tombstoned is a zero-lane board")
|
||||
func everyLaneTombstonedIsAlsoZeroLane() throws {
|
||||
let fixture = try WriterFixture()
|
||||
@Test("A board whose every lane has been deleted is a zero-lane board")
|
||||
func everyLaneDeletedIsAlsoZeroLane() throws {
|
||||
let fixture = try makeBoard()
|
||||
defer { fixture.tearDown() }
|
||||
try fixture.item("", Item.board)
|
||||
try fixture.item(Ident.lane1, tombstoned(order: "1024", title: "Todo"))
|
||||
for lane in [Ident.lane1, Ident.lane2, Ident.lane3] where fixture.exists(lane) {
|
||||
try FileManager.default.removeItem(at: fixture.url(lane))
|
||||
}
|
||||
let snapshot = try BoardLoader.load(boardRoot: fixture.root).model
|
||||
|
||||
// The lanes are still in the snapshot — the trash renders them — but none is on the board,
|
||||
// and "every lane deleted" is the design's own second reading of the zero-lane case.
|
||||
#expect(snapshot.lanes.count == 1)
|
||||
// "Every lane deleted" is the design's own second reading of the zero-lane case — and a lane
|
||||
// delete is physical now, so it is literally the same board as the hand-made one above.
|
||||
#expect(snapshot.lanes.isEmpty)
|
||||
#expect(resolve(snapshot) == nil)
|
||||
}
|
||||
|
||||
@@ -167,15 +167,15 @@ struct NewCardTargetTests {
|
||||
// "A multi-selection anchors at its last member in flatten order (lane `order`, then card
|
||||
// `order`, the multi-drag order)": card3 lives in lane2, which sorts after lane1's pair, so
|
||||
// creation follows card3 — the last-active lane never enters into it.
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1, card3], liveness: .live), lastActive: lane1)
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1, card3], container: .board), lastActive: lane1)
|
||||
== NewCardTarget.Resolution(laneID: lane2, anchorCardID: card3))
|
||||
|
||||
// Within one lane the flatten order is card order: card2 sorts after card1.
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1, card2], liveness: .live), lastActive: lane2)
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [card1, card2], container: .board), lastActive: lane2)
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: card2))
|
||||
|
||||
// A multi-LANE selection appends to the last selected lane's bottom.
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [lane1, lane2], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [lane1, lane2], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane2, anchorCardID: nil))
|
||||
}
|
||||
|
||||
@@ -187,7 +187,7 @@ struct NewCardTargetTests {
|
||||
|
||||
// A selection naming something the board does not render — the reload that drops it has not
|
||||
// landed yet — must not refuse the creation the user just asked for.
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [ItemID(rawValue: Ident.indexless)], liveness: .live))
|
||||
#expect(resolve(snapshot, selection: ItemReferenceSet(ids: [ItemID(rawValue: Ident.indexless)], container: .board))
|
||||
== NewCardTarget.Resolution(laneID: lane1, anchorCardID: nil))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user