xff v0.4.0

Help model design

Status: adopted 2026-08-05. Implemented in slices (see the #154 epic in
TODO.md); this document is the decision record.

The HTML backend shipped after the original plain, Markdown, and roff slices. It emits a
self-contained HTML5 document directly from the model; Pages does not convert Markdown or run a
client-side renderer. Non-console renderers are formats of the full help document rather than
standalone operations: --help=full:markdown, --help=full:html, and --help=full:roff; long
aliases full, and --man remains the conventional roff shortcut.

Problem

Help generation grew into two parallel systems. The expression registry and the
global-options table are a real content single source of truth, and the
WriteReference walk drives the roff / Markdown / plain backends structurally. But
plain --help and most --help=TOPIC bypass all of that: sixteen hand-assembled
Render* functions in xff/cli/help.cc plus kHelpText / kHelpTextExpression
in xff/cli/main.cc build strings directly, with hardcoded prose. The result is
duplicated rendering logic, prose that cannot wrap to width, cross-references that
are dead text, and content that drifts (the dead "see the Printf directives section
below" that #126 had to repair is exactly this failure mode).

Model

Help is a semantic document model (an AST), and every output format is a
separate renderer backend that walks the one model. The model carries meaning,
never presentation.

1. Output independence

Backends: plain text, ANSI-colored text, Markdown, roff (man page), and HTML. The
same node renders natively per backend (a Section is a blank-line heading / a
bold ANSI line / ## / .SH / <h2>). The model has no format-specific strings.

2. Width (text backends only)

Width is a text-backend concern; the model is width-agnostic and Markdown / HTML /
roff never hard-wrap (their consumer reflows: a viewer, the browser, mandoc's fill).
The plain and color backends take a width, resolved highest-precedence-first:

  1. an explicit --width=N (or config value);
  2. the detected terminal width when stdout is a TTY (ioctl(TIOCGWINSZ), honoring
    $COLUMNS);
  3. a fallback of 80 columns (piped / redirected / undetectable), so non-TTY output
    is deterministic.

Per node: Prose free-flows to the width; Row / Entry keep the term column and
wrap the description with a hanging indent; Example is verbatim and never wrapped.
Width and color live in a small render-options struct passed to the text backend
(so a test can feed width=40 and assert the wrap).

3. Highlighting, cross-references, index

4. Authoring

The model is the interchange format; it is authored in C++, not an external markup.

XHTML / Markdown as an authored input was rejected: the content is programmer
authored, terse, and lives with the code, so an XML layer adds a parser, verbose
authoring, and a round-trip for no gain. They remain output backends only. Revisit
only if help ever becomes externally authored by non-coders.