Skip to content

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).


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.

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:

  1. 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.
  2. 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) and verification-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”

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/*.md lives in a category subdirectory. A bare *.md at the top of docs/ is a placement violation (the one exception is a generated docs/index.md navigation root, if present).
  • The category must match the doc’s type frontmatter. type: reference in docs/how-to/ is a contradiction the auditor flags.
  • Filenames are lowercase kebab-case (^[a-z0-9]+(-[a-z0-9]+)*$), extension .md for prose — already enforced by make 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_reviewed expectation — their last_reviewed is their publication date and stays there.

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.


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 | explanation
owner: paulirv # required — GitHub handle accountable for accuracy
last_reviewed: 2026-06-22 # required — ISO 8601 (YYYY-MM-DD) date last verified true
expires: 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:

  • type is the single source of a doc’s mode. The H1 title, the directory, and type must agree; disagreement is an auditor finding.
  • last_reviewed means “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.md and warp-drive-flags.md are committed here, so their generators read owner from the page’s own frontmatter (people.productDocOwner(), #2689) — never from the resolved operator, and never from the origin remote 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 no owner yet takes the visible placeholder unassigned until a human sets one.
  • A doc generated into an operator’s own project takes owner from the operator profile. The doc-bootstrap drafts and the runbook scaffold write the profile’s doc-owner role (people.docOwner(), scripts/lib/operator-people.js, #2269), falling back to the origin remote’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.
  • owner is 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 carry type: reference and last_reviewed set to the publication date (the doc-keeper markdown report writes both itself). They are outside the type/owner governance 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 via make docs-sync. make docs-check verifies 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 title and description. 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 INDEX markers 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 description degrades 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.md lists the tutorials in their order: with a Step and a Prerequisites column. The order is still declared, just by each tutorial itself, not by the index. section-index.js refuses to generate the index when a prerequisite names no tutorial, a prerequisite is not earlier on the path, two tutorials share a step, or an order: 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.

Terminal window
node scripts/doc-keeper/orphan-check.js --staged # what the gate will say
node scripts/doc-keeper/orphan-check.js --all # whole-tree, ad hoc

The 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.

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.

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 --foo flag 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. A make check target 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 from docs/reference/generated/counts.json; make check-doc-counts fails 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 #N for 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`.
  • #N inside 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 into make check), which flags item-word + bare #N in the issue-authoring command templates and can lint a generated body file before gh 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) and Terminal (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 real bin/ entrypoint, make target, 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 in make 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.


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.

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.

History lives on an annotated tag, not in the tree:

Terminal window
git tag -a archive/<YYYY-MM>-<slug> -m "<what is retired, by which issue>" # on the commit BEFORE the delete
git push origin archive/<YYYY-MM>-<slug>
git show archive/<YYYY-MM>-<slug>:<path> # read a retired doc back

The 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).

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.

  • Internal links are repointed or removed in the deleting commit — the auditor’s broken-relative-link rule fails make docs-check on 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.

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_on in 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 legacy bob alias (type: alias) from scripts/bob/gen-shims.sh and commands.json, so the banner cannot disagree with them.

  • Don’t present a retired item as live. make docs-check runs check-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 to scripts/checks/retired-mentions-allow.json with a why.

  • 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.


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.

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.

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-check runs 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 example needs-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,freshness after 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.