diff --git a/docs/adr/0053-contextual-hint-command-and-keybinding.md b/docs/adr/0053-contextual-hint-command-and-keybinding.md new file mode 100644 index 0000000..34a9197 --- /dev/null +++ b/docs/adr/0053-contextual-hint-command-and-keybinding.md @@ -0,0 +1,372 @@ +# ADR-0053: Contextual `hint` — F1 live-input keybinding + `hint` command, with a tier-3 teaching corpus (H2) + +## Status + +Accepted — implementation pending. Revised after a `/runda` review +(2026-06-14): corrected the verbosity-default fact; re-keyed tier-3 +content on a new `hint_id` (not `help_id`) so every command form — simple +and advanced-SQL — gets distinct, mode-correct content; split the +pre-submit-diagnostic and runtime-error paths; added a comprehensiveness +coverage test. The parallel question of whether the in-app `help` command +should likewise distinguish advanced-SQL forms is tracked **separately** +as Gitea issue #36 (it touches shipped, ADR-backed `help` behaviour). + +Decided in conversation 2026-06-14. Closes the last open piece of **A1** +(the canonical app-command set, ADR-0003): every app command is +implemented except `hint`, which ADR-0003's command table listed as +*"Request a hint for the current input (ADR pending)."* This ADR is that +pending decision. Tracked as **H2** in `docs/requirements.md`. + +References ADR-0003 (app-command set + the `:` escape), ADR-0019 (the +friendly error layer / H1), ADR-0021 (per-command usage templates / H1a), +ADR-0022 (ambient typing assistance — colour + hint panel + completion), +ADR-0027 (input validity indicator), ADR-0046 (sidebar navigation + +responsive input hint), ADR-0049 (input-field readline keymap), and +ADR-0051 (context/state-aware keybinding strip). + +## Context + +`hint` is the only unbuilt app command. The naive reading — "show a hint" — +hides a real subtlety, and a real cost. + +**The subtlety: a submitted `hint` command cannot see live input.** App +commands are submitted with Enter, which empties the input buffer. By the +time `hint` dispatches, the partial command it was meant to help with is +gone. So "a hint for the current input" cannot be served by a submitted +command alone — it needs a *keybinding* that acts on the live buffer +without submitting. ADR-0003 said "current input"; `requirements.md` +broadened it to "current input **or the most recent error**." Both are +wanted; they map to two different trigger surfaces. + +**The cost: the value of `hint` is content, not plumbing.** The app +already carries two tiers of contextual text: + +- **Tier 1** — terse, always-on: syntax colour (ADR-0022); the error + *headline* alone (ADR-0019, when `messages_verbosity: Short`). +- **Tier 2** — short contextual lines: the ambient typing prose / + `expected` set, shown live while typing (ADR-0022, catalogue + `hint.ambient_*` / `hint.value_slot_*`); and the error `hint:` field — + which, because `Verbosity::Verbose` is the **default** + (`src/friendly/translate.rs:46`), is shown **by default** beneath every + error headline (`messages short` is the opt-*out*, not `messages + verbose` the opt-in). + +So the verbose error hint is **already on screen by default**. If `hint` +merely re-showed it, it would duplicate what the user can already see (and +the ambient panel). To justify itself, `hint` must add a **tier 3**: a +genuinely deeper, *teaching*-grade explanation — what the command/error +means, a worked example, and the underlying relational concept. That +corpus does not exist yet, and +authoring it (to the standard of a teaching tool, where "pedagogy wins +ties") is the bulk of the work. + +The mechanism is small and reuses everything already present: the command +REGISTRY (`src/dsl/grammar/mod.rs`), the `AppCommand` enum +(`src/dsl/command.rs`), key dispatch (`App::handle_key`, +`src/app.rs:1155`), the `note_help`/`note_help_topic` renderers +(`src/app.rs:2982`/`3021`), the parser/walker expected-set +(`ParseError.expected`, `WalkResult.tail_expected`), the friendly +catalogue + `t!` macro + `keys.rs` validation, and the output styling +vocabulary (`OutputStyleClass::Hint`). + +## Decision + +### D1 — Two surfaces, no topic argument + +`hint` is delivered through **two complementary surfaces**: + +1. **F1 keybinding → live input.** Pressing **F1** while typing renders a + tier-3 hint for the command currently in the buffer, into the output + panel, **without submitting or altering the buffer**. This is the + primary, most-valuable path (it serves the literal "current input"). +2. **`hint` command → most recent error.** Submitting `hint` renders the + tier-3 expansion of the most recent error. This is why the command + exists despite the empty-buffer problem: the thing it helps with is + the *last thing you tried*, not the now-empty buffer. + +`hint` takes **no topic argument**. Explicit per-command reference is +already `help ` (H3); `hint` is purely *contextual*, which keeps +the two cleanly distinct (`hint` = "help me with what I'm doing right +now"; `help insert` = "show me the insert reference"). + +F1 is a **read-only overlay**: it never alters the input buffer, the +cursor, or the live completion memo (ADR-0022) — it only emits a block +into the output journal. (It must therefore be handled in `handle_key` +*before* the "any other key clears the memo" fall-through.) + +### D2 — Trigger matrix + +| Trigger | Buffer / state | Result | +|---|---|---| +| **F1** | non-empty input | tier-3 hint for the command being typed, plus the live "expected next" (from the walker's `tail_expected` / parser `expected`) | +| **F1** | empty input, a recent error exists | tier-3 expansion of that error | +| **F1** | empty input, no recent error | a short "getting started" pointer (press F1 while typing a command; `help` for the full list) | +| **`hint`** (submitted) | a recent error exists | tier-3 expansion of that error (primary use) | +| **`hint`** (submitted) | no recent error | the same "getting started" pointer | + +F1 is inert behind a modal and while a sidebar panel holds navigation +focus (consistent with the existing `handle_key` gates, ADR-0046); it is +active in the input context in both Simple and Advanced mode. + +**Two error sources, one namespace.** Errors come in two kinds and reach +`hint` by different routes: + +- **Pre-submit diagnostics** (the ~33 `diagnostic.*` classes — arity, + type, unknown table/column) are computed *while typing* by the walker. + The **F1 live-input path** reads the current under-cursor diagnostic + directly from the walker (the same source the ambient panel uses) and + renders its `hint.err.` block — no stored state needed. +- **Runtime errors** (the 9 `translate_error` classes) occur *after* + submit. The **`hint` command / empty-input F1** path reads them via the + stored `last_error_hint_key` (D5). + +Both render from the same `hint.err.*` namespace. **`:`-prefix handling:** +on the simple-mode one-shot escape (`: SELECT …`), command +identification for the F1 path strips the leading `:` first, so the +advanced form is matched. + +### D3 — The tier-3 content model + +Tier-3 blocks live in the friendly catalogue under the existing `hint:` +top-level namespace (where tier-2 ambient strings already live), in two +new sub-namespaces: + +- **`hint.cmd.`** — one per command **form**, keyed by a **new + `hint_id: Option<&'static str>`** field added to `CommandNode` + (`src/dsl/grammar/mod.rs:512`, parallel to the existing `help_id` / + `usage_ids`). The F1 live-input path resolves the current input to its + command node and looks up `hint.cmd.`. + + **Why a new field, not `help_id`:** `help_id` is **not** 1:1 with + command forms. The 7 advanced-mode SQL nodes (`SELECT`, `WITH`, + `SQL_INSERT/UPDATE/DELETE`, `EXPLAIN_SQL`) carry `help_id: None` *purely + to dedup the `help` command's printed list* (they share an entry word + with a simple sibling — see `grammar/mod.rs:915-918`), not because they + lack distinct content. Their SQL syntax differs from the simple-DSL + sibling's, so they **must get their own tier-3 block**. A dedicated + `hint_id` gives every one of the ~37 REGISTRY nodes — simple and + advanced-SQL alike — its own key and its own mode-correct example, with + no sharing or deferral. (The analogous gap in the `help` command is out + of scope here — issue #36.) +- **`hint.err.`** — one per error/diagnostic class, keyed by the + friendly error/diagnostic key (e.g. `hint.err.foreign_key.child_side`, + `hint.err.type_mismatch`, `hint.err.insert_arity_mismatch`). Used by + both error routes (D2). + +Each tier-3 block is a **structured entry with three labelled parts**, so +the voice stays consistent and the renderer can style them uniformly: + +```yaml +hint.cmd.dsl.insert: + what: "Add one or more rows to a table." + example: "insert into Customers values ('Ann', 'ann@x.io')" + concept: "A row is one record; each value lines up with a column, in + order. Columns typed `serial`/`shortid` fill themselves — leave them out." +``` + +- **`what`** — one or two plain sentences: what this command does / what + this error means. +- **`example`** — a single concrete, copyable line (rendered neutral, not + muted, so it stands out as runnable). +- **`concept`** — the underlying relational idea, in teaching voice; the + part that makes this tier-3 rather than tier-2. + +`concept` is optional where there is genuinely no concept beyond the +mechanics (e.g. `quit`); `what` + `example` are always present. + +### D4 — Rendering + +Both surfaces render through one new renderer, `App::note_hint*` (sibling +of `note_help`/`note_help_topic`, `src/app.rs`), emitting a small framed +block into the `output` buffer as `OutputKind::System` with +`OutputStyleClass::Hint` on the `what`/`concept` prose and `Neutral` on +the `example` line. The block is **persistent** (scrolls in the journal), +unlike the transient ambient panel — pressing F1 is an explicit request +to *keep* the deeper guidance on screen. The bottom keybinding strip +(ADR-0051) advertises F1 in the editing/typing state. + +### D5 — "Most recent (runtime) error" state + +The **runtime-error route** (submitted `hint`, and empty-input F1) needs +to map the last runtime error back to its `hint.err.` key. Runtime +errors today live only as rendered text in the `output` buffer. We add a +single small piece of `App` state — **`last_error_hint_key: +Option`** — set at the `translate_error` call sites +(`runtime.rs:2615`, `app.rs:2424`) when a friendly error is rendered, +cleared when a later command succeeds. Absent → the "getting started" +pointer. + +The **pre-submit-diagnostic route** (the F1 live-input path) needs no +stored state: it reads the current diagnostic from the walker at F1 time +(D2). This is the cleaner split the `/runda` pass surfaced — typing-time +diagnostics and post-submit runtime errors are genuinely different +sources and should not be funnelled through one stored key. + +### D6 — Content scope: comprehensive for v1 + +v1 ships tier-3 content for the **whole inventory**, not a subset (the +graceful tier-2 fallback below is a safety net, not the plan): + +- **~37 command forms** — every distinct node in `REGISTRY` gets its own + `hint.cmd.` block (app + DSL + DDL + advanced-mode SQL forms), + each with a **mode-correct example** (the advanced-SQL forms show SQL + syntax, their simple siblings show DSL — no sharing). +- **9 runtime error classes** — `unique`, `foreign_key` (×4 sides), + `not_null`, `check`, `type_mismatch`, `not_found`, `already_exists`, + `generic`, `invalid_value` — each gets a `hint.err.*` block. +- **~33 `diagnostic.*` pre-submit classes** — arity, type, unknown + table/column, etc. — each gets a `hint.err.*` block. + +The full enumerated checklist is the implementation plan's tracking +artifact (see *Content inventory*, below). + +**Fallback (safety net):** if a tier-3 key is ever missing at runtime, +the surface degrades to tier 2 — the ambient prose for the command path, +or the verbose error `hint:` for the error path — never to a blank or an +error. The `keys.rs` build-time validation keeps the corpus honest, so a +missing key is caught in tests, not in front of a student. + +### D7 — Authoring process: exemplars-first + +Because the corpus is large and its *voice* is a pedagogical decision the +maintainer owns, content is produced in two stages: + +1. This ADR carries **2–3 worked exemplars** (below) as the canonical + style reference. The `/runda` review of this ADR is where the voice and + depth are approved. +2. Once approved, the remaining blocks are authored to that template in + **reviewable batches** (grouped by area: DDL, DML, app commands, + error classes), not one monolithic drop. + +### Exemplars (the style reference to approve) + +**Command (F1 live-input), `insert`:** + +``` +Hint — insert + What: Add one or more rows to a table. + Example: insert into Customers values ('Ann', 'ann@x.io') + Concept: A row is one record; each value lines up with a column, in + order. Columns typed serial/shortid fill themselves — leave + them out. + Next: a value list `(...)`, or `(col, ...) values (...)` to name columns +``` +(The "Next:" line is the live expected-set from the walker, shown only on +the non-empty-input F1 path.) + +**Error (`hint` command), foreign-key child-side violation:** + +``` +Hint — no parent row to point at + What: The value you inserted into Orders.customer_id doesn't match + any Customers row, so the foreign key has nothing to point at. + Example: First insert into Customers values ('Ann', ...) + Then insert into Orders values (..., 'Ann') + Concept: A foreign key is a promise that every child points at a real + parent. The parent must exist first. To allow orphans on + delete instead, set the relationship's `on delete` to + `set null` or `cascade`. +``` + +**Command (F1 live-input), `add 1:n relationship`:** + +``` +Hint — add relationship + What: Link two tables so a parent row can own many child rows. + Example: add 1:n relationship from Customers.id to Orders.customer_id + Concept: The "1:n" means one parent, many children. The child column + holds the foreign key; `--create-fk` adds it for you if it + doesn't exist yet. +``` + +## Forks (all user-chosen, 2026-06-14) + +- **Trigger model:** both a keybinding (live input) and a submitted + command (last error), rather than command-only or keybinding-only — the + live-input path is the most useful, but the command completes the A1 + slot and serves the error case. +- **Keybinding = F1:** the universal help convention; the key is + genuinely free (no `KeyCode::F(1)` binding exists today — the `"F1"` + strings in `input_render.rs`/tests are scenario labels, not the key, and + ADR-0022 uses no `F1` requirement label). No collision with the ADR-0049 + readline keys, `Ctrl-O` (ADR-0046), `Esc`-clear, or the reserved + `Ctrl-C` cancel (I5). Rejected: `?` (a typeable character — fiddly + position-dependent handling) and a Ctrl/Alt chord (less discoverable, no + advantage). +- **No topic argument:** contextual only; `help ` already owns + explicit reference lookup. +- **Comprehensive content for v1:** the full inventory, not a starter + subset. +- **Exemplars-first authoring:** lock the voice on a few blocks, then + mass-author to template. + +## Consequences + +- **A1 closes.** With `hint` registered and built, all 15 canonical + app-level commands exist in both modes. +- **A third contextual tier exists.** Students get on-demand, teaching- + grade guidance that is deeper than the always-on colour, the headline, + the ambient one-liner, and the verbose error hint — without cluttering + those terse defaults. +- **One new keybinding (F1)** joins the keymap and the ADR-0051 strip. +- **A new `hint_id` field on `CommandNode`** (parallel to `help_id`), one + new field of `App` state (`last_error_hint_key`), and one new renderer + family (`note_hint*`); the `AppCommand` enum gains `Hint`, the grammar a + `HINT` node, the REGISTRY one entry. +- **A large, durable content corpus** (~37 command blocks + ~42 error/ + diagnostic blocks ≈ 80) enters the catalogue under `hint.cmd.*` / + `hint.err.*`, validated by `keys.rs`. This is ongoing surface area: new + commands/error classes should ship with their tier-3 hint (a checklist + item for future feature ADRs). +- **Testing:** Tier-1 unit tests for the trigger matrix (F1 with + empty/non-empty input; `hint` with/without a recent error; + `last_error_hint_key` set on the `translate_error` sites and cleared on + success; the pre-submit-diagnostic vs runtime-error routing; the `:` + strip), the command-identification logic, and the tier-2 fallback; + Tier-2 `insta` snapshots for a representative rendered hint block; + Tier-3 integration tests for the end-to-end flows (type a partial + command → F1 → block appears, **buffer and completion memo untouched**; + run a failing command → `hint` → error expansion). **A + comprehensiveness coverage test** (enforces D6): iterate the REGISTRY + and assert every node has a `hint_id` resolving to a `hint.cmd.*` block, + and every runtime-error/diagnostic class has a `hint.err.*` block — + `keys.rs` only checks that *referenced* keys resolve, not that every + command/error *has* one, so this test is what makes "comprehensive" + enforceable rather than aspirational. + +## Out of scope + +- **Per-topic `hint `** — OOS (rejected): `help ` already + serves explicit lookup; a topic arg would overlap it and double the + content-authoring surface. +- **Re-showing tier-3 inline as the always-on ambient hint** — OOS + (rejected): the ambient panel stays terse by design (ADR-0022); tier-3 + is on-demand. Promoting it would defeat the tiering. +- **Localised tier-3 content beyond `en-US`** — OOS (deferred): the + catalogue is structured for i18n (ADR-0019), but additional locales + follow the project's English-only-for-v1 stance (requirements X2). +- **`hint` for a *successful* command's deeper teaching** (e.g. "you just + created a table — here's what an index would add") — OOS (deferred): a + plausible future tier-3 use, but v1 scopes the command path to errors + and the F1 path to in-progress input. + +## Content inventory (implementation tracking) + +The implementation plan enumerates and checks off every block: + +- **`hint.cmd.`** — one per distinct `REGISTRY` node (~37), each + with its own `hint_id` and a mode-correct example: app (`save`, `save + as`, `load`, `new`, `rebuild`, `export`, `import`, `replay`, `undo`, + `redo`, `mode`, `messages`, `copy`, `help`, `hint`, `quit`); DDL + (`create table`, `create m:n`, `add column`/`relationship`/`index`, + `drop`, `rename`, `change column`); DML (`insert`, `update`, `delete`, + `show`, `seed`, `explain`, `select`/`with`). The **7 advanced-mode SQL + forms** (`SQL CREATE TABLE`, `ALTER TABLE`, `CREATE/DROP INDEX`, `DROP + TABLE`, `SQL INSERT/UPDATE/DELETE`, `EXPLAIN SQL`, raw `SELECT`/`WITH`) + each get their **own** block with SQL syntax — they do **not** reuse + their simple sibling's (this is the `/runda` correction; the parallel + `help`-side gap is issue #36). +- **`hint.err.*`** — one per runtime error class (`unique`, + `foreign_key.{child,parent}_side`, `not_null`, `check`, + `type_mismatch`, `not_found`, `already_exists`, `generic`, + `invalid_value`) and per `diagnostic.*` pre-submit class. diff --git a/docs/adr/README.md b/docs/adr/README.md index 8aeb407..594a97b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -58,3 +58,4 @@ This directory contains the project's ADRs, recorded per - [ADR-0050 — Incidental-DDL confirmations omit relationship info (structure-only)](0050-incidental-ddl-confirmations-omit-relationships.md) — **Accepted + implemented 2026-06-12 (issue #28)**, closes Gitea **#28**. **Supersedes** the incidental-DDL clause of **ADR-0044 §1** and the relationship-block half of **ADR-0016 §5**. Incidental-DDL confirmation echoes (`create table`, `add`/`drop`/`rename`/`change column`, `add`/`drop index`) now render **structure only** — header + column box + `Indexes:` + constraints — with **no `References:` / `Referenced by:` block** (neither prose nor diagram), even when the table carries relationships the user did not touch. Rationale (owner): a confirmation echo reports the change just made, not untouched relationships; ADR-0044's terse prose was the lesser of "prose vs diagram", but the right answer for these surfaces is **neither**. **Relationship-subject surfaces are unchanged** — `show table`, `add`/`drop relationship`, `show relationship` still render ADR-0044 diagrams; relationships appear only when the user asks for (`show table`) or acts on (`add`/`drop relationship`) one, and are one `show table ` away — **no information lost**. Forks both user-chosen: **scope = all incidental DDL** (not just `add column` — the rationale is uniform, the mental model clean, and it's the simpler edit) and **delete the prose renderer** (not retain it dormant — no dead code). **Mechanism:** the `handle_dsl_success` `matches!` routing is unchanged (relationship-subject → diagrams; else → `render_structure`); the change is one line inside `render_structure` (`output_render.rs` — drop the relationship-block call) since all its callers are incidental DDL, plus deletion of the orphaned `relationship_prose_lines` + `cols_disp` helpers. The prose format survives in ADR-0016 §5 + git history for a future OOS-7 always-prose setting. **Tests:** the prose-presence unit test + its snapshot removed; a new unit test asserts `render_structure` on a description carrying **both** inbound and outbound relationships emits the box but no prose; the misnamed `add_relationship_flow_shows_inbound_section_on_parent` integration test (which sent an `AddColumn`) inverted + renamed to assert the add-column echo omits the prose; the diagram tests (`show table`, `add relationship`) unaffected. **2458 pass / 0 fail / 0 skip (1 ignored), clippy clean**. `requirements.md` unaffected (ADR-tracked refinement of a decided area, like ADR-0044 itself) - [ADR-0051 — Bottom keybinding strip: context- and state-aware](0051-context-state-aware-keybinding-strip.md) — **Accepted + implemented 2026-06-13 (issue #27)**, closes Gitea **#27**. Repurposes the bottom status line into a **keystrokes-only, state-selected** strip (builds on ADR-0046 nav focus, ADR-0003 modes, ADR-0049 the #29 readline keys it now advertises, ADR-0022 the completion memo). A pure `status_bar_bindings(app) -> Vec<(key,label)>` chooses the strip by **priority, first match wins**: (1) **sidebar focus** → `Ctrl-O next pane · ↑↓/PgUp/PgDn scroll · Esc input`; (2) **completion memo live** (`last_completion`) → `Tab/Shift-Tab cycle · Esc cancel · Enter run`; (3) **history navigation** (new `App::is_browsing_history()` exposing the private `history_cursor`) → `↑↓ browse · Esc clear · Enter run`; (4) **editing** (input non-empty) → `Esc clear · Ctrl-A/E home/end · Ctrl-W del word · Enter run` (surfaces the #29 keys, closing ADR-0049's deferred advertisement); (5) **default** (empty) → `Ctrl-O sidebar · Tab complete · ↑ history · Enter run`. Priority is correct because Up clears the completion memo and Tab cancels history nav, so states 2/3 never co-occur, and the five are exhaustive for Input focus. **Typed-command words leave the strip** (`mode advanced`/`mode simple` switch, `:` one-shot) and **mode discovery moves to the empty-input hint** (`resolve_hint_lines`), **simple mode only**: `\`mode advanced\` for SQL` (the verb "type" omitted — the prompt implies it; advanced mode shows **no** pointer per a post-trial user decision — a switcher knows how they got there and `help` covers the way back). The one-shot's old `Backspace cancel one-shot` label is subsumed by the editing state (behaviour intact). Forks all user-chosen: **editing state shows the #29 keys** (vs unadvertised); **`Ctrl-C quit` omitted** from the strip (vs always shown); **no width-drop machinery** — the longest strip (~65 cols) fits all supported widths, so a **width-budget unit test** keeps it lean by construction instead (the user's own observation). Catalog: 12 new `shortcut.*` labels + the `panel.hint_mode_advanced` string added to `en-US.yaml`+`keys.rs` (validator-checked 1:1), 5 now-dead strip strings removed. **Modal-aware strip is OOS** (pre-existing: a modal owns the keyboard and carries its own hints; the strip under it is unchanged-in-kind, not worsened). Tests: 9 Tier-1 unit (per-state key sets — completion/history driven through real key events; width budget; mode-pointer presence/absence), 1 Tier-3 rewritten (`status_bar_is_keystroke_only_and_state_aware`), 15 full-panel snapshots re-accepted (reviewed — strip/hint only). **2467 pass / 0 fail / 0 skip (1 ignored), clippy clean.** OOS: modal-aware strip; a full-key cheatsheet overlay; Ctrl-K/U advertisement (editing strip shows the highest-value subset within the width budget) - [ADR-0052 — Mode-tagged history for cross-mode recall](0052-mode-tagged-history-cross-mode-recall.md) — **Accepted + implemented 2026-06-13 (issue #30)**, closes Gitea **#30** — the feature (advanced history reusable in simple mode) **and** the bug in its comment (the `:` one-shot prefix lost across sessions). **Amends ADR-0034** (status field gains a `:adv` tag; **journaling moves from the worker to the dispatch layer**), **ADR-0015 §5/§6** (history.log leaves the worker transaction — `commit-db-last` now scopes yaml/csv/db only), and **ADR-0040** (a success-path journal-write failure is best-effort, not fatal); references ADR-0003. **Root cause:** history carried no mode, and the in-memory ring stored the raw `:select 1` while the worker journalled the *stripped* `select 1`, so the `:` was lost on disk. **Fix:** record the submission mode per entry as a **`:adv` suffix on the status token** (`ok`/`ok:adv`/`err`/`err:adv`) — `source` stays last + canonical so replay is unaffected; the in-memory ring (still `Vec`) stores advanced entries in their `: `-prefixed simple-mode runnable form (a leading `:` unambiguously marks advanced since simple DSL never starts with `:`); recall **strips the `:` in advanced mode** (runs as bare SQL) and keeps it in simple mode (runs via the one-shot escape); hydration reconstructs the `: `-prefix from the tag, so cross-session = in-session. **The architectural turn (user's call):** the first draft kept journaling in the worker + threaded the mode down (~30-site plumbing); on review the user asked why the journal is written deep in the worker when the *failure* path already journals at the top of the chain — it shouldn't (history.log is a journal, not state). So **success journaling moved up** to `spawn_dsl_dispatch` / `run_replay` / the app-command sites (next to the failure path), the worker's `finalize_persistence` now writes only yaml/csv, and the journal write became **best-effort** (the command is already committed — consistent with the failure path; a rare disk-full leaves a committed command unjournalled, state intact). **App commands** journal simple (dispatched outside the spawn) and `submit` excludes them from the ring's advanced flag, so `undo`/`mode advanced` recall bare. Forks user-chosen: status-tag format (vs 4th field / `:`-in-source); unified scope; **dispatch-layer best-effort journaling** (vs worker-coupled-fatal). Two `/runda` passes (the second drove the relocation + app-command exclusion). Tests: the 15 worker-level journaling tests retired (worker no longer journals — yaml/csv/operation checks kept), re-covered at the new layer (history.rs status-tag + `:`-reconstruct; app.rs recall matrix; the #30 cross-session regression in `iteration6`; replay tests cover `run_replay` journaling). **2471 pass / 0 fail / 0 skip (1 ignored), clippy clean.** replay re-journaling mode-fidelity (a replayed advanced line re-journals simple — not a regression). **Follow-up done 2026-06-14:** the vestigial worker `source` plumbing was fully unwound (compiler-guided, no behaviour change) — `_source` removed from `finalize_persistence`/`do_rebuild_from_text`, the three `*_request` wrappers inlined+deleted, the dead `source` param dropped from the ~30 forwarding worker handlers, and the `source` field removed from the `DescribeTable`/`QueryData`/`RunSelect` requests + their `DatabaseHandle` methods (~164 mostly-test call sites); the only worker `source` left is the snapshot/undo label (see ADR-0052 *Consequences*) +- [ADR-0053 — Contextual `hint` command and keybinding](0053-contextual-hint-command-and-keybinding.md) — **Accepted, implementation pending (2026-06-14)**. Settles the `hint` slot ADR-0003 left "ADR pending"; closes the last open piece of **A1** and tracks requirements **H2**. **Two surfaces:** an **F1 keybinding** that renders a deep hint for the *live* partial input without submitting (the primary path — a submitted `hint` command can't see the buffer it would help with, since Enter empties it), and a submitted **`hint` command** that expands on the *most recent error*. **No topic argument** (contextual only — `help ` already owns explicit reference). Introduces a **tier-3 teaching layer**, deeper than the existing tier-1 (colour / error headline) and tier-2 (ambient one-liner; and the error `hint:`, which is shown **by default** since `Verbosity::Verbose` is the default — `messages short` is the opt-*out*); without it `hint` would just duplicate what's already on screen. Tier-3 content lives in the catalogue under `hint.cmd.` (per command form) and `hint.err.` (per error/diagnostic class), each a structured `what`/`example`/`concept` block rendered via a new `note_hint*` family with `OutputStyleClass::Hint`. **Keyed on a new `hint_id` field on `CommandNode`, not `help_id`** (`/runda` correction): `help_id` is not 1:1 with command forms — the 7 advanced-mode SQL nodes carry `help_id: None` purely to dedup the `help` *list*, so they'd be unkeyable and would wrongly share their simple sibling's content despite different syntax; a dedicated `hint_id` gives every one of the ~37 REGISTRY nodes its own mode-correct block (the parallel `help`-side gap is tracked as issue **#36**). Two error routes share `hint.err.*`: pre-submit `diagnostic.*` read live from the walker (F1 path), runtime `translate_error` classes via stored `last_error_hint_key` (`hint` command / empty-F1). Adds `AppCommand::Hint`, a `HINT` grammar node + REGISTRY entry, the `hint_id` field, and `last_error_hint_key`; F1 is a read-only overlay (buffer + completion memo untouched). **Content is the bulk of the work** (the mechanism is ~a day): **comprehensive for v1** — ~37 command forms + 9 runtime error classes + ~33 `diagnostic.*` classes ≈ 80 teaching blocks — authored **exemplars-first** (voice approved in this ADR's `/runda` review, then mass-authored in batches), enforced by a **comprehensiveness coverage test** (every node/error class has a key), with graceful fall-back to tier-2 if a key is ever missing. Forks user-chosen: two-surface model; **F1** (vs `?` / a chord); no-arg; comprehensive scope; exemplars-first. OOS: per-topic `hint ` (rejected — overlaps `help`); always-on tier-3 (rejected — keeps ambient terse); non-`en-US` locales + success-command teaching (deferred); the `help`-side advanced-SQL gap (issue #36)