Build the auto-commit engine
Every settled change on a git-mode board commits, debounced 2s past drag/typing churn, staged whole-root with .gitignore respected. GitCommitOperation reaches the vendored libgit2 directly (same 1.9.2 pin SwiftGitX resolves — importable, not duplicated) for signature-capable commits; add-git's config materialization is gone, identity resolves at commit time (repo-local config, else derived default) per the 2026-07-31 ruling in 06. CommitAttribution classifies per file off EchoLedger receipts: user identity on app-mediated windows, Lanework External <[email protected]> on foreign, the modified-by refinement (<slug>@agents.lanework .invalid) when every foreign file agrees, heal-marked receipts split into their own commit — window split foreign → heal → user. Edit-session granularity: ~700ms saves stay uncommitted, staging excludes open session folders (closure-resolved so mid-session moves stage around the new location), session end nudges the debounce so each session lands exactly one body commit. Flush-before-overwrite gates on known-foreign windows and commits synchronously ahead of the write; close/quit flush the pipeline via CloseFlushCoordinator's committerFlush. index.lock backs off briefly then re-debounces silently; clean tree no-ops; genuine failures ride the standing history-suspension banner and retry next debounce. Abnormal repo states (detached HEAD, merge/rebase/cherry-pick in progress) hold the engine with a 15s re-check; unborn HEAD commits "Initial board state" whole-tree; dirty tree at open catches up through the same engine. Message seam (CommitMessageComposing) ships interim — the semantic composer is the next card. Discovery diffs HEAD against an in-memory index with rename detection (git status alone never pairs a bare mv), and a failed survey reads as "could not look", never "nothing changed". 46 new tests / 8 suites, all real repositories via bundled libgit2. 2240 tests / 383 suites green; InertGitTests untouched. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - HistoryCommitSeam
|
||||
|
||||
/// **The three places the auto-committer touches the store's write and reload paths**
|
||||
/// (06-history-undo.md ▸ Rules ▸ Auto-commit, ▸ Flush-before-overwrite).
|
||||
///
|
||||
/// ### Why a struct of closures rather than a reference to the committer
|
||||
///
|
||||
/// `BoardStore` lives in the live store and must not learn what a repository is: the free tier
|
||||
/// composes no `HistoryStore`, so the engine has to be *structurally* unreachable there rather than
|
||||
/// switched off, and a store holding an optional committer would be a store that knows about git.
|
||||
/// One optional value, `nil` on every board that has no committer, is the same shape `watcherBrackets`
|
||||
/// and `history` already take, and it keeps the three orderings — before the write, after the
|
||||
/// bracket, after the landing — stated in one type instead of three properties that could drift.
|
||||
///
|
||||
/// It is also what makes the ordering testable without a repository: a test binds a seam that records
|
||||
/// its calls and asserts that a write flushed before it landed, exactly as `CloseFlushCoordinator`'s
|
||||
/// closures do for the close sequence.
|
||||
@MainActor
|
||||
public struct HistoryCommitSeam {
|
||||
|
||||
/// **Before an app write** — flush the pending auto-commit if this write could overwrite an
|
||||
/// external version that is not in history yet, so "both versions exist as commits" holds.
|
||||
///
|
||||
/// Synchronous because `performWrite` is: an ordering guarantee *before* a synchronous write can
|
||||
/// only be kept synchronously. See `GitAutoCommitter.noteWillWrite()` for the gate that keeps it
|
||||
/// rare and for the costs it carries.
|
||||
public var willWrite: () -> Void
|
||||
|
||||
/// **After a write bracket closes** — harvest the bracket's receipts and arm the debounce.
|
||||
///
|
||||
/// The harvest is why this is a signal of its own: receipts are consumed by the landing reload
|
||||
/// that classifies them, and this is the last moment they still describe a completed write
|
||||
/// (`EchoLedger.outstandingEntries`).
|
||||
public var writeBracketDidClose: () -> Void
|
||||
|
||||
/// **After a reload lands** — arm the debounce, carrying whether the reload revealed anything the
|
||||
/// ledger did not vouch for.
|
||||
public var reloadDidLand: (_ sawForeignChange: Bool) -> Void
|
||||
|
||||
public init(
|
||||
willWrite: @escaping () -> Void,
|
||||
writeBracketDidClose: @escaping () -> Void,
|
||||
reloadDidLand: @escaping (Bool) -> Void
|
||||
) {
|
||||
self.willWrite = willWrite
|
||||
self.writeBracketDidClose = writeBracketDidClose
|
||||
self.reloadDidLand = reloadDidLand
|
||||
}
|
||||
|
||||
/// The seam a session binds for a board that has a committer — the one production composition,
|
||||
/// kept beside the type so no call site spells the three wirings out.
|
||||
public static func binding(to committer: GitAutoCommitter) -> HistoryCommitSeam {
|
||||
HistoryCommitSeam(
|
||||
willWrite: { [weak committer] in committer?.noteWillWrite() },
|
||||
writeBracketDidClose: { [weak committer] in committer?.noteWriteBracketClosed() },
|
||||
reloadDidLand: { [weak committer] saw in committer?.noteReloadLanded(sawForeignChange: saw) }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user