feat: ADR-0006 §8 steps 1-2 — --no-undo flag + snapshot ring module

Step 1 (Cargo + CLI):
- add the `backup` feature to rusqlite (online backup API)
- `--no-undo` flag (test-first) + help-banner entry

Step 2 (snapshot store, src/undo.rs):
- SnapshotStore: a persisted undo ring + redo stack under
  <project>/.snapshots/ (index.yaml + per-snapshot payload dirs)
- hybrid whole-project snapshot: db via backup API + project.yaml /
  data/*.csv copied as files; restore is text-first, db-last
  (ADR-0015 §6 commit-db-last)
- stage/finalize/discard, undo/redo (each snapshots current to keep
  the inverse possible), N=50 eviction, redo-cleared-on-new-work,
  orphan/staging cleanup, monotonic ids
- 12 Tier-1 tests; adds a crate-visible persistence::utc_iso8601_now

No worker wiring yet (step 3). 1674 passed / 0 failed / 1 ignored; clippy clean.
This commit is contained in:
claude@clouddev1
2026-05-24 20:17:03 +00:00
parent 6cf5705022
commit 64eee3ed6d
7 changed files with 824 additions and 7 deletions
@@ -207,7 +207,7 @@ amendment records:
data/<table>.csv
playground.db
.snapshots/
index.json # ordered ring + redo stack: ids, timestamps,
index.yaml # ordered ring + redo stack: ids, timestamps,
# command text, status; the source of truth
# for ordering/eviction
0001/
@@ -221,7 +221,7 @@ amendment records:
- **`.snapshots/` is git-ignored, export-excluded, and on the
temp-cleanup allowlist** (R13).
- The **ring** (undo) and the **redo stack** are both recorded in
`index.json`; snapshot payload dirs are shared storage referenced by
`index.yaml`; snapshot payload dirs are shared storage referenced by
id. Eviction beyond 50 deletes the oldest payload dir.
- Ids are monotonic; never reused, to avoid stale references.
@@ -253,7 +253,7 @@ A snapshot brackets the existing 4-step persistence sequence:
3. atomic-rename text into place (step 3)
4. commit db (step 4)
5. FINALIZE snapshot (NEW) — atomic-rename .staging/ → .snapshots/<id>/,
append to index.json ring, evict oldest if
append to index.yaml ring, evict oldest if
>50, clear redo stack.
On any failure in 14 → txn rolls back; DISCARD .staging/.
```
@@ -296,7 +296,7 @@ Mirrors the existing two-phase `rebuild` modal flow, with the
1. `undo` parses to `Command::App(AppCommand::Undo)`
`dispatch_app_command` returns `Action::PrepareUndo`.
2. Runtime handles `PrepareUndo`: reads `.snapshots/index.json` top
2. Runtime handles `PrepareUndo`: reads `.snapshots/index.yaml` top
entry (command text + timestamp) — a cheap file read, like
`summarize_project` does for rebuild — and posts
`AppEvent::UndoPrepared { command, when }` (or
@@ -356,7 +356,7 @@ primitive collapses a batch to a single ring entry:
| `src/app.rs` | `Modal::UndoConfirm` (+ redo) struct; event arms; `handle_undo_confirm_key`; dispatch arms |
| `src/ui.rs` | `render_undo_confirm` (mirror `render_rebuild_confirm`) |
| `src/db.rs` | `is_mutating`; `stage/finalize/discard_snapshot`; `Request::Undo`/`Redo`/`BeginBatch`/`EndBatch`; dispatcher wrap; `undo_enabled` + `in_batch` worker fields |
| `src/persistence/` (or new `src/snapshots/`) | ring + redo store, `index.json`, eviction, restore |
| `src/undo.rs` (new) | snapshot ring + redo store, `index.yaml`, stage/finalize/discard/undo/redo/cleanup, eviction, restore (named `undo``src/snapshots/` is the insta dir) |
| `src/runtime.rs` | `is_app_lifecycle_entry_word += undo,redo`; `PrepareUndo/Undo` (+redo) handling; thread `no_undo` to worker; bracket `run_replay` with Begin/EndBatch |
| `src/project/mod.rs` | `.snapshots/` in `.gitignore` template |
| `src/archive.rs` | exclude `.snapshots/` from export |
@@ -423,7 +423,7 @@ explicit future items (hardlink optimisation) for user awareness.
1. **Cargo + CLI:** add `backup` feature; `--no-undo` flag + parse
tests. (R14, R9-partial)
2. **Snapshot store module:** `index.json` model, ring + redo, stage/
2. **Snapshot store module:** `index.yaml` model, ring + redo, stage/
finalize/discard/restore, eviction — Tier-1 tests first against a
temp dir + in-memory/temp db. (R2, R3, R4, R6, R7)
3. **Worker integration:** `is_mutating` (exhaustive), dispatcher wrap,