import Foundation // MARK: - GitOperationStamp /// **The app's declaration that it is about to touch the repository** (06-history-undo.md ▸ Rules /// ▸ Abnormal repo states: "every bracketed operation stamps its intent app-side (per-board registry) /// before touching the repo, so an interrupted app-run rebase or checkout is recognizable as /// Lanework's"). /// /// ### Why it exists at all /// /// The app's standing posture toward a repository it finds in a pause state is to **hold and defer**: /// name the state, disable the controls, and let the tool that created it finish. That posture is /// correct for every leftover except one — the app's own. A checkout interrupted by a crash (or by a /// volume vanishing mid-flight — 02-architecture.md ▸ the root-change composition) leaves a repository /// whose state nobody in a terminal is going to finish, and deferring to a rebase that does not exist /// would leave the board's git surface paused forever. /// /// So the app writes down what it is about to do, **before** it does it. Finding a pause state on a /// later open *with* a matching stamp, it aborts its own unfinished work and says so; finding one /// without, the pause-and-defer stance is unchanged. The stamp is the only thing that distinguishes /// the two, which is why it is written before the first byte and cleared after the last. /// /// ### Where it lives, and why not in the repository /// /// The **per-board registry** — `BoardRecord.gitOperationStamp`, in the app's own Application Support /// home. Two rules pin it there. Files-first is absolute (02 ▸ Per-board app state): "no frontmatter /// key, no sidecar, no xattr" — a marker file in the board folder would be board content, committed by /// the very operation it describes. And the never-mutate rule forbids the obvious git-shaped home: a /// file under `.git/` would be app-written repo state, which is exactly what this mechanism exists to /// keep the app out of. /// /// A consequence worth stating: the stamp is **per machine**, like every other registry record. A /// board whose switch was interrupted on one Mac and then opened on another reads as an ordinary /// unexplained pause — hold, name it, defer — which is the honest answer, since the second machine /// genuinely does not know whose leftover it is. public struct GitOperationStamp: Codable, Sendable, Equatable { /// Which bracketed operation this stamp is for. /// /// One case today. It is an enum rather than a bare marker because 06 names the mechanism for /// "every bracketed operation" and the pull's rebase (07-sync-collab.md) is the next one to stamp; /// a new case then needs no migration, because an unknown-to-old-builds case never appears in a /// file an old build wrote. public enum Kind: String, Codable, Sendable, CaseIterable { case branchSwitch } public let kind: Kind /// The branch HEAD named **before** the operation — where an abort returns to. Empty when the /// repository had no branch to name (a detached HEAD the switch was starting from, which the /// paused-surface rule makes unreachable today). public let fromBranch: String /// The branch the operation was heading for. Not used by the abort — recorded because a recovery /// that could not say what was interrupted would be a worse diagnostic than one that can. public let toBranch: String /// HEAD's commit before the operation, or `nil` on an unborn HEAD. Recorded for the same reason: /// it is the fact a support question ("what was it doing?") is answered with. public let headOID: String? public init(kind: Kind = .branchSwitch, fromBranch: String, toBranch: String, headOID: String?) { self.kind = kind self.fromBranch = fromBranch self.toBranch = toBranch self.headOID = headOID } /// **What the banner says after a successful abort** — 06's own sentence, with the app's /// sentence-shaped capitalization. public static let interruptionMessage = "A branch switch was interrupted — the previous state is restored." } // MARK: - Recovery /// **What to do about a stamp found at open** — a pure decision, so the mechanism's whole rule is /// provable without a repository in a broken state. public enum GitOperationRecovery: Sendable, Equatable { /// No stamp: the ordinary case, and the one every board is in. The pause-and-defer stance applies /// unchanged to whatever state the repository happens to be in. case nothingToDo /// A stamp, but a repository in a state the app writes in perfectly well. The operation finished /// and the clear did not land — a quit between the two, or a registry write that lost a race — so /// there is nothing to abort and the stamp is stale. Dropping it silently is right: nothing /// happened that the user needs told about. case clearStamp /// A stamp **and** a pause state: the app's own unfinished operation. Abort it, restore the /// pre-operation state, say so, then clear. case abort(GitOperationStamp) /// The whole rule, in one function. /// /// The conjunction is the point: a pause **without** a stamp is somebody else's operation and the /// app must not touch it, and a stamp **without** a pause is the app's own finished work. Only /// both together are "the app's own leftovers". public static func decide( stamp: GitOperationStamp?, pause: GitRepositoryPause? ) -> GitOperationRecovery { guard let stamp else { return .nothingToDo } guard pause != nil else { return .clearStamp } return .abort(stamp) } }