The tier axis comes out of the git stack — compose unconditional, postures mode-driven

HistoryStore.compose(boardRoot📒) returns non-optional and runs for
every session — the nil the gate produced was the only nil it ever had.
makeHistoryProvider is a one-axis decision: git-mode boards bind the git
provider, everything else native, in every tier; Session.tier stays
recorded, dormant. BoardGitSection shrinks to the four mode postures
(.absent and .proPointer die, BoardGitNote and the .git probe with them);
every board carries all three popover tabs (BoardInfoTab.available
retired); the titlebar branch shows on any git-mode board; the settings
sheet and card History section stop reading tier. InertGitTests is
repurposed as UntouchedGitTests — the file layer still never opens .git,
now load-bearing for mode-none boards. The accessibility audit reaches the
settings sheet at last: the fixture board hosts it in every tier, so the
free-fixture disabled-row test becomes an open-and-audit test.

Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
2026-08-07 20:21:28 -04:00
parent da0d7fd2d7
commit 798a8bac73
32 changed files with 830 additions and 904 deletions
+159 -141
View File
@@ -335,14 +335,20 @@ public final class AppModel {
/// **The tier a board session composes under**, as an injectable seam.
///
/// Defaulted to the real entitlement's local read and separated from it for `makeHistoryProvider`'s
/// reason exactly: a test binds a tier without needing a StoreKit transaction, an App Store
/// account, or a second `AppModel` initializer. `@MainActor` on the closure type because the
/// entitlement it reads is main-actor state, and `@ObservationIgnored` because nothing renders
/// from it the tier reaches the UI, where it reaches it at all, through `entitlement`.
/// Defaulted to the real entitlement's local read and separated from it so a test binds a tier
/// without needing a StoreKit transaction, an App Store account, or a second `AppModel`
/// initializer. `@MainActor` on the closure type because the entitlement it reads is main-actor
/// state, and `@ObservationIgnored` because nothing renders from it the tier reaches the UI,
/// where it reaches it at all, through `entitlement`.
///
/// **Read once per session, at composition, and never again** (12 The entitlement: "a lapse
/// never interrupts an open session"). `beginSession` is the only caller.
///
/// **Dormant since PIVOT 2026-08-07** (12-editions.md git left the paywall): the answer is
/// still read and still recorded on the session (`BoardSession.tier`), and **nothing git-shaped
/// consults it any more** `HistoryStore.compose` and `makeHistoryProvider` both lost the axis.
/// The seam is kept unchanged, mechanics and all, because the entitlement is correct for
/// whatever the next base/Pro split turns out to gate; what it does not do is decide undo.
@ObservationIgnored
public var currentTier: @MainActor () -> Tier = { .free }
@@ -351,12 +357,14 @@ 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 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.
/// **The provider follows the board and, since PIVOT 2026-08-07, the board alone**
/// (re-ruled 2026-07-31 12 The provider seam; 13-native-undo.md's header; 06 Rules; then
/// 12 PIVOT 2026-08-07, git leaves the paywall). A board's substrate is decided by what the
/// board *is*. The 2026-07-31 re-ruling took the tier out of every row but one it still decided
/// whether git was on the table at all and the pivot took it out of that one too: "every tier
/// composes the git stack on git-mode boards exactly as Pro did". The rule the re-ruling 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
@@ -364,21 +372,24 @@ public final class AppModel {
/// can bind a fake without a second `AppModel` initializer, `@ObservationIgnored` because
/// nothing renders from it.
///
/// ### The two answers, and the `nil` that is no longer one of them
/// ### The two answers, and the two `nil`s that are no longer among them
///
/// - **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`, `repoNested`, and `unverifiable` alike** the **native stack**, exactly as
/// in the free tier. "Boards without app-managed git repo-nested included bind
/// 13-native-undo.md's native stack in **every** tier" (03-board-ui.md Toolbar Catalog,
/// re-ruled 2026-07-31 twice; 12 The provider seam; 13's header). `unverifiable` joins the
/// same branch structurally a denial can never be told apart from a repository actually
/// being there, so it takes `repoNested`'s posture, undo included (06 Rules Detection).
/// - **Mode `git`** the git provider: undo as forward restore commits over HEAD's first-parent
/// ancestry (06). **In every tier** since the pivot; this row used to read "Pro only no other
/// tier composes a git state", because `HistoryStore.compose` answered `nil` off Pro without so
/// much as a `stat`.
/// - **Mode `none`, `repoNested`, and `unverifiable` alike** the **native stack**. "Boards
/// without app-managed git repo-nested included bind 13-native-undo.md's native stack in
/// **every** tier" (03-board-ui.md Toolbar Catalog, re-ruled 2026-07-31 twice; 12 The
/// provider seam; 13's header). `unverifiable` joins the same branch structurally a denial
/// can never be told apart from a repository actually being there, so it takes `repoNested`'s
/// posture, undo included (06 Rules Detection). This is where a board nobody opted git in
/// for lands, permanently: opt-in per board is exactly what the pivot left standing (13's
/// header, read through 12 PIVOT 2026-08-07).
/// - **No `HistoryStore` at all** the argument stays optional for the seam's own reason (a test
/// binds a git-stateless board through it), and no session composes one: `compose` returns a
/// store for every board there is. It answers the native stack, which is what it always
/// answered for the free tier that used to arrive this way.
///
/// **The repo-nested no-undo case is gone** (re-ruled 2026-07-31): 06's leave-strictly-alone
/// stance "concerns *git*, and this stack never touches git memory-only, journal-free,
@@ -402,7 +413,7 @@ public final class AppModel {
/// 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
public var makeHistoryProvider: (BoardStore, HistoryStore?) -> (any HistoryProviding)? = { store, git in
guard let git else { return NativeHistoryProvider() }
switch git.mode {
case .git: return GitHistoryProvider(boardRoot: store.rootURL)
@@ -429,8 +440,9 @@ public final class AppModel {
/// window). It lives here for the store's reason exactly: the session is what every window
/// over this board shares, and "undo is board-local".
///
/// Which implementation it is, is the tier's answer and nobody else's
/// (12-editions.md The provider seam) see `AppModel.makeHistoryProvider`.
/// Which implementation it is, is the board's answer and nobody else's
/// (12-editions.md The provider seam, read through PIVOT 2026-08-07) see
/// `AppModel.makeHistoryProvider`.
///
/// **`nil` is a board with no undo at all, and no board the app composes is one any more**
/// (re-ruled 2026-07-31 see `AppModel.makeHistoryProvider`): boards without app-managed git,
@@ -443,33 +455,35 @@ public final class AppModel {
/// A `var`, unlike `tier` beside it, and for one event only: **add-git**, the design's single
/// 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.
/// (`bindHistoryProvider(for:)`). A tier lapse still cannot touch it `tier` has no setter,
/// and since PIVOT 2026-08-07 it has no say in this either.
public var history: (any HistoryProviding)?
/// **The tier this board composed under** (12-editions.md The entitlement).
/// **The tier this board composed under** (12-editions.md The entitlement) recorded,
/// and **dormant since PIVOT 2026-08-07**.
///
/// A `let`, on a value type, set once by `beginSession` which is the entire mechanism
/// behind "a lapse never interrupts an open session: an open board finishes with the provider
/// it composed; the next open composes the native stack over inert `.git`". There is no
/// setter, no observation, and nothing anywhere that re-evaluates a live session's tier: a
/// subscription ending mid-session is a fact about the *next* open and about nothing that is
/// already on screen.
/// A `let`, on a value type, set once by `beginSession`. That is the entire mechanism behind
/// "a lapse never interrupts an open session": there is no setter, no observation, and
/// nothing anywhere that re-evaluates a live session's tier, so a subscription ending
/// mid-session is a fact about the *next* open and about nothing already on screen.
///
/// It is recorded rather than merely used-and-discarded because the provider it selects is
/// not the only thing that will ever ask. pro-m1's surfaces the card window's History
/// section, View History (12 Tier matrix) are per-board questions asked long after
/// composition, and they must get the answer this board actually opened with rather than
/// whatever the entitlement happens to say when the sidebar renders.
/// **Nothing git-shaped reads it.** Git left the paywall (12 PIVOT 2026-08-07), so the
/// substrate, the committer, the branch controls and the History surfaces are all decided by
/// the board's mode alone. It stays recorded because the entitlement's machinery stays built
/// and correct for whatever the re-decided base/Pro split gates, and because the fact a board
/// opened under is a composition-time answer the way the provider binding is: whatever asks
/// next must get what this board opened with, never what the entitlement says at render time.
public let tier: Tier
/// **This board's git state** (06-history-undo.md Rules; 02-architecture.md Components
/// HistoryStore) the detected mode, the repository behind it in git mode, and the
/// add-git action the popover offers on a board that has none.
///
/// `nil` under the free tier, and that is the inert posture made structural rather than
/// remembered: with no object there is nothing to consult, nothing to detect with, and no
/// path by which a free-tier session could touch `.git` (12-editions.md The free tier and
/// `.git`). `HistoryStore.compose` is the one place the tier decides it.
/// **Never `nil` on a session the app composes**, since PIVOT 2026-08-07 (12-editions.md):
/// `HistoryStore.compose` runs detection for every board in every tier and returns a store.
/// It used to answer `nil` under the free tier the inert-`.git` posture made structural
/// rather than remembered and that posture is retired. The optionality survives for the
/// seam's sake alone: a test binds a git-stateless session through `makeHistoryProvider`.
///
/// A `let` beside `tier`, for `tier`'s reason: which board this is a git story *of* is
/// settled at composition and cannot change under an open session. What can change is the
@@ -477,15 +491,15 @@ public final class AppModel {
public let git: HistoryStore?
/// The mode this board is being edited in, `none` when there is no git state at all which
/// is every free-tier session ("The free tier ships exactly one mode: `none`",
/// 12-editions.md Tier matrix).
/// no session the app composes is any more (see `git` above), and which a board nobody has
/// added git to detects anyway.
///
/// **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` and `repoNested` alike the native stack. The popover's git section is the other
/// reader and the one place the two gitless modes still differ, since add-git is offered on
/// one and explained away on the other.
/// know whether the board has a repository to be an undo stack for, and it is the *mode* and
/// nothing else that decides (re-ruled 2026-07-31; the tier's last say went with PIVOT
/// 2026-08-07): `git` binds the git provider, `none` and `repoNested` alike the native stack.
/// The popover's git section is the other reader and the one place the two gitless modes
/// still differ, since add-git is offered on one and explained away on the other.
///
/// `@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
@@ -886,17 +900,19 @@ public final class AppModel {
func beginSession(ref: BoardWindowRef, store: BoardStore, recordID: UUID, access: ScopedAccess?) {
// **The entitlement read** (12-editions.md The entitlement): "Pro state is read from
// StoreKit's signed on-device transaction store at board-session composition the open path
// gains no network dependency." Synchronous, over facts already in memory, on the same line
// as the provider it selects which is the shape that makes "the open path never waits on
// the App Store" checkable by reading four lines rather than by auditing a call graph. It is
// also the *only* time this board asks: the answer becomes `BoardSession.tier` and nothing
// re-derives it.
// gains no network dependency." Synchronous, over facts already in memory. It is the *only*
// time this board asks: the answer becomes `BoardSession.tier` and nothing re-derives it.
//
// **Recorded, and consulted by nothing below** (12 PIVOT 2026-08-07 git left the
// paywall). This line used to sit on the same line as the git state it gated; the gate is
// gone, and what is left is a dormant fact kept for the base/Pro split still to be ruled.
let tier = currentTier()
// **Mode detection** (06-history-undo.md Rules Detection: "checked at every board
// open"), on the same line as the tier that gates it. Under `.free` this returns `nil`
// without looking at the disk at all the inert posture is unconditional there and under
// `.pro` it is one `stat` per open, freshly, so a board that gained or lost a `.git` since
// its last open opens in the mode it now has.
// open") unconditionally, in every tier. One `stat` per open, freshly, so a board that
// gained or lost a `.git` since its last open opens in the mode it now has. Until the
// 2026-08-07 pivot this call took the tier and answered `nil` under `.free`, which bought
// the free tier a *zero*-stat open (the inert-`.git` posture); that promise is retired with
// the posture, and every open now pays what Pro's opens always paid.
//
// Deliberately *not* re-run anywhere: no reload path, no watcher event, nothing. "The
// running session keeps its mode, and the watcher does not scan for `.git` appearing."
@@ -911,93 +927,91 @@ public final class AppModel {
// callers (the add-git surface, unit tests), and a session that took that default would hand
// the committer a ledger nothing ever writes to: every commit this app made would classify
// foreign and be authored `Lanework External`. The default is a fallback, never this path's.
let git = HistoryStore.compose(boardRoot: store.rootURL, tier: tier, ledger: store.echoes)
let git = HistoryStore.compose(boardRoot: store.rootURL, ledger: store.echoes)
// The board's stack is born here, with the session that owns it, and dies in `tearDown`
// below the whole of 13-native-undo.md's session-only persistence: "the stack lives with
// the board session and dies at close/quit ... standard macOS behavior". On Pro's git boards
// the board session and dies at close/quit ... standard macOS behavior". On a git-mode board
// it is instead the repository's own trail, which survives everything (06 Rules Undo
// survives relaunch) the seam's whole point.
let history = makeHistoryProvider(store, tier, git)
let history = makeHistoryProvider(store, git)
// **The loader's earlier-occurrence-wins history rung** (01-storage-format.md Fractal
// layout Rules; `BoardLoader.IdentityHistoryRanker`): git-mode boards get a ranker,
// everything else keeps injecting nothing. A *provider* rather than a ranker because each
// load wants its own see `BoardStore.makeIdentityHistoryRanker` and because add-git
// flips the mode mid-session, which this closure picks up for free by asking the git state
// at the moment of each load rather than at composition.
if let git {
store.makeIdentityHistoryRanker = { [weak git] in git?.identityHistoryRanker }
// **The auto-commit engine, wired into the session it commits for** (06-history-undo.md
// Rules Auto-commit). Called on every Pro session and not only on git-mode ones,
// because add-git can flip a board mid-session and the committer it builds then must land
// in exactly this shape `activateAutoCommit` remembers the wiring for that.
git.activateAutoCommit { [weak store] committer in
guard let store else { return }
committer.currentSnapshot = { [weak store] in store?.snapshot }
// **The flush awaits the snapshot that covers it** (06 Rules Auto-commit, ruled
// 2026-07-31): "the composer diffs `store.snapshot` against HEAD, so the close flush
// awaits a snapshot generation covering its changed paths before the committer runs
// the commit's subject can never be outrun by its own reload". Both halves of that
// await are reads of the store the composer is already diffing, which is why they are
// wired here rather than reached for: the engine holds the *policy* (when to wait, how
// long), the session supplies the two facts (`GitAutoCommitter.awaitCoveringSnapshot`).
//
// The generation the gate counts is `landedReloads` completed *walks* rather than
// applied snapshots because a value-equal reload skips the assignment and its
// counter since 2026-07-31, and a walk covers a flush's paths whether or not it found
// anything to change (`BoardStore.landedReloads`).
committer.awaitReloadQuiescence = { [weak store] in await store?.awaitQuiescence() }
committer.landedReloads = { [weak store] in store?.landedReloads }
// 02-architecture.md Write-failure surfacing, through the strip the board window
// already renders: a genuine commit failure means "your edits are saved, history has
// stopped advancing", which is exactly what the standing suspension row says. Lock
// contention and a held repository never reach here neither is a failure.
committer.reportFailure = { [weak store] failure in
store?.banners.suspendHistory(reason: failure.message)
}
committer.reportRecovery = { [weak store] in
store?.banners.clearHistorySuspension()
}
// **The corrupt-`.git` loud failure's standing row** (06-history-undo.md Rules,
// ruled 2026-07-31): a repository the app cannot open pauses the whole git surface
// and says so on the strip, "announced per 10-accessibility.md" and the same seam
// heals it, since the paused engine's own 15 s re-read is what notices a repository
// repaired in a terminal. Distinct from the suspension above: that row is history
// failing to advance and retrying, this one is there being nothing to advance into.
committer.reportRepositoryUnreadable = { [weak store] unreadable in
store?.noteRepositoryUnreadable(unreadable)
}
store.commitSeam = .binding(to: committer)
// **The undo stack's ear on the committer** every commit this engine lands, and
// which of it was heal work (06 Rules The stack is HEAD's first-parent ancestry,
// live; Heal commits are transparent to undo). Bound here rather than in
// `wireGitUndo` because add-git builds a *new* committer, and this wiring is what
// `activateAutoCommit` remembers on its behalf.
committer.reportLanded = { [weak self, ref] window in
guard let provider = self?.sessions[ref]?.history as? GitHistoryProvider else { return }
provider.noteLanded(window)
}
store.makeIdentityHistoryRanker = { [weak git] in git?.identityHistoryRanker }
// **The auto-commit engine, wired into the session it commits for** (06-history-undo.md
// Rules Auto-commit). Called on every session and not only on git-mode ones, because
// add-git can flip a board mid-session and the committer it builds then must land in
// exactly this shape `activateAutoCommit` remembers the wiring for that.
git.activateAutoCommit { [weak store] committer in
guard let store else { return }
committer.currentSnapshot = { [weak store] in store?.snapshot }
// **The flush awaits the snapshot that covers it** (06 Rules Auto-commit, ruled
// 2026-07-31): "the composer diffs `store.snapshot` against HEAD, so the close flush
// awaits a snapshot generation covering its changed paths before the committer runs
// the commit's subject can never be outrun by its own reload". Both halves of that
// await are reads of the store the composer is already diffing, which is why they are
// wired here rather than reached for: the engine holds the *policy* (when to wait, how
// long), the session supplies the two facts (`GitAutoCommitter.awaitCoveringSnapshot`).
//
// The generation the gate counts is `landedReloads` completed *walks* rather than
// applied snapshots because a value-equal reload skips the assignment and its
// counter since 2026-07-31, and a walk covers a flush's paths whether or not it found
// anything to change (`BoardStore.landedReloads`).
committer.awaitReloadQuiescence = { [weak store] in await store?.awaitQuiescence() }
committer.landedReloads = { [weak store] in store?.landedReloads }
// 02-architecture.md Write-failure surfacing, through the strip the board window
// already renders: a genuine commit failure means "your edits are saved, history has
// stopped advancing", which is exactly what the standing suspension row says. Lock
// contention and a held repository never reach here neither is a failure.
committer.reportFailure = { [weak store] failure in
store?.banners.suspendHistory(reason: failure.message)
}
// **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)
committer.reportRecovery = { [weak store] in
store?.banners.clearHistorySuspension()
}
// **The form-anchored posture's fallback half** (06 Interaction with external writers,
// ruled 2026-07-31): add-git answers inline in the form that asked, and lands here instead
// when that form has been dismissed before the answer arrived "inline is the primary
// surface, never a silence trap". The banner enumeration is the same one branch switch and
// undo restore post into, one row per failure.
git.reportFailure = { [weak store] failure in
store?.banners.postGitFailure(.addGit, reason: failure.message)
// **The corrupt-`.git` loud failure's standing row** (06-history-undo.md Rules,
// ruled 2026-07-31): a repository the app cannot open pauses the whole git surface
// and says so on the strip, "announced per 10-accessibility.md" and the same seam
// heals it, since the paused engine's own 15 s re-read is what notices a repository
// repaired in a terminal. Distinct from the suspension above: that row is history
// failing to advance and retrying, this one is there being nothing to advance into.
committer.reportRepositoryUnreadable = { [weak store] unreadable in
store?.noteRepositoryUnreadable(unreadable)
}
store.commitSeam = .binding(to: committer)
// **The undo stack's ear on the committer** every commit this engine lands, and
// which of it was heal work (06 Rules The stack is HEAD's first-parent ancestry,
// live; Heal commits are transparent to undo). Bound here rather than in
// `wireGitUndo` because add-git builds a *new* committer, and this wiring is what
// `activateAutoCommit` remembers on its behalf.
committer.reportLanded = { [weak self, ref] window in
guard let provider = self?.sessions[ref]?.history as? GitHistoryProvider else { return }
provider.noteLanded(window)
}
// **The detection-time answer, published once** (06 Rules: "a standing breakage-class
// banner **at detection**"). The probe ran inside `compose` above before this session
// existed, and therefore before the seam that carries its transitions was wired so a
// board that opened into an unreadable repository raises its row here rather than
// waiting for the first debounce to rediscover what composition already knows.
store.noteRepositoryUnreadable(git.isRepositoryUnreadable)
}
// **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)
}
// **The form-anchored posture's fallback half** (06 Interaction with external writers,
// ruled 2026-07-31): add-git answers inline in the form that asked, and lands here instead
// when that form has been dismissed before the answer arrived "inline is the primary
// surface, never a silence trap". The banner enumeration is the same one branch switch and
// undo restore post into, one row per failure.
git.reportFailure = { [weak store] failure in
store?.banners.postGitFailure(.addGit, reason: failure.message)
}
// **The detection-time answer, published once** (06 Rules: "a standing breakage-class
// banner **at detection**"). The probe ran inside `compose` above before this session
// existed, and therefore before the seam that carries its transitions was wired so a
// board that opened into an unreadable repository raises its row here rather than
// waiting for the first debounce to rediscover what composition already knows.
store.noteRepositoryUnreadable(git.isRepositoryUnreadable)
// **The binding 13-native-undo.md Rules' "registration at the Writer boundary" needs**: the
// store is that boundary every app-mediated mutation goes out through one of its write
// methods so it is the store that computes each inverse and registers it. What it cannot
@@ -1291,15 +1305,17 @@ public final class AppModel {
/// 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
/// the provider it composed" was a rule about a **tier** lapsing, which never could change a
/// running session and since PIVOT 2026-08-07 no longer changes the *next* open either, since
/// a git board composes the git provider in every tier; 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], let git = session.git, git.mode == .git else { return }
guard let history = makeHistoryProvider(session.store, session.tier, git) else { return }
guard let history = makeHistoryProvider(session.store, 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
@@ -1351,8 +1367,9 @@ public final class AppModel {
///
/// This is the honest seam between the two halves of the rule: the host knows a window exists, the
/// committer knows what staging is, and only the app model knows which board a card window belongs
/// to and how to reach its committer. A board with no committer the free tier, a Pro board with
/// no repository records nothing, which is the same `nil` every other git seam takes.
/// to and how to reach its committer. A board with no committer any board nobody has added git
/// to, which since PIVOT 2026-08-07 is the only such board there is (12-editions.md) records
/// nothing, which is the same `nil` every other git seam takes.
///
/// The card's folder is handed over as a **closure**, not a URL: a card can change lane, or be
/// moved into the trash, in the middle of a session, and what must be staged around is wherever
@@ -1693,8 +1710,9 @@ public final class AppModel {
// pending auto-commit before teardown; nothing settled is ever left unsaved or
// uncommitted by closing"). By the time it runs, step 1 has ended every card window's
// session *and released its stage-around*, so each session body, comments, purge and
// all lands in exactly one commit. `nil` on every board with no committer, which is the
// whole free tier.
// all lands in exactly one commit. `nil` on every board with no committer, which is
// every board nobody has added git to (12 PIVOT 2026-08-07 the tier no longer
// decides; opt-in per board still does).
committerFlush: { [weak self] in
await self?.sessions[ref]?.git?.committer?.flushNow()
},
+18 -13
View File
@@ -113,8 +113,9 @@ struct BoardWindowHost: View {
/// following walk builds (`BoardRepairRun`, `EchoLedger.adopt`).
///
/// It cannot live anywhere else: the repairs run before a store exists and the receipts have to
/// reach that store's ledger before `beginSession` composes Pro's committer, or the app's own
/// repair commits as `Lanework External`. Cleared once adopted.
/// reach that store's ledger before `beginSession` composes a git board's committer, or the app's
/// own repair commits as `Lanework External`. (Pro's committer, until 12-editions.md PIVOT
/// 2026-08-07 made it every git board's.) Cleared once adopted.
@State private var repairLedger: EchoLedger?
@State private var phase: Phase = .opening
@@ -396,7 +397,8 @@ struct BoardWindowHost: View {
// **The repair's receipts, into the board's own ledger before the session composes**
// (01: "On Pro boards the repairs drop heal-marked receipts and commit separately as one
// repair commit"). `beginSession` is where Pro's committer is built and started, and the
// repair commit" every git board's since 12-editions.md PIVOT 2026-08-07, the sentence
// otherwise unchanged). `beginSession` is where that committer is built and started, and the
// committer harvests the ledger it is handed; receipts adopted after that line would be
// receipts the repair commit never sees, and the app's own repair would be authored
// `Lanework External`.
@@ -730,31 +732,34 @@ struct BoardWindowHost: View {
// load rather than at attach because it carries the store; the controller installs it once,
// whichever of the two arrives second.
//
// The tier and the git state come from the **session**, which `start()` began a moment ago,
// rather than from the entitlement or the disk: a board's popover must describe the board as
// it opened (12-editions.md The entitlement, "an open board finishes with the provider it
// composed"; 06-history-undo.md Rules, mode is an open-time fact). A `nil` session cannot
// happen on this path `beginSession` precedes `configureWindow` and reads as the free
// tier's posture, which is the harmless direction.
// The git state comes from the **session**, which `start()` began a moment ago, rather than
// from the entitlement or the disk: a board's popover must describe the board as it opened
// (12-editions.md The entitlement, "an open board finishes with the provider it composed";
// 06-history-undo.md Rules, mode is an open-time fact). A `nil` session cannot happen on
// this path `beginSession` precedes `configureWindow` and reads as a board with no
// repository, which is the harmless direction.
//
// **The tier is no longer passed down** (12 PIVOT 2026-08-07): git is tier-independent, so
// every one of these surfaces reads the board's mode and nothing else. `BoardSession.tier`
// still exists and is still recorded it just has no git-facing consumer here.
let session = appModel.session(for: ref)
// The settings sheet's two doors validate on the same pair, so they are adopted here rather
// The settings sheet's two doors validate on the same fact, so it is adopted here rather
// than read again somewhere else: the popover's Board Settings row and Board Board
// Settings must never disagree about whether this board has setup to show
// (`BoardSettingsAvailability`). The mode *inside* the git state stays live add-git flipping
// it re-resolves the sheet's sections and both doors, which is the one mid-session transition
// 06 sanctions.
boardSettings.adopt(tier: session?.tier ?? .free, git: session?.git)
boardSettings.adopt(git: session?.git)
windowController.installTitlebarAccessory(
boardInfoTitlebarAccessory(
store: store,
recents: appModel.styleRecents,
tier: session?.tier ?? .free,
git: session?.git,
presentation: boardInfo,
settings: boardSettings
)
)
// The widget above now says the board's name (and, on a git-mode Pro board, its branch)
// The widget above now says the board's name (and, on a git-mode board, its branch)
// itself, so the system title display would only repeat it the card-window seam
// (`CardWindowHost.configureWindow`, `HostedWindowController.hideTitle`), applied here for
// the same reason. `.navigationTitle(windowTitle)` a few lines up in `body` is untouched
+8 -4
View File
@@ -213,7 +213,9 @@ final class CardWindowSession: CardSessionFlushing {
// net effect, and the `comments/.trash/` purge, which must not run while a board step's undo
// still restores comments out of it. Registering answers whether the step took the purge on
// and a board whose substrate keeps no steps has already run it by the time that answer comes
// back, which is how Pro keeps purging at the close flush without a word about tiers here.
// back, which is how a git board keeps purging at the close flush without a word about tiers
// here (and there is none to say since 12-editions.md PIVOT 2026-08-07 the substrate is
// the board's mode, not a subscription).
//
// **This window stops owning its card's comment trash first** (ruled 2026-08-06 the
// open-window carve-out). While the window is open the store defers every purge of this
@@ -418,9 +420,11 @@ struct CardWindowHost: View {
/// **This card's commit trail, or nothing at all** (05-card-window.md History).
///
/// `nil` is the section's absence rule, read from the board's own git state rather than from a
/// flag: no `HistoryStore` means the free tier (12-editions.md where the section never exists),
/// and a mode other than `git` means a board the app manages no history for. The object is held
/// by this host so it survives every snapshot, `thumbnails`' reason exactly.
/// flag: a mode other than `git` means a board the app manages no history for, on any tier. The
/// question stopped having a tier half at 12-editions.md PIVOT 2026-08-07 every session
/// composes a git state now, so what decides the section is whether *this board's* history is
/// git-backed. The object is held by this host so it survives every snapshot, `thumbnails`'
/// reason exactly.
private var cardHistory: CardHistory? {
guard appModel.session(for: ref.board)?.gitMode == .git else { return nil }
return history