Bind the undo provider to the board, not the tier
The 2026-07-31 re-ruling: gitless boards bind the native stack in every tier — a Pro upgrade no longer removes undo from mode-none boards — and Pro git boards bind the git provider; repo-nested stays the no-undo case under Pro, while the free tier (which never runs detection) binds native there too, per 12's inert posture. Add-git now swaps a live native substrate mid-session: the in-flight stack is cleared with the discarded provider, the git trail seeds from the root commit, and the same BoardUndoManager instance keeps nil-target menu validation fresh. 2405 tests in 413 suites green. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
+93
-50
@@ -278,11 +278,12 @@ public final class AppModel {
|
||||
/// **The composition root for `HistoryProviding`** (12-editions.md ▸ The provider seam): what a
|
||||
/// board session's undo stack is built by, called once per board as its session begins.
|
||||
///
|
||||
/// The free tier binds the native stack — a pair of step stacks over the inverses registered at
|
||||
/// the Writer boundary (13-native-undo.md, `NativeHistoryProvider`) — and that is the default
|
||||
/// here because it is the substrate every tier can always fall back to; pro-m1 installs the git
|
||||
/// provider over this closure when the subscription is active (06-history-undo.md). Nothing in
|
||||
/// this file is tier-conditional; the tier difference is which closure the root installs.
|
||||
/// **The provider follows the board, not the tier alone** (re-ruled 2026-07-31 — 12 ▸ The
|
||||
/// provider seam; 13-native-undo.md's header; 06 ▸ Rules): a board's substrate is decided by what
|
||||
/// the board *is*, and the tier only decides whether git is on the table at all. The rule it
|
||||
/// replaced bound the native stack free-tier-wide and nothing at all on Pro's gitless boards,
|
||||
/// which made subscribing *remove* undo from a mode-none board — an upgrade that takes a feature
|
||||
/// away.
|
||||
///
|
||||
/// It takes the store because that is what a provider is a history *of*: the git provider needs
|
||||
/// the board root it is a repository at, and the native one's steps are computed from the same
|
||||
@@ -290,30 +291,40 @@ public final class AppModel {
|
||||
/// can bind a fake without a second `AppModel` initializer, `@ObservationIgnored` because
|
||||
/// nothing renders from it.
|
||||
///
|
||||
/// ### The three answers, and the `nil` among them
|
||||
/// ### The three answers, and the one `nil` among them
|
||||
///
|
||||
/// The closure is now the whole tier matrix, in three lines (12 ▸ Tier matrix; 06 ▸ Rules):
|
||||
///
|
||||
/// - **Free** — the native stack, on every board. There is no `HistoryStore` at all off Pro, so
|
||||
/// the absent git state *is* the tier test; nothing here reads a flag.
|
||||
/// - **Pro, mode `git`** — the git provider: undo as forward restore commits over HEAD's
|
||||
/// first-parent ancestry.
|
||||
/// - **Pro, mode `none` or `repoNested`** — **no provider**. "A board without git has no
|
||||
/// undo/redo", and a repo-nested board is one the app "leaves strictly alone" — so Edit
|
||||
/// ▸ Undo/Redo and the toolbar pair disable there ("the pair disabled on boards with no undo
|
||||
/// provider in the composed tier — under Pro, no-git and repo-nested boards, matching their menu
|
||||
/// items", 03-board-ui.md ▸ Toolbar). Not the native stack: on Pro, a mode-none board's edits
|
||||
/// are deliberately unhistoried, and half-undoing them from an in-memory stack would be a second
|
||||
/// substrate the design does not have.
|
||||
/// - **No `HistoryStore` at all** — the free tier, where `HistoryStore.compose` returns `nil`
|
||||
/// without so much as a `stat`: the **native stack, on every board**. "The free tier binds it
|
||||
/// everywhere (any `.git` inert)" (13), and 12 ▸ The free tier and `.git` names the boards that
|
||||
/// covers by hand — "a formerly-subscribed user's board, a 1.x board, a repo-nested board …
|
||||
/// native undo runs". The absent git state *is* the tier test; nothing here reads a flag.
|
||||
/// - **Mode `git`** (Pro only — no other tier composes a git state) — the git provider: undo as
|
||||
/// forward restore commits over HEAD's first-parent ancestry (06).
|
||||
/// - **Mode `none`** — the **native stack**, exactly as in the free tier. "Gitless boards bind
|
||||
/// the native undo stack in every tier — an upgrade never removes undo" (12).
|
||||
/// - **Mode `repoNested`** — **no provider**, the one no-undo case: a board inside somebody
|
||||
/// else's repository is one the app "leaves strictly alone … so they get **no undo**" (06
|
||||
/// ▸ Rules), and an in-memory stack there would be the app-managed undo journal that rule
|
||||
/// forbids. Edit ▸ Undo/Redo and the toolbar pair disable there and nowhere else but under a
|
||||
/// lock and on an empty stack (03-board-ui.md ▸ Toolbar ▸ Catalog).
|
||||
///
|
||||
/// The `HistoryStore` argument is what makes that decidable here, and it is why `beginSession`
|
||||
/// composes the git state *before* the provider: which substrate a board gets is a question about
|
||||
/// its repository, and a root that had to ask the disk itself would be a second detection.
|
||||
///
|
||||
/// ### Consumers
|
||||
///
|
||||
/// `beginSession` calls it once per board, and `bindHistoryProvider(for:)` calls it again on the
|
||||
/// one event that changes a board's answer under an open session — add-git's commanded flip,
|
||||
/// which swaps `none`'s native stack for `git`'s trail.
|
||||
@ObservationIgnored
|
||||
public var makeHistoryProvider: (BoardStore, Tier, HistoryStore?) -> (any HistoryProviding)? = { store, _, git in
|
||||
guard let git else { return NativeHistoryProvider() }
|
||||
guard git.mode == .git else { return nil }
|
||||
return GitHistoryProvider(boardRoot: store.rootURL)
|
||||
switch git.mode {
|
||||
case .git: return GitHistoryProvider(boardRoot: store.rootURL)
|
||||
case .none: return NativeHistoryProvider()
|
||||
case .repoNested: return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Sessions
|
||||
@@ -335,13 +346,15 @@ public final class AppModel {
|
||||
/// Which implementation it is, is the tier's answer and nobody else's
|
||||
/// (12-editions.md ▸ The provider seam) — see `AppModel.makeHistoryProvider`.
|
||||
///
|
||||
/// **`nil` is a board with no undo at all** — under Pro, mode `none` and repo-nested boards
|
||||
/// (06-history-undo.md ▸ Rules: "A board without git has **no undo/redo**"). The command
|
||||
/// surface disables through `undoManager`, which answers the empty way over an absent
|
||||
/// substrate.
|
||||
/// **`nil` is a board with no undo at all** — a repo-nested board under Pro, and nothing else
|
||||
/// (06-history-undo.md ▸ Rules: boards inside an existing repository "get **no undo**").
|
||||
/// Gitless boards bind the native stack in every tier (re-ruled 2026-07-31 — see
|
||||
/// `AppModel.makeHistoryProvider`). The command surface disables through `undoManager`, which
|
||||
/// answers the empty way over an absent substrate.
|
||||
///
|
||||
/// A `var`, unlike `tier` beside it, and for one event only: **add-git**, the design's single
|
||||
/// sanctioned mid-session mode flip, binds a provider here on the board it flips
|
||||
/// sanctioned mid-session mode flip, *swaps* the substrate here on the board it flips —
|
||||
/// native out, git in, the in-session steps discarded with it
|
||||
/// (`bindHistoryProvider(for:)`). A tier lapse still cannot touch it — `tier` has no setter.
|
||||
public var history: (any HistoryProviding)?
|
||||
|
||||
@@ -379,11 +392,11 @@ public final class AppModel {
|
||||
/// is every free-tier session ("The free tier ships exactly one mode: `none`",
|
||||
/// 12-editions.md ▸ Tier matrix).
|
||||
///
|
||||
/// **The consumer this is waiting for is the git `HistoryProviding` implementation** — the
|
||||
/// undo/redo card two cards further into pro-m1, which binds over `makeHistoryProvider` and
|
||||
/// reads exactly this to know whether it has a repository to be an undo stack for. Until it
|
||||
/// lands both tiers bind the native stack, and this is a recorded fact with one reader: the
|
||||
/// popover's git section.
|
||||
/// **Its first consumer is the provider seam** — `makeHistoryProvider` reads exactly this to
|
||||
/// know whether the board has a repository to be an undo stack for, and it is the *mode*
|
||||
/// rather than the tier that decides (re-ruled 2026-07-31): `git` binds the git provider,
|
||||
/// `none` the native stack, `repoNested` nothing. The popover's git section is the other
|
||||
/// reader.
|
||||
///
|
||||
/// `@MainActor` because the state it reads is: a nested type does not inherit its enclosing
|
||||
/// type's isolation, and everything that asks a session what mode it is in is main-actor
|
||||
@@ -799,8 +812,9 @@ public final class AppModel {
|
||||
provider.noteLanded(window)
|
||||
}
|
||||
}
|
||||
// **Add-git binds undo too** (06 ▸ Rules ▸ Detection — the one commanded mid-session mode
|
||||
// flip). See `bindHistoryProvider(for:)` for the judgment call this records.
|
||||
// **Add-git swaps the undo substrate too** (06 ▸ Rules ▸ Detection — the one commanded
|
||||
// mid-session mode flip; 13-native-undo.md's header — "discards the in-session native
|
||||
// stack and seeds the git trail from the root commit"). See `bindHistoryProvider(for:)`.
|
||||
git.didAddGit = { [weak self] in
|
||||
self?.bindHistoryProvider(for: ref)
|
||||
}
|
||||
@@ -1014,25 +1028,53 @@ public final class AppModel {
|
||||
)
|
||||
}
|
||||
|
||||
/// **Binds the git provider onto an already-open session** — add-git's one caller.
|
||||
/// **Swaps the board's undo substrate onto an already-open session** — add-git's one caller.
|
||||
///
|
||||
/// 06 ▸ Rules ▸ Detection sanctions exactly one mid-session mode flip, the app's own add-git:
|
||||
/// "clicking it flips the open board into git mode immediately — the popover flows straight into
|
||||
/// the git controls, the first auto-commit follows". It does not mention undo, so this is a
|
||||
/// judgment call and it is recorded here: **the flip binds undo too**, live, rather than waiting
|
||||
/// for the next open. Three reasons point the same way — the mode flip already carries the
|
||||
/// *committer* through (`HistoryStore.activateAutoCommit` remembers its wiring for precisely this
|
||||
/// board); 12-editions.md's "an open board finishes with the provider it composed" is a rule about
|
||||
/// a **tier** lapsing, which cannot change a running session at all; and a board that visibly
|
||||
/// starts accumulating commits while ⌘Z stays greyed out until it is closed and reopened would
|
||||
/// read as a defect rather than as a policy.
|
||||
/// the git controls, the first auto-commit follows". 13-native-undo.md's header spells out what
|
||||
/// that does to undo: "**Add-git swaps the substrate mid-session** — the commanded flip discards
|
||||
/// the in-session native stack and seeds the git trail from the root commit, the branch-switch
|
||||
/// discard-and-reseed precedent applied".
|
||||
///
|
||||
/// It only ever adds. A board that already has a provider keeps it, and nothing here can take one
|
||||
/// away — there is no un-add-git.
|
||||
/// ### The discard is the whole of it — there is no migration
|
||||
///
|
||||
/// The board opened mode-none under any tier now carries a live `NativeHistoryProvider` with real
|
||||
/// steps on it (`makeHistoryProvider`), and those steps **die with the substrate**: they are
|
||||
/// in-memory inverse operations against a board that has just acquired a commit trail, and
|
||||
/// replaying one after the swap would walk the board back across a change the root commit already
|
||||
/// records as the baseline. The branch-switch precedent says the same thing about the same
|
||||
/// question — "the undo/redo stack does not survive a switch. It is discarded and reseeded from
|
||||
/// the new HEAD's first-parent ancestry … redo starts empty" (06 ▸ Branch switching) — so the old
|
||||
/// stack is cleared rather than merely dropped, and the git provider's `seed()` (in `wireGitUndo`)
|
||||
/// walks a trail whose only commit is the root, which is the stack's floor and not a step: ⌘Z is
|
||||
/// correctly empty the instant the flip lands.
|
||||
///
|
||||
/// **Nothing is announced.** 06 gives the flip the popover's own flow ("straight into the git
|
||||
/// controls") and 13 gives the discard no surface at all, exactly as the branch switch's discard
|
||||
/// has none; what the user sees is the Edit rows and the toolbar pair revalidating through
|
||||
/// `BoardUndoManager` on AppKit's own cadence, which is the same machinery every other enablement
|
||||
/// change on this board rides. Nothing here posts a banner, and nothing should.
|
||||
///
|
||||
/// ### Why live rather than at the next open
|
||||
///
|
||||
/// A judgment call, recorded when the free-tier matrix still left this board with no provider at
|
||||
/// all: the mode flip already carries the *committer* through (`HistoryStore.activateAutoCommit`
|
||||
/// remembers its wiring for precisely this board); 12-editions.md's "an open board finishes with
|
||||
/// the provider it composed" is a rule about a **tier** lapsing, which cannot change a running
|
||||
/// session at all; and a board that visibly starts accumulating commits while ⌘Z answers from a
|
||||
/// stack the repository knows nothing about would read as a defect rather than as a policy.
|
||||
///
|
||||
/// Called exactly once per board, structurally: `HistoryStore.addGit` refuses any mode but
|
||||
/// `none`, and flips to `.git` before it fires `didAddGit`.
|
||||
func bindHistoryProvider(for ref: BoardWindowRef) {
|
||||
guard var session = sessions[ref], session.history == nil,
|
||||
let git = session.git, git.mode == .git else { return }
|
||||
guard var session = sessions[ref], let git = session.git, git.mode == .git else { return }
|
||||
guard let history = makeHistoryProvider(session.store, session.tier, git) else { return }
|
||||
guard history !== session.history else { return }
|
||||
// Before the reassignment, while `session.history` is still the substrate being replaced: the
|
||||
// in-flight native steps go with it, and any closure that outlives this line finds an empty
|
||||
// stack rather than inverses against a pre-repository board.
|
||||
session.history?.clear()
|
||||
session.history = history
|
||||
sessions[ref] = session
|
||||
session.store.history = history
|
||||
@@ -1256,10 +1298,11 @@ public final class AppModel {
|
||||
///
|
||||
/// ### Declining costs nothing, today least of all
|
||||
///
|
||||
/// Both tiers bind the native history stack until pro-m1 (`makeHistoryProvider`), so a user who
|
||||
/// says Not Now loses exactly nothing that exists yet. The offer is built now because the
|
||||
/// *mechanism* is what the design specifies and because a milestone that shipped the subscription
|
||||
/// without it would leave a visible gap the moment the git provider lands.
|
||||
/// A board that says Not Now keeps the substrate it composed with, and since the provider follows
|
||||
/// the board (`makeHistoryProvider`), a gitless board's undo is the *same* native stack either
|
||||
/// way — declining costs undo nothing at all, and costs a git board only the trail it would have
|
||||
/// gained at its next open. The offer exists because "subscribe takes effect at each board's next
|
||||
/// open" (12 ▸ The entitlement) needs one, not because anything breaks without it.
|
||||
public func reopenOpenBoards() async {
|
||||
// Sorted for `flushAllBoardsForQuit`'s reason: a reproducible order rather than a `Set`'s.
|
||||
let refs = sessions.keys.sorted { $0.path < $1.path }
|
||||
|
||||
@@ -159,8 +159,10 @@ public final class GitAutoCommitter {
|
||||
/// this engine clears the instant a window commits — so the moment of landing is the only moment
|
||||
/// at which "that commit was the heal" is knowable at all.
|
||||
///
|
||||
/// `nil` on every board with no undo provider bound (the free tier's committer does not exist;
|
||||
/// a Pro board's does, and a provider is bound beside it).
|
||||
/// `nil` wherever no `GitHistoryProvider` is listening — which in practice is nowhere a committer
|
||||
/// exists at all: a committer's existence is exactly mode `git`, and mode `git` is exactly where
|
||||
/// the composition root binds the git provider (`AppModel.makeHistoryProvider`). Mode-none boards
|
||||
/// have a native stack and no committer; repo-nested boards have neither.
|
||||
@ObservationIgnored
|
||||
public var reportLanded: (@MainActor (GitLandedWindow) -> Void)?
|
||||
|
||||
|
||||
@@ -19,10 +19,10 @@ import os
|
||||
/// ### What it does not do yet
|
||||
///
|
||||
/// This is the foundation card of pro-m1: mode, a repository, add-git, and the loader's path-history
|
||||
/// ranker. **The provider binding is not part of it** — both tiers still bind
|
||||
/// `NativeHistoryProvider` (`AppModel.makeHistoryProvider`), and the consumer of `mode` is the
|
||||
/// undo/redo card two cards later, which builds the git `HistoryProviding` implementation over
|
||||
/// exactly this object. Auto-commit, commit messages, branch controls, the identity fields, the
|
||||
/// ranker. **The provider binding reads `mode` and nothing else about a tier** — the composition
|
||||
/// root binds the git provider on mode `git`, the native stack on mode `none`, and nothing on a
|
||||
/// repo-nested board (`AppModel.makeHistoryProvider`, re-ruled 2026-07-31: the provider follows the
|
||||
/// board). Auto-commit, commit messages, branch controls, the identity fields, the
|
||||
/// `.gitignore` seed and its periodic housekeeping each arrived as their own card and are composed
|
||||
/// here now; remotes are pro-m2's and deliberately still absent.
|
||||
@MainActor
|
||||
@@ -141,9 +141,11 @@ public final class HistoryStore {
|
||||
/// never on any other path.
|
||||
///
|
||||
/// It exists because the flip has a second consumer beyond the committer: the board's **undo
|
||||
/// substrate**. A session that composed on a mode-none board bound no provider at all
|
||||
/// substrate**. A session that composed on a mode-none board bound the native stack
|
||||
/// (`AppModel.makeHistoryProvider`), and 06 ▸ Rules ▸ Detection's one sanctioned commanded flip
|
||||
/// means the board now has a trail to be an undo stack over. What binding it means is
|
||||
/// means the board now has a trail to be an undo stack over instead — "add-git swaps the
|
||||
/// substrate mid-session … discards the in-session native stack and seeds the git trail from the
|
||||
/// root commit" (13-native-undo.md). What that swap means is
|
||||
/// `AppModel.bindHistoryProvider(for:)`'s to decide and to justify; what this property does is
|
||||
/// keep that decision out of a git state that has no business knowing what a provider is.
|
||||
@ObservationIgnored
|
||||
|
||||
@@ -13,13 +13,13 @@ import AppKit
|
||||
/// exactly how the system's Edit ▸ Undo row and the toolbar's nil-target pair (`BoardToolbar`) light
|
||||
/// up, disable and retitle with no code of the app's own.
|
||||
///
|
||||
/// The seam, though, must not be an `NSUndoManager`: the free tier's stack is one, Pro's is git
|
||||
/// (12-editions.md ▸ The provider seam), and a protocol that vended one could only ever have had a
|
||||
/// single implementation. So the substrate stays behind `HistoryProviding` and *this* object is the
|
||||
/// translation — one per board session, over whichever provider that session was composed with. Pro
|
||||
/// inherits the whole command surface (enablement, dynamic titles, ⌘Z, the toolbar pair) by binding
|
||||
/// its provider and changing nothing here, which is what "a user subscribing (or lapsing) relearns
|
||||
/// nothing" (12) has to mean in code.
|
||||
/// The seam, though, must not be an `NSUndoManager`: a gitless board's stack is one, a Pro git
|
||||
/// board's is git (12-editions.md ▸ The provider seam), and a protocol that vended one could only
|
||||
/// ever have had a single implementation. So the substrate stays behind `HistoryProviding` and
|
||||
/// *this* object is the translation — one per board session, over whichever provider that session
|
||||
/// was composed with. A git board inherits the whole command surface (enablement, dynamic titles,
|
||||
/// ⌘Z, the toolbar pair) by binding its provider and changing nothing here, which is what "a user
|
||||
/// subscribing (or lapsing) relearns nothing" (12) has to mean in code.
|
||||
///
|
||||
/// ### It deliberately keeps its inherited stack empty
|
||||
///
|
||||
@@ -51,22 +51,24 @@ public final class BoardUndoManager: UndoManager {
|
||||
///
|
||||
/// ### `nil` is a board with **no undo provider**, and it is a real state
|
||||
///
|
||||
/// Under Pro, a board in mode `none` or `repoNested` gets no provider at all — "the pair disabled
|
||||
/// on boards with no undo provider in the composed tier — under Pro, no-git and repo-nested
|
||||
/// boards, matching their menu items" (03-board-ui.md ▸ Toolbar ▸ Catalog; 06-history-undo.md
|
||||
/// ▸ Rules). Every question below answers the empty way, so the Edit menu's rows, the toolbar
|
||||
/// pair, and ⌘Z itself go quiet together, through the same validation path a lock uses. Modelling
|
||||
/// it as an absent substrate rather than as a substrate that always says no is the honest shape:
|
||||
/// there is nothing there, and nothing can accidentally accumulate in it.
|
||||
/// A **repo-nested** board under Pro gets no provider at all, and since the re-ruling of
|
||||
/// 2026-07-31 it is the only board that does: "the pair disabled only on repo-nested boards,
|
||||
/// under locks, and on empty stacks — the provider follows the board, so gitless boards bind
|
||||
/// 13-native-undo.md's native stack in **every** tier" (03-board-ui.md ▸ Toolbar ▸ Catalog;
|
||||
/// 06-history-undo.md ▸ Rules). Every question below answers the empty way, so the Edit menu's
|
||||
/// rows, the toolbar pair, and ⌘Z itself go quiet together, through the same validation path a
|
||||
/// lock uses. Modelling it as an absent substrate rather than as a substrate that always says no
|
||||
/// is the honest shape: there is nothing there, and nothing can accidentally accumulate in it.
|
||||
///
|
||||
/// ### Settable, for exactly one event
|
||||
///
|
||||
/// **Add-git** (06 ▸ Rules ▸ Detection) is the design's one sanctioned mid-session mode flip:
|
||||
/// "clicking it flips the open board into git mode immediately — the popover flows straight into
|
||||
/// the git controls, the first auto-commit follows". A board that gains a repository mid-session
|
||||
/// gains a commit trail, and a trail with a dead ⌘Z over it would read as a bug. The composition
|
||||
/// root binds the git provider here on that flip, rather than rebuilding this object, so AppKit
|
||||
/// keeps the identical manager it has already been handed by `windowWillReturnUndoManager`.
|
||||
/// "clicking it flips the open board into git mode immediately". 13's header says what that means
|
||||
/// here — the flip **swaps the substrate**: the mode-none board's native stack is discarded and
|
||||
/// the git trail seeded from the root commit, the branch-switch discard-and-reseed precedent. The
|
||||
/// composition root writes the new provider into this property rather than rebuilding this
|
||||
/// object, so AppKit keeps the identical manager it has already been handed by
|
||||
/// `windowWillReturnUndoManager` and simply revalidates over a different stack.
|
||||
///
|
||||
/// (This is *not* a tier flip. 12-editions.md's "an open board finishes with the provider it
|
||||
/// composed" is about a subscription lapsing, which cannot change a running session's tier at
|
||||
|
||||
@@ -80,9 +80,9 @@ public enum HistoryStepOutcome: Equatable, Sendable {
|
||||
/// 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: the free tier's
|
||||
/// stack is `NSUndoManager`-backed and Pro's is git (12-editions.md ▸ The provider seam), and neither
|
||||
/// substrate appears here.
|
||||
/// design's own terms, and deliberately says nothing about how a stack stores it: a gitless board's
|
||||
/// stack is `NSUndoManager`-backed and a Pro git board's is git (12-editions.md ▸ The provider
|
||||
/// seam), and neither substrate appears here.
|
||||
///
|
||||
/// ### `name` is the 06 vocabulary, unprefixed
|
||||
///
|
||||
@@ -143,10 +143,11 @@ public struct HistoryStep {
|
||||
/// "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: the free tier binds `NativeHistoryProvider` (two step stacks over
|
||||
/// the inverses registered at the Writer boundary), 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.
|
||||
/// which implementation it gets — **following the board, not the tier alone** (re-ruled 2026-07-31):
|
||||
/// a gitless board binds `NativeHistoryProvider` (two step stacks over the inverses registered at
|
||||
/// the Writer boundary) in every tier, a Pro git board binds the git provider (undo as forward
|
||||
/// restore commits over HEAD's first-parent ancestry — 06-history-undo.md), a repo-nested board
|
||||
/// binds none at all, and Teams inherits Pro's.
|
||||
///
|
||||
/// ### What this protocol deliberately does not say
|
||||
///
|
||||
|
||||
@@ -29,9 +29,10 @@ import SwiftUI
|
||||
///
|
||||
/// "Subscribe takes effect at each board's next open ... The purchase flow offers to reopen open
|
||||
/// boards so the upgrade feels immediate" (12 ▸ The entitlement). The offer is raised **once**, from
|
||||
/// here, on the one outcome that means an active subscription just landed — and declining costs
|
||||
/// nothing, today least of all: both tiers bind the native history stack until pro-m1 builds the git
|
||||
/// provider (`AppModel.makeHistoryProvider`).
|
||||
/// here, on the one outcome that means an active subscription just landed — and declining costs a
|
||||
/// gitless board nothing at all: the provider follows the board, so its undo is the same native
|
||||
/// stack before and after (`AppModel.makeHistoryProvider`). What a Not Now defers is the git trail
|
||||
/// on the boards that have a repository.
|
||||
struct ProSettingsSection: View {
|
||||
|
||||
@Environment(AppModel.self) private var appModel
|
||||
|
||||
@@ -23,8 +23,11 @@ public enum Tier: String, Sendable, Equatable, Codable, CaseIterable {
|
||||
/// existing at all.
|
||||
case free
|
||||
|
||||
/// Lanework Pro — an active auto-renewable subscription. Binds the git history provider when
|
||||
/// pro-m1 builds it (06-history-undo.md, 07-sync-collab.md).
|
||||
/// Lanework Pro — an active auto-renewable subscription. It is what puts git on the table; which
|
||||
/// substrate a given board then binds is the *board's* answer, not this case's (re-ruled
|
||||
/// 2026-07-31 — `AppModel.makeHistoryProvider`): the git provider on a git board
|
||||
/// (06-history-undo.md, 07-sync-collab.md), the same native stack the free tier uses on a gitless
|
||||
/// one, nothing on a repo-nested one.
|
||||
case pro
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user