round-5 follow-up: completion + i18n sweep

Four user-reported gaps from the round-4 testing pass:

1. Empty-prompt hint reworded from "(no active hint)" to
   "Type a command — press Tab for options, `help` for a
   list" (6 snapshots updated to reflect 80-col truncation).

2. App-lifecycle commands (quit/q, help, rebuild, save/save as,
   new, load, export, import, mode, messages) now flow through
   the DSL parser:
   - 15 new keywords + catalog token entries
   - new Command::App(AppCommand) AST with 11 variants
   - parse-first dispatch in submit() (app commands work in
     both simple and advanced modes)
   - pre-chumsky source-slice for `export <path>` /
     `import <zip> [as <target>]` mirrors the replay precedent
   - UsageEntry registry entries so parse errors surface
     relevant usage templates
   - `mode <bad>` / `messages <bad>` use try_map for the
     friendly "unknown mode/messages" wording

3. DSL completion gaps:
   - `1:n` surfaces as a composite candidate at `add `
   - --all-rows / --create-fk / --force-conversion /
     --dont-convert surface as new CandidateKind::Flag
     candidates (coloured with tok_flag in hint panel)
   - filter_clause .labelled() wrap removed so chumsky's
     expected-set surfaces the constituent options

4. Hardcoded user-facing strings migrated to catalog:
   - 4 parser custom errors (incl. the known "tables need at
     least one column" wart)
   - UnknownType Display now via parse.custom.unknown_type
   - UI panel titles + mode labels (Output / Hint / SIMPLE /
     ADVANCED / Advanced:)
   - app.rs cascade rendering (action labels + summary)
   - runtime --resume CLI stderr
   - db.rs change-column diagnostic tables (7 headers + 3
     wrapper summaries + force-conversion hint)

Tests: 765 → 769 passing, 0 failed, 1 ignored (same doctest
as before). Clippy clean with -D warnings.

Deferred:
- ~25 thiserror #[error] attributes still hand-rolled
  (DbError, ArgsError, ArchiveError, PersistenceError,
  LockError). Tracked separately.
- DSL/SQL relationship in advanced mode — clarified
  implicitly via parse-first dispatch; broader ADR
  amendment to follow.
- Post-complete-parse completion gap (e.g. `save ` Tab
  can't offer `as` because `save` parses bare; same shape
  as `--create-fk` after a complete `add relationship`).
This commit is contained in:
claude@clouddev1
2026-05-13 15:58:29 +00:00
parent 1eb2e0d01f
commit 1e06490572
22 changed files with 1077 additions and 189 deletions
+72
View File
@@ -142,6 +142,60 @@ pub enum Command {
Replay {
path: String,
},
/// App-lifecycle command (per ADR-0003). These work in both
/// simple and advanced modes; the dispatcher branches on the
/// `Command::App(...)` variant before mode-specific routing.
/// Folded into the DSL parser so they participate in Tab
/// completion + parse-error usage templates alongside the
/// data commands.
App(AppCommand),
}
/// App-level commands surfaced through the DSL parser. These do
/// not touch the database schema or data — they affect app
/// lifecycle, mode, persistence, and verbosity.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AppCommand {
/// Exit cleanly. Accepts the `q` alias.
Quit,
/// Show in-app help. Body comes from `help.in_app_body`.
Help,
/// Rebuild `playground.db` from `project.yaml` + data/, with
/// confirmation modal.
Rebuild,
/// Save the current project under a name (modal-driven).
Save,
/// Save the current project as a copy under a new path
/// (modal-driven).
SaveAs,
/// Close current, create a fresh temp project.
New,
/// Open the project picker modal.
Load,
/// Write a zip of project.yaml + data/. `path` is the user-
/// typed target (may be a name under the data root or an
/// absolute path). `None` opens the path prompt modal.
Export { path: Option<String> },
/// Unpack a zip into a new project and switch to it.
/// `target` overrides the project name (default: taken from
/// the zip).
Import { path: String, target: Option<String> },
/// Switch the persistent input mode.
Mode { value: ModeValue },
/// Show or set the messages verbosity.
Messages { value: Option<MessagesValue> },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModeValue {
Simple,
Advanced,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MessagesValue {
Short,
Verbose,
}
/// Conversion mode for `change column …` (ADR-0017 §5).
@@ -218,6 +272,19 @@ impl Command {
Self::Delete { .. } => "delete from",
Self::ShowData { .. } => "show data",
Self::Replay { .. } => "replay",
Self::App(app) => match app {
AppCommand::Quit => "quit",
AppCommand::Help => "help",
AppCommand::Rebuild => "rebuild",
AppCommand::Save => "save",
AppCommand::SaveAs => "save as",
AppCommand::New => "new",
AppCommand::Load => "load",
AppCommand::Export { .. } => "export",
AppCommand::Import { .. } => "import",
AppCommand::Mode { .. } => "mode",
AppCommand::Messages { .. } => "messages",
},
}
}
@@ -254,6 +321,11 @@ impl Command {
// Replay isn't tied to a single table; the path is
// the most identifying thing for log output.
Self::Replay { path } => path,
// App commands aren't tied to schema entities — the
// verb is the most identifying thing. The
// display_subject override below provides a richer
// form when one exists.
Self::App(_) => "",
}
}