Usage is a toolkit for building command-line tools. Define your CLI's commands,
flags, and args once in a KDL spec — get argument parsing, shell completions,
- --help, docs, and manpages from that one definition. Reference
+ --help, docs, and manpages from that one definition. Experimental reference
frameworks for Rust and Go build your CLI from the spec, with Python and
JavaScript planned.
diff --git a/docs/.vitepress/theme/custom.css b/docs/.vitepress/theme/custom.css
index a63b7a996..9900d6ab0 100644
--- a/docs/.vitepress/theme/custom.css
+++ b/docs/.vitepress/theme/custom.css
@@ -279,7 +279,8 @@
box-shadow: 0 0 14px rgba(185, 103, 255, 0.3);
}
-.usage-tile-soon-pill {
+.usage-tile-soon-pill,
+.usage-tile-experimental-pill {
font-family: Orbitron, sans-serif;
font-size: 0.55rem;
font-weight: 700;
@@ -292,6 +293,11 @@
margin-left: 0.15rem;
}
+.usage-tile-experimental-pill {
+ color: var(--vw-cyan);
+ border-color: var(--vw-cyan);
+}
+
/* Tooltips */
.usage-tile-tooltip {
position: absolute;
diff --git a/docs/go/binding.md b/docs/go/binding.md
new file mode 100644
index 000000000..4a228b660
--- /dev/null
+++ b/docs/go/binding.md
@@ -0,0 +1,83 @@
+# Binding and Values
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+Generated `Parse` does everything on this page for you. It's documented separately because the
+pieces are public — custom binding loops use them directly — and because the _rules_ matter even
+when you never call the functions: they define what your users' command lines mean.
+
+## Resolution order
+
+Each flag or arg resolves **argv → env → default**, matching
+[config resolution](/spec/resolution):
+
+```go
+values, source := argv.Fill(Meta.Lookup(key), given, argv.LookupEnv)
+```
+
+`Source` tells you where the value came from: `FromArgv`, `FromEnv`, `FromDefault`, or `Unset`.
+`Source.Given()` is true only for the first two — a default is a fallback, not something the
+user said, which matters for relations (below).
+
+The env rules are precise and worth knowing:
+
+- an **empty env variable is set** — `EX_JOBS=` provides the value `""`
+- an env value is **one token, never re-split** on whitespace or commas
+- for a value-less (boolean) flag, `argv.EnvTruth` decides whether the variable sets it at all —
+ the allow-list is narrow (`1`, `true`, `True`, `TRUE`), so `yes`, `on`, and `TrUe` do **not**
+ set the flag; this matches usage-lib
+
+## Checks
+
+```go
+if err := argv.Check(meta, values, occurrences); err != nil { /* … */ }
+```
+
+- `required` is asked first: a required variadic given nothing is _missing_, not _short_
+- `choices` are case-sensitive and every value is checked, not just the first
+- `var_min` only fires when something was given — an absent optional variadic hasn't broken its
+ minimum
+- `var_max` counts **occurrences**, so one occurrence bringing three values doesn't break
+ `var_max=1`
+
+## Relations
+
+```go
+err := argv.CheckRelationships(Meta, selectedKeys, sourceOf)
+winners := argv.ApplyOverrides(Meta, tokenOrder)
+```
+
+`conflicts`, `required_if`, and `required_unless` are judged with a deliberate asymmetry: a
+defaulted value counts for the entry _being judged_ but not for the _partners judging it_ — so a
+flag with a default doesn't conflict with everything anyone types.
+
+`ApplyOverrides` implements last-one-wins on token order, symmetric regardless of which flag
+declared the override, and runs _before_ fallbacks so a losing flag isn't refilled from env or a
+default. Remember: [generated `Parse` does not call it](/go/generated-code#what-parse-enforces).
+
+## Typed values
+
+Generated struct fields are `string`/`[]string` — a spec names a value, it doesn't type it.
+Conversions are explicit, and every failure is a `*argv.Error` with `CodeInvalidValue` carrying
+the entry's name, the offending text, and a human phrase for what was expected:
+
+```go
+n, err := argv.Int("jobs", cli.Jobs) // int64 — "a whole number"
+u, err := argv.Uint("retries", cli.Retries) // uint64 — "a whole number, not negative"
+f, err := argv.Float("ratio", cli.Ratio) // float64 — "a number"
+b, err := argv.Bool("color", cli.Color) // bool — "true or false"
+d, err := argv.Duration("wait", cli.Wait) // time.Duration — "a duration such as 30s or 1h30m"
+
+ports, err := argv.Each("ports", cli.Ports, argv.Int) // []int64, stops at the first bad value
+```
+
+Two sharp edges are deliberate:
+
+- **Nothing is trimmed.** `" 8 "` is refused, exactly as `" 8 ".parse::()` is in Rust — the
+ same spec means the same thing in both implementations.
+- **`Bool` is wider than `EnvTruth` on purpose.** `Bool` accepts Go's spellings (`1`, `t`, `T`,
+ `true`, `TRUE`, `True` and the false counterparts) for a value someone typed; `EnvTruth` stays
+ on usage-lib's narrow list for deciding whether an env var sets a value-less flag.
diff --git a/docs/go/completions.md b/docs/go/completions.md
new file mode 100644
index 000000000..17ce2e76f
--- /dev/null
+++ b/docs/go/completions.md
@@ -0,0 +1,84 @@
+# Completions
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+The Go runtime answers the question every completion request boils down to — _what could go
+where the cursor is?_ — from the same tables the parser runs on, so completions can never
+disagree with the grammar.
+
+## Position and candidates
+
+```go
+pos := argv.Walk(mycli.Root, wordsBeforeCursor)
+candidates := argv.Candidates(pos, partialWord, mycli.HelpText, mycli.Meta)
+```
+
+`Walk` treats parse errors as _positions_, not failures — an unfinished command line is the
+whole point. The `Position` tells you where the cursor stands:
+
+```go
+type Position struct {
+ Cmd *argv.Command // the command in scope
+ Chain []*argv.Command // root → here
+ FlagsPossible bool // false past `--`
+ AwaitingValue *argv.Flag // cursor is inside this flag's value
+ NextArg *argv.Arg // the positional that would bind next
+ HelpTopic bool // after `help`: completing a topic, not a command to run
+}
+```
+
+`Candidates` answers by asking the parser's own scope rules, never by re-deriving them:
+
+- subcommands and their visible aliases; hidden commands are never offered
+- flags in scope — a global inside a subcommand yes, a non-global root flag no, and masking is
+ per spelling, matching help and the parser
+- negation spellings (`--no-color`) as first-class candidates
+- a flag awaiting its value takes the position entirely: its `choices` and nothing else
+- the pending positional's `choices` — unless it demands a `--` that hasn't been typed
+- nothing flag-shaped past a `--`
+
+Filtering by the partial word happens here, so every shell agrees on what matches. Each
+candidate carries a `Describe` string for shells that display descriptions.
+
+## Speaking each shell's dialect
+
+```go
+out := argv.RenderAnswer(argv.Answer{Candidates: candidates}, argv.Zsh)
+```
+
+`RenderAnswer` writes one line per candidate, in the format each shell reads: bash gets the
+value alone; zsh gets display, description, and a quoted insert text (what it shows and what it
+types differ); fish, nu, and PowerShell get value-tab-description. Values and descriptions are
+sanitized so a candidate can never rearrange the protocol and make the shell insert something
+nobody offered.
+
+An `Answer` can also request the shell's native file or directory completion:
+
+```go
+argv.RenderAnswer(argv.Answer{Files: argv.AnyFile}, shell) // or argv.Dirs
+```
+
+## Wiring it up
+
+The protocol is the same one the Rust framework's generated shell scripts speak: the script
+calls your binary back with
+
+```
+ __complete_word__ --shell --line ""
+```
+
+Unlike the Rust framework — which intercepts `__complete_word__` automatically — the Go side
+leaves the wiring to you today:
+
+1. **Recognize the hidden subcommand** before normal parsing (check `args[0]`).
+2. **Split the line** into completed words plus the partial word under the cursor.
+3. Call `Walk` → `Candidates` → `RenderAnswer` and print the result.
+4. **Generate the install scripts** from your spec with the Rust CLI:
+ `usage g completion bash mycli --file mycli.usage.kdl` (and zsh/fish/…).
+
+Also not carried into the generated tables yet: spec-level `complete` run-scripts (only
+`choices` are known to `Candidates`) and `value_hint` (derive an `Answer.Files` request
+yourself where you want path completion).
diff --git a/docs/go/generated-code.md b/docs/go/generated-code.md
new file mode 100644
index 000000000..1a7fa26a4
--- /dev/null
+++ b/docs/go/generated-code.md
@@ -0,0 +1,120 @@
+# Generated Code
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+`usage generate go` lowers a KDL spec into one Go file. The output is `gofmt`-clean and carries
+the standard `// Code generated … DO NOT EDIT.` header.
+
+```bash
+usage generate go -f mycli.usage.kdl -o tables.go -p mycli
+```
+
+| Flag | Meaning |
+| ---------------------- | -------------------------------------------------------------------- |
+| `-f --file ` | the KDL spec (`-` for stdin) |
+| `--spec ` | a raw spec string instead of a file |
+| `-o --out-file ` | output path (`-` for stdout) |
+| `-p --package ` | package clause; defaults to the spec's `bin` made into an identifier |
+
+## What the file exports
+
+```go
+const Version = "1.2.3" // only when the spec declares a version
+
+const ( // one key per command, flag, and argument
+ CmdRoot uint64 = 1
+ FlagVerbose uint64 = 2
+ ArgFile uint64 = 3
+ CmdInstall uint64 = 4
+ // …
+)
+
+var Root *argv.Command // the hot parse table
+var Meta argv.Metadata // validation metadata (required, choices, env, defaults, relations)
+var HelpText argv.HelpTable // help text per entry
+var HelpMeta argv.HelpSpec // root-level page furniture (name, bin, version, about)
+
+type Cli struct { /* … */ } // one struct for the root
+type InstallCmd struct { /* … */ } // and one per command
+
+func Parse(args []string) (*Cli, error)
+```
+
+The three tables are separate on purpose: reference only `Root` and the linker drops the
+validation metadata and help text. On mise's spec that's the difference between a 2.60MB and a
+2.82MB contribution to the binary. Dispatch on the key constants, never on `Name` strings — a
+rename in the spec then fails to compile instead of silently misrouting.
+
+## The structs
+
+For the spec on the [intro page](/go/):
+
+```go
+// Cli is the whole command line.
+type Cli struct {
+ Verbose bool // FlagVerbose
+ Jobs string // FlagJobs
+ File string // ArgFile
+ Install *InstallCmd // CmdInstall
+}
+
+// InstallCmd is `install`.
+type InstallCmd struct {
+ Force bool // FlagInstallForce
+ Pkg string // ArgInstallPkg
+}
+```
+
+- The root struct is always `Cli`; a subcommand's is the Pascal-cased path plus `Cmd`
+ (`config ls` → `ConfigLsCmd`).
+- Subcommands are pointers, and at most one per level is non-nil — that's how you tell which
+ path was taken.
+- Field types: `count` flags → `int`; value-less flags → `bool`; `var` flags/args → `[]string`;
+ everything else → `string`. There is no type inference from the spec — a spec says what a
+ value is _called_, never what type it is. Convert with the
+ [typed helpers](/go/binding#typed-values).
+- A flag and a command sharing a name are disambiguated by kind: a `--shell` flag beside a
+ `shell` command yields fields `Shell` and `ShellCmd`, not `Shell2`.
+
+## What `Parse` enforces
+
+`Parse` walks the events, fills the structs, then — for the commands the words actually
+selected — applies fallbacks and checks:
+
+1. values resolve **argv → env → default**, per entry
+2. `required`, `choices`, `var_min`/`var_max` are checked
+3. `conflicts`, `required_if`, `required_unless` are checked across the selected commands
+
+A value-less flag set from an env var goes through `argv.EnvTruth` (usage-lib's narrow
+allow-list: `1`, `true`, `True`, `TRUE`); a `default` on one compares against the literal
+`"true"`. `count` fields are never filled from env or defaults — a count is occurrences, and
+only the command line has those. A `default_subcommand` routes in the parser, so the defaulted
+command's struct is filled with no caller involvement.
+
+Three things `Parse` deliberately does **not** do:
+
+- **`overrides` is not applied.** If your spec uses it, call `argv.ApplyOverrides` yourself.
+- **Help and version are not printed** — they come back as `*argv.Error` with `CodeHelp` /
+ `CodeVersion` for you to render ([Help and errors](/go/help)).
+- **No chain comes back with an error.** The renderers want the command chain; recover it with
+ `argv.Walk(Root, args)`, which returns the chain even for lines that failed to parse.
+
+## Using it against a real spec
+
+From the tests over mise's actual 211-command spec:
+
+```go
+cli, err := mise.Parse([]string{"use", "-g", "node@20"})
+// cli.Use != nil; cli.Use.Global == true; cli.Use.ToolVersion == []string{"node@20"}
+// cli.Config == nil — a command nobody ran is nil
+
+cli, _ = mise.Parse([]string{"tasks", "run", "build", "extra", "--", "--verbose"})
+run := cli.Tasks.Run
+// run.Task == "build"; run.Args == []string{"extra"}; run.ArgsLast == []string{"--verbose"}
+
+_, err = mise.Parse([]string{"--log-level", "chatty"})
+e := err.(*argv.Error) // e.Code == argv.CodeInvalidChoice
+```
diff --git a/docs/go/help.md b/docs/go/help.md
new file mode 100644
index 000000000..7e85f9834
--- /dev/null
+++ b/docs/go/help.md
@@ -0,0 +1,87 @@
+# Help and Errors
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+## Help pages
+
+Three renderers cover the usage line, the `-h` page, and the `--help` page:
+
+```go
+argv.UsageLine(path, cmd, HelpText) // "mise [FLAGS] [TASK] "
+argv.ShortHelp(HelpMeta, path, chain, HelpText) // the -h page
+argv.LongHelp(HelpMeta, path, chain, HelpText) // the --help page
+```
+
+`path` is the command as invoked, binary first (`[]string{"mise", "config", "ls"}`); `chain` is
+the `*argv.Command` chain from the root to the command (`argv.Walk` returns it, even for lines
+that failed to parse). A rendered page:
+
+```
+List config files currently in use
+
+Usage: mise config ls [FLAGS]
+
+Flags:
+ -J, --json Output in JSON format
+ -h, --help Print help
+
+Global flags:
+ -C, --cd Change directory before running command
+```
+
+The output is not merely similar to the reference implementation's — all 211 of mise's usage
+lines, `-h` pages, and `--help` pages are compared **byte for byte** against usage-lib's
+rendering in CI. Layout details you get for free: sections in canonical order, commands sorted
+with `[aliases: …]` shown for visible aliases, `help_heading` groups (first-seen order, unheaded
+entries first), a 4-column short-flag gutter, required entries in angle brackets, `[env: X]` and
+default annotations, and the long page wrapped at a fixed 80 columns.
+
+The short page appends `[choices]`, `[env: X]`, and (for arguments) `(default: …)` inline; the
+long page gives each its own line and prefers `long_help` over `help`. Examples declared on the
+root are inherited by commands that declare none.
+
+One rule is load-bearing: a page only advertises a flag spelling where that flag is the one that
+would _bind_ it. Masking is per spelling — a subcommand redeclaring `--jobs` leaves an inherited
+`-j` advertised if nothing claims it — and matches the parser exactly.
+
+## Rendering failures
+
+```go
+msg := argv.Render(err, path, chain, HelpText)
+```
+
+The shape is clap's, which your users have seen before:
+
+```
+error: unknown flag `--wat`
+
+Usage: ex run [-f --force]
+
+For more information, try `--help`.
+```
+
+- The usage line names the command the user was **in**, not the program.
+- `CodeHelp` and `CodeVersion` render as the empty string — print the page or version instead.
+- Every error code renders something specific. `missing_flag_value` names the likeliest cause
+ and the escape hatch in the flag's actual spelling: ``missing value for `--jobs` (a value
+beginning with `-` has to be attached: `--jobs=-x`)``. `invalid_choice` appends
+ `(expected one of: bash, zsh)`; the variadic codes pluralize correctly; `conflicting_flags`
+ names both sides.
+- Anything quoted back to the user — tokens, unexpected arguments, rejected values — has control
+ characters escaped, so a hostile argv can't smuggle escape sequences to the terminal.
+
+The error type itself is small enough to use directly:
+
+```go
+type Error struct {
+ Code Code // CodeUnknownFlag, CodeMissingRequiredFlag, CodeInvalidChoice, …
+ // plus the specifics: Token, Name, Choices, Bound, Got, Value, Want, Cmd, Long, …
+}
+```
+
+`Error()` (the `error` interface) is a bare one-liner; `Render` is the version for humans. The
+`Code` names are stable strings shared with the conformance corpus (`unknown_flag`,
+`invalid_choice`, `var_too_many`, …), so tests can assert on classes rather than message text.
diff --git a/docs/go/index.md b/docs/go/index.md
new file mode 100644
index 000000000..dfd4736a0
--- /dev/null
+++ b/docs/go/index.md
@@ -0,0 +1,141 @@
+# Go Framework
+
+::: warning Experimental — draft docs
+The Go framework is experimental. Its parsing behavior is verified against the same conformance
+corpus as the Rust implementation, but APIs may still change between releases. These docs are a
+draft: some of what they document is still in open pull requests, and details may change before
+release.
+:::
+
+The Go framework builds your CLI from a usage spec — but unlike most Go CLI libraries, your
+shipped binary never parses the spec. `usage generate go` lowers the KDL into plain Go tables,
+typed structs, and a `Parse` function at build time. The result:
+
+- **Zero dependencies.** The module is `github.com/jdx/usage/go` and imports nothing but the
+ standard library.
+- **Zero-allocation parsing.** A parse allocates nothing, on success and failure paths alike —
+ roughly 57–110ns per parse on mise's real 211-command spec.
+- **Linker-friendly.** Parse tables, validation metadata, and help text are three separate
+ tables; the linker drops the ones you don't reference. No `init` functions.
+- **One source of truth.** The same KDL spec generates your completions, docs, and manpages.
+
+## Quick start
+
+Write a spec:
+
+```kdl
+name "ex"
+bin "ex"
+version "1.0.0"
+flag "-v --verbose" global=#true help="be loud"
+flag "-j --jobs " help="how many jobs"
+arg "" help="the file to process"
+cmd "install" help="install a tool" {
+ alias "i"
+ flag "-f --force"
+ arg ""
+}
+```
+
+Generate the Go code:
+
+```go
+//go:generate usage generate go -f ex.usage.kdl -o tables.go -p ex
+```
+
+Parse:
+
+```go
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/jdx/usage/go/argv"
+)
+
+func main() {
+ cli, err := ex.Parse(os.Args[1:])
+ if err != nil {
+ exit(err.(*argv.Error))
+ }
+ if cli.Install != nil {
+ install(cli.Install.Pkg, cli.Install.Force, cli.Verbose)
+ return
+ }
+ process(cli.File)
+}
+```
+
+`Parse` returns a typed struct per command — `cli.Install` is `nil` unless `install` (or its
+alias `i`) was invoked — with flags bound, `env`/`default` fallbacks applied, and `required`,
+`choices`, `var_min`/`var_max`, and flag relations enforced.
+
+## Handling help, version, and failures
+
+Unlike the Rust framework's `parse()`, the generated Go `Parse` never prints or exits — help and
+version requests come back as errors with `Code` set, and rendering is yours to invoke. The
+standard exit function looks like this:
+
+```go
+func exit(e *argv.Error) {
+ pos := argv.Walk(ex.Root, os.Args[1:])
+ path := []string{"ex"}
+ for _, c := range pos.Chain[1:] {
+ path = append(path, c.Name)
+ }
+ switch e.Code {
+ case argv.CodeHelp:
+ if e.Long {
+ fmt.Print(argv.LongHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText))
+ } else {
+ fmt.Print(argv.ShortHelp(ex.HelpMeta, path, pos.Chain, ex.HelpText))
+ }
+ os.Exit(0)
+ case argv.CodeVersion:
+ fmt.Println("ex " + ex.Version)
+ os.Exit(0)
+ default:
+ fmt.Fprint(os.Stderr, argv.Render(e, path, pos.Chain, ex.HelpText))
+ os.Exit(2)
+ }
+}
+```
+
+The rendered pages match usage-lib's byte for byte, and the failure messages are clap-shaped —
+see [Help and errors](/go/help).
+
+## Why generation instead of a runtime spec?
+
+The Go module has no KDL parser, on purpose. Lowering a spec is `usage-cli`'s job, done once at
+build time; the shipped binary carries tables the linker can lay out as data. Building tables at
+runtime is not supported — generation is the only path, and the point.
+
+Everything is verified against the reference implementation: a shared JSON conformance corpus
+(all vectors passing) covers the parsing grammar, and all 211 of mise's usage lines, `-h` pages,
+and `--help` pages are compared byte-for-byte against usage-lib's rendering in CI.
+
+## Where to go next
+
+- [Generated code](/go/generated-code) — what `usage generate go` emits and what `Parse` does
+- [The parser](/go/parser) — the low-level zero-allocation event API
+- [Binding and values](/go/binding) — env/default resolution, validation, typed conversions
+- [Help and errors](/go/help) — rendering `-h`/`--help` pages and failures
+- [Completions](/go/completions) — answering shell completion requests
+
+## Current limitations
+
+Worth knowing before you commit:
+
+- **`overrides` is not enforced by generated `Parse`.** `conflicts`, `required_if`, and
+ `required_unless` are; a spec relying on last-one-wins `overrides` semantics needs to call
+ `argv.ApplyOverrides` itself.
+- **Fields are `string`, `bool`, `[]string`, or `int` (for counts).** A spec says what a value is
+ called, never what type it is — convert with [`argv.Int`, `argv.Duration`, etc.](/go/binding#typed-values)
+- **`complete` scripts, `config` nodes, `group`, `value_hint`, and `mount` are not carried into
+ the generated tables.** Completions know `choices`; config resolution is not implemented.
+- **Completion shell scripts come from the Rust side.** The Go runtime answers completion
+ requests over the same protocol, but you wire up the hidden subcommand yourself — see
+ [Completions](/go/completions).
+- Command trees deeper than 16 levels are rejected (`CodeTooDeep`); short flags must be ASCII.
diff --git a/docs/go/parser.md b/docs/go/parser.md
new file mode 100644
index 000000000..5e2362e38
--- /dev/null
+++ b/docs/go/parser.md
@@ -0,0 +1,80 @@
+# The Parser
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+Generated `Parse` is the front door, but the event-level API underneath is public and stable —
+use it when you need custom binding, a REPL, or completion positions.
+
+```go
+p := argv.New(mycli.Root, os.Args[1:])
+for p.Next() {
+ switch ev := p.Event(); ev.Kind {
+ case argv.KindCommand:
+ // ev.Command was selected
+ case argv.KindFlag:
+ // ev.Flag; ev.Value if ev.HasValue; ev.Negated for --no-* spellings
+ case argv.KindArg:
+ // ev.Value filled ev.Arg
+ }
+}
+if err := p.Err(); err != nil {
+ // a failure, or a help/version request
+}
+```
+
+`New` returns the parser by value — it lives on the stack, and a parse performs **zero heap
+allocations**, on success and failure paths alike (pinned by a `testing.AllocsPerRun` test).
+An error is terminal: events already yielded are not a partial result; discard the attempt.
+
+`Event.Value` aliases the argv strings — it's the raw OS bytes and is **not guaranteed to be
+valid UTF-8**. Validate where you build your target type.
+
+## Grammar
+
+The behavior below is pinned vector-by-vector in the shared conformance corpus, so it's the same
+grammar usage-lib and the Rust framework parse:
+
+| Input | Result |
+| ------------------------------------- | --------------------------------------------------------------------------- |
+| `--jobs=8`, `--jobs 8`, `-j8`, `-j=8` | the flag with value `8` |
+| `--jobs=` | empty string **is** a value (`HasValue` true) |
+| `--jobs=a=b` | value `a=b` — only the first `=` splits |
+| `-fv` | a short-flag bundle: `force`, then `verbose` |
+| `-fj8` | a value-taking short ends the bundle: `force`, `jobs=8` |
+| `--no-color` | the `color` flag with `Negated` set |
+| `--jobs -1` | negative numbers are the one detached dash-token accepted as a value |
+| `-- --force` | everything after `--` is positional |
+| `-` | a bare `-` binds as a value where it was typed |
+| `install` / `i` | command descent (aliases included) |
+| `--force install` | a subcommand's flags are not in scope above it |
+| `other install` | only the descent position routes — `install` here is a value |
+| `--include a b` | a variadic flag collects values until a flag-like token |
+| `--for` | no abbreviation inference — unknown flags fall through as values by default |
+
+Unknown flags are governed per command by the spec's `unknown_flags`: the default `"value"` lets
+the token fall through to the positionals (specs often wrap someone else's flags); `"error"`
+rejects it — and rejects a bundle like `-fz` **whole**, with no partial `-f` event.
+
+`--help`/`-h` arrive as ordinary flag events pointing at the package-level `argv.HelpShort` /
+`argv.HelpLong` flags. The bare word `help` is a question rather than a command: it stops the
+parse with `CodeHelp` and `Error.Cmd` set to the command asked about. `Error.Long` distinguishes
+`--help` from `-h`.
+
+Counting flags need nothing from the parser — each occurrence is its own event, and the caller
+tallies (generated code does `field++`).
+
+## Limits
+
+- `argv.MaxDepth` is 16; deeper command trees fail with `CodeTooDeep`. (mise is 4 deep.)
+- `Flag.Shorts` must be ASCII — a non-ASCII short can never match, and the rest of a bundle
+ after a value-taking short would begin mid-character.
+
+## Hand-written tables
+
+Tables are plain data (`argv.Command`, `argv.Flag`, `argv.Arg`), so writing them by hand is
+supported — but the generator is the intended path. If you do write them by hand: keys must be
+dense starting from 1, since `Metadata` and `HelpTable` are indexed by key and `Lookup` returns
+`nil` on drift rather than a neighbor.
diff --git a/docs/rust/args-and-flags.md b/docs/rust/args-and-flags.md
new file mode 100644
index 000000000..60cac2110
--- /dev/null
+++ b/docs/rust/args-and-flags.md
@@ -0,0 +1,159 @@
+# Args and Flags
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+A field on a `#[derive(Cli)]` or `#[derive(Args)]` struct becomes a flag when it carries `long`
+or `short`, and a positional argument otherwise (or explicitly with `#[usage(arg)]`).
+
+```rust
+#[derive(Cli)]
+#[usage(bin = "ex")]
+struct Cli {
+ /// User to run as
+ #[usage(short = 'u', long)]
+ user: Option, // flag: -u, --user
+
+ /// The directory to use
+ dir: String, // required positional:
+
+ /// The files to read
+ files: Vec, // variadic positional: [files]...
+}
+```
+
+## Types drive cardinality
+
+Whether something is required, optional, or repeatable is read off the field's type — the type
+has nowhere to put "absent", so `T` means required:
+
+| Field type | Meaning |
+| ---------------------- | --------------------------------------- |
+| `T` | one value, **required** |
+| `Option` | one value or nothing |
+| `Vec` | several values; empty when none arrived |
+| `Option>` | several values; `None` when never given |
+| `bool` | a switch |
+| `u8`…`usize` + `count` | occurrence count (`-vvv` → `3`) |
+
+Values are built with `FromStr`, so `PathBuf`, `usize`, `IpAddr`, and your own types all work.
+The `FromStr` error type must implement `Display` (a compile error names the type otherwise);
+a conversion failure at runtime becomes `Error::InvalidValue { name, value, reason }`.
+
+A `Vec` flag is repeatable (`var` in spec terms) automatically. Two related attributes cover the
+other shapes:
+
+- `var` — makes a _single-value_ flag repeatable where the last occurrence wins
+- `variadic` — one occurrence greedily takes values: `--include a b c`
+
+`var` and `variadic` together is a compile error. A **required** `Vec` is the one place
+required-ness is declared rather than inferred: `#[usage(arg, required)]`.
+
+## Field attributes
+
+```rust
+#[usage(short = 'j', long, env = "EX_JOBS", default = "4")]
+jobs: Option,
+```
+
+| Attribute | Effect |
+| --------------------------------------- | --------------------------------------------------------------------------------------- |
+| `long` / `long = "name"` | `--name` flag (defaults to the kebab-cased field name) |
+| `short` / `short = 'x'` | `-x` flag (defaults to the field name's first letter) |
+| `name = "…"` | Override the arg/flag name used in help and the spec |
+| `arg` | Force the field to be a positional argument |
+| `env = "VAR"` | Fall back to this environment variable when the flag/arg wasn't given |
+| `default = "…"` | Fall back to this value (repeatable for `Vec` fields) |
+| `negate = "--no-x"` | A negation flag that sets a `bool` back to false |
+| `count` | Count occurrences into an integer field |
+| `global` | Usable on any subcommand below this one |
+| `var` / `variadic` | Repeatable / greedy multi-value (see above) |
+| `var_min = n` / `var_max = n` | Bounds on how many values a `Vec` may hold |
+| `choices("a", "b")` | Restrict values to a fixed set |
+| `value_enum` | Take choices from a `#[derive(ValueEnum)]` type |
+| `delimiter = ','` | Split one word into several values ([Validation](/rust/validation#delimiters)) |
+| `group = "name"` | Join a flag group ([Validation](/rust/validation#groups)) |
+| `exclusive` | Must be given alone ([Validation](/rust/validation#exclusive-flags)) |
+| `conflicts(…)` / `requires(…)` | Relations to other flags ([Validation](/rust/validation)) |
+| `overrides(…)` | Later occurrence silently overrides the named flag |
+| `required_if(…)` / `required_unless(…)` | Conditional required-ness |
+| `complete = my_fn` | Custom completion function ([Completions](/rust/completions)) |
+| `value_hint = ValueHint::FilePath` | Complete values as paths (`FilePath`, `DirPath`, `AnyPath`) |
+| `value_name = "…"` | The placeholder shown in help (`--file `) |
+| `help = "…"` / `long_help = "…"` | Help text (doc comments are usually nicer) |
+| `help_heading = "…"` | Group the entry under a heading in help output |
+| `hide` | Omit from help, docs, and completions |
+| `required` | Explicit required-ness (for `Vec` fields) |
+| `value_optional` | Mark the value optional in help (help-only; the parser still wants one) |
+| `double_dash = "…"` | `"optional"`, `"required"`, `"preserve"`, or `"automatic"` `--` handling |
+| `effect = "…"` | `"read"`, `"write"`, or `"destructive"` — see [command effects](/spec/#command-effects) |
+| `setting = "key"` | Bind to a config setting (generates `parse_from_with_settings`) |
+| `verbatim_doc_comment` | Keep the doc comment's line breaks in help |
+
+Flag relations (`conflicts`, `requires`, `overrides`, `required_if`, `required_unless`) name
+their target the way the KDL spec does — `"--long"` or `"-s"`, one value or a list:
+
+```rust
+#[usage(long, conflicts("--file", "--url"))]
+stdin: bool,
+```
+
+Naming a flag that doesn't exist on the command is a **compile error**, not a runtime surprise.
+Relations are flag-to-flag only; a positional cannot carry one.
+
+## Resolution order
+
+After argv is parsed, each field resolves in this order — matching
+[config resolution](/spec/resolution) for the spec at large:
+
+1. the value given on the command line
+2. the `env` variable, if set
+3. the `default`, if declared
+
+Then validation runs: required-ness (skipped for anything a default or env var filled),
+`choices`, and `var_min`/`var_max`. Only the command that actually ran is judged — a required
+flag on a sibling subcommand you didn't invoke costs nothing.
+
+## Global flags
+
+A `global` flag declared on a parent is accepted anywhere below it:
+
+```rust
+/// Say yes to everything
+#[usage(long, short = 'y', global)]
+yes: bool,
+```
+
+A global flag may be given **once per command level**, with the innermost occurrence winning —
+`mycli -y install -y` works, matching clap. Giving it twice at the _same_ level is still a
+`DuplicateFlag` error: `mycli -y -y` is refused.
+
+## Container attributes
+
+On the root `#[derive(Cli)]` struct:
+
+| Attribute | Effect |
+| ----------------------------------- | -------------------------------------------------------------- |
+| `bin = "…"` | The binary name (used in help and the spec) |
+| `name = "…"` | A friendly display name |
+| `version` / `version = "…"` | Enable `--version`/`-V`; bare form uses `CARGO_PKG_VERSION` |
+| `about` / `long_about` | Description (doc comments work too) |
+| `usage = "…"` | Verbatim synopsis line(s), replacing the generated one |
+| `before_help` / `after_help` | Extra text around the help page (`*_long_help` variants too) |
+| `unknown_flags = "value"\|"error"` | Treat unknown flags as values instead of errors |
+| `default_subcommand = "run"` | Command to assume when argv names none |
+| `completion` | Generate completion support ([Completions](/rust/completions)) |
+| `settings` | Generate config-settings bindings |
+| `min_usage_version = "…"` | Declare the minimum usage version the spec needs |
+| `group("name", required, multiple)` | Declare a flag group ([Validation](/rust/validation#groups)) |
+
+On a `#[derive(Args)]` struct (refused on the root):
+
+| Attribute | Effect |
+| ----------------------- | ---------------------------------------------------------------------------- |
+| `alias = "…"` | Alternative command name (`alias_hidden` hides it from help) |
+| `mount = "…"` | Mount a subprocess-provided spec for completions ([Spec output](/rust/spec)) |
+| `restart_token = ":::"` | Token that restarts parsing (for wrapper CLIs) |
+| `effect = "…"` | The command's [effect classification](/spec/#command-effects) |
diff --git a/docs/rust/completions.md b/docs/rust/completions.md
new file mode 100644
index 000000000..50c6a6ae9
--- /dev/null
+++ b/docs/rust/completions.md
@@ -0,0 +1,99 @@
+# Completions
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+Completion support is opt-in: add `completion` to the root attribute and enable the
+`completions` cargo feature (forgetting the feature is a compile error that names it):
+
+```toml
+[dependencies]
+usage = { package = "usage-rs", version = "5", features = ["completions"] }
+```
+
+```rust
+#[derive(Cli)]
+#[usage(bin = "ex", completion)]
+struct Ex { /* … */ }
+```
+
+This generates two methods and wires the runtime protocol into `parse()`:
+
+```rust
+// the script a user installs into their shell
+pub fn completion_script(shell: usage::complete::Shell) -> String;
+
+// answer a runtime completion request, if argv is one
+pub fn completion_request(argv: &[OsString]) -> Option;
+```
+
+`Shell` covers `Bash`, `Zsh`, `Fish`, `Nu`, and `PowerShell`.
+
+## How it works
+
+The installed script calls your binary back at completion time with a hidden
+`__complete_word__` request describing the line and cursor. The request is recognized _before_
+any parsing, so it never appears in your grammar, help, or spec. `parse()` intercepts it
+automatically; with `parse_from`, call `completion_request` first and print whatever it returns.
+
+A typical way to expose the scripts:
+
+```rust
+#[derive(Args)]
+struct Completion {
+ /// Which shell to generate for
+ #[usage(long, value_enum)]
+ shell: Shell,
+}
+
+// in your run function:
+print!("{}", Ex::completion_script(cli.completion.shell.into()));
+```
+
+Candidates come from the same tables the parser uses: subcommands and their visible aliases,
+flags in scope at the cursor (globals included, hidden entries excluded), `choices` and
+`ValueEnum` words for a pending value, and negation spellings.
+
+## Completing values
+
+Three ways to say what a value can be:
+
+```rust
+// a fixed set of words
+#[usage(long, choices("json", "table"))]
+format: Option,
+
+// paths — the shell's native file completion takes over
+#[usage(long, value_hint = usage::ValueHint::FilePath)]
+file: Option,
+
+// anything you can compute
+#[usage(arg, name = "TASK", complete = tasks_in_file)]
+task: Option,
+```
+
+`ValueHint` (`FilePath`, `DirPath`, `AnyPath`) answers with the shell's own file/directory
+completion and also emits `complete "file" type="path"` into the KDL, so external consumers of
+the spec give the same answer.
+
+A custom completer is a plain function, referenced by _path_ — a typo is a compile error, not a
+silent dead completer:
+
+```rust
+fn tasks_in_file(
+ partial: &::Partial,
+ _ctx: &usage::complete::CompleteCtx<'_>,
+) -> Vec> {
+ let file = partial.file.as_deref().unwrap_or("tasks.toml");
+ read_tasks(file)
+ .map(|t| usage::complete::Candidate::described(t.name, t.about))
+ .collect()
+}
+```
+
+The first parameter is the _partial parse_ of the completer's own command — flags the user has
+already typed are available, so a `--file` flag can steer what gets completed. Build candidates
+with `Candidate::new(value)` or `Candidate::described(value, description)`; shells that display
+descriptions (zsh, fish) show them, shells that don't get the value alone.
diff --git a/docs/rust/help.md b/docs/rust/help.md
new file mode 100644
index 000000000..1ccfbe5e1
--- /dev/null
+++ b/docs/rust/help.md
@@ -0,0 +1,82 @@
+# Help, Version, and Errors
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+## Help
+
+`-h` and `--help` are supplied by the parser — you never declare them. They aren't written into
+the spec either, so the help page never disagrees with the spec about what exists. If your CLI
+declares its own `--help`, your declaration wins for that spelling.
+
+`-h` renders the short page, `--help` the long page: the first paragraph of each doc comment
+versus the whole comment, `long_help` over `help`, `long_about` over `about`.
+
+With `parse()`, help is handled for you — printed to stdout, exit `0`. With `parse_from`, a help
+request comes back as an _error_, because a parse that stopped to print help has not produced a
+value (clap models it the same way):
+
+```rust
+use usage::{help, Error};
+
+match Ex::parse_from(&argv) {
+ Ok(cli) => run(cli),
+ Err(Error::Help { cmd, long }) => {
+ print!("{}", help::render(Ex::spec(), cmd, long).unwrap());
+ }
+ Err(Error::Version) => {
+ println!("ex {}", env!("CARGO_PKG_VERSION"));
+ }
+ Err(err) => {
+ eprint!("{}", usage::render_failure(Ex::spec(), &argv, &err));
+ std::process::exit(2);
+ }
+}
+```
+
+`Error` is `#[non_exhaustive]` — always keep a fallback arm.
+
+### Customizing the page
+
+- `usage = "…"` on the root replaces the generated synopsis line(s) verbatim.
+- `before_help`, `after_help`, `before_long_help`, `after_long_help` add text around the page —
+ `after_long_help` is the conventional home for an Examples section.
+- `help_heading` on a field groups it under a heading.
+- `hide` removes an entry from help, docs, and completions while still parsing.
+
+The rendered output matches what usage-lib renders from the same spec — the two renderers are
+held to identical output over mise's 211 command pages in CI.
+
+## Version
+
+Declaring `version` (or bare `version`, which reads `CARGO_PKG_VERSION`) gives the root command
+`--version` and `-V`. Neither is listed in help. If your CLI declares its own `--version` or
+`-V`, your spelling wins and the other still answers — where clap panics at startup for the
+same collision.
+
+`parse()` prints `{bin} {version}` and exits `0`.
+
+## Errors
+
+`parse_from` returns `usage::Error`, which distinguishes every failure the grammar can produce:
+`UnknownFlag`, `MissingFlagValue`, `UnexpectedArg`, `MissingRequired`, `DuplicateFlag`,
+`InvalidChoice`, `InvalidValue`, `VarTooFew`/`VarTooMany`, `ConflictingFlags`, `MissingGroup`,
+`MissingSubcommand`, `ArgRequiresDoubleDash`, and more — plus `Help` and `Version` as described
+above.
+
+`render_failure(spec, argv, &err)` turns any of them into the message users see. With the
+`diagnostics` feature enabled the message is clap-shaped:
+
+```
+error: unexpected argument '--wat' found
+
+Usage: ex [OPTIONS]
+
+For more information, try '--help'.
+```
+
+Without `diagnostics`, it falls back to the `Debug` form of the error — fine for internal tools,
+not what you want to ship. `parse()` prints the rendered failure to **stderr** and exits **2**,
+clap's status, so scripts that check for it keep working.
diff --git a/docs/rust/index.md b/docs/rust/index.md
new file mode 100644
index 000000000..636071a61
--- /dev/null
+++ b/docs/rust/index.md
@@ -0,0 +1,156 @@
+# Rust Framework
+
+::: warning Experimental — draft docs
+The Rust framework is experimental. It is complete enough that `usage-cli` itself is built with it,
+but attribute names and APIs may still change between releases. These docs are a draft: some of
+what they document is still in open pull requests, and details may change before release.
+:::
+
+The Rust framework builds your CLI from Rust types. You declare commands, flags, and args as
+structs and enums; a derive macro compiles that declaration into parse tables **and** a usage
+spec. The same declaration that parses argv is the spec that generates your docs, manpages, and
+shell completions.
+
+```rust
+use usage::Cli;
+
+/// A tool that does things
+#[derive(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,
+
+ /// Print more
+ #[usage(short = 'v', long, count)]
+ verbose: u8,
+
+ /// Colorize output
+ #[usage(long, negate = "--no-color", default = "true")]
+ color: bool,
+
+ /// Files to process
+ files: Vec,
+}
+
+fn main() {
+ let cli = Cli::parse();
+ // cli.jobs, cli.verbose, cli.color, cli.files are ready to use
+}
+```
+
+Doc comments are the help text: the first paragraph becomes the short help shown by `-h`, the
+whole comment becomes the long help shown by `--help`.
+
+## Installation
+
+Add `usage-rs` to your `Cargo.toml`, aliased to `usage`:
+
+```toml
+[dependencies]
+usage = { package = "usage-rs", version = "5" }
+```
+
+The alias is supported directly — the derive resolves its runtime through the package name, so
+depending on `usage-rs` under any name works. `usage-rs` is a facade over two crates you can also
+use directly:
+
+| Crate | Role |
+| -------------- | -------------------------------------------------------------------------- |
+| `usage-rs` | The facade an application depends on; re-exports the whole runtime |
+| `usage-derive` | The derive macros: `Cli`, `Args`, `Subcommands`, `ValueEnum` |
+| `usage-argv` | The zero-allocation, zero-dependency runtime the derive emits code against |
+
+### Cargo features
+
+| Feature | Default | What it enables |
+| ------------- | :-----: | ------------------------------------------------------------ |
+| `spec` | ✅ | Spec metadata and `to_kdl()`; gates the derives |
+| `help` | ✅ | `-h` / `--help` page rendering |
+| `completions` | | Shell completion scripts and the runtime completion protocol |
+| `diagnostics` | | clap-shaped error messages from `render_failure` |
+
+Two footguns worth knowing up front:
+
+- Without `diagnostics`, parse failures print as a `Debug`-formatted error rather than the
+ friendly clap-shaped message. Enable it for anything user-facing.
+- `#[usage(completion)]` without the `completions` feature is a deliberate `compile_error!` that
+ tells you which feature to add.
+
+## Parse entry points
+
+`#[derive(Cli)]` generates these on your struct:
+
+```rust
+// parse std::env::args; print help/version/errors and exit as appropriate
+pub fn parse() -> Self;
+
+// parse the given argv; hand errors (including help/version requests) back to you
+pub fn parse_from<'v>(argv: &'v [&'v OsStr]) -> Result>;
+
+// the static parse tables and spec metadata
+pub fn command() -> &'static usage::Command<'static>;
+pub fn spec() -> &'static usage::spec::Spec<'static>;
+
+// the usage spec as KDL
+pub fn to_kdl() -> String;
+```
+
+`parse()` is the whole program shell: it prints the help page to stdout and exits `0` for
+`-h`/`--help`, prints `{bin} {version}` and exits `0` for `--version`, and prints a rendered
+failure to stderr and exits `2` — clap's exit status, so scripts that check for it keep working.
+`parse_from` gives you the same machinery without the process control; see
+[Help, version, and errors](/rust/help) for handling its `Err` variants.
+
+## One declaration, every artifact
+
+Because the derive also emits a usage spec, everything on this site that consumes a spec works
+with your CLI. The pattern `usage-cli` itself ships is a hidden flag that prints the spec:
+
+```rust
+#[usage(long, hide)]
+usage_spec: bool,
+```
+
+```rust
+if cli.usage_spec {
+ println!("{}", Cli::to_kdl().trim());
+ return;
+}
+```
+
+Then generate everything else from it:
+
+```bash
+mycli --usage-spec > mycli.usage.kdl
+usage g markdown -f mycli.usage.kdl --out-dir docs
+usage g manpage -f mycli.usage.kdl > mycli.1
+usage g completion bash mycli --file mycli.usage.kdl
+```
+
+See [Spec output](/rust/spec) for the round-trip guarantees and what the emitted KDL looks like.
+
+## Where to go next
+
+- [Args and flags](/rust/args-and-flags) — field types, attributes, env vars, defaults
+- [Subcommands](/rust/subcommands) — command enums, nesting, `flatten`, value enums
+- [Validation](/rust/validation) — choices, groups, `exclusive`, `delimiter`, conflicts
+- [Help, version, and errors](/rust/help) — what the parser renders and how to hook it
+- [Completions](/rust/completions) — static scripts and runtime completion
+- [Spec output](/rust/spec) — the emitted KDL and usage-cli integration
+
+## Current limitations
+
+The framework intentionally targets standard GNU-style CLIs, and a few clap features have no
+equivalent yet:
+
+- `example` nodes exist in the spec format but cannot be declared from the derive — put an
+ Examples section in `after_long_help` instead (mise does this).
+- `value_optional` affects help output only; the parser still requires a value for the flag.
+- There is no per-field `value_parser`-style validation — values are built with `FromStr`, and a
+ conversion failure becomes an `InvalidValue` error.
+- Prefix matching (`infer_long_args`) is suggested in error messages but never accepted.
+- Non-UTF-8 argv values are reported precisely in errors rather than lossily replaced, but cannot
+ currently be _accepted_ into fields (the crates forbid the `unsafe` needed to reconstruct an
+ `OsString`).
diff --git a/docs/rust/spec.md b/docs/rust/spec.md
new file mode 100644
index 000000000..84b9c770b
--- /dev/null
+++ b/docs/rust/spec.md
@@ -0,0 +1,99 @@
+# Spec Output
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+`Cli::to_kdl()` writes a complete [usage spec](/spec/) from the same static metadata the parser
+runs on. This is the bridge to the rest of the toolkit: markdown docs, manpages, completion
+scripts for other consumers, SDK generation, and linting all consume that KDL.
+
+For the declarations shown across these pages, the emitted spec looks like:
+
+```kdl
+name "ex"
+bin "ex"
+version "1.2.3"
+about "does things"
+flag "-j --jobs" help="how many jobs" global=#true help_heading="Performance" env="EX_JOBS" default="4" {
+ long_help "More about jobs.\nOn two lines."
+ arg ""
+}
+flag "--color" help="colorize output" negate="--no-color" default="true"
+flag "-v --verbose" hide=#true count=#true
+flag "--include" var=#true var_min=1 var_max=5 overrides="--exclude" {
+ arg "..."
+}
+group "input" "--file" "--url" "--stdin" required=#true
+arg "[file]" help="the file" env="EX_FILE" default="a.txt"
+cmd "install" help="install a tool" effect="write" {
+ alias "i"
+ alias "add" hide=#true
+ flag "-f --force"
+ arg ""
+}
+cmd "run" help="run a task" restart_token=":::" {
+ mount run="ex tasks --usage"
+ arg "[args]..." double_dash="preserve"
+}
+```
+
+## Round-trip guarantee
+
+The emitted KDL parses with usage-lib and every property survives the trip — this is enforced by
+the conformance suite. The test every adopter should write is one line:
+
+```rust
+#[test]
+fn spec_is_valid() {
+ let spec: usage::Spec = Cli::to_kdl().parse().unwrap();
+ let _ = spec;
+}
+```
+
+Beyond parsing, `to_kdl` asserts (in debug builds) that the tree is coherent: no duplicate keys,
+no duplicate flag spellings across a `flatten` boundary, no duplicate group names, no unfillable
+argument after an unbounded variadic. Those fire in your test, not on users.
+
+## Feeding usage-cli
+
+The pattern usage-cli itself ships is a hidden flag that prints the spec, so the binary is the
+source of truth:
+
+```rust
+#[usage(long, hide)]
+usage_spec: bool,
+```
+
+```bash
+mycli --usage-spec > mycli.usage.kdl
+
+usage g markdown -f mycli.usage.kdl --out-dir docs # markdown docs
+usage g manpage -f mycli.usage.kdl > mycli.1 # man page
+usage g completion bash mycli --file mycli.usage.kdl # completion script
+usage g json -f mycli.usage.kdl # JSON form
+usage lint -f mycli.usage.kdl # lint the spec
+```
+
+`min_usage_version = "…"` on the root is written first in the document, as the CLI's claim about
+which usage consumers can read it.
+
+## What the parser does with the spec
+
+Nothing, at runtime. The derive compiles your declaration into static tables that usage-argv
+parses and renders help from directly — no KDL is parsed when your CLI runs, and usage-lib is
+not a dependency of your binary. The spec is the _export_ format. The two implementations are
+held to identical behavior by a shared conformance corpus and by rendering all 211 of mise's
+help pages through both.
+
+## What can't be expressed from the derive
+
+A few spec nodes have no derive attribute yet:
+
+- `example` nodes — declare examples in `after_long_help` instead
+- `allow_hyphen_values`
+- `forwards` / external subcommands
+
+If you need these today, maintain a KDL spec alongside the derive or generate docs from a
+post-processed spec.
diff --git a/docs/rust/subcommands.md b/docs/rust/subcommands.md
new file mode 100644
index 000000000..d4a8bdccd
--- /dev/null
+++ b/docs/rust/subcommands.md
@@ -0,0 +1,157 @@
+# Subcommands
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+Subcommands are an enum. Each variant wraps a `#[derive(Args)]` struct (or nothing), and the
+enum derives `Subcommands`:
+
+```rust
+/// A tool that does things
+#[derive(Cli)]
+#[usage(bin = "ex", version = "1.0")]
+struct Ex {
+ /// Say more
+ #[usage(short = 'v', long, global)]
+ verbose: bool,
+
+ /// What to do
+ #[usage(subcommand)]
+ command: Option,
+}
+
+#[derive(Subcommands)]
+enum Commands {
+ /// Install a tool
+ Install(Install),
+ /// Run a task
+ #[usage(name = "run")]
+ RunTask(Run),
+}
+
+#[derive(Args)]
+struct Install {
+ /// Overwrite an existing install
+ #[usage(short = 'f', long)]
+ force: bool,
+ /// What to install
+ tools: Vec,
+}
+```
+
+- `Option` makes the subcommand optional; a bare `Commands` field makes it
+ **required** (`subcommand_required` in the emitted spec).
+- Variant names kebab-case into command names; override with `#[usage(name = "…")]`.
+- A variant may box its struct — `Install(Box)` — with no semantic change.
+- A **unit variant** is a command with nothing of its own; `name`, `alias`, `hide`, and
+ `effect` go directly on the variant.
+- Nesting is unbounded in practice: an `Args` struct can carry its own
+ `#[usage(subcommand)]` field, up to a maximum depth of 16.
+
+Variant attributes: `name`, `alias`, `alias_hidden`, `hide`, `effect`, `help`, `long_help`,
+`verbatim_doc_comment`. Aliases declared on the variant and on the `Args` struct are joined.
+
+Two variants wrapping the _same_ struct is a compile error — each command needs its own
+declaration (two byte-identical structs in different modules are fine).
+
+## Default subcommand
+
+```rust
+#[derive(Cli)]
+#[usage(bin = "ex", default_subcommand = "run")]
+struct Ex { /* … */ }
+```
+
+When argv selects no command, `run` is assumed. Naming a command that doesn't exist fails the
+**build**, not the run.
+
+## Sharing declarations with `flatten`
+
+`#[usage(flatten)]` splices another struct's flags and args into a command, so two commands can
+share a set of declarations:
+
+```rust
+#[derive(Args)]
+struct Listing {
+ /// Do not print a header
+ #[usage(long)]
+ no_header: bool,
+ /// Output format
+ #[usage(long, choices("json", "table"))]
+ format: Option,
+}
+
+#[derive(Args)]
+struct Config {
+ #[usage(long, short = 'f')]
+ file: Option,
+
+ #[usage(flatten)]
+ listing: Listing, // config gets --no-header and --format too
+}
+```
+
+The tables are joined at compile time and the emitted KDL lists the flags inline — a consumer of
+the spec can't tell a flattened flag from a declared one. Groups and `exclusive` flags declared
+on the flattened struct are enforced (and emitted) on the command that flattens them.
+
+A flattened struct may not declare subcommands; that's a compile-time error with an explanation.
+
+## Value enums
+
+For a flag or arg whose values are a fixed set of words, derive `ValueEnum` instead of listing
+`choices` by hand:
+
+```rust
+#[derive(usage::ValueEnum)]
+enum Shell {
+ Bash,
+ Zsh,
+ #[usage(name = "pwsh")]
+ PowerShell,
+}
+
+#[derive(Args)]
+struct Completion {
+ /// Which shell to generate for
+ #[usage(long, value_enum)]
+ shell: Option,
+}
+```
+
+Variant names kebab-case into the accepted words. The derive also implements `FromStr`, whose
+error lists the valid words. One limitation: a single variant cannot be `cfg`-ed out (the word
+list is a `const`) — put the `cfg` on the whole enum.
+
+## Mounts and restart tokens
+
+Two spec features for wrapper-style CLIs are declared on the `Args` struct:
+
+```rust
+#[derive(Args)]
+#[usage(mount = "ex tasks --usage", restart_token = ":::")]
+struct Run {
+ /// Arguments passed through to the task
+ #[usage(double_dash = "preserve")]
+ args: Vec,
+}
+```
+
+`mount` names a command that prints a spec for dynamically-defined subcommands (like mise
+tasks); it is only consulted during completion — the cold path where running a subprocess is
+affordable. `restart_token` lets one invocation contain several command lines
+(`ex run build ::: test`). See the [spec reference](/spec/reference/cmd) for semantics.
+
+## Command effects
+
+A variant or `Args` struct can declare what running the command does to the world:
+
+```rust
+#[derive(Args)]
+#[usage(effect = "destructive")]
+struct Uninstall { /* … */ }
+```
+
+See [command effects](/spec/#command-effects) for what consumers do with this.
diff --git a/docs/rust/validation.md b/docs/rust/validation.md
new file mode 100644
index 000000000..5baf564ab
--- /dev/null
+++ b/docs/rust/validation.md
@@ -0,0 +1,140 @@
+# Validation
+
+::: warning Draft
+This page is a draft. Some of what it documents is still in open pull requests, and details may
+change before release.
+:::
+
+Everything on this page runs after argv is bound and env/default fallbacks are applied. Only the
+command that actually ran is judged. Contradictory declarations — `choices` on a `bool`,
+`var_min` greater than `var_max`, a default that isn't one of the choices — are compile errors,
+not runtime surprises.
+
+## Choices and bounds
+
+```rust
+/// Output format
+#[usage(long, choices("json", "table"))]
+format: Option,
+
+/// Patterns to include
+#[usage(long, var_min = 1, var_max = 5)]
+include: Vec,
+```
+
+A value outside the set is `Error::InvalidChoice { name, choices }`; too few or too many values
+are `VarTooFew`/`VarTooMany`. For enum-shaped values prefer
+[`ValueEnum`](/rust/subcommands#value-enums).
+
+## Flag relations
+
+`conflicts`, `requires`, `overrides`, `required_if`, and `required_unless` relate one flag to
+another. Targets are named the way the KDL spec names them (`"--long"` or `"-s"`), and naming a
+flag that doesn't exist is a compile error:
+
+```rust
+/// Read from standard input
+#[usage(long, conflicts("--file", "--url"))]
+stdin: bool,
+
+/// Retry count
+#[usage(long, required_if("--retry"))]
+max_retries: Option,
+```
+
+`overrides` is the quiet sibling of `conflicts`: a later occurrence of one flag discards an
+earlier occurrence of the other instead of erroring — useful for `--json` / `--yaml` pairs where
+the last one typed should win.
+
+## Groups
+
+A group relates several flags at once: membership is declared on each field, the group's
+properties on the struct.
+
+```rust
+#[derive(Cli)]
+#[usage(bin = "grp")]
+#[usage(group("input", required))]
+struct Grp {
+ /// Read from a file
+ #[usage(long, group = "input")]
+ file: Option,
+ /// Read from a URL
+ #[usage(long, group = "input")]
+ url: Option,
+ /// Read from standard input
+ #[usage(short = 's', long, group = "input")]
+ stdin: bool,
+}
+```
+
+The two properties compose the way clap's do:
+
+| Declaration | Meaning |
+| ----------------------------------- | ------------ |
+| `group("name")` | at most one |
+| `group("name", required)` | exactly one |
+| `group("name", required, multiple)` | at least one |
+
+An unsatisfied required group is `Error::MissingGroup { group, members }`, rendered as clap
+renders it:
+
+```
+error: one of the following required arguments was not provided (input):
+ --file
+ --url
+ -s, --stdin
+```
+
+Two members of a non-`multiple` group produce `ConflictingFlags` — matched by flag, not
+spelling, so giving one member as `-s` and another as `--file` still counts. A conflict is
+reported before an unsatisfied group.
+
+Groups are emitted into the KDL spec
+(`group "input" "--file" "--url" "--stdin" required=#true`), and a group declared on a
+[flattened](/rust/subcommands#sharing-declarations-with-flatten) struct is enforced on every
+command that flattens it. Malformed groups — one member, no members, declared twice, a group on
+a positional — are compile errors.
+
+## Exclusive flags
+
+An `exclusive` flag has to be given alone — no other flag, no argument, no subcommand:
+
+```rust
+/// Dump the spec and leave
+#[usage(long, exclusive)]
+dump: bool,
+```
+
+This is stronger than `conflicts` with every other flag, because `conflicts` has nowhere to name
+an _argument_. The details:
+
+- `--dump -v` and `--dump somefile` both fail with `ConflictingFlags`.
+- An exclusive flag **bypasses required-ness**: required siblings the flag's command declares are
+ not demanded when the exclusive flag is given — the `--version`-style escape hatch.
+- Declared defaults still apply; only values the user actually _supplied_ count as company —
+ but a value supplied via `env` does count.
+- Exclusivity crosses command boundaries in both directions: selecting a subcommand is company
+ for a parent's exclusive flag, and a parent's flags are company for a child's.
+
+`exclusive` on a positional is a compile error. Emitted KDL: `flag "--dump" exclusive=#true`.
+
+## Delimiters
+
+`delimiter` splits one word into several values, the way clap's `value_delimiter` does:
+
+```rust
+/// Tags to apply
+#[usage(long, delimiter = ',', var_max = 3)]
+tags: Vec,
+```
+
+`--tags a,b,c` yields `["a", "b", "c"]`, and occurrences accumulate: `--tags a,b --tags c` is
+`["a", "b", "c"]`. It works on positionals too (`#[usage(arg, delimiter = ';')]`).
+
+The split runs after `env` fallback and **before every check**, so `choices` judges each split
+value and `var_min`/`var_max` count values, not words — `--tags a,b,c,d` with `var_max = 3` is
+`VarTooMany { got: 4 }`.
+
+The field must be a `Vec`, and the delimiter must be a single ASCII character; both are enforced
+at compile time. Emitted KDL: `flag "--tags " var=#true delimiter=","`.