From 25d2513ccc3cc098240548b036931e9ebb856449 Mon Sep 17 00:00:00 2001 From: rzen Date: Fri, 31 Jul 2026 19:44:51 -0400 Subject: [PATCH] Repo-nested boards bind native undo in every tier Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY --- DESIGN/03-board-ui.md | 2 +- DESIGN/06-history-undo.md | 6 +++--- DESIGN/07-sync-collab.md | 4 ++-- DESIGN/09-templates.md | 2 +- DESIGN/12-editions.md | 4 ++-- DESIGN/13-native-undo.md | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/DESIGN/03-board-ui.md b/DESIGN/03-board-ui.md index b923986..539dddd 100644 --- a/DESIGN/03-board-ui.md +++ b/DESIGN/03-board-ui.md @@ -12,7 +12,7 @@ The board window: layout, lanes, cards, and styling. Interaction mechanics (sele Toolbars are **pure enhancement**: every function they host already has a menu item + shortcut (04-interactions.md's contract), so nothing below is anyone's only path. Both windows' toolbars are **user-customizable, macOS-native** (right-click ▸ Customize Toolbar…, drag to rearrange, system overflow and icon/text display options) — the sets below are shipped defaults, not verdicts. Toolbar item labels match their menu-item titles exactly (Show Trash, Edit Body, Raw Source, …), minus any trailing ellipsis (macOS convention: "Add Attachment…" labels as Add Attachment) — one vocabulary everywhere, and the customize palette self-documents against the menus. One exception: the Undo/Redo toolbar items keep static labels — NSUndoManager rewrites their menu titles dynamically ("Undo Move Card…", 04-interactions.md ▸ Configurable bindings), which a toolbar label doesn't track. -- **Board window default: the search field, nothing else** — trailing, the one default item; the titlebar stays clean. ⌘F always summons search: with the field removed from the toolbar, invoking it surfaces the field transiently until the search clears. **Catalog** (available via Customize): New Card, New Lane, Undo, Redo (the pair disabled only on repo-nested boards, under locks, and on empty stacks — re-ruled 2026-07-31: the provider follows the board, so gitless boards bind 13-native-undo.md's native stack in **every** tier and Pro git boards bind the git provider — 06-history-undo.md), Show Trash (toggle state matching the View menu checkmark). The board popover deliberately has **no toolbar item** — the window-title widget is its committed home (below), and a second entry would muddy it. +- **Board window default: the search field, nothing else** — trailing, the one default item; the titlebar stays clean. ⌘F always summons search: with the field removed from the toolbar, invoking it surfaces the field transiently until the search clears. **Catalog** (available via Customize): New Card, New Lane, Undo, Redo (the pair disabled only under locks and on empty stacks — re-ruled 2026-07-31, twice: 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 and Pro git boards bind the git provider — 06-history-undo.md), Show Trash (toggle state matching the View menu checkmark). The board popover deliberately has **no toolbar item** — the window-title widget is its committed home (below), and a second entry would muddy it. - **Card window default: Edit Body · Raw Source · Add Attachment** — the window's three committed functions, all discoverable from its toolbar; the catalog is the same trio. Edit Body is a **single toggle button** (on-state in Edit — mirroring the View ▸ Edit Body checkmark and the ⌘E/Return/Escape grammar; the pathfinder's segmented Preview|Edit is retired). Raw Source is likewise a toggle showing on-state; while source mode is active, Edit Body disables (Cancel/Apply own the exits — 05-card-window.md). Add Attachment stays enabled in every mode — attachment operations never touch `index.md`, so they're safe alongside a raw edit (the sidebar's feedback returns on exit). ## Lane diff --git a/DESIGN/06-history-undo.md b/DESIGN/06-history-undo.md index 6c1887d..c4e1715 100644 --- a/DESIGN/06-history-undo.md +++ b/DESIGN/06-history-undo.md @@ -2,7 +2,7 @@ **Tier scope: Lanework Pro** (12-editions.md). This doc is the git HistoryProvider; the free tier ships mode:none only, with macOS-native undo (13-native-undo.md) and the inert-`.git` posture (12). The Undo routing section below is 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 settings sheet — 03-board-ui.md; see 07-sync-collab.md's mode progression). A board without git binds the **native undo stack in every tier** (re-ruled 2026-07-31 — the provider follows the board, 13-native-undo.md; formerly no-undo under Pro, which made upgrading remove undo from mode-none boards); repo-nested boards remain the one no-undo case (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. +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 settings sheet — 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. ## Rules @@ -11,7 +11,7 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board - **Detection is nearest-`.git`-wins**, checked at every board open: `.git` at the board root → git mode (adoption above); no `.git` at the root but one at any ancestor → repo-nested (below); neither → mode none. **Denial is not absence** (ruled 2026-07-31): the ancestor walk crosses paths above the board's sandbox grant, and a check the sandbox *refuses* (EACCES/EPERM) must never read as "no repo there" — detection distinguishes **clean none** (every ancestor answered not-found) from **unverifiable** (a check was denied); add-git is offered only on clean none, and unverifiable takes the repo-nested posture (conservative — the popover explains rather than offers). As hardening, add-git's create re-runs full detection and refuses unless it reads clean none, so the forbidden nested init is impossible even on a raced or stale read. Whether the shipped sandbox actually denies ancestor stats is an open empirical question (manual-verification list: a board deep inside an ungranted repo, and an ordinary board under an ungranted parent) — if it denies everywhere, every board would read unverifiable and this posture needs a data-informed revisit. A board can therefore change mode between opens (e.g. the user ran `git init` in a terminal) — the app just reflects what it finds. **Open-time only, deliberately — for *discovery***: a `git init` under an open mode-none board takes effect at the next open — the running session keeps its mode, and the watcher does not scan for `.git` appearing (no mid-session mode flips from watching; stated here so it isn't rediscovered as a bug). The one deliberate mid-session transition is the app's own **add-git** (Opt-in init above): clicking it flips the open board into git mode immediately — the settings sheet flows straight into the git sections, the first auto-commit follows — the rule forbids *discovered* flips, never commanded ones. - **A `.git` that isn't a valid repository still reads as git mode — and fails loudly** (ruled 2026-07-31): detection is presence-shaped (any root `.git` entry, directory or worktree/submodule pointer file), so a corrupt or unopenable repo never falls to mode none — Add Git is never offered against an existing `.git`, whatever its condition (init into a repairable repo is exactly the never-mutate hazard). The board itself loads and edits normally — files are the board — but the failure is **loud**: a standing breakage-class banner at detection ("This board's git repository can't be read — history is paused; Lanework leaves the repository untouched"), announced per 10-accessibility.md, with the whole git surface paused (the abnormal-states posture below) and the popover's git section naming the state; the banner clears when a later open or reload finds the repo readable. Never a silent placeholder discovered only in the popover. - **Abnormal repo states** (settled; adoption never assumes a tidy clone): an **unborn HEAD** (`git init`, no commits yet) is normal git mode — the first auto-commit creates the root commit on the branch HEAD names, and the undo trail simply starts empty. **The root commit has its own subject** (settled): whenever the app creates a repo's first commit — immediately on the app's own add-git (init doesn't wait for the debounce; the board is protected from the moment git exists), or at the first settled change on an adopted unborn repo — it commits the whole tree as **"Initial board state"**, never a folded diff-from-empty: there is no last-committed snapshot to diff against, and forty Adds would bury the event. **The root commit is never split and is user-authored** (blessed 2026-07-31): it is a baseline, not a change-set — the event it records is the user's act of putting the board under git, adopted unborn repos' unwitnessed files included. A **detached HEAD**, or an **in-progress merge/rebase/cherry-pick** left by outside-the-app git (`MERGE_HEAD`, `rebase-merge`/`rebase-apply`, `CHERRY_PICK_HEAD` — pause states that load fine on a clean tree and are otherwise invisible), instead **pauses the git surface honestly — the *whole* surface, remote half included** (settled): auto-commit holds (the auth-pause posture, 07-sync-collab.md — pause, badge, explain, never hammer), Undo/Redo and the branch controls disable, **and Pull, Push, and push-on-commit hold with them** — with auto-commit held, 07's clean-tree-by-pull-time invariant is false, and a pull's rebase (or a rejected push's fetch→rebase→push) would run against a dirty tree carrying uncommitted edits, precisely what flush-before-overwrite exists to prevent; the ahead/behind badge keeps counting (a fetch is a read), and the popover's git section names the state plainly ("HEAD is detached — commits would belong to no branch"; "a merge is in progress") and says resolving it belongs to the tool that created it. Edits keep landing on disk — files are the board — and commit as one settled batch when the state clears. The app **never mutates repo state it didn't create** (no auto branch-at-HEAD, no `merge --abort`); the check runs at open and again before every flush — and, because a terminal's cleanup moves only files under `.git`, which the watcher never delivers, **a standing pause re-reads the repository state every 15 s** (blessed 2026-07-31; injectable cadence, only while paused, a handful of stats — not a retry, nothing is attempted) — so finishing the operation in a terminal resumes the pipeline without ceremony. **The one exemption is the app's own leftovers** (settled): every bracketed operation stamps its intent app-side (per-board registry) before touching the repo, so an interrupted app-run rebase or checkout is recognizable as Lanework's — finding a pause state with a matching stamp, the app **aborts its own unfinished operation** to restore the pre-operation state and says so via banner ("a branch switch was interrupted — the previous state is restored"), then clears the stamp — **on success only; a failed abort keeps the stamp** (blessed 2026-07-31): the stamp is the sole evidence the leftover is the app's, and clearing it on failure would demote the leftover to somebody-else's forever — the pause would then send the user to "the tool that created it," which was this app. Kept, the next open or paused-state re-read recognizes it and retries; recovery converges instead of orphaning. Abort discards nothing: fetched commits stay fetched, local commits are restored — the rebase's own no-loss accounting. Without a matching stamp the leftover is outside git's, and the pause-and-defer stance above holds unchanged. -- **Boards nested inside an existing repository are left strictly alone** — git cannot be added to them (no nested repo, no commits into the user's repo), so they get **no undo** (settled; no app-managed undo journal, which would violate self-containment). The board popover's git section must say so honestly: not a hidden "add git" but a short explanation ("this board lives inside a repository; Lanework leaves it to that repository") — the option is absent because it *can't* apply, and the UI should teach that rather than look broken. +- **Boards nested inside an existing repository are left strictly alone** — git cannot be added to them (no nested repo, no commits into the user's repo), so they get **no app-managed history — while the native session stack still serves ⌘Z there in every tier** (re-ruled 2026-07-31, retiring the old no-undo residue: 13's stack is memory-only and journal-free, so self-containment holds; what repo-nested denies is git, never undo). The board popover's git section must say so honestly: not a hidden "add git" but a short explanation ("this board lives inside a repository; Lanework leaves it to that repository") — the option is absent because it *can't* apply, and the UI should teach that rather than look broken. - **Auto-commit**: every settled change (debounced past drag/typing churn) commits with a descriptive message ("Move card 'Fix login' to Doing"). Board history sees **card-window sessions, not gestures** (re-ruled 2026-07-31, widening the Edit-sessions-not-save-ticks rule — 13-native-undo.md's session-coarsening model, applied to the commit substrate): while a card's window is open, everything happening inside it — the body editor's ~700 ms crash-safe disk saves, comment posts and deletes, draft-save cadence, sidebar changes — stays **uncommitted**, and the committer **stages around the whole open card folder** (the former Edit-session stage-around, widened; comments included); **window close flushes the session as one commit** ("Update card 'Fix login'"-shaped, the composer folding the card-scoped diff, body bullets carrying the events) — granular window activity never litters history. The EchoLedger's two-commit split still applies at close when the held window mixes foreign changes to that card with the app's own. Settled-tree events that cannot wait — a pull's flush-before-overwrite — commit the session's on-disk state as-is (a mechanical exception; 07-sync-collab.md's same-card signpost covers the visible half); branch switch instead gates on explicit save-or-discard (Branch switching below). The cadence constraint below demands batching at least this coarse. **Board-window close and app quit flush the pipeline** — any pending editor save (05-card-window.md), then open card-window sessions, then the pending auto-commit — before teardown; nothing settled is ever left unsaved or uncommitted by closing. - **Flush-before-overwrite**: before an app write overwrites on-disk state that differs from the last-loaded snapshot (an uncommitted external change — e.g. an agent's body rewrite racing the card editor's debounced save, 05-card-window.md), the pending auto-commit is flushed so the external version enters history first. "Both versions exist as commits" is thereby a guarantee, not a likelihood — **as strong as the watcher's knowledge** (bounded, blessed 2026-07-31): the gate fires on *known* foreign changes, learned from landed reloads, so a foreign write still inside the watcher debounce can be overwritten unflushed; the window is the debounce (~200 ms), the disk-level outcome inside it is 05's last-writer-wins — the same ruling at history granularity — and `index.md` writes compose over fresh disk bytes anyway, so wholesale loss needs a body-save or raw-Apply race. A **failed reload counts as a foreign change** (can't know, so protect). A pre-write disk compare was weighed and declined — a read per write to buy back the debounce corner. **The flush commits synchronously on the main actor** (blessed 2026-07-31): the write path it must precede is synchronous, so the ordering guarantee can only be kept by a synchronous commit — 02's hang-avoidance doctrine and this guarantee genuinely conflict here, and the guarantee wins, being rare (the known-foreign window only), bounded (one stage-and-commit plus the composer's HEAD-tree materialization — a few board-sized walks, accepted so exactly the commit that preserves somebody else's version keeps its full semantic subject), and load-bearing (the alternative loses a version no commit protects). One attempt, no lock backoff on this path — a held `index.lock` falls through to the next quiet debounce. The same flush settles the tree before a pull runs (07-sync-collab.md). - **Cadence constraint** (agreed): the auto-commit cadence must not make the history of a remote-shared board unbearable — one commit per drag is fine for a local undo trail but noisy as a shared log. The debounce/batching design here must serve both consumers; the push/pull side is settled in 07-sync-collab.md (optional push-on-commit, automatic fetch-rebase-push on rejected pushes). @@ -25,7 +25,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 — no-git and repo-nested included; "no undo/redo" above means board history, not typing. **Control-class text fields route the same way** (settled): the search field (04-interactions.md ▸ Search), the popover's rename field, and the settings sheet's text fields (commit identity, credentials, remote URL) 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 — git undo here (disabled on boards without it). **No fall-through**: exhausting a focused editor's — or the window's — stack beeps; it never reaches board history. +**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 settings sheet's text fields (commit identity, credentials, remote URL) 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. ## Commit messages diff --git a/DESIGN/07-sync-collab.md b/DESIGN/07-sync-collab.md index 4a9aa73..aabc288 100644 --- a/DESIGN/07-sync-collab.md +++ b/DESIGN/07-sync-collab.md @@ -1,6 +1,6 @@ # Sync & Collaboration -**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 no-undo caveat is superseded in the free tier by native undo (13-native-undo.md). Teams adds tracker-backed sync behind the same seam (deferred). +**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: @@ -14,7 +14,7 @@ A board may be created plain — **without any git repository** (a pivot from th ## Mode: none (local-only) -Plain folders on local disk. **No git repository at all** — and therefore, since git is the undo substrate, no undo/redo (06-history-undo.md). FSEvents live-reload works as on any board. Adding git later initializes the repo and moves the board to git mode. Honest caveat: without git there is no commit-before-overwrite protection, so on a no-git board **real data loss is possible** (e.g. concurrent or external overwrites) — accepted; adding git is the remedy. Repo-nested boards share this caveat: a repo exists, but the app manages no git there (06-history-undo.md), so its protections never run — committing is the user's own workflow. Deletion is the exception: the materialized trash (03-board-ui.md) makes card and lane deletes recoverable even without git — overwrites are the lossy case, and Empty Trash is deliberate. +Plain folders on local disk. **No git repository at all** — and therefore no git history; undo/redo binds the native stack in every tier (13-native-undo.md, re-ruled 2026-07-31). FSEvents live-reload works as on any board. Adding git later initializes the repo and moves the board to git mode. Honest caveat: without git there is no commit-before-overwrite protection, so on a no-git board **real data loss is possible** (e.g. concurrent or external overwrites) — accepted; adding git is the remedy. Repo-nested boards share this caveat: a repo exists, but the app manages no git there (06-history-undo.md), so its protections never run — committing is the user's own workflow. Deletion is the exception: the materialized trash (03-board-ui.md) makes card and lane deletes recoverable even without git — overwrites are the lossy case, and Empty Trash is deliberate. ## Mode: git diff --git a/DESIGN/09-templates.md b/DESIGN/09-templates.md index 3c24322..c583845 100644 --- a/DESIGN/09-templates.md +++ b/DESIGN/09-templates.md @@ -27,7 +27,7 @@ All ten pathfinder templates carry over: Basic, Classic Kanban, Software Project ### Instantiation -Creating a board from a template: copy the tree — **skipping `.trash/`** (Save as Template already strips it, but hand-dropped user templates can carry one; a new board isn't born with trash) — **mint fresh GUIDs** for every lane/card folder, stamp `created`/`modified` fresh (the stated exception to 01-storage-format.md's copies-keep-`created` rule — a new board is born today, not forked from the template), seed the save panel's suggested name from the template title, and **set the new board's `title` to the user-chosen document name** (per 01-storage-format.md's board-naming rule, so display name and folder name start out matching). The `template:` key is kept — inert on an ordinary board. **`.git` is never copied** — a template is content, not history, and a hand-dropped user template that carries one must not produce boards that are silently in git mode (06-history-undo.md's no-silent-auto-init — the principle is *never give the user a repo they didn't ask for*, and it scopes to instantiation: File ▸ Duplicate deliberately carries `.git`, because a duplicate of a git board is a fork of its history — 03-board-ui.md): an instantiated board is never in *git* mode — its actual mode follows 06's nearest-`.git`-wins detection at the destination the save panel chose: mode `none` in a plain folder, repo-nested when saved inside an existing repository (no undo, no add-git — the popover explains, 06-history-undo.md). Beyond the `template:` residue, the result is indistinguishable from a hand-built board. +Creating a board from a template: copy the tree — **skipping `.trash/`** (Save as Template already strips it, but hand-dropped user templates can carry one; a new board isn't born with trash) — **mint fresh GUIDs** for every lane/card folder, stamp `created`/`modified` fresh (the stated exception to 01-storage-format.md's copies-keep-`created` rule — a new board is born today, not forked from the template), seed the save panel's suggested name from the template title, and **set the new board's `title` to the user-chosen document name** (per 01-storage-format.md's board-naming rule, so display name and folder name start out matching). The `template:` key is kept — inert on an ordinary board. **`.git` is never copied** — a template is content, not history, and a hand-dropped user template that carries one must not produce boards that are silently in git mode (06-history-undo.md's no-silent-auto-init — the principle is *never give the user a repo they didn't ask for*, and it scopes to instantiation: File ▸ Duplicate deliberately carries `.git`, because a duplicate of a git board is a fork of its history — 03-board-ui.md): an instantiated board is never in *git* mode — its actual mode follows 06's nearest-`.git`-wins detection at the destination the save panel chose: mode `none` in a plain folder, repo-nested when saved inside an existing repository (no app-managed git, no add-git — the popover explains; native undo still serves, 06-history-undo.md). Beyond the `template:` residue, the result is indistinguishable from a hand-built board. ### Why this format diff --git a/DESIGN/12-editions.md b/DESIGN/12-editions.md index 47d0449..86ad3a9 100644 --- a/DESIGN/12-editions.md +++ b/DESIGN/12-editions.md @@ -30,7 +30,7 @@ One record: `dev.rzen.indie.Kanban`, free, all territories, 2.0 as an update — History (and later sync) is a provider behind one protocol boundary, bound per board session at composition from the entitlement: -- **HistoryProviding** — the undo/redo substrate. **The provider follows the board** (re-ruled 2026-07-31): gitless boards bind the native undo stack (13-native-undo.md: NSUndoManager over inverse `WriteOperation`s) in every tier — an upgrade never removes undo — while Pro binds the git provider on git boards (06-history-undo.md: undo as forward restore commits over HEAD's first-parent ancestry); repo-nested boards bind none. Teams inherits Pro's. Add-git swaps native → git mid-session by the branch-switch discard-and-reseed precedent (13). +- **HistoryProviding** — the undo/redo substrate. **The provider follows the board** (re-ruled 2026-07-31): gitless boards bind the native undo stack (13-native-undo.md: NSUndoManager over inverse `WriteOperation`s) in every tier — an upgrade never removes undo — while Pro binds the git provider on git boards (06-history-undo.md: undo as forward restore commits over HEAD's first-parent ancestry); repo-nested boards bind native too (re-ruled 2026-07-31 — the stack touches no git, so what matters is the absence of *app-managed* git, and the upgrade story is exceptionless). Teams inherits Pro's. Add-git swaps native → git mid-session by the branch-switch discard-and-reseed precedent (13). - **Sync/tracker providers** — deferred with Teams; the reserved schema keys and the one-way file flow (02-architecture.md) are the format-level seam already in place. What is shared across providers (settled): **06's Undo routing is tier-independent** — focus decides text-undo vs board-undo; only the substrate behind board-undo differs. The command surface is identical (⌘Z/⇧⌘Z, dynamically retitled menu items — both providers use NSUndoManager's title rewriting); menu titles draw on the same semantic vocabulary (06 ▸ Commit messages). A user subscribing (or lapsing) relearns nothing. @@ -51,7 +51,7 @@ The feature sort. Everything not listed rides with "board experience" and is ide | Agent integration: agent guide, `modified-by` attribution, tolerance rules | ✓ | ✓ | ✓ | | Accessibility (10-accessibility.md, all of it) | ✓ | ✓ | ✓ | | Comments (designed 2026-07-29 — 01 ▸ Enhanced schema + 05 ▸ The comments column; ships post-2.0) | ✓ | ✓ | ✓ + tracker-synced threads | -| Undo/redo | native (13) | git on git boards, native otherwise (06/13, re-ruled 2026-07-31) | inherits Pro | +| Undo/redo | native (13) | git on git boards, native otherwise — repo-nested included (06/13, re-ruled 2026-07-31) | inherits Pro | | Undo of foreign/agent edits | — (honest gap, 13) | ✓ (stack absorbs foreign commits) | ✓ | | Overwrite protection (flush-before-overwrite, both-versions-as-commits) | — (07's accepted caveat is permanent here; trash + native undo are the safety story) | ✓ | ✓ | | History surfaces: card History sidebar (05), View ▸ History (11) | — | ✓ | ✓ | diff --git a/DESIGN/13-native-undo.md b/DESIGN/13-native-undo.md index dce6739..401deb6 100644 --- a/DESIGN/13-native-undo.md +++ b/DESIGN/13-native-undo.md @@ -1,6 +1,6 @@ -# Native Undo (free tier) +# Native Undo -The undo/redo substrate for **every gitless board, in every tier** (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): the free tier binds it everywhere (any `.git` inert — 12-editions.md), and Pro binds it on mode-none boards, switching to the git provider (06-history-undo.md) where git exists. Repo-nested boards remain the one no-undo case (06's leave-strictly-alone stance). **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 without app-managed git, in every tier** (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): the free tier binds it everywhere (any `.git` inert — 12-editions.md), and Pro binds it on mode-none **and repo-nested** boards alike (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. ## Rules