Lanes delete into the trash — storage, loader, writer, and undo
Phase 1 of the lanes-in-trash card (2026-07-29 ruling, docs led the
code): lane delete is a move into .trash/ with the subtree intact,
arriving at top trash rank — no destructive delete remains outside
the trash.
TrashedLane opaque unit (id/schema/title/order/heldCards) beside
trash cards — deliberately not a Lane, so no card-shaped surface can
believe an empty subtree. Loader's trash walk trusts the kind VALUE
(lane → opaque unit w/ held-card count counted at the loader's own
unit; card → ordinary card; absent/unrecognized → UUID-children
shape, empty-kindless falls to card per 01's honest limit). Writer:
moveIntoTrash generalized with kind passed never derived (an empty
lane would re-derive as card), deleteLaneToTrash mints against the
whole-container rank ladder. Retired: migrateTombstonedLane (lane
deleted: now ignored — loads live, bytes inert, tolerate-tier
warning), removeLane, captureSubtree/recreateSubtree and the
subtree-snapshot machinery. Undo inverse = move back to captured
strip position, redo replays at captured trash rank. Purge walks
lane subtrees; TrashModel.Freight phrases confirms with lane freight
("…and its 5 cards"). ItemPath gains .trashLane; resolve interleaves
the trash by rank; SearchFilter matches lane rows by title only.
Trashed-lane card windows dismiss and pending cuts void via the
ordinary vanish rule — no new plumbing.
Phase 2 (rendering, selection grammar, drag, a11y, agent guide)
follows. Both schemes 1858 tests / 318 suites green.
Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -35,7 +35,7 @@ final class TrashConfirmations {
|
||||
/// reachable inside the trash now: the trash's own staged Delete, and Empty Trash (which names
|
||||
/// the whole container and re-derives its targets at the moment it runs).
|
||||
enum Action: Equatable {
|
||||
case deleteTrashCards(Set<ItemID>)
|
||||
case deleteTrashEntries(Set<ItemID>)
|
||||
case emptyTrash
|
||||
}
|
||||
}
|
||||
@@ -43,8 +43,8 @@ final class TrashConfirmations {
|
||||
/// **File ▸ Delete, staged by place** (04-interactions.md ▸ The map) — with the confirmation the
|
||||
/// trash side owes and the board side does not.
|
||||
///
|
||||
/// A board selection goes straight through: moving a card into the trash and deleting a lane are
|
||||
/// both recoverable (the trash itself, and native undo — 03-board-ui.md § Trash), so neither
|
||||
/// A board selection goes straight through: a card and a lane alike move into the trash, and the
|
||||
/// move is recoverable (the trash itself, and native undo — 03-board-ui.md § Trash), so neither
|
||||
/// stands an alert. A **trash** selection is the permanent one, and it "confirms exactly where
|
||||
/// the loss is real": `purgeIsUnrecoverable` decides.
|
||||
///
|
||||
@@ -71,7 +71,7 @@ final class TrashConfirmations {
|
||||
/// is what a board that keeps history does for every permanent delete (delete-never-forgets).
|
||||
func requestTrashDelete(of ids: Set<ItemID>, in store: BoardStore) {
|
||||
guard store.purgeIsUnrecoverable else {
|
||||
store.deleteTrashCards(ids)
|
||||
store.deleteTrashEntries(ids)
|
||||
return
|
||||
}
|
||||
guard let prompt = TrashModel.purgePrompt(
|
||||
@@ -79,7 +79,7 @@ final class TrashConfirmations {
|
||||
snapshot: store.snapshot,
|
||||
unrecoverable: true
|
||||
) else { return }
|
||||
pending = Pending(prompt: prompt, action: .deleteTrashCards(ids))
|
||||
pending = Pending(prompt: prompt, action: .deleteTrashEntries(ids))
|
||||
}
|
||||
|
||||
/// Raises Empty Trash…'s alert. **Always** — it guards bulk scope rather than per-item
|
||||
@@ -98,7 +98,7 @@ final class TrashConfirmations {
|
||||
guard let pending else { return }
|
||||
self.pending = nil
|
||||
switch pending.action {
|
||||
case let .deleteTrashCards(ids): store.deleteTrashCards(ids)
|
||||
case let .deleteTrashEntries(ids): store.deleteTrashEntries(ids)
|
||||
case .emptyTrash: store.emptyTrash()
|
||||
}
|
||||
}
|
||||
@@ -168,9 +168,10 @@ struct TrashCommands: View {
|
||||
extension BoardStore {
|
||||
|
||||
/// **Trash shown and non-empty** (11-command-nexus.md's own scope for the Empty Trash… row) —
|
||||
/// where "non-empty" reads `.trash/` itself and never the filtered view (03-board-ui.md § Trash:
|
||||
/// "menu validation's non-empty reads `.trash/`, not the filtered view", so a search that hides
|
||||
/// every trash card leaves the command enabled and its confirmation still names the true count).
|
||||
/// where "non-empty" reads `.trash/` itself — **both kinds of entry**, and never the filtered
|
||||
/// view (03-board-ui.md § Trash: "menu validation's non-empty reads `.trash/`, not the filtered
|
||||
/// view", so a search that hides every trash row leaves the command enabled and its confirmation
|
||||
/// still names the true count, lane freight included).
|
||||
///
|
||||
/// The visibility clause is 04-interactions.md's, stated for the whole column: "hidden, it is
|
||||
/// invisible to every gesture".
|
||||
@@ -179,7 +180,7 @@ extension BoardStore {
|
||||
/// menu (`TrashModel`'s own reason for being a pure function of a snapshot).
|
||||
var canEmptyTrash: Bool {
|
||||
guard acceptsBoardMutations, transient.isTrashVisible else { return false }
|
||||
return !snapshot.trash.isEmpty
|
||||
return !snapshot.trash.isEmpty || !snapshot.trashedLanes.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user