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
+49 -45
View File
@@ -39,51 +39,55 @@ import SwiftUI
/// than per board or per app): one per window, `@State` in `BoardWindowHost`, published through the
/// focus system so Board Board Settings means "the board in front".
///
/// **It carries the session's tier and git state** where the popover flag carries nothing, and for a
/// reason the popover does not have: both of this sheet's doors have to *validate*, and one of them
/// is a menu row with no view around it to ask. The facts are adopted once, from the session, at the
/// same moment the titlebar widget adopts them (`BoardWindowHost.configureWindow`) and never
/// re-derived 12-editions.md The entitlement, "a lapse never interrupts an open session". The
/// *mode* inside the git state is `@Observable` and does move, by add-git alone, which is exactly the
/// transition this sheet is where the user performs: the sections re-resolve under it live.
/// **It carries the session's git state** where the popover flag carries nothing, and for a reason
/// the popover does not have: both of this sheet's doors have to *validate*, and one of them is a
/// menu row with no view around it to ask. The fact is adopted once, from the session, at the same
/// moment the titlebar widget adopts it (`BoardWindowHost.configureWindow`) and never re-derived
/// 12-editions.md The entitlement, "a lapse never interrupts an open session". The *mode* inside
/// the git state is `@Observable` and does move, by add-git alone, which is exactly the transition
/// this sheet is where the user performs: the sections re-resolve under it live.
///
/// **The tier came out at 12 PIVOT 2026-08-07.** It was adopted here alongside the git state until
/// that day, because the sheet was Pro's; git is tier-independent now, so what the doors validate on
/// is the board's mode alone.
@MainActor
@Observable
final class BoardSettingsPresentation {
var isPresented = false
/// The tier this window's board composed under the free tier's default until a session says
/// otherwise, which is the harmless direction (an unreachable sheet).
private(set) var tier: Tier = .free
/// This window's board git state, `nil` under the free tier and on a window whose session has not
/// been adopted yet.
/// This window's board git state, `nil` on a window whose session has not been adopted yet
/// which reads as mode `none`, the harmless direction (an add-git sheet, offered to a board that
/// may well already have a repository, is nothing anyone can act on before adoption lands).
private(set) var git: HistoryStore?
/// Called once per window, from the same place the titlebar widget is handed the same two facts.
func adopt(tier: Tier, git: HistoryStore?) {
self.tier = tier
/// Called once per window, from the same place the titlebar widget is handed the same fact.
func adopt(git: HistoryStore?) {
self.git = git
}
/// What the sheet would show right now and therefore, when empty, that there is no sheet to
/// show (`BoardSettingsAvailability`).
///
/// **An unadopted window has none**, and that is a `nil` check rather than a mode reading: every
/// control this sheet hosts writes *through* the git state (`section(_:)` renders nothing without
/// one), so a window that has not been handed its session yet would otherwise offer a sheet of
/// bare headers. It was the free tier's default that kept this shut before 12 PIVOT 2026-08-07;
/// what keeps it shut now is the honest absence of a session, which is the only thing a `nil`
/// ever meant here.
var sections: [BoardSettingsSection] {
BoardSettingsSection.resolve(
tier: tier,
mode: git?.mode ?? .none,
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
guard let git else { return [] }
return BoardSettingsSection.resolve(
mode: git.mode,
isRepositoryUnreadable: git.isRepositoryUnreadable
)
}
/// Both doors' validation: the menu row's `disabled` state and whether the popover shows its row
/// at all.
/// at all. Derived from `sections` rather than from `BoardSettingsAvailability` directly, so the
/// unadopted case above cannot answer one way here and another there.
var isReachable: Bool {
BoardSettingsAvailability.resolve(
tier: tier,
mode: git?.mode ?? .none,
isRepositoryUnreadable: git?.isRepositoryUnreadable ?? false
)
!sections.isEmpty
}
/// **Opening, not toggling** unlike I. A sheet is modal to its window and carries its own
@@ -118,22 +122,30 @@ extension FocusedValues {
// MARK: - What the sheet holds
/// **The sheet's inventory for one board**, as a pure function of the tier and the mode the shape
/// **The sheet's inventory for one board**, as a pure function of the mode the shape
/// `BoardGitSection.resolve` has one surface over, and for the same reason: the *contents* are the
/// part worth pinning and the SwiftUI that renders them is not.
///
/// **The tier axis came out at 12-editions.md PIVOT 2026-08-07**: this resolved `guard tier ==
/// .pro else { return [] }` first and the mode second until git left the paywall. Every board can
/// reach the setup it has now, and what it has is still the mode's answer the sheet's whole
/// vocabulary is repository-shaped, so a board with nothing repository-shaped to say still hosts
/// nothing.
///
/// Ordered as the sheet lays them out, top to bottom. pro-m2's cards each add a case here and a
/// branch in `BoardSettingsSheet.section(_:)` nothing else.
enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
/// Pro, mode `none`: **add-git** (06-history-undo.md Rules Opt-in init).
/// Mode `none`: **add-git** (06-history-undo.md Rules Opt-in init) the offer, on every
/// tier since the pivot, and still never an auto-init (git is opt-in per board, 12 PIVOT
/// 2026-08-07).
case git
/// Pro, mode `git`: **branch creation**. Switching stays in the popover (03 Board popover);
/// Mode `git`: **branch creation**. Switching stays in the popover (03 Board popover);
/// create-and-switch runs 06's identical settle sequence from here.
case branch
/// Pro, mode `git`: the **commit identity** name/email that repo-local `.git/config` carries
/// Mode `git`: the **commit identity** name/email that repo-local `.git/config` carries
/// (06 Interaction with external writers).
case commitIdentity
@@ -154,14 +166,9 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
/// `git` every other mode has no repository for the probe to have failed on, and a caller
/// that has no git state to ask is describing one of those boards.
static func resolve(
tier: Tier,
mode: BoardGitMode,
isRepositoryUnreadable: Bool = false
) -> [BoardSettingsSection] {
// **The free tier has no setup to host** (12-editions.md The free tier and `.git`): git is
// the Pro subscription's, "any `.git` is inert", and 03 gives the free tier's whole git story
// as the popover's one-line pointer. There is nothing for a sheet to be about.
guard tier == .pro else { return [] }
switch mode {
case .none:
return [.git]
@@ -204,15 +211,14 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
/// Reachable **iff the sheet has something to show**, which is the rule rather than a shortcut: a
/// surface whose whole job is hosting setup controls has no honest empty state, and deriving the
/// answer from the inventory is what keeps the two from drifting when pro-m2's sections land. In
/// today's terms that reads: Pro, on a board whose mode is `none` or `git`.
/// today's terms that reads: a board whose mode is `none` or `git`, on **any tier** the Pro
/// requirement that stood beside it retired with 12-editions.md PIVOT 2026-08-07.
///
/// The two unreachable postures are unreachable for *different* reasons, and both are the design's:
///
/// - **The free tier**: no setup exists there at all (03-board-ui.md Board popover;
/// 12-editions.md). The popover's own postures are untouched by this card an ordinary board
/// shows nothing, a board carrying an inert `.git` shows the one-line Pro pointer.
/// - **Pro, repo-nested**: `BoardSettingsSection.resolve`'s own comment carries this one nothing
/// setup-shaped can apply, so the popover's prose stands and no door opens.
/// The unreachable postures are unreachable for reasons that are all the board's, and all the
/// design's `BoardSettingsSection.resolve`'s own comments carry them one by one: **repo-nested**
/// and **unverifiable**, where nothing setup-shaped can apply, and **git mode over a repository that
/// will not open**, where both sections would be writes into something the app cannot open. In each,
/// the popover's prose stands and no door opens.
///
/// The menu **row stays visible and disabled** either way (standard menu validation a command that
/// does not apply here is still a command this app has), while the **popover row appears only where
@@ -221,12 +227,10 @@ enum BoardSettingsSection: String, Equatable, CaseIterable, Identifiable {
enum BoardSettingsAvailability {
static func resolve(
tier: Tier,
mode: BoardGitMode,
isRepositoryUnreadable: Bool = false
) -> Bool {
!BoardSettingsSection.resolve(
tier: tier,
mode: mode,
isRepositoryUnreadable: isRepositoryUnreadable
).isEmpty