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:
+1
-21
@@ -6,7 +6,7 @@ The board's symbol can now wear a color: the symbol picker carries a row of tint
|
||||
|
||||
The board popover's new Theme tab dresses the board in a solid color or a generated pattern: filter by light or dark, colors and saturation, then pick from eight hues.
|
||||
|
||||
The board popover is now organized into three tabs — Info with the board's vital statistics, Theme for backgrounds, and Git.
|
||||
The board popover is now organized into tabs — Info with the board's vital statistics, and Theme for backgrounds.
|
||||
|
||||
The rubber band now highlights cards the moment it touches them, instead of lagging behind on large boards.
|
||||
|
||||
@@ -74,24 +74,4 @@ Board search now matches comment text, and ⌘F in a card window steps through m
|
||||
|
||||
Deleting a comment takes effect immediately, stays undoable in the card's window, and board-level undo can still bring it back after the window closes.
|
||||
|
||||
Every board can now gain git-backed history — commits, undo and redo as history entries, and branches — with remote sync still to come.
|
||||
|
||||
The board popover can now add git version history to any board, with no git installation needed.
|
||||
|
||||
A board that's already a git repository opens with its history live automatically — cloning a shared board just works.
|
||||
|
||||
Every settled change on a git board becomes a history entry with a plain-language description, like *Move card 'Fix login' to Doing*.
|
||||
|
||||
Edits from agents and other apps are recorded under their own authorship, so a git board's history always says who changed what.
|
||||
|
||||
Undo and redo on git boards restore earlier states as new history entries — nothing is ever erased — and the undo trail survives relaunching the app.
|
||||
|
||||
Every card window on a git board gains a History section listing the changes that touched that card, newest first.
|
||||
|
||||
On git boards, everything done in a card window becomes a single history entry when the window closes.
|
||||
|
||||
Switch or create branches from the board popover, with an explicit save-or-discard step protecting unsaved card edits.
|
||||
|
||||
The name and email on a board's history entries are editable in the board popover and stored in the board's own repository.
|
||||
|
||||
Zoom the board in and out from the View menu (⌘+ and ⌘−, ⌘0 for actual size), and the size you settle on is remembered across launches.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# History & Undo
|
||||
|
||||
**RETIRED 2026-08-08** (strategy/01-git-excision.md — the excision ruling): app-managed git is removed from Lanework entirely — the git HistoryProvider, auto-commit, branches, restore-as-forward-commit, all of the machinery this doc designed — so nothing below describes the shipping app. Three things outlive it: the **Undo routing** section migrated to 13-native-undo.md before this note landed (the rule was always tier- and substrate-independent, and 13 is its live home), the **commit-message vocabulary** survives as the harvested change narrator (`Kanban/Changes/`, the designated core of the future activity feed / foreign-change journal), and the safety-net value — history of externally delivered changes — routes to the deferred foreign-change journal (01-git-excision.md ▸ Successors). The stack itself is recoverable at tag `pre-git-excision`. Everything below is the record of the design as it stood.
|
||||
|
||||
**Tier scope: every tier** (Pivot 2026-08-07 — 12-editions.md: git left the paywall; this line formerly scoped the doc to Lanework Pro, with the free tier shipping mode:none only over the now-retired inert-`.git` posture). This doc is the git HistoryProvider, composed on git-mode boards in every tier; boards without app-managed git bind macOS-native undo (13-native-undo.md). The Undo routing section below was always tier-independent — both substrates dispatch through it.
|
||||
|
||||
Git is the undo substrate — on boards that have git. **Git is opt-in per board (a pivot from the pathfinder, which auto-initialized every board): a board may be created without git, and git can be added later** (via the board popover's Git tab — 03-board-ui.md; see 07-sync-collab.md's mode progression). A board without app-managed git binds the **native undo stack in every tier** — repo-nested included (re-ruled 2026-07-31, twice — the provider follows the board, 13-native-undo.md; formerly no-undo under Pro, which made upgrading remove undo from mode-none boards, and the repo-nested no-undo residue retired the same day: the native stack touches no git, so leave-strictly-alone is untouched and no board lacks ⌘Z). (Text editors keep their standard typing undo everywhere; see Undo routing below.) Deletes — card or lane — are recoverable on every board via the materialized trash (03-board-ui.md). Add-git swaps native → git mid-session, discarding the in-session native stack and seeding the git trail — the branch-switch discard-and-reseed precedent. On git-enabled boards, every settled change auto-commits; those mechanics are carried over from the pathfinder with their hard rules intact.
|
||||
@@ -25,7 +27,7 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board
|
||||
|
||||
## Undo routing
|
||||
|
||||
**Routing is by focus** — the platform's first-responder rule, its own section because two undo systems coexist and four docs cite the rule. While a text-editing surface is focused (card title field, body Edit mode, raw source, board inline rename), ⌘Z/⇧⌘Z are that editor's own **text undo** — standard, transient, session-scoped: leaving the editor (mode flip, focus loss, close) ends the session, and from then on that content's undo story is the git trail. Text undo works on **every** board — and since the 2026-07-31 repo-nested re-ruling, so does board-level undo: every board binds a provider (native or git), so "no undo" is no longer a state any board is in. **Control-class text fields route the same way** (settled): the search field (04-interactions.md ▸ Search), the popover's rename field, and the popover's own configuration fields (commit identity, the New Branch… name, and 07's credentials and remote URL when they land) own ⌘Z/⇧⌘Z as field-local text undo while focused — "board menu commands stay enabled" never hands Edit ▸ Undo to git while a text-bearing control has focus; a reflexive undo over a typo must never become a tree checkout. With focus outside every text-bearing surface — editor or control — Edit ▸ Undo/Redo are, **in a card window, that window's own session stack** (13-native-undo.md's two-level model, re-ruled 2026-07-31 — fine-grained window gestures, both tiers; the coarse close unit is the tier-split: one native board step, or one commit), and on board surfaces board history — the board's bound provider, git or native (every board binds one since 2026-07-31; disabling is locks and empty stacks). **No fall-through**: exhausting a focused editor's — or the window's — stack beeps; it never reaches board history.
|
||||
Migrated to 13-native-undo.md (2026-08-08, at this doc\'s retirement) — the rule was always tier- and substrate-independent, and 13 is its live home; the heading stays here for the docs that cite it.
|
||||
|
||||
## Commit messages
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Sync & Collaboration
|
||||
|
||||
**RETIRED 2026-08-08 as written** (strategy/01-git-excision.md): the git-rails sync this doc designed — remotes, pull/push, auth, the mode progression — died with the excision of app-managed git, and none of its remote half was ever built. The successor is the **ops-based sync service** (semantic ops over a server-side worktree API, the phone and eventually the web as worktree clients — 01-git-excision.md ▸ Successors), which arrives with its own workstream and its own docs; the popover's Sync tab (03-board-ui.md) holds the surface until then. The iCloud-Drive warning below outlives the retirement in spirit — iCloud remains unendorsed as a transport. Everything below is the record of the design as it stood.
|
||||
|
||||
**Tier scope: Lanework Pro** (12-editions.md). The free tier ships mode:none only — the state machine below never leaves its first state there, `.git` encountered on disk is inert (12), and the Mode: none section's old no-undo caveat is superseded in every tier by native undo (13-native-undo.md — the provider follows the board, re-ruled 2026-07-31). Teams adds tracker-backed sync behind the same seam (deferred).
|
||||
|
||||
Every board has exactly one **collab mode** at a time, but the mode is not fixed at creation — it can evolve over the board's lifetime:
|
||||
|
||||
@@ -4,6 +4,8 @@ Lanework ships as **one Mac App Store app** — `dev.rzen.indie.Kanban`, free, 2
|
||||
|
||||
**PIVOT 2026-08-07 — git leaves the paywall.** Git integration — detection, adoption, auto-commit, git-backed undo/history, branches (06-history-undo.md), and remotes/auth when they ship (07-sync-collab.md) — is **tier-independent**: every tier composes the git stack on git-mode boards exactly as Pro did. The base/Pro feature split is being re-decided, and git isn't going to be it. Until the new split is ruled: the subscription **machinery stays built and tested but dormant** — the entitlement's mechanics (local read, composition-time, offline grace, the recorded session tier) are unchanged and correct for whatever the next split gates, but the Settings Pro section is not rendered and no surface names or sells Pro. The free-only git postures are **retired**: the inert-`.git` stance and the popover's Pro pointer describe a gate that no longer exists — a `.git` at a board root is live in every tier, detection runs at every board open, and every board carries the popover's Git tab (the mode-driven postures: no-repository door, repo-nested, unverifiable, branch). What the pivot does **not** change: git stays **opt-in per board** (06 — creating a local repository is the user's deliberate choice, never auto-initialized), and a board without app-managed git binds conventional native undo/redo (13) exactly as before — the provider still follows the board. The sections below describing the git gate (the tier matrix's git rows, the inert posture, no-grandfathering) stand as record of the pre-pivot design and are not restated; read them through this note.
|
||||
|
||||
**PIVOT 2026-08-08 — git excised entirely** (strategy/01-git-excision.md, the second step past the note above): the git stack the first pivot made tier-independent is now removed from the app — engine, UI surfaces, seam bindings, SwiftGitX/libgit2 — with the message engine harvested to `Kanban/Changes/` as the future activity-feed narrator and the pre-excision state tagged `pre-git-excision`. The format's git-friendliness (per-card files, stable UUIDs, byte-fidelity, the seeded `.gitignore`) is the surviving promise, free forever; boards inside user repositories stay first-class and untouched. The tier machinery stays dormant exactly as the first pivot left it; the next split is expected to be "your board beyond your Mac" — the iPhone companion plus the ops-based sync service (the strategy workstream's charter) — and is not ruled by this note. 06 and 14 are retired, 07 is retired as written (06's Undo routing migrated to 13, now the sole substrate's doc); read this doc's git rows, modes, and matrix as record.
|
||||
|
||||
**Re-ruled 2026-07-30 — the one-app collapse.** This supersedes the 2026-07-27 two-app split (separate base and Pro targets) and the 2026-07-29 App Group ruling that served it. The split's compile-time purity (base never links libgit2, no network entitlement) dragged permanent coexistence machinery behind it: a shared App Group, per-edition grant slots (security-scoped bookmarks never cross sandboxes), registry freshness stamping between two live processes, UTI-ownership twins, a both-apps-installed rulebook — a tax on every layer that generated a steady stream of design findings, all serving a state (two sandboxed apps sharing app-side state) that existed only because the packaging created it. One app makes that state unrepresentable. Costs accepted with eyes open: libgit2 rides dormant in the free download, and the one app declares the network-client entitlement (exercised only under Pro) — the "free app provably has no network access" story is traded for "no network use until you subscribe," which is honest but weaker.
|
||||
|
||||
## The tiers
|
||||
|
||||
@@ -1,28 +1,34 @@
|
||||
# Native Undo
|
||||
|
||||
The undo/redo substrate for **every board without app-managed git** (re-ruled 2026-07-31 — the provider follows the board, not the tier alone; formerly free-tier-only, which made a Pro upgrade *remove* undo from mode-none boards). Since the 2026-08-07 pivot (12-editions.md — git left the paywall) the tier axis is gone entirely, and one thing the pivot does **not** change: git remains **opt-in per board** — creating a local repository is the user's deliberate choice (add-git, 06), never something the app initializes for them — so a board whose user never opted in keeps this conventional stack for good. The substrate is the board's mode alone: mode-none **and repo-nested** boards bind it (the repo-nested no-undo case retired 2026-07-31: leave-strictly-alone concerns *git*, and this stack never touches git — memory-only, journal-free, session-scoped — so what repo-nested denies is app-managed history, never ⌘Z; the upgrade story is thereby exceptionless), switching to the git provider (06-history-undo.md) where the board's own git exists. **Add-git swaps the substrate mid-session** — the commanded flip discards the in-session native stack and seeds the git trail from the root commit, the branch-switch discard-and-reseed precedent applied; a subscription lapse still never interrupts (12). The design problem is not NSUndoManager itself — it is native undo over **files-are-truth**: the disk can change underneath the stack, because the app is not the only writer.
|
||||
The undo/redo substrate for **every board** (amended 2026-08-08 — strategy/01-git-excision.md: app-managed git is excised, and with it the git provider this doc used to hand off to; the earlier text here described the mode split and the add-git mid-session swap, both gone with the stack). The provider seam survives the excision deliberately — `HistoryProviding` is nil-safe and default-native (01-git-excision.md ▸ Reversibility) — so a future provider (the deferred foreign-change journal, an ops-backed history, or even git again) re-binds without re-plumbing; until one exists, this stack is the only substrate any board binds, repo-nested and user-repo boards included (this stack never touches git — memory-only, journal-free, session-scoped — so leave-strictly-alone holds by construction). The design problem is not NSUndoManager itself — it is native undo over **files-are-truth**: the disk can change underneath the stack, because the app is not the only writer.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Two levels: one stack per board, one per open card window** (re-ruled 2026-07-31 — the session-coarsening model, superseding the pure one-stack rule): the **board stack** is owned by the board session and shared by board surfaces; a **card window owns its own stack** for the session it represents — every gesture issued in that window (comment post/delete/edit, body Edit sessions, style/details changes, attachment ops where undoable) registers there at fine grain, and `window.undoManager` answers with it (standard per-window AppKit scoping). Disk stays live throughout — files-first untouched; this is history granularity only. **Window close coarsens**: the session's net effect registers on the board stack as **one coarse step named "Changes to '⟨card⟩'"** (ruled 2026-07-31 — the board row reads "Undo Changes to 'Fix login'": plural and scope-flavored, distinct from every fine verb, honest about folding many kinds; the fine body-edit wording never leaks onto the board menu), values-based, whose undo restores the card subtree to its session-start state — deleted comments included — and whose redo reapplies the net effect; a session with no net change registers nothing. The coarse step is transactional at apply time: staleness validation runs per component (the field-level predicate below), and any stale component skips the whole step — never a partial session revert. **Session steps anchor by card identity, never by path** (ruled 2026-07-31): the coarse step — and the window's fine steps it folds — stores the card's UUID plus expected values, and apply-time validation resolves the card's *current* folder exactly the way the window itself always resolves its card (the per-snapshot UUID walk; `writeCardBody` already resolves trash locations on purpose). A tracked relocation — a lane move mid-session or after close, a trash move — therefore never stales the step; only genuine content changes do, which is what the validation exists to catch. A card that resolves nowhere (purged, or moved out of the board) is the honest skip. 06 ▸ Undo routing applies unchanged — text-editing surfaces get their session-scoped text undo above either stack. **Two stacks over one open card are the blessed shape** (2026-07-31): a board-issued gesture on a card whose window is open registers on the board stack while the window's own gestures register on the window stack — no ordering relation between the two, interleaving decided by ⌘Z focus (06 ▸ Undo routing); routing board gestures into the open window's stack was considered and rejected, since board ⌘Z must never see card-session steps.
|
||||
- **Registration at the Writer boundary.** Every app-mediated mutation already passes through the Writer as a `WriteOperation` (02-architecture.md) — that closed enum is the exact inventory of undoable operations. Each Writer call site registers the inverse operation, computed from the pre-write snapshot the store already holds: move → move back (original lane, original `order`); reorder → restore original `order`; rename → restore title; restyle → restore prior style; resize → restore prior width; Edit-session body save → restore prior body bytes; card or lane delete (⌫) → move back out of `.trash/` (lanes rejoined the trash 2026-07-29 — the recreate-from-capture inverse retires with the last destructive delete); restore-by-move → move back in; create → remove the created folder.
|
||||
- **What is not undoable** (settled): **Permanently delete** (the trash's Delete, Empty Trash) — `purgeIsUnrecoverable` stays true in base, and the existing confirmation rule (03-board-ui.md) already fires on all base boards, since none have git history: the confirm *is* the safety. **The duplicate-id remint** (01-storage-format.md — a silent scheduled heal since 2026-07-29, formerly the user-gated Repair) — heals aren't user gestures, so nothing enters the stack, and undoing one would recreate the duplicate id it exists to remove. Permanently delete matches its existing "destructive, confirmed, final" posture; the remint sits outside undo as all heals do. **Raw Source Apply** (blessed 2026-07-31): the hatch writes byte-for-byte outside every contract — no `modified` stamp, no attribution clear, and no history step at either level; an Apply-only session folds to no coarse step, and an Apply mixed into a session is invisible to the fold. The hatch's story is "you edited the file," and files-are-truth covers it — on git boards the write commits like any disk change (05-card-window.md's carve-outs are the same statement from the stamping side).
|
||||
- **What is not undoable** (settled): **Permanently delete** (the trash's Delete, Empty Trash) — `purgeIsUnrecoverable` stays true in base, and the existing confirmation rule (03-board-ui.md) already fires on all base boards, since none have git history: the confirm *is* the safety. **The duplicate-id remint** (01-storage-format.md — a silent scheduled heal since 2026-07-29, formerly the user-gated Repair) — heals aren't user gestures, so nothing enters the stack, and undoing one would recreate the duplicate id it exists to remove. Permanently delete matches its existing "destructive, confirmed, final" posture; the remint sits outside undo as all heals do. **Raw Source Apply** (blessed 2026-07-31): the hatch writes byte-for-byte outside every contract — no `modified` stamp, no attribution clear, and no history step at either level; an Apply-only session folds to no coarse step, and an Apply mixed into a session is invisible to the fold. The hatch's story is "you edited the file," and files-are-truth covers it (05-card-window.md's carve-outs are the same statement from the stamping side).
|
||||
- **Coalescing follows commit granularity** (settled; window scoping added 2026-07-31): one gesture, one undo step — a multi-card move is one step with a plural title; an Edit session is one step, registered at the Edit→Preview flip (the effective Save — 05-card-window.md) **on the card window's stack**, like every window gesture; the window close registers the one coarse session step on the board stack (Rules above); a styling batch is one step (03's one-gesture-one-commit rule, substrate swapped). The 06 vocabulary supplies menu titles ("Undo Move 3 Cards"), via NSUndoManager's dynamic retitling — the same naming machinery both editions use.
|
||||
- **Session-only persistence** (settled): the stack lives with the board session and dies at close/quit — standard macOS behavior. Git undo's survive-relaunch property is a Pro difference, stated honestly (12's matrix).
|
||||
- **Foreign writes never join the stack** (settled): NSUndoManager can only undo what the app mediated. An agent's or hand edit is not a step — the honest capability gap vs Pro (12's matrix). Foreign changes also do not clear the stack wholesale; collisions are handled lazily, per step, by validation:
|
||||
- **Session-only persistence** (settled): the stack lives with the board session and dies at close/quit — standard macOS behavior. (The excised git substrate's survive-relaunch property went with it; persistence is a future provider's to offer through the same seam.)
|
||||
- **Foreign writes never join the stack** (settled): NSUndoManager can only undo what the app mediated. An agent's or hand edit is not a step — the honest capability gap, whose designated successor is the deferred foreign-change journal (strategy/01-git-excision.md ▸ Successors). Foreign changes also do not clear the stack wholesale; collisions are handled lazily, per step, by validation:
|
||||
- **Staleness validation before every apply** (settled): an inverse operation re-checks its target against the disk — a fresh read of the target at ⌘Z time (blessed 2026-07-29: not the store snapshot, which is by construction one reload behind the app's own writes; a rapid ⌘Z run validated against the snapshot would compare pre-state and false-skip every step). **The predicate is field-level** (settled — ruled 2026-07-27): each step registers both sides of its write anyway (the before-value is the inverse; the after-value is what its write set), so validation compares the targeted field's current value against the expected after-value — nearly free, and truer to never-surprise-the-file than an existence-only check (an inverse rename must not clobber a foreign rename on a still-existing card; body steps compare bytes). Target folder gone, or the field no longer holding the step's after-value → the step is **skipped, not applied**: popped from the stack with an info-tone banner ("Undo skipped — 'Fix login' changed outside Lanework"), and ⌘Z falls through to the next step. Never apply a stale inverse on top of someone else's newer write. **Delete steps validate their undo by existence only** (blessed 2026-07-31): a delete's forward write sets nothing but the `modified` stamp (the arrival rank mint retired 2026-07-31), and a clock reading is not a choosable after-value — pinning it would false-skip the restore whenever an agent touched the trashed card; the undo therefore expects only that the trashed folder still exists, while the redo stays field-level via the restore's `order` write. **Invalidation is lazy** (settled — ruled 2026-07-27): staleness is discovered at ⌘Z time, never by background pruning — the EchoLedger's foreign diffs do not eagerly drop colliding steps. The stack always looks full; with the field-level predicate a skip fires only on a genuine per-field collision, and a skipped step's banner explains itself, where eager pruning would shrink the stack invisibly mid-session.
|
||||
- **Locks disable the stack** (settled): every read-only lock (vanished root, failed reload after wholesale ops, unwritable location — 02-architecture.md) disables Undo/Redo with the other mutating commands; the stack itself survives the lock and resumes when it clears. Steps landed before a lock validate like any other at apply time.
|
||||
|
||||
## Undo routing
|
||||
|
||||
(Migrated here 2026-08-08 from 06-history-undo.md at that doc's retirement — the rule was always tier- and substrate-independent, and this is its live home now; four docs cite it by this heading.)
|
||||
|
||||
**Routing is by focus** — the platform's first-responder rule. While a text-editing surface is focused (card title field, body Edit mode, raw source, board inline rename), ⌘Z/⇧⌘Z are that editor's own **text undo** — standard, transient, session-scoped: leaving the editor (mode flip, focus loss, close) ends the session, and from then on that content's undo story is board history. Text undo works on **every** board, and so does board-level undo: every board binds a provider (the native stack today; the seam admits future providers), so "no undo" is not a state any board is in. **Control-class text fields route the same way** (settled): the search field (04-interactions.md ▸ Search), the popover's rename field, and the popover's other configuration fields own ⌘Z/⇧⌘Z as field-local text undo while focused — "board menu commands stay enabled" never hands Edit ▸ Undo to board history while a text-bearing control has focus; a reflexive undo over a typo must never become a board-level restore. With focus outside every text-bearing surface — editor or control — Edit ▸ Undo/Redo are, **in a card window, that window's own session stack** (the two-level model in Rules above — fine-grained window gestures; the coarse close unit is one board step), and on board surfaces board history — the board's bound provider (disabling is locks and empty stacks). **No fall-through**: exhausting a focused editor's — or the window's — stack beeps; it never reaches board history.
|
||||
|
||||
## Interaction with the trash
|
||||
|
||||
⌫'s undo is the move back — a delete is a move into `.trash/` (cards resettled 2026-07-28; lanes rejoined 2026-07-29), so its undo is the ordinary inverse move, returning a card to its source lane and rank, a lane to its strip position (subtree intact — it never left the folder); a restore-by-move undoes the same way in reverse. The stack and the trash never conflict — they are the same folder moves addressed by recency instead of by selection. The old lane-delete recreate-from-capture inverse is **retired** — no destructive delete remains outside a trash, so nothing needs byte capture. A **permanent delete registers no step** — the trash's Delete and Empty Trash are not undoable (Rules above), lanes and their freight included; the confirm is the safety.
|
||||
|
||||
**Comments keep the no-capture rule true — on the window stack** (re-ruled 2026-07-31, superseding the board-stack routing): a comment delete is a move into the card's `comments/.trash/` (01-storage-format.md ▸ Enhanced schema — the materialized-trash pattern one level down), its inverse the ordinary move back, and the step lives on the **card window's own stack** (Rules above) — the board stack never carries a granular comment step, so the old stale-after-close skip scenario cannot arise. **The purge of `comments/.trash/` defers with the coarse step** (re-ruled 2026-07-31, superseding purge-at-close): the coarse close step's undo restores deleted comments, so their backing lives as long as the step does — the purge runs when the coarse step leaves the board stack **cleanly** — undone-and-superseded, or dropped off the end — or when the board session ends; **a stale-skipped step's backing instead survives to board-session end** (ruled 2026-07-31, decoupling skip from purge): the skip banner says nothing was applied, and an irreversible purge riding that gesture would be surprise loss — the skip is exactly when the user may want to inspect what the collision left; crash residue still sweeps at the next card-window open — **and residue is defined by the purge-deferral condition itself** (ruled 2026-07-31): `comments/.trash/` content referenced by a live coarse step on the board stack is a step's backing, not residue — the open-time sweep consults the stack and skips owned content, re-arming when the owning step leaves the stack (which is exactly when the deferred purge wanted to run; one condition, two consumers). Reopening a window can therefore never destroy its prior session's undo backing. Unowned content sweeps as before, armed-then-cleared like every heal memo. **Every purge of `comments/.trash/` is per-entry behind the ownership gate** (ruled 2026-08-06 — the container-whole retirement purge retires): a step's retirement and a no-step close remove only entries no live step still backs — the same `backedContent` inventory the sweep consults, making it one condition, *three* consumers. The container-whole purge assumed one owning step per card's comment trash, and two sessions over the same card broke it: the second step's retirement — or a mere reopen-and-close that registered nothing — emptied the first step's backing out from under it, silently killing an undo the stack still promised. Under the gate a purge cannot stale a live step by construction; an entry that outlives its owner is collected by whichever consumer runs next (the next retirement, close, open-time sweep, or session end — convergence, not a leak). One carve-out: **an open card window is itself an owner of its card's comment trash** — a retirement firing while the card's window is open defers its purge to that window's close, because entries deleted in the live session are backed by the window's fine steps, which the board-stack inventory cannot see; the close then settles by the same gate (its coarse step becomes the owner, or the no-step close purges the unowned). On Pro the substrate is history: the close commit nets delete-plus-purge to a removal, revert restores it, so purge rides the close flush there as before — purge timing follows the undo substrate's need.
|
||||
**Comments keep the no-capture rule true — on the window stack** (re-ruled 2026-07-31, superseding the board-stack routing): a comment delete is a move into the card's `comments/.trash/` (01-storage-format.md ▸ Enhanced schema — the materialized-trash pattern one level down), its inverse the ordinary move back, and the step lives on the **card window's own stack** (Rules above) — the board stack never carries a granular comment step, so the old stale-after-close skip scenario cannot arise. **The purge of `comments/.trash/` defers with the coarse step** (re-ruled 2026-07-31, superseding purge-at-close): the coarse close step's undo restores deleted comments, so their backing lives as long as the step does — the purge runs when the coarse step leaves the board stack **cleanly** — undone-and-superseded, or dropped off the end — or when the board session ends; **a stale-skipped step's backing instead survives to board-session end** (ruled 2026-07-31, decoupling skip from purge): the skip banner says nothing was applied, and an irreversible purge riding that gesture would be surprise loss — the skip is exactly when the user may want to inspect what the collision left; crash residue still sweeps at the next card-window open — **and residue is defined by the purge-deferral condition itself** (ruled 2026-07-31): `comments/.trash/` content referenced by a live coarse step on the board stack is a step's backing, not residue — the open-time sweep consults the stack and skips owned content, re-arming when the owning step leaves the stack (which is exactly when the deferred purge wanted to run; one condition, two consumers). Reopening a window can therefore never destroy its prior session's undo backing. Unowned content sweeps as before, armed-then-cleared like every heal memo. **Every purge of `comments/.trash/` is per-entry behind the ownership gate** (ruled 2026-08-06 — the container-whole retirement purge retires): a step's retirement and a no-step close remove only entries no live step still backs — the same `backedContent` inventory the sweep consults, making it one condition, *three* consumers. The container-whole purge assumed one owning step per card's comment trash, and two sessions over the same card broke it: the second step's retirement — or a mere reopen-and-close that registered nothing — emptied the first step's backing out from under it, silently killing an undo the stack still promised. Under the gate a purge cannot stale a live step by construction; an entry that outlives its owner is collected by whichever consumer runs next (the next retirement, close, open-time sweep, or session end — convergence, not a leak). One carve-out: **an open card window is itself an owner of its card's comment trash** — a retirement firing while the card's window is open defers its purge to that window's close, because entries deleted in the live session are backed by the window's fine steps, which the board-stack inventory cannot see; the close then settles by the same gate (its coarse step becomes the owner, or the no-step close purges the unowned). Purge timing follows the undo substrate's need — a future provider with durable history may ride its own flush instead, through the same seam.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Attachment operations, v1** (deferred — ratified 2026-07-27): attach → remove is a clean inverse, but remove-attachment → re-add requires the removed file to survive somewhere (a staging area with a lifecycle — App Support, bounded, its own cleanup rules; possibly shared with 04 ▸ Clipboard's staging). The deferral is the ruling: attachment add/remove registers **no undo step** in v1 (the operations remain, as today, confirmed-or-benign); the staging design pass reopens post-2.0.
|
||||
- **EchoLedger-synthesized foreign undo** (deferred, wishlist — WISHLIST.md item 6): the ledger already classifies foreign diffs for announcements; it could synthesize inverse operations and push foreign steps onto the stack, narrowing the gap to Pro. Real design needed (ordering vs app steps, attribution, user expectations) — not assumed by this doc.
|
||||
- **EchoLedger-synthesized foreign undo** (deferred, wishlist — WISHLIST.md item 6): the ledger already classifies foreign diffs for announcements; it could synthesize inverse operations and push foreign steps onto the stack, narrowing the foreign-change gap (now the deferred journal's charter — strategy/01-git-excision.md ▸ Successors). Real design needed (ordering vs app steps, attribution, user expectations) — not assumed by this doc.
|
||||
|
||||
## Open questions
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Git Operations — Extracted Conclusions
|
||||
|
||||
**RETIRED 2026-08-08** (strategy/01-git-excision.md): a superseded record — the operations these conclusions shaped were built (forward-restore, the safe checkout, housekeeping), shipped in no release, and excised whole; the stack is recoverable at tag `pre-git-excision`, and the forward-restore *principle* — history only ever moves forward — survives as precedent for whatever durable history the deferred foreign-change journal or the ops service one day offers. Everything below is the record of the extraction as it stood.
|
||||
|
||||
**Tier scope: Lanework Pro** (12-editions.md). This doc extracts the settled conclusions from the pathfinder's git-operations analysis (`../../Kanban/AI-ANALYSIS-git-operations.md`, 2026-07-23) so the git milestone has a citable in-repo source. It resolves the design corpus's outstanding tbd — the git-operations doc extraction the Implementation board's root index names as one of two TBDs graduating into work items. The source file catalogued issues and deliberately made no decisions; the decisions were made in 06-history-undo.md and 07-sync-collab.md, and this doc records which of the source's conclusions those docs build on — and which of its leanings later design deliberately settled otherwise. Issue tags (A1, C3, …) are the source file's.
|
||||
|
||||
## The forward-restore model (C3, C9) — the load-bearing extraction
|
||||
|
||||
+5
-5
@@ -18,15 +18,15 @@ Each document covers one aspect of the design. Within each:
|
||||
| [03-board-ui.md](03-board-ui.md) | Board window: layout, lanes, cards, styling, templates |
|
||||
| [04-interactions.md](04-interactions.md) | Selection, drag & drop, keyboard, clipboard, search |
|
||||
| [05-card-window.md](05-card-window.md) | The card window: Markdown preview/edit, attachments |
|
||||
| [06-history-undo.md](06-history-undo.md) | Git-backed undo/redo and history — **Pro tier** |
|
||||
| [07-sync-collab.md](07-sync-collab.md) | Board modes: local-only, git; iCloud Drive warned against — **Pro tier** |
|
||||
| [06-history-undo.md](06-history-undo.md) | Git-backed undo/redo and history — **retired 2026-08-08** (git excised, strategy/01-git-excision.md; Undo routing migrated to 13) |
|
||||
| [07-sync-collab.md](07-sync-collab.md) | Board modes and git-rails sync — **retired 2026-08-08 as written**; successor: the ops-based sync service workstream |
|
||||
| [08-agent-integration.md](08-agent-integration.md) | AI agents as first-class users of the board |
|
||||
| [09-templates.md](09-templates.md) | Board templates: inventory and definition format |
|
||||
| [10-accessibility.md](10-accessibility.md) | VoiceOver, text scaling, visual accommodations |
|
||||
| [11-command-nexus.md](11-command-nexus.md) | The command Nexus — every command and action: bindings, contexts, customizability |
|
||||
| [12-editions.md](12-editions.md) | The tiers (free / Pro subscription / Teams deferred): one-app distribution, entitlement, provider seam, feature matrix |
|
||||
| [13-native-undo.md](13-native-undo.md) | macOS-native undo/redo — the free tier's history substrate |
|
||||
| [14-git-operations.md](14-git-operations.md) | Extracted pathfinder git-operations conclusions: the forward-restore model — **Pro tier** |
|
||||
| [12-editions.md](12-editions.md) | The tiers: one-app distribution, entitlement, provider seam — the git rows are record (pivot 2026-08-07, excision 2026-08-08) |
|
||||
| [13-native-undo.md](13-native-undo.md) | macOS-native undo/redo — the history substrate (sole since 2026-08-08); owns Undo routing |
|
||||
| [14-git-operations.md](14-git-operations.md) | Extracted pathfinder git-operations conclusions — **retired 2026-08-08** (superseded record) |
|
||||
|
||||
## Deferred design iterations
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -245,7 +245,7 @@ struct AgentGuideStoreTests {
|
||||
}
|
||||
|
||||
/// "Left untouched when current or newer" — and untouched means the file is not even opened for
|
||||
/// writing, so its mtime (and, on git boards, the tree) is undisturbed.
|
||||
/// writing, so its mtime is undisturbed.
|
||||
@Test("A current guide is left byte-for-byte alone, mtime included")
|
||||
func currentGuideIsNotRewritten() throws {
|
||||
let fixture = try makeBoard()
|
||||
|
||||
@@ -81,7 +81,7 @@ struct CardBodyWriteGateTests {
|
||||
#expect(!session.isDirty)
|
||||
|
||||
// The pending save was cancelled by the revert rather than firing on a no-op — a write that
|
||||
// landed here would stamp `modified` and mint a commit for nothing.
|
||||
// landed here would stamp `modified` for nothing.
|
||||
await waitUntil { spy.count > 0 }
|
||||
#expect(spy.count == 0)
|
||||
#expect(session.flush() == .unchanged)
|
||||
|
||||
@@ -15,7 +15,8 @@ import Testing
|
||||
/// sheet its own flag here, because the branch-name and commit-identity fields had moved onto it; the
|
||||
/// reversal retired that sheet and brought those fields back into the popover's Git tab, so the
|
||||
/// popover's own open-at-all clause covers every configuration field again and the third disjunct
|
||||
/// went with the surface it described.
|
||||
/// went with the surface it described. (The Git tab itself retired in turn, 2026-08-08 with the git
|
||||
/// excision, `strategy/01-git-excision.md`; the two-flag shape this file tests did not change again.)
|
||||
@MainActor
|
||||
@Suite("caretChordsYield ▸ the caret-chords rule")
|
||||
struct CaretChordTests {
|
||||
|
||||
@@ -110,7 +110,7 @@ private func compose(
|
||||
snapshot: try after.snapshot(),
|
||||
previousSnapshot: try before.snapshot(),
|
||||
agentGuideText: guideText,
|
||||
// Resolved the way a flush resolves it — off the "after" tree, through the committer's own
|
||||
// Resolved the way a flush resolves it — off the "after" tree, through the narrator's own
|
||||
// reader — rather than hand-assembled, for the same reason both snapshots are loaded rather
|
||||
// than built: a map the flush could never produce would prove nothing about the flush.
|
||||
commentTimestamps: ChangeNarrator.commentTimestamps(for: paths, boardRoot: after.root)
|
||||
@@ -834,8 +834,9 @@ struct CommitMessageCommentTests {
|
||||
Data("---\nschema: 1\nkind: comment\nauthor: Ada\ncreated: \(created)\n---\n\(body)\n".utf8)
|
||||
}
|
||||
|
||||
/// **"A commit's comment bullets sort chronologically … never UUID-arbitrary"** (06 ▸ Rules
|
||||
/// ▸ Auto-commit, blessed 2026-07-31).
|
||||
/// **"A commit's comment bullets sort chronologically … never UUID-arbitrary"** — once
|
||||
/// 06-history-undo.md ▸ Rules ▸ Auto-commit's rule (blessed 2026-07-31), now the change
|
||||
/// narrator's (`strategy/01-git-excision.md` ▸ What is harvested).
|
||||
///
|
||||
/// Three comments on one card, each with a *different verb* so the bullets are distinguishable,
|
||||
/// and identities deliberately ordered against their chronology: the earliest comment carries the
|
||||
|
||||
@@ -4,7 +4,7 @@ import Testing
|
||||
|
||||
/// The close flush is an *order*, and an order is only worth stating if something checks it. 02
|
||||
/// § Windows fixes it: the board's card windows and their sessions first, then pending debounced work
|
||||
/// with editor saves before the pending auto-commit, then the registry stamp, then teardown — and
|
||||
/// with editor saves before the board-level flush, then the registry stamp, then teardown — and
|
||||
/// "nothing about this is conditional".
|
||||
///
|
||||
/// So every test here is an assertion about a list. The coordinator is driven with fakes that append
|
||||
@@ -110,7 +110,7 @@ struct CloseFlushCoordinatorTests {
|
||||
"card-session work/card-b",
|
||||
"dismiss work/card-a",
|
||||
"dismiss work/card-b",
|
||||
// Pending debounced work, editor saves before the pending auto-commit.
|
||||
// Pending debounced work, editor saves before the board-level flush.
|
||||
"store-flush work",
|
||||
"editor-flush work",
|
||||
"committer-flush work",
|
||||
|
||||
@@ -142,7 +142,9 @@ struct DedupeRuleTests {
|
||||
#expect(verdict.duplicates.map(\.path) == ["\(Ident.lane2)/\(Dup.lower)"])
|
||||
}
|
||||
|
||||
/// Rung one — the git seam. Base never fills it; pro-m1 does.
|
||||
/// Rung one — once the git seam, which pro-m1 filled and base never did; nothing fills it now
|
||||
/// that app-managed git is excised (`strategy/01-git-excision.md`, 2026-08-08), and the ladder
|
||||
/// falls through to birth date on every board.
|
||||
@Test("Git path history beats birth date")
|
||||
func historyBeatsBirthDate() {
|
||||
let verdict = IntegrityRules.dedupe([
|
||||
@@ -812,7 +814,9 @@ struct DuplicateIdentityRemintTests {
|
||||
}
|
||||
|
||||
/// A rename is a move as far as provenance goes, and **heal-marked**, because the app started it
|
||||
/// on its own — the receipt is what splits it into its own commit on git boards.
|
||||
/// on its own — once the receipt that split it into its own commit on git boards; app-managed
|
||||
/// git is gone now (`strategy/01-git-excision.md`), but the receipt still exists for whatever
|
||||
/// consumes it next.
|
||||
@Test("It drops a heal-marked move receipt")
|
||||
func dropsAHealMarkedReceipt() throws {
|
||||
let (fixture, duplicate) = try board()
|
||||
|
||||
@@ -328,7 +328,8 @@ struct FolderWatcherTests {
|
||||
defer { watcher.stop() }
|
||||
await drainStartupChurn(log)
|
||||
|
||||
// What the app's own auto-commit produces, and what an external `git gc` produces: pure
|
||||
// What an external `git commit` or `git gc` produces — once what the app's own auto-commit
|
||||
// produced too, before it was excised (`strategy/01-git-excision.md`, 2026-08-08): pure
|
||||
// history churn that cannot alter the rendered tree.
|
||||
fixture.write(".git/index", "fake index")
|
||||
fixture.write(".git/objects/ab/cdef", "fake object")
|
||||
|
||||
@@ -129,7 +129,8 @@ struct WriteBoardImageTests {
|
||||
}
|
||||
|
||||
/// The receipt: without one the churn the write produces classifies as somebody else's, and the
|
||||
/// auto-committer would name the commit for a foreign edit.
|
||||
/// announcer — or any future ledger consumer — would attribute the app's own write to a foreign
|
||||
/// editor.
|
||||
@Test("The write leaves a content receipt in the ledger")
|
||||
func dropsAReceipt() throws {
|
||||
let fixture = try makeBoard()
|
||||
@@ -170,7 +171,7 @@ struct GeneratedBackgroundWriteTests {
|
||||
}
|
||||
|
||||
/// One gesture, one bracket — the style batch's rule, which is what makes one reroll one
|
||||
/// app-mediated reload and one commit on a git board, though it writes two files.
|
||||
/// app-mediated reload, though it writes two files.
|
||||
@Test("Two files, one bracket")
|
||||
func oneBracketForBothFiles() throws {
|
||||
let fixture = try makeBoard()
|
||||
|
||||
@@ -9,22 +9,32 @@ import Testing
|
||||
///
|
||||
/// It was written as the **inert-`.git` posture**'s byte-level proof (12-editions.md ▸ The free tier
|
||||
/// and `.git`, as it then read: "any `.git` is inert … the app never reads history, never commits,
|
||||
/// never touches `.git` in any way"). **PIVOT 2026-08-07** retired that posture outright — git left
|
||||
/// the paywall, so a `.git` at a board root is *live* in every tier, detection runs at every open,
|
||||
/// and the auto-committer writes into exactly the directory this file's fixture plants.
|
||||
/// never touches `.git` in any way"). **PIVOT 2026-08-07** retired that posture, if only for a day —
|
||||
/// git left the paywall, so a `.git` at a board root became *live* in every tier, detection ran at
|
||||
/// every open, and an auto-committer wrote into exactly the directory this file's fixture plants.
|
||||
///
|
||||
/// The tests survive the retirement unchanged because none of them ever composed a `HistoryStore` or
|
||||
/// asserted anything about a tier: they drive `BoardWriter` and `BoardLoader` directly, and what they
|
||||
/// pin is that **those layers are git-agnostic**. That claim is not only still true, it is now
|
||||
/// load-bearing in two places the retired posture never reached:
|
||||
/// **EXCISION 2026-08-08** (`strategy/01-git-excision.md`) retired the pivot in turn: app-managed
|
||||
/// git is gone outright. There is no mode detection, no auto-committer, nothing left anywhere under
|
||||
/// `Kanban/` that opens `.git` for writing, on any board, in any tier. What the inert posture claimed
|
||||
/// once, and the pivot scoped down to a single tier for a single day, is again the app's whole git
|
||||
/// story — this time app-wide rather than tier-gated, and for good rather than pending a paywall
|
||||
/// decision. This suite is that restored claim's proof.
|
||||
///
|
||||
/// - **A board nobody added git to** (mode `none`) is the one board the app manages no git for, for
|
||||
/// good — git stays opt-in per board (06 ▸ Rules; 13-native-undo.md's header read through the
|
||||
/// pivot). Nothing but this layer runs on such a board, so this layer's indifference *is* the
|
||||
/// whole promise.
|
||||
/// The tests survived both transitions unchanged because none of them ever composed a `HistoryStore`
|
||||
/// or asserted anything about a tier: they drive `BoardWriter` and `BoardLoader` directly, and what
|
||||
/// they pin is that **those layers are git-agnostic**. That claim was true before the pivot, stayed
|
||||
/// true underneath the pivot's live machinery, and is now — again — the whole of the app's git
|
||||
/// story, load-bearing in two places a tier-gated posture never could have reached uniformly:
|
||||
///
|
||||
/// - **Every board** is a board the app manages no git for. Before the pivot that was true because
|
||||
/// nothing read `.git` at all; during the pivot it was true only for boards nobody had added git
|
||||
/// to (mode `none`); since the excision it is true unconditionally, because there is no mode left
|
||||
/// to be in. Nothing but this layer runs on any board, so this layer's indifference *is* the whole
|
||||
/// promise.
|
||||
/// - **A `.git` the app does not manage** — a repo-nested board's ancestor, a clone dropped inside a
|
||||
/// card folder (which the fixture below plants deliberately) — is left strictly alone by the same
|
||||
/// indifference, whatever the board's own mode or the user's tier.
|
||||
/// indifference, unconditionally now rather than "whatever the board's own mode or the user's
|
||||
/// tier."
|
||||
///
|
||||
/// Two neighbouring claims are pinned elsewhere and are referenced, not repeated: `FolderWatcherTests`
|
||||
/// ▸ ".git filtering" proves the watcher ignores churn under a `.git` at any depth, and
|
||||
|
||||
@@ -7,9 +7,12 @@ import Testing
|
||||
///
|
||||
/// `GitignoreRules` is pure — text in, verdicts out — so this suite is a table and nothing else: no
|
||||
/// filesystem, no board, no store. What it pins is that the app's own matcher is **git's**, because
|
||||
/// the ruling's whole premise is one definition of noise shared with the committer: "on Pro boards
|
||||
/// the same file governs the committer, so ignored noise neither relocates nor commits". A matcher
|
||||
/// that read the file differently from libgit2 would make that one sentence two behaviours.
|
||||
/// `gitignore(5)` is the format's own semantics and a file a user may also be handing to their own
|
||||
/// git — a matcher that read it differently from git itself would make the file lie to one reader or
|
||||
/// the other. (Once doubly true while a committer shared this definition too — "on Pro boards the
|
||||
/// same file governs the committer, so ignored noise neither relocates nor commits" — before
|
||||
/// app-managed git was excised entirely, `strategy/01-git-excision.md`, 2026-08-08; libgit2 is gone,
|
||||
/// but the format's own semantics remain the standard regardless.)
|
||||
///
|
||||
/// The claims are grouped as `gitignore(5)` states them, in its order, plus the seed the app writes
|
||||
/// and the three deliberate divergences the type documents.
|
||||
|
||||
@@ -284,8 +284,9 @@ struct NativeHistoryProviderTests {
|
||||
|
||||
/// A provider with no `NSUndoManager` anywhere in it — which is the point: `BoardUndoManager` is
|
||||
/// tested against *this* rather than against the native stack, because what has to be true is that
|
||||
/// the adapter works for any implementation of the seam (Pro's git provider binds the same protocol
|
||||
/// in pro-m1).
|
||||
/// the adapter works for any implementation of the seam (Pro's git provider once bound the same
|
||||
/// protocol, in pro-m1, before app-managed git was excised entirely,
|
||||
/// `strategy/01-git-excision.md`).
|
||||
@MainActor
|
||||
private final class FakeHistoryProvider: HistoryProviding {
|
||||
|
||||
@@ -637,9 +638,11 @@ struct BoardSessionHistoryTests {
|
||||
// The subscription lapses mid-session — the one thing 12 ▸ The entitlement says must not
|
||||
// disturb a board that is already on screen: "an open board finishes with the provider it
|
||||
// composed". (Its second clause — "the next open composes the native stack over inert
|
||||
// `.git`" — went with PIVOT 2026-08-07: the next open composes the git stack too. What is
|
||||
// pinned here is the recorded fact's immutability, which is what makes the first clause
|
||||
// true whatever a future split gates.)
|
||||
// `.git`" — went with PIVOT 2026-08-07, when for one day the next open could compose a git
|
||||
// stack instead; the 2026-08-08 excision (`strategy/01-git-excision.md`) then restored the
|
||||
// original clause outright — every open composes the native stack over inert `.git` again,
|
||||
// permanently. What is pinned here is the recorded fact's immutability, which is what makes
|
||||
// the first clause true whatever a future split gates.)
|
||||
model.currentTier = { .free }
|
||||
|
||||
#expect(try #require(model.session(for: ref)).tier == .pro)
|
||||
|
||||
@@ -190,8 +190,8 @@ struct InlineRenameWriteTests {
|
||||
let titled = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)")
|
||||
let untouched = try fixture.indexData("\(Ident.lane2)/\(Ident.card3)")
|
||||
|
||||
// An editor opened and dismissed with Return must not stamp `modified` or (on a git board)
|
||||
// mint a commit — the lane-resize rule, for the same reason.
|
||||
// An editor opened and dismissed with Return must not stamp `modified` — the lane-resize
|
||||
// rule, for the same reason.
|
||||
store.transient.beginRename(of: card1, currentTitle: "First")
|
||||
store.commitRename()
|
||||
#expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == titled)
|
||||
@@ -549,7 +549,7 @@ struct LaneReorderWriteTests {
|
||||
let before = try fixture.indexData(Ident.lane1)
|
||||
|
||||
// Index 0 is where lane1 already sits, counted with itself removed — the release must not
|
||||
// stamp `modified` or mint a commit.
|
||||
// stamp `modified`.
|
||||
store.moveLane(lane1, toIndex: 0)
|
||||
|
||||
#expect(try fixture.indexData(Ident.lane1) == before)
|
||||
@@ -806,8 +806,8 @@ struct BoardRenameWriteTests {
|
||||
let store = try BoardStore(rootURL: fixture.root)
|
||||
let titled = try fixture.indexData("")
|
||||
|
||||
// A popover opened and dismissed with Return must not stamp `modified` or (on a git board)
|
||||
// mint a commit — `commitRename`'s rule, for its reason.
|
||||
// A popover opened and dismissed with Return must not stamp `modified` — `commitRename`'s
|
||||
// rule, for its reason.
|
||||
store.renameBoard("Roadmap")
|
||||
#expect(try fixture.indexData("") == titled)
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ struct LaneWidthWriteTests {
|
||||
let atOne = try fixture.indexData(Ident.lane1)
|
||||
|
||||
// A drag that ends where it started, and a stepper pressed against its floor: neither may
|
||||
// stamp `modified` or (on a git board) mint a commit.
|
||||
// stamp `modified`.
|
||||
store.setLaneWidth(ItemID(rawValue: Ident.lane2), units: 3)
|
||||
#expect(try fixture.indexData(Ident.lane2) == atThree)
|
||||
|
||||
|
||||
@@ -559,7 +559,7 @@ struct LooseFileStoreTests {
|
||||
#expect(!fixture.exists("\(cardPath)/notes.txt"))
|
||||
#expect(store.banners.losses.map(\.message) == ["Moved 'notes.txt' into attachments — 'Fix login'"])
|
||||
#expect(store.banners.oneShots.isEmpty)
|
||||
// One gesture, one bracket — one app-mediated reload and, on git boards, one commit.
|
||||
// One gesture, one bracket — one app-mediated reload.
|
||||
#expect(brackets.begins == 1)
|
||||
}
|
||||
|
||||
|
||||
@@ -202,8 +202,8 @@ struct StyleWriteTests {
|
||||
let log = BracketLog()
|
||||
log.attach(to: store)
|
||||
|
||||
// "Choosing a well applies to the whole selection — one gesture, one commit on git boards"
|
||||
// (03 § Styling ▸ Controls): the churn has to round back as ONE app-mediated reload.
|
||||
// "Choosing a well applies to the whole selection — one gesture, one write" (03 § Styling ▸
|
||||
// Controls): the churn has to round back as ONE app-mediated reload.
|
||||
store.applyStyle(to: .items([card1, card2]), background: .set("light-cayenne"))
|
||||
|
||||
#expect(log.begins == 1)
|
||||
@@ -222,8 +222,8 @@ struct StyleWriteTests {
|
||||
let untouchedCard = try fixture.indexData("\(Ident.lane1)/\(Ident.card1)")
|
||||
|
||||
// `card1` is already `{color: fern}` and `card2` has no background at all: only the second file may
|
||||
// move. A well clicked twice must not stamp `modified` or mint a commit on what was already
|
||||
// right (`setLaneWidth`'s rule).
|
||||
// move. A well clicked twice must not stamp `modified` on what was already right
|
||||
// (`setLaneWidth`'s rule).
|
||||
store.applyStyle(to: .items([card1, card2]), background: .set("fern"))
|
||||
|
||||
#expect(try fixture.indexData("\(Ident.lane1)/\(Ident.card1)") == untouchedCard)
|
||||
|
||||
@@ -51,7 +51,7 @@ Lanework is in early development. This list tracks what has actually shipped and
|
||||
|
||||
- **Customizable toolbars** — both windows carry a real macOS toolbar: right-click ▸ Customize Toolbar…, drag to rearrange, the system overflow, and the Icon and Text / Icon Only / Text Only display options, with your arrangement remembered across launches. They are pure enhancement — every item is a menu command with a shortcut, so removing all of them costs you nothing but a click. The board ships with the search field alone, trailing, and offers New Card, New Lane, Zoom In, Zoom Out, Undo, Redo and Show Trash in the palette (Undo and Redo validate exactly as the Edit menu's rows do, and keep static labels because the menu's titles rewrite themselves); the board popover deliberately has no item, since the window-title chevron is its home. Take the search field out and ⌘F still summons search — the field appears in a strip just under the title bar and stays until the search clears, keeping the keyboard while you type. The card window ships Edit Body · Raw Source · Add Attachment, the first two as toggles showing their on-state, with Edit Body disabling while raw source is up and Add Attachment live in every mode.
|
||||
|
||||
- **Undo** — ⌘Z and ⇧⌘Z are native macOS undo, per board: one stack owned by the board's session and shared by every window over it, so a card window's ⌘Z crosses the same step the board window's does, and another board's never does. Every app-mediated mutation registers an inverse at the write boundary — create, move, reorder, rename, restyle, resize, delete, and an Edit session's whole run of saves — with a restore registering as the ordinary move it is — one gesture to one step, named in the app's own vocabulary so the Edit menu reads "Undo Move 3 Cards" and the toolbar's twins light up and dim with it. Undoing is a real write, never an in-memory revert: it goes through the same atomic writer, echoes back through the watcher, and refreshes every window. Because the app is not the only writer, each step re-checks its target the moment you press ⌘Z — field by field, against what its own write left — and a step the disk has moved past is **skipped rather than applied**, with a quiet row saying which item changed outside Lanework, while ⌘Z falls through to the next step; a step that merely failed to write (a full disk, an unplugged volume) stays put to be retried. Permanent deletion and the duplicate-id repair are deliberately outside it — the confirmation is the safety — attachment add and remove register nothing in v1, and foreign edits never join the stack. The read-only lock disables Undo and Redo with every other mutating command and gives them back, stack intact, when it clears. The stack lives with the session and dies at close, standard macOS behaviour. This stack runs on every board without app-managed git; a git board binds git behind the same seam instead, without changing a keystroke — see "Undo as forward commits" below.
|
||||
- **Undo** — ⌘Z and ⇧⌘Z are native macOS undo, per board: one stack owned by the board's session and shared by every window over it, so a card window's ⌘Z crosses the same step the board window's does, and another board's never does. Every app-mediated mutation registers an inverse at the write boundary — create, move, reorder, rename, restyle, resize, delete, and an Edit session's whole run of saves — with a restore registering as the ordinary move it is — one gesture to one step, named in the app's own vocabulary so the Edit menu reads "Undo Move 3 Cards" and the toolbar's twins light up and dim with it. Undoing is a real write, never an in-memory revert: it goes through the same atomic writer, echoes back through the watcher, and refreshes every window. Because the app is not the only writer, each step re-checks its target the moment you press ⌘Z — field by field, against what its own write left — and a step the disk has moved past is **skipped rather than applied**, with a quiet row saying which item changed outside Lanework, while ⌘Z falls through to the next step; a step that merely failed to write (a full disk, an unplugged volume) stays put to be retried. Permanent deletion and the duplicate-id repair are deliberately outside it — the confirmation is the safety — attachment add and remove register nothing in v1, and foreign edits never join the stack. The read-only lock disables Undo and Redo with every other mutating command and gives them back, stack intact, when it clears. The stack lives with the session and dies at close, standard macOS behaviour. This stack is the app's only history substrate, and the provider seam it binds through is kept deliberately open — a future provider (the deferred foreign-change journal) re-binds without re-plumbing.
|
||||
|
||||
- **The agent guide** — every board root carries a `CLAUDE.md` the app writes and keeps current: a condensed, agent-facing rendition of the schema — the folder layout, ordering arithmetic, creating and moving cards, the `.trash/` convention, `attachments/`, `modified-by` self-stamping, the colour and icon palettes, and the git etiquette — so any file-capable agent dropped into the folder already knows how to work the board. It is app-owned and version-gated by a marker in its first line: rewritten when missing or older, left byte-for-byte alone when current or newer, and re-checked on every reload, so a guide deleted or rolled back from outside heals by itself. A `CLAUDE.md` that isn't the app's is never clobbered — it moves to `CLAUDE.user.md` (the user's own extension point, which the app otherwise never touches), and if that name is taken the app simply doesn't write a guide. A symlink or folder wearing the name is moved aside — Finder-style, never destroyed, with a quiet row naming where it went — because the app owns that name; and a board on a read-only volume is skipped in silence: the guide is a courtesy and never an interruption.
|
||||
|
||||
@@ -59,14 +59,9 @@ Lanework is in early development. This list tracks what has actually shipped and
|
||||
|
||||
- **App identity — icon, versioning, About** — the app carries its three-lane glyph icon and a real About window: icon, copyright, version and build stamped at build time from git (`CFBundleVersion` = commit count, plus `BuildDate` and `BuildHash` in the Info.plist — never a hardcoded string), the version line opening the bundled end-user changelog, and the ISC license one link away.
|
||||
|
||||
- **Tiers, and the pivot away from gating git** — one app, one download, one on-disk format. **Git integration is tier-free** (DESIGN/12-editions.md ▸ PIVOT 2026-08-07 — git leaves the paywall): opt-in init and adoption, git-backed undo and history surfaces, and branches — everything below under "Git integration", "Auto-commit", "Undo as forward commits", "Branches and commit identity" and "A card's History" — run the same on any board that carries a repository, whichever tier the eventual base/Pro split turns out to mean; remotes and push/pull are still pro-m2's remaining work. The subscription machinery this used to gate — the entitlement (a **local read, never a network call**: StoreKit's own signed on-device transaction store, reduced to two cached facts and resolved by a pure function at board-session composition, with offline grace resolving toward the paying user), the provider seam, and the storefront — stays built and tested but **dormant**: Settings carries no Pro section, and no surface in the app names or sells Lanework Pro.
|
||||
- **Tiers** — one app, one download, one on-disk format, and **the complete Mac experience is free**: nothing on the board, in the format, or in agent legibility is gated. What Pro will eventually mean is the board beyond the Mac — the iPhone companion and the ops-based sync service, its own workstream — never a Mac feature. The subscription machinery built ahead of that split — the entitlement (a **local read, never a network call**: StoreKit's own signed on-device transaction store, reduced to two cached facts and resolved by a pure function at board-session composition, with offline grace resolving toward the paying user), the provider seam, and the storefront — stays built and tested but **dormant**: Settings carries no Pro section, and no surface in the app names or sells Lanework Pro.
|
||||
|
||||
- **Git integration** — git is **opt-in per board and never silent**. A board's mode is detected freshly at every open, nearest-`.git`-wins: a `.git` at the board root means git mode, a `.git` only further up means the board lives inside somebody else's repository, and neither means no git at all. Detection is an open-time fact by design — a `git init` run in a terminal under an open board takes effect the next time you open it, and nothing watches for a repository appearing. **Adoption is not initialization**: a board whose folder already holds a repository (you cloned it, or you ran `git init` yourself) simply opens in git mode, with no dialog and no adoption step — the repository's presence *is* the opt-in, which is how a second machine joins a shared board. **Add Git** in the board popover is the only thing in the app that ever creates one: it initializes a repository in the board's folder and immediately commits the whole tree as "Initial board state", so the board is protected from the moment git exists, and it flips the open board into git mode on the spot. A board inside an existing repository is left strictly alone — no nested repository, no commits into your project — and the popover says so in plain words instead of showing a disabled button. The git client is **bundled** (libgit2, in-process via SwiftGitX): nothing here shells out, and none of it needs git installed. Git history also settles duplicate-id collisions on git boards — of two folders claiming one identity, the path that entered history first wins. Add Git also writes a minimal `.gitignore` — one line, `.DS_Store` — if the board hasn't got one, and it lands *inside* that first commit, so the Finder's droppings never enter history in the first place. That is the app's only word on the file: a board that already has one is left byte for byte alone, an adopted repository is seeded nothing at all, and nothing ever edits, appends to or restores it afterwards — it's yours from then on. **Repo growth is accepted, and never repaired by forgetting**: deleting a card removes it from the board but never from history, every version of every attachment stays reachable in any git client, and there is no compaction anywhere in the app because compaction would mean rewriting history. What the app does do is **safe housekeeping**: a board whose repository has accumulated enough loose objects gets them packed once, in the background, a while after opening — the same objects in a more compact form, with not one commit, ref, or byte of content altered, and every loose file proved to be readable out of the new pack before it is removed. It never runs beside a commit, never runs while another program holds the repository, and if it doesn't run it simply doesn't: there is no retry and nothing to see.
|
||||
|
||||
- **Auto-commit** — on a git board **every settled change becomes a commit**, debounced a couple of seconds past the churn of a drag or a burst of typing, so one gesture is one commit rather than forty. Agent and hand edits ride the same debounce, so work done in a terminal or by an agent is committed, attributed and undoable exactly like your own. What commits is the **tree**, not the app's model: strays, `CLAUDE.md` and anything else `git status` shows go in too (your `.gitignore` is respected), so a board is never left quietly dirty. **Commit authorship is structural**: the app knows, file by file, what it wrote and what it didn't, so your changes are authored as you while changes from outside are authored as `Lanework External <[email protected]>` — and when every file in an outside batch carries the same `modified-by:` stamp, that batch is authored as that agent instead. A window holding both kinds is split into two commits rather than mixed, outside changes first, and a scheduled repair's files commit separately again. Identity comes from the repository's own `.git/config` when it names one, and otherwise from your macOS account name and machine. **The card editor is the exception, deliberately**: its 700 ms saves keep the file crash-safe but stay uncommitted for the length of a session, and the body lands as exactly one commit when you leave Edit — so a lane move made while you're typing commits the move and steps around the card you're in. Closing a board window or quitting flushes everything pending before teardown, and a board opened with uncommitted changes commits them through the same engine. When another program holds the repository's index, the committer waits, retries briefly, and then simply tries again at the next quiet moment — never an error, and the lock is never removed, because it isn't the app's. A merge, rebase, cherry-pick or detached HEAD left by outside-the-app git **pauses** committing entirely rather than writing into a state the app didn't create; your edits keep landing on disk and commit as one batch when you've finished up in whichever tool started it. **What each commit says is composed, not templated**: at commit time the app diffs the board as HEAD has it against the board as it is now — never by watching what you clicked — and writes what actually happened. "Move card 'Fix login' to Doing". "Rename lane 'Todo' → 'Doing'". "Relabel card 'Fix login'". Items are matched by identity across the whole board, so a card dragged between lanes reads as a move rather than a deletion beside an addition, and a folder moved by hand in the Finder reads exactly the same. Several changes of one kind fold into one line with the destination kept ("Move 3 cards to Done"); a genuinely mixed batch reads "Update board" with every event listed underneath, so `git log --oneline` stays scannable and the full message stays complete. Deleting a lane says "Delete lane 'Todo'" with its cards as detail rather than burying the event in a count. The trash is told apart by shape alone: into `.trash/` is "Delete card 'X'", back out is "Restore card 'X'", and gone for good is "Permanently delete card 'X'" — so the log distinguishes moved-to-trash from gone-forever without being told which gesture you used. Files the board model doesn't cover are described too rather than silently swept in: a changed agent guide reads "Update agent guide (v7)", a comment gets its own verbs off its path shape — "Comment on 'Fix login'", "Edit comment on…", "Delete comment on…", and the quiet "Draft comment on…" for the composer's slow saves — and any other stray reads "Update 'notes.txt'". Bookkeeping stays out of it — a bumped timestamp, a rank rescale, or a backfilled key composes nothing. And because the origin of a change lives in the author field, **an agent's commit reads in exactly the same words as your own**. Still ahead in pro-m2: remotes and push/pull.
|
||||
- **Undo as forward commits** — on a git board ⌘Z and ⇧⌘Z stop being an in-memory stack and become the commit trail itself. There is no stored stack anywhere: **the stack *is* HEAD's first-parent ancestry**, re-read from the repository, so it survives relaunch for free and nothing beside the repo can ever drift from it. **A restore is a new commit, never a rewind** — no reset, no force, no rewritten history: ⌘Z materializes the earlier state and commits it as "Undo: Move card 'Fix login' to Doing", ⇧⌘Z as "Redo: …", and every commit you have ever made stays exactly where it was, inspectable in any git client. Only the *difference* is written, so a card you happen to be editing that the change never touched is simply left alone. Because the app is not the only writer, the stack re-reads HEAD before every keystroke: an agent that committed its own work in the last twenty minutes becomes the top of the stack, so ⌘Z steps back exactly one commit and can never silently swallow somebody else's session — and any commit arriving from anywhere clears redo, the classic rule. The app's own repairs are **transparent**: a heal commit is never a step and is never reverted by one, so a ⌘Z run walks past it instead of fighting the healer. Anything still pending commits *before* the restore does, so both versions of what you undid exist in the trail. When the change would land on a card you have open in Edit or Raw Source, the restore stops and asks — **Save All**, **Discard**, or **Cancel** — rather than silently committing text you hadn't saved or quietly writing it back a moment later; a raw buffer that won't validate cancels the whole thing and puts you in front of the window that refused. Undo is board-local, disabled while an outside-the-app merge or rebase has the repository paused, and absent altogether on boards the app manages no git for — where the Edit menu's rows and the toolbar's twins simply dim. Adding git to an open board turns it on there and then.
|
||||
- **Branches and commit identity** — the board popover's git section is where a git board's branches live: the current branch is itself the picker, and beside the local branches it offers create-and-switch, which starts the new branch at the commit you are on. **Switching is never silent.** If any card window is holding unsaved keystrokes, an open Edit session whose crash-safe saves no commit has yet, or an open raw-source buffer, the switch stops and asks — **Save All**, **Discard**, or **Cancel** — for *every* open window, not just the ones the checkout would touch, because an unapplied raw buffer would otherwise write the whole pre-switch file onto the new branch's card later on. Save All ends each session with its normal commit and applies each raw buffer; a buffer that won't validate cancels the whole switch and puts you in front of the window that refused, nothing half-switched. Discard puts both the buffers and their uncommitted on-disk saves back to the last commit. With the sessions settled the pending auto-commit flushes onto the branch you are **leaving**, so the checkout runs on a genuinely settled tree and cannot fail dirty — and the checkout itself is git's *safe* one, never a force: work the app somehow didn't settle refuses the switch rather than being overwritten. The switch is bracketed like every wholesale operation — the watcher suspended, one full reload at the end, an in-progress row saying "Switching to 'main'…" — and if that final reload fails, the board locks read-only until a reload succeeds rather than letting you edit a snapshot of the branch you just left. Undo and redo do not survive the switch: the stack is discarded and reseeded from the new branch's own history, with redo empty, because replaying a restore from the old branch onto the new one would be wrong. When another program holds the repository's index, the switch waits and retries quietly, the row changing to say it is waiting for another writer's git lock and eventually naming the lock file — never a dialog, and the lock is never removed, because it isn't the app's. **An interrupted switch cleans up after itself**: before touching the repository the app writes down what it is about to do — in its own state, never in your board and never inside `.git` — so a crash or an unplugged volume mid-switch is recognized at the next open, rolled back to the state you were in, and announced ("A branch switch was interrupted — the previous state is restored"); an unfinished merge or rebase that *isn't* the app's is still left strictly alone. Beneath the branches sit the **commit-identity** fields: what you type there is written to the repository's own `.git/config`, so the setting is the file — portable to any git client, per board, and what the next commit is signed with. Leave a field empty and it shows the derived default (your account name, and `you@yourmac`) as a placeholder rather than filling it in, because a value the app wrote there would quietly outrank your own global git config for that board. A config edited from outside while the popover is open refreshes the fields you aren't typing in and leaves the one you are alone.
|
||||
- **A card's History** — the card window's sidebar gains a read-only **History** section on git boards: every commit that touched that card, newest first, each row its own semantic subject over a relative date and the author who made it — so an agent's work and your own read as one story ("Move card 'Fix login' to Doing · 2 days ago · Claude"). It follows the card by identity rather than by path, so moving between lanes — or into the trash and back — keeps one continuous trail. Read-only in this version: restoring a single old version stays a git-client job. The section is simply absent on boards without app-managed git — no placeholder, no greyed-out promise.
|
||||
- **Git-friendly by construction** — the app never runs git, and the format never needs it to be versionable: one folder per item and one file per card, stable UUID folder names as permanent identity, byte-faithful rewrites that never churn what you didn't change, and a seeded one-line `.gitignore` keeping Finder droppings out of diffs — so a board versions cleanly under any tooling you already use (`git init` it, clone it, review board changes in pull requests) while the app reads along live and stays strictly out of `.git`. The app-managed git stack that once shipped here — auto-commit, undo as forward commits, branches, a card's History — was built and then excised before launch (strategy/01-git-excision.md, ruled 2026-08-08; the pre-excision state is tagged `pre-git-excision`).
|
||||
|
||||
- **Lanework for iPhone (in development)** — a companion iOS app, second product rather than second edition (`dev.rzen.indie.KanbanMobile`), whose boards are the same `.kanban` folders in an iCloud Drive container (`iCloud.dev.rzen.indie.Kanban` — a folder the Mac's Finder shows as "Lanework", so the Mac app opens the same boards through the ordinary open panel today and can adopt the container natively later with no migration). It compiles the Mac app's storage engine *verbatim* — same loader, same writer, same byte-fidelity contract, so the two apps cannot drift on the format — and drives it from an `NSMetadataQuery` observer instead of FSEvents. The MVP surface: a Boards tab (boards → lanes → cards → card editor, with per-item download/materialization states honestly rendered), swipe actions to move a card between lanes or send it to `.trash/`, a card editor for title, body, and the typed style attributes (icon, icon color, background), and a Settings tab with an About section for the app's own changelog and license (IndieAbout). No git on the phone — the change signal is the container itself.
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ Scope: the framing document for the strategy effort. It records the strategic go
|
||||
|
||||
Drafted: 2026-08-01
|
||||
|
||||
**Note 2026-08-08:** app-managed git is excised (ruled — `01-git-excision.md`, executed the same day), which moots this charter's git-premised lines: the pro-m1 launch coupling (there is no git milestone to couple to) and any framing of Pro as selling git history. The 2026-08-07 tier-strategy discussion points toward re-anchoring on free = the complete Mac experience, Pro = the board beyond the Mac (iPhone + ops-based sync service); that re-ruling of the anchors is the user's to make and has not been folded in — read the anchors and tensions below through this note until it is.
|
||||
|
||||
## The strategic goal
|
||||
|
||||
A long-term, reliably sustainable business with a clear path to growth.
|
||||
|
||||
Reference in New Issue
Block a user