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
This commit is contained in:
claude@clouddev1
2026-06-15 12:18:41 +00:00
parent 050b36391e
commit 4a5fd1b5c1
11 changed files with 292 additions and 109 deletions
@@ -2,14 +2,18 @@
## Status
Accepted — implementation pending. Revised after a `/runda` review
Accepted — implementation in progress. Revised after a `/runda` review
(2026-06-14): corrected the verbosity-default fact; re-keyed tier-3
content on a new `hint_id` (not `help_id`) so every command form — simple
and advanced-SQL — gets distinct, mode-correct content; split the
pre-submit-diagnostic and runtime-error paths; added a comprehensiveness
coverage test. The parallel question of whether the in-app `help` command
should likewise distinguish advanced-SQL forms is tracked **separately**
as Gitea issue #36 (it touches shipped, ADR-backed `help` behaviour).
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 (separate tracking issue).
The parallel question of whether the in-app `help` command should
likewise distinguish advanced-SQL forms is tracked **separately** as
Gitea issue #36 (it touches shipped, ADR-backed `help` behaviour).
Decided in conversation 2026-06-14. Closes the last open piece of **A1**
(the canonical app-command set, ADR-0003): every app command is
@@ -132,22 +136,42 @@ 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_id: Option<&'static str>`** field added to `CommandNode`
(`src/dsl/grammar/mod.rs:512`, parallel to the existing `help_id` /
`usage_ids`). The F1 live-input path resolves the current input to its
command node and looks up `hint.cmd.<node.hint_id>`.
`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 a new field, not `help_id`:** `help_id` is **not** 1:1 with
command forms. The 7 advanced-mode SQL nodes (`SELECT`, `WITH`,
`SQL_INSERT/UPDATE/DELETE`, `EXPLAIN_SQL`) carry `help_id: None` *purely
to dedup the `help` command's printed list* (they share an entry word
with a simple sibling — see `grammar/mod.rs:915-918`), not because they
lack distinct content. Their SQL syntax differs from the simple-DSL
sibling's, so they **must get their own tier-3 block**. A dedicated
`hint_id` gives every one of the ~37 REGISTRY nodes — simple and
advanced-SQL alike — its own key and its own mode-correct example, with
no sharing or deferral. (The analogous gap in the `help` command is out
of scope here — issue #36.)
**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
@@ -309,10 +333,12 @@ Hint — add relationship
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_id` field on `CommandNode`** (parallel to `help_id`), 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 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 large, durable content corpus** (~37 command blocks + ~42 error/
diagnostic blocks ≈ 80) enters the catalogue under `hint.cmd.*` /
`hint.err.*`, validated by `keys.rs`. This is ongoing surface area: new
@@ -349,6 +375,12 @@ Hint — add relationship
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.
## Content inventory (implementation tracking)