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