docs(specs): add header-driven --help design
Design for making the man-page-style comment header above each function reachable at runtime via -h/--help, instead of hand-writing 40 more menus. One renderer (__fish_help_header) parses the .fish source at call time and prints the header to stdout; each call site is a single line. Audits all 79 published functions that lack help today into 35 shadow/pass-through exempt, 4 prompt-hook exempt, and 40 to convert, with a one-line reason each. Corrects the baseline counts (109 published functions, 30 with menus, 79 without) and records two pre-existing defects found while auditing. Note: AGENTS/ is gitignored upstream, so this file is force-added.
This commit is contained in:
@@ -0,0 +1,446 @@
|
|||||||
|
# Design: Header-Driven `--help`
|
||||||
|
|
||||||
|
**Date:** 2026-09-07
|
||||||
|
**Status:** Proposed
|
||||||
|
**Job:** 3 of the parallel improvement effort (fork of `08e66c8`, branch `work`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`functions/CLAUDE.md` requires every user-facing function to accept `-h`/`--help`
|
||||||
|
and print a formatted menu to stdout, "unless it strictly wraps another tool's
|
||||||
|
help." Most do not.
|
||||||
|
|
||||||
|
The documentation those menus would contain already exists. Every documented
|
||||||
|
function carries a man-page-style comment header — `SYNOPSIS`, `DESCRIPTION`,
|
||||||
|
`ARGUMENTS`, `EXIT STATUS`, `RETURNS`, `EXAMPLE`, `NOTES` — which
|
||||||
|
`2026-07-26-function-headers-as-ssot-design.md` established as the SSOT for
|
||||||
|
Section 5 of the manual, and which `docs/verify-manual.py` already enforces.
|
||||||
|
|
||||||
|
The header is written; it is simply not reachable at runtime. This spec makes it
|
||||||
|
reachable with one renderer function and one line per call site. It authors no
|
||||||
|
new documentation prose, and it changes nothing in the docs build pipeline.
|
||||||
|
|
||||||
|
### Why not write the menus
|
||||||
|
|
||||||
|
Writing a menu per function creates a second copy of facts the header already
|
||||||
|
holds, in a place no verifier checks. The 40 functions in scope would cost
|
||||||
|
roughly 600 lines of `echo` that immediately begin drifting from the header that
|
||||||
|
generates the manual. The 30 functions that already have hand-rolled menus are
|
||||||
|
the evidence: several of them already say things their headers do not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Measured Baseline
|
||||||
|
|
||||||
|
Taken 2026-09-07 against `functions/` at `08e66c8`. Counts come from
|
||||||
|
`manualtools.parse_functions()`, not from grep, so they match what the manual
|
||||||
|
actually publishes.
|
||||||
|
|
||||||
|
| Fact | Value |
|
||||||
|
|---|---|
|
||||||
|
| `.fish` files in `functions/` | 185 |
|
||||||
|
| Files with a `# CATEGORY` comment | 110 |
|
||||||
|
| Functions the parser actually publishes | **109** |
|
||||||
|
| ...that print their own help menu today | **30** |
|
||||||
|
| ...that do not | **79** |
|
||||||
|
| Of those 79: exempt (see §4) | 39 |
|
||||||
|
| Of those 79: to convert | 40 |
|
||||||
|
| `fish tests/run-tests.fish` | 317/317 pass |
|
||||||
|
| `python3 docs/verify-manual.py` | 74/74 pass |
|
||||||
|
|
||||||
|
The job brief quoted 110 / 34 / 78. Three corrections, all verified:
|
||||||
|
|
||||||
|
- **110 → 109.** `zoxide.fish` carries two `# CATEGORY` blocks, but both resolve
|
||||||
|
to `_zoxide_*` names, which `parse_functions` drops as private. It publishes
|
||||||
|
nothing.
|
||||||
|
- **34 → 30.** The grep for `--help` counted `fisher.fish` and
|
||||||
|
`_fzf_configure_bindings_help.fish` (neither is a published function), and
|
||||||
|
`jr.fish` and `yt-dlp.fish`, which mention `--help` only inside their comment
|
||||||
|
headers and do not handle it in the body.
|
||||||
|
- **78 → 79.** `split` matched the grep because it uses `-h` for
|
||||||
|
`--horizontal`. It has no help menu, and its `-h` is spoken for. It belongs in
|
||||||
|
the target set as a special case (§4.3).
|
||||||
|
|
||||||
|
### Pre-existing defects found, not fixed here
|
||||||
|
|
||||||
|
- **`dops.fish` defines `function docker`, not `dops`.** The single header block
|
||||||
|
in that file resolves to the file stem, so the manual publishes an entry for
|
||||||
|
`dops` — a function that does not exist — while `docker` is undocumented. The
|
||||||
|
renderer resolves this correctly by accident (§3.2), but the manual entry
|
||||||
|
stays wrong. Out of scope: fixing it means authoring header content.
|
||||||
|
- **`sponge_filter_secrets.fish`** has a blank line between its header block and
|
||||||
|
its `function` line. The renderer tolerates it (§3.2); noted because it is the
|
||||||
|
only file that does.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Requirements
|
||||||
|
|
||||||
|
1. Every non-exempt published function responds to `-h` and `--help` with its own
|
||||||
|
documentation on **stdout**, exit 0.
|
||||||
|
2. Functions that pass `--help` through to a wrapped tool keep doing so.
|
||||||
|
3. `fish tests/run-tests.fish` stays at 317/317 plus the new checks.
|
||||||
|
4. `python3 docs/verify-manual.py` passes unchanged.
|
||||||
|
5. A test fails when a new user-facing function ships without help.
|
||||||
|
6. The docs build pipeline is not modified.
|
||||||
|
7. No header documentation content is added or rewritten.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Renderer: `__fish_help_header`
|
||||||
|
|
||||||
|
One new autoloaded function, `functions/__fish_help_header.fish`.
|
||||||
|
|
||||||
|
```fish
|
||||||
|
__fish_help_header <name> [args...]
|
||||||
|
```
|
||||||
|
|
||||||
|
It returns **1 — and prints nothing — only when `args[1]` is not a help flag.**
|
||||||
|
In every other case it prints something useful to stdout and returns 0. That
|
||||||
|
asymmetry is the safety-critical invariant: a return of 1 means "carry on with
|
||||||
|
the real work", so a parse failure must never produce it. `upgrade --help`
|
||||||
|
falling through to `paru -Syu --noconfirm` is the failure this rule exists to
|
||||||
|
prevent.
|
||||||
|
|
||||||
|
### 2.1 Call site
|
||||||
|
|
||||||
|
One line, as the **first statement of the function body**, above any opinionated
|
||||||
|
guard:
|
||||||
|
|
||||||
|
```fish
|
||||||
|
function poke --description 'touch with automatic parent directory creation'
|
||||||
|
__fish_help_header (status current-function) $argv; and return 0
|
||||||
|
...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
`status current-function` is evaluated in the caller's scope, so it yields the
|
||||||
|
caller's name. Placing the line above the C1/C4 guard means documentation stays
|
||||||
|
reachable when a component is disabled, and guarantees nothing side-effecting
|
||||||
|
runs before the flag is inspected.
|
||||||
|
|
||||||
|
The `; and return 0` form leaves `$status` at 1 when no help flag was passed.
|
||||||
|
No function in scope reads `$status` as its first act, so this is inert — but
|
||||||
|
new call sites must not be added above a `$status` read.
|
||||||
|
|
||||||
|
### 2.2 First-argument-only interception
|
||||||
|
|
||||||
|
The renderer fires only when **`$argv[1]`** is `-h` or `--help`. Not anywhere in
|
||||||
|
`$argv`.
|
||||||
|
|
||||||
|
This is not a simplification, it is a correctness requirement. `bkg`,
|
||||||
|
`wake-lock`, `split` and `spwin` take a command to run as their arguments.
|
||||||
|
Scanning all of `$argv` would make `wake-lock rsync --help` print `wake-lock`'s
|
||||||
|
help instead of running rsync. First-argument-only is also the pattern
|
||||||
|
`jobrunner` and `rand_string` already use.
|
||||||
|
|
||||||
|
The 30 existing argparse-based menus keep accepting `--help` in any position.
|
||||||
|
This spec does not touch them (§5).
|
||||||
|
|
||||||
|
### 2.3 Output format
|
||||||
|
|
||||||
|
Matches `config-help`, the richest and most recent house menu:
|
||||||
|
|
||||||
|
```
|
||||||
|
<name> bold
|
||||||
|
|
||||||
|
USAGE bold brblue
|
||||||
|
<SYNOPSIS lines> two-space indent, deeper indent preserved
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
...
|
||||||
|
|
||||||
|
ARGUMENTS
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
- `SYNOPSIS` renders as `USAGE`; `EXAMPLE` renders as `EXAMPLES`. Every other
|
||||||
|
label renders verbatim.
|
||||||
|
- `CATEGORY`, `COMPONENT` and `DEPENDENCIES` are build metadata and are skipped.
|
||||||
|
- `EXIT STATUS`, `RETURNS` and `NOTES` render when present.
|
||||||
|
- Blank lines inside a section are preserved (20 headers have multi-paragraph
|
||||||
|
`DESCRIPTION`s); trailing blanks are trimmed, mirroring
|
||||||
|
`manualtools._trailing_blanks`.
|
||||||
|
- **Body text is not colorized.** Only the name and the section headings carry
|
||||||
|
color. Guessing which words in a `DESCRIPTION` are flags or commands is a
|
||||||
|
heuristic with no upside; `config-help`'s hand-tuned coloring stays hand-tuned.
|
||||||
|
|
||||||
|
### 2.4 Degraded output
|
||||||
|
|
||||||
|
If `functions -D` yields no readable file, or the file holds no renderable
|
||||||
|
section, print the function name, its `--description`, and a pointer to
|
||||||
|
`help config <name>` — then return 0. Never return 1.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Where the help text comes from
|
||||||
|
|
||||||
|
### 3.1 Decision: parse the `.fish` source at call time
|
||||||
|
|
||||||
|
Rejected alternatives:
|
||||||
|
|
||||||
|
- **Read the generated `docs/fish-config.md`.** It is committed and
|
||||||
|
pre-formatted, but it is a *generated* artifact: a header edited without a
|
||||||
|
docs rebuild makes `--help` lie, which is the exact drift this job exists to
|
||||||
|
remove. It also costs a 163 KB scan and a `### <name>` heading match that can
|
||||||
|
collide with prose headings elsewhere in the manual.
|
||||||
|
- **Precompute a fish data file at build time**, mirroring the component
|
||||||
|
registry. This is the fastest option at runtime, but it adds a build artifact,
|
||||||
|
a new staleness class, and a step in the docs pipeline — which requirement 6
|
||||||
|
forbids touching.
|
||||||
|
- **Reuse `config-help`.** It is a pager launcher over the whole manual; it
|
||||||
|
cannot emit one function's entry to stdout, and bending it into that shape
|
||||||
|
would cost more than the renderer.
|
||||||
|
|
||||||
|
Source parsing wins because the source cannot be stale, the parse is ~15 lines
|
||||||
|
of `string match`, and the cost is one small file read on a keypress — never at
|
||||||
|
startup.
|
||||||
|
|
||||||
|
### 3.2 Parsing rule
|
||||||
|
|
||||||
|
Locate `function <name>` in the file, then walk **backwards** collecting the
|
||||||
|
contiguous run of `#` lines above it (skipping a blank separator line, for
|
||||||
|
`sponge_filter_secrets`). Within that run, apply `manualtools`' own grammar:
|
||||||
|
|
||||||
|
- `^#\s+([A-Z][A-Z ]*[A-Z])\s*$` starts a section.
|
||||||
|
- Any other `#` line is body: strip `#`, then strip exactly three leading spaces
|
||||||
|
if present, so deeper indentation in nested `ARGUMENTS` tables survives.
|
||||||
|
- `#` lines before the first label (the copyright preamble) are ignored.
|
||||||
|
|
||||||
|
Backward-walking from the `function` line replaces `manualtools._block_identity`
|
||||||
|
and is strictly more accurate at runtime: it resolves the three multi-header
|
||||||
|
files that publish a function (`fish-deps`, `gi`, `y`) correctly, and it gives
|
||||||
|
`docker` its real header in `dops.fish` where the Python parser attributes that
|
||||||
|
block to the file stem.
|
||||||
|
|
||||||
|
### 3.3 Verified mechanics
|
||||||
|
|
||||||
|
Proven in fish 4.9.1, not assumed. An autoloaded function calling
|
||||||
|
`(status current-function)` and `functions -D` on the result resolves to its own
|
||||||
|
defining file, both when called directly and when called from another function:
|
||||||
|
|
||||||
|
```
|
||||||
|
status current-function: probefn
|
||||||
|
functions -D self: .../functions/probefn.fish
|
||||||
|
status filename: .../functions/probefn.fish
|
||||||
|
```
|
||||||
|
|
||||||
|
Called *inside* the renderer, `status current-function` returns `__fish_help_header`
|
||||||
|
and `status stack-trace` returns the caller's path only as `~`-abbreviated prose.
|
||||||
|
Hence the name is passed as an argument rather than discovered. A working
|
||||||
|
prototype rendered `poke --help` in the format of §2.3 and left `poke`'s normal
|
||||||
|
path untouched.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Classification of the 79
|
||||||
|
|
||||||
|
The exemption in `functions/CLAUDE.md` is "strictly wraps another tool's help."
|
||||||
|
Applied literally it splits into two classes, plus one collision case.
|
||||||
|
|
||||||
|
**EXEMPT-A — shadow or pass-through.** The function shadows a real same-named
|
||||||
|
binary, or its entire body forwards `$argv` to one named tool. `ls --help` must
|
||||||
|
reach `ls`. Intercepting is a regression, and for the C1-guarded shadows it also
|
||||||
|
breaks the disabled-fallback contract, where the bare tool is supposed to answer.
|
||||||
|
|
||||||
|
**EXEMPT-B — not a command.** Invoked by fish, never typed. Published in the
|
||||||
|
manual, but `--help` is meaningless.
|
||||||
|
|
||||||
|
### 4.1 EXEMPT-A (35)
|
||||||
|
|
||||||
|
| Function | Reason |
|
||||||
|
|---|---|
|
||||||
|
| `agy` | Shadows `agy`; body ends in `command agy $argv`. |
|
||||||
|
| `antigravity-ide` | Shadows `antigravity-ide`; only filters one stderr line. |
|
||||||
|
| `bash` | Shadows `bash`; adds `--rcfile`, forwards the rest. |
|
||||||
|
| `cat` | Shadows `cat`; C1 guard falls back to `command cat $argv`. |
|
||||||
|
| `cdi` | Whole body is `zi $argv`; zoxide owns the help. |
|
||||||
|
| `cffetch` | `clear` then `fastfetch $argv`. |
|
||||||
|
| `cheat` | `command cheat -c $argv`, else `tldr`/`man`. |
|
||||||
|
| `claude` | Shadows `claude`; body ends in `command claude $argv`. |
|
||||||
|
| `clone` | Whole body is `clone-in-kitty $argv`. |
|
||||||
|
| `clonet` | Whole body is `clone-in-kitty --type=tab $argv`. |
|
||||||
|
| `config-toggle` | Deprecated alias; forwards to `config-settings`, which has help. |
|
||||||
|
| `copy` | Whole body is `command cp $argv` with one directory special case. |
|
||||||
|
| `dops` | The file defines `docker`, which shadows `docker`. |
|
||||||
|
| `du` | Shadows `du`; unmatched args reach `command du`. |
|
||||||
|
| `dusize` | `du -sh $argv[1]` through the `du` shadow. |
|
||||||
|
| `fast-cli` | Whole body is `command fast $argv`. |
|
||||||
|
| `ffetch` | Whole body is `fastfetch $argv`. |
|
||||||
|
| `gitui` | `command gitui -t frappe.ron $argv`. |
|
||||||
|
| `gitup` | Its own `SYNOPSIS` is `gitup [args...]`, forwarded to `git fetch`. |
|
||||||
|
| `jr` | Whole body is `jobrunner $argv`; `jobrunner` has a help menu. |
|
||||||
|
| `joplin` | Whole body is the real `joplin` binary with `NODE_OPTIONS` set. |
|
||||||
|
| `less` | Shadows `less`; resolves a pager and forwards `$argv`. |
|
||||||
|
| `ls` | Shadows `ls`; forwards to `eza`/`lsd`/`command ls`. |
|
||||||
|
| `mkdir` | Shadows `mkdir`; any flag argument goes to `command mkdir -p`. |
|
||||||
|
| `mv` | Shadows `mv`; falls through to `command mv $argv`. |
|
||||||
|
| `paste` | Shadows coreutils `paste`; forwards `$argv` to `wl-paste`/`xclip`. |
|
||||||
|
| `ping` | Shadows `ping`; forwards to `prettyping` or `command ping`. |
|
||||||
|
| `rawfish` | Whole body is `env NO_TMUX=1 fish $argv`. |
|
||||||
|
| `rg` | Shadows `rg`; adds one flag under Kitty, forwards the rest. |
|
||||||
|
| `rm` | Shadows `rm`; unmatched args reach `command rm $argv`. |
|
||||||
|
| `search` | Whole body is `$aur $argv` (paru or yay). |
|
||||||
|
| `ssh` | Shadows `ssh`; forwards to `kitten ssh` or `command ssh`. |
|
||||||
|
| `top` | Shadows `top`; forwards to `btop` or `command top`. |
|
||||||
|
| `view` | Shadows vim's `view`; whole body is `nvim -R $argv`. |
|
||||||
|
| `yt-dlp` | Shadows `yt-dlp`; injects defaults, forwards `$argv` last. |
|
||||||
|
|
||||||
|
### 4.2 EXEMPT-B (4)
|
||||||
|
|
||||||
|
| Function | Reason |
|
||||||
|
|---|---|
|
||||||
|
| `fish_prompt` | Prompt hook; fish calls it, users never do. |
|
||||||
|
| `fish_right_prompt` | Prompt hook. |
|
||||||
|
| `fish_mode_prompt` | Prompt hook; body is empty. |
|
||||||
|
| `sponge_filter_secrets` | `sponge` plugin filter callback, invoked per command. |
|
||||||
|
|
||||||
|
### 4.3 CONVERT (40)
|
||||||
|
|
||||||
|
All take the standard call site of §2.1 unless noted.
|
||||||
|
|
||||||
|
| Function | Reason it is not exempt |
|
||||||
|
|---|---|
|
||||||
|
| `bd-pull` | Own Gitea/Beads logic; `$argv[1]` is a repo slug. |
|
||||||
|
| `bkg` | Command runner; `$argv` is a command, not flags. First-arg rule applies. |
|
||||||
|
| `branch` | Own git logic; `branch --help` currently feeds `--help` to `git checkout -b`. |
|
||||||
|
| `check_fish_deps` | Ignores `$argv`; runs `fish-deps status`. |
|
||||||
|
| `claude-docs` | Ignores `$argv`; fires a fixed prompt. |
|
||||||
|
| `claude-pr` | Ignores `$argv`; fires a fixed prompt. |
|
||||||
|
| `cleanup` | Ignores `$argv`; `--help` currently runs `sudo pacman -Rns`. |
|
||||||
|
| `fast` | Ignores `$argv`; placeholder that prints a notice. |
|
||||||
|
| `fc` | Own history logic; `--help` currently becomes a history search term. |
|
||||||
|
| `fish-deps` | **Reuse:** wire `case -h --help` to the existing `__fish_deps_help`, return 0. Today `--help` hits `case '*'`, prints "Unknown subcommand", exits 1. |
|
||||||
|
| `fzf-update` | Ignores `$argv`; `--help` currently clones and installs fzf. |
|
||||||
|
| `gip` | Ignores `$argv`. |
|
||||||
|
| `gip4` | Ignores `$argv`. |
|
||||||
|
| `gip6` | Ignores `$argv`. |
|
||||||
|
| `hist` | Ignores `$argv`; opens an fzf picker. |
|
||||||
|
| `lD` | Not a binary; three-branch body whose value is the `--only-dirs` preset. |
|
||||||
|
| `ld` | Ignores `$argv`; launches lazydocker with a computed `DOCKER_HOST`. |
|
||||||
|
| `limine-edit` | Ignores `$argv`; `--help` currently runs `sudoedit` and re-enrolls Limine. |
|
||||||
|
| `lock` | Ignores `$argv`; `lock --help` currently locks the screen. |
|
||||||
|
| `lsr` | Not a binary; value is the reverse-time preset. |
|
||||||
|
| `lss` | Not a binary; value is the size-sort preset. |
|
||||||
|
| `lstree` | Not a binary; value is the recursive-tree preset. |
|
||||||
|
| `lt` | Not a binary; value is the depth-2 tree preset. |
|
||||||
|
| `ltr` | Not a binary; value is the reverse-time preset. |
|
||||||
|
| `lx` | Not a binary; value is the extension-sort preset. |
|
||||||
|
| `parur` | Ignores `$argv`; opens an fzf package picker. |
|
||||||
|
| `poke` | Own logic; `--help` would be created as a file. |
|
||||||
|
| `ports` | Ignores `$argv`; runs `sudo lsof`. |
|
||||||
|
| `qr` | `--help` would be encoded into a QR code. |
|
||||||
|
| `sbver` | Own `--brief` flag; runs `sudo sbctl verify`. |
|
||||||
|
| `screensleep` | Ignores `$argv`; `--help` currently blanks the display. |
|
||||||
|
| `split` | **`--help` only, no `-h`.** Its own `ARGUMENTS` documents `-h, --horizontal`. |
|
||||||
|
| `spwin` | Own terminal dispatch; first-arg rule protects `spwin <cmd> --help`. |
|
||||||
|
| `steam-dl` | Ignores `$argv`. |
|
||||||
|
| `sudo-toggle` | Ignores `$argv`; `--help` currently rewrites `/etc/sudoers.d`. |
|
||||||
|
| `swapstat` | Ignores `$argv`. |
|
||||||
|
| `tab` | Own terminal dispatch; first-arg rule protects the forwarded command. |
|
||||||
|
| `tmux-clean` | Ignores `$argv`; `--help` currently kills tmux sessions. |
|
||||||
|
| `upgrade` | Ignores `$argv`; `--help` currently runs `paru -Syu --noconfirm`. |
|
||||||
|
| `wake-lock` | Command runner; first-arg rule protects `wake-lock <cmd> --help`. |
|
||||||
|
|
||||||
|
Eight of these currently perform a destructive or irreversible action when handed
|
||||||
|
`--help`, because they ignore `$argv` entirely and just run: `cleanup`
|
||||||
|
(`sudo pacman -Rns`), `fzf-update` (clone and install), `limine-edit` (`sudoedit`
|
||||||
|
and re-enroll), `lock` (locks the session), `screensleep` (blanks the display),
|
||||||
|
`sudo-toggle` (rewrites `/etc/sudoers.d`), `tmux-clean` (kills tmux sessions) and
|
||||||
|
`upgrade` (`paru -Syu --noconfirm`). For these, converting is a safety fix before
|
||||||
|
it is a documentation one.
|
||||||
|
|
||||||
|
`split` is the only `-h` collision in the whole set; every other CONVERT function
|
||||||
|
was checked and uses `-h` for nothing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. The 30 existing menus stay
|
||||||
|
|
||||||
|
Not converted in this job. The risk is asymmetric:
|
||||||
|
|
||||||
|
- Three of them carry content that **does not exist in any header**, so
|
||||||
|
conversion would delete documentation: `config-help`'s ov navigation keys and
|
||||||
|
pager fallback chain, `qc`'s appended `aichat --help`, `superpowers`' inline
|
||||||
|
usage block. Recovering them means authoring header prose, which requirement 7
|
||||||
|
forbids.
|
||||||
|
- Thirteen use `argparse`/`_flag_help` and accept `--help` in any position.
|
||||||
|
Moving them to the first-argument-only rule of §2.2 is a real behavior change
|
||||||
|
for menus people already use.
|
||||||
|
- No acceptance criterion requires it. The duplication they represent is
|
||||||
|
pre-existing and static — it does not grow as new functions land, because new
|
||||||
|
functions will use the renderer.
|
||||||
|
|
||||||
|
Thirty conversions is thirty independent chances to change output somebody
|
||||||
|
relies on, bought for no test. Recommended as a separate follow-up branch, where
|
||||||
|
each diff can be reviewed against its own before/after output.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Testing
|
||||||
|
|
||||||
|
Two additions to `tests/functional.fish`, which runs inside the sandboxed loaded
|
||||||
|
session where `functions -D` resolves to the sandbox copy.
|
||||||
|
|
||||||
|
**`test_help_renderer`** — behavioral. Renders a fixture function with a known
|
||||||
|
header and asserts the section order, the `SYNOPSIS`→`USAGE` rename, indentation
|
||||||
|
survival, and exit 0. Then spot-checks two real, side-effect-free functions
|
||||||
|
(`poke`, `gip4`) end to end. Runs under `TERM=dumb`, where `set_color` emits
|
||||||
|
nothing, so assertions match plain text.
|
||||||
|
|
||||||
|
**`test_every_user_facing_function_has_help`** — the guard required by
|
||||||
|
acceptance 5. For each function published by the header parser, assert its body
|
||||||
|
contains a help entry point (`__fish_help_header`, `_flag_help`, or a
|
||||||
|
`-h`/`--help` branch) *or* appears in an explicit exempt list.
|
||||||
|
|
||||||
|
The exempt list lives in `tests/functional.fish` as one array with a one-line
|
||||||
|
comment per entry, seeded from §4.1 and §4.2. Rejected: a `# NOHELP` marker
|
||||||
|
comment in each exempt file — 39 file edits for no runtime benefit, and a marker
|
||||||
|
is the kind of thing that gets copy-pasted into a new function and silently
|
||||||
|
exempts it. A central list makes adding an exemption a reviewable act.
|
||||||
|
|
||||||
|
The wiring check is deliberately static. Running all 40 functions with `--help`
|
||||||
|
would be a stronger test, but it executes `upgrade`, `sudo-toggle` and
|
||||||
|
`limine-edit` on the CI machine, and it is precisely wrong when the code is
|
||||||
|
broken — the case the test exists to catch. The renderer's behavior is proven
|
||||||
|
once against a fixture; the wiring is proven by inspection across all 40.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Line delta
|
||||||
|
|
||||||
|
| Change | Lines |
|
||||||
|
|---|---:|
|
||||||
|
| `functions/__fish_help_header.fish` (with license + header) | ~55 |
|
||||||
|
| 39 call sites × 1 line | +39 |
|
||||||
|
| `fish-deps` `case -h --help` | +2 |
|
||||||
|
| `tests/functional.fish` (2 tests + exempt list) | ~60 |
|
||||||
|
| Hand-rolled menus deleted | 0 |
|
||||||
|
| **Net** | **~+156** |
|
||||||
|
|
||||||
|
The number that matters is not in the table: 40 menus not written. At the ~15
|
||||||
|
lines the existing menus average, hand-writing them would have cost ~600 lines
|
||||||
|
of `echo` and a permanent second copy of the manual.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Risks accepted
|
||||||
|
|
||||||
|
- **First-argument-only interception** means `poke somefile --help` does not
|
||||||
|
print help. Correct for command runners, mildly surprising elsewhere. Accepted:
|
||||||
|
the alternative silently breaks `wake-lock rsync --help`.
|
||||||
|
- **Uncolored body text.** Generated menus will look plainer than the
|
||||||
|
hand-tuned ones. Accepted over a guessing heuristic.
|
||||||
|
- **A header missing `ARGUMENTS`** yields a menu without an arguments section.
|
||||||
|
`verify-manual.py` enforces only `SYNOPSIS`, `DESCRIPTION` and `EXAMPLE`.
|
||||||
|
Filling the gaps is authoring prose — out of scope. Note them, do not write them.
|
||||||
|
- **The seven `ls` variants** (`lD`, `lsr`, `lss`, `lstree`, `lt`, `ltr`, `lx`)
|
||||||
|
currently pass `--help` to `eza`, incidentally. After conversion they print
|
||||||
|
their own preset's documentation and `eza --help` reaches eza directly. This
|
||||||
|
is the largest judgment cluster in §4.3 and the most likely place for the
|
||||||
|
classification to be wrong.
|
||||||
|
- **`functions/CLAUDE.md` will still read as violated.** Its one-line rule names
|
||||||
|
only the wrapper exemption, not the prompt-hook class. A two-line amendment is
|
||||||
|
proposed separately; this spec does not change it.
|
||||||
Reference in New Issue
Block a user