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
+62 -52
View File
@@ -4,8 +4,9 @@ import Testing
@testable import Kanban
/// **The board's git state** (06-history-undo.md Rules; 02-architecture.md Components
/// HistoryStore) composed under the tier, detected at open, and changed afterwards by exactly
/// one thing.
/// HistoryStore) composed for every session, detected at open, and changed afterwards by
/// exactly one thing. It was "composed under the tier" until PIVOT 2026-08-07 (12-editions.md git
/// left the paywall); the tier axis is gone from composition and from everything below it.
///
/// Every repository here is a **real** one, made by the app's own add-git through the bundled
/// libgit2: the card's first criterion is that adding git "initializes a repo at the board root with
@@ -40,8 +41,8 @@ private func commitEverything(at boardRoot: URL, message: String) throws {
_ = try repository.commit(message: message)
}
/// Bytes and mtimes of everything under a subtree `InertGitTests`' instrument, in the shape this
/// file needs it: what proves that *reading* a board's mode touched nothing.
/// Bytes and mtimes of everything under a subtree `UntouchedGitTests`' instrument, in the shape
/// this file needs it: what proves that *reading* a board's mode touched nothing.
private struct SubtreeEntry: Equatable {
let path: String
let data: Data?
@@ -102,33 +103,37 @@ private func plantSHA256Repository(in fixture: WriterFixture) throws {
// MARK: - Composition
@MainActor
@Suite("HistoryStore ▸ composition and the tier gate")
@Suite("HistoryStore ▸ composition and open-time detection")
struct HistoryStoreCompositionTests {
@Test("The free tier composes no git state at all, on any board")
func theFreeTierComposesNothing() throws {
@Test("Composition takes no tier: a board carrying a repository opens in git mode, full stop")
func compositionIsUnconditional() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
// Not "mode none on a git board" *nothing*. With no object there is no path by which a
// free-tier session could read history, commit, or touch `.git` (12-editions.md The free
// tier and `.git`, whose byte-level half is `InertGitTests`).
#expect(HistoryStore.compose(boardRoot: fixture.root, tier: .free) == nil)
// **PIVOT 2026-08-07** (12-editions.md git left the paywall). This was the tier gate's own
// test, and it read the other way: `compose(boardRoot:tier: .free)` answered `nil`, so a free
// session had no git state to consult and never stat'ed a `.git` (the inert posture, made
// structural). The gate is gone the parameter with it and detection now runs on this
// board for every session there is, which is what the assertion below says by having no tier
// to name.
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git, "the `.git` at the root is live, not inert")
}
@Test("Pro on a plain board is mode none — and opening one never creates a repository")
func proOnAPlainBoardIsModeNone() throws {
@Test("A plain board is mode none — and opening one never creates a repository")
func aPlainBoardIsModeNone() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
// "No silent auto-init, ever" (06 Rules) the deliberate pivot from the pathfinder, which
// initialized a repository under every board it opened. Composing twice is the whole test:
// two opens, no repository.
_ = HistoryStore.compose(boardRoot: fixture.root, tier: .pro)
_ = HistoryStore.compose(boardRoot: fixture.root)
#expect(!fixture.exists(".git"), "opening a mode-none board is not an opt-in")
}
@@ -139,14 +144,14 @@ struct HistoryStoreCompositionTests {
// The board a second machine meets: someone ran `git init`/`git clone` (here, the app's own
// add-git in a previous session), and the repository is simply there.
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
let afterInit = try #require(GitRepository.headCommit(at: fixture.root))
let before = try snapshotGitDirectory(fixture.root)
// The next open. Adoption is not init: no dialog, no confirmation, no second step the mode
// is simply what the filesystem says, and it says git.
let second = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let second = HistoryStore.compose(boardRoot: fixture.root)
#expect(second.mode == .git)
// And nothing happened to the repository on the way in: same HEAD, same bytes, same mtimes.
@@ -165,7 +170,7 @@ struct HistoryStoreCompositionTests {
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(git.mode == .repoNested)
}
@@ -174,7 +179,7 @@ struct HistoryStoreCompositionTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .none)
// What a `git init` in a terminal under an open board does which is nothing, until the
@@ -183,7 +188,7 @@ struct HistoryStoreCompositionTests {
try plantGitDirectory(in: fixture)
#expect(git.mode == .none, "the open session keeps the mode it composed with")
let nextOpen = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let nextOpen = HistoryStore.compose(boardRoot: fixture.root)
#expect(nextOpen.mode == .git, "and the next open reflects what it finds")
}
}
@@ -207,11 +212,11 @@ struct HistoryStoreUnreadableRepositoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
// The next open, which is where the probe actually runs.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(!git.isRepositoryUnreadable)
#expect(git.committer?.pause == nil)
@@ -226,7 +231,7 @@ struct HistoryStoreUnreadableRepositoryTests {
// filesystem one question, and not a repository at all to libgit2.
try plantGitDirectory(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
// **Never a fall to mode none** "detection is presence-shaped a corrupt or unopenable
// repo never falls to mode none", which is what keeps add-git from ever being offered
@@ -251,7 +256,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantDanglingGitPointer(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git, "a `.git` file is a repository to git — presence is presence")
#expect(git.isRepositoryUnreadable)
}
@@ -262,7 +267,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantSHA256Repository(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
// 06 Repository hygiene: "an adopted SHA-256 repo the engine cannot open takes the
// corrupt-repo loud-failure path never a silent fall to mode-none".
@@ -276,7 +281,7 @@ struct HistoryStoreUnreadableRepositoryTests {
try plantGitDirectory(in: fixture)
let before = try snapshotGitDirectory(fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.isRepositoryUnreadable)
// "Lanework leaves the repository untouched" the same bytes and the same mtimes, on the
@@ -290,7 +295,7 @@ struct HistoryStoreUnreadableRepositoryTests {
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
await git.writeIdentity(name: "Ada", email: "[email protected]")
#expect(!fixture.exists(".git/config"), "no config was written into a repository nothing can open")
@@ -334,7 +339,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
#expect(fixture.exists(".git"), "a repository at the board root — bundled libgit2, no git install")
@@ -358,7 +363,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The one file the app ever writes into a board because of git, and the one moment it writes
@@ -374,7 +379,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
let derived = GitIdentity.derivedDefault()
@@ -388,7 +393,7 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// Deterministic rather than inherited: libgit2's initial branch comes from an
@@ -409,7 +414,7 @@ struct HistoryStoreAddGitTests {
// (06 Rules Abnormal repo states: "an unborn HEAD is normal git mode").
_ = try Repository.create(at: fixture.root)
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(git.mode == .git)
#expect(GitRepository.branchName(at: fixture.root) != nil)
#expect(GitRepository.headCommit(at: fixture.root) == nil, "no commits yet, and that is fine")
@@ -420,11 +425,11 @@ struct HistoryStoreAddGitTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let first = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let first = HistoryStore.compose(boardRoot: fixture.root)
#expect(await first.addGit())
let head = try #require(GitRepository.headCommit(at: fixture.root))
let second = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let second = HistoryStore.compose(boardRoot: fixture.root)
#expect(await second.addGit() == false, "there is nothing to add")
#expect(GitRepository.headCommit(at: fixture.root)?.oid == head.oid, "and nothing was re-initialized")
}
@@ -439,7 +444,7 @@ struct HistoryStoreAddGitTests {
try FileManager.default.createDirectory(at: boardRoot, withIntermediateDirectories: true)
try Data(Item.board.utf8).write(to: boardRoot.appendingPathComponent("index.md"))
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(await git.addGit() == false)
#expect(git.mode == .repoNested)
#expect(!FileManager.default.fileExists(atPath: boardRoot.appendingPathComponent(".git").path))
@@ -475,7 +480,7 @@ struct HistoryStoreAddGitTests {
// Composed while the enclosing folder is still a plain one: the store's mode is `none`, and
// that is the reading that goes stale.
let git = try #require(HistoryStore.compose(boardRoot: boardRoot, tier: .pro))
let git = HistoryStore.compose(boardRoot: boardRoot)
#expect(git.mode == .none)
// A terminal `git init` one level up, after the detection the store is holding.
@@ -530,7 +535,7 @@ struct HistoryStoreAddGitTests {
// Mode is read once, at composition so a store composed before a `.git` appeared still says
// `none` and reaches `create`, which is the layer that refuses. Any refusal will do here; the
// question is where the answer lands.
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
try plantGitDirectory(in: fixture)
var banners: [String] = []
git.reportFailure = { banners.append($0.message) }
@@ -563,7 +568,7 @@ struct GitPathHistoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// A second card, arriving in a later commit the whole point of the rung.
@@ -587,7 +592,7 @@ struct GitPathHistoryTests {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let plain = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let plain = HistoryStore.compose(boardRoot: fixture.root)
#expect(plain.identityHistoryRanker == nil, "mode none injects nothing")
// And the free tier has no `HistoryStore` to ask in the first place pinned in the
@@ -600,7 +605,7 @@ struct GitPathHistoryTests {
defer { fixture.tearDown() }
try fixture.item(Ident.lane2, Item.rich(order: "2048", title: "Doing"))
let git = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let git = HistoryStore.compose(boardRoot: fixture.root)
#expect(await git.addGit())
// The same identity, arriving later in a second lane the copy the duplicate rule is about.
@@ -648,32 +653,37 @@ private func openBoard(_ model: AppModel, at url: URL) throws -> BoardWindowRef
@Suite("Board sessions ▸ the git state they compose")
struct BoardSessionGitTests {
@Test("A free-tier session carries no git state, even on a board that has a repository")
func freeSessionsCarryNoGitState() throws {
@Test("A free-tier session composes a git state too, and detects the repository it finds")
func freeSessionsComposeAGitStateAsWell() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
try plantGitDirectory(in: fixture)
let (model, tearDown) = try makeModel()
defer { tearDown() }
// **PIVOT 2026-08-07** (12-editions.md). The inverse of what this test used to pin: the free
// tier carried no git state at all, reported mode `none` on this very board, and injected
// nothing into the loader. Git left the paywall, so the session composes exactly what a Pro
// session composes same call, no tier in it.
model.currentTier = { .free }
let ref = try openBoard(model, at: fixture.root)
let session = try #require(model.session(for: ref))
#expect(session.git == nil)
#expect(session.gitMode == .none, "the free tier ships exactly one mode")
#expect(session.store.makeIdentityHistoryRanker == nil, "and injects nothing into the loader")
#expect(session.git != nil)
#expect(session.gitMode == .git, "the mode is the disk's answer, not the tier's")
#expect(session.store.makeIdentityHistoryRanker != nil, "and the loader's rung is wired")
#expect(session.tier == .free, "the tier is still recorded — it just decides nothing here")
}
@Test("A Pro session on a git board composes git mode and wires the loader's ranker")
func proSessionsCarryTheDetectedMode() async throws {
@Test("A session on a git board composes git mode and wires the loader's ranker")
func sessionsCarryTheDetectedMode() async throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()
defer { tearDown() }
// A real repository, so the ranker has something to read.
let seed = try #require(HistoryStore.compose(boardRoot: fixture.root, tier: .pro))
let seed = HistoryStore.compose(boardRoot: fixture.root)
#expect(await seed.addGit())
model.currentTier = { .pro }
@@ -685,13 +695,13 @@ struct BoardSessionGitTests {
let ranker = try #require(provider())
#expect(ranker.rank("\(Ident.lane1)/\(Ident.card1)") != nil)
// The provider binding arrived with the undo/redo card: a Pro session on a git board binds
// the git substrate over exactly this mode (12-editions.md The provider seam).
// The provider binding arrived with the undo/redo card: a session on a git board binds the
// git substrate over exactly this mode (12-editions.md The provider seam).
#expect(session.history is GitHistoryProvider)
}
@Test("A Pro session on a plain board is mode none and injects nothing")
func proSessionsOnPlainBoardsInjectNothing() throws {
@Test("A session on a plain board is mode none and injects nothing")
func sessionsOnPlainBoardsInjectNothing() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let (model, tearDown) = try makeModel()