Make the card window the commit unit on Pro boards

Phase C of the two-level undo card: the committer stages around the
whole open card folder — comments included — so gestures in an open
window never land in interim commits; window close flushes the session
as one semantically-named commit ("Edit card 'X'" with the thread as
body bullets, "Mixed update — N changes to card 'X'" when events mix),
with the two-commit foreign/user split preserved and the
comments/.trash purge riding the same bracket. Comment gestures lose
their per-gesture commits structurally (they write inside the held
folder). Branch-switch settle releases every window's staging before
checkout and re-arms on resume.

Fixes two latent pro-m1 defects: the committer was composed without
the store's EchoLedger, so every production commit classified foreign
and was authored Lanework External; and interim flushes dropped
harvest receipts they had not spent, unvouching the session's own
writes at close. Also lands 06's mixed-subject re-ruling (the retired
"Update board" fallback) and phase B's two files missed by the
previous commit's pathspec.

2444 tests in 422 suites green.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-07-31 20:23:45 -04:00
parent 9119aa1e9a
commit a381fac742
11 changed files with 1709 additions and 105 deletions
+11 -10
View File
@@ -439,7 +439,7 @@ struct AutoCommitAttributionTests {
// MARK: - The stage-around
@MainActor
@Suite("Auto-commit ▸ staging around open Edit sessions")
@Suite("Auto-commit ▸ staging around open card windows")
struct AutoCommitStageAroundTests {
@Test("A lane move mid-session commits the move without touching the session card's folder")
@@ -449,7 +449,7 @@ struct AutoCommitStageAroundTests {
let committer = try quickCommitter(git)
let sessionFolder = fixture.url("\(Ident.lane1)/\(Ident.card1)")
committer.beginEditSession(UUID()) { sessionFolder }
committer.beginCardSession(UUID()) { sessionFolder }
// The editor's ~700 ms save lands on disk, uncommitted
try fixture.item("\(Ident.lane1)/\(Ident.card1)", plain(order: "1024", title: "First", body: "half-typed"))
@@ -474,7 +474,7 @@ struct AutoCommitStageAroundTests {
let committer = try quickCommitter(git)
let sessionFolder = fixture.url("\(Ident.lane1)/\(Ident.card1)")
committer.beginEditSession(UUID()) { sessionFolder }
committer.beginCardSession(UUID()) { sessionFolder }
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/diagram.txt", Data("x\n".utf8))
try fixture.file("elsewhere.txt", Data("y\n".utf8))
@@ -486,7 +486,7 @@ struct AutoCommitStageAroundTests {
.contains("\(Ident.lane1)/\(Ident.card1)/attachments/diagram.txt"))
}
@Test("Ending the session produces exactly one body commit for it")
@Test("Ending the session produces exactly one commit for it")
func endingTheSessionCommitsOnce() async throws {
let (fixture, git, _) = try await makeGitBoard()
defer { fixture.tearDown() }
@@ -494,7 +494,7 @@ struct AutoCommitStageAroundTests {
let token = UUID()
let sessionFolder = fixture.url("\(Ident.lane1)/\(Ident.card1)")
committer.beginEditSession(token) { sessionFolder }
committer.beginCardSession(token) { sessionFolder }
// Three debounced saves inside one session each a real write, none of them a commit
// ("the body editor's ~700 ms disk saves stay uncommitted").
@@ -506,11 +506,12 @@ struct AutoCommitStageAroundTests {
}
#expect(committer.commitCount == 0, "no save tick may become a commit")
// The EditPreview flip "the effective Save button".
committer.endEditSession(token)
// The window close "window close flushes the session as one commit" (06 Rules
// Auto-commit, widened 2026-07-31: the unit is the window, not the EditPreview flip).
committer.endCardSession(token)
try await waitUntil { committer.commitCount == 1 }
#expect(committer.commitCount == 1, "exactly one body commit per session")
#expect(committer.commitCount == 1, "exactly one commit per card-window session")
#expect(isClean(at: fixture.root))
}
@@ -523,7 +524,7 @@ struct AutoCommitStageAroundTests {
// The registry holds a resolver, not a URL, so a lane move under an open session keeps the
// right folder excluded rather than the one Edit was entered in.
var lane = Ident.lane1
committer.beginEditSession(UUID()) { fixture.url("\(lane)/\(Ident.card1)") }
committer.beginCardSession(UUID()) { fixture.url("\(lane)/\(Ident.card1)") }
try fixture.item(Ident.lane2, plain(order: "2048", title: "Doing"))
try fixture.moveFolder("\(Ident.lane1)/\(Ident.card1)", to: "\(Ident.lane2)/\(Ident.card1)")
@@ -544,7 +545,7 @@ struct AutoCommitStageAroundTests {
defer { fixture.tearDown() }
let committer = try quickCommitter(git)
committer.beginEditSession(UUID()) { fixture.url("\(Ident.lane1)/\(Ident.card1)") }
committer.beginCardSession(UUID()) { fixture.url("\(Ident.lane1)/\(Ident.card1)") }
try fixture.item("\(Ident.lane1)/\(Ident.card1)", plain(order: "1024", title: "First", body: "typing"))
committer.noteWriteBracketClosed()
await committer.flushNow()