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