The canonical documentation rules for Contentstack, and the toolchain that enforces them.
This repository holds a rule set and its enforcement layer. The first is a set of markdown files stating what a documentation page must look like, one file per doc type plus a shared rule spine. The second is a linter, a rule registry, a corpus sweep, a set of fixers and a test suite that turn those statements into findings a machine can report. A rule that lives only in prose is a suggestion. A rule in the registry with a check behind it is enforced.
Nothing here is published. Every file is either a rule, a template, a script or a test.
- Start here
- Repository map
- Pick a doc type
- Rule system
- Toolchain
- Workflows
- Wider setup
- Known gaps
- Editing this repo
Two generated companion files hold the exhaustive detail:
- REFERENCE-RULES.md states all 181 rules, with rationale, exception and enforcing check.
- REFERENCE-CHECKS.md maps all 86 check IDs to the module that implements each one.
Find your job in this table and go to the section it names.
| You want to | Command | Or read |
|---|---|---|
| Write a new documentation page | none, do it by hand | Pick a doc type, then workflow 1 |
| Fix an existing page against the rules | /revamp-doc, /revamp-api-ref or /revamp-cli-doc |
Workflow 2 |
| Audit a whole documentation corpus | /audit-docs |
Workflow 3 |
Declare doc_type: across a corpus |
/classify-doc-types |
Workflow 7 |
| Judge the rules no script can check | /adjudicate-tier3 |
Workflow 6 |
| Understand why the linter missed something | /doc-gap |
Workflow 4 |
| Add a rule, or write a check for one | /add-doc-rule |
Rule system, then workflow 5 |
| Check whether it is safe to merge | /doc-gate |
Editing this repo |
| Wire this up for a new project | /onboard-docs-repo |
Workflow 7 |
| Look up what a rule ID means | none | REFERENCE-RULES.md |
| Set this up on a new machine | none | Wider setup |
| Know what is broken today | none | Known gaps |
Four paths, and knowing which one is running matters when a result surprises you.
- The editor hooks. Every
WriteorEditof a markdown file under adocs/path in a consuming repo runs the dash check and then the linter. Tier 1 findings block the edit. This path runs on every documentation edit, without anyone invoking it. - The ten slash commands. One per task this repository can perform, from revamping a single page to running the pre-merge gate. They are git-tracked in
.claude/commands/and symlinked into~/.claude/commands/byscripts/install-commands.sh, so they work from any repository..claude/commands/README.mdis the index. - The npm scripts. Run manually from
scripts/when auditing a corpus, closing a gap or rebuilding generated data. - Agent context. An agent working on documentation in another repo loads
doc-templates/feature-docs/common-rules.mdand the matching type file as context.
Every doc-standards folder on this machine has been merged into this repository. This is the only copy that should be edited. Treat the copies under Studio/, MCP Profile Hub/, SDK Project/, CLI Project/ and Region Endpoints/ as read-only history, and point agents and skills at this repository rather than at a per-project copy.
MERGE-NOTES.md records how the merge happened, every conflict resolution and every rule-ID renumbering. Read it when you need to know why a rule has the number it has. Read this file when you need to know what the rules are.
| Path | Holds | Edited by |
|---|---|---|
doc-templates/feature-docs/ |
The shared rule spine and eight prose doc types | Anyone changing a prose rule |
doc-templates/api-ref/ |
Three API reference page shapes, plus one derivation record | Anyone changing an SDK reference page shape |
doc-templates/cli-templates/ |
The shared CLI rules and four CLI doc types | Anyone changing a CLI rule |
scripts/ |
The whole enforcement layer | Anyone adding a check, a fixer or a test |
scripts/checks/ |
42 check modules, one concern each | Adding or widening a check |
scripts/lib/ |
14 shared parsers and helpers | Adding a primitive two checks need |
scripts/data/ |
The rule registry, the check map, the wordlists, two generated tables | Adding a rule or a wordlist entry |
scripts/judges/ |
The four Claude-backed judges: tone, reading, tier-3 review, doc-type classification | Changing what gets sent to a model |
scripts/tools/ |
Corpus probes, wordlist audits, the mirror sync, the flat-doc merger | Maintaining the toolchain itself |
scripts/fix/ |
10 fixers, deterministic and model-backed | Fixing findings at corpus scale |
scripts/build/ |
Two generators that produce committed artifacts | After changing a section-order table or a rule |
scripts/test/ |
35 test files plus fixtures, 631 tests | Every change to any of the above |
MERGE-NOTES.md |
The merge history and the maintenance policy | When merging another fork |
REFERENCE-RULES.md |
Generated rule catalog | Never manually, run npm run build:readme |
REFERENCE-CHECKS.md |
Generated check catalog | Never manually, run npm run build:readme |
.doc-review/ |
Scratch output from the probe, review and judge scripts | Generated, and gitignored |
Twelve types are linted by lint-doc.js. Three more page shapes are linted by lint-api-ref.js. Getting this choice right matters more than any single rule, because the type determines which sections are required and which are forbidden. A misclassified page produces a long list of findings that are all wrong in the same way.
| The page is about | Type | Template |
|---|---|---|
| The product's single entry point | getting-started |
doc-templates/feature-docs/getting-started.md |
| A concept, behavior or pattern | conceptual-guide |
doc-templates/feature-docs/conceptual-guide.md |
| One product feature, from enabling it to configuring it | feature-doc |
doc-templates/feature-docs/feature-doc.md |
| One task, titled as an instruction | how-to-guide |
doc-templates/feature-docs/how-to-guide.md |
| Configuring an environment or SDK | setup-guide |
doc-templates/feature-docs/setup-guide.md |
| A runnable starter application | kickstarter |
doc-templates/feature-docs/kickstarter.md |
| Upgrading across a breaking change | migration-guide |
doc-templates/feature-docs/migration-guide.md |
| The landing page of a chapter | chapter-index |
doc-templates/feature-docs/chapter-index.md |
| The command surface of a CLI plugin | cli-command-reference |
doc-templates/cli-templates/cli-command-reference.md |
| One CLI operation, from first command to verification | cli-task-runbook |
doc-templates/cli-templates/cli-task-runbook.md |
| A CLI lookup table of identifiers | cli-module-reference |
doc-templates/cli-templates/cli-module-reference.md |
Building and publishing a csdx plugin |
cli-plugin-guide |
doc-templates/cli-templates/cli-plugin-guide.md |
| An SDK reference landing page | usage_guide |
doc-templates/api-ref/api-ref-usage-guide-v2.md |
| One SDK class | class_reference |
doc-templates/api-ref/api-ref-class-v2.md |
| One SDK method | method page | doc-templates/api-ref/api-ref-method-v2.md |
Declare the choice in the page's front matter as doc_type: <value>, using the value from the table. A declared type beats the linter's heuristic, which matters: on one corpus of 355 files the heuristic answered conceptual-guide for 327 of them, and the section rules then asked all 327 for sections they should never have. judges/classify-doc-type.js can declare the whole corpus in one pass.
Rule inheritance differs by family, so read the right pair of files.
| Family | Types | Rules that apply | Linter |
|---|---|---|---|
| Prose | The eight doc-templates/feature-docs/ entries above |
doc-templates/feature-docs/common-rules.md B1, B2, C1 to C9, plus the type file |
lint-doc.js |
| CLI | The four doc-templates/cli-templates/ entries |
The prose rules, plus doc-templates/cli-templates/cli-common-rules.md CLI-C1 to CLI-C15, plus the type file |
lint-doc.js |
| API reference | usage_guide, class_reference, method page |
The doc-templates/api-ref/ templates, plus the AR and UG rules |
lint-api-ref.js |
The API reference family needs its own linter rather than a flag on the shared one. An api-ref method page has no H1 title and no H2 sections, so the shared type detection falls through to conceptual-guide and the run fills with findings for a missing Overview and missing Prerequisites that the shape never had.
doc-templates/feature-docs/section-matrix.md is the cross-type lookup: one table of 26 sections against 12 doc types, each cell Required, Optional or Not used. Use it to answer "does this type take a Troubleshooting section" without opening four files. It does not cover the three api-ref shapes. /revamp-api-ref routes a reader to those, and doc-templates/api-ref/api-ref-usage-guide-v2.md and checks/api-ref-structure.js also enumerate them. One caveat before relying on the matrix. data/section-matrix.json is generated but never read at runtime. lib/section-index.js requires it and re-exports it unused, and nothing else in scripts/ touches it, so the matrix is documentation rather than enforcement.
Generated from scripts/data/section-order.json, which is what the linter actually reads.
| Type | Required sections after the title |
|---|---|
getting-started |
Overview, Role-Based Routing Table, Prerequisites, Quick Start, Documentation Map, Next Steps |
conceptual-guide |
Overview, Main Content, Next Steps |
feature-doc |
Overview, Main Content, Troubleshooting, Next Steps |
how-to-guide |
Overview, Prerequisites, Main Content, Next Steps |
setup-guide |
Overview, Prerequisites, Main Content, Troubleshooting, Next Steps |
kickstarter |
Overview, Prerequisites, Main Content, Next Steps |
migration-guide |
Overview, Prerequisites, Type Mapping Reference, Main Content, Gradual Migration, Troubleshooting, Pre-Upgrade Checklist, Next Steps |
chapter-index |
Overview, On this chapter, See also |
cli-command-reference |
Overview, Prerequisites, Commands, Examples, Limitations |
cli-task-runbook |
Overview, Prerequisites, Steps for Execution, Limitations |
cli-module-reference |
Overview, Quick Reference, Main Content |
cli-plugin-guide |
Overview, Prerequisites, Plugin Structure, Creating a Plugin, Plugin Registration and Linking, Commands and Flags, Publishing, Next Steps |
Every type also requires SEO front matter with title, description and url. A migration-guide requires a fourth key, version. An api-ref page is the exception: it carries exactly three keys, uid, seo_title and seo_description, and a method page leaves both SEO fields empty because it is a fragment rather than a standalone URL.
The forbidden list is the more useful half, because a page that carries a section belonging to another type competes with that type.
| Type | Must not contain |
|---|---|
chapter-index |
Troubleshooting, Prerequisites, Quick Start, Role-Based Routing Table, Documentation Map. It is navigation, so a fact a reader can learn only from the index is on the wrong page. |
kickstarter |
Theory sections. The page exists to get an application running. |
getting-started |
Deep conceptual explanation, exhaustive API detail, more than ten Quick Start steps. |
migration-guide |
Theory sections, and any unlabelled statement about a version. Every claim says which version it applies to. |
cli-command-reference |
A page-level Troubleshooting section. Link the troubleshooting hub instead. |
cli-task-runbook |
A page-level Troubleshooting section, more than one procedure spine, a command shown before the step that runs it. |
cli-module-reference |
Prerequisites, Troubleshooting, Limitations. Entries link out rather than duplicating what they link to. |
cli-plugin-guide |
A page-level Troubleshooting section. |
| Every CLI type | A heading below H3, because the renderer emits anchors for H2 and H3 only. |
usage_guide |
Per-method detail. It states what is true of the whole SDK. |
The chapter index type exists because of this failure mode. Fifteen index.md pages were being judged against the get started row, which produced 72 findings asking a chapter index for a Quick Start, a Role-Based Routing Table and a Documentation Map. A chapter index carrying all three would be a second get started guide.
A rule ID is a prefix plus a number. The prefix says which group states it and, indirectly, which file owns it.
| Prefix | Governs | Count |
|---|---|---|
B1 |
The ordered audit checklist. Stop at the first "No". | 11 |
B2 |
The anti-pattern table. | 9 |
C1 |
Structure and flow. Do, then understand, then debug. | 6 |
C2 |
Scannability. Tables, bullets, headings, diagrams, callouts. | 14 |
C3 |
Language and tone. The largest group. | 30 |
C4 |
Code versus prose. | 8 |
C5 |
Cross-references. | 6 |
C6 |
Content accuracy and grouping. | 8 |
C7 |
Duplication. | 6 |
C8 |
Developer tone. No marketing language. | 9 |
C9 |
CLI command documentation, from the prose side. | 4 |
FM |
Front matter. | 2 |
RS1 to RS3 |
Get started routing, quick start, exclusions. | 11 |
MIG |
Migration guide specifics. | 9 |
AR |
API reference page anatomy. | 10 |
UG |
Usage guide anatomy. | 13 |
CLI |
Shared CLI rules. | 20 |
PLG |
Plugin guide specifics. | 5 |
Three more groups exist in prose only and have no registry entry, so nothing enforces them. CMD1 to CMD3 in the command reference, RUN1 to RUN5 in the task runbook, and MOD1a to MOD4 in the module reference. See Known gaps.
IDs are dense but not category-semantic. C3-13 is the retry-count rule and C3-14 the ordered-list rule, which are not siblings in meaning. Never infer the next number from a topic.
The tier determines what happens when a rule fires, and it is the concept that explains hook behavior, the gate and the whole review loop.
| Tier | Rules | Behavior |
|---|---|---|
| 1 | 78 | Blocks. The linter's exit code comes from the tier-1 count alone, and the editor hook refuses the edit. |
| 2 | 57 | Advisory. Reported as context, never blocking. |
| 3 | 46 | Needs a reader. No automated check, adjudicated by a human or by a model through the review loop. |
A tier-3 rule must not name a check, and a tier-1 or tier-2 rule must name one. lib/rules-registry.js enforces both directions.
scripts/data/rules-registry.json is the machine-readable source of truth. 181 entries, each with eight fields.
| Field | Meaning |
|---|---|
id |
The stable identifier a finding reports |
source |
The markdown file that states the rule in prose |
docTypes |
all, or the specific types it is scoped to |
rule |
What the rule requires |
why |
The rationale, usually the incident that produced it |
exception |
When it does not apply |
tier |
1, 2 or 3 |
checkId |
The check that enforces it, absent for tier 3 |
lib/rules-registry.js validates the registry as one artifact, and this is deliberate. A second registry in a subfolder was tried and reverted, because it sits outside this validation, which is how a rule ships claiming coverage that never existed. The load-bearing invariant is the last one: a check that emits a rule ID owned by a different check reports the wrong rule text to the reader while looking perfectly healthy. That happened once, when the ordered-list check shipped emitting C3-13, which belongs to the retry-count rule.
Which file states which group:
| Source file | Rules |
|---|---|
doc-templates/feature-docs/common-rules.md |
111 |
doc-templates/cli-templates/cli-common-rules.md |
19 |
doc-templates/api-ref/api-ref-usage-guide-v2.md |
13 |
doc-templates/feature-docs/getting-started.md |
11 |
doc-templates/api-ref/api-ref-method-v2.md |
10 |
doc-templates/feature-docs/migration-guide.md |
9 |
doc-templates/cli-templates/cli-plugin-guide.md |
5 |
doc-templates/cli-templates/cli-command-reference.md |
1 |
section-order.json and parse-markdown.js |
2, both synthetic |
Compute the next ID. Never hand-count it.
cd scripts
npm run probe -- --next-id=C3This returns one past the highest number the prefix holds. test/gap-loop.test.js asserts that as an invariant rather than as a literal, so the guard survives every future rule.
Everything runs from scripts/. Every corpus-taking script defaults to ../../docs, so the layout it expects is this repository sitting beside a docs/ folder inside a consuming project. No script names a specific project.
Grouped by job rather than alphabetically.
| Job | Command | What it does |
|---|---|---|
| Lint one prose page | npm run lint -- <file> --type=<type> |
Runs all 42 checks, prints findings by tier. Exit code from the tier-1 count. |
| Lint an api-ref page | npm run lint:api-ref -- <file> |
The AR and UG checks, for the three api-ref shapes. |
| Sweep a corpus | npm run sweep |
Lints every prose doc under a target and aggregates findings by rule, so you see the ranking. |
| Sweep blocking only | npm run sweep:tier1 |
The same, tier 1 only. |
| Check links | npm run links |
Three-layer link and anchor check across the corpus. |
| Declare doc types | npm run classify, then classify:apply |
Judges each page's type and writes doc_type: into its front matter. |
| Judge tone | npm run judge:tone |
The model-backed judge for the judgment-bearing rules. |
| Judge counts and labels | npm run reading |
C2-13, a stated count that disagrees with what it counts, and C2-14, a link label that misdescribes its destination. |
| Review tier 3 | npm run review, then judge, then --reconcile |
Generate candidates, judge them, reconcile verdicts. |
| Probe for a gap | npm run probe |
Finds every instance of a phrase and classifies why the linter missed it. |
| Fix dashes | npm run fix:dashes |
The deterministic C3-05 pass. |
| Audit wordlists | npm run audit:wordlists |
Finds literal entries that are narrower than their rule. |
| Mirror two folders | npm run mirror, mirror:fix |
Restores byte-identical parity between a source and its mirror. |
| Rebuild section data | npm run build:section-order |
Regenerates the two generated JSON tables. |
| Rebuild the catalogs | npm run build:readme |
Regenerates the two REFERENCE files. |
| Everything that gates | npm run gate |
test, then links, then sweep:tier1. Run before any commit. |
| Run the tests | npm test |
631 tests across 35 files. |
The judges and the mirror sit outside gate on purpose. The judges cost money per call and need the claude CLI, so they must never run on a keystroke. The mirror rewrites files.
npm script
|
v
lint-doc.js / lint-api-ref.js / sweep-docs.js
|
+--> checks/*.js 42 modules, one concern each
| |
| +--> lib/*.js 14 shared parsers and helpers
| +--> data/*.json wordlists and generated tables
|
v
lib/report.js findings keyed by rule ID and tier
|
v
exit code from the tier-1 count
lint-doc.js answers "is this file clean". sweep-docs.js answers "how often does this fire, and where else", which is the question that drives a rule change, because the ranking says which single wordlist entry or widened pattern buys the most.
A check module exports a function taking the parsed document and returning findings. lint-doc.js holds them in one CHECKS array, and a new check is wired in by adding it there. The five kinds, from data/check-sources.json:
| Kind | Count | Meaning |
|---|---|---|
structural |
39 | Parses the document model and reports on shape, order or completeness. |
unimplemented |
21 | Registered with no module behind it. The rule is stated and tiered, and nothing enforces it. |
regex |
16 | Matches a named pattern against prose, outside code fences. |
wordlist |
8 | Matches entries from a JSON data file, so the rule widens by data rather than by code. |
candidate |
2 | Emits a tier-3 candidate for adjudication rather than a finding. |
Four modules carry a disproportionate share of the rules. api-ref-structure.js owns 18 check IDs, which is every AR and UG rule. banned-phrases.js addresses 13 rules from wordlist data. tier3-candidates.js is the whole tier-3 candidate engine and states the principle the judges follow, that a candidate is a question and a lint failure has to be an answer. section-structure.js drives section order off the generated JSON.
Per-check detail is in REFERENCE-CHECKS.md.
These are the reuse surface. Before writing a helper inside a new check, look here, because most of these exist precisely because two checks each carried their own copy and the copies disagreed.
| Module | Provides |
|---|---|
parse-markdown.js |
The parser. Sections, tables, fences, front matter. |
doc-model.js |
Convenience accessors over a parsed document, including prose-only line ranges. |
prose-mask.js |
One definition of which characters on a line are prose. The reason no check fires inside a code fence. |
section-index.js |
Maps a section-order row to a real heading, handling the rows that are not literal H2 headings. |
rules-registry.js |
Registry access, tier and doc-type filtering, nextRuleId, and the validation invariants. |
report.js |
The Finding shape, and tier semantics. |
slugify.js |
One definition of the anchor a heading gets. There were two, and they disagreed on 180 of 3,314 headings. |
markdown-links.js |
One definition of how a link is read off a page, shared by the link checker and the label check. |
table-shape.js |
Where a table row's cells begin and end. Exists because a punctuation pass corrupted 19 rows and nothing caught it. |
similarity.js |
Word-shingle overlap, the shared basis of every duplication check. |
phrase-list.js |
The phrase-matching primitives shared by the wordlist checks and the gap probe. |
inflect.js |
Generates the surface variants a literal wordlist entry fails to cover. |
corpus-class.js |
Which corpus a file belongs to, and therefore which page-shape rules apply. |
claude-runner.js |
One place to shell out to the claude CLI in headless mode, with a re-prompt on a violated constraint. |
data/ holds the registry, the check map, the wordlists and two generated tables.
The wordlist folders are banned-phrases/ with 12 files, passive-voice/ with 9, anthropomorphism/, metaphors/, periphrasis/, vague-reference/ and house-verbs/, plus acronyms.json and wordy-connectors.json. A wordlist rule widens by adding an entry, which is why it needs no code change and why audit:wordlists exists to catch entries narrower than their rule.
section-order.json and section-matrix.json are build output. build/build-section-order.js produces them from the ## Section Order tables in doc-templates/feature-docs/*.md and doc-templates/cli-templates/*.md and from doc-templates/feature-docs/section-matrix.md. The linter never parses those markdown files at runtime, it reads only the generated JSON. Editing a section-order table without rebuilding changes nothing.
corpus-classes.json determines which files count as documentation pages. It exists because rules describing a page's shape were firing on files whose shape something else owns. On one corpus, 2,558 of 3,789 tier-1 findings were on files that are never published, including 254 asking for SEO front matter on an agent skill file. Override it per project with the DOC_STANDARDS_CORPUS_CLASSES environment variable, which is tried before the built-ins and merged over them. A malformed override is reported and ignored.
Two kinds. A deterministic fixer rewrites text by rule. A model-backed fixer shells out to the claude CLI once per finding, because the edit needs judgment.
| Fixer | Rules | Kind |
|---|---|---|
fix-dashes.js |
C3-05 | Deterministic. C3-05 was 78 percent of all tier-1 findings on one corpus, 4,497 lines across 169 files, which is far too many for one model call each. |
fix-emoji-italics.js |
C3-27, C3-28, C3-30 | Deterministic, with an optional model pass. |
fix-acronym-first-use.js |
C8-07 | Deterministic. Expands a closed-list acronym at its first bare use, once per acronym per file. |
fix-banned-phrases.js |
The banned-phrase rules | Model-backed. Swapping a phrase in verbatim breaks grammar. |
fix-concision.js |
C3-07 | Model-backed. Judging a stacked clause needs reading. |
fix-tense.js |
C3-19 | Model-backed. Mechanical swaps break agreement about half the time. |
fix-anthropomorphism.js |
C3-18 | Model-backed. Four verbs carried 480 of 648 findings. |
repair-anchors.js |
None directly | Keeps links working across a pass that rewrites headings. A heading's anchor comes from its text, and 794 headings on one corpus contained a dash. |
finish-dash-pass.js |
None directly | Runs every step that must follow a corpus-wide C3-05 pass, in order. The order is load-bearing, and two of the steps produce no error when run out of sequence. |
verify-edit-integrity.js |
None directly | Proves a punctuation pass changed punctuation and nothing else. |
Read that last row before running any fixer at corpus scale. A --report showing zero remaining findings proves only that the findings are gone. It says nothing about whether a sentence, a table row, a code sample or a link survived.
Three scripts call a model because the rule needs a reader.
| Script | Rules | Note |
|---|---|---|
judges/judge-tone.js |
C3-18, C3-21, C2-09, C3-25 | Also has a discovery mode that hunts wording no wordlist covers. |
judges/judge-reading.js |
C2-13, C2-14 | A stated count that disagrees with what it counts, and a link label that misdescribes its destination. |
judges/classify-doc-type.js |
None. It declares types. | Replaces a heuristic that answered conceptual-guide for 327 of 355 files. |
All three need the claude CLI and cost money per call. None is in gate, and none may ever run inside a check.
npm test runs node --test test/*.test.js. 631 tests across 35 files, all passing, none skipped.
Fixtures work as matched controls: clean-feature-doc.md beside broken-feature-doc.md, clean-cli-command-reference.md beside its broken twin, and api-ref-good/ beside api-ref-broken/. A clean fixture that starts reporting findings is as much a failure as a broken fixture that stops.
The rule that governs a new test comes from the gap loop: a regression test that has never failed is not yet a test. Write the test, watch it fail against the unfixed code, then fix.
- Select the type from Types by subject.
- Read
doc-templates/feature-docs/common-rules.mdB1 and B2, then the type file, in full. - Declare
doc_type:in the front matter. - Build the page in the section order the type requires.
- Lint it:
cd scripts && npm run lint -- ../path/to/page.md --type=<type>. - Fix every tier-1 finding. Read the tier-2 findings and fix what is genuinely wrong.
Run /revamp-doc and let it drive. It detects the type, lints, reads the rule files, runs the B1 and B2 audit, then edits in place. Use /revamp-api-ref for an api-ref page and /revamp-cli-doc for one of the four CLI types, since each family has its own templates and its own linter.
The three revamp commands cover all fifteen page shapes between them, and each one routes a page that belongs to another family rather than linting it as the wrong type. /revamp-doc now handles chapter-index as well as the seven other prose types.
Never write a -revamped.md sibling. Confirm the file is clean in git status first, then edit in place.
cd scripts
npm run sweep # every finding, ranked by rule
npm run sweep:tier1 # blocking findings only
npm run links # links and anchorsRead the ranking rather than the file list. The top rule by count is where a single wordlist entry or a widened pattern buys the most.
Someone spotted a violation the linter did not catch. Run /doc-gap, which drives the whole loop. When the rule is already decided and only the check is missing, run /add-doc-rule instead. The shape of it:
- Restate the violation as a probe label.
- Baseline with
sweep-docs.js. - Seed a probe and run
tools/probe-corpus.js. - Read the corpus manually for meaning-siblings. This is the step no script can do.
- Widen, and apply the convergence test.
- Classify the gap as
NOT_A_GAP,WORDLIST_GAP,REGEX_TOO_NARROWorNO_RULE. - Apply the decision rule: add a wordlist entry, or widen a regex, or add a module and an ID. The tie-breaker is that if you have to rewrite the rule sentence, it is a new rule.
- Codify and test.
npm run probe -- --next-id=<PREFIX>for the ID.- State the rule in prose in the owning markdown file, in the
Rule,Why,Exceptionformat. - Add the registry entry with all eight fields.
- Add the
data/check-sources.jsonentry. - Write the check in
checks/, and wire it into theCHECKSarray inlint-doc.js. - Extend a fixture and add a test. Watch it fail first.
npm run build:readmeto regenerate the catalogs.npm run gate.
Skipping step 4 or 7 leaves the registry claiming coverage the catalog does not show, which is the exact failure the validation exists to prevent.
cd scripts
npm run review # generate candidates
npm run judge # ask the model, one candidate at a time
npm run review -- --reconcileGeneration only writes candidates and judgment only writes verdicts, one direction each, so regeneration is idempotent and cannot clobber a judgment already made.
- Place this repository so that
../../docsfromscripts/resolves to the project's docs folder, or pass a target explicitly. - If the project has non-documentation markdown under
docs/, write a corpus-class file and pointDOC_STANDARDS_CORPUS_CLASSESat it. Skipping this is what produces thousands of findings on files nobody publishes. - Run
npm run classifyand thenclassify:applyto declaredoc_type:across the corpus. - Baseline with
npm run sweep:tier1. - Confirm the editor hooks are installed, per the next section.
Three pieces outside this repository make it work. All are per-machine, in ~/.claude/.
The commands now live in this repository, at .claude/commands/, and are the same artifacts the
symlinks in ~/.claude/commands/ point at. Run bash scripts/install-commands.sh once per machine.
.claude/commands/README.md holds the full index and the shared path resolver.
| Command | Does |
|---|---|
/revamp-doc |
Brings one prose page into compliance. Detects the type, lints, reads the rule files, audits, edits in place |
/revamp-api-ref |
The same for the three api-ref page shapes, using lint-api-ref.js and the api-ref templates |
/revamp-cli-doc |
The same for the four CLI types, and carries the sixteen unenforced CLI rules as a manual checklist |
/audit-docs |
Sweeps a corpus and ranks findings by rule. Routes api-ref subtrees to their own linter |
/classify-doc-types |
Declares doc_type: across a corpus, generate then judge then reconcile then apply |
/adjudicate-tier3 |
Generates tier-3 candidates, judges each one, and reconciles so a partial review cannot read as complete |
/doc-gap |
Turns a missed violation into a permanent check with a test. Diagnoses first |
/add-doc-rule |
Codifies a rule already decided, with its registry row, check, fixture and test |
/doc-gate |
The pre-merge check. Runs the gate, then verifies the generated catalogs are current |
/onboard-docs-repo |
Wires the toolchain to a new project, corpus classes first, then a baseline |
scripts/test/commands.test.js guards them. It asserts all ten are present, that each carries the
shared resolver verbatim, that none has reverted to the single-layout form, that none hardcodes a
consuming project, and that none passes a flag its linter would reject.
~/.claude/settings.json registers a PostToolUse hook chain on Write|Edit, in this order:
~/.claude/hooks/check_doc_dashes.py, 15 second timeout. Enforces C3-05 in Python, outside fenced code and inline code spans. Blocks.~/.claude/hooks/lint_doc_standards.py, 60 second timeout. Runs the repository's own linter.
The second hook is worth understanding, because its behavior is deliberate at three points.
- It walks up from the edited file looking for
doc-standards/scripts/lint/lint-doc.jsrather than hardcoding a path, because the linter is checked into the repository it lints and this repository is not the only consumer. - Tier 1 blocks so the agent self-corrects. Tier 2 arrives as non-blocking context, because blocking on every passive-voice candidate would make editing documentation impossible.
- It fails open everywhere. A hook that fails closed blocks all editing.
It excludes three path markers: doc-standards/, .doc-review/ and doc-standards/scripts/test/fixtures/. Fixtures carry violations on purpose, so linting them would block every edit to the test suite.
Note the asymmetry. The dash hook treats doc-standards/ as a documentation path and so does check this repository's own prose, while the linter hook excludes it.
~/.claude/CLAUDE.md points at ~/.claude/global-instructions/doc-writing-rules.md for all documentation work, and says to prefer a project-level doc-standards folder for anything it covers.
Treat this repository as authoritative. That global file states its own provenance as a static distillation of one of the five July forks, and it has drifted. It hard-states that passive voice is never acceptable, which contradicts C3-10's exception for predicate adjectives, and it adds rules that canonical C3 does not carry.
git clone https://github.com/aravindh-cstk/doc-standards.git
cd doc-standards/scripts
npm test # expect 631 passingThen place revamp-doc.md and doc-gap.md in ~/.claude/commands/, place both hook scripts in ~/.claude/hooks/, and add the PostToolUse block to ~/.claude/settings.json. Confirm the chain works by writing a deliberate em dash into a markdown file under a docs/ path and checking that the edit is refused.
Accurate as of 2026-09-08. These are documented rather than fixed, so nobody trusts a rule that is not enforced or follows a path that no longer resolves.
Counts are the linter's own tier-1 findings, not a raw character scan, so a glyph inside a code fence or an inline code span is already excluded.
| File | Breaks |
|---|---|
doc-templates/feature-docs/getting-started.md |
C3-05, 25 findings across 12 em dashes, 5 en dashes and 11 semicolons |
doc-templates/feature-docs/common-rules.md |
C3-27, 15 findings across 17 arrow glyphs. C3-28, 1 |
doc-templates/feature-docs/migration-guide.md |
C3-27, 1 arrow glyph |
doc-templates/api-ref/api-ref-method-v2.md |
C3-27, 7 findings across four check marks, one cross mark and two arrows. C3-28, 4 |
doc-templates/api-ref/api-ref-class-v2.md |
C3-27, 1 arrow glyph. C3-28, 3. C8, the superlative in its Optional but powerful bullet label |
doc-templates/api-ref/api-ref-usage-guide-v2.md |
C3-28, 7 |
doc-templates/api-ref/usage-guide-derivation.md |
C3-28, 10 |
The 25 C3-28 italics findings fall into two mechanical patterns. The *(required when X)* parenthetical idiom accounts for 14 across the three api-ref templates, and the italic paragraph lead-in label accounts for 9 in usage-guide-derivation.md.
doc-templates/api-ref/usage-guide-derivation.md also contains an en dash, a semicolon and four arrows, and the linter correctly reports none of them. They sit inside a fence the author opened deliberately, saying so at line 83: quoted verbatim in a fenced block because the source punctuation does not follow this repository's house style. Whether a fence is an acceptable escape hatch for quoted text is an open question, not a violation.
MERGE-NOTES.md claims every file in the folder avoids these characters. That claim is currently false.
Every other entry in this section is a missing finding. These three are wrong findings, which costs more trust, and all three are reproducible today.
- A correct callout label fires a false error.
checks/callout-taxonomy.jsaccepts only the singularAdditional Resourceand emitsC2-11at tier 1.AR-06requires the plural whenever a callout carries two or more links, andchecks/api-ref-structure.jsenforces that agreement. So a callout correctly written> **Additional Resources:**for three links is a tier-1 error.test/fixtures/api-ref-good/Taxonomy/class_reference.mdcarries a plural label and escapes only becauselint-api-ref.jsexcludes this check. - A compliant Quick Reference fires a false error.
checks/quick-reference-table.jshard-codes the expected columns as Use case, Section and Key call, and emitsC2-04at tier 1 for a missing one.MOD2indoc-templates/cli-templates/cli-module-reference.mdspecifies a different shape for this type, each module or command mapped to its anchor. A module reference built to its own template fails the linter. - The drift form the rule exists to catch is invisible. The blockquote pattern in
checks/callout-taxonomy.jsmatches only the colon-inside-the-bold form, so> **Note**:matches nothing and produces no finding, from eitherC2-11orCLI-09.CLI-09is the rule whose entire purpose is that spelling.
The nine prose rule files moved from the repository root into doc-templates/feature-docs/ on 2026-09-08, and four references were not updated. Each still points at a root common-rules.md or section-matrix.md that no longer exists.
doc-templates/api-ref/api-ref-usage-guide-v2.md, line 18, which also still says "the repo root"doc-templates/api-ref/api-ref-method-v2.md, line 72doc-templates/api-ref/usage-guide-derivation.md, lines 484 and 487
Line 18 carries a second error in the same sentence. It says the class and method templates apply common-rules.md the same way, but api-ref-class-v2.md never references that file.
Fourteen more references across nine doc-templates/feature-docs/ files say common-rules.md with no path. Those resolve correctly, because the files sit in the same folder, so they are an inconsistency rather than a break. The occurrences in MERGE-NOTES.md describe the pre-move layout and are correct as history.
Two paths in doc-templates/cli-templates/cli-common-rules.md, at lines 223 and 249, carry a doc-standards/ prefix that resolves from one directory up rather than from the repository root. The files exist at scripts/data/banned-phrases/absent-docs.json and scripts/checks/internal-link-form.js.
doc-templates/cli-templates/cli-common-rules.md defines CLI-C1 to CLI-C15, with no gaps. Three of the four CLI templates tell the reader to apply CLI-C1 to CLI-C14, so CLI-C15 is invisible from them. Only cli-plugin-guide.md cites the full range. The shared file returns the favor: its own line 5 lists the per-type files and omits cli-plugin-guide.md.
One rule carries two numbers. cli-command-reference.md calls the Installation rule CMD2, while the registry entry CLI-07 and checks/cli-specific.js both call it CMD4.
21 entries in data/check-sources.json carry kind: unimplemented, covering 23 rules between them. Each rule is stated and tiered, and nothing enforces it, so a page can break it and lint clean: AR-10, B1-07, B2-07, C4-02, C4-03, C6-03, C9-01 to C9-03, CLI-08 to CLI-15, CLI-18, CLI-20, and PLG-01 to PLG-04.
Twelve of those checkId values are placeholders that name a different rule than the one they are attached to, which is a trap for whoever writes the check. CLI-08 is the placeholder-bracket rule but its checkId is cli-flag-prose-heuristic. CLI-10 is the no-shell-prompt rule but its checkId is cli-error-entry-heuristic. Renaming one means editing both the key in data/check-sources.json and the checkId on the rule, because lib/rules-registry.js fails on a mismatch.
Not every one of the 23 can become a check. On inspection, 13 are decidable by a parse or a regex, 7 can only flag candidates and belong at tier 2 or tier 3, and 3 genuinely need a reader and should be retiered: AR-10, because a parameter default's provenance is not on the page, CLI-15, whose own rationale names the defeating case of a flag that kept its name and changed its meaning, and RUN3, because correct step order is a fact about the operation.
A further 18 pairs sit in the unemitted-claims list, where a module owns the rule but never names its ID. Fourteen are legitimate: one emitted finding covers several registry rules, or the ID is built from a variable that the source scan cannot see. Four are real, all under callout-frequency. B1-08 and C5-01 ask whether every outbound callout has been classified, which needs a reader. B2-04 and C5-03 forbid a callout placed mid-flow, which is mechanically checkable and simply unwritten. test/gap-loop.test.js freezes the list so it can shrink but not grow unnoticed. Both lists are in REFERENCE-CHECKS.md.
Four of the twelve linted types have no type-specific registry entries. Nothing cites doc-templates/cli-templates/cli-task-runbook.md, doc-templates/cli-templates/cli-module-reference.md, doc-templates/feature-docs/chapter-index.md, doc-templates/feature-docs/section-matrix.md or doc-templates/api-ref/api-ref-class-v2.md as a source, and cli-command-reference.md supplies exactly one rule.
Having no registry entry is not the same as having no enforcement. Five of these prose rules are already enforced under another rule's ID, which is why they never needed one:
| Prose rule | Enforced as |
|---|---|
CMD3 and RUN5, no Troubleshooting section |
CLI-19, and section-structure.js keys it off whether the doc is a CLI doc rather than off its type |
RUN4, Limitations is required |
C1-01, through the required row in section-order.json |
MOD2, a Quick Reference must be present |
C1-01, the same way. Its completeness half is not checked |
| The chapter-index ban on Quick Start, Role-Based Routing Table and Documentation Map | C1-01, through the get-started-only forbidden list |
What genuinely has no enforcement: CMD1, the other direction of CMD2, RUN1, RUN2, RUN3, MOD1a, MOD1b, the completeness half of MOD2, the Prerequisites and Limitations halves of MOD3, MOD4, and four of the six chapter-index rules, which /revamp-doc now enforces by eye in its Step 5 rather than by script. The cheapest of those is MOD3, because section-structure.js already has the exact per-type forbidden-list shape it needs, and compareOrder already computes an unexpected array that nothing reads.
- The callout-label rule appears twice in C2 with inconsistent wording. One states four valid labels ending in
Additional Resources, plural, and has no registry entry. The other states four ending inAdditional Resource, singular, and isC2-11. The implemented check enforces the singular, which is the first of the three bugs above. C2 has a wider bookkeeping problem: 15 prose rules against 14 registry IDs, with two prose rules unregistered andC2-10registered with no prose counterpart. - Three rules state that no CLI doc carries a page-level Troubleshooting section.
CLI-19at tier 1 covers the command reference, task runbook and module reference.PLG-05at tier 1 covers the plugin guide. Both are enforced bysection-structure.js, so all four CLI types are in fact covered.CLI-18restates the same thing at tier 2 across every CLI doc, and itscheckIdis unimplemented, which makes it redundant rather than a coverage hole. It still counts toward the unimplemented list above.
- The editor hook only ever runs
lint-doc.js. Editing aclass_reference.mdor a method page in a consuming repository produces findings for sections that shape never had, becausedetectDocTypereads it as a conceptual guide. Route those pages through/revamp-api-refand read hook output on them with suspicion. The fix is a filename test in~/.claude/hooks/lint_doc_standards.py. lint-api-ref.jsfilters by filename only when its target is a directory. Given a single file it lints it whatever the name, treating anything that is not a class page or usage guide as a method page. Pointed at a prose page it reports a full set of confident nonsense rather than refusing.- Six
CLIandPLGcheck names indata/check-sources.jsondo not describe the rule they are attached to.cli-exit-code-heuristicis bound toCLI-15, which is about re-verifying a derived doc, andcli-error-entry-heuristictoCLI-10, which is about shell prompts in code fences. All areunimplemented, so nothing behaves wrongly today, and a reader picking a name up as a specification would be misled.
doc-templates/cli-templates/cli-common-rules.md cites notes/reports/flag-inventory.json twice and scripts/gen_flag_accuracy_report.py once. Neither exists here, both were CLI Project artifacts. changelog/, troubleshooting/ and repo/cli-plugins in the same file are consuming-repo paths, not paths in this repository.
The cost is not even across the two. At line 79 the missing filename is incidental, because the instruction that matters, verify against the oclif.manifest.json in the published npm tarball, survives without it. At line 201 the entire "How to check" procedure for CLI-C11 is built on both missing files, so that rule currently ships with no way to satisfy it. Its four defect classes are worth keeping as a manual checklist.
doc-templates/feature-docs/writing-guide-agent-skills.md used to maintain a second copy of the C8 marketing table, the jargon table with the same per-word fixes, and the acronym list. Its scope note said it supplemented rather than replaced C8, and the two copies drifted independently anyway. The four rules it held that C8 did not, the marketing call to action, the audience-enumerating opener, the unnamed-mechanism intelligence claim and rhetorical immediacy, are now C8 rules, and its before-and-after table is the one at the end of C8. The file is gone.
doc-types/api-reference/SKILL.md used to sit here as a routing file that owned no rules. It was named SKILL.md and no skill loader read it, because it sat outside any .claude/skills tree. Its two useful sections, the test for whether a page belongs to the api-ref family and the reason the AR-* rules live in the root registry, now sit in /revamp-api-ref, which is where a reader is already looking when they need either one. The folder is gone.
Five conventions.
- This prose follows these rules. No em dashes, no en dashes, no semicolons, per C3-05. No emoji and no arrow glyphs, per C3-27. No italics, per C3-28. The dash hook checks this repository, so a violation blocks the edit.
- Edit this copy only. Every other
doc-standardsfolder on the machine is read-only history. - Rebuild after a table change. Touching a
## Section Ordertable meansnpm run build:section-order. The linter reads only the JSON. - Rebuild after a rule change. Adding or retiering a rule means
npm run build:readme, or the catalogs drift from the registry. - Gate before committing.
npm run gateruns the tests, the link check and the tier-1 sweep.
For more information on how the current rule set was assembled, refer to MERGE-NOTES.md.