docs/dev/writing-docs.md
Conventions for the hand-written pages under docs/. For which pages are generated and
must not be edited, see Documentation toolchain.
Do not restate something the page's own structure already establishes.
The beta status of the spec pipeline, for example, is carried by the Status row in
guide/modes.md, by (beta) in the section headings, by the ::: warning Beta callouts,
by the version timeline, and by the π§ͺ in the page titles. A sentence saying "this is the
only mode not marked beta" adds nothing and rots independently of the places that do.
The test is not "does this sentence read well" but "does the reader learn anything here they could not already see". This applies to maturity, version requirements, which namespace to use, and what a mode does.
Marketing filler and restatement filler are the same defect wearing different clothes. Replacing "the stable, production-ready mode" with "the only mode not marked beta" is not an improvement.
Orientation may appear in several places; detail may not.
It is fine β often necessary β for two pages to introduce the same subject, because they are read by different people arriving from different directions. What must not happen is the same specifics being spelled out twice, because the two copies drift and nothing reveals which one is stale.
The split that works is by depth, not by topic. A page aimed at users says what a thing does and why it matters, then links onward. The page aimed at contributors carries the mechanism, the rules, the exact lists. Neither repeats the other's half.
reference/architecture.md and dev/pipeline.md are the worked example: the reference
page describes the Assembler in a paragraph and links onward, while the internals page owns
slot maps, the resolution algorithm and the root-attribute list. Before this split both
described slot maps, augmenter ordering and the class hierarchy, in different words.
When you find yourself writing something a second time, that is the signal to link instead.
When the same text genuinely has to appear in more than one output, extract it and pull it in. Copying is what rots.
Two mechanisms are already in use here:
docs/snippets/preamble_*.md β markdown fragments the reference generators splice into
their pages, via DocGenerator::snippetContent(). Editing the fragment updates the
generated page.<<< @/snippets/path.php β transcludes a real, test-executed source file into a page, so
a documented example cannot drift from code that runs.A third, <!--@include: ./path.md-->, is supported by Vitepress but not yet used here. It
suits prose shared between hand-written pages.
Prefer extracting a fragment over pasting, and prefer generating over both where the content is derivable at all β see the toolchain notes for what is already generated.
A page describes its subject. Unless the documentation is the subject β as on this page, or the toolchain notes β it does not explain how the docs are produced or why a cross-reference can be trusted.
No page, of either kind, needs to announce its own audience or justify its existence. The title and the link that brought the reader here have already done that.
Three real examples, all from one page:
The Augmenters reference β generated from the pipeline itself, so it cannot fall out of step with the code
see Spec pipeline internals, which is written for people changing the pipeline rather than using it
This page stays at the level of "what happens and in what order"
Each is true, and each is invisible to the only question the reader has, which is how the thing works. Whether a reference is generated matters to whoever maintains it; the reader just follows the link.
The usual tell is a clause attached to a link, explaining the link. Let the link text do that work: "see [Spec pipeline internals]" is complete. Anything after the comma is the page talking about itself.
A list someone has to remember to update is a list that will be wrong.
Enumerate a set only when it is closed β defined somewhere in code, changing only when
that code changes. The generated pages are closed: tools/docgen.php names them, so a table
listing them holds until someone edits the generator. "Everything else under docs/" is
open-ended; it grows whenever anyone adds a page, so it gets described rather than listed.
The same test applies to a count attached to a list. If the set can change, so can the number β which is why "six pages" goes along with the enumeration it summarised.
Command output, default configuration, and API listings should be copied from the real
thing, not composed. A hand-written openapi -h block once carried the wrong format, a
wrong --mode description, a --version default that did not exist, and four missing
options β for as long as nobody ran it.
./bin/openapi -h # capture this
./bin/openapi --mode spec -D src
Class::method() survives refactoring; Foo.php:110 does not. Of three line references in
ADR-001, two had already rotted.
If a page compares spec mode to classic mode, run both. Hedged comparisons β "classic mode may emit the array form" β are a sign the comparison was reasoned about rather than tested, and they are impossible for a reader to act on.
Either verify and state it plainly, or document only the behavior you know.
Two examples from the same page:
If a claim needs the reader to ignore the next paragraph, cut the claim.
The codebase is mixed. normalise sits alongside serialization; both spellings of
behavio(u)r appear in src/ and in docs/. There is no single convention to appeal to.
Match whatever the file you are editing already uses, and leave the rest alone. A bulk correction would churn far more than it fixes.
docs/snippets/ are executed by the test suite β see
Testing for what a new snippet requires.<codeblock> component renders annotation / attribute / spec variants as tabs; keep
the three implementations in sync./reference/architecture), not relative file
paths, so they survive being rendered at a different depth.A pass to run over a documentation diff β your own or someone else's. Every item below caught a real defect in this codebase at least once.
Where a check fails, fix the source rather than the symptom: a wrong generated page means a wrong docblock or generator, not a page to hand-edit.
composer docs:gen leaves no diff on the pages listed
in the toolchain notes.composer <script> mentioned exists in composer.json.Class::method(), file paths, CLI
flags, config keys β check each against the source. This catches the most damaging
class of error, where confident prose describes an API that was renamed or never
existed.--help text, default config
dump or API listing should come from a real run.Foo.php:123 rots silently; cite Class::method().composer docs:build covers the rest.#heading-slug link is only safe when
the heading is plain words β punctuation and dashes make the generated slug
ambiguous. If you have not confirmed it, link to the page.composer.json.docs/ is US, src/ is British. Neither is
worth correcting in bulk.