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