pub mod paint;
pub mod taxonomy_graph;
macro_rules! a_refusal_is_not_an_artifact {
() => {
"A run that refuses writes nothing here: the account is one English sentence on standard \
error and the status is 1"
};
}
const JSON_BESIDE_FORMAT: &str = concat!(
"write this run as one JSON document on standard output. It is the artifact `--format json` \
writes, byte for byte. A run that states both is refused rather than resolved, because two \
names for one target is a question answered twice. ",
a_refusal_is_not_an_artifact!()
);
const JSON_ALONE: &str = concat!(
"write this run as one JSON document on standard output, instead of the report a person \
reads. The document names its own shape in a `version` member, so a consumer pins that \
rather than the version of this engine. It moves no exit status. ",
a_refusal_is_not_an_artifact!()
);
const ROOT_TEXT: &str = "the repository to read. Defaults to the working directory";
const VERSION_TEXT: &str = "the version of this engine. It is the number a package's \
`requires_engine` range is read against, and it is the number to quote in a bug report. One \
line on standard output, and no repository is needed to ask";
const WIDE_TEXT: &str = "lay the help, and the report of `headwater check`, out at the width \
`COLUMNS` states, held to the range 80 to 120. A reading that is absent or is not a number \
gives 80, which is what a run with no flag gives. Without it nothing reads `COLUMNS`, so a run \
piped into a file and a run under a terminal write the same bytes. A shell keeps `COLUMNS` to \
itself, so the form that carries it is `COLUMNS=100 headwater --wide --help`. A run that lays \
nothing out, a machine format included, refuses it rather than accepting a flag that does \
nothing";
const NO_COLOR_TEXT: &str = "force plain text on both streams: bold and dim weight and glyphs, no \
escape sequence. Without it, this binary senses whether each stream is a terminal and renders \
color there, plain text otherwise. `NO_COLOR`, set to any value, has the same effect. It is \
declared so that a caller who writes it out of habit is answered rather than refused";
const NO_BANNER_TEXT: &str = "suppress the masthead: the line naming this binary and its version, \
and the rule beneath it, that the root help screen alone prints above `Usage:`. \
`HEADWATER_NO_BANNER`, set to any value, has the same effect. It is accepted, and inert, on \
every verb's own page, the same posture `--no-color` already takes for a flag that changes \
nothing there";
#[derive(Debug)]
pub struct Global {
pub id: &'static str,
pub name: &'static str,
pub summary: &'static str,
pub description: &'static str,
}
impl Global {
#[must_use]
pub fn covers(&self, id: &str) -> bool {
self.id == id
}
}
const ROOT: Global = Global {
id: "root",
name: "--root <path>",
summary: ROOT_TEXT,
description: ROOT_TEXT,
};
const VERSION: Global = Global {
id: "version",
name: "-V, --version",
summary: "the version of this engine, on one line, from anywhere",
description: VERSION_TEXT,
};
const WIDE: Global = Global {
id: "wide",
name: "--wide",
summary: "lay the help and the check report out at `COLUMNS`, 80 to 120",
description: WIDE_TEXT,
};
const NO_COLOR: Global = Global {
id: "no_color",
name: "--no-color",
summary: "force plain text, no matter what either stream senses",
description: NO_COLOR_TEXT,
};
const NO_BANNER: Global = Global {
id: "no_banner",
name: "--no-banner",
summary: "suppress the masthead this binary prints on the root screen",
description: NO_BANNER_TEXT,
};
const HELP: Global = Global {
id: "help",
name: "-h, --help",
summary: "this screen, or the long form of one verb",
description: "Print help",
};
pub const GLOBALS: &[&Global] = &[&ROOT, &VERSION, &WIDE, &NO_COLOR, &NO_BANNER, &HELP];
use clap::{Command, CommandFactory, FromArgMatches, Parser, Subcommand};
use headwater_check::Date;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
name = headwater_verbs::BINARY,
bin_name = headwater_verbs::BINARY,
color = clap::ColorChoice::Never,
disable_help_subcommand = true,
disable_version_flag = true
)]
pub struct Cli {
#[arg(
long,
global = true,
value_name = "path",
help = ROOT_TEXT
)]
pub root: Option<PathBuf>,
#[arg(
short = 'V',
long,
global = true,
help = VERSION_TEXT
)]
pub version: bool,
#[arg(
long,
global = true,
help = WIDE_TEXT
)]
pub wide: bool,
#[arg(
long = "no-color",
global = true,
help = NO_COLOR_TEXT
)]
pub no_color: bool,
#[arg(
long = "no-banner",
global = true,
help = NO_BANNER_TEXT
)]
pub no_banner: bool,
#[command(subcommand)]
pub verb: Option<Verb>,
}
#[derive(Subcommand, Debug)]
pub enum Verb {
Check {
#[arg(
long,
help = "exit non-zero when a finding is an error. Without it the run is advisory and \
always exits 0, which is the default spec 6 fixes"
)]
strict: bool,
#[arg(
long,
help = "write the patch that rides with a finding, in this working tree. A finding \
carries one only when the fix is mechanical and total, and a finding an author \
suppressed carries none. Every patch is held against the bytes it names and \
the result is read back before it lands, so a file whose shape this engine \
guessed wrong is refused with nothing written. The report that follows is the \
run after the write, and the account of what was written goes to standard \
error. It exits non-zero on a refusal"
)]
fix: bool,
#[arg(
long = "no-cache",
help = "read and write no cache, and evaluate every instance. This run and a cached \
one write the same bytes to standard output, and a difference between them is \
a defect in the cache rather than a result"
)]
no_cache: bool,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today. Spec 12 \
makes the clock an injected value rather than a syscall inside a check, and \
this flag is where it is injected: same corpus, same lock, same date, same \
bytes"
)]
now: Option<Date>,
#[arg(
long,
value_name = "manifest",
help = "the manifest of the change this run is scoped to. The first line is \
`headwater change 1`, and a file that opens with anything else is refused \
rather than read. Each line after it names one document \
the change carries, as `added<tab><path>` or `prior<tab><path><tab><file>`, \
and the second form names a file holding the bytes that stood before the \
change. A document the manifest does not name did not change. It is what a \
rule that reads a transition needs, and without it every instance of such a \
rule is reported as skipped rather than passed. This engine walks no history: \
the caller anchors the prior version to the state on the branch where the \
change lands, which spec 12 fixes as the merge base of a proposed change and \
the committed `HEAD` of a working-tree hook. Every path is held against the \
corpus this run walks, and one that reaches no row of it is counted and named \
in the report rather than absorbed. No path is normalized, so `./docs/a.md` \
reaches no row. What this engine cannot check is whether the manifest tells \
the truth: a line that says `added` for a document that already stood, and a \
document the change carried and the manifest omits, are both invisible without \
the history that spec 12 rules out as an input"
)]
change: Option<PathBuf>,
#[arg(
long = "read-set",
value_name = "path",
help = "write the read set of this run to a file as well as to the report. The \
artifact is what decides whether a verdict survives a merge without running \
the checks again, and `headwater gate` is what reads it"
)]
read_set: Option<PathBuf>,
#[arg(
long,
value_name = "path",
help = "write the register of this run to a file as well as to the report. Spec 4 \
makes it a projection of the `obligations` and `controls` declarations, \
generated and never authored: every obligation with its disposition, every \
control with its health, and what escaped under each"
)]
register: Option<PathBuf>,
#[arg(
long,
value_name = "text|json|sarif|markdown",
help = concat!(
"which vocabulary to write the run in. `text` is the report a person reads and \
the default. `sarif` is what a forge ingests as a check run, `markdown` is a \
job summary or a review comment, and `json` is the finding shape spec 4 \
declares, for an adapter nobody here wrote. `sarif` writes its own loss set \
into the artifact. `markdown` declares one in the source and not in the \
artifact, because nothing it writes is machine-readable. `text` declares one \
drop there too, the routing of each skip, and carries the census and the graph \
that no other format holds. `json` declares one there as well, the \
per-document account of coverage, and writes no loss set of its own. ",
a_refusal_is_not_an_artifact!()
)
)]
format: Option<String>,
#[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
json: bool,
},
Change {
#[arg(
value_name = "base-rev",
help = "the revision to compare the working tree against. Spec 12 fixes two: the \
committed `HEAD` for a working-tree hook, and the merge base of a proposed \
change for a CI job. This verb runs no history walk beyond `git diff` and \
`git show` against this one revision"
)]
base: Option<String>,
#[arg(
value_name = "out-dir",
help = "the directory to write the manifest and the prior versions into. The caller \
owns this directory and removes it; this verb only ever creates inside it. \
The manifest's own path, `<out-dir>/manifest`, is printed on standard output, \
which is the file `headwater check --change` takes"
)]
out: Option<PathBuf>,
},
Gate {
#[arg(
long = "read-set",
value_name = "path",
help = "the read set to hold against this tree, and it is required here. \
`headwater check --read-set <path>` is what writes one. The artifact is what \
decides whether a verdict survives a merge without running the checks again"
)]
read_set: Option<PathBuf>,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the day the question is asked about, as `YYYY-MM-DD`. Defaults to today. A run \
that read the clock is void on any other day"
)]
now: Option<Date>,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Conformance {
#[arg(
long,
value_name = "name",
help = "the rung to ask about, by the name the package declares. It exits non-zero on \
a gap under that rung that no live waiver covers. It never moves the level the \
report states, which is computed from met rules alone"
)]
level: Option<String>,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
)]
now: Option<Date>,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Derived {},
MergeDriver {
#[arg(
value_name = "ancestor",
help = "the file git wrote the common ancestor's version into, `%O`. Not read"
)]
ancestor: Option<String>,
#[arg(
value_name = "current",
help = "the file holding the current side's version, `%A`. Left byte for byte, because git reads the merge result from it"
)]
current: Option<String>,
#[arg(
value_name = "other",
help = "the file holding the other side's version, `%B`. Not read"
)]
other: Option<String>,
#[arg(
value_name = "path",
help = "the path of the artifact in the tree, `%P`. It decides which producer the message names"
)]
path: Option<String>,
},
Route {
#[arg(
value_name = "task description",
help = "what you are about to do, in your own words. Every word after the verb is one \
description, so it needs no quoting to hold together"
)]
task: Vec<String>,
#[arg(
long,
value_name = "n",
value_parser = a_budget,
help = "how many ranked pointers it may offer. It never removes a document that \
governs a path the task named, and it says how many it withheld. Five by \
default"
)]
budget: Option<usize>,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Neighbors {
#[arg(
value_name = "task description",
help = "what you are about to do, in your own words. Every word after the verb is one \
description, so it needs no quoting to hold together"
)]
task: Vec<String>,
#[arg(
long,
value_name = "dir",
help = "the directory holding the fetched model files the pin in \
`.headwater/embedding.yml` names. `.headwater/models` by default"
)]
model: Option<PathBuf>,
#[arg(
long,
value_name = "n",
value_parser = a_budget,
help = "how many documents it prints, nearest first. Ten by default"
)]
top: Option<usize>,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Explain {
#[arg(
value_name = "path|identifier",
help = "the document to explain, as a path under the corpus root or as the identifier \
it declares"
)]
target: Option<String>,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Query {
#[arg(
value_name = "expression",
help = "the expression to run, and no document of this repository states what one is"
)]
expression: Vec<String>,
},
Capture {
#[arg(
long,
value_name = "text|json",
help = concat!(
"`text` is the report a person reads and the default, and `json` is the same \
numbers for a program. Neither carries a reading the store does not hold. ",
a_refusal_is_not_an_artifact!()
)
)]
format: Option<String>,
#[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
json: bool,
},
Mcp {
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today. It is read \
once and fixed for the life of the server, and every result states it"
)]
now: Option<Date>,
#[arg(
long,
help = "register the working-tree write class, which is `new` and `fix`. Spec 5 keeps \
it off by default, because a client may connect to a checkout that the user \
did not intend to change, so the consent is a word somebody typed rather than \
a setting a tree carries. A tool that lands a change is registered by no \
switch. The first call that moves a byte ends the server: it walked the corpus \
once, so every later answer would be about a tree that is gone"
)]
write: bool,
},
New {
#[arg(
value_name = "kind",
help = "the kind of document to scaffold, by the name the resolved taxonomy declares \
for it"
)]
kind: Option<String>,
#[arg(
long,
value_name = "text",
help = "what the document is called. Required, because the file name and the facet in \
the `name` role both come from it"
)]
title: Option<String>,
#[arg(
long,
value_name = "text",
help = "the one sentence a reader meets where a list of documents is rendered. \
Fills the facet in the `scent` role directly, exactly as `--title` fills the \
one in the `name` role. Without it the field carries a prompt, and a person \
edits the front matter by hand before the document is current"
)]
summary: Option<String>,
#[arg(
long,
value_name = "relation=identifier",
value_parser = a_pair,
help = "an edge to propose, as a relation and the identifier of the document at the \
other end. Repeatable. It is refused unless the taxonomy declares \
`created_by: scaffold` on the relation, unless both ends are kinds the \
relation permits, and unless the target resolves. Where reciprocity is \
required and the new document opens at an initial state, the far half is \
owed until the document leaves that state, and `headwater check --fix` \
writes it then. In every other case the far half is written into the \
target document at once"
)]
relates: Vec<(String, String)>,
#[arg(
long,
value_name = "facet=value",
value_parser = a_pair,
help = "a value for a facet this kind requires, as `<facet>=<value>`. Repeatable. A \
facet the kind does not require is refused, and so is a value outside a \
closed set, with the set printed. A facet that a declaration decides is \
refused too: an engine role decides its facet's value, and the kind decides \
the discriminator of a heterogeneous shelf"
)]
facet: Vec<(String, String)>,
#[arg(
long,
value_name = "path",
help = "the directory to write into, relative to the root, for a kind whose shelf \
fixes the file name under a glob, such as `docs/modules/*/README.md`. The \
document is written as `<path>/` and the file name the shelf fixes, and must match the \
shelf. Refused on any other shelf, whose path already decides the directory"
)]
directory: Option<String>,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date the document is stamped with, as `YYYY-MM-DD`. Defaults to today"
)]
now: Option<Date>,
},
Infer {
#[arg(
long,
value_name = "name",
help = "who owns the debt it proposes. Required with `--write`, because an owner is \
the field that ranks declared debt above a suppression and this engine will \
not invent one"
)]
owner: Option<String>,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the last day the tasks it proposes hold, as `YYYY-MM-DD`. Ninety days out by \
default"
)]
until: Option<Date>,
#[arg(
long,
help = "put the payload in the lock, which is committed and reviewed. Without it \
nothing is written"
)]
write: bool,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
)]
now: Option<Date>,
},
Generate {
#[arg(
long,
help = "write nothing and exit non-zero when what is committed is not what a run \
produces. It reads the corpus through the lock, so it answers whether a \
derived artifact is current"
)]
check: bool,
},
Import {
#[arg(
value_name = "name",
help = "which declared import to read, by the name its block carries in \
`.headwater/taxonomy.yml`. One declared import needs no name and two do, \
because choosing for the caller would import whichever the file listed first"
)]
name: Option<String>,
#[arg(
long,
value_name = "digest",
help = "the digest to check the snapshot against. It defaults to the `digest` of the \
import block in `.headwater/taxonomy.yml`, and the verb refuses when neither \
is there rather than reading an unpinned directory"
)]
expect: Option<String>,
#[arg(
long,
help = "write the edge halves into the documents at their near ends. Without it the \
edges are reported and nothing is touched"
)]
write: bool,
},
Export {
#[arg(
long,
value_name = "name",
help = "which declared export profile to emit. Every declared profile by default, so \
a filtered audience is never omitted by accident"
)]
profile: Option<String>,
#[arg(
long,
value_name = "json|jsonschema",
help = concat!(
"the emitter target. `json` is the native property graph with no loss and \
`jsonschema` constrains front matter. The other five targets of spec 6 parse \
and report the consumer each one waits on. With this flag the artifact goes to \
standard output and no declared output path is touched. ",
a_refusal_is_not_an_artifact!()
)
)]
format: Option<String>,
#[arg(
long,
value_name = "date",
value_parser = a_date_as_written,
help = "the generation time the artifact states, as `YYYY-MM-DD`. Absent by default, \
because an artifact that `--check` compares by byte cannot carry a clock \
reading. Spec 6 asks a filtered export that leaves the repository to state \
one, and this is where it is injected"
)]
at: Option<String>,
#[arg(
long,
help = "write nothing and exit non-zero when a declared output is not what a run \
produces. It holds every export the taxonomy names a path for to regeneration"
)]
check: bool,
#[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
json: bool,
},
Sweep {
#[command(subcommand)]
word: Option<SweepWord>,
},
Probe {
#[command(subcommand)]
word: Option<ProbeWord>,
},
Init {
#[arg(
long,
value_name = "dir",
help = "the corpus root to declare. Proposed from the tree by default"
)]
corpus: Option<String>,
#[arg(
long,
value_name = "name",
help = "the package to take. `headwater/standard` by default"
)]
package: Option<String>,
#[arg(
long,
help = "append a `-merge` line to `.gitattributes` for each fold \
`headwater taxonomy resolve` and `headwater generate` write in this tree, and \
print the two `git config` lines that name `headwater merge-driver` and the \
`info/attributes` lines that select it. A clone that already names the driver \
gets those lines written. It runs after the first `headwater generate`, and on \
a repository that is already bound it does this and nothing else"
)]
git: bool,
#[arg(
long = "git-config",
requires = "git",
help = "also run the two `git config` lines `--git` prints, in this clone, and then \
write the `info/attributes` lines that select the driver. Git takes no driver \
from a repository, so without this flag the lines are printed and the adopter \
runs them"
)]
git_config: bool,
},
Taxonomy {
#[command(subcommand)]
word: Option<TaxonomyWord>,
},
Json {
#[command(subcommand)]
word: Option<JsonWord>,
},
Help {
#[arg(
value_name = "verb",
help = "the verb to describe, with its second word where it takes one: \
`headwater help taxonomy diff`. Without one this screen is printed"
)]
verb: Vec<String>,
},
Completions {
#[arg(
value_name = "shell",
help = "the shell to write a script for. A name outside the four is refused with the \
four printed, and no script is written"
)]
shell: Option<Shell>,
},
#[command(external_subcommand)]
Other(Vec<String>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
pub enum Shell {
Bash,
Zsh,
Fish,
Powershell,
}
impl From<Shell> for clap_complete::Shell {
fn from(shell: Shell) -> Self {
match shell {
Shell::Bash => clap_complete::Shell::Bash,
Shell::Zsh => clap_complete::Shell::Zsh,
Shell::Fish => clap_complete::Shell::Fish,
Shell::Powershell => clap_complete::Shell::PowerShell,
}
}
}
impl Shell {
pub fn typed(self) -> &'static str {
match self {
Shell::Bash => "bash",
Shell::Zsh => "zsh",
Shell::Fish => "fish",
Shell::Powershell => "powershell",
}
}
pub const ALL: &'static [Shell] = &[Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Powershell];
}
#[derive(Subcommand, Debug)]
pub enum JsonWord {
Field {
#[arg(
value_name = "key",
help = "the path of steps to the member, outermost first. A step into an object \
is a key, and a step into an array is a decimal index counted from 0. \
`headwater json field tool_input file_path` reads the `file_path` member of \
the `tool_input` member, and `headwater json field related 0 target` reads \
the `target` member of the first element of `related`. Without one, the object is read and no member of it \
is named, which is refused"
)]
path: Vec<String>,
},
Count {
#[arg(
value_name = "key",
help = "the path of steps to the array or the object whose elements are counted, \
outermost first, where a step into an array is a decimal index. Without one, the object on standard input is the one counted"
)]
path: Vec<String>,
},
Quote,
#[command(external_subcommand)]
Other(Vec<String>),
}
#[derive(Subcommand, Debug)]
pub enum SweepWord {
Plan {
#[arg(
long,
value_name = "path",
help = "the slice, as a path prefix under the repository root. The whole corpus by \
default. There is no sampling rule here: a slice this engine picked would be \
an unreproducible sample dressed as a reproducible one, and the plan reports \
its own extent instead"
)]
under: Option<String>,
},
Report {
#[arg(
value_name = "path",
help = "the file an agent wrote back. `headwater sweep plan` prints the shape of it"
)]
path: Option<String>,
#[arg(
long,
value_name = "text|json",
help = concat!(
"`text` is the report a person reads and the default, and `json` is the finding \
shape spec 4 declares with the provenance and the evidence a sweep adds. ",
a_refusal_is_not_an_artifact!()
)
)]
format: Option<String>,
#[arg(long, conflicts_with = "format", help = JSON_BESIDE_FORMAT)]
json: bool,
},
#[command(external_subcommand)]
Other(Vec<String>),
}
#[derive(Subcommand, Debug)]
pub enum ProbeWord {
Plan {
#[arg(
long,
value_name = "regression|campaign",
help = "which tier of `.headwater/probe.yml` to plan against. A tier declares the \
ceiling, the session cost, the repetitions and the arms, and the plan is \
projected against all four. `regression` by default"
)]
tier: Option<String>,
#[arg(
long,
value_name = "present|absent",
help = "narrow the selection to one arm the tier declares. Every arm the tier \
declares by default, which is one for `regression` and two for `campaign`. \
An arm the tier does not declare refuses the run rather than planning \
another one, and the refusal names the arms the tier declares"
)]
arm: Option<String>,
#[arg(
long,
value_name = "name",
help = "narrow the selection to one probe category, by the name this engine declares \
for it. Every category by default, a name outside the closed set is refused \
with the set printed, and a category no probe of this corpus carries is \
refused rather than planned as a run of nothing"
)]
category: Option<String>,
#[arg(
long,
value_name = "n",
default_value_t = 0,
help = "the rotation seed, which is a member of the run identity spec 5 declares. It \
is the caller's number: a run that states none states zero, and it is \
recorded as stated. No selection is drawn from it — every declared probe is \
selected — so it identifies a run rather than choosing one"
)]
seed: u64,
},
Record {
#[arg(
value_name = "path",
help = "the transcript a recorder wrote. `headwater probe plan` prints the run \
identity it has to carry"
)]
path: Option<String>,
},
Grade {
#[arg(
value_name = "path",
help = "the transcript a recorder wrote. It is graded against the probes this corpus \
declares, re-derived here rather than taken from the transcript"
)]
path: Option<String>,
},
Stale,
#[command(external_subcommand)]
Other(Vec<String>),
}
#[derive(Subcommand, Debug)]
pub enum TaxonomyWord {
Validate,
Resolve {
#[arg(
long,
help = "write nothing and exit non-zero when what is committed is not what a run \
produces. It reads the taxonomy sources, so it answers whether the lock is \
current"
)]
check: bool,
},
Audit {
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date a staleness reading and a dwell reading are taken at, as \
`YYYY-MM-DD`. Defaults to today, and two audits of one tree at one date write \
the same bytes"
)]
now: Option<Date>,
#[arg(
long,
help = "append this run's adoption reading to `.headwater/adoption.jsonl`. Without it \
the verb writes nothing. A reading the store already holds at this lock and \
this date is not appended twice, so two recorded audits of one tree at one \
date still write the same bytes"
)]
record: bool,
},
Publish {
#[arg(
long,
value_name = "name",
help = "the package to publish. The one this repository's own declaration takes, by \
default, because a publisher usually publishes what it also consumes. Refused \
together with `--from`, which names the same thing by its directory instead"
)]
package: Option<String>,
#[arg(
long,
value_name = "dir",
help = "read the manifest at this directory directly, bypassing the lookup by name \
under `.headwater/packages/` that `--package` drives. For a repository that both \
publishes a package and consumes it: `taxonomy vendor` refuses to install over \
a directory that carries no release record, so a maintained source cannot sit \
where its own artifact would be installed. This reads it from wherever it \
actually sits instead"
)]
from: Option<PathBuf>,
#[arg(
long,
value_name = "name",
help = "derive and publish this named assembly from the source package. It uses the \
same source selection as `--package` or `--from`, and produces one flattened \
package with no runtime bundle selection"
)]
assembly: Option<String>,
#[arg(
long,
value_name = "dir",
help = "where to write the artifact. The directory must be empty or absent, because a \
published artifact is every file under its root and a stray one would be a \
member the publisher never shipped. A run that cannot finish leaves it as it \
found it, so a second run meets the same precondition the first one did"
)]
out: Option<PathBuf>,
#[arg(
long,
help = "remove what a publish killed part-way left at `--out`, and publish in the \
same run. It removes one state and nothing else: files at `--out` with no \
release record, beside a `<out>~staging` directory holding both the files a \
publish writes there to say it could not move the artifact into place and is \
writing into `--out` one file at a time. Only a killed publish leaves those \
two together, and the second one names the output path it was writing. A \
directory holding anything else, and an `--out` that carries a release \
record, are left exactly as they are and the publish refuses as it does \
without this flag"
)]
clear_killed: bool,
#[arg(
long,
help = "write nothing, and exit 1 when the vendored copy under `.headwater/packages/` \
is not what a fresh publish of the source at `--from` produces. It publishes \
into a private directory outside the tree, removes it, and names each member \
that moved. For a repository that maintains a package and also consumes it, \
so that a source changed without a republish fails a gate. Needs `--from`, \
and is refused with `--out`, `--package`, `--assembly`, `--clear-killed` and \
`--json`"
)]
check: bool,
#[arg(long, help = JSON_ALONE)]
json: bool,
},
Vendor {
#[arg(
value_name = "dir-or-location",
help = "the directory of an artifact somebody already fetched, or the https:// \
location of a published artifact zip, which this verb fetches through \
`headwater-fetch`, a crate nothing in the checking loop links (HW-DR-0075)"
)]
path: Option<String>,
#[arg(
long,
value_name = "digest",
help = "the digest to check the artifact against. It defaults to `taxonomy.digest` in \
`.headwater/taxonomy.yml`, and the verb refuses when neither is there. A pin \
the engine took from the artifact in front of it would be a pin against itself"
)]
expect: Option<String>,
},
Diff {
#[arg(
value_name = "dir",
help = "the directory of an artifact somebody already fetched. This verb opens no \
socket, so it takes a path and never a location"
)]
path: Option<String>,
#[arg(
long,
value_name = "version",
help = "the version the artifact is expected to be, written as a version or as a \
range: `4.0.0`, or `>=4 <5` with the quoting your shell needs. This verb \
fetches nothing, so the directory decides which artifact is compared and this \
flag holds it to what the caller meant. It is read by the one range reader \
the engine has, which is what reads `requires_engine`"
)]
to: Option<String>,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
)]
now: Option<Date>,
},
Migrate {
#[arg(
value_name = "dir",
help = "the directory of an artifact somebody already fetched. This verb opens no \
socket, so it takes a path and never a location"
)]
path: Option<String>,
#[arg(
long,
value_name = "version",
help = "the version the artifact is expected to be, written as a version or as a \
range: `4.0.0`, or `>=4 <5` with the quoting your shell needs"
)]
to: Option<String>,
#[arg(
long,
help = "write the files each step names. Without it every file each step would write \
is reported and nothing is written"
)]
apply: bool,
#[arg(
long,
value_name = "date",
value_parser = a_date,
help = "the date to evaluate against, as `YYYY-MM-DD`. Defaults to today"
)]
now: Option<Date>,
},
Graph {
#[arg(
long,
value_enum,
default_value_t,
help = "which drawing to print. `concrete` draws the concrete kinds, the anchors and \
the relations between them. `abstract` draws each abstract kind, the kinds \
declared under it and the relations that name it, which `concrete` leaves out"
)]
view: crate::taxonomy_graph::View,
#[arg(
long,
help = "add a key that draws each shape and each edge style the drawing uses, and \
names the family each edge color stands for"
)]
legend: bool,
},
#[command(external_subcommand)]
Other(Vec<String>),
}
pub fn command() -> Command {
command_at(paint::width())
}
pub fn command_at(width: usize) -> Command {
command_in(width, paint::stdout_color())
}
pub fn command_in(width: usize, mode: paint::ColorMode) -> Command {
let mut root = Cli::command()
.about(format!(
"{} — {}",
headwater_verbs::BINARY,
headwater_verbs::TAGLINE
))
.color(paint::color_choice(mode))
.styles(paint::help_styles(mode))
.help_template(first_screen(width, mode));
for verb in headwater_verbs::VERBS {
if root.find_subcommand(verb.name).is_some() {
root = root.mut_subcommand(verb.name, |one| described(one, verb, width, mode));
}
}
paint::painted(root, width)
}
pub fn parsed() -> Result<Cli, clap::Error> {
let matches = command().try_get_matches()?;
if let Some(message) = a_width_for_a_run_that_lays_nothing_out(&matches) {
return Err(clap::Error::raw(
clap::error::ErrorKind::ArgumentConflict,
message,
));
}
Cli::from_arg_matches(&matches)
}
fn a_width_for_a_run_that_lays_nothing_out(matches: &clap::ArgMatches) -> Option<String> {
let mut leaf = matches;
while let Some((_, inner)) = leaf.subcommand() {
leaf = inner;
}
if leaf.try_get_one::<bool>("wide").ok().flatten() != Some(&true) {
return None;
}
if matches.subcommand_name() == Some("help") {
return None;
}
let format = leaf.try_get_one::<String>("format").ok().flatten();
let json = leaf.try_get_one::<bool>("json").ok().flatten() == Some(&true);
let laid_out = matches.subcommand_name() == Some("check")
&& !json
&& matches!(format.map(String::as_str), None | Some("text"));
if laid_out {
return None;
}
let says = match format.filter(|value| value.as_str() != "text") {
Some(format) => format!("`--format {format}` writes an artifact that nothing lays out"),
None if json => "`--json` writes an artifact that nothing lays out".to_string(),
None => "this run lays nothing out".to_string(),
};
Some(format!(
"`--wide` says how wide the help and the report of `{0} check` are laid out, and {says}. A \
run carrying it would carry one flag that does nothing, so it is refused rather than run. \
The runs it widens are `{0} --wide --help`, `{0} <verb> --wide --help`, `{0} --wide help \
<verb>` and `{0} check --wide`",
headwater_verbs::BINARY
))
}
fn described(
command: Command,
verb: &headwater_verbs::Verb,
width: usize,
mode: paint::ColorMode,
) -> Command {
let mut one = command.about(verb.description);
if !verb.words.is_empty() {
one = one.help_template(second_words(verb, width, mode));
for word in verb.words {
if one.find_subcommand(word.name).is_none() {
continue;
}
one = one.mut_subcommand(word.name, |inner| inner.about(word.description));
}
}
one
}
const COLUMN: usize = 15;
fn first_screen(width: usize, mode: paint::ColorMode) -> String {
let mut out = format!(
"{{usage-heading}} {{usage}}\n\n{}:\n",
paint::paint(paint::Role::Heading, "Examples", mode)
);
for (line, says) in [
(
"headwater check --strict",
"run the checks, and fail on an error",
),
(
"headwater route \"add rate limiting\"",
"the documents that govern a task",
),
(
"headwater explain HW-DR-0033",
"why a document is the kind it is",
),
(
"headwater new decision --title \"Adopt an overlay\"",
"scaffold a document of a kind",
),
(
"headwater help taxonomy diff",
"the long description of one verb",
),
] {
out.push_str(&format!(" {line}\n"));
out.push_str(&paint::fold_indented(says, width, 6));
}
for group in headwater_verbs::groups() {
out.push_str(&format!(
"\n{}:\n",
paint::paint(paint::Role::Heading, group, mode)
));
for verb in headwater_verbs::VERBS
.iter()
.filter(|one| one.group == group)
{
out.push_str(&paint::painted_row(
verb.name,
verb.summary,
COLUMN,
width,
paint::Role::Verb,
mode,
));
}
}
let longest = GLOBALS
.iter()
.map(|one| one.name.chars().count())
.max()
.unwrap_or(0);
let at = 2 + longest + 2;
out.push_str(&format!(
"\n{}:\n",
paint::paint(paint::Role::Heading, "Global flags", mode)
));
for one in GLOBALS {
out.push_str(&paint::painted_row(
one.name,
one.summary,
at,
width,
paint::Role::Path,
mode,
));
}
out.push('\n');
out.push_str(&paint::fold_indented(
&format!(
"Run `{0} help <verb>` for the long description of one verb, or `{0} <verb> --help`.",
headwater_verbs::BINARY
),
width,
0,
));
out
}
fn second_words(verb: &headwater_verbs::Verb, width: usize, mode: paint::ColorMode) -> String {
let mut out = String::from("{about}\n\n{usage-heading} {usage}\n\nSecond words:\n");
for word in verb.words {
out.push_str(&paint::painted_row(
word.name,
word.summary,
COLUMN,
width,
paint::Role::Verb,
mode,
));
}
out.push_str("\nFlags:\n{options}\n\n");
out.push_str(&paint::fold_indented(
&format!(
"Run `{} help {} <word>` for the long description of one.",
headwater_verbs::BINARY,
verb.name
),
width,
0,
));
out
}
fn a_date(text: &str) -> Result<Date, String> {
Date::parse(text).ok_or_else(|| "a date written `YYYY-MM-DD`".to_string())
}
fn a_date_as_written(text: &str) -> Result<String, String> {
a_date(text).map(|_| text.to_string())
}
fn a_budget(text: &str) -> Result<usize, String> {
match text.parse::<usize>() {
Ok(value) if value > 0 => Ok(value),
_ => Err("a whole number above zero".to_string()),
}
}
fn a_pair(text: &str) -> Result<(String, String), String> {
match text.split_once('=') {
Some((left, right)) if !left.is_empty() && !right.is_empty() => {
Ok((left.to_string(), right.to_string()))
}
_ => Err(
"`<left>=<right>`, as in `--relates supersedes=HW-DR-0007` or \
`--facet probe_category=discovery`"
.to_string(),
),
}
}
pub fn headline(error: &clap::Error) -> String {
let rendered = error.render().to_string();
let head: Vec<String> = rendered
.lines()
.take_while(|line| !line.starts_with("Usage:") && !line.starts_with("For more information"))
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| line.strip_prefix("error: ").unwrap_or(line).to_string())
.collect();
match head.is_empty() {
true => rendered.split_whitespace().collect::<Vec<_>>().join(" "),
false => head.join("\n"),
}
}
#[cfg(test)]
mod tests {
use super::{a_budget, a_date, a_pair, command, headline, Cli};
use clap::{CommandFactory, Parser};
#[test]
fn the_declared_parse_is_a_command_clap_can_build() {
Cli::command().debug_assert();
command().debug_assert();
}
#[test]
fn a_value_a_flag_cannot_take_is_named_rather_than_defaulted() {
assert!(a_date("2026-13-45").is_err());
assert!(a_date("2026-01-01").is_ok());
assert!(a_budget("0").is_err());
assert!(a_budget("x").is_err());
assert_eq!(a_budget("3"), Ok(3));
assert!(a_pair("nope").is_err());
assert!(a_pair("=right").is_err());
assert!(a_pair("left=").is_err());
assert_eq!(
a_pair("supersedes=HW-DR-0007"),
Ok(("supersedes".to_string(), "HW-DR-0007".to_string()))
);
}
#[test]
fn a_refusal_carries_the_message_and_not_the_grammar() {
let error = Cli::try_parse_from(["headwater", "check", "--nonsense"])
.expect_err("`--nonsense` is not a flag `check` reads");
let headline = headline(&error);
assert!(
headline.contains("--nonsense"),
"the refusal names the offending word: {headline}"
);
assert!(
!headline.contains("--root <path>"),
"the refusal does not reprint the grammar: {headline}"
);
assert!(
!headline.contains("Usage:"),
"the refusal does not reprint the usage block: {headline}"
);
assert!(
!headline.contains("For more information"),
"the pointer is `fail`'s and is written once: {headline}"
);
}
#[test]
fn a_flag_of_another_verb_does_not_reach_this_one() {
assert!(Cli::try_parse_from(["headwater", "check", "--level", "L0"]).is_err());
assert!(Cli::try_parse_from(["headwater", "conformance", "--level", "L0"]).is_ok());
assert!(Cli::try_parse_from(["headwater", "sweep", "report", "f", "--strict"]).is_err());
assert!(Cli::try_parse_from(["headwater", "check", "--strict"]).is_ok());
}
#[test]
fn the_corpus_flag_reaches_every_verb_from_either_side_of_it() {
for arguments in [
["headwater", "check", "--root", "/tmp"],
["headwater", "--root", "/tmp", "check"],
] {
let cli = Cli::try_parse_from(arguments).expect("`--root` is global");
assert_eq!(cli.root.as_deref(), Some(std::path::Path::new("/tmp")));
}
}
}