docs: ADR-0006 Amendment 1 — undo/snapshot design (every-mutation, hybrid, batch) + plan

Settles the undo/snapshot half (U1/U2) before implementation:
- every-mutation single-step undo (supersedes destructive-only model)
- hybrid whole-project snapshot (db backup API + yaml/csv copy),
  reconciling ADR-0006 with ADR-0015's derived-db model
- persisted N=50 ring; redo discarded on new work
- batch ops (replay + future) record one undo step; import excluded
- --no-undo disable switch
Adds the implementation plan and updates README index, requirements
U1/U2, and CLAUDE.md in lockstep.
This commit is contained in:
claude@clouddev1
2026-05-24 19:57:47 +00:00
parent 2e3131669a
commit 6cf5705022
5 changed files with 628 additions and 9 deletions
@@ -87,3 +87,123 @@ authoritative state of the project (that lives in `project.yaml`
with no additional storage commitment.
- Tutorial authors gain a natural "starter script" format for
exercises.
## Amendment 1 — Single-step undo: every-mutation snapshots, hybrid storage, batch granularity (2026-05-24)
The replay/journal half of this ADR (U3/U4) shipped via ADR-0034. This
amendment settles the **undo/snapshot half (U1/U2)** before
implementation, and **supersedes the original Decision's
"snapshots only before destructive operations" model** and its
confirmation rationale. Written with explicit user approval; the
implementation plan is `docs/plans/20260524-adr-0006-undo-snapshots.md`.
**Not yet implemented** at the time of writing — this records the
agreed design.
### Snapshot scope — every mutation (single-step undo)
The original Decision snapshots "before any destructive operation —
`DROP`, `DELETE`, `TRUNCATE`, schema-rebuild migrations, restore" and
explicitly treats inserts/updates/schema-additions as *non-destructive
work between snapshots*. That is **replaced**: a snapshot is taken
before **every** data/schema mutation — insert, update, delete, drop,
all DDL, and all SQL DML. Undo therefore behaves like a familiar
single-step "undo my last command" (Ctrl-Z), which is the right model
for this teaching environment (clarity over micro-optimisation, per
the project's "pedagogy wins ties" posture).
A consequence is that the original confirmation clause — "counts of
rows added/modified/deleted per table and any schema changes since the
snapshot" — **collapses**: with a snapshot per command there is no
intervening un-snapshotted work, so undo rolls back exactly one
command and the confirmation simply **names that command**. No
db-diff machinery is needed.
### Confirmation rationale
The original justified the always-on prompt by "undo is rare and
consequential." Under single-step undo, undo is *more frequent*, but
the prompt is **kept** anyway, now justified by "the prompt names the
exact command being undone." There is still no flag to suppress it.
`undo` and `redo` each confirm (`Y` confirms; `N`/`Esc` cancels),
mirroring the existing `rebuild` modal. The redo prompt names the
command that will be re-applied.
### Snapshot mechanism — hybrid db + text (reconciles ADR-0015)
The original specifies SQLite's online backup API. Since then,
ADR-0015 made `playground.db` a *derived* artifact with
`project.yaml` + `data/*.csv` as the authoritative source, committed
last for crash recovery. A db-only restore is therefore no longer
sufficient on its own. The agreed mechanism is a **hybrid
whole-project snapshot**:
- the database is copied via the **online backup API** (honouring this
ADR; it is also the only safe way to copy a live database), **and**
- `project.yaml` + `data/*.csv` are copied as inert files.
Undo **restores all three directly** — no rebuild, no re-derivation —
re-establishing a consistent `(db, yaml, csv)` triple. This satisfies
both this ADR (the backup API *is* used) and ADR-0015 (text remains
authoritative). The snapshot is staged *before* the mutation's
transaction and finalised into the ring *after* the database commit,
preserving ADR-0015 §6's commit-db-last ordering; a rolled-back
operation leaves no snapshot.
### Storage and lifetime — persisted ring, N = 50
Snapshots are **persisted on disk** under the project in a
`.snapshots/` directory and survive quit (undo works after reopening).
The ring keeps the most recent **N = 50** snapshots (the original's
N = 10 is raised, since single-step undo means N counts *commands*;
still a single tunable constant), evicting the oldest on overflow.
`.snapshots/` is added to the `.gitignore` template, **excluded from
`export`** (like `playground.db` and `history.log`), and on the
temp-project cleanup allowlist so an otherwise-empty temp carrying a
snapshots directory remains safely deletable.
### Redo
`redo` is supported (as the original states). New semantics are
pinned: **the redo stack is discarded on any new mutation** (standard
linear undo/redo). Each undo pushes the pre-undo state so redo can
restore it.
### Batch operations — one undo step; `import` excluded
A single user command that runs many sub-operations — `replay` today,
and any future in-project batch command — records **one** boundary
snapshot for the whole batch (not one per sub-command), via a
Begin/EndBatch worker primitive that suppresses per-command staging and
finalises a single ring entry only if ≥1 mutation actually ran. This is
a performance win (a long `history.log` replay is one database copy,
not N) and the consistent reading of "one undo step per user command."
`import` is **outside** the undo model entirely: per ADR-0015 §11 it
creates a *new* project and switches to it, leaving the current project
untouched on disk, so there is nothing to snapshot and it takes no undo
step (the new project simply starts with an empty ring). Project-switch
navigation undo ("go back to the previous project") is a separate,
out-of-scope mechanism — the prior project is intact and reachable via
`load` / `--resume`.
### Disable switch
A `--no-undo` CLI flag turns snapshotting off entirely (zero
per-command overhead), as a hardware escape hatch should per-command
snapshots prove too heavy. When set, `undo` / `redo` report that undo
is turned off. CLI-only for v1 (no in-app toggle).
### Consequences
- Per-mutation snapshotting costs one database backup + a text copy per
command; a bulk paste of N inserts makes N snapshots. Bounded by the
N = 50 ring and the `--no-undo` escape hatch; the ADR-0015 "batch"
command remains the future remedy, and a hardlink/copy-on-write dedup
of unchanged files between consecutive snapshots is a possible future
optimisation (not v1).
- 50 × (database + text) of persisted snapshots can reach tens of MB
for larger projects — an accepted, bounded cost.
- `Cargo.toml` gains the `backup` feature on `rusqlite`.
- The Phase-3 N/A matrix row ("auto-snapshot fires for SQL DML the same
as DSL") becomes non-vacuous: the snapshot hook lives in the worker
dispatch and covers DSL and SQL mutations uniformly.
+1 -1
View File
@@ -11,7 +11,7 @@ This directory contains the project's ADRs, recorded per
- [ADR-0003 — Input modes and command dispatch](0003-input-modes-and-command-dispatch.md)
- [ADR-0004 — Project file format](0004-project-file-format.md)
- [ADR-0005 — Column type vocabulary](0005-column-type-vocabulary.md)
- [ADR-0006 — Undo snapshots and replay log](0006-undo-snapshots-and-replay-log.md)
- [ADR-0006 — Undo snapshots and replay log](0006-undo-snapshots-and-replay-log.md)**Accepted**. The **replay/journal half** (U3/U4) shipped via ADR-0034; the **undo/snapshot half** (U1/U2) is settled by **Amendment 1 (2026-05-24)** but **not yet implemented** (plan: `docs/plans/20260524-adr-0006-undo-snapshots.md`). Amendment 1 **supersedes the original "snapshots only before destructive operations" model**: a snapshot is taken before **every** data/schema mutation (DSL + SQL) for familiar single-step (Ctrl-Z) undo — so the confirmation collapses to *naming the one command being undone* (no db-diff). Snapshot is a **hybrid whole-project copy** — database via the online backup API **plus** `project.yaml`/`data/*.csv` as files — reconciling this ADR with ADR-0015's "text is authoritative, db is derived"; undo restores all three directly. Staged before the mutation's transaction, finalised after the db commit (preserves ADR-0015 §6 commit-db-last); rolled-back ops leave no snapshot. **Persisted** ring under `.snapshots/`, **N = 50** (raised from 10), git-ignored + export-excluded + temp-cleanup-aware. `redo` supported, **redo stack discarded on new work**. **Batch ops record one undo step** (`replay` + future batch via a Begin/EndBatch worker primitive); **`import` is outside undo** (it switches projects per ADR-0015 §11, leaving the current project untouched). A **`--no-undo` CLI flag** disables snapshotting (hardware escape hatch). Adds the `backup` feature to `rusqlite`
- [ADR-0007 — Sharing and export](0007-sharing-and-export.md)
- [ADR-0008 — Testing approach](0008-testing-approach.md)
- [ADR-0009 — DSL command syntax conventions](0009-dsl-command-syntax-conventions.md)