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
+50 -1
View File
@@ -3152,7 +3152,7 @@ impl App {
let (view, cursor, _off) = self.feedback_view();
let probe = view.to_string();
let mode = self.effective_mode().as_mode();
if let Some(id) = crate::dsl::grammar::hint_id_for_input_in_mode(&probe, mode)
if let Some(id) = crate::dsl::grammar::hint_key_for_input_in_mode(&probe, mode)
&& self.emit_tier3_block(&format!("hint.cmd.{id}"))
{
return;
@@ -5800,6 +5800,55 @@ mod tests {
assert!(output_contains(&app, "explain the most recent error"));
}
// ── Phase B: tier-3 exemplar content renders ────────────────
#[test]
fn f1_on_insert_input_renders_the_insert_hint_block() {
let mut app = App::new();
type_str(&mut app, "insert into Customers ");
f1(&mut app);
assert!(
output_contains(&app, "Add one or more rows to a table"),
"expected the insert tier-3 block"
);
}
#[test]
fn f1_on_add_relationship_renders_the_relationship_block() {
let mut app = App::new();
type_str(&mut app, "add 1:n relationship from Customers.id to Orders.cust ");
f1(&mut app);
assert!(
output_contains(&app, "one parent, many children"),
"expected the add-relationship tier-3 block"
);
}
#[test]
fn f1_on_add_column_does_not_render_the_relationship_block() {
// Per-form disambiguation (ADR-0053 D3): `add column` resolves
// to `add_column` (no tier-3 block yet → tier-2 fallback), NOT
// the relationship block — proving the multi-form node keys
// per form, not per node.
let mut app = App::new();
type_str(&mut app, "add column Note text to Customers");
f1(&mut app);
assert!(!output_contains(&app, "one parent, many children"));
assert!(!output_contains(&app, "1:n"));
}
#[test]
fn hint_renders_the_foreign_key_error_block() {
let mut app = App::new();
app.last_error_hint_key = Some("foreign_key.child_side".to_string());
type_str(&mut app, "hint");
submit(&mut app);
assert!(
output_contains(&app, "doesn't match any parent row"),
"expected the FK child-side tier-3 block"
);
}
#[test]
fn messages_command_toggles_verbosity_and_reports() {
let mut app = App::new();