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:
@@ -3,9 +3,9 @@ import Foundation
|
||||
|
||||
// MARK: - Vocabulary
|
||||
|
||||
/// What the user chose at the save-or-discard step (06-history-undo.md ▸ Branch switching: "**Save
|
||||
/// All** ends every session with its normal commit …, **Discard** reverts buffers and uncommitted
|
||||
/// saves to HEAD, **Cancel** keeps the current branch and the sessions").
|
||||
/// What the user chose at the save-or-discard step: **Save All** ends every session with its normal
|
||||
/// save, **Discard** reverts buffers and leaves the wholesale operation to reconcile what already
|
||||
/// reached disk, **Cancel** keeps everything exactly as it is.
|
||||
public enum SessionSettleChoice: Sendable, Equatable {
|
||||
case saveAll
|
||||
case discard
|
||||
@@ -21,10 +21,9 @@ public enum SessionSettleOutcome: Sendable, Equatable {
|
||||
/// The user chose Cancel. "Cancel keeps everything" — nothing was written, nothing reverted.
|
||||
case cancelled
|
||||
|
||||
/// **Save All met a raw-source buffer that would not validate.** "Since Apply validates, a buffer
|
||||
/// that fails validation cancels the whole switch with focus on the offending window, nothing
|
||||
/// half-switched" (06 ▸ Branch switching). The payload is that window's session id, already
|
||||
/// focused by the gate.
|
||||
/// **Save All met a raw-source buffer that would not validate.** Since Apply validates, a buffer
|
||||
/// that fails validation cancels the whole operation with focus on the offending window, nothing
|
||||
/// half-done. The payload is that window's session id, already focused by the gate.
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
@@ -79,7 +78,7 @@ public struct SettleableSession {
|
||||
///
|
||||
/// Matched component-wise against the paths a wholesale operation would write, which is what
|
||||
/// makes the match survive a lane move: a card's own folder component never changes, only the
|
||||
/// lane above it (`GitHistoryWalk.path(_:isInsideFolderNamed:)`, the same trick, same reason).
|
||||
/// lane above it (`SessionSettleGate.path(_:isInsideFolderNamed:)`).
|
||||
public let cardFolderName: String
|
||||
|
||||
/// Whether this session is holding state a wholesale tree operation would disturb: unsaved
|
||||
@@ -91,10 +90,9 @@ public struct SettleableSession {
|
||||
/// the raw buffer. `false` means the raw buffer failed validation — the whole operation is off.
|
||||
public let saveAll: @MainActor () -> Bool
|
||||
|
||||
/// **Discard** for this one session: revert the buffer and leave raw source without writing. The
|
||||
/// on-disk uncommitted saves are reverted by the operation itself, which is comparing this card's
|
||||
/// folder against the working tree rather than against HEAD for exactly that reason
|
||||
/// (`GitRestoreOperation.plan(at:target:excluding:reconciling:)`).
|
||||
/// **Discard** for this one session: revert the buffer and leave raw source without writing. What
|
||||
/// already reached disk is reconciled by the wholesale operation behind the gate, which is the
|
||||
/// only party that knows the state it is writing towards.
|
||||
public let discard: @MainActor () -> Void
|
||||
|
||||
public init(
|
||||
@@ -117,23 +115,22 @@ public struct SettleableSession {
|
||||
/// **The save-or-discard step**, as one reusable decision procedure (06-history-undo.md ▸ Rules
|
||||
/// ▸ Undo restore vs open Edit sessions; ▸ Branch switching).
|
||||
///
|
||||
/// ### One machinery, two callers, by design
|
||||
/// ### One machinery, every caller, by design
|
||||
///
|
||||
/// 06 does not describe two gates. It describes the branch-switch step and then hands undo the same
|
||||
/// one by name: "When the diff *does* touch a session card, the restore **gates on the branch-switch
|
||||
/// save-or-discard step** (Branch switching below — Save All / Discard / Cancel, same machinery, same
|
||||
/// rationale)." So this object is written for both from the start; the undo provider is its first
|
||||
/// caller and the branch controls will be its second, passing the paths a checkout would write
|
||||
/// instead of the paths a restore would.
|
||||
/// The design never described a gate per operation: it described one save-or-discard step — Save All
|
||||
/// / Discard / Cancel — and handed it to whatever wants to move the tree wholesale under an open
|
||||
/// editor. So this object is written for all of them. **Nothing calls it today** (`strategy/
|
||||
/// 01-git-excision.md` ▸ What is deleted: the git restore and branch switch were its two callers and
|
||||
/// went with the stack); it is kept because it is the settled answer to a question the next wholesale
|
||||
/// operation will ask on its first day.
|
||||
///
|
||||
/// ### The diff decides whether it appears at all
|
||||
/// ### The paths decide whether it appears at all
|
||||
///
|
||||
/// "A restore materializes only the diff between the current tree and the target state, so a card
|
||||
/// whose open Edit session the diff doesn't touch is simply unaffected — its uncommitted ~700 ms
|
||||
/// saves and the stage-around rule continue undisturbed, and most undos never meet an editor at all."
|
||||
/// That is `settle(touching:)`'s first line, and it is why the gate takes paths rather than a
|
||||
/// yes/no: a modal that appeared on every ⌘Z because *some* window somewhere was in Edit would be a
|
||||
/// different, much worse feature.
|
||||
/// An operation that materializes only a diff leaves a card whose open Edit session the diff doesn't
|
||||
/// touch simply unaffected — that card's uncommitted ~700 ms saves continue undisturbed. That is
|
||||
/// `settle(touching:)`'s first line, and it is why the gate takes paths rather than a yes/no: a modal
|
||||
/// that appeared on every gesture because *some* window somewhere was in Edit would be a different,
|
||||
/// much worse feature. `settleAll()` is the other shape, for an operation that moves everything.
|
||||
///
|
||||
/// ### Why the ask is a closure
|
||||
///
|
||||
@@ -174,17 +171,16 @@ public struct SessionSettleGate {
|
||||
await decide(over: Self.reached(by: paths, among: sessions()))
|
||||
}
|
||||
|
||||
/// **Settles every open session, whatever the operation writes** — the branch switch's gate
|
||||
/// (06 ▸ Branch switching).
|
||||
/// **Settles every open session, whatever the operation writes** — the gate for an operation that
|
||||
/// replaces the tree wholesale rather than materializing a diff.
|
||||
///
|
||||
/// The path filter above is the *restore's* narrowing and belongs to it alone: "a restore
|
||||
/// materializes only the diff between the current tree and the target state, so a card whose open
|
||||
/// Edit session the diff doesn't touch is simply unaffected". A branch switch has no such
|
||||
/// property. It moves the whole tree at once, and the raw-source hazard 06 singles out — "an
|
||||
/// unsettled raw buffer … its Apply later writes the *entire* pre-switch `index.md` byte-for-byte
|
||||
/// onto the new branch's card" — is about the buffer belonging to the old branch, not about
|
||||
/// whether the checkout happened to rewrite that card. So this asks about every session that is
|
||||
/// holding something, and about no path at all.
|
||||
/// The path filter above is a diff-shaped operation's narrowing and belongs to it alone: a card
|
||||
/// whose open Edit session the diff doesn't touch is simply unaffected. A wholesale replacement
|
||||
/// has no such property, and the raw-source hazard is the reason it cannot borrow one — an
|
||||
/// unsettled raw buffer's Apply later writes the *entire* pre-operation `index.md` byte-for-byte
|
||||
/// over whatever now stands there, which is about the buffer describing a state that has gone, not
|
||||
/// about whether the operation happened to rewrite that card. So this asks about every session
|
||||
/// that is holding something, and about no path at all.
|
||||
public func settleAll() async -> SessionSettleOutcome {
|
||||
await decide(over: sessions())
|
||||
}
|
||||
@@ -225,9 +221,21 @@ public struct SessionSettleGate {
|
||||
) -> [SettleableSession] {
|
||||
guard !paths.isEmpty else { return [] }
|
||||
return sessions.filter { session in
|
||||
paths.contains { GitHistoryWalk.path($0, isInsideFolderNamed: session.cardFolderName) }
|
||||
paths.contains { path($0, isInsideFolderNamed: session.cardFolderName) }
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `path` lies inside a folder named `name` — component-exact, so a card whose id is a
|
||||
/// prefix of another's cannot be dragged into a step it has no business in.
|
||||
///
|
||||
/// Moved here from the git history walk when the git stack was unbound (`strategy/
|
||||
/// 01-git-excision.md` ▸ Sequencing ▸ Seam unbind); the walk was its only other caller.
|
||||
///
|
||||
/// `dropLast()` because the *containing* folders are the question: a path that ends in the folder
|
||||
/// name is a file called that, not a file inside it.
|
||||
static func path(_ path: String, isInsideFolderNamed name: String) -> Bool {
|
||||
path.split(separator: "/").dropLast().contains { $0 == name }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The presented step
|
||||
@@ -236,12 +244,11 @@ public struct SessionSettleGate {
|
||||
///
|
||||
/// One of 02-architecture.md's sanctioned modal moments, and it is modal for `DirtyBufferGuard`'s
|
||||
/// reason exactly: the operation behind it cannot proceed until the user has decided what happens to
|
||||
/// text no commit protects, and there is no non-modal shape for a question whose three answers are
|
||||
/// text no save protects, and there is no non-modal shape for a question whose three answers are
|
||||
/// mutually exclusive and immediate.
|
||||
///
|
||||
/// The wording is 06's own vocabulary. The default is **Cancel**, deliberately: a Return pressed
|
||||
/// reflexively at a dialog nobody read must be the answer that changes nothing, and both other
|
||||
/// answers write.
|
||||
/// The default is **Cancel**, deliberately: a Return pressed reflexively at a dialog nobody read must
|
||||
/// be the answer that changes nothing, and both other answers write.
|
||||
public enum SessionSettleStep {
|
||||
|
||||
public static let title = "Unsaved card edits"
|
||||
@@ -251,12 +258,12 @@ public enum SessionSettleStep {
|
||||
Save them, discard the changes, or cancel.
|
||||
"""
|
||||
|
||||
/// The same three buttons, asked for the other caller. **One sentence differs, deliberately**: the
|
||||
/// consequence a user is deciding about is not the same one — a restore would change the cards
|
||||
/// being edited, while a switch takes every card to a different branch — and a step that described
|
||||
/// the wrong operation would be a worse modal than no wording at all.
|
||||
public static let branchSwitchMessage = """
|
||||
Switching branches would replace the cards you are editing. \
|
||||
/// The same three buttons, asked for an operation that replaces the tree rather than editing part
|
||||
/// of it. **One sentence differs, deliberately**: the consequence a user is deciding about is not
|
||||
/// the same one, and a step that described the wrong operation would be a worse modal than no
|
||||
/// wording at all. Kept unused beside the gate, for the gate's reason.
|
||||
public static let wholesaleReplacementMessage = """
|
||||
This would replace the cards you are editing. \
|
||||
Save them, discard the changes, or cancel.
|
||||
"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user