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
@@ -246,6 +246,22 @@ Push is the user's step.
|
||||
available at the err-append site (it is for recall today;
|
||||
verify for the journal).
|
||||
|
||||
## Implementation outcome (2026-05-24)
|
||||
|
||||
Both sub-tasks landed test-first as planned. A **third concern surfaced
|
||||
during implementation** and was resolved with the user, becoming
|
||||
**ADR-0034 Amendment 1**: making `replay history.log` work (Sub-task 2)
|
||||
exposed that the journal also records app-lifecycle commands
|
||||
(`save as` / `load` / `new` / `export` / `import` / `rebuild` / `mode`),
|
||||
which would panic the worker dispatch or abort replay. Replay now
|
||||
**skips** every app-lifecycle command + nested `replay` (re-applying
|
||||
only schema/data writes); all skips continue (the prior nested-`replay`
|
||||
refusal is reversed), with a `[skip]` warning on `import` / nested
|
||||
`replay`. Classification is by entry word so the modal / incomplete
|
||||
forms skip uniformly rather than aborting. See the amendment in
|
||||
`docs/adr/0034-…` and `tests/replay_command.rs`. `requirements.md`
|
||||
U3 / U4 updated to match.
|
||||
|
||||
## What this plan does NOT contain
|
||||
|
||||
- Time estimates (milestones, not hours).
|
||||
|
||||
+17
-6
@@ -370,15 +370,26 @@ handoff-14 cleanup; 449 after B2/C2.)
|
||||
- [ ] **U2** `undo` restores the most recent snapshot; `redo`
|
||||
re-applies; both prompt for confirmation showing the snapshot
|
||||
timestamp and a summary of the changes that will be discarded.
|
||||
- [x] **U3** `history.log` records every successfully executed
|
||||
command in append-only form (Iteration 2). Format:
|
||||
`<ISO-8601 Z>|ok|<source>` per ADR-0015 §5.
|
||||
- [x] **U3** `history.log` records every submitted command in
|
||||
append-only form, tagged with its outcome (Iteration 2;
|
||||
broadened by ADR-0034). Format: `<ISO-8601 Z>|<status>|<source>`
|
||||
per ADR-0015 §5 / ADR-0034 §1 — `status` is `ok` for a
|
||||
successful command and `err` for one that failed to parse or
|
||||
execute. Hydration (cross-session recall) reads all records;
|
||||
replay reads `ok` only.
|
||||
- [x] **U4** `replay` runs commands from a `history.log` or
|
||||
`.commands` file. *(Implemented via ADR-0024 Phase E:
|
||||
`runtime::run_replay` parses each non-blank, non-`#`-comment
|
||||
line with the schema-aware parser and dispatches it through
|
||||
the normal pipeline; stops at the first error, no rollback;
|
||||
nested replay refused. Covered by `tests/replay_command.rs`.)*
|
||||
line in advanced mode and dispatches it through the normal
|
||||
pipeline; stops at the first genuine error, no rollback.
|
||||
ADR-0034 §3: replay reads journal records (`<ts>|<status>|
|
||||
<source>`), running `ok` records and skipping non-`ok`, while
|
||||
still accepting bare-command scripts. ADR-0034 Amendment 1:
|
||||
replay re-applies only schema/data write commands and **skips**
|
||||
every app-lifecycle command + nested `replay` — all skips
|
||||
continue (a nested `replay` is now skipped, not refused), with a
|
||||
`[skip]` warning on `import` / nested-`replay`. Covered by
|
||||
`tests/replay_command.rs`.)*
|
||||
|
||||
## Sharing and export (per ADR-0007)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user