Files
rdbms-playground/docs/plans/20260614-adr-0053-contextual-hint-H2.md
T
claude@clouddev1 9868442889 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".
2026-06-14 22:18:59 +00:00

12 KiB
Raw Blame History

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 (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.