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
+5 -5
View File
@@ -41,7 +41,7 @@ enum NewCardTarget {
/// the two can never disagree.
///
/// - Parameters:
/// - selection: the board's current selection, liveness side included. A `.trashed` selection
/// - selection: the board's current selection, container included. A `.trash` selection
/// "never anchors creation" and is treated exactly as an empty one the settled precedent
/// 04 Clipboard cites for paste, applied here to its source rule ("a trashed card's live
/// disk-lane never leaks in as 'the selected card's lane'").
@@ -53,7 +53,7 @@ enum NewCardTarget {
lastActiveLaneID: ItemID?,
snapshot: BoardModel
) -> Resolution? {
let lanes = snapshot.lanes.filter { !$0.isDeleted }
let lanes = snapshot.lanes
guard !lanes.isEmpty else { return nil }
if let anchor = flattenAnchor(selection: selection, snapshot: snapshot) {
@@ -86,21 +86,21 @@ enum NewCardTarget {
/// which falls out of never looking at the trashed side at all), and a stale one whose ids name
/// nothing the board renders.
static func flattenAnchor(selection: ItemReferenceSet, snapshot: BoardModel) -> Resolution? {
guard selection.liveness == .live, !selection.ids.isEmpty else { return nil }
guard selection.container == .board, !selection.ids.isEmpty else { return nil }
// The snapshot's lanes and cards are already in display order, so the flatten order is one
// walk, and the *last* hit is the anchor. Selection is homogeneous (cards XOR lanes), so only
// one of the two branches ever fires within a walk; a sole selection is simply the degenerate
// one-member case of the same rule.
var anchor: Resolution?
for lane in snapshot.lanes where !lane.isDeleted {
for lane in snapshot.lanes {
// A selected lane: creation appends at its bottom, Return consistency; paste lands after
// the lane itself.
if selection.ids.contains(lane.id) {
anchor = Resolution(laneID: lane.id, anchorCardID: nil)
}
// A selected card: its lane, immediately after it paste-anchor consistency.
for card in lane.cards where !card.isDeleted && selection.ids.contains(card.id) {
for card in lane.cards where selection.ids.contains(card.id) {
anchor = Resolution(laneID: lane.id, anchorCardID: card.id)
}
}