feat(spec): add help_heading, and render it - #802
Conversation
📝 WalkthroughWalkthroughThe change adds optional ChangesHelp heading support
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Clap
participant Spec
participant DocsModel
participant HelpTemplate
participant MarkdownTemplate
Clap->>Spec: provide flag and argument help_heading
Spec->>DocsModel: convert metadata
DocsModel->>DocsModel: group entries by heading
DocsModel->>HelpTemplate: provide arg_groups and flag_groups
DocsModel->>MarkdownTemplate: provide grouped visible entries
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThe PR adds
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (5): Last reviewed commit: "fix(docs): group global flags by heading..." | Re-trigger Greptile |
d340027 to
e388012
Compare
|
Both findings — Rebased on the latest #801, which picked up its four fixes as well. AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable. |
Instruction counts
1 benchmark(s) above the 1% gate: Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run. Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cc568bc. Configure here.
cc568bc to
8fed243
Compare
A CLI with dozens of flags is unreadable without sections, clap has had help_heading for years, and the spec had no way to record one — so the clap bridge was dropping it. Every CLI in the fleet that groups its flags lost the grouping on the way into its spec; mise groups its whole watch passthrough set that way. Now a field on both flags and arguments, accepted as a property or a child node, written back out, and mapped from clap's own help_heading. usage-argv carries it too, which is what the change was for: the derive cannot emit something the spec cannot express, so the spec goes first. Nothing renders a heading yet — grouping flags in help and markdown output needs the docs models and templates to change, which is its own PR and is now a box in PLAN.md. The field is not idle in the meantime: a clap-derived spec stops losing information the moment this lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A spec field nothing displays is half a feature, so help output and generated markdown both group by heading now. Grouping happens in the docs models rather than in a template, because Tera can filter on an attribute's value but cannot partition on one, and "everything without a heading" is not expressible as a filter. Unheaded entries keep the default section title and come first; a heading with nothing visible in it produces no section, so a CLI that heads every flag does not get an empty "Flags:". The groups hold clones, which bit once already: render_md mutates the flag and arg lists after the model is built, so groups made before that point published copies without their rendered markdown. They are rebuilt at the end of render_md, and the method that does it says why. Every existing snapshot is unchanged — output is byte-identical when nothing has a heading — and four new ones cover the grouped case in both renderers, including hidden entries and a heading whose only entry is hidden. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generated markdown renders global flags in their own section, built from the flat list, so a `help_heading` on a global flag was ignored there while help output honored it — the same CLI documented two different ways. A heading now beats the default title, so a grouped global flag lands in the section it belongs to and only ungrouped ones fall under "Global Flags". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
8fed243 to
da412c7
Compare
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/src/docs/models.rs`:
- Around line 295-296: Filter out entries with hide set to true from both flags
and args before passing them to group_by_heading in the constructor, and apply
the same filtering in regroup. Ensure grouped items and headings containing only
hidden entries are excluded from CLI help output.
In `@lib/src/spec/builder.rs`:
- Around line 250-257: Correct the doc comments on the builder methods: in
help_heading, document the help-output heading, and in env, document that it
sets the environment variable name. Keep the method implementations unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Central YAML (base), Organization UI (inherited)
Review profile: CHILL
Plan: Pro Plus
Run ID: 913863e3-5e4f-4465-98dc-c75bf95ed3db
⛔ Files ignored due to path filters (1)
conformance/tests/snapshots/spec_roundtrip__the_emitted_spec_is_stable.snapis excluded by!**/*.snap
📒 Files selected for processing (14)
PLAN.mdargv/src/spec.rsconformance/tests/spec_roundtrip.rsdocs/spec/reference/arg.mddocs/spec/reference/flag.mdlib/src/docs/cli/mod.rslib/src/docs/cli/templates/spec_template_long.teralib/src/docs/cli/templates/spec_template_short.teralib/src/docs/markdown/cmd.rslib/src/docs/markdown/templates/cmd_template.md.teralib/src/docs/models.rslib/src/spec/arg.rslib/src/spec/builder.rslib/src/spec/flag.rs
| flag_groups: group_by_heading(&flags, |f| f.help_heading.as_deref()), | ||
| arg_groups: group_by_heading(&args, |a| a.help_heading.as_deref()), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Exclude hidden entries before building groups.
args and flags include entries where hide is true. The CLI templates render every grouped item. Hidden entries therefore appear in help output, and headings that contain only hidden entries are not omitted.
Filter hidden flags and arguments before grouping in this constructor and in regroup at Lines 465-466.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/docs/models.rs` around lines 295 - 296, Filter out entries with hide
set to true from both flags and args before passing them to group_by_heading in
the constructor, and apply the same filtering in regroup. Ensure grouped items
and headings containing only hidden entries are excluded from CLI help output.
| /// Set environment variable name | ||
| /// Heading to list this under in help output. | ||
| pub fn help_heading(mut self, help_heading: impl Into<String>) -> Self { | ||
| self.inner.help_heading = Some(help_heading.into()); | ||
| self | ||
| } | ||
|
|
||
| pub fn env(mut self, env: impl Into<String>) -> Self { |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Move the environment-variable documentation to env.
help_heading now documents itself as setting an environment variable. env has no documentation. Put the heading documentation on help_heading. Put Set environment variable name on env.
Also applies to: 395-402
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/src/spec/builder.rs` around lines 250 - 257, Correct the doc comments on
the builder methods: in help_heading, document the help-output heading, and in
env, document that it sets the environment variable name. Keep the method
implementations unchanged.
Stacked on #802 (which is stacked on #801) — review those first; this diff includes them until they merge. The piece the last three PRs were building toward: one Rust type in, a parser and a spec out. ```rust /// A tool that does things #[derive(usage::Cli)] #[usage(bin = "ex", version = "1.0")] struct Cli { /// How many jobs to run at once #[usage(short = 'j', long, env = "EX_JOBS", default = "4")] jobs: Option<String>, /// Colorize output #[usage(long, negate = "--no-color", default = "true")] color: bool, /// Files to process files: Vec<String>, } ``` That gives you `Cli::parse_from(argv)`, `Cli::command()`, `Cli::spec()`, and `Cli::to_kdl()` — so the same declaration feeds `usage g markdown|manpage`, the completion generators, and grouped help output from #802. ## What's generated Three things, and the split is the design: - **`static` parse tables** — all a successful parse reads. - **`static` metadata** — what spec emission and help need, which a parse never touches. - **a parse function** — a `match` on table keys assigning straight into the struct's fields. No map to build and read back, nothing allocated that does not end up in the result. `command()` returns a `&'static`, so there is no command tree to construct before parsing starts. A test asserts the pointer is the same every call, which is the property the whole project exists for. A field with `long` or `short` is a flag; anything else is positional. Help comes from the doc comment — first paragraph short, whole comment long. ## Scope, stated plainly **One command per struct.** Subcommands need an enum of variants, cross-type table references, and a nested path through the parse function; that is its own PR and a box in `PLAN.md`. **Values are text** — `bool`, `String`, `Option<String>`, `Vec<String>`, or an unsigned integer with `count`. Converting to other types is also where `env`, required-ness, and `choices` get enforced, and that layer does not exist yet. So `Option<u32>` is a *compile error* explaining exactly that, rather than something that silently half-works. ## The error messages got real attention They are the surface an author actually interacts with, so: - `short = "j"` → *a short flag is a character: write `short = 'j'`* - a duplicate `--flag` → points at both declarations, second first - an argument after a variadic one → *can never be filled, because the variadic takes every remaining word* - an unknown option → lists the ones that exist - `count` on a `String` → says it has to be an unsigned integer ## Tests Twelve, over a deliberately awkward CLI: attached and bundled shorts, `--flag=value`, repeated flags, a negation turning off a default, a hidden flag that still parses, `--` passthrough, and a typo reported rather than bound. Then the same declaration is checked to emit a spec usage-lib accepts field by field, render as markdown and a manpage, and group by heading. Two more CLIs cover the empty cases — no flags, no positionals — which is where generated code tends to break on an unused variable or an empty `match`. Not published: a CLI framework that cannot express subcommands is not one to depend on by accident. The version tracks the workspace so it is ready the moment it can. *AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.* <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Large new proc-macro surface that defines CLI parsing behavior for adopters; mitigated by extensive conformance tests but still pre-v1 (no subcommands, limited types, env not enforced at parse time). > > **Overview** > Adds the **`usage-derive`** workspace crate and **`#[derive(Cli)]`**, so a single struct with `#[usage(...)]` attributes becomes a **`usage-argv` parser**, **static spec metadata**, and **KDL** for docs/completions. > > Generated code exposes **`parse_from` / `parse`**, **`command()`** and **`spec()`** as `&'static` tables, and **`to_kdl()`**. Parsing is a direct `match` on flag/arg keys into prefixed locals (avoids field-name clashes). The model layer rejects invalid declarations at compile time (duplicate flags, `var` vs `variadic`, unsupported types, dashed long/name normalization, etc.) with targeted errors. > > **v0 scope:** one command per struct; text-ish field types only (`bool`, `String`, `Option<String>`, `Vec<String>`, counting integers). Subcommands and typed value conversion are explicitly deferred in **PLAN.md**. > > **Conformance** gains **`conformance/tests/derive.rs`** end-to-end tests (parsing, spec round-trip, markdown/manpage/help). Release tooling includes **`usage-derive`** in publish and git-cliff paths. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 973a60a. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added `usage-derive`, enabling CLI definitions through a `#[derive(Cli)]` macro. * Supports flags, positional arguments, defaults, aliases, repeatable values, negation, help text, environment settings, and generated command specifications. * Added parsing, help/documentation rendering, and KDL serialization for derived CLI types. * Added compile-time diagnostics for unsupported or invalid declarations. * **Documentation** * Documented supported attributes, value types, limitations, and the current roadmap. * **Tests** * Added comprehensive end-to-end coverage for parsing, help output, specifications, errors, and CLI configurations. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

Stacked on #801 — review that first; this PR's diff will include it until it merges.
Closes the gap #801 turned up: writing the spec emitter, I reached for
help_headingand found the spec has no such field.Why it matters more than it looks
A CLI with dozens of flags is unreadable without sections, and clap has had
help_headingfor years. Because the spec could not record one,From<&clap::Arg>was silently dropping it — so every CLI in the fleet that groups its flags has been losing the grouping on the way into its spec. mise groups its entirewatchpassthrough set that way (31 uses, all in one file).That is the part worth noting: this was not a missing feature so much as a leak that was invisible because nothing downstream could have shown it.
The spec side
help_headingonSpecFlagandSpecArg, as a property (help_heading="Filtering") or a child node for longer text, written back out.get_help_heading()for both flags and positionals.usage-argvcarries it in bothFlagMetaandArgMeta, which is what prompted this: the derive cannot emit what the spec cannot express, so per the canonicality rule the spec went first.The rendering side
Help output and generated markdown both group by heading now — a field nothing displays is half a feature.
Grouping happens in the docs models, not in a template: Tera can filter on an attribute's value but cannot partition on one, and "everything without a heading" is not expressible as a filter. Behaviour:
Flags:/Arguments:) and come first.Flags:.One thing worth knowing if you touch this: the groups hold clones, and
render_mdmutates the flag and arg lists after the model is built — so grouping at construction published copies without their rendered markdown. That cost me a debugging round; groups are rebuilt at the end ofrender_mdand the method that does it says why.Every existing snapshot is unchanged — output is byte-identical when nothing has a heading — and four new ones cover the grouped case across both renderers, including hidden entries.
mise run renderproduces no diff.AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.
Note
Low Risk
Presentational metadata and docs/help rendering only; parsing behavior is unchanged and output without headings remains identical.
Overview
Adds
help_headingto flags and positionals in the usage spec (KDL parse/serialize, builders, reference docs) and threads it throughusage-argvmetadata and KDL emission so derive output can record section titles losslessly.Clap bridge fix:
From<&clap::Arg>now mapsget_help_heading(), so grouped flags are no longer dropped when converting to a spec.Rendering: CLI help (short/long templates) and generated markdown partition flags and args by heading via
flag_groups/arg_groupsin the docs models. Unheaded items stay under default Arguments / Flags and appear first; custom headings follow in first-seen order; sections with only hidden entries are omitted. Markdown regroups afterrender_mdso grouped clones keep rendered help text.Conformance roundtrip tests and new snapshot tests cover grouped help and global flags; existing output stays byte-identical when no headings are set.
Reviewed by Cursor Bugbot for commit da412c7. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by CodeRabbit