From 8ee880c58212ec7a85989197395e029dfc7d31fd Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:09:51 +0000 Subject: [PATCH 1/2] docs: write down what clap can say and we cannot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An audit of clap's surface against the derive's attribute list, usage-argv, and the spec model, done now that most of the "what a CLI framework has to have" list is ticked. Ordered by whether a gap changes what a CLI *does* rather than how its help looks, since that is what decides whether an adopter can move. Argument groups and `exclusive` are dropped by `clap_usage` as well, so every spec in the fleet generated from a clap command is already lossy in exactly that way. That is the same shape as the `conflicts` hole, and both are readable from a clap `Command`, so the bridge gains them once the spec can hold them. `requires` is the one the bridge will never carry. clap 4.6 gives `Arg::requires` and its variants as setters with no getter and keeps the field `pub(crate)`, so a `Command` cannot be asked what it requires. It is in `Debug for Arg`, so scraping would technically recover it — declined, because that format carries no compatibility promise and would break by producing a wrong spec rather than a failed build. So `requires` reaches a spec only by being declared in usage, which is an argument for declaring it here rather than a bug on our side. Also ticks three boxes the audit found had quietly been done: self-contained completions, the `help` subcommand, and diagnostics. A plan file whose unchecked boxes are the status is worth less every time one of them is wrong, and the "secondary flag aliases" the derive supposedly could not declare turned out to be two of those. Co-Authored-By: Claude Opus 5 --- PLAN.md | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 132 insertions(+), 13 deletions(-) diff --git a/PLAN.md b/PLAN.md index 4365bf2e9..16f734df7 100644 --- a/PLAN.md +++ b/PLAN.md @@ -174,10 +174,14 @@ manpages, and SDKs — never a runtime dependency of somebody else's program. an unbounded variadic on one side of a flatten and a later positional on the other — and the flag-form collision. Both are invisible to either expansion and visible where the tables are joined. -- [ ] **`usage-derive` v1** — everything mise needs: constraints - (`requires`/`conflicts`/`overrides`/`required_unless`), `var`, `count`, - `env`, defaults, delimiters, the `double_dash` modes, global flags, flatten, - boxed subcommand variants, headings, `cfg`-gated variants. +- [ ] **`usage-derive` v1** — everything mise needs. `conflicts`, `overrides`, + `required_if`, `required_unless`, `var`, `count`, `env`, defaults, the four + `double_dash` modes, global flags, flatten, boxed subcommand variants, + headings and `cfg`-gated variants have all landed since this was written. + What is left of the original list is **`requires`**, which no part of this + repository can express — not the derive, not the spec, not the clap bridge — + and **delimiters**, where a value's `,` is split at parse time rather than + only in a clap default. Both are in the clap-parity list below. - [x] **The post-binding layer** — `required`, `choices`, `env` fallback, defaults, `var_min`, `conflicts`, `required_if`, `required_unless`, and `overrides` — `var_max` moved to the binder, see the decision below. All of them need a value's type, so they belong with the derive @@ -270,19 +274,134 @@ tasks --usage"`, so task names are meant to come from running that. usage-argv d flags so a CLI that declares its own keeps it, and a request comes back as `Error::Help` carrying the command it was asked about. `parse()` renders and exits; `parse_from` returns it, so a library embedding this decides for itself. Costs 111 instructions, since the check - is only reached by a flag that matched nothing. What is left is the `help` _subcommand_, - which every CLI with subcommands should have. Holding the rendering to parity found ten - things a spec could say that the derive could not, and two bugs in usage-lib's own renderer. -- [ ] **Completions, self-contained** — ` completion ` emits the - script; a hidden ` complete-word` serves requests from the binary's own - embedded spec. Same dispatch shape usage-cli uses today, without requiring - `usage` on the end user's machine. bash, zsh, fish first. + is only reached by a flag that matched nothing. The `help` _subcommand_ landed with it: + asked after the subcommand lookup, so a CLI declaring its own `help` keeps it, and the + words after it name a command rather than being descended into. Holding the rendering to + parity found ten things a spec could say that the derive could not, and two bugs in + usage-lib's own renderer. +- [x] **Completions, self-contained** — ` completion ` emits the + script; a hidden `__complete_word__` serves requests from the binary's own + tables. Same dispatch shape usage-cli uses today, without requiring `usage` + on the end user's machine. bash, zsh, fish and PowerShell, behind the + `complete` feature and asked for with `#[usage(completion)]`. - [ ] **Docs and manpages** — no new code: confirm the emitted KDL feeds `usage g markdown|manpage` exactly as a clap-derived spec does today, so an adopter's docs pipeline does not change. -- [ ] **Diagnostics** — rich errors behind a feature. The hot path returns a +- [x] **Diagnostics** — rich errors behind the `diagnostics` feature. The hot path returns a compact code; rendering re-examines the command line only once it has - already failed. + already failed, and says what clap would have said, colour included, down to + suggesting what was probably meant. `parse()` renders and exits the way a + program does; `parse_from` hands the error back. + +### What clap can say that we cannot + +An audit of clap's surface against `derive/src/model.rs`, `argv/src/` and the +spec model in `lib/src/spec/`, done once the framework list above was mostly +ticked. This is the list that decides whether an adopter can move without losing +behaviour, so a gap here is worth more than another point of speed. + +Some of these are dropped by **`clap_usage` too**, which means every spec in the +fleet generated from a clap command is already lossy in exactly this way — the +same shape as the `conflicts` hole above, found the same way. Those are marked +_(bridge too)_ and are the reason this list is ordered as it is. + +One of them the bridge will never carry. clap 4.6 gives `Arg::requires`, +`requires_if`, `requires_ifs` and `requires_all` as **setters with no getter**, +and keeps the field `pub(crate)`, so a `Command` cannot be asked what it +requires. It does appear in `Debug for Arg`, so scraping the debug format would +technically recover it — declined, because that format carries no compatibility +promise and would break by producing a wrong spec rather than a failed build. +The consequence is worth stating plainly: `requires` reaches a spec **only** by +being declared in usage, and a CLI that keeps its declaration in clap does not +have this constraint in its spec at all. + +Groups are the opposite case: `Command::get_groups`, `ArgGroup::get_args` and +`Arg::is_exclusive_set` are all public, so the bridge gains those for free. + +**Changes what a CLI does** + +- [ ] **`requires` / `requires_if` / `requires_ifs`** — "this flag needs that + one". The spec has `conflicts`, `overrides`, `required_if` and + `required_unless`, and no way to say the positive form. Nothing in + `lib/src/spec/flag.rs` parses it and the derive has no attribute. The one + true blocker in the original v1 list. The bridge will not carry it, per the + note above, so this is a reason to declare in usage rather than a bridge + bug: a CLI that moves its declaration here gains a constraint its generated + spec never had. +- [ ] **`ArgGroup`, and `exclusive`** _(bridge too)_ — "exactly one of these + three", "at least one of these". Only pairwise `conflicts` exists, which is + O(n²) declarations for what clap says once, and cannot express requiredness + across a set at all. Readable from a clap `Command`, so the bridge gains + these once the spec can hold them. +- [ ] **`value_delimiter`** — `--tags a,b,c` as three values. `lib/src/spec/arg.rs` + splits a clap _default_ by it and says the spec has a list rather than a + delimiter, which is true of a default and not of a command line: nothing + splits a value at parse time. +- [ ] **`default_missing_value`, and optional-value flags** — `--color` versus + `--color=always`, which is `Option>` in clap. `Shape` has no + variant for it. +- [ ] **`default_value_if` / `default_value_ifs`** — a default that depends on + another flag. Ours are unconditional. +- [ ] **`value_parser`** — clap takes an arbitrary parser function and range + validators (`value_parser!(u16).range(1..=65535)`). We are `T: FromStr` and + nothing else, so there is no per-field validation and no bounded numeric. +- [ ] **`allow_hyphen_values` on the derive path** — the spec says it and + usage-lib honours it (`lib/src/parse.rs`), but there is no derive attribute + and no mention in `argv/src/`. A spec-versus-derive asymmetry rather than a + plain absence, which makes it cheaper than the rest of this group. +- [ ] **`require_equals`** — accept `--flag=value` and refuse `--flag value`. +- [ ] **`#[arg(skip)]`** — a field that is not an argument at all, filled from + `Default`. Every field is currently a flag or a positional. + +**Changes what a CLI accepts, less sharply** + +- [ ] **`ignore_case` on values, and `#[value(alias = …)]`** — subcommand variants + take aliases; enumerated _values_ take neither an alias nor a case-insensitive + match. +- [ ] **`infer_subcommands` / `infer_long_args`** — unambiguous prefixes. We + suggest what was probably meant but do not accept it. +- [ ] **`external_subcommand`** — the bridge reads clap's flag to compute + `forwards` in `lib/src/spec/cmd.rs`, and the derive cannot declare one. +- [ ] **`multicall` and `no_binary_name`** — busybox-style applets, and parsing an + argv that has no `argv[0]`. +- [ ] **`arg_required_else_help`, `args_conflicts_with_subcommands`, + `subcommand_negates_reqs`, `allow_missing_positional`.** + +**Help output** + +- [ ] **Colour.** Errors are styled (`argv/src/diagnostic.rs`); help is not. clap + colours headings and flag names by default and exposes `Command::styles`. +- [ ] **`term_width` / `max_term_width`** — we wrap to `COLUMNS` and take no + instruction about it. +- [ ] **The granular hides** — `hide_default_value`, `hide_env`, `hide_env_values`, + `hide_possible_values`, `hide_short_help`, `hide_long_help`. We have + whole-entry `hide` and nothing finer. +- [ ] **`help_template`, `next_line_help`, `flatten_help`, + `subcommand_help_heading`, `subcommand_value_name`.** +- [ ] **`verbatim_doc_comment`, `rename_all`, `rename_all_env`** — casing is kebab + via `to_kebab`, with no opt-out. + +**API surface** + +- [ ] **`update_from` / `try_update_from`** — merge a parse into an existing struct. +- [ ] **The builder** — `Command::new`, `augment_args`, `CommandFactory`, + `ArgMatches::get_one`, hand-written `FromArgMatches`. Architectural, and + deliberate: usage-lib interprets a spec at run time and covers the dynamic + case from the other side. Worth writing down as a decision rather than + leaving it to be discovered as an absence. + +**What is _not_ a gap**, checked rather than assumed, because two of these were +recorded as gaps here and had quietly been closed: flag aliases (several `long` +and `short` per field), subcommand aliases and hidden aliases, `--help`/`-h`, the +`help` subcommand, `--version`/`-V` with per-command propagation, self-contained +completions for four shells, `flatten`, `global`, `count`, `env`, `negate` +(clap's `SetFalse`), `value_enum`, `num_args` via `var_min`/`var_max`, clap's +`last` via `double_dash`, `help_heading`, `subcommand_required`, declaration +order, and non-UTF-8 `OsString`/`PathBuf` values. + +And the other direction, which `gen-shadow` already counts: `mount`, +`restart_token`, `default_subcommand`, `effect`, and three of the four +`double_dash` modes are things a spec says that clap cannot hear. ### The gate From f34e7b0b4ba46e3f7bd26d3fb287bf3e2e1d27cc Mon Sep 17 00:00:00 2001 From: default <216188+jdx@users.noreply.github.com> Date: Sun, 16 Aug 2026 23:38:48 +0000 Subject: [PATCH 2/2] docs: say which side drops a `double_dash` mode, and name the conditional forms MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two corrections from review of the audit. The claim that clap cannot hear three of the four `double_dash` modes was repeating an older line in this file without checking it. The bridge carries three: `lib/src/spec/arg.rs` reads `last` as `required` and `trailing_var_arg` as `automatic`, and the default as `optional`. Only `preserve` has no clap spelling. What drops the other two is `gen-shadow` writing a clap shadow, whose `clap_double_dash` emits only `last` — a gap in the shadow generator rather than in clap, and worth closing so the shadow stops overstating the distance. And the milestone line named only `requires`, which would have read as done once the unconditional form landed. `requires_if` and `requires_ifs` are their own work. Co-Authored-By: Claude Opus 5 --- PLAN.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/PLAN.md b/PLAN.md index 16f734df7..0f00ec31b 100644 --- a/PLAN.md +++ b/PLAN.md @@ -178,10 +178,12 @@ manpages, and SDKs — never a runtime dependency of somebody else's program. `required_if`, `required_unless`, `var`, `count`, `env`, defaults, the four `double_dash` modes, global flags, flatten, boxed subcommand variants, headings and `cfg`-gated variants have all landed since this was written. - What is left of the original list is **`requires`**, which no part of this - repository can express — not the derive, not the spec, not the clap bridge — - and **delimiters**, where a value's `,` is split at parse time rather than - only in a clap default. Both are in the clap-parity list below. + What is left of the original list is **`requires`** — with `requires_if` and + `requires_ifs`, the conditional forms, which are their own work rather than + a detail of the first — none of which any part of this repository can + express: not the derive, not the spec, not the clap bridge. And + **delimiters**, where a value's `,` is split at parse time rather than only + in a clap default. Both are in the clap-parity list below. - [x] **The post-binding layer** — `required`, `choices`, `env` fallback, defaults, `var_min`, `conflicts`, `required_if`, `required_unless`, and `overrides` — `var_max` moved to the binder, see the decision below. All of them need a value's type, so they belong with the derive @@ -399,9 +401,19 @@ completions for four shells, `flatten`, `global`, `count`, `env`, `negate` `last` via `double_dash`, `help_heading`, `subcommand_required`, declaration order, and non-UTF-8 `OsString`/`PathBuf` values. -And the other direction, which `gen-shadow` already counts: `mount`, -`restart_token`, `default_subcommand`, `effect`, and three of the four -`double_dash` modes are things a spec says that clap cannot hear. +And the other direction: `mount`, `restart_token`, `default_subcommand` and +`effect` are things a spec says that clap cannot hear, and `gen-shadow` counts +them. + +`double_dash` is the one to state carefully, because two different claims about +it have been made in this file. The **bridge** carries three of the four modes: +`lib/src/spec/arg.rs` reads clap's `last` as `required` and `trailing_var_arg` as +`automatic`, and the default as `optional`. Only `preserve` — where the `--` is +itself a value — has no clap spelling. What drops the other two is **`gen-shadow` +writing a clap shadow**, whose `clap_double_dash` emits only `last`; that is a +gap in the shadow generator rather than in clap, since clap's derive does have +`trailing_var_arg`, and it is worth closing so the shadow stops overstating the +distance. ### The gate