Iteration 6: --resume + persistent input history + migration scaffold
Closes out track 2's ADR-0015 backlog. * `--resume` CLI flag (L1a, ADR-0015 §7) opens the most- recently-used project, tracked in <data-root>/last_project. Mutually exclusive with a positional <project-path>; errors cleanly to stderr (above the shell prompt) on missing file or stale recorded path. last_project is rewritten on every successful project open (startup, load, new, save as, import). * Persistent input history (I2-persist, ADR-0015 §12). On project open, the in-memory navigable history is hydrated from the tail of history.log (capped at the in-memory cap). ProjectSwitched gains a `history_entries` payload field; App::seed_history is the entry point. Pipes inside source text round-trip via splitn(3); unknown escape sequences are passed through literally. * Migration framework scaffold (F3, ADR-0015 §9). New persistence::migrations module with MigratorRegistry + migrate_to_latest + ensure_project_yaml_migrated. Empty in v1 (production registry has no migrators); the loader runs through it on every project open and is exercised by tests with a fake v1→v2 migrator. Writes project.yaml.v<N>.bak before any migrator runs; verifies each step bumps the version field. Refreshes docs/requirements.md (A1 / I2 / F3 / E1 / L1a / test baseline) and adds docs/handoff/20260508-handoff-3.md covering both Iter 5 and Iter 6. Total tests: 408 passing, 0 failing, 0 skipped (up from 345 at handoff-2). Clippy clean.
This commit is contained in:
+44
-1
@@ -18,8 +18,14 @@ pub struct Args {
|
||||
pub data_dir: Option<PathBuf>,
|
||||
/// Positional path argument: open an existing project at
|
||||
/// this path (L1, ADR-0015 §1). Mutually exclusive with
|
||||
/// `--resume` once that lands.
|
||||
/// `--resume`.
|
||||
pub project_path: Option<PathBuf>,
|
||||
/// `--resume`: open the most-recently-used project at
|
||||
/// startup (L1a, ADR-0015 §7). Reads the path from
|
||||
/// `<data-root>/last_project`. Mutually exclusive with
|
||||
/// `<project-path>` — supplying both is an error rather
|
||||
/// than silently picking one.
|
||||
pub resume: bool,
|
||||
/// `--help` / `-h`: print usage to stdout and exit. The
|
||||
/// runtime checks this flag before doing any other work.
|
||||
pub help: bool,
|
||||
@@ -44,6 +50,11 @@ Options:
|
||||
--data-dir <PATH> Use PATH as the data root instead of
|
||||
the OS-standard location for this run.
|
||||
--log-file <PATH> Write tracing output to PATH.
|
||||
--resume Open the most-recently-used project
|
||||
(path tracked under <data-root>/last_project).
|
||||
Errors out if no previous project is
|
||||
recorded. Mutually exclusive with
|
||||
<project-path>.
|
||||
|
||||
App-level commands (typed inside the app, available in both modes):
|
||||
quit / q Exit cleanly.
|
||||
@@ -80,6 +91,11 @@ pub enum ArgsError {
|
||||
Unknown(String),
|
||||
#[error("only one project path may be supplied; got both `{first}` and `{second}`")]
|
||||
MultiplePaths { first: String, second: String },
|
||||
#[error(
|
||||
"--resume and a positional <project-path> are mutually exclusive; \
|
||||
pass one or the other"
|
||||
)]
|
||||
ResumeWithPath,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
@@ -98,6 +114,7 @@ impl Args {
|
||||
let mut log_path = env::var_os("RDBMS_PLAYGROUND_LOG_FILE").map(PathBuf::from);
|
||||
let mut data_dir: Option<PathBuf> = None;
|
||||
let mut project_path: Option<PathBuf> = None;
|
||||
let mut resume = false;
|
||||
let mut help = false;
|
||||
let mut iter = iter.into_iter().map(Into::into);
|
||||
while let Some(arg) = iter.next() {
|
||||
@@ -105,6 +122,9 @@ impl Args {
|
||||
"--help" | "-h" => {
|
||||
help = true;
|
||||
}
|
||||
"--resume" => {
|
||||
resume = true;
|
||||
}
|
||||
"--theme" => {
|
||||
let value = iter.next().ok_or(ArgsError::MissingValue("theme"))?;
|
||||
theme = match value.as_str() {
|
||||
@@ -141,11 +161,15 @@ impl Args {
|
||||
}
|
||||
}
|
||||
}
|
||||
if resume && project_path.is_some() {
|
||||
return Err(ArgsError::ResumeWithPath);
|
||||
}
|
||||
Ok(Self {
|
||||
theme,
|
||||
log_path,
|
||||
data_dir,
|
||||
project_path,
|
||||
resume,
|
||||
help,
|
||||
})
|
||||
}
|
||||
@@ -264,6 +288,25 @@ mod tests {
|
||||
assert!(args.help);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_flag_parses() {
|
||||
let args = Args::parse(["--resume"]).unwrap();
|
||||
assert!(args.resume);
|
||||
assert!(args.project_path.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_with_positional_path_errors() {
|
||||
let err = Args::parse(["--resume", "/some/path"]).unwrap_err();
|
||||
assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn positional_path_with_resume_errors_in_either_order() {
|
||||
let err = Args::parse(["/some/path", "--resume"]).unwrap_err();
|
||||
assert!(matches!(err, ArgsError::ResumeWithPath), "got: {err:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_double_dash_flag_errors_even_with_positional() {
|
||||
// Make sure the path-vs-flag distinction is robust:
|
||||
|
||||
Reference in New Issue
Block a user