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
+33
View File
@@ -29,6 +29,12 @@ pub struct Args {
/// `--help` / `-h`: print usage to stdout and exit. The
/// runtime checks this flag before doing any other work.
pub help: bool,
/// `--no-undo`: disable the auto-snapshot / undo machinery for
/// this run (ADR-0006 Amendment 1). When set, no snapshots are
/// taken — zero per-command overhead — and `undo` / `redo`
/// report that undo is turned off. The escape hatch for small
/// hardware where per-command snapshotting is too heavy.
pub no_undo: bool,
}
/// Usage banner printed by `--help`.
@@ -109,6 +115,7 @@ impl Args {
let mut project_path: Option<PathBuf> = None;
let mut resume = false;
let mut help = false;
let mut no_undo = false;
let mut iter = iter.into_iter().map(Into::into);
while let Some(arg) = iter.next() {
match arg.as_str() {
@@ -118,6 +125,9 @@ impl Args {
"--resume" => {
resume = true;
}
"--no-undo" => {
no_undo = true;
}
"--theme" => {
let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
theme = match value.as_str() {
@@ -164,6 +174,7 @@ impl Args {
project_path,
resume,
help,
no_undo,
})
}
}
@@ -300,6 +311,28 @@ mod tests {
assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
}
#[test]
fn no_undo_flag_parses() {
let args = Args::parse(["--no-undo"]).unwrap();
assert!(args.no_undo);
}
#[test]
fn no_undo_defaults_off() {
let args = Args::parse(std::iter::empty::<&str>()).unwrap();
assert!(!args.no_undo, "undo is enabled unless --no-undo is given");
}
#[test]
fn no_undo_coexists_with_positional_path() {
let args = Args::parse(["--no-undo", "/home/me/MyProject"]).unwrap();
assert!(args.no_undo);
assert_eq!(
args.project_path.as_deref(),
Some(std::path::Path::new("/home/me/MyProject"))
);
}
#[test]
fn unknown_double_dash_flag_errors_even_with_positional() {
// Make sure the path-vs-flag distinction is robust: