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:
+159
-141
@@ -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()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user