ADR-034: --explain Pages Through a Pager on an Interactive Terminal¶
Status: Accepted
Context: --explain CL-XXXX dumps the rule's full markdown doc to
stdout. The docs have grown past 100 lines (CL-0001 is 115), so on an
interactive terminal the top of the doc — the directive and the
why-it-matters prose, the strongest content — scrolls out of view instantly
and the reader lands on the tail. The README hero GIF captured this
faithfully: the recorded --explain step shows a single-frame whoosh and
settles on the ATT&CK table. The GIF is a symptom; the terminal UX is the
problem.
The constraint is the CLI output contract: stdout carries
data, and --explain prints the doc raw — machine consumers pipe it, tests
byte-compare it, and fix/CI never touch a pager.
Decision: When stdout is a TTY, --explain pipes the doc through a
pager, exactly the way git log does; in every other case the output is
byte-identical to before. The gate, in order:
- not a TTY → plain dump (pipes, redirects, CI, tests — by construction, not by configuration);
NO_PAGERset (any non-empty value) → plain dump, mirroringNO_COLOR's contract for color;TERMunset ordumb→ plain dump;--no-pager→ plain dump;PAGERset → that command (split withshlex; blank disables), elseless -RFX(-Fexits when the doc fits one screen, so short docs never trap the reader;-Xkeeps the tail in scrollback) plus a labeled status prompt —-Ps"CL-XXXX · Space next · b back · q quit"— because the pager's controls are pure convention and the bare:labels none of them. The prompt rides only on the default pager; a user's ownPAGERkeeps whatever prompt they configured.
A pager that cannot be spawned falls back silently to the plain dump. That
case is real, not defensive: the published image is distroless, so
docker run -t produces a TTY with no pager binary behind it — the
fallback is what keeps that invocation working unchanged. A pager that
exits nonzero falls back the same way: busybox's less (Alpine) rejects
-Ps with a usage error, and without the rescue that rejection would
swallow the doc. The cost is bounded — a pager dying nonzero after showing
content prints the doc twice, never zero times.
Consequences: Non-TTY output is untouched, so the stdout-carries-data
contract and every existing consumer are unaffected; the tests prove the
piped bytes identical with and without a pager configured. A human at a
terminal reads the doc top-down. The change is presentation only and ships
as MINOR. The paging applies to --explain alone — the findings report
stays unpaged, because a lint run's verdict belongs in the scrollback of
the command that produced it, not behind a q.