Files
rdbms-playground/docs/adr/0053-contextual-hint-command-and-keybinding.md
T
claude@clouddev1 329adfc935 fix(hint): labelled tier-3 block format + snapshot (ADR-0053 /runda)
Final /runda found the rendered block was three bare unlabelled lines,
deviating from the approved exemplar format. Fix:
- emit_tier3_block now renders a `Hint` heading + aligned `What:` /
  `Example:` / `Concept:` lines (hint.block.* labels); concept stays muted
- lock the format with an insta snapshot (hint_block_insert)
- amend ADR-0053 D2/D4 + exemplars: drop the `Next:` line (tier-2 ambient
  already owns live position-awareness — user-confirmed), align exemplars
  to the shipped format

2499 pass / 1 ignored, clippy clean.
2026-06-15 16:45:47 +00:00

429 lines
22 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.
# ADR-0053: Contextual `hint` — F1 live-input keybinding + `hint` command, with a tier-3 teaching corpus (H2)
## Status
Accepted — **implemented 2026-06-15** (plan:
`docs/plans/20260614-adr-0053-contextual-hint-H2.md`; the F1 keybinding +
`hint` command, the `hint_ids` per-form keying + `hint_key_for_input_in_mode`,
`last_error_hint_key` + `friendly::error_hint_class`, the `note_hint*`
renderers, and the `hint.cmd.*`/`hint.err.*` corpus for every command form
+ the 9 runtime error classes, with the comprehensiveness coverage test
and the ADR-0051 strip advertising F1). Closes **A1** + requirements
**H2**. Deferred: the pre-submit-diagnostic route + `diagnostic.*` blocks
(#38), clause-concept hints (#37). Revised after a `/runda` review
(2026-06-14): corrected the verbosity-default fact; re-keyed tier-3
content off `help_id`; split the pre-submit-diagnostic and runtime-error
paths; added a comprehensiveness coverage test. Revised again during
Phase B implementation (2026-06-15): the first exemplar showed per-*node*
keying is too coarse for multi-form commands (`add`/`drop`/`show`/
`create`), so D3 now keys tier-3 content **per form** via a
`hint_ids: &[&str]` array mirroring `usage_ids` — and **clause-concept
hints** are recorded as a deferred extension (issue #37). During Phase C
the **pre-submit-diagnostic route + the ~33 `diagnostic.*` blocks** were
**deferred** (issue #38) — `Diagnostic` doesn't carry its class key, so
the route needs a broad change for marginal value (D6). v1 therefore
ships command-form hints + the 9 runtime error-class hints. The parallel
question of whether the in-app `help` command should likewise distinguish
advanced-SQL forms is tracked **separately** as Gitea issue #36.
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 <topic>` (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. (No "expected next" line — the always-on tier-2 ambient panel already shows it live; tier-2 owns position-awareness.) |
| **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.
**Error routes.** **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) and renders their
`hint.err.<class>` block. (A second route for **pre-submit diagnostics**
on the F1 live-input path was specified but is **deferred** — D6 / issue
#38; with a diagnostic present, F1 shows the command block and tier-2
shows the diagnostic.) **`:`-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.<hint_id>`** — one per command **form**, keyed by a **new
`hint_ids: &'static [&'static str]`** field on `CommandNode`
(`src/dsl/grammar/mod.rs:512`), **mirroring the existing `usage_ids`**.
The F1 live-input path resolves the current input to its form's hint key
via `hint_key_for_input_in_mode`, which reuses the same form-word
disambiguation as `usage_key_for_input_in_mode`.
**Why an array mirroring `usage_ids`, not a per-node `hint_id`**
*(`/runda`/implementation revision, 2026-06-15)*: a single per-node key
is too coarse. Several entry words are **one node spanning many forms**
`add` (column/relationship/index/constraint), `drop` (table/column/
relationship/index), `show` (data/table/tables/relationships/indexes),
`create` (table/index). A live-input hint for `add 1:n relationship` is
only useful if it is *specific to relationships*, so the content must be
**per form**, not per node. The project already solved exactly this for
usage templates (`usage_ids` is a per-form array, disambiguated by the
form word), so `hint_ids` mirrors it. Single-form nodes carry one entry;
multi-form nodes carry one per form. This also covers the advanced-SQL
forms whose `usage_ids` are empty (`SQL_INSERT/UPDATE/DELETE`,
`EXPLAIN_SQL`) — they get their own `hint_ids` directly, independent of
usage, with mode-correct SQL examples. (The `help`-list collapse of
advanced-SQL forms is a separate gap — issue #36.)
**Deferred extension — clause-concept hints** (issue #37): per-form is
the right granularity for tier-3 *teaching* (position-awareness within a
form is owned by tier-2 ambient + the live `Next:` line, D4). But some
**concepts live inside a clause**, not a form — `… on delete ⟨cascade|
set null|restrict⟩` (referential actions), the `create table` constraint
slots (`primary`/`unique`/`check`/`foreign`), `with pk`, `1:n`/`m:n`
cardinality. A learner parked in such a clause may want teaching deeper
than tier-2's candidate list but narrower than the whole-form block. v1
does **not** build this (it would multiply content for points whose value
we can't yet measure, and we don't expect to accumulate usage statistics
to drive it empirically — it will be tackled as a deliberate follow-up
job). The keying does not lock it out: a later `hint.concept.<topic>`
namespace can be surfaced when the cursor sits in a recognized clause,
layered on top of the per-form block.
- **`hint.err.<class>`** — 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 the `App::note_hint*` family (sibling of
`note_help`/`note_help_topic`, `src/app.rs`) via `emit_tier3_block`,
emitting into the `output` buffer as `OutputKind::System`: a **`Hint`
heading** followed by aligned **`What:` / `Example:` / `Concept:`** lines
(labels + heading from `hint.block.*`). The `concept` line is muted
(`OutputStyleClass::Hint`); the rest are plain. 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. Its rendered shape is locked by an `insta` snapshot
(`hint_block_insert`). The bottom keybinding strip (ADR-0051) advertises
F1 in the editing (leading) and default states.
### 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.<class>` 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<String>`** — 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 reading the
under-cursor diagnostic) is **deferred** — see the scope note in D6.
### D6 — Content scope for v1
v1 ships tier-3 content for the **command forms and runtime error
classes** — comprehensive for those (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.<hint_id>` 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` (child/parent
side), `not_null`, `check`, `type_mismatch`, `not_found`,
`already_exists`, `generic`, `invalid_value` — each gets a
`hint.err.*` block.
**Deferred — the ~33 `diagnostic.*` pre-submit classes and the F1
diagnostic route** *(Phase C scope decision, 2026-06-15; issue #38)*. The
original "comprehensive" scope included them, but implementation revealed
`Diagnostic` (`walker/outcome.rs`) carries only its rendered `message`,
not its class key — so a live diagnostic can't be mapped to
`hint.err.<class>` without adding a `class` field threaded through every
diagnostic-creation site (a broad change). Weighed against the value, it
isn't worth it for v1: pre-submit diagnostics are already surfaced by
tier-2 (ambient message + validity indicator, ADR-0027); F1 still shows
the useful command block when a diagnostic is present; and many
diagnostic classes duplicate runtime classes already covered
(`type_mismatch`, `unknown_table``not_found`, arity↔`invalid_value`).
Deferred to issue #38, additively (the keying doesn't lock it out).
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 **23 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; shipped as the rendered format)
**Command (F1 live-input), `insert`** (the rendered shape, locked by the
`hint_block_insert` snapshot — a `Hint` heading + aligned labels, no
`Next:` line since tier-2 owns position-awareness):
```
Hint
What: Add one or more rows to a table.
Example: insert into Customers values ('Ann', 'ann@example.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.
```
**Error (`hint` command), foreign-key child-side violation:**
```
Hint
What: The value you gave for the child column doesn't match any
parent row, so the foreign key has nothing to point at.
Example: First insert the parent (insert into Customers …), then the
child that references it.
Concept: A foreign key is a promise that every child points at a real
parent, so 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
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 <topic>` 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_ids: &[&str]` field on `CommandNode`** (mirroring
`usage_ids`) + a `hint_key_for_input_in_mode` lookup (reusing the
`usage_key_for_input_in_mode` form-disambiguation), 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 durable content corpus** (~37 command blocks + 10 runtime
error-class blocks) 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). (Diagnostic-class blocks deferred — #38.)
- **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 mode-aware form resolution; 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 with a
`hint_ids` entry resolves to a `hint.cmd.*` block, and every runtime
error class resolves to 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 the scope enforceable rather than
aspirational. (Diagnostic classes are out of this scope — D6 / #38.)
## Out of scope
- **Per-topic `hint <topic>`** — OOS (rejected): `help <topic>` 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.
- **Clause-concept hints** (`… on delete ⟨action⟩`, constraint slots,
`with pk`, cardinality) — OOS (deferred, issue #37): a
`hint.concept.<topic>` layer surfaced when the cursor sits in a
recognized clause, deeper than tier-2's candidate list but narrower than
the per-form block. Per-form keying (D3) does not lock it out. To be
tackled as a deliberate follow-up job, not gated on usage statistics.
- **Pre-submit-diagnostic route + `diagnostic.*` tier-3 blocks** — OOS
(deferred, issue #38): needs a class field on `Diagnostic` threaded
through every creation site (broad change) for marginal value, since
tier-2 already surfaces diagnostics and many duplicate runtime classes
(D6).
## Content inventory (implementation tracking)
The implementation plan enumerates and checks off every block:
- **`hint.cmd.<hint_id>`** — 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`). The `diagnostic.*` pre-submit classes are **deferred**
(D6 / issue #38).