import Foundation // MARK: - HistoryStepOutcome /// What happened when a step was asked to walk its write back (or forward again). /// /// The second case is 13-native-undo.md ▸ Rules ▸ **staleness validation**, in the vocabulary the /// provider needs it in: "target folder gone, or the field no longer holding the step's after-value /// → the step is **skipped, not applied**: popped from the stack ... and ⌘Z falls through to the /// next step". The provider reads this answer and nothing else — the *predicate* (field-level, /// settled) and the info-tone banner that explains a skip both belong to the step, which is the only /// side that knows what it wrote and which board to say it on. public enum HistoryStepOutcome: Equatable, Sendable { /// The step ran. Its mirror image joins the opposite stack. case applied /// The step declined: the board no longer holds the value this step's write left, so applying /// its inverse would clobber somebody else's newer edit. Nothing ran, the step is dropped, and /// the crossing continues with the next one down. case skipped } // MARK: - HistoryStep /// One undoable step: a name, and the pair of actions that walk the board backwards and forwards /// across it. /// /// ### An operation pair, not an `NSUndoManager` registration /// /// 13-native-undo.md ▸ Rules puts every undoable change at the Writer boundary — "each Writer call /// site registers the inverse operation, computed from the pre-write snapshot the store already /// holds: move → move back ...; rename → restore title" — and both halves of that write are already /// in the caller's hands: the before-value *is* the inverse, and the after-value is what the write /// set (which is also what the staleness predicate compares). A step is therefore that pair, in the /// design's own terms, and deliberately says nothing about how a stack stores it: base's stack is /// `NSUndoManager`-backed and Pro's is git (12-editions.md ▸ The provider seam), and neither /// substrate appears here. /// /// ### `name` is the 06 vocabulary, unprefixed /// /// "The 06 vocabulary supplies menu titles ('Undo Move 3 Cards'), via NSUndoManager's dynamic /// retitling — the same naming machinery both editions use" (13). What the step carries is the bare /// phrase — `"Move Card"`, `"Move 3 Cards"`, `"Rename Lane"` — in the vocabulary of /// 06-history-undo.md ▸ Commit messages, plural-folded by the same rule ("one gesture, one undo /// step — a multi-card move is one step with a plural title"). The **"Undo "/"Redo " prefix is /// never part of it**: the platform composes and localizes that (`BoardUndoManager`), and a step /// that spelled it would read "Undo Undo Move Card" in the Edit menu. /// /// ### Both closures are `@MainActor` /// /// Everything they touch — the store, the snapshot, the banners — is, and a step exists to be run /// from a menu command. Marking them says so at the seam instead of leaving each provider to /// rediscover it. public struct HistoryStep { /// The menu phrase, unprefixed — see the type's note. public let name: String /// Walks the board back across this step. Registered at the Writer boundary as the *inverse* of /// the write that just landed. public let undo: @MainActor () -> HistoryStepOutcome /// Walks it forward again — the original write, replayed. Reached only after `undo` applied, /// because that is the only way a step reaches the redo stack. public let redo: @MainActor () -> HistoryStepOutcome public init( name: String, undo: @escaping @MainActor () -> HistoryStepOutcome, redo: @escaping @MainActor () -> HistoryStepOutcome ) { self.name = name self.undo = undo self.redo = redo } /// The same step read backwards — what a provider puts on the opposite stack once this one has /// applied. The name does not change, which is the whole of "Undo Move Card" becoming "Redo Move /// Card": the phrase names the *gesture*, not the direction. public var reversed: HistoryStep { HistoryStep(name: name, undo: redo, redo: undo) } } // MARK: - HistoryProviding /// The undo/redo substrate, behind one protocol boundary (12-editions.md ▸ The provider seam). /// /// ### One per board session /// /// "One stack per board, owned by the board session. Not per-window: every window over a board /// (board window, its card windows) shares the store and shares the stack" (13-native-undo.md /// ▸ Rules). `AppModel.BoardSession` is where that ownership lives, and the composition root binds /// which implementation it gets: base binds `NativeHistoryProvider` (an `NSUndoManager` stack over /// inverse `WriteOperation`s), Pro binds the git provider in pro-m1 (undo as forward restore commits /// over HEAD's first-parent ancestry — 06-history-undo.md), Teams inherits Pro's. /// /// ### What this protocol deliberately does not say /// /// - **No `NSUndoManager`, anywhere in the signature.** It is base's implementation detail, and a /// seam that vended one would be a seam only one provider could ever satisfy — the opposite of /// the reason the split exists at all ("base's native undo is the first proof the seam is real", /// 12). AppKit still needs an `UndoManager` to hand the responder chain; that adapter is /// `BoardUndoManager`, which sits *over* this protocol rather than inside it. /// - **No persistence promise.** Base's stack dies with the session (13); Pro's survives relaunch /// because git does (06). Both are honest implementations of these seven members. /// - **No routing.** Which surface ⌘Z reaches is focus's answer, not the substrate's /// (06 ▸ Undo routing, edition-independent) — `BoardUndoRouting`. @MainActor public protocol HistoryProviding: AnyObject { /// Records one undoable step, on top of the undo stack, clearing the redo stack — the classic /// rule, and the one every substrate shares. /// /// Called once per *gesture*, never once per write: "coalescing follows commit granularity ... /// a multi-card move is one step with a plural title; an Edit session is one step, registered at /// the Edit→Preview flip" (13 ▸ Rules). func register(_ step: HistoryStep) /// Whether there is a step to cross. What the Edit menu's Undo row and the toolbar's Undo item /// enable on, through the same responder-chain answer. var canUndo: Bool { get } var canRedo: Bool { get } /// The name of the step ⌘Z would cross, or `nil` when there is none — the phrase the menu title /// is composed from ("Move 3 Cards" → "Undo Move 3 Cards"). var undoActionName: String? { get } var redoActionName: String? { get } /// Crosses one step backwards. A stale step is skipped rather than applied, and the crossing /// falls through to the next one (13 ▸ Rules ▸ staleness validation); an empty stack does /// nothing. func undo() func redo() /// Drops every step in both directions — session-only persistence (13 ▸ Rules), run at the board /// session's teardown. Also what a substrate that must re-seed (a branch switch, 06) calls first. func clear() }