ADR-0039: CLI DevEx pass 2 — completions distribution, drift-in-CI, format consistency
Date: 2026-06-30 / Status: Accepted (no wire-contract / golden / ABI change; the
fmf remit is unchanged — still a developer/diagnostic tool,
ADR-0026). Decision 1’s bundling half is
superseded: the end-user ZIP ships neither fmf.exe nor completion scripts,
because the CLI is not an end-user surface. The fmf completions <shell>
subcommand — the always-fresh half of that decision — is the remaining
distribution path, and the rest of this ADR stands.
Context
ADR-0026 brought fmf to a first-class developer CLI (--version, --color/-q/--format, FMF_E_* exit codes, a versioned JSON envelope, generated completions + docs/cli.md). A second audit against industry-standard CLI ergonomics found gaps — some where the documentation claimed a behaviour the implementation never had:
- Completions were not actually distributed. ADR-0026 and the
codegenexample both said completions were “bundled at release time”, but neitherxtask publishnorpackagecopiedbuild/completions/into the bundle — and there were no install instructions anywhere. - Generated CLI Markdown duplicated live help. Keeping a generator, committed output, and drift test added machinery without serving a distinct consumer.
--format jsonwas inconsistent.statsignored--formatentirely (it always dumped pretty JSON, even in human mode, as several separate documents);io-probe,spikeandcriterion-gatedid not even receive the format context, so--format jsonwas silently ignored.- Help was thin. Positional
drivearguments had no help on any command, there were no usage examples,long_aboutwas unused, andwrap_helpwas off (long help did not wrap to the terminal). - A confusing flag name.
bench --json <path>(write a report file) collided with the global--format json(stream to stdout). - No CLI control of log level. The level was hard-coded to
info; verbosity could only be raised via theFMF_LOGenv var.
Decision
A second polish pass, entirely within ADR-0026’s remit (no fmf search, no TUI, no new engine seams; the clap surface stays logic-free).
- Completions: distributed + on-demand subcommand. A new
fmf completions <shell>subcommand prints a completion script to stdout (the gh/rustup pattern:eval "$(fmf completions bash)"), rendered from the singlecommand()tree.clap_completemoves from a dev-dependency to a normal one.xtask publishnow shipscompletions/{fmf.bash,_fmf,fmf.fish,_fmf.ps1}by invoking the just-builtapp/fmf.exe completions <shell>— so the bundled scripts are produced by the exact binary they ship beside and cannot drift. Install steps are documented in the repo README and the bundledREADME.txt. - No generated CLI Markdown.
fmf --helpis the live reference; the generator, committed copy, and drift test are removed. --format jsoneverywhere. Every result-producing command honours--format:statsemits one combinedformat_version-stamped document in json mode (human keeps the per-column dump);io-probe,spikeandcriterion-gatereceiveCtxand emit JSON when asked. The interactiveindexREPL andcompletionsare text-only by nature.- Help quality.
drivehelp on every command, help on the remainingio-probeflags, a rootlong_about(stating this is a developer/diagnostic tool — the product is the WinUI app) and anafter_helpexamples block, and the clapwrap_helpfeature. bench --json <path>→bench --out <path>, removing the collision with the global--format json. (just bench-baselineupdated.)-v/--verbose(repeatable) maps toinfo/debug/trace;FMF_LOGstill overrides it (init_diag).
These are additive to the JSON envelope (format_version unchanged).
Rationale
- Generate the bundled completions from the shipped binary: any other source (the
codegenexample, a committed copy) could drift from the binary’s real surface;fmf.exe completionscannot. - Live help over generated Markdown: one executable surface cannot drift from itself and needs no documentation-only dependency.
--format jsonconsistency over “not every command has JSON”: a flag that is silently ignored is a worse experience than one that always means the same thing; the dev/measurement commands all have structured results worth emitting.completionssubcommand AND bundled files: the subcommand is the portable, always-fresh path (and what power users expect); the bundled files mean a downloaded copy needs nothing built to install completions.
Rejected alternatives
- A
manpage (clap_mangen). Rejected: find-my-files is Windows-only and ships no man reader, so a man page would have no consumer. Following the Unix convention here would add a build artdefact nobody can use — the project’s dsa-first discipline says evaluate and decline, not follow blindly. - A committed generated CLI reference. Rejected: it duplicates
fmf --helpand requires generator/drift machinery with no separate consumer. - Erroring on
--format jsonfor commands without a JSON form. Rejected in favour of actually giving every result-producing command a JSON form — the consistent, less surprising outcome. - Reviving
fmf search/ a TUI. Out of scope here; still governed by ADR-0026’s deferral (needs its own ADR + a pipe client).
Re-examination triggers
- If a command’s JSON shape needs to change meaning (not just add fields), bump
format_version(ADR-0026). - If completion scripts grow shell-specific install complexity, consider a
fmf completions --installhelper. - If the CLI ever needs to be a scriptable end-user search surface, that remains an ADR-0026 question (
fmf searchvia a pipe client, new ADR) — not a DevEx-polish change.