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