feat: ADR-0034 — history journal records err + replay parses/filters the journal
Replay (§3): run_replay parses <ts>|<status>|<source> journal records — runs ok, skips non-ok — while still accepting bare .commands scripts (prefix-detected so a | inside a bare command isn't misread). Fixes replay history.log, which died on line 1. Journal failures (§1/§2): failed commands are recorded err via a new Action::JournalFailure, emitted by the pure-sync App for both parse failures and worker-execution failures (runtime appends best-effort, never fatal). Hydration reads all records so typo'd/rejected commands are recallable across sessions. Amendment 1 — replay filters app-lifecycle commands: a working replay history.log exposed that the journal also records save as/load/new/export/import/rebuild/mode (which would panic the worker dispatch or abort replay). Replay now re-applies only schema/data writes and skips every app-lifecycle command + nested replay, classified by entry word so modal/incomplete forms (save as, bare mode) and quit skip uniformly rather than aborting. All skips continue (reversing the nested-replay refusal); import and nested replay warn. replay.error_nested removed; replay.skipped_import/_replay added; ReplayCompleted carries warnings. requirements.md U3/U4 updated; app-command runtime-failure journalling tracked as a follow-up. 1659 passing / 0 failing / 0 skipped / 1 ignored. Clippy clean.
This commit is contained in:
@@ -219,6 +219,93 @@ code lands as two tracked sub-tasks (test-first):
|
||||
the `err` ones; a plain `.commands` script still replays
|
||||
unchanged; a bare command containing `|` is not misparsed.
|
||||
|
||||
Both shipped 2026-05-24 (test-first), along with Amendment 1.
|
||||
|
||||
**Deferred follow-up (user-confirmed, 2026-05-24).** `err`
|
||||
journalling covers parse failures of *any* submitted line and
|
||||
DSL/SQL *worker* execution failures (which surface as
|
||||
`DslFailed`). An app-lifecycle command that *parses* and then
|
||||
fails at the *runtime* stage (e.g. a `save as` / `import` that
|
||||
fails on I/O) is **not** yet journalled `err` — those failures
|
||||
surface as their own runtime events, not `DslFailed`. Recording
|
||||
them is a tracked follow-up; the recall-typos motivation of §2 is
|
||||
already met by parse-failure journalling.
|
||||
|
||||
## Amendment 1 — Replay filters out app-lifecycle commands (2026-05-24)
|
||||
|
||||
This amendment **extends Decision §2's "each consumer filters"** to a
|
||||
second filter dimension on replay, and **supersedes the runtime's
|
||||
prior nested-`replay` refusal**. It was written during implementation,
|
||||
after the §3 change (replay learning the journal format) made a latent
|
||||
problem reachable, and is recorded with explicit user approval.
|
||||
|
||||
### The finding — a working `replay history.log` exposes app commands
|
||||
|
||||
§3 makes `replay history.log` actually work. But the journal is a
|
||||
*complete* record (§1), and the runtime journals successful
|
||||
app-lifecycle commands too — `save as` / `load` / `new` (project
|
||||
switches), `export` / `import`, `rebuild`, `mode`. Before §3 these were
|
||||
unreachable (replay died on line 1); now replay reaches them, and:
|
||||
|
||||
- the ones that parse to `Command::App` (`export`, `mode`, `rebuild`,
|
||||
…) hit the worker dispatch's `unreachable!()` arm — a **panic**;
|
||||
- the ones whose target comes from a modal (`save as <name>` /
|
||||
`load <name>` / `new <name>`) **fail to parse** on the command line
|
||||
and **abort** the whole replay.
|
||||
|
||||
Either way, a normal journal breaks replay. Executing these would also
|
||||
be *wrong*: they are session/project orchestration, not schema/data
|
||||
reconstruction — `load` / `new` would switch projects mid-replay.
|
||||
|
||||
### The decision — replay re-applies only state mutations
|
||||
|
||||
Replay re-applies **only the schema/data write commands** (create/drop/
|
||||
alter table, add/drop/change column, add/drop relationship, add/drop
|
||||
index, add/drop constraint, insert/update/delete — DSL and SQL). It
|
||||
**skips** every `Command::App(_)` and a nested `Command::Replay`. Reads
|
||||
(`show` / `select` / `explain`) still run (harmless; they never appear
|
||||
in a journal anyway).
|
||||
|
||||
- **All skips continue** — replay never aborts on a skippable command.
|
||||
This reverses the prior nested-`replay` *refusal*: a journal that
|
||||
happens to contain a `replay` the user once ran must not force them
|
||||
to hand-edit the log. Skipping a nested `replay` also removes the
|
||||
infinite-loop footgun by construction (the nested file is never
|
||||
re-entered).
|
||||
- **Two skips warn; the rest are silent.** Skipping `import` or a
|
||||
nested `replay` can leave the replayed state *incomplete* (the
|
||||
imported data / the nested file's commands are not reconstructed),
|
||||
so each emits a `[skip]` warning (`replay.skipped_import` /
|
||||
`replay.skipped_replay`) surfaced in the replay summary. `save` /
|
||||
`save as` / `load` / `new` / `export` / `mode` / `messages` /
|
||||
`help` / `quit` / `rebuild` skip silently — omitting them changes
|
||||
nothing about the reconstructed schema/data.
|
||||
|
||||
### Detection
|
||||
|
||||
A line is classified after the §3 status filter: parse it; a
|
||||
`Command::App(_)` or `Command::Replay` is skipped (warned for `import`
|
||||
/ `replay`); a write/read is dispatched. The modal forms (`save as` /
|
||||
`load` / `new`) do not parse, so a parse failure whose entry word is
|
||||
one of those is skipped rather than aborting — any *other* parse
|
||||
failure is a genuine malformed command and still stops replay with a
|
||||
line-numbered error (so a typo in a hand-built script fails loudly).
|
||||
|
||||
### Consequences
|
||||
|
||||
- `AppEvent::ReplayCompleted` carries `warnings: Vec<String>`; the App
|
||||
renders them after the `[ok] replay …` summary.
|
||||
- The `replay.error_nested` catalog key is removed (nested replay is
|
||||
no longer an error); `replay.skipped_import` / `replay.skipped_replay`
|
||||
are added.
|
||||
- A journal whose later write command depended on `import` /
|
||||
`rebuild`-created state may not fully replay (that command can fail
|
||||
on the missing dependency). This is accepted: replay reconstructs the
|
||||
*command sequence*, and externally-sourced data is not a reproducible
|
||||
command. The `import` warning flags exactly this risk.
|
||||
- ADR-0006's "`history.log` can be replayed" is now true in practice
|
||||
for the state-building subset.
|
||||
|
||||
## See also
|
||||
|
||||
- ADR-0004 — the project format `history.log` lives in.
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user