The seams unbind — the provider is always native, and the git stack compiles dead

Step 4 of strategy/01-git-excision.md, the entangled one: AppModel's makeHistoryProvider collapses to the native provider (the seam stays injectable per the reversibility posture), the session's git state and its wiring go (wireGitUndo, wireBranchSwitching, the card-session staging threading), BoardStore sheds commitSeam and the identity-history ranker (the loader's nil-safe rung now tops out at birth date — today's no-git behavior), SessionSettleGate keeps the gate and inherits the path utility it borrowed, BoardRegistry drops the persisted operation stamp (decode-safe), and the git banner family leaves BannerCenter with its announcer and accessibility phrases. One missed harvest tie severed (the narrator's root subject is its own now). Nothing outside Kanban/Git/ references the stack — proven by sweep. 2,855 tests green.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 11:25:41 -04:00
parent f6a24132b6
commit cdba512512
24 changed files with 281 additions and 2567 deletions
+11 -120
View File
@@ -288,10 +288,9 @@ public final class BoardStore: HealHost {
/// still refresh the card window's thread and board search's comment index. Keying either on
/// `snapshotGeneration` would make the value-equal skip a freshness bug rather than an
/// optimization.
/// - **The auto-committer's covering gate** (`GitAutoCommitter.awaitCoveringSnapshot`): what
/// covers a flush is a *walk* that started after its writes hit disk, and a completed walk
/// covers them whether or not the tree turned out to differ. A gate waiting on the applied
/// counter would spin out its whole deadline on any value-equal landing.
/// - **Any covering gate** a consumer waiting for a walk that started after its writes hit
/// disk. A completed walk covers them whether or not the tree turned out to differ, so a gate
/// waiting on the applied counter would spin out its whole deadline on any value-equal landing.
///
/// A failed reload bumps neither counter: it produced no snapshot, so it covers nothing and
/// refreshes nothing the pre-skip behaviour of `snapshotGeneration` exactly, kept exactly.
@@ -471,41 +470,10 @@ public final class BoardStore: HealHost {
losses: banners.losses,
suspension: banners.historySuspension,
operations: banners.operations,
signposts: banners.signposts,
gitFailures: banners.gitFailures,
repositoryUnreadable: banners.isRepositoryUnreadable
signposts: banners.signposts
)
}
/// **The unreadable repository, raised or healed** (06-history-undo.md Rules, "A `.git` that
/// isn't a valid repository still reads as git mode and fails loudly", ruled 2026-07-31) the
/// session's one call for a condition that is the *git state's* truth rather than this store's
/// (`HistoryStore.isRepositoryUnreadable`, wired in `AppModel.beginSession`).
///
/// It does two things because the ruling asks for two: the row stands on the strip, and it is
/// **announced** "announced per 10-accessibility.md", which makes a standing banner "an
/// accessibility element announced when it appears and when it clears". This condition never
/// arrives on a reload (it is detected at open and healed by the paused engine's own re-read), so
/// it takes `announceLockChange(from:)`'s path exactly: through `BoardAnnouncer`'s ladder rather
/// than posting directly, so the sentence a user hears and the sentence they read off the row are
/// one string.
///
/// Idempotent, and silent when nothing changed: the 15 s re-read that keeps confirming an
/// unreadable repository must not say so every 15 s.
public func noteRepositoryUnreadable(_ unreadable: Bool) {
guard banners.isRepositoryUnreadable != unreadable else { return }
if unreadable {
Self.logger.error("this board's git repository could not be opened — history is paused")
banners.raiseRepositoryUnreadable()
} else {
banners.clearRepositoryUnreadable()
}
var facts = BoardAnnouncer.ReloadFacts()
facts.repositoryUnreadableBefore = !unreadable
facts.repositoryUnreadableAfter = unreadable
announce(BoardAnnouncer.speech(for: facts))
}
// MARK: Wiring
/// The watcher's bracket calls, injected rather than owned: the registry holds the watcher and
@@ -569,18 +537,6 @@ public final class BoardStore: HealHost {
@ObservationIgnored
public weak var history: (any HistoryProviding)?
/// **Where Pro's auto-committer meets the write and reload paths** (06-history-undo.md Rules
/// Auto-commit), or `nil` on every board there is no committer for which is every free-tier
/// board and every Pro board without a repository at its root.
///
/// Injected like `watcherBrackets` and `history`, and for their reason: the committer belongs to
/// the *session* (`HistoryStore.committer`), and a store that reached for one would be a second
/// answer to which committer a board has. `nil` keeps every method below behaving exactly as it
/// did before this milestone which is what makes the free tier's inert posture structural
/// rather than conditional.
@ObservationIgnored
public var commitSeam: HistoryCommitSeam?
// MARK: The open card windows
/// **Which of this board's cards have a card window open right now** one fact, kept for one
@@ -761,28 +717,6 @@ public final class BoardStore: HealHost {
@ObservationIgnored
var announce: @MainActor (String?) -> Void = { AccessibilityAnnouncer.post($0) }
/// **Where git path history reaches the loader** (01-storage-format.md Fractal layout
/// Rules, the duplicate-id winner rule; `BoardLoader.IdentityHistoryRanker`) `nil` on every
/// board the app manages no git for, which is every free-tier board and every Pro board without
/// a repo at its root.
///
/// A **provider** rather than a ranker, for two reasons that point the same way. Each load wants
/// its own ranker, so that a load never answers from a history that has moved since the last one
/// (the ranker caches internally, once, per load). And add-git flips a board into git mode
/// mid-session, which a closure asked at load time absorbs by construction while a value handed
/// over at composition never could.
///
/// `@MainActor` because it is called here, on the main actor, at the head of each reload; what
/// it returns is `Sendable` and does its git work off-main, inside the walk that consults it.
///
/// **The board's first load predates this** `init` runs inside `BoardStoreRegistry.acquire`,
/// before a session exists to compose the git state that supplies it so an opening board's
/// duplicate-id ladder falls through to birth date, and every reload after it consults history.
/// Deliberate, and the narrow cost of composing the git state where the design puts it
/// (`AppModel.beginSession`) rather than where the first walk happens to run.
@ObservationIgnored
var makeIdentityHistoryRanker: (@MainActor () -> BoardLoader.IdentityHistoryRanker?)?
private static let logger = Logger(subsystem: "dev.rzen.indie.Kanban", category: "store")
// MARK: - Init
@@ -904,10 +838,6 @@ public final class BoardStore: HealHost {
let generation = reloadGeneration
let root = rootURL
let barrier = loadBarrier
// Asked once per load, on the main actor, and answered off it: what comes back is a lazy
// `Sendable` value that touches libgit2 only if this walk finds a duplicate identity to
// break a tie for. `nil` everywhere the app manages no git.
let historyRanker = makeIdentityHistoryRanker?()
// **This session's consented skips, on every walk it runs** (`skippedPaths`): the open's
// decision stands for the session, so a reload sees the board the user chose to open.
let skipping = skippedPaths
@@ -928,7 +858,6 @@ public final class BoardStore: HealHost {
outcome = .success(try BoardLoader.load(
boardRoot: root,
skipping: skipping,
historyRanker: historyRanker,
memo: memo,
counter: counter
))
@@ -982,13 +911,6 @@ public final class BoardStore: HealHost {
facts.lockBefore = readOnlyLock
facts.breakageBefore = reloadFailure
// **Whether this reload revealed anything the app does not vouch for** the one bit the
// auto-committer's flush-before-overwrite gate turns on (06 Rules Flush-before-overwrite).
// A failed reload counts as foreign, conservatively: a file the loader could not read is one
// the app certainly did not write, and the safe direction is to let the next app write commit
// what is there before overwriting it.
var sawForeignChange = false
switch outcome {
case let .success(result):
// **What changed, who changed it, and what it cost the cursor** all three computed
@@ -1020,7 +942,6 @@ public final class BoardStore: HealHost {
includingTrash: shownTrash
)
facts.diff = verdicts.foreign
sawForeignChange = verdicts.foreign.boardChanged || !verdicts.foreignItems.isEmpty
// The vanishing-focus sentence takes the same gate, one rung up the ladder: it says
// "deleted *externally*", which would be a lie about an app-mediated delete whose
// own command already chose a successor (04-interactions.md The map's rule) and
@@ -1170,7 +1091,6 @@ public final class BoardStore: HealHost {
if endsWholesaleOperation, readOnlyLock == nil {
readOnlyLock = .bracketedReloadFailed
}
sawForeignChange = true
Self.logger.error("reload \(generation, privacy: .public) failed: \(error.description, privacy: .public)")
}
@@ -1181,18 +1101,6 @@ public final class BoardStore: HealHost {
facts.lockAfter = readOnlyLock
facts.breakageAfter = reloadFailure
announce(BoardAnnouncer.speech(for: facts))
// **The auto-commit debounce, armed by every landing** (06 Rules Auto-commit;
// Interaction with external writers: "Agent and hand edits arrive through the watcher like
// any change and get auto-committed on the same debounce").
//
// Here rather than at the watcher, deliberately: a reload landing means the tree walk is
// over, so the committer never races the loader for the same files. **Unconditional on what
// changed**, equally deliberately a reload lands whether or not the snapshot moved, and the
// committer's condition is the *tree*, not the snapshot diff, so a window that touched only
// strays or only `CLAUDE.md` still commits (06 Commit messages Non-snapshot files commit
// too). A landing that finds nothing to commit is the silent no-op, not a wasted trip.
commitSeam?.reloadDidLand(sawForeignChange)
}
/// Installs the recovery `BoardAnnouncer` chose for a focus that vanished under a foreign
@@ -1448,24 +1356,11 @@ public final class BoardStore: HealHost {
if let readOnlyLock {
throw BoardStoreWriteRefusal.readOnlyLocked(readOnlyLock)
}
// **Flush-before-overwrite** (06-history-undo.md Rules), before the bracket rather than
// inside it: what the committer may need to do here is *commit*, and a commit taken with the
// watcher suspended would be a commit whose own reload never arrives. It is a no-op unless
// the window holds a change the app does not vouch for see `GitAutoCommitter.noteWillWrite`
// for the gate, and for the two costs it is recorded as carrying.
commitSeam?.willWrite()
watcherBrackets?.begin()
// `defer`, not a trailing call: a Writer operation that fails partway has still touched disk,
// and an unbalanced bracket would leave the watcher suspended for the rest of the session.
//
// **The receipt harvest rides the same defer**, and after `end()` deliberately: the committer
// copies the ledger's receipts here because the landing reload *consumes* them, and this is
// the last moment they still describe a completed write nothing has classified yet
// (`EchoLedger.outstandingEntries`). A partway failure harvests too bytes that reached disk
// are bytes the next commit will carry, whoever they belong to.
defer {
watcherBrackets?.end()
commitSeam?.writeBracketDidClose()
}
// **The receipt seam** (02-architecture.md Components EchoLedger). Binding the ledger
// here rather than passing it down is what keeps `BoardWriter` the stateless enum of statics
@@ -1535,25 +1430,21 @@ public final class BoardStore: HealHost {
try operation()
} catch let error as BoardWriteError {
// Same honesty rule as `performWrite`, applied to the one error type the banner has
// phrasing for. A wholesale operation is usually git's (m7), whose own failure
// vocabulary is not `BoardWriteError` and whose surfacing the suspended-history
// condition, the in-progress row swapping for an error is the committer's to drive;
// but a `BoardWriteError` escaping here is an ordinary failed write and may no more
// bypass the strip than one from `performWrite`.
// phrasing for. A wholesale operation may carry a failure vocabulary of its own, whose
// surfacing is that operation's to drive; but a `BoardWriteError` escaping here is an
// ordinary failed write and may no more bypass the strip than one from `performWrite`.
banners.post(error)
throw error
}
}
/// The same bracket over work that **awaits** the undo restore (06-history-undo.md) and, next,
/// the branch switch.
/// The same bracket over work that **awaits**.
///
/// A sibling rather than a replacement, and the reason is a hard fact about the two callers: the
/// synchronous version above exists because `performWrite`-shaped work is synchronous, while a
/// git operation is a detached libgit2 task the main actor must not block on
/// (`GitRepository`'s isolation rule). Both keep the bracket, the reload floor and the completion
/// phrase in one place; the distinct argument label is what keeps overload resolution from having
/// to guess which one a trailing closure meant.
/// wholesale operation can be a detached task the main actor must not block on. Both keep the
/// bracket, the reload floor and the completion phrase in one place; the distinct argument label
/// is what keeps overload resolution from having to guess which one a trailing closure meant.
///
/// The refusal, the ordering and the arming are the synchronous version's, unchanged see its
/// doc comment for all three.