Skip to content

feat(derive): compile a struct into parse tables and a spec - #803

Merged
jdx merged 8 commits into
mainfrom
agent/derive-v0
Aug 11, 2026
Merged

feat(derive): compile a struct into parse tables and a spec#803
jdx merged 8 commits into
mainfrom
agent/derive-v0

Conversation

@jdx

@jdx jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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.

/// 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 textbool, 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.


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.

Reviewed by Cursor Bugbot for commit 973a60a. Bugbot is set up for automated code reviews on this repo. Configure here.

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.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds the usage-derive procedural macro. It validates CLI declarations, generates static parser and specification APIs, adds conformance tests, and integrates the crate into workspace and release workflows.

Changes

Derive CLI implementation

Layer / File(s) Summary
Crate setup and declaration model
derive/Cargo.toml, derive/src/lib.rs, derive/src/model.rs
Adds the procedural-macro crate, supported CLI field shapes, attribute parsing, validation, diagnostics, name normalization, and documentation-derived help.
Static tables and generated parsing
derive/src/codegen.rs
Generates static command tables, specifications, metadata, KDL output, accessors, and argv parsing for supported field shapes.
Generated CLI conformance coverage
conformance/Cargo.toml, conformance/tests/derive.rs
Tests parsing, defaults, flags, positionals, separators, errors, specifications, help output, name normalization, empty CLI shapes, and static table reuse.
Workspace and release integration
Cargo.toml, PLAN.md, tasks/release-plz
Adds usage-derive to the workspace, development dependency graph, release milestones, publishing order, version detection, changelog generation, and staged artifacts.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant derive_cli
  participant Cli_from_input
  participant emit
  participant GeneratedCli
  participant usage_argv
  derive_cli->>Cli_from_input: Parse and validate CLI declaration
  Cli_from_input->>emit: Provide validated model
  emit->>GeneratedCli: Generate static tables and parser APIs
  GeneratedCli->>usage_argv: Parse argv and receive events
  usage_argv-->>GeneratedCli: Return fields or errors
Loading

Possibly related PRs

  • jdx/usage#798: Provides the usage-argv parser types and events consumed by generated code.
  • jdx/usage#801: Provides the usage_argv::spec types and Spec::to_kdl() API used by the generated specification.
  • jdx/usage#762: Introduces the double_dash_required positional parsing behavior tested by the derive implementation.

Poem

A rabbit checks each flag in line,
With static tables neat and fine.
The parser hops through argv’s trail,
While specs and help reveal the tale.
Tests thump paws: the derive is bright!
Workspace carrots ship tonight. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding a derive feature that compiles a struct into parse tables and a spec.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: markdown on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1, startup on bamboo-v2-ubuntu24.04-x64-30vcpu-24gb-rust1.97.1

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.

54eb63eb31f5 vs 8fed24376aa6 · measured on the runner, not pushed to the history.

Comment thread derive/src/model.rs Outdated
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces the usage-derive procedural macro, generating static parsing tables, specification metadata, and direct struct-field binding from a single CLI declaration.

  • Adds the new derive crate and workspace/release integration.
  • Generates parsing, KDL specification, and documentation metadata.
  • Adds end-to-end conformance coverage for flags, positionals, aliases, defaults, negation, counts, and rendering.
  • The current changes resolve the previously reported alias validation, generic-struct, Unicode-short, and count-overflow issues.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
derive/src/model.rs Parses and validates derive declarations, including normalized aliases, collision detection, addressable short forms, empty-name rejection, and unsupported generic parameters.
derive/src/codegen.rs Emits static parser/spec tables and direct binding code, with safe ASCII short emission and saturating count accumulation.
derive/src/lib.rs Exposes the new procedural derive and documents its supported types, attributes, and current limitations.
conformance/tests/derive.rs Exercises generated parsing and specification behavior across representative flags, positionals, defaults, negation, repetition, rendering, and edge cases.
Cargo.toml Registers usage-derive as a workspace member and shared dependency.

Reviews (8): Last reviewed commit: "fix(derive): stop generated code from co..." | Re-trigger Greptile

Comment thread derive/src/model.rs Outdated
Comment thread derive/src/codegen.rs
Comment thread derive/src/codegen.rs
Comment thread derive/src/codegen.rs Outdated

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

All five findings fixed. Every one was real, and the first is the interesting one.

finding fix
var enabled greedy collection var and variadic are now separate, spelled as a spec spells them; a Vec flag is repeatable without saying so
negation aliases bypassed collision checks a negation is another long form, so it collides like one
generated impl dropped struct generics refused, with a reason — the tables are static and fields must be concrete
non-ASCII shorts truncated refused: a short is matched as one byte, so could never be recognized
u8 count overflowed saturates, with the accumulator typed as the field rather than inferred

The first one deserves a note: that is the same repeatable-vs-greedy conflation #799 fixed in the conformance harness, recurring in the derive's attribute vocabulary a day later. Twice in one week is a design smell, not bad luck — the spec spells greedy as a variadic argument (--include <pattern>...) and repeatable as var on the flag, and any layer that collapses them will eat a positional. The derive now uses both words, and three tests pin the difference.

Worth knowing that my original test missed it by always writing --include twice, which is exactly the case where the two behave identically.

17 tests, and the three new compile errors were checked by hand for message and span.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

Comment thread derive/src/model.rs Outdated
Comment thread derive/src/model.rs

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Two more findings from the latest review, both fixed.

long = "--no-color" was unreachable. Stored with its dashes, while a token has its -- taken off before matching — so nothing could ever reach it, and the duplicate check could not see that it collided with a plain no-color. Explicit long names are now stripped the way negations already were, and a test covers the round trip: the spec records color on the long form and --no-color on the negation, which is how a spec spells each.

Some shorts can never be delivered. -5 reads as a negative number so that --offset -1 works, -- ends flag parsing, and = separates a short from its value in -j=8. Each is refused with that reason instead of compiling into a flag nobody can type.

Both are the same shape of bug as the earlier ones: the derive accepted a declaration the grammar cannot honor. Worth noting that is the failure mode to look for in the rest of this crate — the grammar is written down, so the derive's job is to refuse anything it does not permit, and every finding so far has been a place where it did not.

18 tests. On the check status: Greptile reports failure because the review scored 3/5 against this repo's 4/5 threshold, not because a finding is outstanding — its summary says as much.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

Comment thread derive/src/model.rs Outdated
@jdx
jdx force-pushed the agent/spec-help-heading branch from cc568bc to 8fed243 Compare August 11, 2026 13:55
@jdx
jdx force-pushed the agent/derive-v0 branch from 2c22f03 to e51a6aa Compare August 11, 2026 13:56
Comment thread derive/src/model.rs Outdated
Comment thread derive/src/model.rs Outdated
Comment thread derive/src/model.rs

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Three more findings fixed, and two of them are the same distinction biting a third time.

finding fix
variadic on a non-Vec field kept only the last value has to be a Vec — the only field that can hold what the flag collects
every Vec flag marked repeatable, including variadic-only ones variadic no longer implies var; emitting both claimed two ways of collecting at once
a bare short took the field's first letter at read time resolved after all attributes are read, as a bare long already was

The middle one is worth dwelling on. That is now three places the repeatable-vs-variadic distinction has gone wrong: the conformance harness (#799), the derive's attribute vocabulary, and now its spec emission. Each time the symptom differed — a stolen positional, then a flag that claims both var=#true and a ... argument — and each time the cause was a layer treating "collects several values" as one idea.

Two tests now assert the emitted spec for both shapes side by side (--include repeatable with a single-value argument, --exclude with a variadic argument and no var), which is the check that would have caught all three.

The bare-short fix needed a test where the field name and the renamed name start with different letters — verbose renamed to quiet — since anything else passes by coincidence.

21 tests. Also rebased on the updated #802 and #801.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

Comment thread derive/src/model.rs

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

One new finding fixed; the other three in that batch are re-posts of what the previous two commits already fixed — the bots re-review the cumulative diff, so those threads read worse than the code is. Verified in the current tree: variadic requires a Vec, variadic no longer implies var, and a bare short resolves after all attributes are read.

New: a short of ' ' or '\t' was written straight into the flag's forms string, which a spec spells space-delimited — so the declaration could not be read back. Refused with the reason.

That is the fifth character class a short cannot be, and they now sit together in one place: non-ASCII, digits, -, =, and whitespace or control characters. Each one is the same underlying shape — the derive accepting a declaration the grammar or the spec format cannot carry — which is the thing to look for in the rest of this crate. Worth saying plainly: seven of the eleven findings on this PR have been that, so the derive's job is less "translate attributes" than "refuse what cannot round-trip", and I'd treat any new attribute as guilty until it has a rejection test.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

@jdx
jdx force-pushed the agent/spec-help-heading branch from 8fed243 to da412c7 Compare August 11, 2026 15:47
Base automatically changed from agent/spec-help-heading to main August 11, 2026 15:50
jdx and others added 7 commits August 11, 2026 15:50
usage-derive reads a Rust type and emits the two trees the previous PRs
defined: static parse tables for usage-argv, static metadata for the spec,
and a parse function that is a match on table keys assigning straight into
the struct's fields. Nothing is built at run time — `command()` hands back
a static — and a successful parse touches only the tables.

One declaration is now enough to get a parser, a spec, markdown, a
manpage, and grouped help. A field with `long` or `short` is a flag and
anything else is positional; help comes from the doc comment, first
paragraph short and the whole thing long.

Scope is one command per struct. Subcommands need an enum, cross-type table
references, and a nested path through the parse function, which is its own
PR. Values are held as the text they arrive as, because converting them is
also where `env`, required-ness, and choices get enforced and that layer
does not exist yet — so any other field type is a compile error rather than
a surprise, and PLAN.md carries both as boxes.

Twelve tests parse a deliberately awkward CLI and check the same
declaration emits a spec usage-lib accepts, renders as docs, and groups by
heading. The error messages get their own attention: a wrong `short = "j"`
says to write `short = 'j'`, a duplicate flag points at both declarations,
and an argument after a variadic one explains why it could never be filled.

Not published yet: a CLI framework that cannot express subcommands is not
one to depend on by accident. The version tracks the workspace so it is
ready when it can.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Five review findings, all real.

The first is the distinction #799 fixed in the conformance harness,
recurring here: `var` was setting the parser's greedy collection, so
`--include a b` gave the flag both values and stole the positional that `b`
should have filled. The two are now spelled the way a spec spells them —
`var` for a flag that may be repeated, `variadic` for one occurrence that
keeps taking values — and a `Vec` flag is repeatable without having to say
so. The original test missed this by always writing `--include` twice.

A negation is another long form, so it now collides like one; two flags
could previously answer to the same token with only the first reachable.
Generic parameters were silently dropped from the generated impl, and are
now refused with a reason. A non-ASCII `short` was truncated to one byte and
could never be matched, so it is refused too. And a `u8` count given 256
occurrences panicked in debug and wrapped to zero in release; it saturates,
with the accumulator typed as the field rather than inferred.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two more review findings.

An explicit `long = "--no-color"` was stored with its dashes, while a token
has its `--` taken off before it is matched — so the flag could never be
reached, and the duplicate check could not see that it collided with a
plain `no-color`. Explicit long names are stripped the way negations
already were.

Some ASCII shorts can never be delivered because the grammar spends the
character on something else first: `-5` reads as a negative number so that
`--offset -1` works, `--` ends flag parsing, and `=` separates a short from
its value. Each is refused with the reason rather than compiled into a flag
nobody can type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`#[usage(name = "--color", long)]` derived its long form from the raw name,
so the flag answered to a token nobody could send — a token has its dashes
taken off before it is matched. The spec name is stripped where it is read,
which fixes the derived long form and keeps a dashed spelling out of the
spec's flag name at the same time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…late

Three more findings, and two of them are the same distinction biting again.

A `variadic` flag on a non-`Vec` field was accepted, and the generated code
assigned each value in turn, so only the last survived. It now has to be a
`Vec`, since that is the only field that can hold what the flag collects.

Every `Vec` flag was also marked repeatable, including one declared only
`variadic` — which emitted `var=#true` alongside a `...` argument, claiming
both ways of collecting at once. `variadic` no longer implies `var`.

And a bare `short` took the field's first letter at the moment it was read,
so `short` before `name = "quiet"` gave `-v` for a field called `verbose`.
It is resolved after all attributes are read, as a bare `long` already was.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A spec spells a flag's forms as one space-delimited string, so a short of
`' '` or `'\t'` was written straight into it and produced a declaration
nothing could read back. Refused, with the reason.

That is the fifth character class a short cannot be, so they are now all
checked in one place: non-ASCII, digits, `-`, `=`, and whitespace or
control characters.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Held back on the grounds that a CLI framework without subcommands is not
one to depend on by accident. Publishing it instead: it is easier to argue
with something that exists, the limits are stated plainly in the crate
docs, and the version is shared so it tracks the rest of the workspace.

The release script publishes it in dependency order, includes derive/ when
computing the version bump and changelog, and stages its bumped manifest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the agent/derive-v0 branch from 54eb63e to 0b820df Compare August 11, 2026 15:51

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 0b820df. Configure here.

Comment thread derive/src/model.rs
Comment thread derive/src/model.rs
Comment thread derive/src/model.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
derive/src/codegen.rs (1)

136-141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The helper functions trigger dead_code warnings in consumer crates.

text and value_text are nested functions inside parse_from. If a CLI declares only bool or count flags and no positionals, neither helper is called, and the consumer crate gets dead_code warnings from code it did not write. The #[allow(...)] at Line 67 applies to the hidden module only.

♻️ Proposed change
+                #[allow(dead_code)]
                 fn text(value: &[u8]) -> ::std::string::String {
                     ::std::string::String::from_utf8_lossy(value).into_owned()
                 }
+                #[allow(dead_code)]
                 fn value_text(value: ::std::option::Option<&[u8]>) -> ::std::string::String {
                     value.map(text).unwrap_or_default()
                 }
🤖 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 `@derive/src/codegen.rs` around lines 136 - 141, Update the nested helper
functions text and value_text inside parse_from so they do not emit dead_code
warnings when no positional arguments use them. Apply an appropriate local allow
attribute to these helpers, preserving their existing behavior and keeping the
suppression scoped to the generated functions.
derive/src/lib.rs (1)

43-65: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Document the required usage-argv dependency and its spec feature.

Generated code always imports ::usage_argv::spec::{ArgMeta, CommandMeta, FlagMeta, Spec} (derive/src/codegen.rs Line 69). A consumer crate that depends on usage-argv without the spec feature gets an unresolved-import error inside the expansion, which is hard to trace back to the derive. Add one sentence to the crate docs that states the dependency and the feature.

🤖 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 `@derive/src/lib.rs` around lines 43 - 65, Add a sentence to the crate-level
documentation in derive/src/lib.rs stating that generated code requires the
usage-argv dependency with its spec feature enabled. Place it near the existing
declaration and usage documentation without changing the documented option
behavior.
🤖 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 `@derive/src/codegen.rs`:
- Around line 136-173: Prefix all generated accumulator locals and parser
internals in the emitted parser code with a reserved __usage_ prefix to avoid
collisions with user field names; update field initialization, flag_arm,
arg_arm, helper functions, parser loop bindings, and event-pattern bindings
consistently. In the final Self struct literal, map each field explicitly from
its renamed accumulator local rather than relying on shorthand initialization.
- Around line 313-343: Update field_init for Shape::Many and Shape::Bool so
parsing applies defaults exactly as recorded by flag_meta and arg_meta:
initialize repeated fields from field.default rather than always using an empty
Vec, and use the model’s supported boolean-default semantics instead of only
recognizing the exact lowercase "true". If those defaults are unsupported,
reject them during model validation rather than emitting a spec that disagrees
with parsing.

In `@derive/src/model.rs`:
- Line 115: The default command name in the model generation path currently
passes the struct identifier directly through `to_kebab`, which does not convert
CamelCase. Add or reuse a helper that converts identifiers such as `MyTool` and
`Cli` to lowercase kebab-case, and use it for the `name` derived from
`input.ident`; keep `to_kebab` unchanged for snake_case field names.
- Around line 383-409: Add validation alongside the existing !is_flag checks in
the model’s field-kind validation to reject positional bool fields lacking both
long and short forms. Detect the bool field shape/type before code generation
and return a syn::Error anchored at span, so derive/src/codegen.rs::arg_arm is
never asked to assign text to a bool.

---

Nitpick comments:
In `@derive/src/codegen.rs`:
- Around line 136-141: Update the nested helper functions text and value_text
inside parse_from so they do not emit dead_code warnings when no positional
arguments use them. Apply an appropriate local allow attribute to these helpers,
preserving their existing behavior and keeping the suppression scoped to the
generated functions.

In `@derive/src/lib.rs`:
- Around line 43-65: Add a sentence to the crate-level documentation in
derive/src/lib.rs stating that generated code requires the usage-argv dependency
with its spec feature enabled. Place it near the existing declaration and usage
documentation without changing the documented option behavior.
🪄 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: e8636906-488a-46f1-8676-e38bc12ef92c

📥 Commits

Reviewing files that changed from the base of the PR and between 5b917ad and 0b820df.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml
  • PLAN.md
  • conformance/Cargo.toml
  • conformance/tests/derive.rs
  • derive/Cargo.toml
  • derive/src/codegen.rs
  • derive/src/lib.rs
  • derive/src/model.rs
  • tasks/release-plz

Comment thread derive/src/codegen.rs Outdated
Comment thread derive/src/codegen.rs
Comment on lines +313 to +343
fn field_init(field: &Field) -> TokenStream {
let ident = &field.ident;
match field.shape {
Shape::Bool => {
// A negatable flag's default is whatever `default` says, since
// `--no-x` has to be able to turn something off.
let start = field.default.as_deref() == Some("true");
quote!(let mut #ident = #start;)
}
Shape::Count => {
// Typed, so `saturating_add` below resolves to the field's own integer.
let ty = &field.ty;
quote!(let mut #ident: #ty = 0;)
}
Shape::Optional => {
let default = match field.default.as_deref() {
Some(d) => quote!(::std::option::Option::Some(#d.to_string())),
None => quote!(::std::option::Option::None),
};
quote!(let mut #ident = #default;)
}
Shape::Required => {
let default = match field.default.as_deref() {
Some(d) => quote!(#d.to_string()),
None => quote!(::std::string::String::new()),
};
quote!(let mut #ident = #default;)
}
Shape::Many => quote!(let mut #ident = ::std::vec::Vec::new();),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

The spec records defaults that the parse does not apply.

Two cases diverge from flag_meta/arg_meta, which always write field.default into the spec:

  • Line 341: Shape::Many starts as an empty Vec and ignores field.default. A Vec<String> field with default = "x" documents x but parses to [].
  • Line 319: Shape::Bool compares against the exact string "true". default = "TRUE" or default = "1" silently yields false.

Either apply the default in both shapes, or reject the unsupported combinations in model.rs so the spec and the parse agree.

♻️ One option for `Shape::Many`
-        Shape::Many => quote!(let mut `#ident` = ::std::vec::Vec::new();),
+        Shape::Many => match field.default.as_deref() {
+            Some(d) => quote!(let mut `#ident` = ::std::vec![`#d.to_string`()];),
+            None => quote!(let mut `#ident` = ::std::vec::Vec::new();),
+        },
🤖 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 `@derive/src/codegen.rs` around lines 313 - 343, Update field_init for
Shape::Many and Shape::Bool so parsing applies defaults exactly as recorded by
flag_meta and arg_meta: initialize repeated fields from field.default rather
than always using an empty Vec, and use the model’s supported boolean-default
semantics instead of only recognizing the exact lowercase "true". If those
defaults are unsupported, reject them during model validation rather than
emitting a spec that disagrees with parsing.

Comment thread derive/src/model.rs
Comment thread derive/src/model.rs
Seven findings, and the first is the one that would have bitten a real
adopter. Generated locals were named after the fields, and the parse
function also defines helpers called `text`, `value_text`, `parser`, and
`argv` — so a CLI with a field of any of those names failed to compile, or
worse, read the wrong binding. Every generated name now carries a
`__usage_` prefix and the struct is built with explicit `field: local`
pairs, so a field can be called anything. A test declares six fields named
after the internals.

The rest are declarations that recorded something the generated code would
not honor, which is this crate's recurring failure mode:

- `var` together with `variadic` claimed both ways of collecting at once
- a `bool` positional had nowhere to put the word it was given
- a `bool` default other than true/false, a `count` default, and a default
  on a collecting field were written into the spec and never applied
- a `long` of only dashes stripped to nothing and could never be matched
- a bare `long` alongside an explicit one kept the pre-rename name

And a `CamelCase` struct kept its spelling as the command name, so
`MyLittleCli` is now `my-little-cli` — which is what it would be typed as.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx
jdx force-pushed the agent/derive-v0 branch from e524ae2 to 973a60a Compare August 11, 2026 15:58
@jdx
jdx merged commit 8bf7c96 into main Aug 11, 2026
7 of 8 checks passed
@jdx
jdx deleted the agent/derive-v0 branch August 11, 2026 16:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant