Files
lanework/Kanban/History/HistoryPhrase.swift
T
rzen 092300c7d2 Collapse the edition split — one target, twins merged, verify-editions retired
Phase 1 of the 2026-07-30 one-app pivot (DESIGN 0bec9a6, card c3a3ddd5):
the KanbanPro target, LaneworkPro scheme, KanbanProTests module-alias
bundle, KanbanPro/ source root and scripts/verify-editions.sh retire
wholesale. project.yml reads as a single-target file again (anchors
inlined, header rewritten in tier vocabulary).

The edition twins merge: EditionTypes -> PasteboardTypes (one
UTType(exportedAs:) home — the one app owns the family types),
EditionAbout -> AboutBox (the quiet Pro signpost survives as the About
box's one line; "…in Settings" deferred until the StoreKit phase gives
it somewhere to point). InertGitTests drops its Base prefix — the
inert-.git posture is unconditional app behavior, unsubscribed and
lapsed being one state.

Entitlements gain com.apple.security.network.client, declared now and
dormant until Pro's remotes use it; no keychain access group. The App
Group key deliberately stays — it goes with AppGroup.swift in phase 2,
since pulling it first would silently drop the app into the fallback
container. README/RELEASE.md build-and-pipeline prose updated to the
one-record world; the subscription story lands with phase 3.

1893 tests in 322 suites green.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
2026-07-30 17:25:50 -04:00

97 lines
4.2 KiB
Swift

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
/// tiers 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 delete — ⌫, drop-on-trash, the card window's Actions ▸ Delete — which is a *move* into
/// `.trash/` for a card and a physical removal for a lane (03-board-ui.md § Trash).
///
/// **There is no `restore` verb**: restoring is an ordinary move out (drag or ⌘X/⌘V), so it
/// registers as `move` like any other, and Put Back is retired with the tombstone model
/// (resettled 2026-07-28).
case delete = "Delete"
/// 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 Edit→Preview 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)"
}
}