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
+4 -2
View File
@@ -39,8 +39,10 @@ import Foundation
public enum BoardGitMode: String, Sendable, Equatable, CaseIterable {
/// No `.git` at the board root and none above it **every** ancestor check answered not-found,
/// "clean none" in 06's own words. The only mode the free tier ships (12-editions.md Tier
/// matrix), the one add-git moves a board out of, and now that this axis exists the one mode
/// "clean none" in 06's own words. The mode of every board nobody has opted into git for which
/// is what a board without app-managed git *is* now that the tier axis is gone (12-editions.md
/// PIVOT 2026-08-07; it used to be the only mode the free tier shipped, over the retired
/// inert-`.git` posture). The one add-git moves a board out of, and now that this axis exists the one mode
/// add-git's own re-detection requires before it will act: a raced or stale read that turns out
/// to be `.unverifiable` or `.repoNested` refuses the init exactly as those modes always did.
case none
+8 -5
View File
@@ -44,12 +44,15 @@ public struct GitLandedWindow: Sendable, Equatable {
/// **Every settled change becomes a commit** (06-history-undo.md Rules Auto-commit), debounced
/// past drag and typing churn, on git-mode boards and nowhere else.
///
/// ### Structurally unreachable off Pro
/// ### Structurally unreachable on a board with no repository
///
/// One of these exists per `HistoryStore` in mode `git`, and a `HistoryStore` exists only under Pro
/// (`HistoryStore.compose` is the tier gate). The free tier therefore has no committer to disable,
/// no debounce to cancel and no `.git` to touch 12-editions.md's inert posture as a shape rather
/// than as a flag, which `InertGitTests` pins against real bytes.
/// One of these exists per `HistoryStore` in mode `git` and nowhere else. Until the 2026-08-07 pivot
/// there was a second gate above it a `HistoryStore` existed only under Pro and that one is gone
/// (12-editions.md PIVOT 2026-08-07: git left the paywall, and every tier composes the git stack
/// on git-mode boards). What remains is the stronger of the two anyway, because it never depended on
/// a subscription: git is **opt-in per board** (06 Rules), so a board the user never added git to
/// detects `none`, composes no committer, and has no debounce to cancel and no `.git` to touch. The
/// file layer's own indifference to `.git` is pinned against real bytes by `UntouchedGitTests`.
///
/// ### What arms it
///
+5 -3
View File
@@ -254,13 +254,15 @@ public final class GitHistoryProvider: HistoryProviding {
// MARK: - HistoryProviding
/// **Deliberately nothing except the one thing a dropped step is owed.** On a git board an undo
/// step is a commit, and the Writer boundary's inverse operations are the *free* tier's substrate
/// (13-native-undo.md). `BoardStore` registers against whatever provider the session bound, and
/// step is a commit, and the Writer boundary's inverse operations are the *gitless* board's
/// substrate (13-native-undo.md the tier axis it once read as went with 12-editions.md
/// PIVOT 2026-08-07; the substrate is the board's mode alone). `BoardStore` registers against
/// whatever provider the session bound, and
/// this one has a repository to read instead so the registrations arrive and are dropped, which
/// is exactly what "the commit trail itself is the substrate" (14 C1) means in code.
///
/// Dropping a step means **retiring** it (`HistoryStep.Retirement`), and that is what keeps the
/// tier split in 13's purge rule structural rather than conditional: "on Pro the substrate is
/// substrate split in 13's purge rule structural rather than conditional: "on Pro the substrate is
/// history: the close commit nets delete-plus-purge to a removal, revert restores it, so purge
/// rides the close flush there as before" (13 Interaction with the trash). A card window's close
/// step registered here is retired on arrival, so its deferred `comments/.trash/` purge runs
+5 -5
View File
@@ -377,12 +377,12 @@ enum GitHousekeeping {
/// **When a housekeeping pass runs** (06-history-undo.md Repository hygiene) one per git-mode
/// board session, scheduled at board open and never again.
///
/// ### Structurally unreachable off Pro
/// ### Structurally unreachable on a board with no repository
///
/// One of these exists per `HistoryStore` in mode `git`, and a `HistoryStore` exists only under Pro
/// (`HistoryStore.compose` is the tier gate) `GitAutoCommitter`'s rule, for its reason. The free
/// tier has no housekeeper to disable and no `.git` to pack (12-editions.md The free tier and
/// `.git`, which `InertGitTests` pins against real bytes).
/// One of these exists per `HistoryStore` in mode `git` and nowhere else `GitAutoCommitter`'s
/// rule, for its reason, including the tier gate that used to sit above it and no longer does
/// (12-editions.md PIVOT 2026-08-07). A board the user never added git to detects `none`, so there
/// is no housekeeper to disable and no `.git` to pack.
///
/// ### Off the open path, on purpose
///
+5 -3
View File
@@ -7,9 +7,11 @@ import Foundation
///
/// ### Why a struct of closures rather than a reference to the committer
///
/// `BoardStore` lives in the live store and must not learn what a repository is: the free tier
/// composes no `HistoryStore`, so the engine has to be *structurally* unreachable there rather than
/// switched off, and a store holding an optional committer would be a store that knows about git.
/// `BoardStore` lives in the live store and must not learn what a repository is: most boards have no
/// committer at all git is opt-in per board (06 Rules), and since the 2026-08-07 pivot that is
/// the *only* reason a board lacks one (12-editions.md) so the engine has to be *structurally*
/// unreachable there rather than switched off, and a store holding an optional committer would be a
/// store that knows about git.
/// One optional value, `nil` on every board that has no committer, is the same shape `watcherBrackets`
/// and `history` already take, and it keeps the three orderings before the write, after the
/// bracket, after the landing stated in one type instead of three properties that could drift.
+38 -25
View File
@@ -7,19 +7,25 @@ import os
/// opened in, the repository behind it when there is one, and the two operations that can change
/// either the app's own add-git, and nothing else.
///
/// ### One per board session, composed under the tier
/// ### One per board session, composed at every open in every tier
///
/// `compose(boardRoot:tier:)` is the whole gate: **the free tier gets no `HistoryStore` at all**, so
/// a free-tier session runs no detection, opens no repository, and does not so much as `stat` a
/// `.git` "any `.git` is inert the app never reads history, never commits, never touches `.git`
/// in any way" (12-editions.md The free tier and `.git`), which `InertGitTests` pins against real
/// bytes. Nothing in this type is conditional on a tier, because the tier decided whether the type
/// exists.
/// `compose(boardRoot:ledger:)` runs detection and hands back a store for **every** session
/// (12-editions.md PIVOT 2026-08-07 git left the paywall: "every tier composes the git stack on
/// git-mode boards exactly as Pro did"). It used to be the gate: the free tier got no object at all,
/// so a free session ran no detection and did not so much as `stat` a `.git` the inert-`.git`
/// posture made structural rather than remembered. That posture is **retired**. A `.git` at a board
/// root is live in every tier, detection runs at every board open off the same path Pro's always
/// used, and nothing in this type ever asks what anybody paid.
///
/// What the pivot does **not** change is why mode `none` still exists at all: git stays **opt-in per
/// board** (06 Rules "No silent auto-init, ever"). A board whose user never asked for a
/// repository composes here, detects `none`, and builds no committer, no switcher and no
/// housekeeper nothing that could touch a `.git` it does not have.
///
/// ### 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 reads `mode` and nothing else about a tier** the composition
/// ranker. **The provider binding reads `mode` and nothing else** the composition
/// root binds the git provider on mode `git` and the native stack on modes `none` and `repoNested`
/// alike (`AppModel.makeHistoryProvider`, re-ruled 2026-07-31: the provider follows the board, and
/// what a repo-nested board denies is app-managed history, never Z). Auto-commit, commit messages,
@@ -82,9 +88,11 @@ public final class HistoryStore {
/// **The auto-commit engine** (06-history-undo.md Rules Auto-commit), or `nil` on a board
/// there is no repository to commit into.
///
/// Its existence is exactly `mode == .git`, and that invariant is the tier gate one level down:
/// no `HistoryStore` off Pro means no committer anywhere off Pro, with nothing to disable and no
/// flag to forget.
/// Its existence is exactly `mode == .git`, and since the 2026-08-07 pivot (12-editions.md) that
/// invariant carries the whole story on its own: what keeps a committer off a board is the
/// board's own mode git is opt-in per board, so a user who never asked for a repository has
/// nothing here to disable and no flag anyone could forget. It used to rest on a tier gate one
/// level up (no `HistoryStore` off Pro meant no committer off Pro); the gate is gone.
///
/// **Composed inert and started separately.** Composition happens on the board-open path, where
/// nothing may block and where a session does not exist yet; `activateAutoCommit(_:)` is what
@@ -256,14 +264,19 @@ public final class HistoryStore {
housekeeper.schedule()
}
/// **The tier gate and the open-time detection, in one line** (12-editions.md The provider
/// seam; 06-history-undo.md Rules Detection) called by `AppModel.beginSession` beside the
/// entitlement read that supplies `tier`.
/// **The open-time detection** (06-history-undo.md Rules Detection: "checked at every board
/// open") called by `AppModel.beginSession`, unconditionally, once per board.
///
/// `nil` under `.free` means exactly what it says: no git state exists for that session, so no
/// caller can accidentally consult one. Under `.pro` the mode is whatever the filesystem says
/// right now, and a board that has changed mode since its last open simply opens in the new one
/// "the app just reflects what it finds".
/// ### This was the tier gate, and is not one any more
///
/// It took a `tier` and answered `nil` under `.free`: no git state existed for such a session, so
/// no caller could consult one and no free open ever stat'ed a `.git`. **PIVOT 2026-08-07**
/// (12-editions.md) retired that whole axis "detection runs at every board open" in every tier
/// so the parameter is *removed* rather than ignored, and the zero-stat promise dies with it:
/// every open now pays the same handful of `stat`s Pro's opens always paid, off this same path.
///
/// The mode is whatever the filesystem says right now, and a board that has changed mode since
/// its last open simply opens in the new one "the app just reflects what it finds".
///
/// **Adoption needs no step of its own**: a board whose root already carries `.git` lands in
/// `.git` here, silently, with no dialog and nothing to confirm "the repo's presence *is* the
@@ -274,8 +287,7 @@ public final class HistoryStore {
/// store-less `HistoryStore` still composes: an empty ledger vouches for nothing, which is the
/// honest answer for a git state with no session behind it (everything reads foreign, the
/// launch-catch-up doctrine).
public static func compose(boardRoot: URL, tier: Tier, ledger: EchoLedger = EchoLedger()) -> HistoryStore? {
guard tier == .pro else { return nil }
public static func compose(boardRoot: URL, ledger: EchoLedger = EchoLedger()) -> HistoryStore {
let mode = BoardGitMode.detect(boardRoot: boardRoot)
logger.debug("board opened in git mode \(mode.rawValue, privacy: .public)")
return HistoryStore(boardRoot: boardRoot, mode: mode, ledger: ledger)
@@ -286,10 +298,11 @@ public final class HistoryStore {
/// **Opt-in init** (06-history-undo.md Rules): initializes a repository at the board root and
/// immediately commits the whole tree as "Initial board state".
///
/// Reachable from one place the board settings sheet's Git section under Pro and from nowhere
/// else:
/// Reachable from one place the board settings sheet's Git section, in every tier since the
/// 2026-08-07 pivot (12-editions.md) and from nowhere else:
/// "No silent auto-init, ever", a deliberate pivot from the pathfinder, which initialized a repo
/// under every board it opened.
/// under every board it opened. Opt-in is what the pivot deliberately left standing: git leaving
/// the paywall widened *who* may ask, never *whether* asking is required.
///
/// **It flips the open board's mode immediately**, which is the design's one sanctioned
/// mid-session transition: "clicking it flips the open board into git mode immediately the
@@ -419,8 +432,8 @@ public final class HistoryStore {
// MARK: - The loader's history seam
/// **The git-backed `IdentityHistoryRanker`** (01-storage-format.md Fractal layout Rules;
/// `BoardLoader.IdentityHistoryRanker`), or `nil` on any board the app manages no git for the
/// free tier and modes `none`/`repoNested` alike, all of which fall through to the ladder's
/// `BoardLoader.IdentityHistoryRanker`), or `nil` on any board the app manages no git for
/// modes `none`, `repoNested` and `unverifiable` alike, all of which fall through to the ladder's
/// remaining rungs (birth date, then traversal order).
///
/// **A fresh ranker per ask, deliberately.** Each one computes its map at most once, lazily, and