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:
@@ -471,15 +471,18 @@ public final class BannerCenter {
|
||||
/// **A loss row for `postRelocatedLooseFiles`' exact reason**, and it is the same shape of event:
|
||||
/// the app moved the user's folders on its own initiative, on a board it opened rather than on a
|
||||
/// gesture they made. That must be said out loud, must not evaporate unread, and must not rank
|
||||
/// as an error, because no action failed. The one nuance worth naming: the *lane* half is a
|
||||
/// resurrection rather than a removal — cards nobody asked to see again may reappear on the
|
||||
/// board — which is exactly the kind of surprise this class exists to announce.
|
||||
/// as an error, because no action failed.
|
||||
///
|
||||
/// `cards` and `lanes` are the migrated items' titles, in the order they were written, `nil` for
|
||||
/// an untitled one — "Untitled" is a rendering, never a value (03-board-ui.md § Card face), so
|
||||
/// the phrasing layer decides what to call it. A migration that migrated nothing posts nothing.
|
||||
public func postMigratedTombstones(cards: [String?], lanes: [String?]) {
|
||||
guard let message = Self.migratedTombstonesMessage(cards: cards, lanes: lanes) else { return }
|
||||
/// **Cards only** (01-storage-format.md § Deletion, lane clause re-ruled 2026-07-29): the lane
|
||||
/// half of this notice retired with the lane migration itself — a lane's `deleted:` is inert now,
|
||||
/// nothing is written for it, and a row announcing an act the app did not perform would be worse
|
||||
/// than silence.
|
||||
///
|
||||
/// `cards` holds the migrated cards' titles, in the order they were written, `nil` for an
|
||||
/// untitled one — "Untitled" is a rendering, never a value (03-board-ui.md § Card face), so the
|
||||
/// phrasing layer decides what to call it. A migration that migrated nothing posts nothing.
|
||||
public func postMigratedTombstones(cards: [String?]) {
|
||||
guard let message = Self.migratedTombstonesMessage(cards: cards) else { return }
|
||||
postLoss(message)
|
||||
}
|
||||
|
||||
@@ -962,49 +965,28 @@ public final class BannerCenter {
|
||||
return "Moved '\(name)' into attachments — \(subject)"
|
||||
}
|
||||
|
||||
/// The legacy tombstone migration's line — **one folded sentence for both halves**, written in
|
||||
/// `relocatedLooseFilesMessage`'s voice because it is the same kind of notice: the act first,
|
||||
/// the subject after an em dash, plurals folded, a sole item named.
|
||||
/// The legacy tombstone migration's line, written in `relocatedLooseFilesMessage`'s voice
|
||||
/// because it is the same kind of notice: the act first, the subject after an em dash, plurals
|
||||
/// folded, a sole item named.
|
||||
///
|
||||
/// The two clauses are joined rather than posted as two rows, because it is **one migration**:
|
||||
/// a board opened, its old deletion markers were resolved, and that is one thing that happened
|
||||
/// to the user's files. Two rows would also mean two dismissals for one event, and would rank a
|
||||
/// resurrection and a relocation against each other for no reason.
|
||||
/// **One clause, since the lane half retired** (01-storage-format.md § Deletion, re-ruled
|
||||
/// 2026-07-29 — lanes ignore the key rather than migrating). The shapes:
|
||||
///
|
||||
/// The shapes, in the relocation's own idiom:
|
||||
/// - **One card**: "Moved 'Fix login' to the trash — it carried an old deleted marker".
|
||||
/// - **Several**: "Moved 3 cards to the trash — they carried old deleted markers".
|
||||
///
|
||||
/// - **One card**, no lanes: "Moved 'Fix login' to the trash — it carried an old deleted marker".
|
||||
/// - **Several cards**: "Moved 3 cards to the trash — they carried old deleted markers".
|
||||
/// - **One lane**, no cards: "Restored 'Doing' — it carried an old deleted marker".
|
||||
/// - **Both**: "Moved 3 cards to the trash and restored 2 lanes — they carried old deleted markers".
|
||||
///
|
||||
/// **The tail names the cause once**, and it is the whole explanation the row owes: the user did
|
||||
/// not delete anything just now, and without the clause the sentence would read as an action
|
||||
/// they had somehow just taken. The singular/plural of the tail follows the *total*, so the
|
||||
/// mixed case never has to spell a singular (two clauses carry at least two items).
|
||||
/// **The tail names the cause**, and it is the whole explanation the row owes: the user did not
|
||||
/// delete anything just now, and without the clause the sentence would read as an action they
|
||||
/// had somehow just taken.
|
||||
///
|
||||
/// `nil` when nothing migrated — a migration that migrated nothing is not news.
|
||||
public nonisolated static func migratedTombstonesMessage(cards: [String?], lanes: [String?]) -> String? {
|
||||
let total = cards.count + lanes.count
|
||||
guard total > 0 else { return nil }
|
||||
|
||||
var clauses: [String] = []
|
||||
if !cards.isEmpty {
|
||||
let subject = cards.count == 1
|
||||
? sole(cards[0])
|
||||
: "\(cards.count) cards"
|
||||
clauses.append("Moved \(subject) to the trash")
|
||||
}
|
||||
if !lanes.isEmpty {
|
||||
let subject = lanes.count == 1
|
||||
? sole(lanes[0])
|
||||
: "\(lanes.count) lanes"
|
||||
clauses.append(clauses.isEmpty ? "Restored \(subject)" : "restored \(subject)")
|
||||
}
|
||||
let tail = total == 1
|
||||
public nonisolated static func migratedTombstonesMessage(cards: [String?]) -> String? {
|
||||
guard !cards.isEmpty else { return nil }
|
||||
let subject = cards.count == 1 ? sole(cards[0]) : "\(cards.count) cards"
|
||||
let tail = cards.count == 1
|
||||
? "it carried an old deleted marker"
|
||||
: "they carried old deleted markers"
|
||||
return "\(clauses.joined(separator: " and ")) — \(tail)"
|
||||
return "Moved \(subject) to the trash — \(tail)"
|
||||
}
|
||||
|
||||
/// The claimed-name displacement's line — the relocation's own voice (the act first, the subject
|
||||
|
||||
Reference in New Issue
Block a user