Skip to content

Scaffolder command-authoring parity

Status: APPROVED — two related GTB-parity gaps from the v0.22.0 audit (Phase 2, §A). Both are scaffolder command-authoring polish with no open decisions. Lifts the explicit deferral in commands/generate/command.rs:14 and mirrors GTB (internal/cmd/generate/command.go, internal/generator/validate.go).

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).