Files
rdbms-playground/docs/plans/20260614-adr-0053-contextual-hint-H2.md
T
claude@clouddev1 4a5fd1b5c1 feat(hint): H2 Phase B — per-form keying + the three exemplars (ADR-0053)
The first exemplar (`add 1:n relationship`) showed per-node keying is
too coarse for multi-form commands, so revise the mechanism to per-form.

- CommandNode `hint_id: Option<&str>` -> `hint_ids: &[&str]` (mirrors
  usage_ids); hint_key_for_input_in_mode reuses a factored-out
  pick_form_key (shared digit/m:n/suffix form disambiguation with
  usage_key_for_input_in_mode)
- wire INSERT + ADD (all four forms) with hint_ids
- author the three approved exemplars: hint.cmd.insert,
  hint.cmd.add_relationship, hint.err.foreign_key.child_side
  (what/example/concept) + keys.rs registration
- revise ADR-0053 D3 to per-form; record clause-concept hints as a
  deferred extension (issue #37); update README + plan
- +5 tests; 2488 pass / 1 ignored, clippy clean
2026-06-15 12:18:41 +00:00

244 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<String>` 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.<class>` 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.<id>.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.<hint_id>.{what,example,
concept}` and `hint.err.<class>.{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` (12 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 AC; gate it so CI
isn't broken mid-stream (e.g. `#[ignore]` until the final batch), then
make passing it the completion criterion.
```