From bf559bbbb859d3dcdf860bc2e303a9cc89b71fb7 Mon Sep 17 00:00:00 2001 From: rzen Date: Wed, 29 Jul 2026 14:09:28 -0400 Subject: [PATCH] =?UTF-8?q?Healing=20mutations=20commit=20separately=20?= =?UTF-8?q?=E2=80=94=20the=20heal=20class=20of=20the=20two-commit=20split?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit User ruling on the integrity pass: a scheduled heal's batch never folds into a commit carrying anyone else's work. The committer's two-commit split gains a heal class keyed by heal-marked EchoLedger receipts; the composer stays a pure diff reader and names heal commits from their own diff shape. On-touch heals ride their host write's commit (the backfilled kind joins the bookkeeping no-event class), inline heals batch with their gesture, and the user-gated Repair already commits alone. Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97 --- DESIGN/01-storage-format.md | 2 ++ DESIGN/06-history-undo.md | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/DESIGN/01-storage-format.md b/DESIGN/01-storage-format.md index 8c8b4bd..343ab1f 100644 --- a/DESIGN/01-storage-format.md +++ b/DESIGN/01-storage-format.md @@ -141,6 +141,8 @@ Heals classify on two axes. **Consent**: *app-initiated* when the repair is sema **Write discipline needs no rule of its own** (settled — the existing stamp rule decides every case): a heal that rewrites an `index.md` is an ordinary app write — stamps `modified`, clears `modified-by`; an on-touch heal rides its host write's stamp — while a heal that only renames or relocates folders and files never opens `index.md` and stamps nothing (the relocation, the remint — identity repairs, not edits). Atomicity is per file (temp + rename); interrupted batches are accepted per the renumber precedent — every intermediate state is valid. **Every scheduled heal re-verifies its defect against disk at write time** and no-ops when it is gone: losing the race to a foreign fix — an agent mid-edit, another repair — is success, never an error (the Repair-races-a-vanished-duplicate precedent, generalized). Scheduled heals are memo-guarded against hot loops — armed before the attempt, cleared explicitly on success, re-armed only by a changed defect signature — and **defer, never abandon**, under any read-only lock. +**Healing mutations commit separately** (ruled 2026-07-29 — largely redundant by construction, made a guarantee): on git boards, a scheduled heal's batch is never folded into a commit carrying anyone else's work — a debounce window holding both splits the heal's paths into their own commit, the two-commit split gaining a heal class (06-history-undo.md ▸ Commit messages: the Writer's heal operations drop heal-marked receipts in the EchoLedger — attribution machinery like the author split, never message tagging; the composer stays a pure diff reader and names the heal commit from its own diff shape, the way "Update agent guide (vN)" already commits alone today). In practice each scheduled healer already runs its own write bracket at the reload tail — normally its own window, its own commit; the split converts that timing accident into a rule. The carve-outs are structural, not exceptions to intent: an **on-touch** heal rides its host write's single atomic rewrite — inseparable from the host's commit, the backfilled `kind` composing no event (bookkeeping, the renumber-rescale class — 06); **inline** heals batch with the gesture that triggered them (Ordering above — never a standalone renumber, never a separately-named remint); the user-gated **Repair** is its own gesture and already commits as its own subject. + The vocabulary is per-kind and extensible: adding an object kind (the enhanced schema's comment) adds its field table and shape rules in one place, never a parallel mechanism. ## Document packaging diff --git a/DESIGN/06-history-undo.md b/DESIGN/06-history-undo.md index adde757..69f08ff 100644 --- a/DESIGN/06-history-undo.md +++ b/DESIGN/06-history-undo.md @@ -29,10 +29,11 @@ Git is the undo substrate — on boards that have git. **Git is opt-in per board The pathfinder's message engine carries over as the model — it is what earns the "semantic" in semantic commit messages, and it stays a pure, testable function: -- **Pure snapshot diff, no write-site tagging.** Messages compose at commit time from a structural diff of two board snapshots (last-committed vs. current) — never by intercepting operations. Items match by id across the *whole* board, so a lane change is distinguishable from delete+add and a cross-lane move reads as a move. Bookkeeping — `order` changes that preserve sibling sequence (a renumber's rescale — 01-storage-format.md), `modified`/`created` — produces no events: a diff touching only those composes nothing. Sequence is what the diff compares, not raw `order` values: an order change that *repositions* an item among its siblings composes Reorder, so a foreign writer's single-file reorder still reads as one. A midpoint-exhaustion renumber batches with the insert or move that triggered it, so its commit reads as that event. +- **Pure snapshot diff, no write-site tagging.** Messages compose at commit time from a structural diff of two board snapshots (last-committed vs. current) — never by intercepting operations. Items match by id across the *whole* board, so a lane change is distinguishable from delete+add and a cross-lane move reads as a move. Bookkeeping — `order` changes that preserve sibling sequence (a renumber's rescale — 01-storage-format.md), `modified`/`created`, and an on-touch heal's backfilled `kind` (01-storage-format.md ▸ Validation and healing) — produces no events: a diff touching only those composes nothing. Sequence is what the diff compares, not raw `order` values: an order change that *repositions* an item among its siblings composes Reorder, so a foreign writer's single-file reorder still reads as one. A midpoint-exhaustion renumber batches with the insert or move that triggered it, so its commit reads as that event. - **Vocabulary**: Add / Delete / Move / Rename / Edit / Restyle / Resize / Reorder over cards, lanes, and the board, plus Attach / Remove for attachment files ("Move card 'Fix login' to Doing", "Rename lane 'Todo' → 'Doing'"), plus **Repair** for the duplicate-id remint ("Repair duplicate of 'Fix login'" — 01-storage-format.md's banner-gated fix; app-mediated, so the composer names it directly instead of reading the folder swap as Permanently delete + Add), plus the trash pair (settled) — **Restore** and **Permanently delete** — distinguished by diff shape alone, keeping the composer a pure snapshot diff: a move into `.trash/` is Delete, a move out of it is Restore ("Restore card 'X'" — drag-to-restore, cut+paste), and an item *leaving the tree entirely* is Permanently delete ("Permanently delete card 'X'" — Delete Immediately, Empty Trash, and any foreign hard removal, which the shape rule catches and describes accurately for free). The trail thereby tells moved-to-trash from gone-forever — the distinction Deleting never forgets (below) asks users to learn. Plural folding applies as usual: Empty Trash reads "Permanently delete 12 cards", cleanly distinct from a multi-select ⌫'s "Delete 12 cards". One commit per debounce window: a single event is the subject (with a detail body where one helps); several events of one kind fold into a plural subject, with shared destinations preserved ("Move 3 cards to Done"); genuinely mixed windows fall back to "Update board" — always with a bulleted body naming every event, so the oneline log stays scannable and the full message stays complete. - **Implied events don't steal the subject**: deleting a lane with five cards reads "Delete lane 'X'" with the card deletions as body bullets — not "Update board". - **Non-snapshot files commit too** (settled — the repo tracks more than the model: the agent guide, `CLAUDE.user.md`, the seeded `.gitignore`, and strays at every level): the committer **stages the whole board root** — whatever `git status` shows, `.gitignore` respected, **open Edit sessions still staged around** (settled): the session card's folder stays excluded exactly as in Rules ▸ Auto-commit, whole-root staging widening *what* commits, never overriding the exclusion — and its commit condition is the *tree*, not the snapshot diff, so a stray-only window commits rather than leaving the tree dirty (firing mid-session, it commits the strays and leaves the session folder untouched) (a permanently dirty stray would break branch switch's cannot-fail-dirty guarantee and void flush-before-overwrite for every file the model can't see). The composer's input extends accordingly: beside the snapshot diff it receives the changed-path list, and non-snapshot paths compose **path-shaped events** — `CLAUDE.md` composes "Update agent guide (vN)", the version read from the guide's marker first line (a pure function of file content, *not* write-site tagging — the no-interception rule stands); any other non-snapshot path composes "Update '⟨path⟩'", folding plural ("Update 3 files"). Model events keep the subject when present; non-snapshot changes then ride as body bullets — recorded, never silently absorbed under an unrelated subject. Attribution needs no new rule: the EchoLedger (02-architecture.md) is path-keyed, so the app's guide write classifies app-mediated by its receipt (the mechanism behind the guide-attribution exception below) and a stray edit classifies foreign, the two-commit split applying per file as everywhere else. +- **Healing mutations commit separately** (ruled 2026-07-29 — 01-storage-format.md ▸ Validation and healing): a debounce window holding a scheduled heal's changes alongside anyone else's splits the heal's paths into their own commit — the two-commit split's mechanism with a third class, keyed by the Writer's heal-marked receipts in the EchoLedger (attribution machinery, like the author split; the composer stays a pure diff reader and names the heal commit from its own diff shape — a loose-file relocation reads as its Attach-shaped event, a legacy migration's move into `.trash/` as Delete, the guide as "Update agent guide (vN)"; that the shape vocabulary doesn't say "healed" is accepted, the banner already told the user). Mostly redundant — each scheduled healer runs its own bracket at the reload tail, normally its own window — but the split makes separation a guarantee rather than a timing accident. On-touch and inline heals are structurally exempt: each lives inside a host write or its triggering gesture and rides that commit, the backfilled `kind` composing no event per the bookkeeping rule above. - **Titles truncate in subjects only** (~40 chars, keeping `git log --oneline` sane); body lines carry full titles. An untitled item reads "(untitled)" — never a bare `""` (a pathfinder edge fixed, not carried). Undo/redo restores commit as "Undo: ⟨subject⟩" / "Redo: ⟨subject⟩"; the undo-menu labels are the *crossed* commit's subject, so labels never nest. **The external gap, closed** (the pathfinder weakness this section exists to fix): the composer is origin-agnostic, but external writers routinely touch what the pathfinder's diff never modeled — `labels`, `assignees`, `due`, custom frontmatter keys — so their commits degraded to a generic fallback even though the attribution machinery knew plenty. The rewrite: