Skip to main content

headwater_cli/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2//! The command line of `headwater`, declared once and derived.
3//!
4//! # What this replaced, and the contract that went with it
5//!
6//! Until [HW-DR-0033](../../../../docs/decisions/0033-q33-whether-the-command-line-is-derived-and-who-a-flag-belongs-to.md)
7//! the parse was a loop over `std::env::args()` in `main`, with thirty-three
8//! arms that read a flag and twenty that read a verb. Every flag of the binary
9//! was admitted before the verb was decided, so a flag belonging to another
10//! verb was accepted and did nothing: `headwater check --level L0` exited 0 and
11//! wrote the report that `headwater check` writes. Two interface contracts and
12//! [spec 12](../../../../docs/spec/12-check-layer.md) stated that as a promise.
13//! The decision record withdraws it. A flag belongs to the verb that reads it,
14//! and a verb refuses a flag it does not.
15//!
16//! # Why the types are a library and not a module of the binary
17//!
18//! An integration test cannot reach an item of a `[[bin]]` target, which is why
19//! `tests/verbs.rs` used to read `main.rs` as **source text** and scrape the
20//! `["word", …]` patterns out of it. A scrape is a parser of Rust that nothing
21//! holds, and it would go blind the moment the arms stopped being written by
22//! hand. With the surface here, that test holds [`command`] against
23//! [`headwater_verbs::VERBS`] in both directions, over the tree `clap` itself
24//! builds.
25//!
26//! # Where the words come from, and what is deliberately absent
27//!
28//! No `about` on a verb and no summary is written in this file. [`command`]
29//! reads both off [`headwater_verbs::VERBS`] and puts them on the tree, because
30//! a summary written here would be the fifth hand-kept copy of the verb list
31//! that [#257](https://github.com/headwater-ai/headwater/issues/257) was filed
32//! about. The correspondence is by command line rather than by a name repeated
33//! at each variant: [`command`] walks the table and calls `mut_subcommand`, so
34//! a verb renamed in one place and not the other is a verb the walk in
35//! `tests/verbs.rs` reports.
36//!
37//! **A flag is the other way round, and for the reason `HW-DR-0033` gives.** A
38//! flag belongs to the verb that reads it, so its description is written at the
39//! declaration of that flag, here. `engine/crates/cli/tests/help.rs` holds every
40//! argument of every command in the tree to carrying one, which is what stops
41//! the next flag arriving undescribed the way `--facet`, `--tier`, `--arm`,
42//! `--category` and `--seed` did.
43//!
44//! **A global flag is described once and printed twice.** [`GLOBALS`] carries a
45//! one-line summary beside each description, and [`first_screen`] prints the
46//! summaries rather than letting `clap` print the descriptions. The description
47//! is the one `clap` propagates onto every verb page, so the long form is
48//! reachable everywhere it was, and the screen a reader meets first is a list
49//! rather than five paragraphs.
50//! [HW-DR-0042](../../../../docs/decisions/0042-q42-what-one-screen-means-for-the-first-help-screen.md)
51//! rules that, and it rules out `Arg::long_help` as the way to do it: `clap`
52//! renders `long_help` for `--help` and `help` for `-h`, so the two spellings
53//! would stop printing the same text.
54//!
55//! **A `///` comment on a derived item becomes help text.** The commentary on
56//! the types below is `//` for that reason, and the module documentation you
57//! are reading is `//!`, which `clap` does not read either. A house-style doc
58//! comment on a variant or a field would be printed to a caller.
59//!
60//! Color is declared off. Nothing here emits an escape sequence, which is the
61//! state the binary was already in and the state its recorded fixtures read.
62//! `--no-color` is declared anyway, and it is declared as what it is: a caller
63//! who writes it out of habit is answered rather than refused, and its help
64//! says outright that this binary has no color to turn off. That is the
65//! opposite of a flag whose name implies an effect it does not have.
66//!
67//! **The width is decided in [`paint`] and never by the terminal.** Every string
68//! below is folded before `clap` sees it, because `clap` cannot fold at all in
69//! this workspace and the feature that would let it reads the terminal. So the
70//! strings here are written as one long line each and reach a caller folded.
71
72pub mod paint;
73pub mod taxonomy_graph;
74
75/// What every output-target help says about a run that refuses.
76///
77/// One sentence with six readers — the two `--json` descriptions below and the
78/// four `--format` ones — for the reason [`JSON_BESIDE_FORMAT`] gives: six
79/// literals agree until somebody edits one of them.
80/// [HW-DR-0043](../../../../docs/decisions/0043-q43-whether-a-refusal-under-json-is-a-json-document.md) rules
81/// that `--json` names the shape of an artifact and moves neither the stream a
82/// refusal is written on nor the grammar it is written in. So a consumer reads
83/// nothing on standard output when a run refuses, and reads the account on the
84/// other stream.
85///
86/// **It says "refuses" and not "exits non-zero", because those are different
87/// sets.** Five of the twelve reasons `check` exits 1 are decided after the
88/// report is already on standard output, which
89/// `docs/interfaces/headwater-check.md` states under *Exit status*. A refusal
90/// is decided before anything is written.
91///
92/// A macro and not a `const`, because the six readers reach it through
93/// [`concat!`], which takes a literal and never a name.
94macro_rules! a_refusal_is_not_an_artifact {
95    () => {
96        "A run that refuses writes nothing here: the account is one English sentence on standard \
97         error and the status is 1"
98    };
99}
100
101/// What `--json` says on a verb that also declares `--format`.
102///
103/// One constant with four readers rather than four literals that agree until
104/// somebody edits one of them. It is not the copy of the verb list that
105/// [#257](https://github.com/headwater-ai/headwater/issues/257) rules against:
106/// it is one sentence about one flag, and the flag means the same thing at
107/// every declaration of it because [`Verb`]'s dispatch maps all four onto the
108/// one value `--format json` already named.
109///
110/// **Stating both is refused and never resolved.** `conflicts_with` is what
111/// refuses it, so the refusal is `clap`'s message under this binary's exit 1.
112/// The alternative was a precedence rule, and a precedence rule is how a caller
113/// states a value and the engine substitutes its own — which is the defect
114/// [#337](https://github.com/headwater-ai/headwater/issues/337) and
115/// [#338](https://github.com/headwater-ai/headwater/issues/338) are open about.
116const JSON_BESIDE_FORMAT: &str = concat!(
117    "write this run as one JSON document on standard output. It is the artifact `--format json` \
118     writes, byte for byte. A run that states both is refused rather than resolved, because two \
119     names for one target is a question answered twice. ",
120    a_refusal_is_not_an_artifact!()
121);
122
123/// What `--json` says on a verb that declares no `--format`.
124///
125/// These four have two renderings and not four, so the flag is a boolean rather
126/// than a second `--format` whose closed set would hold two values. `--format`
127/// stays where it is on the four verbs that have it, because #321 asks that
128/// `--json` be accepted where `--format json` already is and never that it
129/// replace anything.
130const JSON_ALONE: &str = concat!(
131    "write this run as one JSON document on standard output, instead of the report a person \
132     reads. The document names its own shape in a `version` member, so a consumer pins that \
133     rather than the version of this engine. It moves no exit status. ",
134    a_refusal_is_not_an_artifact!()
135);
136
137/// What `--root` says, on the first screen and on every verb page.
138///
139/// One sentence, so the summary and the description are the same string. The
140/// four flags below it are the ones whose description is a paragraph.
141const ROOT_TEXT: &str = "the repository to read. Defaults to the working directory";
142
143/// What `-V, --version` says on a verb page.
144const VERSION_TEXT: &str = "the version of this engine. It is the number a package's \
145    `requires_engine` range is read against, and it is the number to quote in a bug report. One \
146    line on standard output, and no repository is needed to ask";
147
148/// What `--wide` says on a verb page.
149const WIDE_TEXT: &str = "lay the help, and the report of `headwater check`, out at the width \
150    `COLUMNS` states, held to the range 80 to 120. A reading that is absent or is not a number \
151    gives 80, which is what a run with no flag gives. Without it nothing reads `COLUMNS`, so a run \
152    piped into a file and a run under a terminal write the same bytes. A shell keeps `COLUMNS` to \
153    itself, so the form that carries it is `COLUMNS=100 headwater --wide --help`. A run that lays \
154    nothing out, a machine format included, refuses it rather than accepting a flag that does \
155    nothing";
156
157/// What `--no-color` says on a verb page.
158///
159/// [HW-DR-0045](../../../../docs/decisions/0045-coloring-the-cli-and-where-the-banner-goes.md)
160/// rewrites this rather than patches it: the sentence it stated before this
161/// ruling is the opposite of the behavior below.
162const NO_COLOR_TEXT: &str = "force plain text on both streams: bold and dim weight and glyphs, no \
163    escape sequence. Without it, this binary senses whether each stream is a terminal and renders \
164    color there, plain text otherwise. `NO_COLOR`, set to any value, has the same effect. It is \
165    declared so that a caller who writes it out of habit is answered rather than refused";
166
167/// What `--no-banner` says on a verb page.
168///
169/// [HW-DR-0045](../../../../docs/decisions/0045-coloring-the-cli-and-where-the-banner-goes.md)
170/// scopes the masthead to the root screen alone, so this flag is accepted and
171/// honestly described as inert everywhere else, the posture `--no-color`
172/// already set for a flag that changes nothing on the verb page carrying it.
173const NO_BANNER_TEXT: &str = "suppress the masthead: the line naming this binary and its version, \
174    and the rule beneath it, that the root help screen alone prints above `Usage:`. \
175    `HEADWATER_NO_BANNER`, set to any value, has the same effect. It is accepted, and inert, on \
176    every verb's own page, the same posture `--no-color` already takes for a flag that changes \
177    nothing there";
178
179/// One global flag, as the first screen prints it and as a verb page prints it.
180///
181/// The summary and the description are declared together, at the flag, which is
182/// what #321 clause 5 asks of a short form: a summary written a second time
183/// somewhere else is the second copy of a description that
184/// [#257](https://github.com/headwater-ai/headwater/issues/257) was filed
185/// about. The table is here rather than in `headwater-verbs`, where
186/// [`headwater_verbs::Verb`] declares the same pair for a verb, because
187/// `HW-DR-0033` rules that a flag belongs to the verb that reads it and that its
188/// description is written at the declaration of that flag. A global flag is
189/// declared in this file, so its summary is too.
190///
191/// [HW-DR-0042](../../../../docs/decisions/0042-q42-what-one-screen-means-for-the-first-help-screen.md)
192/// holds `summary` to one line of the first screen, and
193/// `engine/crates/cli/tests/help.rs` is what holds it.
194#[derive(Debug)]
195pub struct Global {
196    /// The identifier `clap` knows the argument by.
197    ///
198    /// The entry is bound to the flag by this rather than by a name that
199    /// resembles it, so a flag that arrives with no entry is reported against
200    /// the identifier a reader of the parser will recognize.
201    pub id: &'static str,
202    /// The flag as the first screen names it: every spelling, and any value.
203    pub name: &'static str,
204    /// One line of the first screen, for a reader who is choosing a verb.
205    pub summary: &'static str,
206    /// The whole of it, which is what `clap` carries onto every verb page.
207    pub description: &'static str,
208}
209
210impl Global {
211    /// Whether this entry is the entry of the argument `clap` calls `id`.
212    #[must_use]
213    pub fn covers(&self, id: &str) -> bool {
214        self.id == id
215    }
216}
217
218const ROOT: Global = Global {
219    id: "root",
220    name: "--root <path>",
221    summary: ROOT_TEXT,
222    description: ROOT_TEXT,
223};
224
225const VERSION: Global = Global {
226    id: "version",
227    name: "-V, --version",
228    summary: "the version of this engine, on one line, from anywhere",
229    description: VERSION_TEXT,
230};
231
232const WIDE: Global = Global {
233    id: "wide",
234    name: "--wide",
235    summary: "lay the help and the check report out at `COLUMNS`, 80 to 120",
236    description: WIDE_TEXT,
237};
238
239const NO_COLOR: Global = Global {
240    // `clap`'s derive takes the identifier from the field and not from the
241    // spelling, so this is `no_color` where the flag is `--no-color`.
242    id: "no_color",
243    name: "--no-color",
244    summary: "force plain text, no matter what either stream senses",
245    description: NO_COLOR_TEXT,
246};
247
248const NO_BANNER: Global = Global {
249    id: "no_banner",
250    name: "--no-banner",
251    summary: "suppress the masthead this binary prints on the root screen",
252    description: NO_BANNER_TEXT,
253};
254
255/// `-h, --help` is `clap`'s own argument, so this entry supplies the first
256/// screen's line for it and states what `clap` puts on a verb page.
257///
258/// The description is the only one of the five this repository did not write.
259/// `Command::mut_arg` panics before the build adds the argument, and
260/// `Command::mut_args` documents that it does not reach the built-in help
261/// argument at all.
262/// [HW-OBL-0158](../../../../docs/obligations/0158-clap-owns-the-help-flag-so-h-help-reads-print-help-on-all-32-verb-pages.md)
263/// holds the gap and names the route that would close it.
264const HELP: Global = Global {
265    id: "help",
266    name: "-h, --help",
267    summary: "this screen, or the long form of one verb",
268    description: "Print help",
269};
270
271/// The order the first screen prints the global flags in.
272///
273/// Named constants rather than positions, so that a reordering here cannot
274/// silently give one flag another flag's summary.
275pub const GLOBALS: &[&Global] = &[&ROOT, &VERSION, &WIDE, &NO_COLOR, &NO_BANNER, &HELP];
276
277use clap::{Command, CommandFactory, FromArgMatches, Parser, Subcommand};
278use headwater_check::Date;
279use std::path::PathBuf;
280
281// Every command line this binary answers to.
282//
283// The subcommand is optional because `headwater` with no verb has a message of
284// its own, and because `--version` answers from outside a corpus with no verb
285// in front of it.
286#[derive(Parser, Debug)]
287#[command(
288    name = headwater_verbs::BINARY,
289    bin_name = headwater_verbs::BINARY,
290    color = clap::ColorChoice::Never,
291    disable_help_subcommand = true,
292    disable_version_flag = true
293)]
294pub struct Cli {
295    #[arg(
296        long,
297        global = true,
298        value_name = "path",
299        help = ROOT_TEXT
300    )]
301    pub root: Option<PathBuf>,
302
303    // `-V` and `--version`, held here rather than by `clap`.
304    //
305    // `clap` prints `{name} {version}`, and this binary prints the version
306    // alone: the value is `headwater_resolve::release::ENGINE`, which is what
307    // a `requires_engine` range is read against, so a caller pastes one line
308    // into a bug report and a reader compares it to a range.
309    #[arg(
310        short = 'V',
311        long,
312        global = true,
313        help = VERSION_TEXT
314    )]
315    pub version: bool,
316
317    // `--wide`, which is the one reader of `COLUMNS` in this binary.
318    //
319    // It is declared here so that a caller meets it in the help and so that a
320    // verb refuses it in the one place it means nothing. `paint::width` reads
321    // the raw arguments for it rather than this field, because the answer is
322    // needed to build the tree that produces this field.
323    #[arg(
324        long,
325        global = true,
326        help = WIDE_TEXT
327    )]
328    pub wide: bool,
329
330    // `--no-color`, which is a flag this binary has nothing to turn off with.
331    //
332    // Declaring a flag that changes no byte is the defect
333    // [#337](https://github.com/headwater-ai/headwater/issues/337) and
334    // [#338](https://github.com/headwater-ai/headwater/issues/338) are filed
335    // about, and this is the case those two are not: there the name implies a
336    // narrowing the code does not perform and the caller is told nothing, and
337    // here the help states the whole truth in its first sentence. What it buys
338    // is that `headwater check --no-color` runs, where it exited 1 before, and
339    // a caller who writes the near-universal spelling meets an answer rather
340    // than a refusal about a flag every other tool carries.
341    #[arg(
342        long = "no-color",
343        global = true,
344        help = NO_COLOR_TEXT
345    )]
346    pub no_color: bool,
347
348    // `--no-banner`, accepted (and inert) on every verb page, the same
349    // posture `--no-color` above already takes for a flag that changes
350    // nothing on the page carrying it. `paint::banner_suppressed` reads the
351    // raw command line for it rather than this field, for the reason
352    // `paint::width` reads the raw arguments for `--wide`: the answer is
353    // needed to build the tree that produces this field.
354    #[arg(
355        long = "no-banner",
356        global = true,
357        help = NO_BANNER_TEXT
358    )]
359    pub no_banner: bool,
360
361    #[command(subcommand)]
362    pub verb: Option<Verb>,
363}
364
365// The first word.
366//
367// The order is [`headwater_verbs::VERBS`]' order, which is the order the first
368// screen prints and the order the generated verb index carries.
369#[derive(Subcommand, Debug)]
370pub enum Verb {
371    Check {
372        #[arg(
373            long,
374            help = "exit non-zero when a finding is an error. Without it the run is advisory and \
375                    always exits 0, which is the default spec 6 fixes"
376        )]
377        strict: bool,
378        #[arg(
379            long,
380            help = "write the patch that rides with a finding, in this working tree. A finding \
381                    carries one only when the fix is mechanical and total, and a finding an author \
382                    suppressed carries none. Every patch is held against the bytes it names and \
383                    the result is read back before it lands, so a file whose shape this engine \
384                    guessed wrong is refused with nothing written. The report that follows is the \
385                    run after the write, and the account of what was written goes to standard \
386                    error. It exits non-zero on a refusal"
387        )]
388        fix: bool,
389        #[arg(
390            long = "no-cache",
391            help = "read and write no cache, and evaluate every instance. This run and a cached \
392                    one write the same bytes to standard output, and a difference between them is \
393                    a defect in the cache rather than a result"
394        )]
395        no_cache: bool,
396        #[arg(
397            long,
398            value_name = "date",
399            value_parser = a_date,
400            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today. Spec 12 \
401                    makes the clock an injected value rather than a syscall inside a check, and \
402                    this flag is where it is injected: same corpus, same lock, same date, same \
403                    bytes"
404        )]
405        now: Option<Date>,
406        #[arg(
407            long,
408            value_name = "manifest",
409            help = "the manifest of a change, which the rules that read a transition use. It \
410                    does not narrow the documents that the run checks. The first line is \
411                    `headwater change 1`, and a file that opens with anything else is refused \
412                    rather than read. Each line after it names one document \
413                    the change carries, as `added<tab><path>` or `prior<tab><path><tab><file>`, \
414                    and the second form names a file holding the bytes that stood before the \
415                    change. A document the manifest does not name did not change. It is what a \
416                    rule that reads a transition needs, and without it every instance of such a \
417                    rule is reported as skipped rather than passed. This engine walks no history: \
418                    the caller anchors the prior version to the state on the branch where the \
419                    change lands, which spec 12 fixes as the merge base of a proposed change and \
420                    the committed `HEAD` of a working-tree hook. Every path is held against the \
421                    corpus this run walks, and one that reaches no row of it is counted and named \
422                    in the report rather than absorbed. No path is normalized, so `./docs/a.md` \
423                    reaches no row. What this engine cannot check is whether the manifest tells \
424                    the truth: a line that says `added` for a document that already stood, and a \
425                    document the change carried and the manifest omits, are both invisible without \
426                    the history that spec 12 rules out as an input"
427        )]
428        change: Option<PathBuf>,
429        #[arg(
430            long = "read-set",
431            value_name = "path",
432            help = "write the read set of this run to a file as well as to the report. The \
433                    artifact is what decides whether a verdict survives a merge without running \
434                    the checks again, and `headwater gate` is what reads it"
435        )]
436        read_set: Option<PathBuf>,
437        #[arg(
438            long,
439            value_name = "path",
440            help = "write the register of this run to a file as well as to the report. Spec 4 \
441                    makes it a projection of the `obligations` and `controls` declarations, \
442                    generated and never authored: every obligation with its disposition, every \
443                    control with its health, and what escaped under each"
444        )]
445        register: Option<PathBuf>,
446        #[arg(
447            long,
448            value_name = "text|json|sarif|markdown",
449            help = concat!(
450                "which vocabulary to write the run in. `text` is the report a person reads and \
451                 the default. `sarif` is what a forge ingests as a check run, `markdown` is a \
452                 job summary or a review comment, and `json` is the finding shape spec 4 \
453                 declares, for an adapter nobody here wrote. `sarif` writes its own loss set \
454                 into the artifact. `markdown` declares one in the source and not in the \
455                 artifact, because nothing it writes is machine-readable. `text` declares one \
456                 drop there too, the routing of each skip, and carries the census and the graph \
457                 that no other format holds. `json` declares one there as well, the \
458                 per-document account of coverage, and writes no loss set of its own. ",
459                a_refusal_is_not_an_artifact!()
460            )
461        )]
462        format: Option<String>,
463        #[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
464        json: bool,
465    },
466    Change {
467        // Optional here and required by the verb, on the terms `gate` already
468        // states for `--read-set`: the refusal names what a base revision is
469        // and points at the two anchors spec 12 fixes, which a
470        // missing-argument message from the parser cannot.
471        #[arg(
472            value_name = "base-rev",
473            help = "the revision to compare the working tree against. Spec 12 fixes two: the \
474                    committed `HEAD` for a working-tree hook, and the merge base of a proposed \
475                    change for a CI job. This verb runs no history walk beyond `git diff` and \
476                    `git show` against this one revision"
477        )]
478        base: Option<String>,
479        #[arg(
480            value_name = "out-dir",
481            help = "the directory to write the manifest and the prior versions into. The caller \
482                    owns this directory and removes it; this verb only ever creates inside it. \
483                    The manifest's own path, `<out-dir>/manifest`, is printed on standard output, \
484                    which is the file `headwater check --change` takes"
485        )]
486        out: Option<PathBuf>,
487    },
488    Gate {
489        // Optional here and required by the verb, so that the refusal a caller
490        // reads is the one the verb wrote: it names what a read set is and how
491        // to produce one, which a missing-argument message cannot.
492        #[arg(
493            long = "read-set",
494            value_name = "path",
495            help = "the read set to hold against this tree, and it is required here. \
496                    `headwater check --read-set <path>` is what writes one. The artifact is what \
497                    decides whether a verdict survives a merge without running the checks again"
498        )]
499        read_set: Option<PathBuf>,
500        #[arg(
501            long,
502            value_name = "date",
503            value_parser = a_date,
504            help = "the day the question is asked about, as `YYYY-MM-DD`. Defaults to today. A run \
505                    that read the clock is void on any other day"
506        )]
507        now: Option<Date>,
508        #[arg(long, help = JSON_ALONE)]
509        json: bool,
510    },
511    Conformance {
512        #[arg(
513            long,
514            value_name = "name",
515            help = "the rung to ask about, by the name the package declares. It exits non-zero on \
516                    a gap under that rung that no live waiver covers. It never moves the level the \
517                    report states, which is computed from met rules alone"
518        )]
519        level: Option<String>,
520        #[arg(
521            long,
522            value_name = "date",
523            value_parser = a_date,
524            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
525        )]
526        now: Option<Date>,
527        #[arg(long, help = JSON_ALONE)]
528        json: bool,
529    },
530    // No flag at all. The verb computes one answer about the tree in front of
531    // it, and every input it has is the tree.
532    Derived {},
533    // One operand, the directory a site generator wrote. An `Option`, so that
534    // a missing one reaches the verb's own sentence rather than `clap`'s.
535    Site {
536        #[arg(
537            value_name = "site-dir",
538            help = "the directory a site generator wrote, such as MkDocs' `site/`"
539        )]
540        dir: Option<PathBuf>,
541    },
542    // The four operands git hands a merge driver, in git's order. Each is an
543    // `Option` so that a missing one reaches the verb's own sentence rather
544    // than `clap`'s, which is the posture every positional of this parse takes.
545    MergeDriver {
546        #[arg(
547            value_name = "ancestor",
548            help = "the file git wrote the common ancestor's version into, `%O`. Not read"
549        )]
550        ancestor: Option<String>,
551        #[arg(
552            value_name = "current",
553            help = "the file holding the current side's version, `%A`. Left byte for byte, because git reads the merge result from it"
554        )]
555        current: Option<String>,
556        #[arg(
557            value_name = "other",
558            help = "the file holding the other side's version, `%B`. Not read"
559        )]
560        other: Option<String>,
561        #[arg(
562            value_name = "path",
563            help = "the path of the artifact in the tree, `%P`. It decides which producer the message names"
564        )]
565        path: Option<String>,
566    },
567    Route {
568        #[arg(
569            value_name = "task description",
570            help = "what you are about to do, in your own words. Every word after the verb is one \
571                    description, so it needs no quoting to hold together"
572        )]
573        task: Vec<String>,
574        #[arg(
575            long,
576            value_name = "n",
577            value_parser = a_budget,
578            help = "how many ranked pointers it may offer. It never removes a document that \
579                    governs a path the task named, and it says how many it withheld. Five by \
580                    default"
581        )]
582        budget: Option<usize>,
583        #[arg(long, help = JSON_ALONE)]
584        json: bool,
585    },
586    Neighbors {
587        #[arg(
588            value_name = "task description",
589            help = "what you are about to do, in your own words. Every word after the verb is one \
590                    description, so it needs no quoting to hold together"
591        )]
592        task: Vec<String>,
593        #[arg(
594            long,
595            value_name = "dir",
596            help = "the directory holding the fetched model files the pin in \
597                    `.headwater/embedding.yml` names. `.headwater/models` by default"
598        )]
599        model: Option<PathBuf>,
600        #[arg(
601            long,
602            value_name = "n",
603            value_parser = a_budget,
604            help = "how many documents it prints, nearest first. Ten by default"
605        )]
606        top: Option<usize>,
607        #[arg(long, help = JSON_ALONE)]
608        json: bool,
609    },
610    Explain {
611        #[arg(
612            value_name = "path|identifier",
613            help = "the document to explain, as a path under the corpus root or as the identifier \
614                    it declares"
615        )]
616        target: Option<String>,
617        #[arg(long, help = JSON_ALONE)]
618        json: bool,
619    },
620    Show {
621        #[arg(
622            value_name = "path|identifier",
623            help = "the document to print, as a path under the corpus root or as the identifier \
624                    it declares"
625        )]
626        target: Option<String>,
627    },
628    Query {
629        #[arg(
630            value_name = "expression",
631            help = "the expression to run, and no document of this repository states what one is"
632        )]
633        expression: Vec<String>,
634    },
635    Capture {
636        #[arg(
637            long,
638            value_name = "text|json",
639            help = concat!(
640                "`text` is the report a person reads and the default, and `json` is the same \
641                 numbers for a program. Neither carries a reading the store does not hold. ",
642                a_refusal_is_not_an_artifact!()
643            )
644        )]
645        format: Option<String>,
646        #[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
647        json: bool,
648    },
649    Mcp {
650        #[arg(
651            long,
652            value_name = "date",
653            value_parser = a_date,
654            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today. It is read \
655                    once and fixed for the life of the server, and every result states it"
656        )]
657        now: Option<Date>,
658        #[arg(
659            long,
660            help = "register the working-tree write class, which is `new` and `fix`. Spec 5 keeps \
661                    it off by default, because a client may connect to a checkout that the user \
662                    did not intend to change, so the consent is a word somebody typed rather than \
663                    a setting a tree carries. A tool that lands a change is registered by no \
664                    switch. The first call that moves a byte ends the server: it walked the corpus \
665                    once, so every later answer would be about a tree that is gone"
666        )]
667        write: bool,
668    },
669    New {
670        #[arg(
671            value_name = "kind",
672            help = "the kind of document to scaffold, by the name the resolved taxonomy declares \
673                    for it"
674        )]
675        kind: Option<String>,
676        #[arg(
677            long,
678            value_name = "text",
679            help = "what the document is called. Required, because the file name and the facet in \
680                    the `name` role both come from it"
681        )]
682        title: Option<String>,
683        #[arg(
684            long,
685            value_name = "text",
686            help = "the one sentence a reader meets where a list of documents is rendered. \
687                    Fills the facet in the `scent` role directly, exactly as `--title` fills the \
688                    one in the `name` role. Without it the field carries a prompt, and a person \
689                    edits the front matter by hand before the document is current"
690        )]
691        summary: Option<String>,
692        #[arg(
693            long,
694            value_name = "relation=identifier",
695            value_parser = a_pair,
696            help = "an edge to propose, as a relation and the identifier of the document at the \
697                    other end. Repeatable. It is refused unless the taxonomy declares \
698                    `created_by: scaffold` on the relation, unless both ends are kinds the \
699                    relation permits, and unless the target resolves. Where reciprocity is \
700                    required and the new document opens at an initial state, the far half is \
701                    owed until the document leaves that state, and `headwater check --fix` \
702                    writes it then. In every other case the far half is written into the \
703                    target document at once"
704        )]
705        relates: Vec<(String, String)>,
706        #[arg(
707            long,
708            value_name = "facet=value",
709            value_parser = a_pair,
710            help = "a value for a facet this kind requires, as `<facet>=<value>`. Repeatable. A \
711                    facet the kind does not require is refused, and so is a value outside a \
712                    closed set, with the set printed. A facet that a declaration decides is \
713                    refused too: an engine role decides its facet's value, and the kind decides \
714                    the discriminator of a heterogeneous shelf"
715        )]
716        facet: Vec<(String, String)>,
717        #[arg(
718            long,
719            value_name = "path",
720            help = "the directory to write into, relative to the root, for a kind whose shelf \
721                    fixes the file name under a glob, such as `docs/modules/*/README.md`. The \
722                    document is written as `<path>/` and the file name the shelf fixes, and must match the \
723                    shelf. Refused on any other shelf, whose path already decides the directory"
724        )]
725        directory: Option<String>,
726        #[arg(
727            long,
728            value_name = "date",
729            value_parser = a_date,
730            help = "the date the document is stamped with, as `YYYY-MM-DD`. Defaults to today"
731        )]
732        now: Option<Date>,
733    },
734    Infer {
735        #[arg(
736            long,
737            value_name = "name",
738            help = "who owns the debt it proposes. Required with `--write`, because an owner is \
739                    the field that ranks declared debt above a suppression and this engine will \
740                    not invent one"
741        )]
742        owner: Option<String>,
743        #[arg(
744            long,
745            value_name = "date",
746            value_parser = a_date,
747            help = "the last day the tasks it proposes hold, as `YYYY-MM-DD`. Ninety days out by \
748                    default"
749        )]
750        until: Option<Date>,
751        #[arg(
752            long,
753            help = "put the payload in the lock, which is committed and reviewed. Without it \
754                    nothing is written"
755        )]
756        write: bool,
757        #[arg(
758            long,
759            value_name = "date",
760            value_parser = a_date,
761            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
762        )]
763        now: Option<Date>,
764    },
765    Generate {
766        #[arg(
767            long,
768            help = "write nothing and exit non-zero when what is committed is not what a run \
769                    produces. It reads the corpus through the lock, so it answers whether a \
770                    derived artifact is current"
771        )]
772        check: bool,
773    },
774    Import {
775        #[arg(
776            value_name = "name",
777            help = "which declared import to read, by the name its block carries in \
778                    `.headwater/taxonomy.yml`. One declared import needs no name and two do, \
779                    because choosing for the caller would import whichever the file listed first"
780        )]
781        name: Option<String>,
782        #[arg(
783            long,
784            value_name = "digest",
785            help = "the digest to check the snapshot against. It defaults to the `digest` of the \
786                    import block in `.headwater/taxonomy.yml`, and the verb refuses when neither \
787                    is there rather than reading an unpinned directory"
788        )]
789        expect: Option<String>,
790        #[arg(
791            long,
792            help = "write the edge halves into the documents at their near ends. Without it the \
793                    edges are reported and nothing is touched"
794        )]
795        write: bool,
796    },
797    Export {
798        #[arg(
799            long,
800            value_name = "name",
801            help = "which declared export profile to emit. Every declared profile by default, so \
802                    a filtered audience is never omitted by accident"
803        )]
804        profile: Option<String>,
805        #[arg(
806            long,
807            value_name = "json|jsonschema",
808            help = concat!(
809                "the emitter target. `json` is the native property graph with no loss and \
810                 `jsonschema` constrains front matter. The other five targets of spec 6 parse \
811                 and report the consumer each one waits on. With this flag the artifact goes to \
812                 standard output and no declared output path is touched. ",
813                a_refusal_is_not_an_artifact!()
814            )
815        )]
816        format: Option<String>,
817        #[arg(
818            long,
819            value_name = "date",
820            value_parser = a_date_as_written,
821            help = "the generation time the artifact states, as `YYYY-MM-DD`. Absent by default, \
822                    because an artifact that `--check` compares by byte cannot carry a clock \
823                    reading. Spec 6 asks a filtered export that leaves the repository to state \
824                    one, and this is where it is injected"
825        )]
826        at: Option<String>,
827        #[arg(
828            long,
829            help = "write nothing and exit non-zero when a declared output is not what a run \
830                    produces. It holds every export the taxonomy names a path for to regeneration"
831        )]
832        check: bool,
833        #[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
834        json: bool,
835    },
836    Sweep {
837        #[command(subcommand)]
838        word: Option<SweepWord>,
839    },
840    Probe {
841        #[command(subcommand)]
842        word: Option<ProbeWord>,
843    },
844    Init {
845        #[arg(
846            long,
847            value_name = "dir",
848            help = "the corpus root to declare. Proposed from the tree by default"
849        )]
850        corpus: Option<String>,
851        #[arg(
852            long,
853            value_name = "name",
854            help = "the package to take. `headwater/standard` by default"
855        )]
856        package: Option<String>,
857        #[arg(
858            long,
859            help = "append a `-merge` line to `.gitattributes` for each fold \
860                    `headwater taxonomy resolve` and `headwater generate` write in this tree and a \
861                    `merge=union` line for each append-only store the engine writes, and \
862                    print the two `git config` lines that name `headwater merge-driver` and the \
863                    `info/attributes` lines that select it. A clone that already names the driver \
864                    gets those lines written. It runs after the first `headwater generate`, and on \
865                    a repository that is already bound it does this and nothing else"
866        )]
867        git: bool,
868        #[arg(
869            long = "git-config",
870            requires = "git",
871            help = "also run the two `git config` lines `--git` prints, in this clone, and then \
872                    write the `info/attributes` lines that select the driver. Git takes no driver \
873                    from a repository, so without this flag the lines are printed and the adopter \
874                    runs them"
875        )]
876        git_config: bool,
877    },
878    Taxonomy {
879        #[command(subcommand)]
880        word: Option<TaxonomyWord>,
881    },
882    // The one verb that reads no corpus. `headwater_verbs` states why it is a
883    // verb at all, and what the hook contract's third term does and does not
884    // forbid.
885    Json {
886        #[command(subcommand)]
887        word: Option<JsonWord>,
888    },
889    // `headwater help <verb>`, which is a variant here rather than the
890    // subcommand `clap` injects during `build()`.
891    //
892    // The injected one carries a copy of the whole command tree under itself —
893    // `headwater help sweep plan` and forty-two more — and the dispatch table
894    // carries no such command line, so `tests/verbs.rs` would either fail or
895    // need an exclusion written into it. One variant with one positional adds
896    // the command line #321 asks for and leaves that walk exact.
897    Help {
898        #[arg(
899            value_name = "verb",
900            help = "the verb to describe, with its second word where it takes one: \
901                    `headwater help taxonomy diff`. Without one this screen is printed"
902        )]
903        verb: Vec<String>,
904    },
905    // `headwater completions <shell>`, whose operand is optional to the parser
906    // for the reason every other required operand here is: a bare
907    // `headwater completions` names the four shells and says where a script
908    // goes, and `clap`'s missing-argument message says neither.
909    Completions {
910        #[arg(
911            value_name = "shell",
912            help = "the shell to write a script for. A name outside the four is refused with the \
913                    four printed, and no script is written"
914        )]
915        shell: Option<Shell>,
916    },
917    // A first word this binary does not carry.
918    //
919    // It reaches the message that names every word it does carry, which is the
920    // message this binary printed before the migration and the reason the
921    // external form is declared at all: `clap` would otherwise say
922    // `unrecognized subcommand` and name at most one near miss.
923    #[command(external_subcommand)]
924    Other(Vec<String>),
925}
926
927/// The shells `headwater completions` writes a script for.
928///
929/// # Four, where `clap_complete` offers five
930///
931/// `clap_complete::Shell` carries `Elvish` as well. It is not here, and the
932/// reason is spec 6's own rule about the CLI grammar block: a name that block
933/// declares either runs or states its wait. Clause 8 of
934/// [#321](https://github.com/headwater-ai/headwater/issues/321) names four
935/// shells, the grammar block names the same four, and each of the four is a
936/// script this repository has run rather than a name passed through to a
937/// generator. A fifth would be a name in the grammar that nothing here has
938/// ever executed.
939///
940/// A name outside the four is refused by `clap` with the four printed, because
941/// this is the value parser rather than a match arm underneath one.
942#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
943pub enum Shell {
944    Bash,
945    Zsh,
946    Fish,
947    Powershell,
948}
949
950impl From<Shell> for clap_complete::Shell {
951    fn from(shell: Shell) -> Self {
952        match shell {
953            Shell::Bash => clap_complete::Shell::Bash,
954            Shell::Zsh => clap_complete::Shell::Zsh,
955            Shell::Fish => clap_complete::Shell::Fish,
956            Shell::Powershell => clap_complete::Shell::PowerShell,
957        }
958    }
959}
960
961impl Shell {
962    /// The name a caller types, which is the name the refusal prints.
963    pub fn typed(self) -> &'static str {
964        match self {
965            Shell::Bash => "bash",
966            Shell::Zsh => "zsh",
967            Shell::Fish => "fish",
968            Shell::Powershell => "powershell",
969        }
970    }
971
972    /// The four, in the order a caller meets them in the help.
973    pub const ALL: &'static [Shell] = &[Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Powershell];
974}
975
976// The second word of `json`.
977#[derive(Subcommand, Debug)]
978pub enum JsonWord {
979    Field {
980        #[arg(
981            value_name = "key",
982            help = "the path of steps to the member, outermost first. A step into an object \
983                    is a key, and a step into an array is a decimal index counted from 0. \
984                    `headwater json field tool_input file_path` reads the `file_path` member of \
985                    the `tool_input` member, and `headwater json field related 0 target` reads \
986                    the `target` member of the first element of `related`. Without one, the object is read and no member of it \
987                    is named, which is refused"
988        )]
989        path: Vec<String>,
990    },
991    Count {
992        #[arg(
993            value_name = "key",
994            help = "the path of steps to the array or the object whose elements are counted, \
995                    outermost first, where a step into an array is a decimal index. Without one, the object on standard input is the one counted"
996        )]
997        path: Vec<String>,
998    },
999    Quote,
1000    #[command(external_subcommand)]
1001    Other(Vec<String>),
1002}
1003
1004// The second word of `sweep`.
1005#[derive(Subcommand, Debug)]
1006pub enum SweepWord {
1007    Plan {
1008        #[arg(
1009            long,
1010            value_name = "path",
1011            help = "the slice, as a path prefix under the repository root. The whole corpus by \
1012                    default. There is no sampling rule here: a slice this engine picked would be \
1013                    an unreproducible sample dressed as a reproducible one, and the plan reports \
1014                    its own extent instead"
1015        )]
1016        under: Option<String>,
1017    },
1018    Report {
1019        #[arg(
1020            value_name = "path",
1021            help = "the file an agent wrote back. `headwater sweep plan` prints the shape of it"
1022        )]
1023        path: Option<String>,
1024        #[arg(
1025            long,
1026            value_name = "text|json",
1027            help = concat!(
1028                "`text` is the report a person reads and the default, and `json` is the finding \
1029                 shape spec 4 declares with the provenance and the evidence a sweep adds. ",
1030                a_refusal_is_not_an_artifact!()
1031            )
1032        )]
1033        format: Option<String>,
1034        #[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
1035        json: bool,
1036    },
1037    #[command(external_subcommand)]
1038    Other(Vec<String>),
1039}
1040
1041// The second word of `probe`.
1042#[derive(Subcommand, Debug)]
1043pub enum ProbeWord {
1044    Plan {
1045        #[arg(
1046            long,
1047            value_name = "regression|campaign|documentation",
1048            help = "which tier of `.headwater/probe.yml` to plan against. A tier declares the \
1049                    ceiling, the session cost, the repetitions, the arms and the ablation its \
1050                    absent arm removes, and the plan is projected against them. A paired tier \
1051                    refuses a probe whose predicate names a document its own ablation removes. \
1052                    `regression` by default"
1053        )]
1054        tier: Option<String>,
1055        #[arg(
1056            long,
1057            value_name = "present|absent",
1058            help = "narrow the selection to one arm the tier declares. Every arm the tier \
1059                    declares by default, which is one for `regression` and two for `campaign` and \
1060                    `documentation`. \
1061                    An arm the tier does not declare refuses the run rather than planning \
1062                    another one, and the refusal names the arms the tier declares"
1063        )]
1064        arm: Option<String>,
1065        #[arg(
1066            long,
1067            value_name = "name",
1068            help = "narrow the selection to one probe category, by the name this engine declares \
1069                    for it. Every category by default, a name outside the closed set is refused \
1070                    with the set printed, and a category no probe of this corpus carries is \
1071                    refused rather than planned as a run of nothing"
1072        )]
1073        category: Option<String>,
1074        // Zero is the default and it is a value like any other. The seed is
1075        // the caller's, so a run that states none states zero, and a run that
1076        // repeats a seed repeats a selection.
1077        //
1078        // It is a member of the run identity and not an input to the selection.
1079        // `crates/probe/src/plan.rs` records it and prints it, and the selection
1080        // is every declared probe, narrowed by category and sorted by
1081        // identifier. Spec 5 asks for deterministic rotation and this engine
1082        // implements none, so the help says that rather than implying a draw.
1083        #[arg(
1084            long,
1085            value_name = "n",
1086            default_value_t = 0,
1087            help = "the rotation seed, which is a member of the run identity spec 5 declares. It \
1088                    is the caller's number: a run that states none states zero, and it is \
1089                    recorded as stated. No selection is drawn from it — every declared probe is \
1090                    selected — so it identifies a run rather than choosing one"
1091        )]
1092        seed: u64,
1093        #[arg(
1094            long,
1095            value_name = "probe",
1096            help = "leave one probe out of the selection, by its identifier. Repeat it for more \
1097                    than one. The selection digest is taken after the exclusion, so it states \
1098                    the run that happens, and an identifier the selection does not hold is \
1099                    refused rather than ignored"
1100        )]
1101        exclude: Vec<String>,
1102        #[arg(
1103            long,
1104            value_name = "n",
1105            help = "run fewer repetitions than the tier declares, for a pilot of the same \
1106                    selection and arms. It may only lower the count: a higher one is refused, \
1107                    because spending more is a change to `.headwater/probe.yml` that a person \
1108                    makes"
1109        )]
1110        repetitions: Option<u32>,
1111    },
1112    Record {
1113        #[arg(
1114            value_name = "path",
1115            help = "the transcript a recorder wrote. `headwater probe plan` prints the run \
1116                    identity it has to carry"
1117        )]
1118        path: Option<String>,
1119    },
1120    Grade {
1121        #[arg(
1122            value_name = "path",
1123            help = "the transcript a recorder wrote. It is graded against the probes this corpus \
1124                    declares, re-derived here rather than taken from the transcript"
1125        )]
1126        path: Option<String>,
1127    },
1128    Stale,
1129    #[command(external_subcommand)]
1130    Other(Vec<String>),
1131}
1132
1133// The second word of `taxonomy`.
1134#[derive(Subcommand, Debug)]
1135pub enum TaxonomyWord {
1136    Validate,
1137    Resolve {
1138        #[arg(
1139            long,
1140            help = "write nothing and exit non-zero when what is committed is not what a run \
1141                    produces. It reads the taxonomy sources, so it answers whether the lock is \
1142                    current"
1143        )]
1144        check: bool,
1145    },
1146    Audit {
1147        #[arg(
1148            long,
1149            value_name = "date",
1150            value_parser = a_date,
1151            help = "the date a staleness reading and a dwell reading are taken at, as \
1152                    `YYYY-MM-DD`. Defaults to today, and two audits of one tree at one date write \
1153                    the same bytes"
1154        )]
1155        now: Option<Date>,
1156        #[arg(
1157            long,
1158            help = "append this run's adoption reading to `.headwater/adoption.jsonl`. Without it \
1159                    the verb writes nothing. A reading the store already holds at this lock and \
1160                    this date is not appended twice, so two recorded audits of one tree at one \
1161                    date still write the same bytes"
1162        )]
1163        record: bool,
1164    },
1165    Publish {
1166        #[arg(
1167            long,
1168            value_name = "name",
1169            help = "the package to publish. The one this repository's own declaration takes, by \
1170                    default, because a publisher usually publishes what it also consumes. Refused \
1171                    together with `--from`, which names the same thing by its directory instead"
1172        )]
1173        package: Option<String>,
1174        #[arg(
1175            long,
1176            value_name = "dir",
1177            help = "read the manifest at this directory directly, bypassing the lookup by name \
1178                    under `.headwater/packages/` that `--package` drives. For a repository that both \
1179                    publishes a package and consumes it: `taxonomy vendor` refuses to install over \
1180                    a directory that carries no release record, so a maintained source cannot sit \
1181                    where its own artifact would be installed. This reads it from wherever it \
1182                    actually sits instead"
1183        )]
1184        from: Option<PathBuf>,
1185        #[arg(
1186            long,
1187            value_name = "name",
1188            help = "derive and publish this named assembly from the source package. It uses the \
1189                    same source selection as `--package` or `--from`, and produces one flattened \
1190                    package with no runtime bundle selection"
1191        )]
1192        assembly: Option<String>,
1193        #[arg(
1194            long,
1195            value_name = "dir",
1196            help = "where to write the artifact. The directory must be empty or absent, because a \
1197                    published artifact is every file under its root and a stray one would be a \
1198                    member the publisher never shipped. A run that cannot finish leaves it as it \
1199                    found it, so a second run meets the same precondition the first one did"
1200        )]
1201        out: Option<PathBuf>,
1202        #[arg(
1203            long,
1204            help = "remove what a publish killed part-way left at `--out`, and publish in the \
1205                    same run. It removes one state and nothing else: files at `--out` with no \
1206                    release record, beside a `<out>~staging` directory holding both the files a \
1207                    publish writes there to say it could not move the artifact into place and is \
1208                    writing into `--out` one file at a time. Only a killed publish leaves those \
1209                    two together, and the second one names the output path it was writing. A \
1210                    directory holding anything else, and an `--out` that carries a release \
1211                    record, are left exactly as they are and the publish refuses as it does \
1212                    without this flag"
1213        )]
1214        clear_killed: bool,
1215        #[arg(
1216            long,
1217            help = "write nothing, and exit 1 when the vendored copy under `.headwater/packages/` \
1218                    is not what a fresh publish of the source at `--from` produces. It publishes \
1219                    into a private directory outside the tree, removes it, and names each member \
1220                    that moved. For a repository that maintains a package and also consumes it, \
1221                    so that a source changed without a republish fails a gate. Needs `--from`, \
1222                    and is refused with `--out`, `--package`, `--assembly`, `--clear-killed` and \
1223                    `--json`"
1224        )]
1225        check: bool,
1226        #[arg(long, help = JSON_ALONE)]
1227        json: bool,
1228    },
1229    Vendor {
1230        #[arg(
1231            value_name = "dir-or-location",
1232            help = "the directory of an artifact somebody already fetched, or the https:// \
1233                    location of a published artifact zip, which this verb fetches through \
1234                    `headwater-fetch`, a crate nothing in the checking loop links (HW-DR-0075)"
1235        )]
1236        path: Option<String>,
1237        #[arg(
1238            long,
1239            value_name = "digest",
1240            help = "the digest to check the artifact against. It defaults to `taxonomy.digest` in \
1241                    `.headwater/taxonomy.yml`, and the verb refuses when neither is there. A pin \
1242                    the engine took from the artifact in front of it would be a pin against itself"
1243        )]
1244        expect: Option<String>,
1245    },
1246    Diff {
1247        #[arg(
1248            value_name = "dir",
1249            help = "the directory of an artifact somebody already fetched. This verb opens no \
1250                    socket, so it takes a path and never a location"
1251        )]
1252        path: Option<String>,
1253        #[arg(
1254            long,
1255            value_name = "version",
1256            help = "the version the artifact is expected to be, written as a version or as a \
1257                    range: `4.0.0`, or `>=4 <5` with the quoting your shell needs. This verb \
1258                    fetches nothing, so the directory decides which artifact is compared and this \
1259                    flag holds it to what the caller meant. It is read by the one range reader \
1260                    the engine has, which is what reads `requires_engine`"
1261        )]
1262        to: Option<String>,
1263        #[arg(
1264            long,
1265            value_name = "date",
1266            value_parser = a_date,
1267            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
1268        )]
1269        now: Option<Date>,
1270    },
1271    Migrate {
1272        #[arg(
1273            value_name = "dir",
1274            help = "the directory of an artifact somebody already fetched. This verb opens no \
1275                    socket, so it takes a path and never a location"
1276        )]
1277        path: Option<String>,
1278        #[arg(
1279            long,
1280            value_name = "version",
1281            help = "the version the artifact is expected to be, written as a version or as a \
1282                    range: `4.0.0`, or `>=4 <5` with the quoting your shell needs"
1283        )]
1284        to: Option<String>,
1285        #[arg(
1286            long,
1287            help = "write the files each step names. Without it every file each step would write \
1288                    is reported and nothing is written"
1289        )]
1290        apply: bool,
1291        #[arg(
1292            long,
1293            value_name = "date",
1294            value_parser = a_date,
1295            help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
1296        )]
1297        now: Option<Date>,
1298    },
1299    Graph {
1300        #[arg(
1301            long,
1302            value_enum,
1303            default_value_t,
1304            help = "which drawing to print. `concrete` draws the concrete kinds, the anchors and \
1305                    the relations between them. `abstract` draws each abstract kind, the kinds \
1306                    declared under it and the relations that name it, which `concrete` leaves out"
1307        )]
1308        view: crate::taxonomy_graph::View,
1309        #[arg(
1310            long,
1311            help = "add a key that draws each shape and each edge style the drawing uses, and \
1312                    names the family each edge color stands for"
1313        )]
1314        legend: bool,
1315    },
1316    #[command(external_subcommand)]
1317    Other(Vec<String>),
1318}
1319
1320/// The command tree this binary parses with, and the one every reader takes.
1321///
1322/// [`Cli::command`] is the derived half and carries the grammar alone. This
1323/// function is what puts the words on it, and every word it puts there comes
1324/// out of [`headwater_verbs::VERBS`]: the group headings and the one-line
1325/// summary of the first screen, the long description a verb prints for itself,
1326/// and the same pair for each second word.
1327///
1328/// `main` prints help through this and `tests/verbs.rs` walks it, so a reader
1329/// of the help and a reader of the test meet the same tree. A caller that used
1330/// [`Cli::command`] directly would meet a tree with no prose on it at all.
1331pub fn command() -> Command {
1332    command_at(paint::width())
1333}
1334
1335/// The same tree, laid out at a width the caller states.
1336///
1337/// Every string it carries is folded to `width` before `clap` sees it, and
1338/// `clap` folds nothing, so this number and the strings are the whole of the
1339/// layout. [`command`] is this at [`paint::WIDTH`] unless the command line
1340/// carries `--wide`.
1341pub fn command_at(width: usize) -> Command {
1342    command_in(width, paint::stdout_color())
1343}
1344
1345/// The same tree again, at a width and in a color mode the caller states.
1346///
1347/// [`command_at`] is this with the mode read off standard output, which is the
1348/// only place that reading happens. A caller that states the mode gets a tree
1349/// whose help renders the same bytes wherever it runs, which is what a test and
1350/// a completion script both need: `main`'s `completions` states
1351/// [`paint::ColorMode::Plain`], and `tests/width.rs` renders both modes and
1352/// compares them.
1353pub fn command_in(width: usize, mode: paint::ColorMode) -> Command {
1354    let mut root = Cli::command()
1355        .about(format!(
1356            "{} — {}",
1357            headwater_verbs::BINARY,
1358            headwater_verbs::TAGLINE
1359        ))
1360        // The choice and the palette are set together and from one mode. The
1361        // choice alone would turn on `clap`'s own bold-and-underline defaults,
1362        // which `HW-DR-0045` does not rule on; the palette alone would be
1363        // stripped at write time by the `Never` the derive declares. See
1364        // `paint::color_choice` for why this is never `ColorChoice::Auto`.
1365        .color(paint::color_choice(mode))
1366        .styles(paint::help_styles(mode))
1367        .help_template(first_screen(width, mode));
1368    for verb in headwater_verbs::VERBS {
1369        // A name the derive does not carry is skipped rather than added.
1370        //
1371        // `Command::mut_subcommand` panics on a name it cannot find, and
1372        // `Command::subcommand` would put a command in the tree with no variant
1373        // behind it and nothing to dispatch to. Either one would answer a
1374        // discrepancy between the table and the parser here, where a caller
1375        // running `--help` meets it. It is answered in
1376        // `engine/crates/cli/tests/verbs.rs` instead, which walks this tree
1377        // against the table in both directions and prints the command lines that
1378        // are on one side and not the other.
1379        if root.find_subcommand(verb.name).is_some() {
1380            root = root.mut_subcommand(verb.name, |one| described(one, verb, width, mode));
1381        }
1382    }
1383    paint::painted(root, width)
1384}
1385
1386/// The command line this process was started with, parsed through [`command`].
1387///
1388/// `Cli::parse` and `Cli::try_parse` build their own tree out of the derive
1389/// alone, which carries the grammar and none of the words. A binary that parsed
1390/// through one tree and printed help out of another would answer `--help` from
1391/// a command nothing had described, which is the state this returned before the
1392/// words were put on it. One entry point is what keeps the two the same tree.
1393pub fn parsed() -> Result<Cli, clap::Error> {
1394    let matches = command().try_get_matches()?;
1395    if let Some(message) = a_width_for_a_run_that_lays_nothing_out(&matches) {
1396        return Err(clap::Error::raw(
1397            clap::error::ErrorKind::ArgumentConflict,
1398            message,
1399        ));
1400    }
1401    Cli::from_arg_matches(&matches)
1402}
1403
1404/// `--wide` on a run that lays nothing out, which is a run it would do nothing
1405/// in.
1406///
1407/// # The rule was wider than clause 12 asked, and it has narrowed
1408///
1409/// Clause 12 of [#321](https://github.com/headwater-ai/headwater/issues/321)
1410/// asks that `--wide` be refused alongside `--format json|sarif|markdown`. The
1411/// rule here was wider than that: it refused **every** run that printed no help,
1412/// because the flag laid out the help and laid out nothing else, and
1413/// `headwater check --wide --format text` would have been as inert as
1414/// `--format json` and would have said so to nobody. The doc comment recorded
1415/// that the refusal would narrow to the machine formats when a report gained a
1416/// layout.
1417///
1418/// [#340](https://github.com/headwater-ai/headwater/issues/340) gave it one, and
1419/// this is the narrowing. The text report of `headwater check` is laid out by
1420/// `headwater_check::fill` at the width `paint::width` states, so `--wide` is
1421/// answered there rather than refused. Every other run that lays nothing out is
1422/// still refused, and the machine formats are still named by name: this
1423/// repository has two open issues about flags accepted and silently ignored —
1424/// [#337](https://github.com/headwater-ai/headwater/issues/337) and
1425/// [#338](https://github.com/headwater-ai/headwater/issues/338) — and a third
1426/// would have been this one.
1427///
1428/// # Why the predicate names a verb and not a format
1429///
1430/// "The format is `text`" is not the test. `headwater capture --format text`
1431/// exists and lays nothing out, and so does every other verb that prints text
1432/// nobody folded. What is laid out is the report of one verb, so the check names
1433/// that verb and the absence of a machine format on it.
1434///
1435/// # Why it reads two flags for one format
1436///
1437/// A machine format reaches this verb under two names. `--format json` is a
1438/// value, `--json` is a boolean, and
1439/// [HW-DR-0033](../../../../docs/decisions/0033-q33-whether-the-command-line-is-derived-and-who-a-flag-belongs-to.md)
1440/// rules that the two are one target under two spellings. A predicate that read
1441/// `format` alone would answer `check --wide --json` and let the width flag do
1442/// nothing, which is the exact defect the paragraph above says a third issue
1443/// would have been about. **Whenever a refusal narrows, every spelling of the
1444/// thing it narrows on has to be enumerated**, and `tests/width.rs` carries a
1445/// row for each.
1446///
1447/// # Why reaching this function is already the test
1448///
1449/// `clap` answers `--help` inside `try_get_matches` and returns before this
1450/// runs, so a run that printed help never arrives here. The one route that
1451/// prints help and does arrive is `headwater help <verb>`, which is a verb of
1452/// this binary rather than a flag, and it is the one command the check lets
1453/// through.
1454///
1455/// The `format` value is read off the matches rather than off the parsed
1456/// `Verb`, so every verb that declares one is named by the same two lines and a
1457/// verb that gains one later is named without an edit.
1458fn a_width_for_a_run_that_lays_nothing_out(matches: &clap::ArgMatches) -> Option<String> {
1459    let mut leaf = matches;
1460    while let Some((_, inner)) = leaf.subcommand() {
1461        leaf = inner;
1462    }
1463    if leaf.try_get_one::<bool>("wide").ok().flatten() != Some(&true) {
1464        return None;
1465    }
1466    if matches.subcommand_name() == Some("help") {
1467        return None;
1468    }
1469    let format = leaf.try_get_one::<String>("format").ok().flatten();
1470    // `--json` is the second spelling of `--format json`, and it is a boolean of
1471    // its own rather than a value of `format`. A predicate that read `format`
1472    // alone would let `check --wide --json` through with the flag doing nothing,
1473    // which is the defect this whole refusal exists to prevent. HW-DR-0033 rules
1474    // that the two names reach one target, so every reader of one reads both.
1475    let json = leaf.try_get_one::<bool>("json").ok().flatten() == Some(&true);
1476    // The one report this binary lays out at a width. `check` with no format
1477    // named, in either spelling, writes text.
1478    let laid_out = matches.subcommand_name() == Some("check")
1479        && !json
1480        && matches!(format.map(String::as_str), None | Some("text"));
1481    if laid_out {
1482        return None;
1483    }
1484    let says = match format.filter(|value| value.as_str() != "text") {
1485        Some(format) => format!("`--format {format}` writes an artifact that nothing lays out"),
1486        None if json => "`--json` writes an artifact that nothing lays out".to_string(),
1487        None => "this run lays nothing out".to_string(),
1488    };
1489    Some(format!(
1490        "`--wide` says how wide the help and the report of `{0} check` are laid out, and {says}. A \
1491         run carrying it would carry one flag that does nothing, so it is refused rather than run. \
1492         The runs it widens are `{0} --wide --help`, `{0} <verb> --wide --help`, `{0} --wide help \
1493         <verb>` and `{0} check --wide`",
1494        headwater_verbs::BINARY
1495    ))
1496}
1497
1498/// One verb of the tree, with the words the table carries for it.
1499fn described(
1500    command: Command,
1501    verb: &headwater_verbs::Verb,
1502    width: usize,
1503    mode: paint::ColorMode,
1504) -> Command {
1505    let mut one = command.about(verb.description);
1506    if !verb.words.is_empty() {
1507        one = one.help_template(second_words(verb, width, mode));
1508        for word in verb.words {
1509            if one.find_subcommand(word.name).is_none() {
1510                continue;
1511            }
1512            one = one.mut_subcommand(word.name, |inner| inner.about(word.description));
1513        }
1514    }
1515    one
1516}
1517
1518/// The column the second field of a printed list starts at.
1519const COLUMN: usize = 15;
1520
1521/// The template `headwater --help` renders.
1522///
1523/// The literal parts of a `clap` template are written out as they stand, and
1524/// only the `{…}` tags are rendered, so this is where the layout of the first
1525/// screen is decided rather than in a `write!` somewhere else. `{subcommands}`
1526/// is deliberately absent: `clap` renders one flat list and the screen this
1527/// builds is grouped, and the groups come off
1528/// [`headwater_verbs::groups`] in the order the table first names each one.
1529/// `{options}` is absent for the same kind of reason: `clap` renders the whole
1530/// description of every global flag, and this screen prints the one-line summary
1531/// [`GLOBALS`] declares beside each of them.
1532///
1533/// The examples are the one part of this screen that no earlier version of the
1534/// binary carried. #321 measured the old help and found no example anywhere in
1535/// its 25,415 bytes, so these are written rather than recovered, and each one
1536/// is a command line that runs.
1537fn first_screen(width: usize, mode: paint::ColorMode) -> String {
1538    // `{about}` is dropped rather than kept beside the masthead: the two say
1539    // the same tagline, and `HW-DR-0045`'s masthead is printed separately, by
1540    // plain I/O, before this template is ever reached — never embedded in it.
1541    //
1542    // A literal ANSI escape sequence placed in a `clap` help template does
1543    // not survive `print_help()` under `ColorChoice::Never`: `clap_builder`
1544    // strips it regardless of the real stream's terminal state, proven with a
1545    // minimal reproduction against this workspace's exact `clap` version
1546    // before this comment was written. `ColorChoice::Always` keeps the bytes,
1547    // but also turns on `clap`'s own default styling of `Usage:` and every
1548    // other element it recognizes, which is color this decision never rules
1549    // on. So the masthead is not this template's problem: `wants_root_help`
1550    // in `main.rs` decides when to print it, with `paint::banner`, entirely
1551    // outside `clap`'s own writer.
1552    let mut out = format!(
1553        "{{usage-heading}} {{usage}}\n\n{}:\n",
1554        paint::paint(paint::Role::Heading, "Examples", mode)
1555    );
1556    for (line, says) in [
1557        (
1558            "headwater check --strict",
1559            "run the checks, and fail on an error",
1560        ),
1561        (
1562            "headwater route \"add rate limiting\"",
1563            "the documents that govern a task",
1564        ),
1565        (
1566            "headwater explain HW-DR-0033",
1567            "why a document is the kind it is",
1568        ),
1569        (
1570            "headwater new decision --title \"Adopt an overlay\"",
1571            "scaffold a document of a kind",
1572        ),
1573        (
1574            "headwater help taxonomy diff",
1575            "the long description of one verb",
1576        ),
1577    ] {
1578        // The command line and what it does are stacked rather than columned.
1579        // The longest of the five is 48 columns, so a column wide enough to
1580        // hold it leaves 26 for a description and every one of the five is
1581        // longer than that. Two lines each is what 80 columns buys.
1582        out.push_str(&format!("  {line}\n"));
1583        out.push_str(&paint::fold_indented(says, width, 6));
1584    }
1585    // A group heading is a section heading and a verb name is a verb name, so
1586    // both take the role `HW-DR-0045` gives them and neither invents one. They
1587    // are painted here rather than through `clap`'s `Styles`, because this
1588    // screen is a template written by this crate and `clap` renders a template's
1589    // literal text without knowing what any of it is. Under
1590    // `paint::ColorMode::Plain` both calls return the byte for byte string this
1591    // template carried before the palette reached it.
1592    for group in headwater_verbs::groups() {
1593        out.push_str(&format!(
1594            "\n{}:\n",
1595            paint::paint(paint::Role::Heading, group, mode)
1596        ));
1597        for verb in headwater_verbs::VERBS
1598            .iter()
1599            .filter(|one| one.group == group)
1600        {
1601            out.push_str(&paint::painted_row(
1602                verb.name,
1603                verb.summary,
1604                COLUMN,
1605                width,
1606                paint::Role::Verb,
1607                mode,
1608            ));
1609        }
1610    }
1611    // The global flags are rendered here for the reason the verbs above are.
1612    //
1613    // `{options}` renders the whole description of every one of them, which is
1614    // twenty-five lines of paragraph on the one screen an adopter meets first
1615    // and none of it helps a reader choose a verb. `HW-DR-0042` holds this
1616    // screen to one line per entry, so the summary of each flag is printed here
1617    // and the description stays where `clap` already puts it, on all 32 verb
1618    // pages.
1619    //
1620    // The column is derived rather than written down. `paint::row` indents by
1621    // two and leaves what is left of the column to the name, so a column
1622    // narrower than the longest name overruns `width` on the first line of that
1623    // row. `COLUMN` above is `2 + 11 + 2`, which is the longest verb name and
1624    // the gutter this screen keeps between the two fields; the longest flag name
1625    // is `--root <path>` at thirteen, so this is the same arithmetic on a longer
1626    // name rather than a second discipline.
1627    let longest = GLOBALS
1628        .iter()
1629        .map(|one| one.name.chars().count())
1630        .max()
1631        .unwrap_or(0);
1632    let at = 2 + longest + 2;
1633    out.push_str(&format!(
1634        "\n{}:\n",
1635        paint::paint(paint::Role::Heading, "Global flags", mode)
1636    ));
1637    for one in GLOBALS {
1638        out.push_str(&paint::painted_row(
1639            one.name,
1640            one.summary,
1641            at,
1642            width,
1643            paint::Role::Path,
1644            mode,
1645        ));
1646    }
1647    out.push('\n');
1648    out.push_str(&paint::fold_indented(
1649        &format!(
1650            "Run `{0} help <verb>` for the long description of one verb, or `{0} <verb> --help`.",
1651            headwater_verbs::BINARY
1652        ),
1653        width,
1654        0,
1655    ));
1656    out
1657}
1658
1659/// The template a verb with second words renders.
1660///
1661/// The same argument as [`first_screen`]: `clap`'s own subcommand list would
1662/// print each second word's `about`, which is its long description here, so a
1663/// caller who typed `headwater sweep` to find out what `plan` is would meet
1664/// both descriptions in full. This prints the summary the table carries and
1665/// names where the long one is.
1666fn second_words(verb: &headwater_verbs::Verb, width: usize, mode: paint::ColorMode) -> String {
1667    let mut out = String::from("{about}\n\n{usage-heading} {usage}\n\nSecond words:\n");
1668    for word in verb.words {
1669        out.push_str(&paint::painted_row(
1670            word.name,
1671            word.summary,
1672            COLUMN,
1673            width,
1674            paint::Role::Verb,
1675            mode,
1676        ));
1677    }
1678    out.push_str("\nFlags:\n{options}\n\n");
1679    out.push_str(&paint::fold_indented(
1680        &format!(
1681            "Run `{} help {} <word>` for the long description of one.",
1682            headwater_verbs::BINARY,
1683            verb.name
1684        ),
1685        width,
1686        0,
1687    ));
1688    out
1689}
1690
1691/// A date the engine compares against, as `YYYY-MM-DD`.
1692fn a_date(text: &str) -> Result<Date, String> {
1693    Date::parse(text).ok_or_else(|| "a date written `YYYY-MM-DD`".to_string())
1694}
1695
1696/// The same date, kept as the caller wrote it.
1697///
1698/// `export --at` puts the value into an artifact rather than comparing it, so
1699/// it is checked here and carried on unparsed.
1700fn a_date_as_written(text: &str) -> Result<String, String> {
1701    a_date(text).map(|_| text.to_string())
1702}
1703
1704/// A pointer budget, which is a count and never zero.
1705fn a_budget(text: &str) -> Result<usize, String> {
1706    match text.parse::<usize>() {
1707        Ok(value) if value > 0 => Ok(value),
1708        _ => Err("a whole number above zero".to_string()),
1709    }
1710}
1711
1712/// `<left>=<right>`, with neither half empty.
1713///
1714/// `--relates supersedes=HW-DR-0007` and `--facet probe_category=discovery` are
1715/// the two callers, and `clap` prints the flag it was refusing in front of
1716/// whatever this returns.
1717fn a_pair(text: &str) -> Result<(String, String), String> {
1718    match text.split_once('=') {
1719        Some((left, right)) if !left.is_empty() && !right.is_empty() => {
1720            Ok((left.to_string(), right.to_string()))
1721        }
1722        _ => Err(
1723            "`<left>=<right>`, as in `--relates supersedes=HW-DR-0007` or \
1724                  `--facet probe_category=discovery`"
1725                .to_string(),
1726        ),
1727    }
1728}
1729
1730/// What a caller reads when `clap` refuses a command line.
1731///
1732/// `clap` renders a refusal as the message, then a usage block, then a line
1733/// telling the caller to try `--help`. Two of those three are the grammar and a
1734/// pointer to it, and [#306](https://github.com/headwater-ai/headwater/issues/306)
1735/// already settled what this binary does with both: a refusal names where the
1736/// grammar is rather than reprinting it, and the pointer names the binary so a
1737/// caller can paste it. So the message is what is taken here, with any `tip:`
1738/// line under it, and `fail` supplies the prefix and the pointer.
1739///
1740/// The message text is `clap`'s, which is the whole reason for taking the
1741/// crate: it names the offending word, and it enumerates the legal values of a
1742/// flag that has a closed set.
1743pub fn headline(error: &clap::Error) -> String {
1744    let rendered = error.render().to_string();
1745    let head: Vec<String> = rendered
1746        .lines()
1747        .take_while(|line| !line.starts_with("Usage:") && !line.starts_with("For more information"))
1748        .map(str::trim)
1749        .filter(|line| !line.is_empty())
1750        .map(|line| line.strip_prefix("error: ").unwrap_or(line).to_string())
1751        .collect();
1752    match head.is_empty() {
1753        true => rendered.split_whitespace().collect::<Vec<_>>().join(" "),
1754        false => head.join("\n"),
1755    }
1756}
1757
1758#[cfg(test)]
1759mod tests {
1760    use super::{a_budget, a_date, a_pair, command, headline, Cli};
1761    use clap::{CommandFactory, Parser};
1762
1763    #[test]
1764    fn the_declared_parse_is_a_command_clap_can_build() {
1765        Cli::command().debug_assert();
1766        command().debug_assert();
1767    }
1768
1769    #[test]
1770    fn a_value_a_flag_cannot_take_is_named_rather_than_defaulted() {
1771        assert!(a_date("2026-13-45").is_err());
1772        assert!(a_date("2026-01-01").is_ok());
1773        assert!(a_budget("0").is_err());
1774        assert!(a_budget("x").is_err());
1775        assert_eq!(a_budget("3"), Ok(3));
1776        assert!(a_pair("nope").is_err());
1777        assert!(a_pair("=right").is_err());
1778        assert!(a_pair("left=").is_err());
1779        assert_eq!(
1780            a_pair("supersedes=HW-DR-0007"),
1781            Ok(("supersedes".to_string(), "HW-DR-0007".to_string()))
1782        );
1783    }
1784
1785    /// The refusal a caller reads carries the message and never the usage block.
1786    ///
1787    /// The marker is `--root <path>`, which is a line of the help body and of no
1788    /// refusal. `engine/crates/cli/tests/wiring.rs` holds the same marker over
1789    /// the running binary; this holds it over the string this function returns,
1790    /// where a failure names the line rather than a process.
1791    #[test]
1792    fn a_refusal_carries_the_message_and_not_the_grammar() {
1793        let error = Cli::try_parse_from(["headwater", "check", "--nonsense"])
1794            .expect_err("`--nonsense` is not a flag `check` reads");
1795        let headline = headline(&error);
1796        assert!(
1797            headline.contains("--nonsense"),
1798            "the refusal names the offending word: {headline}"
1799        );
1800        assert!(
1801            !headline.contains("--root <path>"),
1802            "the refusal does not reprint the grammar: {headline}"
1803        );
1804        assert!(
1805            !headline.contains("Usage:"),
1806            "the refusal does not reprint the usage block: {headline}"
1807        );
1808        assert!(
1809            !headline.contains("For more information"),
1810            "the pointer is `fail`'s and is written once: {headline}"
1811        );
1812    }
1813
1814    /// A flag of another verb is refused rather than accepted and ignored.
1815    ///
1816    /// This is the reversal HW-DR-0033 records, at the parse rather than at the
1817    /// exit status.
1818    #[test]
1819    fn a_flag_of_another_verb_does_not_reach_this_one() {
1820        assert!(Cli::try_parse_from(["headwater", "check", "--level", "L0"]).is_err());
1821        assert!(Cli::try_parse_from(["headwater", "conformance", "--level", "L0"]).is_ok());
1822        assert!(Cli::try_parse_from(["headwater", "sweep", "report", "f", "--strict"]).is_err());
1823        assert!(Cli::try_parse_from(["headwater", "check", "--strict"]).is_ok());
1824    }
1825
1826    /// `--root` is the one flag every verb reads, so it is the one flag declared
1827    /// global. A global flag is a declaration per flag, which is the opposite of
1828    /// the namespace that admitted all of them everywhere.
1829    #[test]
1830    fn the_corpus_flag_reaches_every_verb_from_either_side_of_it() {
1831        for arguments in [
1832            ["headwater", "check", "--root", "/tmp"],
1833            ["headwater", "--root", "/tmp", "check"],
1834        ] {
1835            let cli = Cli::try_parse_from(arguments).expect("`--root` is global");
1836            assert_eq!(cli.root.as_deref(), Some(std::path::Path::new("/tmp")));
1837        }
1838    }
1839}