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:
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user