docs(plan): H2 contextual hint implementation plan (ADR-0053)
Phased build plan: mechanism skeleton with tier-2 fallback first (hint_id field, AppCommand::Hint, F1 read-only overlay, last_error_hint_key, note_hint* renderer), then catalogue + the three approved exemplars, then comprehensive content in batches, then polish. Reuses the existing command_for_entry_word / usage_keys_for_input_in_mode lookups for command identification. Test spine includes the comprehensiveness coverage test that gates "comprehensive for v1".
This commit is contained in:
@@ -0,0 +1,233 @@
|
|||||||
|
# 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_id` field + the `HINT` node
|
||||||
|
|
||||||
|
### 3a. New `CommandNode.hint_id`
|
||||||
|
- Add `pub hint_id: Option<&'static str>` to `CommandNode`
|
||||||
|
(`src/dsl/grammar/mod.rs:512`, beside `help_id` / `usage_ids`), with a
|
||||||
|
doc comment mirroring `help_id`'s. Compiler will force every node
|
||||||
|
literal (~37, across `grammar/app.rs`, `data.rs`, `ddl.rs`) to set it —
|
||||||
|
in Phase A set them all to `None` (everything falls back to tier-2);
|
||||||
|
fill them in Phase C.
|
||||||
|
- **Why `hint_id` 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_id` is 1:1 with
|
||||||
|
forms.
|
||||||
|
|
||||||
|
### 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` (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.
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user