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
+63 -61
View File
@@ -9,53 +9,54 @@ import Testing
/// They are pinned here for `BoardGitSection.resolve`'s reason one surface over: the *inventory* is
/// the decision worth asserting and the SwiftUI that renders it is not. Every case below is a plain
/// value no board on disk, no window, no session.
///
/// **The tier axis came out at 12-editions.md PIVOT 2026-08-07**: this inventory resolved `guard
/// tier == .pro` first and the mode second until git left the paywall. What survives is the mode
/// reading, which was always the part carrying the design's reasoning.
@Suite("Board settings sheet ▸ the sections")
struct BoardSettingsSectionTests {
@Test("Pro, mode none: the sheet is add-git and nothing else")
@Test("Mode none: the sheet is add-git and nothing else")
func modeNoneHoldsAddGit() {
// "add-git (mode none; opt-in init 06)" 03's own first entry for this surface, and the
// control that moved here out of the popover with the split.
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .none) == [.git])
// control that moved here out of the popover with the split. Every tier's since
// 12-editions.md PIVOT 2026-08-07, and still an offer rather than an auto-init.
#expect(BoardSettingsSection.resolve(mode: .none) == [.git])
}
@Test("Pro, git mode: branch creation and the commit identity, in that order")
@Test("Git mode: branch creation and the commit identity, in that order")
func gitModeHoldsCreationAndIdentity() {
// "branch creation (switching stays in the popover)" and "commit identity name/email (06
// the visibility-scoped 2 s config re-read rides with the fields)".
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
#expect(BoardSettingsSection.resolve(mode: .git) == [.branch, .commitIdentity])
}
@Test("Every section is reachable from some posture, and no posture invents one")
func theInventoryIsTotal() {
let offered = Set(
Tier.allCases.flatMap { tier in
BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(tier: tier, mode: $0) }
}
)
let offered = Set(BoardGitMode.allCases.flatMap { BoardSettingsSection.resolve(mode: $0) })
#expect(offered == Set(BoardSettingsSection.allCases))
}
@Test("Pro, unverifiable: the sheet has nothing to show — structurally like repo-nested")
@Test("Unverifiable: the sheet has nothing to show — structurally like repo-nested")
func unverifiableHoldsNothing() {
// "Denial is not absence" (06 Rules Detection, ruled 2026-07-31): a denied ancestor check
// can never be told apart from a repository actually being there, so add-git stays as
// unreachable here as it is on a genuinely nested board.
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .unverifiable) == [])
#expect(BoardSettingsSection.resolve(mode: .unverifiable) == [])
}
@Test("Pro, git mode with an unreadable repository: nothing setup-shaped applies either")
@Test("Git mode with an unreadable repository: nothing setup-shaped applies either")
func anUnreadableRepositoryHoldsNothing() {
// **The corrupt-`.git` loud failure** (06 Rules, ruled 2026-07-31): both sections here are
// *writes* to a repository a branch created in it, an identity written into its config
// and there is no repository the app can open to write either into. Repo-nested's emptiness,
// reached one step further along.
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true) == [])
#expect(BoardSettingsSection.resolve(mode: .git, isRepositoryUnreadable: true) == [])
// and the mode is still `git` throughout: this is emptiness *within* git mode, never the
// fall to mode none that would let add-git be offered against an existing `.git`.
#expect(BoardSettingsSection.resolve(tier: .pro, mode: .git) == [.branch, .commitIdentity])
#expect(BoardSettingsSection.resolve(mode: .git) == [.branch, .commitIdentity])
}
@Test("The sections carry the headers VoiceOver navigates by")
@@ -69,41 +70,34 @@ struct BoardSettingsSectionTests {
}
}
/// **Where the sheet can be opened from, tier by mode** the answer both doors validate on: Board
/// **Where the sheet can be opened from, mode by mode** the answer both doors validate on: Board
/// Board Settings's `disabled` state, and whether the popover's git section renders its Board
/// Settings row at all.
///
/// **The Pro clause is gone** (12-editions.md PIVOT 2026-08-07): every board can reach the setup
/// its own mode leaves it, on any tier.
@Suite("Board settings sheet ▸ availability")
struct BoardSettingsAvailabilityTests {
@Test("The whole matrix: Pro on a none-or-git board, and nowhere else")
@Test("The whole matrix: a none-or-git board, and nowhere else")
func theMatrix() {
// Pro, and a board whose mode leaves something to set up.
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .none))
#expect(BoardSettingsAvailability.resolve(tier: .pro, mode: .git))
// A board whose mode leaves something to set up.
#expect(BoardSettingsAvailability.resolve(mode: .none))
#expect(BoardSettingsAvailability.resolve(mode: .git))
// **Pro, repo-nested**: "nothing setup-shaped can apply" no add-git (06's prose, not a
// **Repo-nested**: "nothing setup-shaped can apply" no add-git (06's prose, not a
// disabled button), no branch of ours to create, no repo-local config of ours to write. The
// popover's explanation stands and no door opens.
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .repoNested))
#expect(!BoardSettingsAvailability.resolve(mode: .repoNested))
// **Pro, unverifiable**: the same unreachability, for the denial-not-absence reason a
// denied ancestor check is never distinguishable from a repository actually being there.
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .unverifiable))
// **Unverifiable**: the same unreachability, for the denial-not-absence reason a denied
// ancestor check is never distinguishable from a repository actually being there.
#expect(!BoardSettingsAvailability.resolve(mode: .unverifiable))
// **Pro, git mode over a repository that will not open**: no door either, so neither the
// **Git mode over a repository that will not open**: no door either, so neither the
// popover's Board Settings row nor the menu command offers a surface with nothing on it
// (06 Rules, the corrupt-`.git` loud failure).
#expect(!BoardSettingsAvailability.resolve(tier: .pro, mode: .git, isRepositoryUnreadable: true))
// **The free tier**: no setup exists there at all (12-editions.md The free tier and
// `.git`), whatever mode a stray value claims detection never runs off Pro, so the mode is
// swept for completeness rather than because it can vary.
for mode in BoardGitMode.allCases {
#expect(
!BoardSettingsAvailability.resolve(tier: .free, mode: mode),
"the free tier has no board settings sheet in any mode"
)
}
#expect(!BoardSettingsAvailability.resolve(mode: .git, isRepositoryUnreadable: true))
}
@Test("Reachable means exactly 'has something to show'")
@@ -111,13 +105,11 @@ struct BoardSettingsAvailabilityTests {
// The derivation, not a coincidence: a surface whose whole job is hosting setup controls has
// no honest empty state, so the two answers are one answer. pro-m2's sections join the
// inventory and this identity keeps holding.
for tier in Tier.allCases {
for mode in BoardGitMode.allCases {
#expect(
BoardSettingsAvailability.resolve(tier: tier, mode: mode)
== !BoardSettingsSection.resolve(tier: tier, mode: mode).isEmpty
)
}
for mode in BoardGitMode.allCases {
#expect(
BoardSettingsAvailability.resolve(mode: mode)
== !BoardSettingsSection.resolve(mode: mode).isEmpty
)
}
}
}
@@ -133,10 +125,11 @@ struct BoardSettingsPresentationTests {
let presentation = BoardSettingsPresentation()
#expect(presentation.isPresented == false)
#expect(presentation.tier == .free)
#expect(presentation.git == nil)
// The free tier's posture is the safe default for a window whose session has not been adopted
// yet: an unreachable sheet, rather than a sheet with no sections in it.
// No session adopted yet, so no sheet: every control this surface hosts writes *through* the
// git state, and a sheet of bare headers is the empty state the design forbids. (It was the
// free tier's default that kept this shut before 12-editions.md PIVOT 2026-08-07; the
// absence of a session is what keeps it shut now.)
#expect(presentation.isReachable == false)
#expect(presentation.sections.isEmpty)
}
@@ -146,7 +139,7 @@ struct BoardSettingsPresentationTests {
let presentation = BoardSettingsPresentation()
presentation.present()
#expect(presentation.isPresented == false, "the free tier has no sheet to open")
#expect(presentation.isPresented == false, "an unadopted window has no sheet to open")
}
@Test("Two windows hold their own flags")
@@ -160,19 +153,20 @@ struct BoardSettingsPresentationTests {
#expect(second.isPresented == false)
}
@Test("Adopting a Pro git session makes the sheet reachable, and dismissal is idempotent")
@Test("Adopting a session makes the sheet reachable, and dismissal is idempotent")
func adoptingASession() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
// Mode `none` the add-git posture, which is the sheet's whole job on a board with no
// repository yet.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
// repository yet. Composed with no tier in sight: git is tier-independent since
// 12-editions.md PIVOT 2026-08-07, so this is every session's git state, not Pro's.
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
let presentation = BoardSettingsPresentation()
presentation.adopt(tier: .pro, git: git)
presentation.adopt(git: git)
#expect(presentation.isReachable)
#expect(presentation.sections == [.git])
@@ -185,23 +179,31 @@ struct BoardSettingsPresentationTests {
#expect(presentation.isPresented == false)
}
@Test("The free tier's session leaves both doors shut, .git on the folder or not")
func aFreeTierSessionIsUnreachable() throws {
@Test("A board carrying a `.git` is a git board — the inert posture is retired")
func anInertGitIsNoLongerInert() throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
try fixture.item("", Item.board)
try fixture.file(".git/HEAD", Data("ref: refs/heads/main\n".utf8))
// `compose` is the tier gate: the free tier gets no git state at all, so there is nothing for
// a settings sheet to be about even on a board carrying an inert `.git` the popover's
// one-line Pro pointer is that board's whole story (12-editions.md).
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
// The retired posture, asserted where it used to bite: a `.git` at a board root was **inert**
// off Pro never read, never written, the popover's one-line Pro pointer that board's whole
// story. Detection runs at every board open on every tier now (12-editions.md PIVOT
// 2026-08-07), so this composes in git mode with no tier asked for.
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
// And what these three bytes actually are is a repository libgit2 will not open, so the board
// takes **06's corrupt-`.git` posture, not the retired inert one** (06 Rules, ruled
// 2026-07-31): git mode throughout never a fall to mode none that would offer add-git
// against an existing `.git` with both doors shut because every section here is a write
// into something the app cannot open.
#expect(git.isRepositoryUnreadable)
let presentation = BoardSettingsPresentation()
presentation.adopt(tier: .free, git: nil)
presentation.adopt(git: git)
#expect(presentation.isReachable == false)
presentation.present()
#expect(presentation.isPresented == false)
#expect(presentation.sections.isEmpty)
}
}