refactor(spec): make missed SpecCommand fields a compile error, and fix the four that were already missed - #740
Conversation
|
Warning Review limit reached
Next review available in: 30 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Central YAML (base), Organization UI (inherited) Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (3)
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 makes
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Reviews (3): Last reviewed commit: "test(spec): exercise every round-trippab..." | Re-trigger Greptile |
|
Good catch, and it mattered — Fixed by populating I also added a guard so this can't silently regress: after the equality check, the test asserts each of those fields is actually non-default in the parsed spec, and fails with This comment was generated by an AI coding assistant. |
…be missed Adding a field to SpecCommand means updating seven places, and nothing enforces any of them. Miss one and data is silently dropped: a merge that loses a value, a docs model that can't see it, a spec that doesn't survive serialization. Make merge, From<&SpecCommand> for KdlNode, and the docs model's From destructure their source with no `..`, so a new field is a compile error in each place that owes it a decision. Runtime-derived fields are bound to `_` with a comment rather than skipped, so the reason is visible. Behavior is unchanged. Three fields turned out to already fall through gaps; each is bound with a NOTE here and fixed in its own commit so the change is reviewable on its own: - `deprecated` is not carried by merge - `restart_token` never reaches the docs model - `examples` is not serialized to KDL subcommand_lookup becomes pub(crate) so other modules can name it in a pattern; it stays private outside the crate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
merge composes included and mounted specs onto a command. It handled every other optional field but not `deprecated`, so an overlay that merely restated a command's help silently un-deprecated it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
restart_token was parsed and serialized but never copied into docs::models::SpecCommand, so no doc, manpage or CLI-help template could render it even though the spec declared it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SpecExample already had a KdlNode conversion, but From<&SpecCommand> for KdlNode never called it, so `spec.to_string()` silently dropped every example a spec declared. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nodes
These three were only accepted as props, but From<&SpecCommand> for KdlNode
writes them as child nodes. Any spec that set markdown help therefore
serialized to KDL that usage itself refused to parse:
Error: unsupported cmd key help_md
Mirror how long_help, before_long_help and after_long_help are already
handled in the child-node match.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Exhaustive destructuring makes the compiler catch a field that merge or a serializer forgets, but it cannot catch the parser: that is a match on node names, not a struct pattern, so a field the serializer writes and the parser rejects compiles fine. Both bugs fixed in the preceding two commits were of that shape. Parse a spec exercising every field, serialize it, reparse, and compare the serde representations. Comparing serialized values rather than named fields means a field added later is covered without anyone remembering to extend this test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…re does The fixture left before_help_long, after_help_long, before_help_md, after_help_md, mounts and command-level complete at their defaults, so equality passed whether or not those fields survived. Two of them are parser paths this branch adds, which meant the fix shipped untested. Populate them, and assert that each field is actually non-default in the parsed spec. Without that guard the test degrades silently: an edit that empties the fixture still passes, because comparing two empty values is always equal. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d5d7838 to
53055a0
Compare
⚠️ **CAUTION: this is a major update, indicating a breaking change!**⚠️ This MR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [usage](https://github.com/jdx/usage) | tools | major | `3.5.6` → `5.1.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>jdx/usage (usage)</summary> ### [`v5.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#510---2026-08-09) [Compare Source](jdx/usage@v5.0.0...v5.1.0) ##### 🚀 Features - **(spec)** parse usage comments from strings by [@​jdx](https://github.com/jdx) in [#​782](jdx/usage#782) ##### 🐛 Bug Fixes - **(spec)** avoid inferred metadata from included specs by [@​jdx](https://github.com/jdx) in [#​786](jdx/usage#786) ##### 🧪 Testing - **(windows)** make the suite runnable on Windows by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​771](jdx/usage#771) ##### 📦️ Dependency Updates - update rust crate rmcp to v3 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​780](jdx/usage#780) ### [`v5.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#500---2026-08-02) [Compare Source](jdx/usage@v4.1.0...v5.0.0) ##### 🚀 Features - **(cli)** allow overriding the shell program with USAGE\_SHELL\_<SHELL> by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​767](jdx/usage#767) ##### 🐛 Bug Fixes - **(cli)** forward parsed args to WSL bash via WSLENV on windows by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​764](jdx/usage#764) - **(cli)** let generate markdown write to stdout by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​766](jdx/usage#766) - **(complete)** use `type -P` so the CLI-presence guard ignores shell functions by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​760](jdx/usage#760) - **(parse)** enforce double\_dash="required" for positional args by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​762](jdx/usage#762) - **(windows)** run `run=` scripts with sh when available by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​765](jdx/usage#765) ##### 🎨 Styling - fix clippy and deprecation warnings in test and bench targets by [@​JamBalaya56562](https://github.com/JamBalaya56562) in [#​763](jdx/usage#763) ### [`v4.1.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#410---2026-07-30) [Compare Source](jdx/usage@v4.0.0...v4.1.0) ##### 🚀 Features - **(cli)** declare what each usage command does to the world by [@​jdx](https://github.com/jdx) in [#​751](jdx/usage#751) - **(mcp)** serve a usage spec to an agent over stdio by [@​jdx](https://github.com/jdx) in [#​746](jdx/usage#746) - **(spec)** add a top-level `repository` field by [@​jdx](https://github.com/jdx) in [#​747](jdx/usage#747) ##### 🐛 Bug Fixes - **(parse)** keep a re-declared global's aliases on one flag by [@​jdx](https://github.com/jdx) in [#​752](jdx/usage#752) - complete repeated variadic args by [@​Jai-JAP](https://github.com/Jai-JAP) in [#​753](jdx/usage#753) ##### New Contributors - [@​Jai-JAP](https://github.com/Jai-JAP) made their first contribution in [#​753](jdx/usage#753) ### [`v4.0.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#400---2026-07-25) [Compare Source](jdx/usage@v3.6.0...v4.0.0) ##### 🚀 Features - **(spec)** allow effect= on flags and args by [@​jdx](https://github.com/jdx) in [#​742](jdx/usage#742) ### [`v3.6.0`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#360---2026-07-25) [Compare Source](jdx/usage@v3.5.7...v3.6.0) ##### 🚀 Features - **(spec)** add effect= to declare what a command does to the world by [@​jdx](https://github.com/jdx) in [#​739](jdx/usage#739) ##### 🚜 Refactor - **(spec)** make missed SpecCommand fields a compile error, and fix the four that were already missed by [@​jdx](https://github.com/jdx) in [#​740](jdx/usage#740) ### [`v3.5.7`](https://github.com/jdx/usage/blob/HEAD/CHANGELOG.md#357---2026-07-25) [Compare Source](jdx/usage@v3.5.6...v3.5.7) ##### 🐛 Bug Fixes - **(parse)** don't leak the mounting CLI's flags into mounted commands; scan past non-global flags by [@​jdx](https://github.com/jdx) in [#​738](jdx/usage#738) </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODguMCIsInVwZGF0ZWRJblZlciI6IjQzLjI4OC4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6WyJSZW5vdmF0ZSBCb3QiLCJhdXRvbWF0aW9uOmJvdC1hdXRob3JlZCIsImRlcGVuZGVuY3ktdHlwZTo6bWFqb3IiXX0=-->
Follow-up to the review on #739, where
effectwas dropped bySpecCommand::mergeand by the docs model. Neither omission could have been caught by anything but a reviewer noticing.The problem
Adding a field to
SpecCommandmeans updating seven places — the struct,Default, the prop-parse arm, the child-node-parse arm, the KDL serializer,merge, the builder, anddocs::models::SpecCommand+ itsFrom. Nothing enforces any of them, and every omission fails silently.Auditing every existing field against the three impls turned up four fields already falling through, all on
main:deprecatedmergerestart_tokenexamplesspec.to_string()drops every examplehelp_md,before_help_md,after_help_mdError: unsupported cmd key help_mdThe last one is the worst and I only found it because of the test in the final commit.
The fix
mergeand bothFromimpls now destructure their source with no... Add a field and you get three compile errors pointing at exactly the places that owe it a decision. Runtime-derived fields are bound to_with a comment so the reason is visible rather than implied. No macros, no new machinery, no runtime cost.A round-trip test covers what the compiler can't. The parser is a
matchon node names, not a struct pattern, so a field the serializer writes and the parser rejects still compiles — which is exactly how thehelp_mdbug survived. The test parses a spec exercising every field, serializes it, reparses, and compares the two serde representations. Comparing serialized values rather than named fields means a field added later is covered without anyone remembering to extend the test.Commits
Each is reviewable on its own, and the refactor is behavior-neutral:
refactor(spec):exhaustive destructuring — no behavior change; the four gaps are bound withNOTEcomments so they're visible before being fixedfix(spec):carrydeprecatedthroughmergefix(docs):passrestart_tokento the docs modelfix(spec):serializeexamplesto KDLfix(spec):accepthelp_md/before_help_md/after_help_mdas child nodestest(spec):assert a spec survives a KDL round-tripConsidered and skipped
A
#[derive(SpecNode)]macro generating parse/serialize/merge from field attributes would cover all of it, but the irregular fields are a real tax —deprecatedaccepts bool-or-string,aliassplits into visible and hidden, the collection children each parse differently. Destructuring plus the round-trip test gets nearly the same coverage for a fraction of the machinery. Worth revisiting if the struct keeps growing.Notes
mainrather than#739, so it's independently mergeable. Whichever lands second gets a trivial conflict in the destructuring lists (one added binding foreffect).subcommand_lookupbecamepub(crate)so other modules can name it in a pattern. Still private outside the crate.serde_jsonadded as a dev-dependency for the round-trip comparison.Verified
cargo test -p usage-libpasses, 271 + doctests.cargo test --workspacehas the same two pre-existing failures inusage-cli --test examples(test_usage_double_slash_execution,..._old) present on a cleanmain.cargo clippyhas one pre-existing error inchoices.rsunder a feature-gated path, also present onmainand untouched here.This PR was generated by an AI coding assistant.
Note
Medium Risk
Changes core spec merge, KDL parse/serialize, and docs mapping used when composing specs (includes/mounts), but fixes are narrow and guarded by new round-trip and merge tests.
Overview
SpecCommandmaintenance is enforced at compile time by replacing..skips with exhaustive destructuring inmerge, KDL serialization (From<&SpecCommand> for KdlNode), and the docsFromimpl—new struct fields must be handled explicitly in each path.Behavior fixes close four silent gaps:
deprecatedis merged like other optional metadata;restart_tokenis copied into the docs model;examplesare emitted when serializing to KDL; andhelp_md/before_help_md/after_help_mdare accepted as child nodes on reparse (matching how they are written), so round-tripped specs no longer fail with unsupportedhelp_mdkeys.Tests add
deprecatedmerge coverage and a KDL parse →to_string()→ reparse test that compares fullserde_jsonsnapshots, plus fixture guards so the round-trip exercise stays meaningful.subcommand_lookupispub(crate)only so other modules can name it in destructuring patterns.Reviewed by Cursor Bugbot for commit 53055a0. Bugbot is set up for automated code reviews on this repo. Configure here.