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:
claude@clouddev1
2026-05-24 18:59:06 +00:00
parent 504c24c996
commit e4f2f5fa15
18 changed files with 730 additions and 76 deletions
@@ -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
View File
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
View File
@@ -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)