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:
claude@clouddev1
2026-05-08 08:27:50 +00:00
parent c6cf3df6dc
commit 67d68db5f8
12 changed files with 1544 additions and 34 deletions
+23
View File
@@ -248,6 +248,27 @@ impl App {
}
}
/// Replace the in-memory navigable history with `entries`,
/// truncating to the in-memory cap.
///
/// Used by the runtime to hydrate from the project's
/// `history.log` on open (I2-persist, ADR-0015 §12).
/// Entries should arrive in chronological order (oldest
/// first); the most recent stays at the back, which is
/// where Up/Down navigation expects it.
///
/// Cancels any in-flight history navigation so a hydrate
/// during a session (e.g. after `load`) doesn't leave a
/// dangling cursor pointing at a now-removed entry.
pub fn seed_history(&mut self, entries: Vec<String>) {
self.history = entries;
while self.history.len() > HISTORY_CAPACITY {
self.history.remove(0);
}
self.history_cursor = None;
self.history_draft = None;
}
/// Effective mode for the *next* submission, given the
/// persistent mode and the current input buffer. See
/// [`EffectiveMode`].
@@ -353,12 +374,14 @@ impl App {
AppEvent::ProjectSwitched {
display_name,
is_temp,
history_entries,
} => {
self.note_system(format!("[ok] now editing: {display_name}"));
self.project_name = Some(display_name);
self.project_is_temp = is_temp;
self.tables.clear();
self.current_table = None;
self.seed_history(history_entries);
Vec::new()
}
AppEvent::ProjectSwitchFailed { error } => {