The paper agrees with the code — guide v11, README, DESIGN re-rulings, and the adjudicated sweep
Step 7 of strategy/01-git-excision.md, the companions. The agent guide bumps to v11: the Git section teaches repo-resident etiquette alone (stage only your own paths, commit your own changes, leave app-maintained files to the app) — existing boards heal to the new text on next open. README re-anchors: the four git feature bullets out, tiers say the complete Mac experience is free, and one bullet states the format's git-friendliness promise. The changelog drops the never-shipped git entries. DESIGN re-rules: 06 retired with Undo routing migrated to 13 (now the sole substrate's doc, seam kept open), 07 retired as written pending the ops-service workstream, 14 retired as superseded record, 12 carries the second pivot note, the index reflects all of it; the charter gets a pointer note (the anchors' full re-ruling stays with the user). InertGitTests renames to GitAgnosticStorageTests — the excision restores its original claim app-wide. And the sweep: ~70 comment sites across 36 files adjudicated against the keeper list, every present-tense description of the excised machinery made past tense or repointed, keepers untouched. 2,707 tests green. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -13,8 +13,9 @@ import Foundation
|
||||
/// "Duplicate is a full fork, `.trash/` included — dropping it would leave the copy's working tree
|
||||
/// disagreeing with its own copied HEAD". This file does nothing to achieve that: the trash is an
|
||||
/// ordinary folder under the root, so the tree walk carries it by declining to be clever.
|
||||
/// - **`.git` comes along** — a duplicate of a git board is a fork of its history — with only its
|
||||
/// remote configuration stripped, which is m7's.
|
||||
/// - **`.git` comes along** — a duplicate of a board that has one is a fork of its history —
|
||||
/// verbatim, remote configuration included; a stripped-remote protection was once slated for this
|
||||
/// (m7-git), never built before the git excision (`strategy/01-git-excision.md`, 2026-08-08).
|
||||
/// - Timestamps, unknown keys, strays, `CLAUDE.user.md`, attachments: verbatim, for the same reason.
|
||||
/// **Nothing here reads a board file at all.**
|
||||
///
|
||||
@@ -245,11 +246,13 @@ enum BoardDuplicator {
|
||||
}
|
||||
|
||||
BoardTreeCopy.restoreAttributes(from: rootAttributes, onto: destination)
|
||||
// m7-git: strip the copy's remote configuration — "the duplicate keeps `.git` but has its
|
||||
// remote configuration stripped ... it must not silently push into the original's remote"
|
||||
// (03-board-ui.md). Remotes only: the repo-local `user.name`/`user.email` survives, so the
|
||||
// fork keeps its commit identity (06-history-undo.md's identity home). Push-on-commit needs
|
||||
// nothing here — it lives on the registry record, and the copy's record is born fresh.
|
||||
// Once slated (m7-git) to strip the copy's remote configuration here — "the duplicate keeps
|
||||
// `.git` but has its remote configuration stripped ... it must not silently push into the
|
||||
// original's remote" (03-board-ui.md) — before app-managed git was excised entirely
|
||||
// (`strategy/01-git-excision.md`, 2026-08-08) with the milestone unbuilt. `.git`, remote
|
||||
// configuration included, now rides along exactly as verbatim as every other file this walk
|
||||
// carries; a duplicate of a board sitting in a user's own repo is that user's to manage, same
|
||||
// as the original was.
|
||||
return destination
|
||||
}
|
||||
|
||||
|
||||
@@ -107,9 +107,10 @@ struct BoardWindowHost: View {
|
||||
/// following walk builds (`BoardRepairRun`, `EchoLedger.adopt`).
|
||||
///
|
||||
/// It cannot live anywhere else: the repairs run before a store exists and the receipts have to
|
||||
/// reach that store's ledger before `beginSession` composes a git board's committer, or the app's
|
||||
/// own repair commits as `Lanework External`. (Pro's committer, until 12-editions.md ▸ PIVOT
|
||||
/// 2026-08-07 made it every git board's.) Cleared once adopted.
|
||||
/// reach that store's ledger before the store's own landing reload classifies them — arrive late
|
||||
/// and the repair's own writes would misattribute as foreign, and the announcer would speak about
|
||||
/// them. (Once fed a git board's auto-committer instead; app-managed git is gone now,
|
||||
/// `strategy/01-git-excision.md`, 2026-08-08.) Cleared once adopted.
|
||||
@State private var repairLedger: EchoLedger?
|
||||
|
||||
@State private var phase: Phase = .opening
|
||||
@@ -225,8 +226,10 @@ struct BoardWindowHost: View {
|
||||
}
|
||||
// (**The board settings sheet was presented here** between 2026-07-31 and 2026-08-07,
|
||||
// when the popover/sheet split was reversed: the sheet retired, its contents rehomed
|
||||
// into the popover's Git tab, and this window has no modal configuration surface at all
|
||||
// now — 03-board-ui.md ▸ Board settings sheet, marked retired.)
|
||||
// into the popover's Git tab — itself retired 2026-08-08 with the git excision
|
||||
// (`strategy/01-git-excision.md`), the git-specific contents gone with it — and this
|
||||
// window still has no modal configuration surface at all, now for either reason —
|
||||
// 03-board-ui.md ▸ Board settings sheet, marked retired.)
|
||||
//
|
||||
// "The board in front", for the menu items that act on it (`LaneWidthCommands`), and
|
||||
// beside it the window's own popover flag, which is what File ▸ Board Info toggles, its
|
||||
@@ -382,13 +385,13 @@ struct BoardWindowHost: View {
|
||||
|
||||
loading.end()
|
||||
|
||||
// **The repair's receipts, into the board's own ledger — before the session composes**
|
||||
// (01: "On Pro boards the repairs drop heal-marked receipts and commit separately as one
|
||||
// repair commit" — every git board's since 12-editions.md ▸ PIVOT 2026-08-07, the sentence
|
||||
// otherwise unchanged). `beginSession` is where that committer is built and started, and the
|
||||
// committer harvests the ledger it is handed; receipts adopted after that line would be
|
||||
// receipts the repair commit never sees, and the app's own repair would be authored
|
||||
// `Lanework External`.
|
||||
// **The repair's receipts, into the board's own ledger — before the session's landing reload
|
||||
// classifies them.** (01-storage-format.md read, of a now-retired repair-commit path: "On Pro
|
||||
// boards the repairs drop heal-marked receipts and commit separately as one repair commit" —
|
||||
// every git board's, since 12-editions.md ▸ PIVOT 2026-08-07. App-managed git left the app
|
||||
// entirely on 2026-08-08, `strategy/01-git-excision.md`; there is no repair commit now.)
|
||||
// Receipts adopted after this line would be receipts the landing reload never classifies, and
|
||||
// the repair's own writes would misattribute as foreign.
|
||||
if let repairLedger {
|
||||
store.echoes.adopt(repairLedger)
|
||||
self.repairLedger = nil
|
||||
|
||||
@@ -195,12 +195,13 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
func endSession() async {
|
||||
guard !hasEnded else { return }
|
||||
hasEnded = true
|
||||
// pro-m1: this is the boundary the auto-committer coalesces on — one commit per Edit
|
||||
// session, "never per save tick" (06-history-undo.md ▸ Rules ▸ Auto-commit). The debounced
|
||||
// saves inside the session are ordinary bracketed writes; what makes them one commit is that
|
||||
// the committer's own debounce outlives them and this call is where the session is known to
|
||||
// be over. It is also where the body's *last* fine step joins this window's stack, which is
|
||||
// why it has to precede the fold below.
|
||||
// Once the boundary the auto-committer coalesced on too — "one commit per Edit session,
|
||||
// never per save tick" (06-history-undo.md ▸ Rules ▸ Auto-commit, retired with app-managed
|
||||
// git, `strategy/01-git-excision.md`, 2026-08-08). What is left is native undo's own version
|
||||
// of the same idea: the debounced saves inside the session are ordinary bracketed writes, and
|
||||
// what makes them one step is that `endEditSession` registers exactly one (13-native-undo.md
|
||||
// ▸ Rules) once the session is known to be over. It is also where the body's *last* fine step
|
||||
// joins this window's stack, which is why it has to precede the fold below.
|
||||
body.endEditSession()
|
||||
// **The saves, in the order the comments build fixed**: the inline session's flush, then the
|
||||
// draft's (`CardComments.endSession`). Both may register their own last fine step, so both
|
||||
@@ -212,10 +213,10 @@ final class CardWindowSession: CardSessionFlushing {
|
||||
// This is the one place that knows both halves: the window's stack, which is the session's
|
||||
// net effect, and the `comments/.trash/` purge, which must not run while a board step's undo
|
||||
// still restores comments out of it. Registering answers whether the step took the purge on —
|
||||
// and a board whose substrate keeps no steps has already run it by the time that answer comes
|
||||
// back, which is how a git board keeps purging at the close flush without a word about tiers
|
||||
// here (and there is none to say since 12-editions.md ▸ PIVOT 2026-08-07 — the substrate is
|
||||
// the board's mode, not a subscription).
|
||||
// and a board whose substrate declines to keep a step (`registerCardSession`'s three `false`
|
||||
// cases above) has already run the purge by the time that answer comes back. The seam stays
|
||||
// nil-safe on purpose for whatever substrate registers next (`strategy/01-git-excision.md` ▸
|
||||
// Reversibility); today the native stack is the only one that ever says yes.
|
||||
//
|
||||
// **This window stops owning its card's comment trash first** (ruled 2026-08-06 — the
|
||||
// open-window carve-out). While the window is open the store defers every purge of this
|
||||
|
||||
@@ -84,9 +84,10 @@ public extension CardSessionFlushing {
|
||||
/// call site and supplies the real ones.
|
||||
///
|
||||
/// The two flush seams that are `nil` today — `editorFlush` and `committerFlush` — are named rather
|
||||
/// than left to be discovered: 02 fixes their relative order ("editor saves before the pending
|
||||
/// auto-commit"), and the milestone that adds a debounced editor save should have nowhere to put it
|
||||
/// except the slot that already sits in the right place.
|
||||
/// than left to be discovered: 02 fixes their relative order (editor saves before any board-level
|
||||
/// flush), and a future consumer should have nowhere to land except the slot that already sits in
|
||||
/// the right place. `committerFlush`'s original consumer was the auto-committer, excised 2026-08-08
|
||||
/// (strategy/01-git-excision.md); the slot outlives it for whatever durable-history flush comes next.
|
||||
@MainActor
|
||||
public struct CloseFlushCoordinator {
|
||||
|
||||
@@ -121,12 +122,13 @@ public struct CloseFlushCoordinator {
|
||||
public var storeFlush: () async -> Void
|
||||
|
||||
/// The card windows' debounced body saves (05-card-window.md, m6). Runs **before**
|
||||
/// `committerFlush`: 02 is explicit that editor saves land before the pending auto-commit, so a
|
||||
/// session's last keystrokes are inside the commit that closes it rather than orphaned in the
|
||||
/// next one.
|
||||
/// `committerFlush`: 02 is explicit that editor saves land before any board-level flush, so a
|
||||
/// session's last keystrokes ride whatever board-level flush follows rather than being orphaned
|
||||
/// behind it.
|
||||
public var editorFlush: (() async -> Void)?
|
||||
|
||||
/// The pending debounced auto-commit (06-history-undo.md, m7).
|
||||
/// The board-level flush that ran last — the excised auto-committer's slot, permanently `nil`
|
||||
/// since 2026-08-08 (strategy/01-git-excision.md); see the header for why the slot survives.
|
||||
public var committerFlush: (() async -> Void)?
|
||||
|
||||
// MARK: Step 3 — the record
|
||||
|
||||
@@ -112,8 +112,9 @@ public struct SettleableSession {
|
||||
|
||||
// MARK: - SessionSettleGate
|
||||
|
||||
/// **The save-or-discard step**, as one reusable decision procedure (06-history-undo.md ▸ Rules
|
||||
/// ▸ Undo restore vs open Edit sessions; ▸ Branch switching).
|
||||
/// **The save-or-discard step**, as one reusable decision procedure — 06-history-undo.md ▸ Rules
|
||||
/// ▸ Undo restore vs open Edit sessions; ▸ Branch switching, retired with the callers that read it
|
||||
/// (`strategy/01-git-excision.md`, 2026-08-08).
|
||||
///
|
||||
/// ### One machinery, every caller, by design
|
||||
///
|
||||
|
||||
@@ -25,10 +25,11 @@ import os
|
||||
/// rule this file invents:
|
||||
///
|
||||
/// - **The two exclusions are top-level only** (`BoardTreeCopy`): `.git` and `.trash/` mean
|
||||
/// something at a board root and nowhere else. `.git` is skipped so an instantiated board is never
|
||||
/// silently in git mode (06-history-undo.md's no-silent-auto-init); its actual mode follows 06's
|
||||
/// nearest-`.git`-wins detection at the destination the save panel chose. `.trash/` is skipped
|
||||
/// because "a new board isn't born with trash".
|
||||
/// something at a board root and nowhere else. `.git` is skipped so a template's own repository
|
||||
/// never rides along into every board it stamps out — once phrased as keeping an instantiated
|
||||
/// board from silently landing in git mode (06-history-undo.md's no-silent-auto-init, retired with
|
||||
/// app-managed git, `strategy/01-git-excision.md`); there is no mode left to land in today.
|
||||
/// `.trash/` is skipped because "a new board isn't born with trash".
|
||||
/// - **Everything else copies verbatim** — strays, `CLAUDE.user.md`, a seeded `.gitignore`,
|
||||
/// attachments, card bodies, unknown keys, line endings (09 ▸ Save as Template, "Strays copy
|
||||
/// through … *and* instantiation alike"), and **symlinks as symlinks**, never traversed
|
||||
|
||||
@@ -15,7 +15,8 @@ import os
|
||||
/// touched and what they said before. It also keeps the one rule every step must obey in a single
|
||||
/// place — **an inverse is performed as an ordinary app-mediated write**, through `performWrite`,
|
||||
/// so an undo brackets the watcher, echoes back through the reload like any other change, refreshes
|
||||
/// every window on the board, and (on git boards, pro-m1) commits. Undone changes are real writes,
|
||||
/// every window on the board — once it also committed on git boards, pro-m1, before app-managed
|
||||
/// git was excised (`strategy/01-git-excision.md`, 2026-08-08). Undone changes are real writes,
|
||||
/// never in-memory reverts.
|
||||
///
|
||||
/// ### Values, never live references
|
||||
|
||||
@@ -13,13 +13,15 @@ import AppKit
|
||||
/// exactly how the system's Edit ▸ Undo row and the toolbar's nil-target pair (`BoardToolbar`) light
|
||||
/// up, disable and retitle with no code of the app's own.
|
||||
///
|
||||
/// The seam, though, must not be an `NSUndoManager`: a gitless board's stack is one, a Pro git
|
||||
/// board's is git (12-editions.md ▸ The provider seam), and a protocol that vended one could only
|
||||
/// ever have had a single implementation. So the substrate stays behind `HistoryProviding` and
|
||||
/// The seam, though, must not be an `NSUndoManager`: a protocol that vended one could only ever have
|
||||
/// had a single implementation, and the substrate stays behind `HistoryProviding` so a future
|
||||
/// provider can bind without touching this class (`strategy/01-git-excision.md` ▸ Reversibility). So
|
||||
/// *this* object is the translation — one per board session, over whichever provider that session
|
||||
/// was composed with. A git board inherits the whole command surface (enablement, dynamic titles,
|
||||
/// ⌘Z, the toolbar pair) by binding its provider and changing nothing here, which is what "a user
|
||||
/// subscribing (or lapsing) relearns nothing" (12) has to mean in code.
|
||||
/// was composed with. (Once two providers shared the seam this way — a gitless board's native stack,
|
||||
/// a Pro git board's git history, 12-editions.md ▸ The provider seam — with whichever bound
|
||||
/// inheriting the whole command surface, enablement, dynamic titles, ⌘Z, the toolbar pair, by
|
||||
/// binding and changing nothing here. App-managed git is gone now, 2026-08-08, and native is the
|
||||
/// only one left.)
|
||||
///
|
||||
/// **And one per open card window**, over that window's own stack (13 ▸ Rules ▸ two levels, re-ruled
|
||||
/// 2026-07-31 — `CardWindowUndo.manager`): the second level needs precisely the same translation, so
|
||||
@@ -52,8 +54,8 @@ import AppKit
|
||||
/// responder chain — validates through this object, so answering `false` here disables all of them
|
||||
/// at once, exactly as the lock's other victims disable through menu validation (02-architecture.md
|
||||
/// § "The lock's scope"). Putting it in the *provider* would have been the same answer in the wrong
|
||||
/// place: the stack is not the thing that is locked, the board is, and a Pro session binding the git
|
||||
/// provider must inherit the rule without reimplementing it.
|
||||
/// place: the stack is not the thing that is locked, the board is, and whatever provider a session
|
||||
/// binds — a future one included — must inherit the rule without reimplementing it.
|
||||
public final class BoardUndoManager: UndoManager {
|
||||
|
||||
/// The substrate this manager is a face for. Strong: the session owns both, and the manager is
|
||||
@@ -65,7 +67,10 @@ public final class BoardUndoManager: UndoManager {
|
||||
/// only under locks and on empty stacks — the provider follows the board, so boards without
|
||||
/// app-managed git — repo-nested included — bind 13-native-undo.md's native stack in **every**
|
||||
/// tier" (03-board-ui.md ▸ Toolbar ▸ Catalog). The repo-nested board was the last holder of this
|
||||
/// state and no longer is: that rule was about *git*, and this stack never touches git.
|
||||
/// state and no longer is: that rule was about *git*, and this stack never touches git. (The
|
||||
/// distinction the quote draws collapsed entirely with the 2026-08-08 excision,
|
||||
/// `strategy/01-git-excision.md`: every board is "without app-managed git" now, not just
|
||||
/// repo-nested ones.)
|
||||
///
|
||||
/// The state stays modelled because the seam still admits it — a test binds a substrate-less
|
||||
/// session through `AppModel.makeHistoryProvider` — and because an absent substrate is the honest
|
||||
@@ -73,20 +78,19 @@ public final class BoardUndoManager: UndoManager {
|
||||
/// toolbar pair, and ⌘Z itself go quiet together through the same validation path a lock uses,
|
||||
/// and nothing can accidentally accumulate in a stack that is not there.
|
||||
///
|
||||
/// ### Settable, for exactly one event
|
||||
/// ### Settable, once for exactly one event — now for none
|
||||
///
|
||||
/// **Add-git** (06 ▸ Rules ▸ Detection) is the design's one sanctioned mid-session mode flip:
|
||||
/// "clicking it flips the open board into git mode immediately". 13's header says what that means
|
||||
/// here — the flip **swaps the substrate**: the mode-none board's native stack is discarded and
|
||||
/// the git trail seeded from the root commit, the branch-switch discard-and-reseed precedent. The
|
||||
/// composition root writes the new provider into this property rather than rebuilding this
|
||||
/// object, so AppKit keeps the identical manager it has already been handed by
|
||||
/// `windowWillReturnUndoManager` and simply revalidates over a different stack.
|
||||
/// **Add-git** (06-history-undo.md ▸ Rules ▸ Detection, retired) was the design's one sanctioned
|
||||
/// mid-session mode flip: "clicking it flips the open board into git mode immediately", swapping
|
||||
/// the substrate — the mode-none board's native stack discarded for the git trail seeded from the
|
||||
/// root commit. The composition root wrote the new provider into this property rather than
|
||||
/// rebuilding this object, so AppKit kept the identical manager it had already been handed by
|
||||
/// `windowWillReturnUndoManager` and simply revalidated over a different stack.
|
||||
///
|
||||
/// (This is *not* a tier flip. 12-editions.md's "an open board finishes with the provider it
|
||||
/// composed" is about a subscription lapsing, which cannot change a running session's tier at
|
||||
/// all — `BoardSession.tier` is a `let` with no setter. Mode can change, by explicit command,
|
||||
/// and only in this one direction.)
|
||||
/// App-managed git left the app entirely on 2026-08-08 (`strategy/01-git-excision.md`), and
|
||||
/// add-git went with it: nothing left in the app ever assigns this property after composition. It
|
||||
/// stays a `var` rather than a `let` for the same reason `HistoryProviding` stays a protocol — a
|
||||
/// future substrate swap, mid-session or not, re-binds without re-plumbing this class.
|
||||
var history: (any HistoryProviding)?
|
||||
|
||||
/// Whether the board is refusing writes — `BoardStore.isReadOnly`, read through a closure rather
|
||||
|
||||
@@ -15,10 +15,10 @@ import Foundation
|
||||
///
|
||||
/// **The fine stack**: an ordinary `NativeHistoryProvider`, on *every* board. The steps a card window
|
||||
/// registers are values-based inverses at the Writer boundary — the same shape whatever substrate the
|
||||
/// board's own history has — so a git board's card window still walks its own gestures with the
|
||||
/// native grammar, and only the *coarse* close unit splits by substrate ("one native board step, or
|
||||
/// one commit" — 06-history-undo.md ▸ Undo routing). The split was written as a tier split and is a
|
||||
/// board split since 12-editions.md ▸ PIVOT 2026-08-07; nothing in this type ever read either.
|
||||
/// board's own history has. (Once only the *coarse* close unit split by substrate — "one native board
|
||||
/// step, or one commit" — 13-native-undo.md ▸ Undo routing; the split was written as a tier split and
|
||||
/// became a board split with the 2026-08-07 pivot before the 2026-08-08 excision removed the git half
|
||||
/// of it outright, `strategy/01-git-excision.md`. Nothing in this type ever read either axis.)
|
||||
///
|
||||
/// **The fold**: window close registers "one coarse step ... whose undo restores the card subtree to
|
||||
/// its session-start state ... and whose redo reapplies the net effect". That net effect is exactly
|
||||
|
||||
@@ -80,9 +80,10 @@ public enum HistoryStepOutcome: Equatable, Sendable {
|
||||
/// holds: move → move back ...; rename → restore title" — and both halves of that write are already
|
||||
/// in the caller's hands: the before-value *is* the inverse, and the after-value is what the write
|
||||
/// set (which is also what the staleness predicate compares). A step is therefore that pair, in the
|
||||
/// design's own terms, and deliberately says nothing about how a stack stores it: a gitless board's
|
||||
/// stack is `NSUndoManager`-backed and a Pro git board's is git (12-editions.md ▸ The provider
|
||||
/// seam), and neither substrate appears here.
|
||||
/// design's own terms, and deliberately says nothing about how a stack stores it: the board's stack
|
||||
/// is `NSUndoManager`-backed, and no substrate appears here. (Once two substrates shared this seam —
|
||||
/// a gitless board's native stack, a Pro git board's git history, 12-editions.md ▸ The provider
|
||||
/// seam — before app-managed git left the app entirely, 2026-08-08, `strategy/01-git-excision.md`.)
|
||||
///
|
||||
/// ### `name` is the 06 vocabulary, unprefixed
|
||||
///
|
||||
@@ -262,27 +263,30 @@ public struct HistoryStep {
|
||||
/// seam from a window is the one coarse step its close registers.
|
||||
///
|
||||
/// `AppModel.BoardSession` is where the board half's ownership lives, and the composition root binds
|
||||
/// which implementation it gets — **following the board, and since the 2026-08-07 pivot the board
|
||||
/// alone** (re-ruled 2026-07-31; 12-editions.md ▸ PIVOT 2026-08-07): a board without app-managed git
|
||||
/// — repo-nested included (re-ruled 2026-07-31) — binds `NativeHistoryProvider` (two step stacks over
|
||||
/// the inverses registered at the Writer boundary), and a git-mode board binds the git provider (undo
|
||||
/// as forward restore commits over HEAD's first-parent ancestry — 06-history-undo.md). Both answers
|
||||
/// are the same in every tier; the tier used to decide whether git was on the table at all, and no
|
||||
/// longer does.
|
||||
/// which implementation it gets — **always `NativeHistoryProvider`** now (two step stacks over the
|
||||
/// inverses registered at the Writer boundary), on every board, in every tier
|
||||
/// (`AppModel.makeHistoryProvider`). A git-mode board once bound a git provider instead — undo as
|
||||
/// forward restore commits over HEAD's first-parent ancestry, 06-history-undo.md — between the
|
||||
/// 2026-08-07 pivot (12-editions.md ▸ PIVOT 2026-08-07, when the tier stopped deciding whether git
|
||||
/// was on the table at all) and the 2026-08-08 excision that removed the git provider outright
|
||||
/// (`strategy/01-git-excision.md`).
|
||||
///
|
||||
/// ### What this protocol deliberately does not say
|
||||
///
|
||||
/// - **No `NSUndoManager`, anywhere in the signature.** It is the native provider's implementation
|
||||
/// detail, and a seam that vended one would be a seam only one provider could ever satisfy — the
|
||||
/// opposite of the reason the seam exists at all ("the free tier's native undo is the first proof
|
||||
/// the seam is real", 12 — written when native undo was the free tier's; the proof it names is
|
||||
/// two working substrates, which the pivot leaves standing). AppKit still needs an `UndoManager`
|
||||
/// to hand the responder chain; that adapter is `BoardUndoManager`, which sits *over* this
|
||||
/// protocol rather than inside it.
|
||||
/// - **No persistence promise.** The native stack dies with the session (13); the git provider's
|
||||
/// survives relaunch because git does (06). Both are honest implementations of these seven members.
|
||||
/// the seam is real", 12 — written when native undo was the free tier's; the proof it named was
|
||||
/// two working substrates, which the pivot left standing and the 2026-08-08 excision then narrowed
|
||||
/// back to one, `strategy/01-git-excision.md` — the seam itself is what stays proved either way).
|
||||
/// AppKit still needs an `UndoManager` to hand the responder chain; that adapter is
|
||||
/// `BoardUndoManager`, which sits *over* this protocol rather than inside it.
|
||||
/// - **No persistence promise.** The native stack dies with the session (13); a git provider's once
|
||||
/// survived relaunch because git does, before app-managed git was excised entirely (2026-08-08,
|
||||
/// `strategy/01-git-excision.md`). Both were honest implementations of these seven members; only
|
||||
/// native's promise is live to keep now.
|
||||
/// - **No routing.** Which surface ⌘Z reaches is focus's answer, not the substrate's
|
||||
/// (06 ▸ Undo routing, tier-independent) — `BoardUndoRouting`.
|
||||
/// (13-native-undo.md ▸ Undo routing, tier-independent) — `BoardUndoRouting`.
|
||||
///
|
||||
/// ### One obligation every implementation shares: retire what you let go of
|
||||
///
|
||||
@@ -298,7 +302,8 @@ public struct HistoryStep {
|
||||
/// nonetheless stands to the session's end, because "the skip banner says nothing was applied, and an
|
||||
/// irreversible purge riding that gesture would be surprise loss". A substrate that pops stale steps
|
||||
/// therefore has a place to put them (`NativeHistoryProvider.strandedSteps`); one that never keeps a
|
||||
/// step is untouched by the distinction, which is why the git provider needed no change.
|
||||
/// step is untouched by the distinction — which is why the git provider, while it existed, needed no
|
||||
/// change for this rule at all.
|
||||
///
|
||||
/// ### And its mirror: say what you are still holding
|
||||
///
|
||||
|
||||
@@ -2,10 +2,10 @@ import Foundation
|
||||
|
||||
// MARK: - NativeHistoryProvider
|
||||
|
||||
/// The undo substrate of every board without app-managed git: one stack per board session
|
||||
/// (13-native-undo.md, whose header lost its tier axis with the 2026-08-07 pivot — the substrate is
|
||||
/// the board's mode alone, and git is opt-in per board, so a board nobody opted in keeps this stack
|
||||
/// for good) — **and the substrate of every open card window's stack, on any board** (re-ruled 2026-07-31, the
|
||||
/// The undo substrate of every board: one stack per board session (13-native-undo.md, whose header
|
||||
/// lost its tier axis with the 2026-08-07 pivot and then its mode axis with the 2026-08-08 git
|
||||
/// excision, `strategy/01-git-excision.md` — there is no other substrate left to be the board's
|
||||
/// alternative to) — **and the substrate of every open card window's stack, on any board** (re-ruled 2026-07-31, the
|
||||
/// session-coarsening model): a window's fine-grained gestures are values-based inverses whatever the
|
||||
/// board's own substrate is, so `CardWindowUndo` holds one of these too. Nothing below knows which of
|
||||
/// the two it is; both need the same four-line grammar.
|
||||
@@ -147,7 +147,8 @@ public final class NativeHistoryProvider: HistoryProviding {
|
||||
|
||||
public func redo() { cross(.redo) }
|
||||
|
||||
/// Session teardown, the add-git substrate swap, a branch reseed — every step goes, so every step
|
||||
/// Session teardown — once the add-git substrate swap and a branch reseed too, both gone with
|
||||
/// app-managed git (`strategy/01-git-excision.md`, 2026-08-08) — every step goes, so every step
|
||||
/// retires: "the purge runs when the coarse step leaves the board stack ... or the board session
|
||||
/// ends" (13 ▸ Interaction with the trash).
|
||||
///
|
||||
|
||||
@@ -12,20 +12,22 @@ import Foundation
|
||||
/// is explicit about why that is the right shape: "a value-type snapshot comparison is both cheaper
|
||||
/// and more trustworthy than trying to reconstruct a tree mutation from a stream of paths that may
|
||||
/// have been coalesced, dropped, or reordered". So this is a comparison of two `BoardModel` values
|
||||
/// and nothing else — no FSEvents paths, no receipts, no git. That is also what makes it work on a
|
||||
/// no-git board, which 10 requires in as many words ("on no-git boards the same classifier runs
|
||||
/// without the committer").
|
||||
/// and nothing else — no FSEvents paths, no receipts, no git. That is also what makes it work
|
||||
/// regardless of git: app-managed git is gone now (`strategy/01-git-excision.md`, 2026-08-08), and 10
|
||||
/// once required this in as many words for the no-git case specifically ("on no-git boards the same
|
||||
/// classifier runs without the committer") — today there is no other case to distinguish it from.
|
||||
///
|
||||
/// ### Why it lives in `LiveStore/` and not beside the announcement
|
||||
///
|
||||
/// 10 says announcements "reuse the auto-committer's summarizer" and 06-history-undo.md says the
|
||||
/// committer synthesizes its commit messages from the same comparison. The committer is Pro-tier
|
||||
/// work (pro-m1) and does not exist yet, so the summarizer is built here first, in the layer both
|
||||
/// consumers can reach: the store layer is below the history providers, so it is available to the
|
||||
/// committer by construction, where a type living inside the git provider would not be reachable
|
||||
/// from the announcer (12-editions.md ▸ The provider seam). It imports
|
||||
/// 10 once said announcements "reuse the auto-committer's summarizer" and 06-history-undo.md said the
|
||||
/// committer synthesized its commit messages from the same comparison — Pro-tier work (pro-m1) that
|
||||
/// never shipped before app-managed git was excised entirely (`strategy/01-git-excision.md`,
|
||||
/// 2026-08-08). The summarizer was built here first anyway, in the layer any future consumer can
|
||||
/// reach: the store layer sits below the history providers, so it stays available to whatever
|
||||
/// replaces the committer — today the foreign-change journal (`strategy/01-git-excision.md` ▸
|
||||
/// Successors) — by construction. It imports
|
||||
/// `Foundation` alone and touches no view, no window, and no `NSAccessibility` — the phrasing is
|
||||
/// `AccessibilityPhrases`' job, and pro-m1's message composer will phrase the very same counts
|
||||
/// `AccessibilityPhrases`' job, and a future message composer will phrase the very same counts
|
||||
/// differently without either of them knowing about the other.
|
||||
///
|
||||
/// The per-category **identity sets** rather than bare tallies are for that second consumer: a
|
||||
|
||||
@@ -442,9 +442,10 @@ public final class BoardStore: HealHost {
|
||||
/// answer to "which ledger is this board's", and a second one would be a second provenance.
|
||||
///
|
||||
/// Its consumers today are the announcer's digest and the vanishing-focus sentence, both
|
||||
/// through `land`. Pro's auto-committer (06-history-undo.md) becomes the second one without
|
||||
/// this line changing — which is why the type lives in `LiveStore/` beside `BoardDiff` rather
|
||||
/// than inside the git provider.
|
||||
/// through `land`. The foreign-change journal is slated to become the second one without this
|
||||
/// line changing (`strategy/01-git-excision.md` ▸ Successors) — once the auto-committer's job,
|
||||
/// before app-managed git was excised 2026-08-08 — which is why the type lives in `LiveStore/`
|
||||
/// beside `BoardDiff` rather than inside any one consumer's module.
|
||||
@ObservationIgnored
|
||||
public let echoes = EchoLedger()
|
||||
|
||||
@@ -668,9 +669,11 @@ public final class BoardStore: HealHost {
|
||||
/// raised lock above the completion), and the phrase must not survive to be spoken by some
|
||||
/// later, unrelated reload.
|
||||
///
|
||||
/// **`nil` on every free-tier bracket today.** The free tier has no git operations, and the design's
|
||||
/// examples ("Pulled 3 commits", "Switched to branch 'redesign'") are pro-m1's; the parameter
|
||||
/// exists so that milestone supplies phrasing rather than re-plumbing the seam.
|
||||
/// **`nil` on every bracket today.** App-managed git is gone (`strategy/01-git-excision.md`,
|
||||
/// 2026-08-08) and nothing currently supplies a phrase — the design's examples ("Pulled 3
|
||||
/// commits", "Switched to branch 'redesign'") were git's. The parameter stays so a future
|
||||
/// wholesale operation (the ops-based sync service — `strategy/01-git-excision.md` ▸ Successors)
|
||||
/// can supply one without re-plumbing the seam.
|
||||
@ObservationIgnored
|
||||
private var wholesaleCompletion: String?
|
||||
|
||||
@@ -1399,12 +1402,13 @@ public final class BoardStore: HealHost {
|
||||
///
|
||||
/// - Parameter completion: what to announce when the closing reload lands
|
||||
/// (10-accessibility.md ▸ Live board announcements: "bracketed operations announce once, at
|
||||
/// completion" — "Pulled 3 commits", "Switched to branch 'redesign'"). `nil`, the default, is
|
||||
/// an operation whose completion is not worth speech, which is **every free-tier bracket
|
||||
/// today**: no git operations run there, and the two app-initiated writes that do reach disk on
|
||||
/// their own — the loose-file relocation and the legacy-tombstone migration — are ordinary
|
||||
/// `performWrite` calls that already say what they did on the banner strip. The parameter is
|
||||
/// the seam pro-m1 fills; see `wholesaleCompletion`.
|
||||
/// completion" — "Pulled 3 commits", "Switched to branch 'redesign'", both git's). `nil`, the
|
||||
/// default, is an operation whose completion is not worth speech, which is **every bracket
|
||||
/// today**: app-managed git is gone (`strategy/01-git-excision.md`, 2026-08-08), and the two
|
||||
/// app-initiated writes that do reach disk on their own — the loose-file relocation and the
|
||||
/// legacy-tombstone migration — are ordinary `performWrite` calls that already say what they
|
||||
/// did on the banner strip. The parameter is the seam a future wholesale operation fills; see
|
||||
/// `wholesaleCompletion`.
|
||||
///
|
||||
/// - Throws: `BoardStoreWriteRefusal.readOnlyLocked` if the board is already locked — a locked
|
||||
/// board refuses to *start* wholesale work, not just ordinary writes. Otherwise rethrows
|
||||
@@ -1484,8 +1488,7 @@ public final class BoardStore: HealHost {
|
||||
/// Three ways this does nothing, all deliberate: a count below 1 clamps to 1 (a lane spans at
|
||||
/// least one unit), an id that is not in the snapshot is ignored (the lane vanished under the
|
||||
/// gesture — the reload that removed it is the authority), and a count already equal to what the
|
||||
/// lane displays writes nothing (a drag that ends where it started must not stamp `modified` or
|
||||
/// mint a git commit).
|
||||
/// lane displays writes nothing (a drag that ends where it started must not stamp `modified`).
|
||||
///
|
||||
/// Failures are already the banner's: `performWrite` posts every `BoardWriteError` before it
|
||||
/// rethrows, so the rethrow is swallowed here rather than propagated to a gesture that has no
|
||||
@@ -1519,7 +1522,7 @@ public final class BoardStore: HealHost {
|
||||
|
||||
/// The one commit point every width mechanism shares — the edge drag, the context-menu stepper,
|
||||
/// and the menu items' batch. One `performWrite` bracket whatever the count: one gesture, one
|
||||
/// app-mediated reload, one commit on git boards (the style batch's rule).
|
||||
/// app-mediated reload (the style batch's rule).
|
||||
///
|
||||
/// **A width landing on 1 removes the `width` key** (03-board-ui.md § Lane, settled — the
|
||||
/// remove-at-default family beside the empty rename's `title` and the None well's
|
||||
@@ -1674,13 +1677,14 @@ public final class BoardStore: HealHost {
|
||||
/// row with them.
|
||||
///
|
||||
/// **One bracket, whatever the target set's size.** "Choosing a well applies to the whole
|
||||
/// selection — one gesture, one commit on git boards" (§ Controls), so every target's `index.md`
|
||||
/// is rewritten inside a single `performWrite`: the churn rounds back as one app-mediated reload,
|
||||
/// and the auto-committer (m7) sees one operation rather than N.
|
||||
/// selection — one gesture, one write" (§ Controls), so every target's `index.md`
|
||||
/// is rewritten inside a single `performWrite`: the churn rounds back as one app-mediated reload
|
||||
/// — once one operation for the auto-committer too (m7), before app-managed git was excised
|
||||
/// entirely (`strategy/01-git-excision.md`, 2026-08-08).
|
||||
///
|
||||
/// **No-ops are skipped per dimension and per target** — `setLaneWidth`'s rule, for its reason: a
|
||||
/// well clicked twice, or a batch where half the cards are already that colour, must not stamp
|
||||
/// `modified` or mint a commit on the items that were already right. A dimension whose value is
|
||||
/// `modified` on the items that were already right. A dimension whose value is
|
||||
/// already what the gesture asks contributes nothing; a target both of whose dimensions are
|
||||
/// no-ops is dropped entirely; and a gesture that changes nothing anywhere never opens the
|
||||
/// bracket at all.
|
||||
@@ -1883,8 +1887,8 @@ public final class BoardStore: HealHost {
|
||||
/// The image lands first and the frontmatter second, so a failure to write the picture never
|
||||
/// leaves the board naming one that is not there. The reverse order would; the two are not atomic
|
||||
/// together, and this is the ordering that makes the non-atomic half harmless. Both are inside the
|
||||
/// same bracket, so the churn rounds back as one app-mediated reload and mints one commit on git
|
||||
/// boards — the style batch's rule, one gesture one commit.
|
||||
/// same bracket, so the churn rounds back as one app-mediated reload — the style batch's rule,
|
||||
/// one gesture one write.
|
||||
///
|
||||
/// ### The name is chosen, not minted
|
||||
///
|
||||
@@ -2218,7 +2222,7 @@ public final class BoardStore: HealHost {
|
||||
/// "Committing an empty rename on an existing item removes its `title` key"), rather than
|
||||
/// writing `title: ""` — titles are optional, and the face shows the untitled placeholder.
|
||||
/// - **An unchanged title writes nothing.** `setLaneWidth`'s rule, for the same reason: an
|
||||
/// editor opened and dismissed with Return must not stamp `modified` or mint a commit.
|
||||
/// editor opened and dismissed with Return must not stamp `modified`.
|
||||
///
|
||||
/// The folder is re-derived from the *current* snapshot, which is what makes a foreign move
|
||||
/// mid-rename invisible: the editor follows the UUID, and the write lands wherever the item is
|
||||
@@ -2319,8 +2323,9 @@ public final class BoardStore: HealHost {
|
||||
/// Ticks or unticks a Preview task-list checkbox — **the app's one write into a card's body**
|
||||
/// (05-card-window.md ▸ Preview), and otherwise an entirely ordinary one: the same
|
||||
/// `performWrite` bracket, the same banner on failure, the same one-way flow back through the
|
||||
/// watcher. "A toggle is an ordinary user edit — the standard atomic write, auto-committed and
|
||||
/// undoable on git boards."
|
||||
/// watcher. "A toggle is an ordinary user edit — the standard atomic write." (Once
|
||||
/// "auto-committed and undoable on git boards" too; app-managed git is gone now,
|
||||
/// `strategy/01-git-excision.md`.)
|
||||
///
|
||||
/// `bodyOffset` is the UTF-8 byte offset the parse handed the renderer (`BodyTask
|
||||
/// .markerOffset`) and `checked` is the state the user was looking at; both travel to
|
||||
@@ -2330,9 +2335,10 @@ public final class BoardStore: HealHost {
|
||||
///
|
||||
/// **It registers no undo step.** 13-native-undo.md ▸ Rules' inventory names the body write it
|
||||
/// makes undoable precisely — "Edit-session body save → restore prior body bytes" — and a Preview
|
||||
/// checkbox is not one: it belongs to no session, has no flip to coalesce at, and 05 files it
|
||||
/// under what is "undoable on git boards", which is the *other* substrate's answer. Registering it
|
||||
/// here would be extending 13's inventory rather than implementing it.
|
||||
/// checkbox is not one: it belongs to no session and has no flip to coalesce at. (05 once filed it
|
||||
/// under "undoable on git boards" instead, the other substrate's answer; app-managed git is gone
|
||||
/// now, `strategy/01-git-excision.md`, so a checkbox toggle is simply not undoable today.)
|
||||
/// Registering it here would be extending 13's inventory rather than implementing it.
|
||||
///
|
||||
/// **A checkbox in a card that has gone writes nothing** — the vanished-target guard every
|
||||
/// gesture in this file makes, ancestor-walked through `boardItem`: the card window would be
|
||||
@@ -2356,8 +2362,9 @@ public final class BoardStore: HealHost {
|
||||
/// flush that closes the window (05-card-window.md ▸ Edit).
|
||||
///
|
||||
/// An ordinary store write in every mechanical respect: one `performWrite` bracket, so the churn
|
||||
/// rounds back as a single app-mediated reload (and, on git boards, sits inside the session's
|
||||
/// one commit — see `CardBodyEditSession` for that seam); the banner posts itself on failure;
|
||||
/// rounds back as a single app-mediated reload (once it also sat inside the session's one commit
|
||||
/// on git boards — see `CardBodyEditSession` for that seam, native-only now since the excision,
|
||||
/// `strategy/01-git-excision.md`); the banner posts itself on failure;
|
||||
/// the snapshot is never touched here, because the watcher's reload is what brings the text
|
||||
/// back.
|
||||
///
|
||||
@@ -2605,7 +2612,7 @@ public final class BoardStore: HealHost {
|
||||
/// name* (§ Board naming) — never the "Untitled" placeholder cards and lanes show, and never
|
||||
/// `title: ""`, which would be a real if blank title with nothing to fall back to.
|
||||
/// - **An unchanged title writes nothing**, so a popover opened and dismissed with Return
|
||||
/// neither stamps `modified` nor mints a commit.
|
||||
/// does not stamp `modified`.
|
||||
///
|
||||
/// There is no vanished-target guard, because a board cannot tombstone itself out of its own
|
||||
/// window (01-storage-format.md § Deletion): the only way this target goes away is the root
|
||||
@@ -2652,8 +2659,8 @@ public final class BoardStore: HealHost {
|
||||
/// exactly one file — the moved lane's `order`.
|
||||
///
|
||||
/// **A drag that ends where it started writes nothing**: `index == from` re-inserts the lane in
|
||||
/// its own slot, and a no-op must not stamp `modified` or mint a commit — the resize drag's
|
||||
/// rule, and for the same reason.
|
||||
/// its own slot, and a no-op must not stamp `modified` — the resize drag's rule, and for the
|
||||
/// same reason.
|
||||
public func moveLane(_ id: ItemID, toIndex index: Int) {
|
||||
let lanes = snapshot.lanes
|
||||
guard let from = lanes.firstIndex(where: { $0.id == id }) else { return }
|
||||
@@ -2730,14 +2737,13 @@ public final class BoardStore: HealHost {
|
||||
///
|
||||
/// `moveLane`'s plural, and it exists rather than a loop over it because "one `performWrite`
|
||||
/// bracket per gesture whatever the set's size" is load-bearing (DRAG-REORDER.md § The drop
|
||||
/// commits): one app-mediated reload, and on git boards one commit rather than N.
|
||||
/// commits): one app-mediated reload rather than N.
|
||||
///
|
||||
/// The run keeps **board order**, which is the lane level's flatten order — a multi-lane drag has
|
||||
/// no other relative order to preserve.
|
||||
///
|
||||
/// A drag that changes nothing writes nothing, stated as the arrangement rather than as a special
|
||||
/// case: if the strip would render exactly what it renders now, no rank is rewritten and no
|
||||
/// commit is minted.
|
||||
/// case: if the strip would render exactly what it renders now, no rank is rewritten.
|
||||
public func moveLanes(_ ids: Set<ItemID>, toIndex index: Int) {
|
||||
let lanes = snapshot.lanes
|
||||
let members = lanes.filter { ids.contains($0.id) }
|
||||
@@ -2910,8 +2916,7 @@ public final class BoardStore: HealHost {
|
||||
// produces their `index` is DRAG-REORDER.md's, implemented in `DropSlotMath`).
|
||||
//
|
||||
// **One `performWrite` bracket per gesture**, whatever the set's size — the style batch's and
|
||||
// the tombstone batch's rule, for their reason: one gesture, one app-mediated reload, one commit
|
||||
// on git boards.
|
||||
// the tombstone batch's rule, for their reason: one gesture, one app-mediated reload.
|
||||
//
|
||||
// **`index` always means the same thing**: a position among the destination's *rendered* items
|
||||
// counted with the dragged run already removed — the resting layout's own convention, so the
|
||||
@@ -2920,7 +2925,7 @@ public final class BoardStore: HealHost {
|
||||
// must not trap.
|
||||
//
|
||||
// **Ranks are inserted, never permuted.** A drop rewrites only the dragged items' `order`, so
|
||||
// the siblings' files — and `modified`, and a git commit — stay honest about what actually
|
||||
// the siblings' files — and `modified` — stay honest about what actually
|
||||
// moved. That is the one place these differ from `sortSelection`, which permutes because its
|
||||
// gesture is a permutation. `Ranks.insertionRanks` answering `nil` is the renumber trigger, and
|
||||
// the fallback is `moveLane`'s: compact the destination, then place against the fresh ladder.
|
||||
@@ -2928,8 +2933,8 @@ public final class BoardStore: HealHost {
|
||||
// **Silent no-ops throughout**, all of them the reload being the authority rather than the
|
||||
// gesture: a destination lane that is gone or tombstoned (04's "a card is never filed under a
|
||||
// `deleted:` parent"), a dragged set emptied by a foreign reload, and a drop that lands exactly
|
||||
// where everything already is (a drag that ends where it started must not stamp `modified` or
|
||||
// mint a commit — the resize drag's rule).
|
||||
// where everything already is (a drag that ends where it started must not stamp `modified` —
|
||||
// the resize drag's rule).
|
||||
|
||||
/// One member of a dragged card set, resolved against the snapshot: **where it is now** — a lane,
|
||||
/// or the board's trash.
|
||||
@@ -3507,8 +3512,7 @@ public final class BoardStore: HealHost {
|
||||
/// `FileManager` call in the view: it mutates the card's folder, so the churn has to round back
|
||||
/// as one *app-mediated* reload (the echo the watcher would otherwise read as a foreign edit),
|
||||
/// it has to refuse under the read-only lock like every other mutation (`performWrite`'s gate),
|
||||
/// and its failures have to reach the banner strip like every other write's. On git boards it
|
||||
/// is also one commit, for free, for the same reason.
|
||||
/// and its failures have to reach the banner strip like every other write's.
|
||||
///
|
||||
/// The guards are `importAttachments`' exactly, and its inverse in every way: **the board
|
||||
/// container and only it** (`boardItem`), so a trashed card is as unreachable as a deleted one
|
||||
@@ -3546,12 +3550,13 @@ public final class BoardStore: HealHost {
|
||||
/// **It registers no undo step**, and unlike its neighbours that is not a deferral: nobody asked
|
||||
/// for it. The relocation is the app tidying its own house on a reload, not a gesture — there is
|
||||
/// no ⌘Z that should follow it, and putting one on the stack would let the next ⌘Z undo something
|
||||
/// the user never did. (It is `renumberVisibleChildren`'s posture: bookkeeping composes no event,
|
||||
/// 06-history-undo.md ▸ Commit messages.)
|
||||
/// the user never did. (It is `renumberVisibleChildren`'s posture: bookkeeping composes no event —
|
||||
/// once 06-history-undo.md ▸ Commit messages' rule, now the change narrator's,
|
||||
/// `Kanban/Changes/ChangeNarrator.swift`.)
|
||||
///
|
||||
/// **One bracket over the whole board's worth of relocation**, so the churn rounds back as a
|
||||
/// single app-mediated reload and (on git boards) a single commit — the style batch's rule,
|
||||
/// applied to a batch the app started itself. The snapshot is not touched here any more than it
|
||||
/// single app-mediated reload — the style batch's rule, applied to a batch the app started
|
||||
/// itself. The snapshot is not touched here any more than it
|
||||
/// is anywhere else: the files move, the watcher notices, the reload lands.
|
||||
///
|
||||
/// **The write half re-verifies against disk**: `BoardWriter.relocateLooseFiles` re-reads each
|
||||
@@ -3728,13 +3733,13 @@ public final class BoardStore: HealHost {
|
||||
/// Board ▸ Move Up / Move Down (⌥⌘↑/⌥⌘↓) — the within-lane sort (04-interactions.md ▸ The map).
|
||||
///
|
||||
/// **One `performWrite` bracket**, like every other batch here: one gesture, one app-mediated
|
||||
/// reload, one commit on git boards.
|
||||
/// reload.
|
||||
///
|
||||
/// **The ranks are permuted, not invented.** The lane's existing `order` values, read in display
|
||||
/// order, are already a sorted ladder of exactly the right length — so the new ordering takes
|
||||
/// them rung for rung and only the cards whose *position* changed are rewritten. A block stepping
|
||||
/// past one sibling therefore touches the block plus that sibling and nothing else, which is what
|
||||
/// keeps `modified` (and, later, a git commit) honest about what actually moved.
|
||||
/// keeps `modified` honest about what actually moved.
|
||||
///
|
||||
/// The one case that ladder cannot serve is **duplicate `order` values**, where display order is
|
||||
/// decided by the folder-name tie-break (`Ranks.isOrderedForDisplay`) rather than by the rank —
|
||||
@@ -3824,14 +3829,15 @@ public final class BoardStore: HealHost {
|
||||
/// therefore whether a permanent delete stands an alert between one keystroke and unrecoverable
|
||||
/// deletion (03-board-ui.md § Trash, "Both confirm exactly where the loss is real").
|
||||
///
|
||||
/// **Every board is `true` today**, because every board is history mode *none*: nothing in the
|
||||
/// app keeps a second copy, so a purge is final everywhere.
|
||||
/// **Every board is `true`**, because app-managed git is gone (`strategy/01-git-excision.md`,
|
||||
/// 2026-08-08) and every board is history mode *none*: nothing in the app keeps a second copy, so
|
||||
/// a purge is final everywhere.
|
||||
///
|
||||
// m7-git: git boards answer `false` here — "on git boards they act immediately (delete-never-
|
||||
// forgets)" (06-history-undo.md). Repo-nested boards stay `true` alongside mode none: the app
|
||||
// manages no history for them either. The named predicate exists now so the committer card
|
||||
// changes one expression rather than hunting the confirmation logic out of two menu items and an
|
||||
// alert.
|
||||
// Once slated for a `false` answer on git boards — "on git boards they act immediately
|
||||
// (delete-never-forgets)" (06-history-undo.md, retired) — before the excision closed that door.
|
||||
// Repo-nested boards stay `true` alongside every other board now: the app manages no history for
|
||||
// any of them. The predicate stays a predicate rather than a literal at its two call sites
|
||||
// regardless — cheap insurance if a future substrate ever needs a `false` again.
|
||||
public var purgeIsUnrecoverable: Bool { true }
|
||||
|
||||
/// **File ▸ Delete ⌘⌫ and its plain-⌫ grammar twin — staged by place** (04-interactions.md ▸
|
||||
@@ -4323,8 +4329,8 @@ public final class BoardStore: HealHost {
|
||||
/// `comments` is a tolerated stray until the feature consumes the name.
|
||||
///
|
||||
/// **One bracket over every displacement the load found**, whatever their levels: the batch is one
|
||||
/// app-mediated reload and, on git boards, one heal commit — the loose-file relocation's rule, and
|
||||
/// this heal's own memo is board-wide anyway.
|
||||
/// app-mediated reload — the loose-file relocation's rule, and this heal's own memo is board-wide
|
||||
/// anyway.
|
||||
///
|
||||
/// **Displacement, never destruction**, and never a mint: the freed name is left empty and the next
|
||||
/// gesture that needs it creates the real folder — the next delete mints `.trash/`, the next import
|
||||
@@ -4483,9 +4489,11 @@ public final class BoardStore: HealHost {
|
||||
// MARK: - The board's noise definition
|
||||
|
||||
/// Puts the seeded `.gitignore` on a board that has none, and does nothing at all to a board
|
||||
/// that has one — 06-history-undo.md ▸ Repository hygiene's whole scheduling ("a board missing
|
||||
/// the file gains it by scheduled heal at open (the guide-refresh cadence)", re-ruled
|
||||
/// 2026-07-31).
|
||||
/// that has one — the scheduled-heal cadence every heal in this file shares (02-architecture.md ▸
|
||||
/// Components ▸ HealScheduler): "a board missing the file gains it by scheduled heal at open (the
|
||||
/// guide-refresh cadence)", re-ruled 2026-07-31. (06-history-undo.md ▸ Repository hygiene once
|
||||
/// carried this rule under a git-flavored heading; the doc retired with the git excision,
|
||||
/// `strategy/01-git-excision.md`, 2026-08-08 — the noise-gate rule itself did not.)
|
||||
///
|
||||
/// **`refreshAgentGuide()`'s twin, deliberately**, down to the shape of this method: the file is
|
||||
/// a board-root courtesy the app owns, its defect is *presence* rather than anything a tree walk
|
||||
@@ -4502,7 +4510,8 @@ public final class BoardStore: HealHost {
|
||||
/// 6), which is what makes a foreign deletion healable: the picture "missing" is restored by the
|
||||
/// delete, and a standing memo would make that deletion the one thing this could not answer —
|
||||
/// while a failing write still gets exactly one attempt per changed picture rather than one per
|
||||
/// reload. "Deletion is answered by re-seeding" is 06's own wording.
|
||||
/// reload. "Deletion is answered by re-seeding" was 06-history-undo.md's own wording; the rule
|
||||
/// survives the doc's retirement (`strategy/01-git-excision.md`, 2026-08-08).
|
||||
///
|
||||
/// **It registers no undo step**, like every heal: nobody asked for it.
|
||||
public func seedGitignore() {
|
||||
|
||||
@@ -320,8 +320,9 @@ extension BoardStore {
|
||||
/// — undone-and-superseded, or dropped off the end — or when the board session ends. A **stale
|
||||
/// skip is not a clean exit** and does not purge (the decoupling ruled 2026-07-31): that step's
|
||||
/// backing survives to session end instead, where `clear()` retires it and this method finally
|
||||
/// runs. On a git board the step is never kept, so the retirement fires at the close flush, which
|
||||
/// is where the purge always ran there ("purge rides the close flush").
|
||||
/// runs. (On a git board the step was never kept, so the retirement fired at the close flush
|
||||
/// instead, which is where the purge always ran there — "purge rides the close flush"; app-managed
|
||||
/// git is gone now, `strategy/01-git-excision.md`, so every board takes the native path above.)
|
||||
///
|
||||
/// ### Entries, not the container — the ownership gate, on this side too
|
||||
///
|
||||
|
||||
@@ -361,8 +361,9 @@ public final class FolderWatcher {
|
||||
/// reconciling reload of its own — nothing is lost by not failing redundantly.
|
||||
///
|
||||
/// An unbalanced call — `endBracket()` at depth 0 — is ignored rather than trapping: the
|
||||
/// consumer's brackets wrap `do`/`catch` spans over git operations, and a bug there should
|
||||
/// not take the app down.
|
||||
/// consumer's brackets wrap `do`/`catch` spans over the write they guard (once git operations
|
||||
/// among them; app-managed git is gone now, `strategy/01-git-excision.md`), and a bug there
|
||||
/// should not take the app down.
|
||||
public func endBracket() {
|
||||
guard bracketDepth > 0 else { return }
|
||||
bracketDepth -= 1
|
||||
|
||||
@@ -183,9 +183,10 @@ public enum TrashModel {
|
||||
/// the freight explicitly ("and its 5 cards"), which is 03's own phrasing and says the same
|
||||
/// thing where the user is already reading.
|
||||
private static func message(unrecoverable: Bool) -> String {
|
||||
// m7-git: on a git board the content stays reachable in history, so the second sentence is
|
||||
// the honest one — and the trash's own Delete does not confirm there at all
|
||||
// (`BoardStore.purgeIsUnrecoverable`).
|
||||
// Once slated to answer differently on a git board — there the content stays reachable in
|
||||
// history, so the second sentence would have been the honest one — before the excision closed
|
||||
// that door (`strategy/01-git-excision.md`, 2026-08-08) and `unrecoverable` is `true`
|
||||
// unconditionally (`BoardStore.purgeIsUnrecoverable`).
|
||||
unrecoverable
|
||||
? "This can\u{2019}t be undone."
|
||||
: "The board\u{2019}s history still has them."
|
||||
|
||||
@@ -84,7 +84,15 @@ enum AgentGuide {
|
||||
/// the zero-read minimum (`mkdir` plus one `index.md`, no `order`, no `schema`; it lands at the
|
||||
/// lane's bottom and the app stamps a real rank on its first touch) while still teaching
|
||||
/// *writing* `order` as the way to control position, which is the only way to control it.
|
||||
static let version = 10
|
||||
/// **v11 rewrites Git for the excision** (strategy/01-git-excision.md, ruled 2026-08-08):
|
||||
/// app-managed git is gone — the app never runs git, and the auto-commit the old section
|
||||
/// promised no longer exists, so shipping v10's text would document machinery to agents that
|
||||
/// isn't there. The section now teaches repo-resident etiquette alone — the format stays
|
||||
/// deliberately git-friendly, a board may live in a repository of the user's own, and there an
|
||||
/// agent stages only its own paths, commits its own changes with clear messages, and leaves the
|
||||
/// app-maintained files to the app. The stamping section drops its auto-commit clause the same
|
||||
/// way.
|
||||
static let version = 11
|
||||
|
||||
// MARK: - The version marker
|
||||
|
||||
@@ -235,9 +243,8 @@ enum AgentGuide {
|
||||
/// when the decision called for it.
|
||||
///
|
||||
/// **Called inside `BoardStore.performWrite`**, so both halves ride one watcher bracket: the
|
||||
/// rescue and the guide land as a single app-mediated reload, and (under Pro) as a
|
||||
/// single honestly-attributed commit rather than a foreign-looking rename followed by an
|
||||
/// app write (06-history-undo.md ▸ Commit messages, "Update agent guide (vN)").
|
||||
/// rescue and the guide land as a single app-mediated reload rather than as two foreign-looking
|
||||
/// events.
|
||||
///
|
||||
/// The move is `FileManager.moveItem` and nothing else: it preserves the bytes exactly — the
|
||||
/// displaced file may not even be UTF-8 — and it **fails rather than overwrite** if
|
||||
@@ -298,8 +305,8 @@ enum AgentGuide {
|
||||
}
|
||||
|
||||
try BoardWriter.atomicReplace(text: content, at: guideURL, operation: .agentGuide)
|
||||
// Heal-marked: the guide's refresh is app-initiated work, and its commit is its own
|
||||
// ("Update agent guide (vN)" already commits alone — 06-history-undo.md ▸ Commit messages).
|
||||
// Heal-marked: the guide's refresh is app-initiated work, and the ledger records it as the
|
||||
// heal it is rather than as anyone's edit.
|
||||
EchoLedger.current?.markHeal(at: guideURL)
|
||||
return displaced
|
||||
}
|
||||
@@ -420,11 +427,11 @@ enum AgentGuide {
|
||||
|
||||
Add `modified-by: <your-name>` (e.g. `modified-by: claude`) to the
|
||||
frontmatter of every `index.md` you write — it attributes the change in the
|
||||
app and, on git boards, in the auto-commit. The app clears the stamp on its
|
||||
own writes, so **re-stamp on every write, and after every move**: a bare
|
||||
folder move rewrites no file, so the moved card arrives unstamped unless you
|
||||
touch its `index.md` again. When you need exact authorship, commit your
|
||||
changes yourself instead (see Git below).
|
||||
app. The app clears the stamp on its own writes, so **re-stamp on every
|
||||
write, and after every move**: a bare folder move rewrites no file, so the
|
||||
moved card arrives unstamped unless you touch its `index.md` again. On a
|
||||
board that lives in a git repository of the user's own, committing your
|
||||
changes yourself (see Git below) records exact authorship as well.
|
||||
|
||||
## Creating a card
|
||||
|
||||
@@ -567,14 +574,20 @@ enum AgentGuide {
|
||||
|
||||
## Git
|
||||
|
||||
Some boards are git repositories — because the board lives inside a repo of
|
||||
yours, or because Lanework Pro manages its history. Two rules when one is:
|
||||
Lanework itself never runs git: the app manages no repository, makes no
|
||||
commits, and never reads `.git`. But the format is deliberately
|
||||
git-friendly — one file per card, stable UUID folder names, byte-faithful
|
||||
rewrites — and a board may live inside a repository of the user's own.
|
||||
When it does:
|
||||
|
||||
- **Stage only your own paths** — never `git add -A` or `git add .`: a
|
||||
sweep would commit the user's not-yet-committed changes under your name.
|
||||
- Committing your changes yourself is fine and gives you exact authorship;
|
||||
the app follows along. If you don't commit, Lanework Pro auto-commits
|
||||
your changes as external edits (attributed via `modified-by` when you
|
||||
stamped it).
|
||||
- **Commit your own changes, with clear messages** — nothing else will
|
||||
commit them for you, and a semantic message ("Move card 'Fix login' to
|
||||
Doing") is the history the user will actually read.
|
||||
- **Leave the app-maintained files to the app** — this guide and the
|
||||
seeded `.gitignore` are rewritten by Lanework when they need to be;
|
||||
don't edit or delete them, and don't commit changes to the user's other
|
||||
files that you didn't make.
|
||||
"""
|
||||
}
|
||||
|
||||
@@ -1060,19 +1060,22 @@ public enum BoardLoader: Sendable {
|
||||
|
||||
// MARK: - The earlier-occurrence-wins history seam
|
||||
|
||||
/// **Where git path history plugs into the duplicate-id winner rule** (01-storage-format.md
|
||||
/// **Where git path history once plugged into the duplicate-id winner rule** (01-storage-format.md
|
||||
/// § Fractal layout ▸ Rules: "on git boards, the path history already tracks outranks the
|
||||
/// newcomer (both tracked: the path that entered history first)").
|
||||
/// newcomer (both tracked: the path that entered history first)") — kept as a seam, unfilled,
|
||||
/// since app-managed git was excised entirely (`strategy/01-git-excision.md`, 2026-08-08).
|
||||
///
|
||||
/// A seam rather than an implementation because the first rung of that ladder is unreachable
|
||||
/// without git: the free tier runs no git machinery at all (12-editions.md ▸ The inert posture),
|
||||
/// so the loader consults an injected ranker and falls through to birth date and traversal order
|
||||
/// when there is none — which is every free-tier board, and every Pro board without a repo.
|
||||
/// A seam rather than an implementation because the first rung of that ladder was unreachable
|
||||
/// without git even before the excision: the free tier ran no git machinery at all
|
||||
/// (12-editions.md ▸ The inert posture), so the loader consults an injected ranker and falls
|
||||
/// through to birth date and traversal order when there is none — which is every board today, no
|
||||
/// exceptions.
|
||||
///
|
||||
/// Deliberately one closure and no protocol: the loader asks one question — "how early did this
|
||||
/// path enter history" — and pro-m1's implementation answers it from `git log --diff-filter=A
|
||||
/// --follow`-shaped plumbing behind the provider seam. `nil` means "untracked, or no history
|
||||
/// here", which the rule reads as *outranked by anything tracked*.
|
||||
/// path enter history" — and pro-m1's implementation once answered it from `git log
|
||||
/// --diff-filter=A --follow`-shaped plumbing behind the provider seam, before that plumbing left
|
||||
/// with the excision. `nil` means "untracked, or no history here", which the rule reads as
|
||||
/// *outranked by anything tracked*; nothing installs a ranker today, so every path reads `nil`.
|
||||
///
|
||||
/// - Parameter rank: keyed by the occurrence's **board-root-relative path**, which is what a
|
||||
/// repo's path history knows; lower is earlier.
|
||||
|
||||
@@ -585,9 +585,9 @@ public enum BoardWriter: Sendable {
|
||||
///
|
||||
/// - **`.DS_Store`** — the Finder writes one into every folder a user looks at, and on a board
|
||||
/// that means one per lane and one per card, each churning as icons and window positions move.
|
||||
/// On a git board it would be committed by the whole-tree stage, forever, under the user's own
|
||||
/// name; on every board it is now also the plainest thing the loose-file heal must not treat as
|
||||
/// a card's attachment.
|
||||
/// Once committed by the whole-tree stage on a git board, forever, under the user's own name;
|
||||
/// app-managed git is gone now (`strategy/01-git-excision.md`), and on every board it is the
|
||||
/// plainest thing the loose-file heal must not treat as a card's attachment.
|
||||
/// - **`.*.lanework-*`** — this file's own temp pattern, and the one entry the app is entitled to
|
||||
/// have an opinion about: `atomicReplace` writes `.<name>.lanework-<uuid>` beside its target,
|
||||
/// so a crashed write's residue is the app's litter rather than the user's file. The two must
|
||||
@@ -1890,7 +1890,8 @@ public enum BoardWriter: Sendable {
|
||||
/// `updateIndex`'s closure cannot. Everything else is identical and deliberately so — read
|
||||
/// fresh from disk, refuse an uneditable frontmatter shape, edit, stamp `modified` and clear
|
||||
/// `modified-by`, replace atomically. A toggle is "an ordinary user edit — the standard atomic
|
||||
/// write, auto-committed and undoable on git boards" (05), not a special case of anything.
|
||||
/// write" (05) — once "auto-committed and undoable on git boards" too, before app-managed git was
|
||||
/// excised (`strategy/01-git-excision.md`, 2026-08-08) — not a special case of anything.
|
||||
///
|
||||
/// ### The offset, and why it is re-checked
|
||||
///
|
||||
@@ -2085,7 +2086,7 @@ public enum BoardWriter: Sendable {
|
||||
///
|
||||
/// Text identical to what is already on disk writes nothing and returns `false` — `writeBody`'s
|
||||
/// untouched gate, applied to the whole file instead of the body span. Apply on a buffer the user
|
||||
/// only read must not churn `mtime`, wake every watcher, and (on git boards) mint an empty commit.
|
||||
/// only read must not churn `mtime` or wake every watcher.
|
||||
///
|
||||
/// - Returns: `true` when bytes were written, `false` when the file already read exactly like
|
||||
/// `text`.
|
||||
@@ -2591,9 +2592,11 @@ public enum BoardWriter: Sendable {
|
||||
/// reminting it would change an identity for no reason at all — the vanished-duplicate race,
|
||||
/// read from the surviving side.
|
||||
///
|
||||
/// **Heal-marked**, because the app started it on its own: the receipt is what splits the remint
|
||||
/// **Heal-marked**, because the app started it on its own: once the receipt that split the remint
|
||||
/// into its own commit on git boards, named for the Repair verb (06-history-undo.md ▸ Commit
|
||||
/// messages). Undo never sees it — heals are not gestures (13-native-undo.md).
|
||||
/// messages, retired with app-managed git, `strategy/01-git-excision.md`) — the receipt still
|
||||
/// exists for whatever consumes it next. Undo never sees it — heals are not gestures
|
||||
/// (13-native-undo.md).
|
||||
///
|
||||
/// - Parameter duplicate: the withheld occurrence, `path` relative to `boardRoot` so the write
|
||||
/// lands wherever the board lives *now*.
|
||||
@@ -3020,7 +3023,8 @@ public enum WriteOperation: Sendable, Equatable, CustomStringConvertible {
|
||||
|
||||
/// The board-root `.gitignore` being seeded — at creation, at instantiation, and by the
|
||||
/// scheduled heal that gives a board born before the ruling the file it now needs
|
||||
/// (06-history-undo.md ▸ Repository hygiene, re-ruled 2026-07-31; `seedGitignoreIfAbsent`).
|
||||
/// (06-history-undo.md ▸ Repository hygiene, retired with app-managed git,
|
||||
/// `strategy/01-git-excision.md`; `seedGitignoreIfAbsent`).
|
||||
///
|
||||
/// **No payload**, for `.agentGuide`'s reason: there is one such file per board, its name is
|
||||
/// fixed, and it is not an item with a title. Its own case beside `.agentGuide` rather than a
|
||||
|
||||
@@ -4,15 +4,19 @@ import Foundation
|
||||
/// one, and asked one question: is this board-relative path ignored?
|
||||
///
|
||||
/// The file outgrew git on 2026-07-31 (01-storage-format.md § Fractal layout ▸ Rules, "`.gitignore`
|
||||
/// is the noise gate"; 06-history-undo.md ▸ Repository hygiene): it is now the one definition of
|
||||
/// is the noise gate"; 06-history-undo.md ▸ Repository hygiene, retired with app-managed git,
|
||||
/// `strategy/01-git-excision.md`): it is now the one definition of
|
||||
/// noise the **loose-file relocation heal** obeys, on every board, git or not. A file matching it
|
||||
/// keeps the ordinary stray posture — "skipped, preserved verbatim, logged, never relocated, never
|
||||
/// announced" — and "the exclusion list is exactly that file, nothing hardcoded". On a Pro board the
|
||||
/// same file governs the committer through libgit2, so ignored noise neither relocates nor commits:
|
||||
/// one definition, two consumers. This type is the app's half of that, because the load path cannot
|
||||
/// reach for libgit2 — the free tier opens a board without so much as a `fileExists` under `.git`
|
||||
/// (12-editions.md ▸ The free tier and `.git`), and the heal runs on boards that have no repository
|
||||
/// at all.
|
||||
/// announced" — and "the exclusion list is exactly that file, nothing hardcoded". (On a Pro board the
|
||||
/// same file once governed the committer through libgit2 too, so ignored noise neither relocated nor
|
||||
/// committed — one definition, two consumers; app-managed git is gone now, so the heal is the only
|
||||
/// consumer left.) This type is the app's half of that — once split from libgit2 because the load
|
||||
/// path couldn't reach for it while the committer did; libgit2 left the project entirely with the
|
||||
/// git excision (`strategy/01-git-excision.md`, step 6), so today there is no other implementation to
|
||||
/// duplicate at all. The free tier always opened a board without so much as a `fileExists` under
|
||||
/// `.git` (12-editions.md ▸ The free tier and `.git`), and the heal runs on boards that have no
|
||||
/// repository at all.
|
||||
///
|
||||
/// **Pure: no I/O, no filesystem, no `URL`.** It parses text and answers about paths. Reading the
|
||||
/// bytes is `BoardLoader.ignoreRules(atBoardRoot:)`' job, once per walk — the loader stays a pure
|
||||
|
||||
@@ -878,8 +878,10 @@ public enum IntegrityRules: Sendable {
|
||||
/// The filesystem birth date (`.creationDateKey`), `nil` when it cannot be read — the
|
||||
/// **second** rung of the earlier-occurrence-wins ladder.
|
||||
public let birth: Date?
|
||||
/// Where the git path history places this path, `nil` when the board has no history or the
|
||||
/// path is untracked — the **first** rung, injected through `BoardLoader.IdentityHistoryRanker`.
|
||||
/// Where a history ranker once placed this path — `nil` unconditionally today, since
|
||||
/// app-managed git was excised entirely (`strategy/01-git-excision.md`, 2026-08-08) and
|
||||
/// nothing installs `BoardLoader.IdentityHistoryRanker` any more. The **first** rung when
|
||||
/// populated; nil-safe by design, so the ladder falls straight through to birth date now.
|
||||
public let historyRank: Int?
|
||||
|
||||
public init(
|
||||
@@ -958,8 +960,10 @@ public enum IntegrityRules: Sendable {
|
||||
/// folder and often the tracked one, so every other rung would withhold the very card the user
|
||||
/// just restored and render its ghost instead. The heal remints the trashed occurrence. The same
|
||||
/// preference governs a trashed lane sharing a live lane's UUID.
|
||||
/// 1. **Git path history** (`historyRank`): both tracked, the path that entered history first
|
||||
/// wins; one tracked, it outranks the newcomer outright.
|
||||
/// 1. **History rank** (`historyRank`): once git path history — both tracked, the path that
|
||||
/// entered history first wins; one tracked, it outranks the newcomer outright. Always `nil`
|
||||
/// today, no ranker installed since the git excision (`strategy/01-git-excision.md`,
|
||||
/// 2026-08-08); the ladder falls through to rung 2.
|
||||
/// 2. **Filesystem birth date** (`birth`): the older folder wins. Only consulted when *both*
|
||||
/// dates are readable and they differ — one unreadable date is no comparison at all.
|
||||
/// 3. **Deterministic traversal order**, which is `occurrences`' own order and therefore the
|
||||
|
||||
@@ -215,7 +215,9 @@ private struct BannerRowView: View {
|
||||
switch control {
|
||||
case let .cancel(cancel):
|
||||
// Cancel appears on safe copies only (02, settled): it means "remove the partial copy,
|
||||
// nothing lost". Git brackets pass no closure and therefore get no button.
|
||||
// nothing lost". Wholesale brackets that pass no closure get no button — once git's,
|
||||
// before the excision (`strategy/01-git-excision.md`, 2026-08-08); no caller passes one
|
||||
// today.
|
||||
Button(control.label, action: cancel)
|
||||
.buttonStyle(.link)
|
||||
.font(.callout)
|
||||
|
||||
@@ -93,11 +93,13 @@ extension BoardStore {
|
||||
/// broader lockdown that already covers these two items.
|
||||
/// - **The board popover's fields** are covered by disabling while the popover is open at all —
|
||||
/// coarser than per-field focus, but it is a configuration surface (04's carve-out) and no lane
|
||||
/// move belongs under it. That clause covers **every** configuration field the app has again since
|
||||
/// move belongs under it. That clause covered **every** configuration field the app had since
|
||||
/// 2026-08-07: the 2026-07-31 split moved most of them to a board settings sheet, whose flag this
|
||||
/// function read as a third disjunct, and the reversal brought them back — branch creation, commit
|
||||
/// identity, and pro-m2's remote URL and credentials all live in the popover's Git tab, under the
|
||||
/// popover's own open-at-all rule.
|
||||
/// function read as a third disjunct, and the reversal brought them back under the popover's own
|
||||
/// open-at-all rule — branch creation, commit identity, and pro-m2's remote URL and credentials
|
||||
/// among them, all in the popover's Git tab at the time. The Git tab itself retired with
|
||||
/// app-managed git (`strategy/01-git-excision.md`, 2026-08-08); the popover's open-at-all rule
|
||||
/// now guards only `BoardInfoTabView`'s ordinary fields.
|
||||
/// - **The search field** is per-focus and exact (`BoardSearchPresentation.isFocused`), which it has
|
||||
/// to be: the field's own rule is that board commands *stay enabled* while it holds the keyboard
|
||||
/// (04 § Search), so these two are the narrow exception to it and nothing coarser would do.
|
||||
|
||||
@@ -43,9 +43,10 @@ extension NSToolbarItem.Identifier {
|
||||
/// `windowWillReturnUndoManager` hands back the session's `BoardUndoManager`, and both therefore
|
||||
/// enable exactly when that board has a step to cross and no read-only lock stands
|
||||
/// (13-native-undo.md ▸ Rules). **Every board has undo in every tier**, so there is no tier-shaped
|
||||
/// disablement to write: 03's parenthetical about boards without undo is 06's *git* substrate, which
|
||||
/// a board binds by being in git mode — on any tier since 12-editions.md ▸ PIVOT 2026-08-07, and
|
||||
/// under a Pro subscription before it.
|
||||
/// disablement to write: 03's parenthetical about boards without undo was 06's *git* substrate,
|
||||
/// which a board once bound by being in git mode — on any tier since 12-editions.md ▸ PIVOT
|
||||
/// 2026-08-07, and under a Pro subscription before it, until app-managed git left the app entirely
|
||||
/// (`strategy/01-git-excision.md`, 2026-08-08) and there stopped being a substrate to bind.
|
||||
///
|
||||
/// Their labels are the design's one exception to the menu-title rule: `NSUndoManager` rewrites the
|
||||
/// *menu* titles as the stack changes ("Undo Move Card"), which a toolbar label does not track, so
|
||||
|
||||
@@ -75,9 +75,9 @@ public final class CardBodyPresentation {
|
||||
/// is the type every path that *enters* Edit already holds, so attaching the announcement to the
|
||||
/// flip makes "always" true by construction rather than by three call sites remembering.
|
||||
///
|
||||
/// Filled in by the window with its edit session's `beginEditSession()`. Its consumer is pro-m1's
|
||||
/// auto-committer, which stages around the card's folder for as long as the session stands
|
||||
/// (06-history-undo.md ▸ Rules ▸ Auto-commit).
|
||||
/// Filled in by the window with its edit session's `beginEditSession()`. Its original second
|
||||
/// consumer — the auto-committer's stage-around — left with the git excision (2026-08-08,
|
||||
/// strategy/01-git-excision.md); the session bookkeeping it drives is its own reason now.
|
||||
public var beginEdits: (() -> Void)?
|
||||
|
||||
/// Whether the opening rule has already run for this window.
|
||||
@@ -97,7 +97,7 @@ public final class CardBodyPresentation {
|
||||
hasOpened = true
|
||||
mode = CardBodyMode.opening(body: body)
|
||||
// A card that opened straight into Edit because its body was empty is in a session exactly
|
||||
// like one the user pressed ⌘E in, and the committer has to stage around it either way.
|
||||
// like one the user pressed ⌘E in, and the session bookkeeping applies either way.
|
||||
if mode == .edit { beginEdits?() }
|
||||
return mode
|
||||
}
|
||||
|
||||
@@ -146,8 +146,8 @@ public final class CommentEditSession {
|
||||
/// **Save, or ⌘↩** — the session's commit point (05 ▸ The comments column). Flushes and ends.
|
||||
///
|
||||
/// A named call rather than a bare `flush()` for `CardBodyEditSession.endEditSession()`'s reason:
|
||||
/// this is the boundary a Pro auto-commit coalesces on, one commit per session and never per save
|
||||
/// tick (06-history-undo.md ▸ Rules ▸ Auto-commit).
|
||||
/// this is the session's one settlement boundary — the excised auto-committer coalesced on it
|
||||
/// (one commit per session, never per save tick), and any future history consumer would too.
|
||||
@discardableResult
|
||||
public func commit() -> Bool {
|
||||
guard !hasEnded else { return false }
|
||||
|
||||
Reference in New Issue
Block a user