From d6c5674bf59ab4dd68898817df09a8b3d3986136 Mon Sep 17 00:00:00 2001 From: "claude@clouddev1" Date: Sun, 24 May 2026 20:53:00 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20ADR-0006=20=C2=A78=20step=206=20?= =?UTF-8?q?=E2=80=94=20.snapshots/=20gitignore=20+=20export=20+=20cleanup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The undo ring is local working state, handled at all three project-file seams (R13): - .gitignore template ignores /.snapshots/ - export excludes .snapshots/ (like playground.db / history.log) - safely_delete_temp_project allowlists .snapshots/ so a temp that was modified then undone back to empty stays auto-deletable - undo::SNAPSHOTS_DIR is now a pub const referenced by all three - tests: gitignore content, export exclusion, cleanup allowlist 1693 passed / 0 failed / 1 ignored; clippy clean. --- src/archive.rs | 21 +++++++++++++++++++++ src/project/mod.rs | 13 ++++++++++--- src/undo.rs | 6 +++++- tests/iteration4b_lifecycle_commands.rs | 18 ++++++++++++++++++ tests/project_lifecycle.rs | 2 ++ 5 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/archive.rs b/src/archive.rs index d57b666..6dd3eda 100644 --- a/src/archive.rs +++ b/src/archive.rs @@ -43,6 +43,9 @@ const EXPORT_EXCLUDED_NAMES: &[&str] = &[ PLAYGROUND_DB, HISTORY_LOG, ".rdbms-playground.lock", + // Undo snapshot ring (ADR-0006 Amendment 1): local working + // state, never shared. + crate::undo::SNAPSHOTS_DIR, ]; /// Maximum auto-suffix attempts when resolving a colliding @@ -306,6 +309,7 @@ fn add_directory_recursive( /// - `playground.db` (always derived; ADR-0007 / ADR-0015 §11) /// - `history.log` (ADR-0007 amendment 1: user-private) /// - `.rdbms-playground.lock` (per-process) +/// - `.snapshots/` (undo ring; local working state, ADR-0006 Am. 1) /// - `*.tmp` (atomic-write staging files) /// - `project.yaml.v*.bak` (migration backups; recipient /// doesn't need our local recovery aids) @@ -524,6 +528,18 @@ mod tests { fs::write(p.join(GITIGNORE), "/playground.db\n").unwrap(); // Stray atomic-write staging file — must be excluded. fs::write(p.join("project.yaml.tmp"), "stale").unwrap(); + // Undo snapshot ring — local working state, must be excluded. + fs::create_dir_all(p.join(crate::undo::SNAPSHOTS_DIR).join("0")).unwrap(); + fs::write( + p.join(crate::undo::SNAPSHOTS_DIR).join("index.yaml"), + "next_id: 1\nundo: []\nredo: []\n", + ) + .unwrap(); + fs::write( + p.join(crate::undo::SNAPSHOTS_DIR).join("0").join(PLAYGROUND_DB), + [0u8; 16], + ) + .unwrap(); p } @@ -548,6 +564,11 @@ mod tests { assert!(!names.iter().any(|n| n.contains("history.log"))); assert!(!names.iter().any(|n| n.contains(".lock"))); assert!(!names.iter().any(|n| n.ends_with(".tmp"))); + // Undo snapshot ring is local working state — never shared. + assert!( + !names.iter().any(|n| n.contains(".snapshots")), + "snapshots leaked into export: {names:?}" + ); // .gitignore IS included (sensible default for the recipient). assert!(names.iter().any(|n| n == "MyProject/.gitignore")); } diff --git a/src/project/mod.rs b/src/project/mod.rs index 8fec435..98d5436 100644 --- a/src/project/mod.rs +++ b/src/project/mod.rs @@ -416,10 +416,12 @@ impl Project { write_if_missing(&path.join(PROJECT_YAML), &yaml)?; // .gitignore template (ADR-0015 §11). Excludes the - // derived `.db`, the per-process lock, and migration - // backups. `history.log` is intentionally NOT ignored + // derived `.db`, the per-process lock, migration backups, + // and the undo snapshot ring (ADR-0006 Amendment 1 — local + // working state). `history.log` is intentionally NOT ignored // (ADR-0007 amendment 1: per-user choice). - let gitignore = "/playground.db\n/.rdbms-playground.lock\n/project.yaml.v*.bak\n"; + let gitignore = + "/playground.db\n/.rdbms-playground.lock\n/project.yaml.v*.bak\n/.snapshots/\n"; write_if_missing(&path.join(GITIGNORE), gitignore)?; Ok(()) @@ -502,6 +504,10 @@ const ALLOWED_PROJECT_ENTRIES: &[&str] = &[ PLAYGROUND_DB, GITIGNORE, ".rdbms-playground.lock", + // Undo snapshot ring (ADR-0006 Amendment 1): a temp that was + // modified then undone back to empty can still carry this, and + // must remain auto-deletable. + crate::undo::SNAPSHOTS_DIR, ]; /// Reasons `safely_delete_temp_project` refuses to delete a @@ -841,6 +847,7 @@ mod tests { let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap(); assert!(gi.contains("/playground.db")); assert!(gi.contains("/.rdbms-playground.lock")); + assert!(gi.contains("/.snapshots/"), "undo ring should be ignored"); assert!(!gi.contains("history.log"), "history.log should NOT be ignored"); } diff --git a/src/undo.rs b/src/undo.rs index fba8ae4..2202e4e 100644 --- a/src/undo.rs +++ b/src/undo.rs @@ -49,7 +49,11 @@ use crate::project::{DATA_DIR, PLAYGROUND_DB, PROJECT_YAML}; pub const DEFAULT_RING_CAPACITY: usize = 50; /// Directory under the project that holds the ring. -const SNAPSHOTS_DIR: &str = ".snapshots"; +/// +/// Public so the `.gitignore` template, the export exclusion, and +/// the temp-project cleanup allowlist all reference the one +/// canonical name (ADR-0006 Amendment 1). +pub const SNAPSHOTS_DIR: &str = ".snapshots"; /// In-flight snapshot directory (one at a time). const STAGING_DIR: &str = ".staging"; /// The ring index file. diff --git a/tests/iteration4b_lifecycle_commands.rs b/tests/iteration4b_lifecycle_commands.rs index 78b0c99..b26a7bc 100644 --- a/tests/iteration4b_lifecycle_commands.rs +++ b/tests/iteration4b_lifecycle_commands.rs @@ -494,6 +494,24 @@ fn safely_delete_allows_migration_backups_and_tmp_files() { assert!(!path.exists()); } +#[test] +fn safely_delete_allows_undo_snapshot_ring() { + // A temp that was modified then undone back to empty can still + // carry the `.snapshots/` ring; it must remain auto-deletable + // (ADR-0006 Amendment 1). + let data = tempdir(); + let project = project::open_or_create(None, Some(data.path())).unwrap(); + let path = project.path().to_path_buf(); + let snaps = path.join(".snapshots"); + fs::create_dir_all(snaps.join("3")).unwrap(); + fs::write(snaps.join("index.yaml"), "next_id: 4\nundo: []\nredo: []\n").unwrap(); + fs::write(snaps.join("3").join("playground.db"), [0u8; 16]).unwrap(); + drop(project); + + safely_delete_temp_project(&path, data.path()).expect("should delete"); + assert!(!path.exists()); +} + #[cfg(unix)] #[test] fn safely_delete_refuses_symlink_top_level() { diff --git a/tests/project_lifecycle.rs b/tests/project_lifecycle.rs index 2ba090a..a8bed7b 100644 --- a/tests/project_lifecycle.rs +++ b/tests/project_lifecycle.rs @@ -43,6 +43,8 @@ fn no_args_creates_temp_project_under_data_root() { // .gitignore must NOT include history.log (ADR-0007 amendment). let gi = fs::read_to_string(path.join(GITIGNORE)).unwrap(); assert!(!gi.contains("history.log")); + // …but it must ignore the undo ring (ADR-0006 Amendment 1). + assert!(gi.contains("/.snapshots/")); } #[test]