feat(history): mode-tagged history + top-of-chain journaling (#30)
Record the submission mode per history entry so advanced commands are reusable in simple mode, and fix the bug where a ':'-one-shot command lost its ':' across sessions (ADR-0052, closing #30). Format: the history.log status token gains an optional ':adv' suffix (ok / ok:adv / err / err:adv); 'source' stays last and canonical, so replay is unaffected. The in-memory ring (still Vec<String>) stores advanced entries ': '-prefixed; recall strips the ':' in advanced mode and keeps it in simple; hydration reconstructs the prefix from the tag. Journaling moved from the worker to the dispatch layer (spawn_dsl_- dispatch / run_replay / app-command sites), where the mode is in scope with no worker plumbing; finalize_persistence writes only yaml/csv (commit-db-last still atomic for state). The journal write is now best-effort (command already committed), consistent with the failure path. App commands journal simple, so they recall bare. Journaling is now uniform (every successful command, per ADR-0034) — closing a gap where show tables/relationships/explain didn't journal. Amends ADR-0034 (status tag + journaling location), ADR-0015 §6 (history.log out of the worker tx), ADR-0040 (journal-write best-effort). 15 worker-level journaling tests retired, re-covered at the new layer (history.rs format, app.rs recall matrix, iteration6 cross-session regression, replay). 2471 pass / 0 fail / 0 skip, clippy clean.
This commit is contained in:
+48
-9
@@ -479,17 +479,19 @@ async fn run_loop(
|
||||
command,
|
||||
source,
|
||||
submission_mode,
|
||||
session.project().path().to_path_buf(),
|
||||
);
|
||||
}
|
||||
Action::JournalFailure { source } => {
|
||||
Action::JournalFailure { source, advanced } => {
|
||||
// ADR-0034 §1/§4: record a failed command as an
|
||||
// `err` record. Best-effort — a failure to record
|
||||
// a failure must never escalate a user error into
|
||||
// a fatal, so the result is logged and ignored.
|
||||
// `err` record (ADR-0052: `err:adv` when advanced).
|
||||
// Best-effort — a failure to record a failure must
|
||||
// never escalate a user error into a fatal, so the
|
||||
// result is logged and ignored.
|
||||
if let Err(e) = crate::persistence::Persistence::new(
|
||||
session.project().path().to_path_buf(),
|
||||
)
|
||||
.append_history_failure(&source)
|
||||
.append_history_failure(&source, advanced)
|
||||
{
|
||||
tracing::warn!(error = %e, "failed to journal err record (ignored)");
|
||||
}
|
||||
@@ -971,7 +973,9 @@ async fn perform_switch(
|
||||
// history.log. The worker's persistence is wired but not
|
||||
// directly addressable from here, so we use a fresh
|
||||
// Persistence handle for this single line.
|
||||
let _ = Persistence::new(new_path.clone()).append_history(&source);
|
||||
// App-lifecycle command (save-as/load/new): journalled simple
|
||||
// (ADR-0052 — app commands run in any mode, so no `:` on recall).
|
||||
let _ = Persistence::new(new_path.clone()).append_history(&source, false);
|
||||
|
||||
// Update the resume pointer so the next `--resume` launch
|
||||
// reopens the project we just switched to — unless it is a
|
||||
@@ -1040,7 +1044,9 @@ fn spawn_export(
|
||||
source: String,
|
||||
event_tx: mpsc::Sender<AppEvent>,
|
||||
) {
|
||||
let _ = crate::persistence::Persistence::new(project_path.clone()).append_history(&source);
|
||||
// `export` app command: journalled simple (ADR-0052).
|
||||
let _ = crate::persistence::Persistence::new(project_path.clone())
|
||||
.append_history(&source, false);
|
||||
tokio::spawn(async move {
|
||||
let outcome = tokio::task::spawn_blocking(move || {
|
||||
do_export(&project_path, &project_name, &data_root, target.as_deref())
|
||||
@@ -1295,11 +1301,20 @@ fn spawn_rebuild(
|
||||
source: String,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
let source_for_journal = source.clone();
|
||||
match database
|
||||
.rebuild_from_text(project_path.clone(), Some(source))
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
// ADR-0052: journal `rebuild` at the dispatch layer (the
|
||||
// worker no longer journals); simple (app command),
|
||||
// best-effort.
|
||||
if let Err(e) = crate::persistence::Persistence::new(project_path.clone())
|
||||
.append_history(&source_for_journal, false)
|
||||
{
|
||||
warn!(error = %e, "failed to journal rebuild (ignored)");
|
||||
}
|
||||
let summary = summarize_project(&project_path)
|
||||
.unwrap_or_else(|_| "rebuild complete".to_string());
|
||||
let _ = event_tx
|
||||
@@ -1407,10 +1422,11 @@ fn spawn_dsl_dispatch(
|
||||
command: Command,
|
||||
source: String,
|
||||
submission_mode: crate::app::EffectiveMode,
|
||||
project_path: std::path::PathBuf,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
// Retain the source for `DslFailed` so the App can journal a
|
||||
// rejected command as `err` (ADR-0034 §1/§2).
|
||||
// Retain the source for journaling (ADR-0034 §1/§2; ADR-0052
|
||||
// moved success journaling here, next to the failure path).
|
||||
let source_for_journal = source.clone();
|
||||
// ADR-0038: the DSL → SQL teaching echo fires for a DSL-form
|
||||
// command submitted in an advanced effective mode (ADR-0037).
|
||||
@@ -1431,6 +1447,19 @@ fn spawn_dsl_dispatch(
|
||||
let lookups = collect_echo_lookups(&database, &command, submission_mode).await;
|
||||
let echo = crate::echo::echo_for(&command, submission_mode);
|
||||
let outcome = execute_command_typed(&database, command.clone(), source).await;
|
||||
// ADR-0052 (issue #30): journal a SUCCESSFUL command here, at the
|
||||
// top of the chain — the canonical source + submission mode are
|
||||
// both in scope, so no mode-plumbing into the worker is needed.
|
||||
// Best-effort (ADR-0040 amended): the command is already committed;
|
||||
// a journal-write failure is logged, never fatal. Failures stay on
|
||||
// the `JournalFailure` path (Ok/Err are exclusive — no double
|
||||
// journal). `:adv` tags an advanced submission (ADR-0052).
|
||||
if outcome.is_ok()
|
||||
&& let Err(e) = crate::persistence::Persistence::new(project_path)
|
||||
.append_history(&source_for_journal, submission_mode.is_advanced())
|
||||
{
|
||||
warn!(error = %e, "failed to journal ok record (ignored)");
|
||||
}
|
||||
let event = match outcome {
|
||||
Ok(CommandOutcome::Schema(description)) => {
|
||||
let schema_echo = build_schema_echo(
|
||||
@@ -1569,6 +1598,7 @@ fn spawn_dsl_dispatch(
|
||||
error,
|
||||
facts,
|
||||
source: source_for_journal,
|
||||
advanced: submission_mode.is_advanced(),
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -2540,6 +2570,15 @@ pub async fn run_replay(
|
||||
execute_command_typed(database, command, command_text.clone()).await;
|
||||
match outcome {
|
||||
Ok(_) => {
|
||||
// ADR-0052: journal the replayed line at the dispatch
|
||||
// layer (the worker no longer journals). Replay is
|
||||
// mode-agnostic, so the re-written record is tagged
|
||||
// simple; best-effort, like the interactive path.
|
||||
if let Err(e) = crate::persistence::Persistence::new(project_root.to_path_buf())
|
||||
.append_history(&command_text, false)
|
||||
{
|
||||
warn!(error = %e, "failed to journal replayed line (ignored)");
|
||||
}
|
||||
count += 1;
|
||||
}
|
||||
Err(DbError::PersistenceFatal {
|
||||
|
||||
Reference in New Issue
Block a user