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
+13 -23
View File
@@ -110,7 +110,7 @@ extension BoardStore {
// gone is not one to pop: the stack is about to be cleared with the session anyway.
guard let store else { return .failed }
guard HistoryStaleness.isCurrent(expectations, under: store.rootURL) else {
guard HistoryStaleness.isCurrent(expectations) else {
store.banners.postSkippedStep(direction, subject: subject)
return .skipped
}
@@ -157,11 +157,11 @@ extension BoardStore {
/// Registers a create's step: undo removes the folders, redo puts them back byte-for-byte.
///
/// **Removal, not a tombstone**, exactly as 13 words it: an undone create leaves *no trace*,
/// because the item was born of the gesture being undone a tombstone would leave a trash row
/// for a card the user never really made. `purgeIsUnrecoverable` is untouched by this: that flag
/// is about Delete Immediately, whose loss is the user's own final gesture, while this loss is
/// one Z away.
/// **Removal, not a move into the trash**, exactly as 13 words it: an undone create leaves *no
/// trace*, because the item was born of the gesture being undone filing it in the trash would
/// leave a card the user never really made for them to find. `purgeIsUnrecoverable` is untouched
/// by this: that flag is about Delete Immediately, whose loss is the user's own final gesture,
/// while this loss is one Z away.
///
/// The honest edge, recorded rather than papered over: anything that happened *inside* the
/// created folder through an operation that registers no step of its own an attachment added by
@@ -169,19 +169,19 @@ extension BoardStore {
/// 13's own two rulings (remove the folder; attachment operations register nothing in v1) rather
/// than from anything decided here.
///
/// **Its staleness predicate is existence and liveness, and deliberately nothing else** (13:
/// "existence/liveness for create/delete/restore steps"). A create's after-value *is* the item's
/// being there, so the undo validates that the folders are still there and still live and the
/// redo that they are still gone. The same honest edge follows: a foreign *edit* inside a created
/// card does not stop Z from removing it, because the create never wrote that field while a
/// foreign *delete* does, since the trash row the user is looking at is not this step's to purge.
/// **Its staleness predicate is existence, and deliberately nothing else** (13: "existence
/// for create/delete/restore steps"). A create's after-value *is* the item's being there, so the
/// undo validates that the folders are still at their paths and the redo that they are still
/// gone. The same honest edge follows: a foreign *edit* inside a created card does not stop Z
/// from removing it, because the create never wrote that field while a foreign *delete* does,
/// since the card the user is looking at in the trash is at a different path now.
func registerCreation(_ items: [CreatedItem], kind: HistoryPhrase.Kind, subject: String? = nil) {
guard !items.isEmpty else { return }
let operation: WriteOperation = kind == .lane ? .createLane : .createCard
registerStep(
HistoryPhrase.name(.add, kind: kind, count: items.count),
subject: subject,
undoExpects: items.map { .live($0.folder) },
undoExpects: items.map { .present($0.folder) },
redoExpects: items.map { .absent($0.folder) }
) { _ in
// Reversed, so a lane and a card created by one gesture unwind child-first the same
@@ -282,14 +282,4 @@ extension BoardStore {
}
}
/// Puts an item's tombstone back **with the timestamp it carried**, not with `now`.
///
/// The inverse of Put Back is the item returning to the trash exactly where it was, and the trash
/// sorts by `deleted` (03-board-ui.md § Trash Contents): re-stamping would file the row under
/// today and quietly reorder a list the user was reading. A prior that was malformed (or, by
/// construction impossibly, missing) falls back to `now` the item has to be tombstoned, and an
/// unreadable timestamp is not a value to preserve.
static func restoreTombstone(_ prior: FieldValue<Date>, in document: inout FrontmatterDocument) {
document.set(FrontmatterKeys.deleted, to: .date(prior.value ?? Date()))
}
}