Iteration 2: per-command write-through to project.yaml, CSVs, history.log

Every successful user command now persists through to YAML, the
affected CSVs, and history.log inside the same SQLite transaction,
with the commit-db-last ordering from ADR-0015 §6: validate ->
mutate -> stage text + fsync -> atomic rename -> append history ->
commit. A failure in any text-write step rolls back the SQLite tx,
so disk state is unchanged on failure. Persistence failures are
routed through a new AppEvent::PersistenceFatal which sets a
fatal_message on the App, emits Action::Quit, and is printed to
stderr after terminal teardown so the banner remains above the
shell prompt (ADR-0015 §8).

New persistence module owns the file formats: hand-rolled YAML
schema writer, per-type CSV encoder (RFC 4180, NULL distinct from
empty string, base64 blobs), append-only history.log with ISO-8601
timestamps and successful-only entries. Atomic per-file writes via
tmp + fsync + rename.

The db worker holds an Option<Persistence>; tests still use
Database::open(":memory:") with no persistence. Action::ExecuteDsl
gains a source field carrying the user-typed text, threaded
through to history.log.

Tests: 289 passing (256 lib + 7 new integration + 9 lifecycle + 17
walking-skeleton), 0 failing, 0 skipped. Clippy clean with nursery
lints.
This commit is contained in:
claude@clouddev1
2026-05-07 21:09:15 +00:00
parent 601d3b6c51
commit 5c076f6d8f
15 changed files with 2275 additions and 213 deletions
+8
View File
@@ -47,4 +47,12 @@ pub enum AppEvent {
},
/// Refreshed list of tables in the database.
TablesRefreshed(Vec<String>),
/// A persistence failure occurred (ADR-0015 §8). The
/// application surfaces a fatal banner and exits cleanly so
/// the message remains above the shell prompt.
PersistenceFatal {
operation: String,
path: std::path::PathBuf,
message: String,
},
}