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
-115
View File
@@ -1,5 +1,4 @@
import Foundation
import SwiftGitX
import Testing
@testable import Kanban
@@ -653,117 +652,3 @@ struct BoardDecisionSurfaceAttendanceTests {
#expect(second.access == nil)
}
}
// MARK: - The Pro repair commit
/// HEAD's first-parent ancestry, newest first read through SwiftGitX, never through the committer
/// that made the commits (`AutoCommitTests`' rule, kept: nothing here shells out to `git`).
private func repairHistory(at boardRoot: URL, limit: Int = 8) throws -> [(subject: String, authorName: String, authorEmail: String, committerName: String)] {
let repository = try Repository.open(at: boardRoot)
guard !repository.isHEADUnborn, let tip = try repository.HEAD.target as? Commit else { return [] }
var records: [(String, String, String, String)] = []
var current: Commit? = tip
while let commit = current, records.count < limit {
records.append((commit.summary, commit.author.name, commit.author.email, commit.committer.name))
current = (try? commit.parents)?.first
}
return records
}
@MainActor
@Suite("Decision surface ▸ the Pro repair commit")
struct BoardDecisionSurfaceRepairCommitTests {
/// An `AppModel` whose app-side state lives in temp rather than in the app's real Application
/// Support home, and which reads as Pro `AutoCommitCompositionRootTests`' fixture.
private func makeModel() throws -> (model: AppModel, tearDown: () -> Void) {
let folder = FileManager.default.temporaryDirectory
.appendingPathComponent("DecisionRepairCommit-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: folder, withIntermediateDirectories: true)
let model = AppModel(
registryStorageURL: folder.appendingPathComponent("board-registry.json"),
clipboardStagingRoot: folder.appendingPathComponent("Clipboard", isDirectory: true)
)
model.currentTier = { .pro }
return (model, { try? FileManager.default.removeItem(at: folder) })
}
/// **A repaired Pro board's first flush is one heal commit, authored by the integrity identity**
/// (01-storage-format.md § Malformed input: "On Pro boards the repairs drop heal-marked receipts
/// and commit separately as one repair commit, never folded into anyone else's work";
/// 06-history-undo.md Commit messages Healing mutations commit separately).
///
/// The whole chain is exercised end to end, because every link in it can fail silently and the
/// symptom is identical each time a commit blaming the outside world for the app's own repair:
///
/// 1. The surface's default resolution is the minted repair.
/// 2. `BoardRepairRun` writes it store-lessly and marks every receipt in its own ledger.
/// 3. The store built by the following walk **adopts** that ledger (`EchoLedger.adopt`)
/// before `beginSession`, which is where Pro's committer is composed and started.
/// 4. `GitAutoCommitter.start()` harvests, so the debounce it arms can see receipts that were
/// dropped before any write bracket of this session existed.
/// 5. `CommitAttribution.split` sorts the repaired path into the heal class, and the heal class
/// is authored `Lanework Integrity <integrity@lanework.invalid>` with the user as committer.
@Test("A repaired board under Pro + git produces a separate heal commit")
func aRepairCommitsAsTheIntegrityIdentity() async throws {
let fixture = try WriterFixture()
defer { fixture.tearDown() }
// A board whose root is missing `schema` the minted stamp's own case. The lane is here so
// the repository has an ordinary tree around the file being repaired.
try fixture.item("", "---\ntitle: Needs A Stamp\ncreated: 2026-01-01T09:00:00Z\n---\nBoard.\n")
try fixture.item(Ident.lane1, "---\nschema: 1\ntitle: Todo\norder: 1024\n---\n")
let (model, tearDown) = try makeModel()
defer { tearDown() }
// The repository, with everything as it stands committed including the broken root, which is
// what makes the repair a real change rather than a fresh file.
let git = HistoryStore.compose(boardRoot: fixture.root, ledger: EchoLedger())
#expect(await git.addGit())
let commitsBefore = try repairHistory(at: fixture.root).count
git.stopAutoCommit()
// The surface, exactly as the window builds it.
let surface = BoardDecisionSurfaceModel(failure: try failure(of: fixture), boardRoot: fixture.root)
#expect(surface.canRepairAndOpen, "a lone missing root schema is a minted repair, preselected")
// Repair and Open's write half.
let outcome = BoardRepairRun.apply(surface.plannedRepairs, boardRoot: fixture.root)
#expect(outcome.failure == nil)
// The walk that follows, and the store it builds. Built directly rather than through the
// registry so this case is about the repair's commit and not about the open's *other* heals
// (the agent guide, the `.gitignore` seed), which the registry's acquire also fires.
let result = try BoardLoader.load(boardRoot: fixture.root, skipping: surface.skipSet)
let store = BoardStore(rootURL: fixture.root, loaded: result, skipping: surface.skipSet)
// The adoption, before the session composes the committer.
store.echoes.adopt(outcome.ledger)
let ref = BoardWindowRef(url: fixture.root)
let recordID = model.boardRegistry.recordOpen(of: fixture.root)
model.beginSession(ref: ref, store: store, recordID: recordID, access: nil)
let committer = try #require(model.session(for: ref)?.git?.committer)
// The debounce `start()` armed is not what this asserts; the explicit flush is.
committer.stop()
committer.debounceInterval = .seconds(30)
committer.coveringSnapshotDeadline = .milliseconds(50)
committer.coveringSnapshotPollInterval = .milliseconds(5)
await committer.flushNow()
let log = try repairHistory(at: fixture.root)
#expect(log.count == commitsBefore + 1, "one repair commit, never folded and never split further")
let head = try #require(log.first)
#expect(head.authorName == CommitAttribution.integrityAuthorName)
#expect(head.authorEmail == CommitAttribution.integrityAuthorEmail)
#expect(
head.authorEmail != CommitAttribution.externalAuthorEmail,
"the app's own repair must never be blamed on the outside world"
)
// "the committer stays the user (the recorded-by convention)".
#expect(head.committerName == GitCommitOperation.userIdentity(at: fixture.root).name)
#expect(GitCommitOperation.changedPaths(at: fixture.root).isEmpty, "the flush leaves nothing dirty")
}
}