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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user