# Plan — ADR-0053: contextual `hint` command + F1 keybinding (H2) Implements ADR-0053. Closes the last open piece of **A1** (the canonical app-command set) and requirements **H2**. No Gitea issue — this is requirements-driven work; any genuine "later" item found en route gets its own issue (cf. #36, already filed for the parallel `help`-side gap). ## 1. Goal Give learners on-demand, **teaching-grade** contextual help — a *third* tier beneath the existing terse always-on text (tier 1) and the short contextual lines that are already shown (tier 2: the live ambient prose, and the error `hint:` which is on by default since `Verbosity::Verbose` is the default). Two surfaces: - **F1** (read-only overlay) → a tier-3 block for the **live partial input**, or — on empty input — for the **most recent runtime error**. - **`hint`** (submitted app command) → the tier-3 block for the **most recent runtime error** (the buffer is empty post-submit, so it can only act on recent context). The mechanism is small; the **content corpus is the feature** (~80 blocks, comprehensive for v1, authored exemplars-first per ADR-0053 D7). ## 2. The shape of the work (why this order) The mechanism and the content are separable, and the mechanism should land first with **graceful tier-2 fallback** so every surface works before any tier-3 text exists. That lets us: - build + test the trigger matrix / routing / `:`-strip / read-only- overlay behaviour against a skeleton (TDD), then - pour in content in reviewable batches without re-touching the wiring, - and turn on the **comprehensiveness coverage test** only once the corpus is complete (it is red until then — by design). Build order: **Phase A** (mechanism skeleton, falls back to tier-2) → **Phase B** (catalogue structure + the three approved exemplars) → **Phase C** (comprehensive content, batched) → **Phase D** (polish: strip advertisement, snapshots, full green). ## 3. Grammar: the `hint_ids` field + the `HINT` node ### 3a. New `CommandNode.hint_ids` (per-form — revised in Phase B) - Add `pub hint_ids: &'static [&'static str]` to `CommandNode` (`src/dsl/grammar/mod.rs:512`, beside `help_id` / `usage_ids`), **mirroring `usage_ids`** — *not* a per-node `Option<&str>`. The Phase-B exemplar (`add 1:n relationship`) showed per-*node* keying is too coarse: `add`/`drop`/`show`/`create` are each one node spanning many forms, and a live-input hint must be specific to the typed form. Compiler forces every node literal (~37, across `grammar/app.rs`, `data.rs`, `ddl.rs`) to set it — Phase A/B leave most `&[]` (tier-2 fallback); Phase C fills them. **Multi-form nodes list ALL their form keys** (e.g. `add` → `["add_column", "add_relationship", "add_index", "add_constraint"]`) so the form-word disambiguation resolves correctly and unauthored forms fall back at render rather than mis-resolving to a sibling. - **Lookup:** `hint_key_for_input_in_mode(source, mode)` returns the single typed form's hint stem, reusing `pick_form_key` (factored out of `usage_key_for_input_in_mode` — shared digit/`m:n`/suffix disambiguation). - **Why a new field, not `help_id`** (ADR-0053 D3): `help_id` is `None` on the 7 advanced-SQL forms purely to dedup the `help` *list*; those forms have distinct SQL syntax and need their own block. `hint_ids` is per form. (The parallel `help`-side gap is issue #36; clause-concept hints are deferred — issue #37.) ### 3b. `AppCommand::Hint` + the `HINT` node - `AppCommand::Hint` variant (no fields — no topic arg) in `src/dsl/command.rs:544`. - `pub static HINT: CommandNode` in `grammar/app.rs` mirroring `HELP` but with **no topic shape** (bare keyword, like `UNDO`): `entry: Word::keyword("hint")`, `shape: EMPTY_SEQ` (as `UNDO`, `grammar/app.rs:333`), `ast_builder: build_hint` (returns `Command::App(AppCommand::Hint)`), `help_id: Some("app.hint")`, `hint_id: Some("app.hint")`, `usage_ids: &["parse.usage.hint"]`. - Register `(&app::HINT, CommandCategory::Simple)` in `REGISTRY` (`grammar/mod.rs`), beside `HELP`. (App commands are available in both modes via the existing mechanism.) ## 4. Command identification (live-input → node) The F1 live-input path needs "which command form is being typed." **The lookup machinery already exists** — do not rebuild entry matching: - `command_for_entry_word(word) -> Option<(usize, &'static CommandNode)>` (`grammar/mod.rs:811`) returns the matched node for an entry word (Simple-first; the caller extracts the first word of the input). - `usage_keys_for_input_in_mode(source, mode)` (`grammar/mod.rs:564`) already performs the **mode-aware** Simple/Advanced selection the hint path needs (advanced `create` → the SQL nodes, simple → the DSL node) — it just returns `usage_ids` rather than the node. - **The only new bit:** a thin `hint_id_for_input_in_mode(source, mode)` (or a node-returning sibling of `usage_keys_for_input_in_mode`) that applies the same mode selection and returns the chosen node's `hint_id`. Mirror the existing function; don't duplicate its matching. - **`:`-strip:** in Simple mode, strip a leading `:` (one-shot escape, ADR-0003) before identification so `: SELECT …` resolves to the advanced `SELECT` node. - No match (empty / unrecognised entry word) → the "getting started" pointer (D2). ## 5. F1 keybinding (read-only overlay) In `App::handle_key` (`src/app.rs:1155`): - Add an F1 arm (`KeyCode::F(1)`) **after** the modal gate and the sidebar-nav gate (inert there, per D2), and **before** the "any other key clears the completion memo" fall-through (`_ => self.last_completion = None`, ~line 1228) — F1 must **not** clear the memo or touch the buffer/cursor (D1). - Behaviour (the trigger matrix, D2): - non-empty input → `note_hint_for_input()` (the command's `hint.cmd` block + the live "Next:" expected-set from the walker). - empty input + `last_error_hint_key` set → `note_hint_for_error()`. - empty input + no recent error → `note_getting_started()`. - Returns `Vec::new()` (pure output emission, like `help`). - `demo_badge_label` (`app.rs:520`) gains an `F1 → "[F1]"` entry so demo mode surfaces it (ADR-0047). ## 6. The two error routes (D2 / D5) - **Runtime errors:** add `last_error_hint_key: Option` to `App`. Set it where friendly errors are rendered (`runtime.rs:2615`, `app.rs:2424`) from the error's class key; clear on the next successful command. The `hint` command and empty-input F1 read it. - **Pre-submit diagnostics:** the F1 live-input path, when the input carries an under-cursor diagnostic, reads it straight from the walker (`input_diagnostics_in_mode`, the same source the ambient panel uses) and renders that diagnostic's `hint.err.` block instead of (or alongside) the command block. No stored state. - Both render from `hint.err.*`. ## 7. Rendering: the `note_hint*` family (D4) - New `App::note_hint_for_input`, `note_hint_for_error`, `note_getting_started` (siblings of `note_help`/`note_help_topic`, `app.rs:2982`/`3021`). - A tier-3 block is **structured** (`what` / `example` / `concept`, plus the live `Next:` line on the input path). The catalogue stores each part under sub-keys (`hint.cmd..what`, `.example`, `.concept`); the renderer fetches each via `t!` and lays them out as a small framed block. - Styling: `OutputKind::System`; `OutputStyleClass::Hint` (muted) on `what`/`concept`/`Next`, `Neutral` on `example` so the runnable line stands out. Reuse `OutputLine::styled` + `push_category_three_prose` patterns (`app.rs:3121`). - **Fallback:** if a node's `hint_id` is `None` or a key is missing, degrade to tier-2 (ambient prose for the input path; the verbose error `hint:` for the error path) — never blank. ## 8. Catalogue + `keys.rs` - New sub-namespaces under the existing top-level `hint:` in `src/friendly/strings/en-US.yaml`: `hint.cmd..{what,example, concept}` and `hint.err..{what,example,concept}`. - Register every key + its placeholders in `src/friendly/keys.rs` (`KEYS_AND_PLACEHOLDERS`) so the build-time validation covers them. - `parse.usage.hint` + `help.app.hint` strings for the command itself. ## 9. Content (Phase C — the bulk, batched per D7) Exemplars approved in the ADR (`insert` live-input, FK child-side error, `add relationship`) are the template. Author in reviewable batches: 1. **App commands** (~16): save/save as/load/new/rebuild/export/import/ replay/undo/redo/mode/messages/copy/help/hint/quit. 2. **DDL** (simple): create table, create m:n, add column/relationship/ index, drop, rename, change column. 3. **DML** (simple): insert, update, delete, show, seed, explain, select/with. 4. **Advanced-mode SQL forms** (7): SQL CREATE TABLE, ALTER TABLE, CREATE/DROP INDEX, DROP TABLE, SQL INSERT/UPDATE/DELETE, EXPLAIN SQL — **own blocks, SQL-syntax examples**. 5. **Runtime error classes** (9): unique, foreign_key ×{child,parent}, not_null, check, type_mismatch, not_found, already_exists, generic, invalid_value. 6. **`diagnostic.*` classes** (~33): arity/type/unknown-table-column/etc. Each block: `what` (1–2 sentences), `example` (one runnable line, mode-correct), `concept` (the relational idea — the teaching part; optional only where genuinely none, e.g. `quit`). ## 10. Tests Written test-first against the Phase-A skeleton where possible. - **Tier 1 (unit, `app.rs`):** - trigger matrix: F1 non-empty → command block; F1 empty + recent error → error block; F1 empty + none → getting-started; `hint` command + error → error block; `hint` + none → getting-started. - `last_error_hint_key` set on a failing command, cleared on the next success. - routing: a pre-submit diagnostic on the input drives the diagnostic `hint.err`; a runtime error drives the stored-key route. - `:`-strip: `: SELECT …` in Simple mode resolves to the advanced node. - **read-only overlay:** F1 leaves `input`, `input_cursor`, and `last_completion` unchanged. - tier-2 fallback when `hint_id`/key absent. - **Tier 2 (`insta`):** snapshot a representative rendered tier-3 block (the `insert` exemplar) so the framed layout + styling spans are locked. - **Tier 3 (integration, `tests/it/`):** type a partial command → F1 → block appears, buffer untouched; run a failing insert → `hint` → FK error expansion. - **Comprehensiveness coverage test** (enforces D6, the key one): iterate `REGISTRY` and assert every node has a `hint_id` resolving to a `hint.cmd.*` block; assert every runtime-error + `diagnostic.*` class has a `hint.err.*` block. **Red until Phase C completes** — enable (un-`ignore`) as the final gate. - `keys.rs` validation continues to guarantee every *referenced* key resolves. ## 11. Keybinding strip + discoverability (Phase D) - The ADR-0051 bottom strip advertises **F1 = hint** in the editing/ typing state (and on the empty-input state, since F1 still does something there). Re-accept the affected full-panel snapshots. ## 12. ADR / docs - ADR-0053 is committed (`e16ad50`). On completion, flip its Status from "implementation pending" to implemented (with date), and update the README index entry + `requirements.md` **H2 → [x]** and **A1 → [x]** (A1 closes when `hint` lands). ## 13. Risks / watch-list - **Command-identification reuse.** The lookup exists (`command_for_entry_word` + the mode-aware `usage_keys_for_input_in_mode`, `grammar/mod.rs:811`/`564`); the only new code is a thin node/`hint_id` variant that reuses their selection. Do **not** re-implement entry-word matching — mirror the existing functions. - **Structured-key ergonomics.** Three sub-keys per block × ~80 blocks is ~240 catalogue keys; keep the `keys.rs` registration generation tidy (consider a helper that registers the `{what,example,concept}` triple for an id). - **Content voice drift across batches.** Re-check each batch against the approved exemplars; the `concept` line is where drift (too terse / too advanced) creeps in. Pedagogy wins ties. - **F1 terminal capture.** A few terminals intercept F1; acceptable (it's the convention) but note it if testing surfaces it. - **Snapshot churn.** The strip change re-accepts ADR-0051 snapshots; keep that diff isolated. - **Coverage-test timing.** It is red through Phases A–C; gate it so CI isn't broken mid-stream (e.g. `#[ignore]` until the final batch), then make passing it the completion criterion. ```