feat(help): distinct help for advanced-mode SQL forms; split list by mode (#36)

The six advanced SQL DML/query forms (SELECT, WITH, SQL_INSERT, SQL_UPDATE,
SQL_DELETE, EXPLAIN_SQL) carried help_id: None, so `help select`/`help with`
resolved to nothing and `help insert` showed only the simple form. Give each
its own distinct help_id (data.select, data.sql_insert, …) with a hand-curated
help.data.* page. Distinct strings keep the dedup invariant intact, and
note_help_topic needs no change — `help insert` now shows the simple block and
the sql_insert block (like `help create` already did), and the advanced-only
forms resolve.

note_help now groups the list by CommandCategory: app-lifecycle commands first
(unlabelled), then "Simple-mode commands:" and "Advanced-mode (SQL) commands:"
sections — replacing the old single "DSL data commands (in simple mode):"
header, which used the banned "DSL" term and mis-labelled the advanced SQL
forms it already contained.

Four new help_command tests (red→green). Recorded as ADR-0024 Amendment 1;
CHANGELOG updated.
This commit is contained in:
claude@clouddev1
2026-06-22 19:01:27 +00:00
parent e88fa79f09
commit 3ad4affef2
9 changed files with 223 additions and 41 deletions
+35 -17
View File
@@ -3047,34 +3047,52 @@ impl App {
/// output panel.
///
/// Assembled from the command REGISTRY (ADR-0024 §help_id):
/// the framing (`help.intro`, `help.dsl_section`,
/// `help.types_reference`) comes from the catalog, and each
/// command's body is the catalog entry named by its
/// `help_id`. A newly-registered command appears here
/// automatically — no edit to this function or a hand-kept
/// the framing (`help.intro`, `help.simple_section`,
/// `help.advanced_section`, `help.types_reference`) comes from
/// the catalog, and each command's body is the catalog entry
/// named by its `help_id`. A newly-registered command appears
/// here automatically — no edit to this function or a hand-kept
/// list. Each catalog line becomes its own `OutputLine` so
/// the scroll-position math (one logical line = one display
/// row) stays accurate per the renderer's invariant.
///
/// Issue #36: the commands group by mode. App-lifecycle commands
/// (`help_id` in the `app.*` namespace) work in either mode and
/// list first, unlabelled, under the intro. The rest split by
/// [`CommandCategory`] into a simple-mode group and an
/// advanced-mode (SQL) group — replacing the old single "DSL data
/// commands (in simple mode)" header, which both used the banned
/// "DSL" term and wrongly claimed simple mode for the advanced SQL
/// forms the section already contained.
fn note_help(&mut self) {
use crate::dsl::grammar::REGISTRY;
use crate::dsl::grammar::{CommandCategory, REGISTRY};
let mut lines: Vec<String> = Vec::new();
lines.push(crate::t!("help.intro"));
// REGISTRY is ordered app-commands first; emit the
// "DSL data commands" sub-header at the first command
// whose help_id leaves the `app.` namespace.
let mut dsl_header_done = false;
for (command, _category) in REGISTRY {
let mut simple: Vec<String> = Vec::new();
let mut advanced: Vec<String> = Vec::new();
for (command, category) in REGISTRY {
let Some(help_id) = command.help_id else {
continue;
};
if !dsl_header_done && !help_id.starts_with("app.") {
lines.push(crate::t!("help.dsl_section"));
dsl_header_done = true;
let body = crate::friendly::translate(&format!("help.{help_id}"), &[]);
let block: Vec<String> = body.lines().map(str::to_string).collect();
if help_id.starts_with("app.") {
lines.extend(block);
} else if matches!(category, CommandCategory::Advanced) {
advanced.extend(block);
} else {
simple.extend(block);
}
let key = format!("help.{help_id}");
let body = crate::friendly::translate(&key, &[]);
lines.extend(body.lines().map(str::to_string));
}
if !simple.is_empty() {
lines.push(crate::t!("help.simple_section"));
lines.extend(simple);
}
if !advanced.is_empty() {
lines.push(crate::t!("help.advanced_section"));
lines.extend(advanced);
}
lines.extend(
crate::t!("help.types_reference")
+13 -13
View File
@@ -1886,12 +1886,12 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
entry: Word::keyword("explain"),
shape: EXPLAIN_SQL_SHAPE,
ast_builder: build_explain_sql,
// No `help_id` / `usage_ids` — this is the `Advanced` half of the
// shared `explain` entry word, so it defers to the `Simple`
// `EXPLAIN` node's help/usage (which now covers the SQL forms
// too). Mirrors the `SQL_INSERT`/`SQL_UPDATE`/`SQL_DELETE`
// precedent; otherwise `note_help` would print `explain` twice.
help_id: None,
// Issue #36: its own `help` page (`data.explain_sql`), listed under the
// advanced-mode (SQL) section and shown by `help explain` next to the
// simple `EXPLAIN` form (distinct help_id ⇒ no dedup clash). `usage_ids`
// stays empty — the `Simple` `EXPLAIN` node's usage block already covers
// the shared `explain` entry word.
help_id: Some("data.explain_sql"),
hint_ids: &["explain_sql"],
usage_ids: &[],
};
@@ -1902,13 +1902,13 @@ pub static EXPLAIN_SQL: CommandNode = CommandNode {
/// The shape is the post-`SELECT` portion of a top-level
/// statement; the registry's entry-word dispatch consumes the
/// leading `SELECT` keyword before the shape walks (sub-phase
/// 2c migration). `help_id` is `None` until the `help sql`
/// page lands (ADR-0030 Phase 6).
/// 2c migration). Carries its own `help` page (`data.select`),
/// listed under the advanced-mode (SQL) section (issue #36).
pub static SELECT: CommandNode = CommandNode {
entry: Word::keyword("select"),
shape: Node::Subgrammar(&sql_select::SQL_SELECT_TAIL),
ast_builder: build_select,
help_id: None,
help_id: Some("data.select"),
hint_ids: &["select"],
usage_ids: &["parse.usage.select"],
};
@@ -1924,7 +1924,7 @@ pub static WITH: CommandNode = CommandNode {
entry: Word::keyword("with"),
shape: Node::Subgrammar(&sql_select::SQL_WITH_TAIL),
ast_builder: build_select,
help_id: None,
help_id: Some("data.with"), // issue #36: own help page, advanced section
hint_ids: &["with"],
usage_ids: &["parse.usage.with"],
};
@@ -1943,7 +1943,7 @@ pub static SQL_INSERT: CommandNode = CommandNode {
entry: Word::keyword("insert"),
shape: Node::Subgrammar(&sql_insert::SQL_INSERT_SHAPE),
ast_builder: build_sql_insert,
help_id: None,
help_id: Some("data.sql_insert"), // issue #36: own help page, advanced section
hint_ids: &["sql_insert"],
usage_ids: &[],
};
@@ -1957,7 +1957,7 @@ pub static SQL_UPDATE: CommandNode = CommandNode {
entry: Word::keyword("update"),
shape: Node::Subgrammar(&sql_update::SQL_UPDATE_SHAPE),
ast_builder: build_sql_update,
help_id: None,
help_id: Some("data.sql_update"), // issue #36: own help page, advanced section
hint_ids: &["sql_update"],
usage_ids: &[],
};
@@ -1973,7 +1973,7 @@ pub static SQL_DELETE: CommandNode = CommandNode {
entry: Word::keyword("delete"),
shape: Node::Subgrammar(&sql_delete::SQL_DELETE_SHAPE),
ast_builder: build_sql_delete,
help_id: None,
help_id: Some("data.sql_delete"), // issue #36: own help page, advanced section
hint_ids: &["sql_delete"],
usage_ids: &[],
};
+11 -6
View File
@@ -537,8 +537,11 @@ pub struct CommandNode {
/// block). `hint_key_for_input_in_mode` disambiguates by the form
/// word, reusing `usage_key_for_input_in_mode`'s logic. Empty
/// until a form's tier-3 block is authored (the surface falls back
/// to tier-2 ambient/error text). Distinct from `help_id` (which is
/// `None` on advanced-SQL forms purely to dedup the `help` list).
/// to tier-2 ambient/error text). Parallel to `help_id` but
/// finer-grained: every form (simple and advanced) carries a
/// `hint_id`, whereas the `help <topic>` view groups forms by entry
/// word (so a shared-entry simple + SQL pair both surface under e.g.
/// `help insert`).
pub hint_ids: &'static [&'static str],
/// Catalog keys under `parse.usage.*` to render in the
/// "usage:" block when a parse error fires for this command
@@ -1156,10 +1159,12 @@ mod usage_key_tests {
#[test]
fn no_two_registered_commands_share_a_help_id() {
// `note_help` emits one help block per `help_id: Some(_)`
// with no dedup, so a duplicate help_id prints the same
// command twice in `help`. Shared-entry-word `Advanced`
// nodes (SQL_INSERT, …, EXPLAIN_SQL) therefore carry
// `help_id: None` and defer to their `Simple` sibling.
// with no dedup, so a duplicate help_id string prints the same
// block twice. Distinct help_ids are fine — a shared-entry-word
// simple + SQL pair (e.g. `data.insert` + `data.sql_insert`,
// issue #36) each get their own block, grouped under one topic
// by `help <topic>` and split across the simple/advanced
// sections of the full list.
let mut seen = std::collections::HashSet::new();
for (command, _category) in super::REGISTRY {
if let Some(id) = command.help_id {
+9 -1
View File
@@ -180,7 +180,8 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
// In-app `help` — framing + per-command entries keyed by
// each CommandNode's `help_id` (ADR-0024 §help_id).
("help.intro", &[]),
("help.dsl_section", &[]),
("help.simple_section", &[]),
("help.advanced_section", &[]),
("help.types_reference", &[]),
("help.detail_hint", &[]),
("help.unknown_topic", &["topic"]),
@@ -223,6 +224,13 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
("help.data.delete", &[]),
("help.data.replay", &[]),
("help.data.explain", &[]),
// Issue #36: advanced-mode (SQL) help pages.
("help.data.select", &[]),
("help.data.with", &[]),
("help.data.sql_insert", &[]),
("help.data.sql_update", &[]),
("help.data.sql_delete", &[]),
("help.data.explain_sql", &[]),
// ---- Hint panel ambient typing assistance (ADR-0022 §6) ----
("hint.ambient_complete", &[]),
("hint.ambient_error_with_usage", &["message", "usage"]),
+26 -3
View File
@@ -246,7 +246,13 @@ help:
# are multi-line-capable — the renderer emits one output row
# per line so scroll math stays accurate.
intro: "Supported commands:"
dsl_section: "DSL data commands (in simple mode):"
# Issue #36: the command list groups by mode. App-lifecycle commands list
# first (unlabelled, under the intro — they work in either mode); the rest
# split into these two sections by command category. (Replaces the old
# single "DSL data commands (in simple mode):" header, which used the banned
# "DSL" term and mis-labelled the advanced SQL forms it already contained.)
simple_section: "Simple-mode commands:"
advanced_section: "Advanced-mode (SQL) commands:"
# H3: footer on the full `help` list, and the not-found note
# for `help <topic>`. `{topic}` is the word the user typed.
detail_hint: "Type `help <command>` for detail on one command (e.g. `help insert`), or `help types` for the type reference."
@@ -368,8 +374,25 @@ help:
explain show data <T> | explain update <T> ... | explain delete from <T> ...
— show how the database would run a query, without
running it (safe even for update / delete)
explain <select|with|insert|update|delete …> (advanced mode)
— the same plan for the SQL you wrote
# Issue #36: advanced-mode (SQL) forms. Each has its own help page, listed
# under the "Advanced-mode (SQL) commands:" section and shown by
# `help <topic>` alongside its simple-mode sibling — so `help insert` shows
# both the simple form and `sql_insert` (like `help create` already does).
select: |-
select <cols> | * from <T> [where …] [group by …] [order by …] [limit n]
— query rows (advanced SQL)
with: |-
with <name> as (<select>) [, …] <select> — query through a named
sub-query / CTE (advanced SQL)
sql_insert: |-
insert into <T> (col, …) values (val, …) — add a row (advanced SQL)
sql_update: |-
update <T> set <col> = <val>, … where <expr> — change matching rows (advanced SQL)
sql_delete: |-
delete from <T> where <expr> — remove matching rows (advanced SQL)
explain_sql: |-
explain <select|with|insert|update|delete …> — show the plan for a SQL statement,
without running it (advanced SQL)
# Type reference, appended after the command list.
types_reference: |
Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid