Skip to content

Scaffolder command-authoring parity

Status: IN PROGRESS — two related GTB-parity gaps from the v0.22.0 audit (Phase 2, §A). Scaffolder command-authoring polish. Lifts the explicit deferral in commands/generate/command.rs:14 and mirrors GTB (internal/cmd/generate/command.go, internal/generator/validate.go).

Resolved decisions (2026-07-04)

Implementation surfaced two things the original "no decisions needed" framing missed:

  1. Prerequisite landed. generate command/generate flag emitted non-compiling code on the cli preset. Fixed first in 2026-06-28-scaffolder-preset-aware-codegen (IMPLEMENTED) — cli commands are framework passthrough commands. This spec's metadata now threads into that shape.
  2. --short/--long need a CommandSpec extension (Option A, MC). The framework CommandSpec had no short/long_about. Decision: add both fields + a CommandSpec::DEFAULT const and migrate every literal to ..CommandSpec::DEFAULT (a permissible pre-1.0 break). build_clap_tree maps short → .short_flag, long_about → .long_about.

Progress: - ✅ Part 2 (Rust-keyword rejection). - ✅ Manifest commands: (CommandMeta) field. - ✅ CommandSpec extension (Option A) — short/long_about + DEFAULT; all literals migrated; build_clap_tree wires .short_flag/.long_about. - ✅ --aliases/--short/--long rendered into both presets + persisted to commands:. Compile + runtime verified (alias + short-flag dispatch) on minimal and cli. - ⬜ --args (name:type specs → struct fields / .arg() blocks). - ⬜ nested --parent (minimal main-loop nesting + cli inner subcommand). - ⬜ regenerate round-trip: re-inject commands: metadata (lives outside the marker regions, so marker-splicing alone loses it).

Part 1 — richer generate command metadata

rtb generate command today accepts only <name> [--about TEXT] and a single flat segment (command.rs:1-20 documents the deferral). GTB supports the full command metadata. Add, mirroring the already-shipped generate flag --shorthand (flag.rs:40):

rtb generate command <name> [--about TEXT] [--long TEXT]
                            [--short <char>] [--aliases a,b,c]
                            [--parent <command-path>] [--args <spec>]
  • --short <char>.short_flag('x') / single-char alias (validated as one ASCII alnum, like flag --shorthand).
  • --aliases a,b,cCommandSpec::aliases (each validated as a command name; deduped; not colliding with reserved names or the primary).
  • --parent <command-path>nested command (the deferred multi- segment path); validated segment-by-segment (validate.rs already allows reserved words in nested position, :246), depth-capped at MAX_COMMAND_PATH_DEPTH.
  • --long TEXT → long help; --args <spec> → positional/arg scaffolding (a simple name:type list rendered into the generated command's clap derive; richer arg modelling stays incremental).
  • These thread into the generated command's marker-region render + manifest commands: entry so regenerate preserves them.

Part 2 — Rust-keyword command-name rejection

GTB rejects Go keywords as command names (validate.go:117, token.IsKeyword) because a command name becomes a generated module/ident. RTB's validate.rs has RESERVED_NAMES (framework + scaffolder verbs + help) but no Rust-keyword guard — a command named match, move, use, fn, type, impl, etc. would collide with generated identifiers.

Add a RUST_KEYWORDS set (strict + reserved + 2018 keywords) and reject a top-level command name that is a Rust keyword, with a clear diagnostic naming the collision. (Nested segments map to path strings, not idents, so the guard is top-level-name-scoped, consistent with the existing reserved-name rule at validate.rs:151.)

strict:   as break const continue crate else enum extern false fn for if
          impl in let loop match mod move mut pub ref return self Self
          static struct super trait true type unsafe use where while
reserved: abstract become box do final macro override priv typeof unsized
          virtual yield try
2018:     async await dyn

Testing (TDD)

  • Unit (Part 1): --short one-char validation; --aliases parse/dedup/ reserved-collision reject; --parent nested-path validation + depth cap; generated render contains .short_flag/aliases/nested module; manifest round-trips the metadata.
  • Unit (Part 2): each Rust keyword rejected as a top-level name with the keyword-collision diagnostic; the same word accepted as a nested segment; a non-keyword reserved word still rejected by the existing rule.
  • E2E (assert_cmd): generate command deploy --short d --aliases dep --parent cluster produces a nested cluster deploy command that cargo checks and exposes the alias; generate command match is rejected.

Out of scope

  • Full positional/flag arg modelling beyond a simple name:type list (incremental).
  • Reserved-word handling for flags (flags already validated separately).