Documentation Standard
Documentation Standard
Section titled “Documentation Standard”This is the spec every BoB doc must obey. Each doc declares one Diátaxis mode, lives in the matching
docs/category, carries a frontmatter contract (type/owner/last_reviewed), and states each fact once (link, don’t duplicate). The governance tooling (R2) and CI (R3) enforce what follows; this document is the source of truth they read.
This doc is itself reference mode (look things up; don’t read end-to-end). It
lives in docs/reference/ and carries the frontmatter it mandates — it is its own
first conformance test. It grounds out in
doc-ia-audit-2026-06-22.md, which catalogues
the disorder this standard exists to end.
Principles it encodes: Krug (scannable, self-evident), Rosenfeld & Morville (findability through organization / labeling / navigation), Diátaxis (one mode per doc), inverted pyramid (answer first), and the Prime Directive (one owner per fact).
1. Diátaxis: one mode per doc
Section titled “1. Diátaxis: one mode per doc”Every doc serves exactly one of four user needs. Mixing modes is the single biggest failure in BoB’s current docs — the 1,455-line handbook is all four at once, so no reader’s need is met cleanly. Pick one mode before you write; if a doc needs two, it is two docs.
| Mode | Serves | Reader is… | Answers | Voice |
|---|---|---|---|---|
| Tutorial | Learning | a beginner, learning by doing | “Teach me, start to finish” | “We will… now do this.” Lessons, not options. |
| How-to | A task | a practitioner with a goal | “How do I X?” | Imperative steps. Assumes competence. |
| Reference | Looking up | someone who knows what they need | “What exactly is X?” | Dry, exhaustive, scannable. Tables over prose. |
| Explanation | Understanding | someone building a mental model | “Why is it this way?” | Discursive. Trade-offs, history, rationale. |
Choosing the mode
Section titled “Choosing the mode”Ask what the reader is doing when they open the doc:
- Studying with no specific goal yet → tutorial (
docs/tutorials/). - Working, with a concrete task in hand → how-to (
docs/how-to/). - Pausing mid-task to confirm a name, flag, field, or value → reference
(
docs/reference/). - Stepping back to understand why a thing exists → explanation
(
docs/explanation/).
Two tests resolve almost every case:
- The “during vs. between” test. Tutorials and how-tos are read while doing; reference and explanation are read between doing. If your steps and your rationale fight for space, split them.
- The completeness test. Reference must be complete and accurate (every flag); a how-to must be sufficient for the goal (only the flags that matter). A doc that can’t decide how much to include is mixing the two.
Templates that already do this well:
warp-drive.md’s TL;DR-first lede (how-to done right) andverification-system.md’s tight reference scoping. Reuse their shape.
2. Directory taxonomy & the root-docs rule
Section titled “2. Directory taxonomy & the root-docs rule”2.1 The docs/ taxonomy
Section titled “2.1 The docs/ taxonomy”Findability comes from organization + labeling (Rosenfeld & Morville). Every doc
lives in exactly one category, and the category name is the Diátaxis mode — so a
doc’s location declares its contract. No more flat pile at the top of docs/.
| Directory | Holds | Mode | Examples (target home) |
|---|---|---|---|
docs/tutorials/ |
Learning paths, getting-started walkthroughs | tutorial | Quick Start, first-project setup |
docs/how-to/ |
Task recipes — “how do I X?” | how-to | deploy, new-machine-setup, cdfork |
docs/reference/ |
Lookup material — contracts, glossaries, tables, this standard | reference | hooks reference, CLI tables, frontmatter contract |
docs/explanation/ |
Architecture, rationale, the conceptual spine | explanation | prime-directive, orchestration/ (handbook + orchestrator) |
docs/audits/ |
Dated point-in-time analyses (named *-YYYY-MM-DD.md) |
reference (frozen) | doc-ia-audit-2026-06-22 |
docs/research/ |
Exploratory notes, not-yet-normative findings | explanation (draft) | model-routing research |
There is no docs/archive/: retired docs are deleted to a git tag (§5).
Rules:
- Every
docs/*.mdlives in a category subdirectory. A bare*.mdat the top ofdocs/is a placement violation (the one exception is a generateddocs/index.mdnavigation root, if present). - The category must match the doc’s
typefrontmatter.type: referenceindocs/how-to/is a contradiction the auditor flags. - Filenames are lowercase kebab-case (
^[a-z0-9]+(-[a-z0-9]+)*$), extension.mdfor prose — already enforced bymake check-doc-naming. - Audit/research artifacts are dated and frozen: they record a moment, are never
edited after publication, and don’t carry a rolling
last_reviewedexpectation — theirlast_reviewedis their publication date and stays there.
2.2 The root-docs rule
Section titled “2.2 The root-docs rule”The repository root is not a documentation directory. Stray root docs (plan.md,
bob-identity.md) are how BoB’s IA rotted. The root *.md allowlist is closed:
| File | Allowed at root? | Rationale |
|---|---|---|
README.md |
✅ Required | Repository entry point; GitHub renders it. |
CLAUDE.md |
✅ Required | Claude Code reads it at the root by contract. |
CHANGELOG.md |
✅ Allowed | Release tooling convention; tools expect it at root. |
LICENSE / LICENSE.md |
✅ Allowed | Legal convention; GitHub detects it at root. |
CONTRIBUTING.md |
✅ Allowed | GitHub surfaces it from the root in PR/issue flows. |
| Anything else | ❌ Move into docs/<category>/ |
No exceptions; relocate and repoint its inbound links. |
Why these five are exempt: each is read at the root by a tool or platform contract (GitHub, Claude Code, release automation), not by human browsing. A doc a person would seek belongs in the taxonomy. R3 enforces this allowlist in CI.
3. The frontmatter contract
Section titled “3. The frontmatter contract”Every doc under docs/ (excluding frozen audits/) MUST
open with a YAML frontmatter block. This is the metadata the freshness, ownership, and
mode-purity checks read — no frontmatter, no governance.
---type: reference # required — one of: tutorial | how-to | reference | explanationowner: paulirv # required — GitHub handle accountable for accuracylast_reviewed: 2026-06-22 # required — ISO 8601 (YYYY-MM-DD) date last verified trueexpires: 2026-12-22 # optional — ISO date after which the doc is presumed stale---| Field | Required | Type | Contract |
|---|---|---|---|
type |
Yes | enum | Exactly one Diátaxis mode. Must match the directory (§2.1). |
owner |
Yes | string | A GitHub handle (not a team, not “team”) — one accountable human. |
last_reviewed |
Yes | ISO date | YYYY-MM-DD. Bumped only when the content is re-verified, never as a side effect of an unrelated edit. |
expires |
No | ISO date | If set and past, the auditor flags the doc stale regardless of last_reviewed. Use for docs with a known shelf life (migration guides, dated plans). |
title |
No | string | The doc’s short label in its section index. Falls back to the H1, which is often longer than a table row wants. |
description |
No | string | One sentence — the doc’s row text in its section index (§3.1). Falls back to nothing; the row still appears. |
order |
Tutorials only | integer | The tutorial’s step on the learning path (§3.1). A positive integer, unique within docs/tutorials/. Without one the tutorial is listed after every step, with none. |
subject |
No | map | {type, name}: the one item a page is about, where type is hooks · commands · skills · agents · alias. It lets the build show a retirement banner when that item retires (§5.5). |
prerequisites |
Tutorials only | list | Tutorial slugs (filenames without .md) to finish first, as [a, b]. Each must name a tutorial that comes earlier on the path; [] when there are none. |
Rules:
typeis the single source of a doc’s mode. The H1 title, the directory, andtypemust agree; disagreement is an auditor finding.last_reviewedmeans “a human confirmed this is true on this date.” Sourced via timelord, never from memory. A mechanical edit (fixing a count, repairing a link) does not reset it — only re-reading for accuracy does.- A generated BoB product page owns its
owner.docs/reference/cli.md,workflow-config.mdandwarp-drive-flags.mdare committed here, so their generators readownerfrom the page’s own frontmatter (people.productDocOwner(), #2689) — never from the resolved operator, and never from theoriginremote either (a fork’s origin owner differs, which is the same fault one level down). The render is therefore byte-identical under any$BOB_OPERATOR, so the drift gates pass on an unmodified checkout whoever runs them. A page with noowneryet takes the visible placeholderunassigneduntil a human sets one. - A doc generated into an operator’s own project takes
ownerfrom the operator profile. The doc-bootstrap drafts and the runbook scaffold write the profile’sdoc-ownerrole (people.docOwner(),scripts/lib/operator-people.js, #2269), falling back to theoriginremote’s owner when no operator resolves. There the operator’s doc-owner is the right owner. Either way a handle is never inlined in a generator. owneris one accountable handle. “One owner per fact” (§4) starts here: every doc has exactly one person who answers for it.- Frozen artifacts are exempt from staleness, not from dating.
docs/audits/*artifacts record a moment and don’t carry a rolling review expectation; they carrytype: referenceandlast_reviewedset to the publication date (the doc-keeper markdown report writes both itself). They are outside thetype/ownergovernance scan, but a published page still shows its age (§3.3).
3.1 Section indexes are derived, not maintained
Section titled “3.1 Section indexes are derived, not maintained”Section indexes (docs/how-to/index.md, docs/reference/index.md, …) are generated
from the directory contents — putting a file in a section is all it takes to make it
reachable (#1443). There is no second step, and no index file to remember to edit.
Two surfaces read the same derivation, so they cannot disagree:
- The table is regenerated by
scripts/docs-site/lib/section-index.js— on every docs-site build, and viamake docs-sync.make docs-checkverifies it and names the fix when an index is stale. - The orphan detector treats derived membership as an inbound link, so a doc that lives in a section is never an orphan merely because the committed table has not caught up. Without this, “add a doc” would still be two steps — the manual edit would have moved, not disappeared.
What this means when you write a doc:
- Give it
titleanddescription. That prose is its index row — it lives next to the doc so it cannot drift from what the doc became. - Only the region between the
BEGIN/END GENERATED SECTION INDEXmarkers is machine-written. The index’s frontmatter, H1, and intro blockquote are hand-authored and stay that way — curating what a section is for still has value; curating a list of files does not. - Omitting
descriptiondegrades the row, never the membership: the doc still appears, falling back to its H1. - Rows sort by filename, so ordering is derived too — there is no hidden hand-ordering to drift.
- Except tutorials, which are a learning path.
docs/tutorials/index.mdlists the tutorials in theirorder:with a Step and a Prerequisites column. The order is still declared, just by each tutorial itself, not by the index.section-index.jsrefuses to generate the index when a prerequisite names no tutorial, a prerequisite is not earlier on the path, two tutorials share a step, or anorder:is not a positive integer. Each generated tutorial ends with a “Next tutorial” line that comes from the same path, so the line and the index always agree (Rendering the scenarios page).
3.2 An orphan is caught at commit time, not in CI
Section titled “3.2 An orphan is caught at commit time, not in CI”Derived membership covers a doc that lands in a section directory. A doc that lands
anywhere else — a nested subdirectory with its own index, a new directory, the docs/
root — still needs an inbound link, and until #1444 that gap was discovered by
make docs-check in CI, on master, after the change had landed.
hooks/doc-orphan-gate.sh (PreToolUse) now refuses the commit instead, naming both
the unreachable file and the index that should link it. It runs on any branch and needs
no PR. Detection is not a second implementation: the hook shells out to
scripts/doc-keeper/orphan-check.js, which runs the auditor’s own link-graph rule over
rules.json, so the local gate and the CI gate cannot disagree about what an orphan is.
node scripts/doc-keeper/orphan-check.js --staged # what the gate will saynode scripts/doc-keeper/orphan-check.js --all # whole-tree, ad hocThe gate fails open — any internal error allows the commit, because make docs-check
remains the non-bypassable backstop. Escape hatch for a project that does not publish
its docs: _workflow.suppress_doc_orphan_gate=true.
3.3 Every page shows its age — two signals, never merged
Section titled “3.3 Every page shows its age — two signals, never merged”The published site shows every page’s age as two separate dates, because they answer different questions:
| Signal | Label on the page | Source | Answers |
|---|---|---|---|
| Last changed | Last changed (page footer) | git — the last commit that touched the page’s source file | When did the text last move? |
| Last reviewed | Last reviewed (above the content) | the authored last_reviewed frontmatter |
When did a human last confirm it is true? |
A page a count-sync edited yesterday has not been re-verified; a page verified in
June has not changed since. Showing one date would hide either case, so both
engines render both, labelled apart: VitePress in the managed theme’s provenance
strip above the content, beside the build and the page’s staleness grade
(#2620, see
Docs-Site Lifecycle),
Starlight through lastUpdated plus its LastUpdated override
(templates/docs-site/). The docs-site build copies pages out of git, so the sync
step stamps each copy with its source commit date first
(scripts/docs-site/lib/page-dates.js stamp).
Which pages carry which date:
| Pages | Last reviewed | Last changed |
|---|---|---|
Every published page under a declared source directory (docs/, runbooks/) |
Required — last_reviewed |
From git |
docs/audits/* |
Required — the publication date (§3) | From git |
docs/reference/generated/* |
Exempt — carries generated_at instead, written by its generator and shown as Generated |
From generated_at (the files are gitignored build output) |
docs/archive/* |
Exempt — frozen, never re-reviewed | From git |
Root files published as pages (README.md, CLAUDE.md) |
Exempt — root docs carry no frontmatter contract (§2.2) | From git |
make docs-check fails when a page the table requires to carry last_reviewed (or
generated_at) lacks an ISO date (page-dates.js check). How old a
last_reviewed may get is §3.4.
3.4 Staleness is graded against a declared policy
Section titled “3.4 Staleness is graded against a declared policy”Every page under docs/ and runbooks/ is graded from its last_reviewed date:
| Grade | Meaning |
|---|---|
fresh |
Reviewed within its mode’s aging_days |
aging |
Past aging_days, within stale_days — re-verify it soon |
stale |
Past stale_days, or past its expires date |
frozen |
A dated artifact exempt from re-review (docs/research/, §2.1) |
undated |
No ISO last_reviewed (§3.3 already fails the build on this) |
The thresholds are declared once, in the freshness rule of
scripts/doc-keeper/rules.json, and differ per Diátaxis mode because modes age
at different speeds — a tutorial walks the reader through today’s commands, an
explanation argues for a design that rarely moves:
| Mode | Aging after | Stale after |
|---|---|---|
| tutorial | 45 days | 90 days |
| how-to | 60 days | 120 days |
| reference | 90 days | 180 days |
| explanation | 120 days | 240 days |
fail_on: ["stale"] in the same rule makes a stale page fail make docs-check;
aging pages are reported, never failed. The grader is
node scripts/doc-keeper/staleness.js (--json for doc-staleness/1), the
freshness finding (§6.1) reads the same thresholds, and /doc-audit prints each
page’s grade beside its verdict. The fix for a stale page is to re-verify it
against the code and bump last_reviewed, or to archive it (§5) — never to bump
the date without re-reading the page.
4. Writing principles
Section titled “4. Writing principles”Mode and metadata get a doc filed; these principles get it read. They apply to every mode, though the emphasis shifts (reference leans hardest on scannability; explanation on the inverted pyramid’s “why”).
Scope: this section owns documentation prose. For prose rules that span all authored artifacts — commit messages, PR/issue bodies, comments — see the Writing Styleguide, which references these principles rather than restating them.
4.1 Inverted pyramid — answer first
Section titled “4.1 Inverted pyramid — answer first”Lead with the conclusion, then the detail, then the background. A reader who stops
after the first paragraph should still have the answer. This is why every BoB doc
opens with a TL;DR blockquote (see this doc’s lede, and warp-drive.md’s).
- First screen carries the payload. Put the “what is this / what do I do” above the fold; push history, edge cases, and rationale down.
- Never bury the lede in setup. A how-to that spends two screens on context before the first command has the pyramid upside-down.
4.2 Scannability — readers skim, they don’t read
Section titled “4.2 Scannability — readers skim, they don’t read”Krug’s law: people scan for what they need and ignore the rest. Write for the scan.
- Descriptive headings that say what the section answers — a reader navigating by heading alone should find their answer. Avoid cute or generic headings (“Overview”).
- Tables over prose for any enumerable set (flags, fields, modes, options). If you’re writing “X does A, Y does B, Z does C,” it’s a table.
- Short paragraphs (≤ 3–4 sentences) and bulleted lists for steps or sets.
- Front-load the keyword. Start list items and table rows with the term being
defined, not with filler (“You can use the
--fooflag to…” → “--foo— …”). - Bold the load-bearing phrase in a dense paragraph so the skim catches it.
4.3 Controlled vocabulary — one name per thing
Section titled “4.3 Controlled vocabulary — one name per thing”Findability dies when the same concept has three names (Rosenfeld & Morville). BoB uses one canonical term per concept, everywhere:
- One name per concept.
BOB_SOURCE(not “the source repo” / “the git repo” interchangeably); “requirement” (not “ticket” / “story” / “task”); “warp-drive” (not “the loop” / “auto-dev”). Pick the canonical term; use it verbatim. - One casing/spelling per name.
dev.json,bob provision,make check— as written, every time. - Define a term once, at its canonical home, and link to that definition rather than re-defining it inline (this is §4.4 applied to vocabulary).
4.4 Single source of truth — one owner per fact
Section titled “4.4 Single source of truth — one owner per fact”The Prime Directive, applied to prose: every fact is maintained in exactly one doc; everywhere else links to it. Duplication guarantees drift — the audit found the architecture diagram copied in four places and no two component counts in the repo agreeing.
- State each fact once, at its owning doc, then link. A count, a table, a diagram, a procedure lives in one place.
- Link, don’t paste. If you’re tempted to copy a table “for convenience,” link to it instead. Convenience copies are drift waiting to happen.
- The owner is the doc whose
type/topic the fact most belongs to. Amake checktarget table belongs in the verification reference; everything else links there. - A count is owned by a generator, not a doc. In BoB every component count is
a
<!-- count:<key> -->N<!-- /count -->marker spliced fromdocs/reference/generated/counts.json;make check-doc-countsfails on a hand-edited or stale one (#2087). - If two docs legitimately need the same fact, one owns it and the other transcludes or links — never two hand-maintained copies.
The R2 auditor’s
cross_cutting[]/ duplication detector exists to enforce this mechanically. Until it ships, treat duplication as a review-blocking defect.
4.5 Issue/PR bodies — never use a bare #N as an item number
Section titled “4.5 Issue/PR bodies — never use a bare #N as an item number”This applies to any text a command authors into a GitHub issue, PR, or comment
body (not docs/ prose). GitHub autolinks every #<digits> in rendered markdown
into a cross-reference to the issue/PR holding that number. In a mature repo those
low numbers are unrelated closed issues, so a body that numbers its own
items — “Critical #1”, “High #6”, “fix Medium #10” — sprouts a cluster of
spurious, misleading links (the real cause of example-co/example-cms#674; tracked here as #520).
- Reserve
#Nfor genuine issue/PR references only (Part of #42,fixes #51, the(#260)provenance tag). - For an intra-document item / finding / step number, use a non-linking form:
C1/H2/M3(severity+ordinal), “item 1”, “finding 3”, or backtick it —`#1`. #Ninside a fenced code block or backticks does not autolink and is safe (e.g. a terminal mockup listing real issue numbers).
Enforced by
scripts/checks/check-issue-autolink.sh(wired intomake check), which flags item-word + bare#Nin the issue-authoring command templates and can lint a generated body file beforegh issue create.
4.6 How-to docs — show both the slash command and the terminal command
Section titled “4.6 How-to docs — show both the slash command and the terminal command”A BoB task can usually be run two ways: as a slash command inside a Claude
Code session (e.g. /warp-drive), or from a terminal as a script / bin/
entrypoint / make target (e.g. bob warp, node ~/.claude/scripts/...). A how-to
must make both paths explicit and labelled, so a reader always knows which
world a command belongs to — never leave them guessing whether /doc-bootstrap
is something they type in a shell.
- Label every runnable command with the path it belongs to. Use the headings
Slash command(in a Claude Code session) andTerminal(a shell, outside any session), verbatim, every time. - Show both when both exist. Where a task has a slash command and a terminal entrypoint, present a labelled pair (two fenced blocks, or a two-column table). The Doc-Bootstrap how-to is the reference shape.
- Single-path tasks say which and why. If a task can only be done one way, show that one and add a one-line reason — e.g. “Terminal only — runs before any Claude session exists” (New Machine Setup) or “Slash command only — needs the session’s conversation context”. Do not invent the missing form.
- Never document a command that does not exist. A slash command must have a
commands/<name>.md(or a slash-invocable registry skill); a terminal command must be a realbin/entrypoint,maketarget, or~/.claude/scripts/...path. Verify before writing — a fabricated invocation is a correctness defect, not a style nit.
4.7 Vue-safe prose — no raw angle-bracket placeholders
Section titled “4.7 Vue-safe prose — no raw angle-bracket placeholders”The docs site (VitePress) compiles every page into a Vue template, so a raw
<placeholder> token in prose is parsed as an HTML element and fails the
site build (“Element is missing end tag”) — this class froze the published
site for days in 2026-07 (#988), and again for ~44 hours in 2026-09 (#2555).
Three constructs produce one:
- A bare token in prose: writing “pass –budget <usd>” without backticks.
Always backtick it:
--budget <usd>. - A backticked span wrapped across a line break whose continuation line
starts with the
<token>. The site build claims that line as raw HTML before backtick pairing runs, so the span never forms and the token lands in prose. Keep any code span containing<…>on a single line — rewrap the sentence, not the span. - A pipe inside a code span in a table row. A table splits each row into
cells on every unescaped pipe before backticks pair, so a span such as
jq '.jobs[] \| .name'written with a bare pipe is torn in two: its backticks end up unbalanced and whatever it held — a<label>token, a GitHub Actions expression — reaches the page as a raw element or a live Vue interpolation. Escape every pipe inside a table-cell code span as\|; the cell renders it as a plain pipe. Outside a table no escape is needed.
Fenced code blocks, single-line backticked spans, HTML comments, autolinks
(<https://…>), and deliberate standard HTML (<details>, <br>) are all
safe. Only non-HTML tag-shaped tokens in effective prose break the build.
Enforced by
scripts/docs-site/vue-safe-lint.js, which predicts the build verdict without running the build: it runs repo-wide inmake docs-check(and therefore in the CI docs gate) and per-commit in the doc-drift hook, which blocks a commit that adds a vue-unsafe token to a docs-site source. Escape hatch for projects with no docs site:_workflow.suppress_vue_safe_lint=true.
4.8 How-to and tutorial pages — instructions, not issue history
Section titled “4.8 How-to and tutorial pages — instructions, not issue history”A how-to grows by issue number when every fix leaves a sentence behind (“#1923 added the
preflight gate after a run started on a drifted machine”). That sentence is rationale,
and rationale belongs on the issue it cites — the page keeps only the instruction. The
doc-keeper rule issue-keyed-rationale reports any how-to or tutorial paragraph whose
first sentence contains an issue reference (#NNN or longer, outside code) and does not
open with an imperative verb as category needs-relocation, with the cited issue as the
finding’s found — the relocation target. A step that cites an issue in passing (“Run
bob ready to converge the project (#1922).”) is not flagged, and neither is any
reference or explanation page. It warns (§6.1); gate on it with
audit.js --fail-on-categories needs-relocation.
5. Deprecation & retirement
Section titled “5. Deprecation & retirement”Docs die. BoB keeps no in-tree graveyard: a retired doc is deleted, and git keeps
it under a named tag, so the live docs/ set is exactly the docs that are true today.
An in-tree docs/archive/ plus redirect tombstones grew to 18,664 words — a tenth of
the set — that every search, count and audit had to step around; it was retired to a
tag in #2089. Never delete a doc that anything links to without repointing or
removing those links in the same commit.
5.1 Decide the doc’s fate
Section titled “5.1 Decide the doc’s fate”| Situation | Action |
|---|---|
| Content moved into another doc | Repoint every inbound link to the new home, then delete the old file. |
| Doc is obsolete (nothing replaces it) | Tag it (§5.2), remove or rewrite each inbound link, then delete it. |
| Doc is a dated artifact (audit, status report) | Leave in audits/; it’s frozen, not deprecated. Never edit; never delete. |
5.2 Retirement tags
Section titled “5.2 Retirement tags”History lives on an annotated tag, not in the tree:
git tag -a archive/<YYYY-MM>-<slug> -m "<what is retired, by which issue>" # on the commit BEFORE the deletegit push origin archive/<YYYY-MM>-<slug>git show archive/<YYYY-MM>-<slug>:<path> # read a retired doc backThe deleting commit names the tag. Existing tags: archive/2026-09-dead-slices (dead
code, #2079) and archive/2026-09-docs (the former docs/archive/ — the 2026-02-pm/
and 2026-05-pm/ PM docs and six tombstones — #2089).
5.3 No tombstones
Section titled “5.3 No tombstones”A retired doc leaves no redirect file behind. A tombstone is a page that exists only to say another page exists; it clutters the namespace, the link graph and every count.
5.4 Redirect handling
Section titled “5.4 Redirect handling”- Internal links are repointed or removed in the deleting commit — the auditor’s
broken-relative-linkrule failsmake docs-checkon any link left dangling. - References to history (an explanation noting what a retired doc said) cite the
tag and path —
archive/2026-09-docs:docs/archive/how-to-auto-loop.md— never a relative link. - External / deep links (issues, external sites) to a deleted doc 404. That is the accepted cost; the tag keeps the content recoverable.
- Generated nav/indexes are derived from the directory contents (§3.1), so a deleted doc drops out of them with no edit.
5.5 Pages about a retired item
Section titled “5.5 Pages about a retired item”A doc can outlive the item it describes. Retiring a skill, command, agent or hook
retires the item, not the page. The record in scripts/lib/retired-items.json is
what tells readers about it (Retired Items).
-
Declare the page’s subject. A page about one item names it in frontmatter:
subject: {type: skills, name: dependency-auditing}When that item retires, or is scheduled to retire (
retires_onin its record), the site shows a banner above the provenance strip (§3.3). It says what happened or on which date the item retires, and links the replacement. Do not write the date or the replacement into the page. They are read from the record, or for a legacybobalias (type: alias) fromscripts/bob/gen-shims.shandcommands.json, so the banner cannot disagree with them. -
Don’t present a retired item as live.
make docs-checkrunscheck-retired-mentions. It fails any current doc line that links to a retired item, or tells the reader to use it, without naming its replacement or saying it is retired. Name the replacement, rewrite the line as history, or, for a deliberate mention such as an example of cleaning up a dangling link, add it toscripts/checks/retired-mentions-allow.jsonwith awhy. -
The generated references follow the record. A retired item is listed only in the Retired section of its generated reference page, never among the live items.
6. Enforcement & severity model
Section titled “6. Enforcement & severity model”This standard is enforced by the doc-keeper auditor (scripts/doc-keeper/audit.js,
run via make docs-check) and in CI (.github/workflows/ci.yml). Each finding
carries a severity; whether it blocks (fails CI) or warns (reported, non-
blocking) is set here — a single source of truth the tooling reads.
6.1 What blocks vs. warns
Section titled “6.1 What blocks vs. warns”| Finding | Category | Blocks CI? |
|---|---|---|
| Broken relative link | broken-link |
Blocks (severity high) — report-only until re-gated (#4001) |
Short-form priority label (p1 vs p1-critical); release notes under docs/releases/ are exempt |
label-name |
Blocks (severity high) — report-only until re-gated (#4001); fix with make docs-sync |
| Orphan doc (nothing links to it) | orphan |
Reports, never blocks (§6.2) |
Root *.md outside the allowlist |
root-placement |
Reports, never blocks (§6.2) |
| Missing / stale required frontmatter (stale = the §3.4 grade) | freshness |
Reports, never blocks (§6.2) |
Published page without an ISO last_reviewed (or generated_at on a generated page, §3.3) |
page-dates |
Blocks (make docs-check) |
Page graded stale (§3.4) |
doc-staleness |
Blocks (make docs-check); aging warns |
| Filename not lowercase-kebab | filename-violation |
Blocks (make check-doc-naming, now in CI) |
| Vue-unsafe angle-bracket token in prose, or a pipe tearing a table-cell code span (§4.7) | vue-safe-lint |
Blocks (make docs-check + commit-time doc-drift hook) |
| Single-source-of-truth duplication | duplication |
Warns |
| Diátaxis mode-mixing | mode-mixing |
Warns |
| Issue-keyed rationale paragraph in a how-to / tutorial (§4.8) | needs-relocation |
Warns |
| Component / registry count drift | component-count etc. |
Warns (judgment) for unmarked prose; a stale or hand-edited count marker Blocks (make check-doc-counts, a docs-check prerequisite — fix with make docs-sync, #2087) |
Blocking means make docs-check exits non-zero, failing the build. Every
auditor finding it prints names the page and line, the category and the fix, one
line each — make docs-sync for a mechanical finding, the edit to make for a
judgment one — so a red run says what to do (#4001). The auditor line in
docs-check runs with --report-only until it has had a clean week on master;
dropping that flag re-gates the high findings. Warning
findings still appear in the audit report and feed the curation worklist, but never
fail CI — they require human judgement (which copy is canonical, how to split a doc)
that a gate can’t make.
6.2 The baseline ratchet
Section titled “6.2 The baseline ratchet”The three findability categories (orphan, root-placement, freshness) break
nothing — the site still builds and every link still resolves — so they are
reported, never failing (#4001). The auditor refuses them in
--fail-on-categories (exit 2); they go in --report-categories. The baseline at
scripts/doc-keeper/audit-baseline.json grandfathers the existing ones, so the
report lists only the new ones:
make docs-checkruns the auditor with--fail-on-drift --report-categories orphan,root-placement,freshness --baseline scripts/doc-keeper/audit-baseline.json --report-only. A findability finding is listed, with its fix, only if its signature is not already in the baseline. A category passed to--fail-on-categories(for exampleneeds-relocation) fails the same way, on new findings only.- The baseline is accepted documentation debt, and the only correct direction is down. Remediation work (restructuring, archival, adding frontmatter) shrinks it; regenerate with
audit.js --write-baseline scripts/doc-keeper/audit-baseline.json --report-categories orphan,root-placement,freshnessafter a cleanup. When it reaches[], the report lists every finding with zero suppression. - Never grow the baseline to silence a new finding — that defeats the ratchet. New violations are fixed, not baselined.