Register inverse operations at the Writer boundary

The store is the Writer boundary, so it computes and registers
inverses: a weak history sink bound at session composition, one
HistoryStep per gesture at exactly the brackets that were already one
performWrite each — multi-card moves, style batches, width pairs, and
multi-row restores each undo as one plurally-titled step, and the Edit
session registers once at the flip from the bytes disk held before its
first landed write, debounce ticks registering nothing. Crossings run
through performWrite, so an undo brackets the watcher, echoes through
the reload, and reaches every window; every closure captures values,
never snapshots. The inventory follows 13 exactly: moves return to
origin lane and order, renames restore or remove the title key,
restyles and resizes restore field values or absence, tombstones and
restores swap with captured timestamps, and an undone create is a real
removal — no trace — with redo re-materializing the same UUID from
bytes captured at gesture time. Purge, attachments, repair,
bookkeeping, checkbox flips, raw Apply, and the whole arrival family
register nothing, each exclusion documented where it lives. Step names
speak 06's verb vocabulary through the new HistoryPhrase.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-07-28 14:24:31 -04:00
parent 93fad2ef1e
commit 2148ebb379
8 changed files with 1732 additions and 64 deletions
+93
View File
@@ -0,0 +1,93 @@
import Foundation
// MARK: - HistoryPhrase
/// The menu phrase an undo step carries "Move 3 Cards", "Rename Lane", "Restyle Board".
///
/// ### Why the vocabulary is 06's and not a new one
///
/// 13-native-undo.md Rules hands the naming straight over: "The 06 vocabulary supplies menu titles
/// ('Undo Move 3 Cards'), via NSUndoManager's dynamic retitling the same naming machinery both
/// editions use." So the verbs here are exactly 06-history-undo.md Commit messages' list *Add /
/// Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board*, plus
/// the trash pair's **Restore** and the plural rule is that section's own plural folding ("Delete
/// 12 cards"), which is also 13's coalescing sentence read out loud: "a multi-card move is one step
/// with a plural title".
///
/// The **"Undo "/"Redo " prefix is never here**: the platform composes and localizes it
/// (`BoardUndoManager.undoMenuItemTitle`), and a phrase that spelled it would read "Undo Undo Move
/// Card" in the Edit menu see `HistoryStep.name`.
///
/// ### Title case, unlike a commit subject
///
/// A commit subject is a sentence ("Move 3 cards to Done"); a menu item is a title, and macOS titles
/// its Edit-menu rows. The words are 06's; the casing is the menu's. Nothing else differs and the
/// destination clause a commit subject carries has no place in a title that has to stay short enough
/// for a menu row.
///
/// Pure, and its own type rather than a `String` built at each call site, because a phrase composed
/// in eleven places is a vocabulary that drifts in eleven places.
public enum HistoryPhrase {
// MARK: Verbs
/// 06-history-undo.md Commit messages' verb list, restricted to the operations 13 makes
/// undoable. `Permanently delete`, `Attach`, `Remove` and `Repair` are deliberately absent
/// those are exactly the operations that register no step at all (13 Rules what is not
/// undoable, Out of scope).
public enum Verb: String, Sendable, CaseIterable {
/// A create File New Lane, the new-card placeholder's commit, a Finder file drop's cards.
case add = "Add"
/// A tombstone (, drop-on-trash, the card window's Actions Delete).
case delete = "Delete"
/// Put Back and drag-to-restore.
case restore = "Restore"
/// A drop that changes an item's parent.
case move = "Move"
/// A drop, a lane drag or / that changes rank among unchanged siblings.
case reorder = "Reorder"
case rename = "Rename"
case restyle = "Restyle"
case resize = "Resize"
/// An Edit session's body save one step at the EditPreview flip (13 Rules).
case edit = "Edit"
}
// MARK: Nouns
/// What the gesture acted on. `board` is deliberately count-less: there is one board, and
/// "Rename 1 Board" is not a phrase anyone writes.
public enum Kind: Sendable, Equatable {
case card
case lane
case board
var singular: String {
switch self {
case .card: "Card"
case .lane: "Lane"
case .board: "Board"
}
}
var plural: String {
switch self {
case .card: "Cards"
case .lane: "Lanes"
case .board: "Board"
}
}
}
// MARK: Composition
/// The phrase for one gesture: `"Move Card"`, `"Move 3 Cards"`, `"Restyle Board"`.
///
/// A `count` of one or less folds to the singular a batch that turned out to name a single item
/// is one item, and a zero never reaches here because a gesture that wrote nothing registers
/// nothing.
public static func name(_ verb: Verb, kind: Kind, count: Int = 1) -> String {
guard count > 1, kind != .board else { return "\(verb.rawValue) \(kind.singular)" }
return "\(verb.rawValue) \(count) \(kind.plural)"
}
}