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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user