Help: consume CommandNode.help_id — REGISTRY-driven in-app help

Every CommandNode declared a help_id that nothing read; the in-app
`help` body was a single hand-kept catalog block that drifted from
the command set (handoff-12 §2.1).

note_help now iterates the command REGISTRY and translates each
CommandNode's help_id (`help.<id>`), framed by help.intro /
help.dsl_section / help.types_reference. A newly-registered command
appears in `help` automatically — no edit to note_help or a hand-kept
list. Added 20 per-command help entries plus the 3 framing entries;
removed help.in_app_body.

Per-command entries use block scalars: a libyml 0.0.5 scanner bug
panics on long internal space runs in double-quoted scalars, and the
entries are space-aligned.
This commit is contained in:
claude@clouddev1
2026-05-15 22:45:18 +00:00
parent f46606b12e
commit 03dd9003df
4 changed files with 139 additions and 56 deletions
+38 -14
View File
@@ -1595,22 +1595,46 @@ impl App {
}
}
/// Note a flat list of currently-supported app-level
/// commands to the output panel.
/// Note the list of currently-supported commands to the
/// output panel.
///
/// This is the simple Iteration-4 stand-in for a richer
/// help system (H3 in the requirements doc); it gives the
/// user a quick "what can I type?" reference that's
/// always accurate against the build they're running. As
/// new commands land, append them here.
/// 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
/// 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.
fn note_help(&mut self) {
// Body lives in the i18n catalog (`help.in_app_body`).
// Each YAML line becomes its own `OutputLine` so the
// scroll-position math (one logical line = one display
// row) stays accurate per the renderer's invariant.
let body = crate::t!("help.in_app_body");
for line in body.lines() {
self.note_system(line.to_string());
use crate::dsl::grammar::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 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 key = format!("help.{help_id}");
let body = crate::friendly::translate(&key, &[]);
lines.extend(body.lines().map(str::to_string));
}
lines.extend(
crate::t!("help.types_reference")
.lines()
.map(str::to_string),
);
for line in lines {
self.note_system(line);
}
}
+5 -1
View File
@@ -351,7 +351,11 @@ pub struct CommandNode {
/// as a per-node validator (Phase A: none — every app
/// command's ast_builder is infallible).
pub ast_builder: fn(&MatchedPath) -> Result<Command, ValidationError>,
#[allow(dead_code)]
/// Catalog key (`help.<id>`) for this command's in-app
/// `help` entry. Consumed by `App::note_help`, which
/// iterates the REGISTRY and translates each `help_id` —
/// so a newly-registered command appears in `help`
/// automatically (ADR-0024 §help_id).
pub help_id: Option<&'static str>,
/// Catalog keys under `parse.usage.*` to render in the
/// "usage:" block when a parse error fires for this command
+25 -1
View File
@@ -120,7 +120,31 @@ pub const KEYS_AND_PLACEHOLDERS: &[(&str, &[&str])] = &[
),
// ---- Help text ----
("help.cli_banner", &[]),
("help.in_app_body", &[]),
// In-app `help` — framing + per-command entries keyed by
// each CommandNode's `help_id` (ADR-0024 §help_id).
("help.intro", &[]),
("help.dsl_section", &[]),
("help.types_reference", &[]),
("help.app.quit", &[]),
("help.app.help", &[]),
("help.app.rebuild", &[]),
("help.app.save", &[]),
("help.app.new", &[]),
("help.app.load", &[]),
("help.app.export", &[]),
("help.app.import", &[]),
("help.app.mode", &[]),
("help.app.messages", &[]),
("help.ddl.create", &[]),
("help.ddl.drop", &[]),
("help.ddl.add", &[]),
("help.ddl.rename", &[]),
("help.ddl.change", &[]),
("help.data.show", &[]),
("help.data.insert", &[]),
("help.data.update", &[]),
("help.data.delete", &[]),
("help.data.replay", &[]),
// ---- Hint panel ambient typing assistance (ADR-0022 §6) ----
("hint.ambient_complete", &[]),
(
+68 -37
View File
@@ -208,46 +208,77 @@ help:
import <zip> [as <t>] Unpack <zip> into a new project and
switch to it. <t> overrides the target
name (else taken from the zip).
# In-app `help` command output. Same shape as
# `cli_banner` — multi-line block, consumers iterate
# lines and emit each as its own output row so scroll
# math stays accurate.
in_app_body: |
Supported commands:
quit — exit
help — this list
mode simple|advanced — switch input mode
messages — show current verbosity
messages short|verbose— switch error wording (verbose is the default)
rebuild — rebuild .db from project.yaml + data/ (with confirmation)
save — save current temp project under a name
save as — copy current project to a new name/path
new — close current, start a fresh temp project
# In-app `help` command output (ADR-0024 §help_id). The
# renderer iterates the command REGISTRY and translates each
# CommandNode's `help_id` — so a newly-registered command
# appears in `help` automatically, with no edit here. `intro`
# and `dsl_section` frame the two command groups; each
# `app.*` / `ddl.*` / `data.*` entry is keyed by a command's
# `help_id`; `types_reference` closes the block. All entries
# 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):"
# Per-command help, keyed by `CommandNode.help_id`. Block
# scalars (`|-`) so the column alignment survives — the
# double-quoted form trips a libyml scanner bug on long
# internal space runs. The renderer emits one output row per
# line, so multi-form commands list each form on its own line.
app:
quit: |-
quit — exit the app
help: |-
help — show this command list
rebuild: |-
rebuild — rebuild the project database from project.yaml + data/ (with confirmation)
save: |-
save — save the current temp project under a name
save as — copy the current project to a new name/path
new: |-
new — close the current project, start a fresh temp project
load: |-
load — open the project picker
export [<path>] — write a zip of project.yaml + data/ (excludes .db, history.log)
import <zip> [as <t>] — unpack a zip and switch to the new project
DSL data commands (in simple mode):
create table <T> with pk [<col>:<type>...]
drop table <T>
add column [to] [table] <T>: <col> (<type>)
export: |-
export [<path>] — write a zip of project.yaml + data/ (excludes the database file and history.log)
import: |-
import <zip> [as <target>] — unpack a zip into a new project and switch to it
mode: |-
mode simple|advanced — switch input mode
messages: |-
messages [short|verbose] — show or switch error-message verbosity (verbose is the default)
ddl:
create: |-
create table <T> with pk [<col>:<type>, ...] — create a table
drop: |-
drop table <T> — remove a table
drop column [from] [table] <T>: <col> — remove a column
drop relationship <name> — remove a relationship
add: |-
add column [to] [table] <T>: <col> (<type>) — add a column
(for serial/shortid on a non-empty table: existing rows auto-filled)
drop column [from] [table] <T>: <col>
rename column [in] [table] <T>: <old> to <new>
change column [in] [table] <T>: <col> (<newtype>)
[--force-conversion | --dont-convert]
(to serial/shortid: null cells auto-filled with generated values)
add 1:n relationship [as <name>] from <P>.<col> to <C>.<col>
[on delete <action>] [on update <action>] [--create-fk]
drop relationship <name>
insert into <T> [(cols)] [values] (vals)
update <T> set <c>=<v>... where <c>=<v> | --all-rows
delete from <T> where <c>=<v> | --all-rows
show table <T>
show data <T>
replay <path> — run each non-blank, non-`#`-comment line
of <path> as a command. Stops at the first
error (no rollback). Relative paths resolve
under the current project's directory.
[on delete <action>] [on update <action>] [--create-fk] — declare a relationship
rename: |-
rename column [in] [table] <T>: <old> to <new> — rename a column
change: |-
change column [in] [table] <T>: <col> (<newtype>) [--force-conversion | --dont-convert]
— change a column's type (to serial/shortid: null cells auto-filled with generated values)
data:
show: |-
show table <T> — show a table's structure
show data <T> — show a table's rows
insert: |-
insert into <T> [(cols)] [values] (vals) — add a row
update: |-
update <T> set <c>=<v>, ... where <c>=<v> | --all-rows — change matching rows
delete: |-
delete from <T> where <c>=<v> | --all-rows — remove matching rows
replay: |-
replay <path> — run each non-blank, non-`#`-comment line of <path>
as a command. Stops at the first error (no rollback);
relative paths resolve under the project directory.
# Type reference, appended after the command list.
types_reference: |
Types: text, int, real, decimal, bool, date, datetime, blob, serial, shortid
Auto-generated types (serial, shortid):
serial — integer that auto-fills with the next sequence value