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
12 KiB
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]toCommandNode(src/dsl/grammar/mod.rs:512, besidehelp_id/usage_ids), mirroringusage_ids— not a per-nodeOption<&str>. The Phase-B exemplar (add 1:n relationship) showed per-node keying is too coarse:add/drop/show/createare each one node spanning many forms, and a live-input hint must be specific to the typed form. Compiler forces every node literal (~37, acrossgrammar/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, reusingpick_form_key(factored out ofusage_key_for_input_in_mode— shared digit/m:n/suffix disambiguation). - Why a new field, not
help_id(ADR-0053 D3):help_idisNoneon the 7 advanced-SQL forms purely to dedup thehelplist; those forms have distinct SQL syntax and need their own block.hint_idsis per form. (The parallelhelp-side gap is issue #36; clause-concept hints are deferred — issue #37.)
3b. AppCommand::Hint + the HINT node
AppCommand::Hintvariant (no fields — no topic arg) insrc/dsl/command.rs:544.pub static HINT: CommandNodeingrammar/app.rsmirroringHELPbut with no topic shape (bare keyword, likeUNDO):entry: Word::keyword("hint"),shape: EMPTY_SEQ(asUNDO,grammar/app.rs:333),ast_builder: build_hint(returnsCommand::App(AppCommand::Hint)),help_id: Some("app.hint"),hint_id: Some("app.hint"),usage_ids: &["parse.usage.hint"].- Register
(&app::HINT, CommandCategory::Simple)inREGISTRY(grammar/mod.rs), besideHELP. (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 (advancedcreate→ the SQL nodes, simple → the DSL node) — it just returnsusage_idsrather than the node.- The only new bit: a thin
hint_id_for_input_in_mode(source, mode)(or a node-returning sibling ofusage_keys_for_input_in_mode) that applies the same mode selection and returns the chosen node'shint_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 advancedSELECTnode.- 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'shint.cmdblock + the live "Next:" expected-set from the walker). - empty input +
last_error_hint_keyset →note_hint_for_error(). - empty input + no recent error →
note_getting_started().
- non-empty input →
- Returns
Vec::new()(pure output emission, likehelp). demo_badge_label(app.rs:520) gains anF1 → "[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>toApp. 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. Thehintcommand 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'shint.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 ofnote_help/note_help_topic,app.rs:2982/3021). - A tier-3 block is structured (
what/example/concept, plus the liveNext:line on the input path). The catalogue stores each part under sub-keys (hint.cmd.<id>.what,.example,.concept); the renderer fetches each viat!and lays them out as a small framed block. - Styling:
OutputKind::System;OutputStyleClass::Hint(muted) onwhat/concept/Next,Neutralonexampleso the runnable line stands out. ReuseOutputLine::styled+push_category_three_prosepatterns (app.rs:3121). - Fallback: if a node's
hint_idisNoneor a key is missing, degrade to tier-2 (ambient prose for the input path; the verbose errorhint:for the error path) — never blank.
8. Catalogue + keys.rs
- New sub-namespaces under the existing top-level
hint:insrc/friendly/strings/en-US.yaml:hint.cmd.<hint_id>.{what,example, concept}andhint.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.hintstrings 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:
- App commands (~16): save/save as/load/new/rebuild/export/import/ replay/undo/redo/mode/messages/copy/help/hint/quit.
- DDL (simple): create table, create m:n, add column/relationship/ index, drop, rename, change column.
- DML (simple): insert, update, delete, show, seed, explain, select/with.
- 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.
- Runtime error classes (9): unique, foreign_key ×{child,parent}, not_null, check, type_mismatch, not_found, already_exists, generic, invalid_value.
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;
hintcommand + error → error block;hint+ none → getting-started. last_error_hint_keyset 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, andlast_completionunchanged. - tier-2 fallback when
hint_id/key absent.
- trigger matrix: F1 non-empty → command block; F1 empty + recent error
→ error block; F1 empty + none → getting-started;
- Tier 2 (
insta): snapshot a representative rendered tier-3 block (theinsertexemplar) 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
REGISTRYand assert every node has ahint_idresolving to ahint.cmd.*block; assert every runtime-error +diagnostic.*class has ahint.err.*block. Red until Phase C completes — enable (un-ignore) as the final gate. keys.rsvalidation 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.mdH2 → [x] and A1 → [x] (A1 closes whenhintlands).
13. Risks / watch-list
- Command-identification reuse. The lookup exists
(
command_for_entry_word+ the mode-awareusage_keys_for_input_in_mode,grammar/mod.rs:811/564); the only new code is a thin node/hint_idvariant 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.rsregistration 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
conceptline 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.