## Summary PR 1 corrected 9 annotations that claimed enforcement the config doesn't provide. This adds the check that keeps them correct, because the same drift will recur: annotations are prose, `.vale.ini` is config, and nothing tied them together. `scripts/styleclaims` reconciles three sources and fails `make lint`. It also runs in `lint-light`, so the fast pre-commit hook covers docs-only changes: the checker's inputs all live under `docs/.style/`, and leaving it out would let this drift land through the hook that docs changes actually use. It is the only `lint-light` entry that compiles Go, which costs one small build on first run and nothing after. What it reports: 1. `.vale.ini` plus `docs/.style/styles/Coder/`, which is the truth. 2. The per-rule annotations in the style guide subpages. 3. The two coverage tables on the style guide landing page. ### What it reports | Finding | Example it would have caught | |---|---| | A `Coder.*` rule cited as active with no rule file | `Coder.FirstPersonSingular`, `Coder.FirstPersonPlural` | | A third-party rule cited as active while `BasedOnStyles = Coder` | the 8 `Google.*` citations | | A rule file that no section cites as active | a rule landing without the section the per-rule PR pattern requires | | Checks-table severity, scope, or membership drift | a rule re-scoped in `.vale.ini` without updating the table | | Coverage-count drift | the per-section and total rows | ### Also in this PR - One annotation reworded so each planned citation is marked individually (`Google.Gender` and `Google.GenderBias`). A trailing `(both planned)` covering several citations isn't machine-readable, and the checker depends on that precision. - The annotation format documented in `scripts/styleclaims/README.md`, with the per-rule PR pattern in `docs/.style/README.md` pointing at it. - `make lint/prose` prints a coverage pointer after running, so the advisory result doesn't read as a pass. ## Verification - `go test ./scripts/styleclaims/` covers the parser and each finding class, including the mixed `scripts/check_emdash.sh` plus `Coder.EmDash` (planned) annotation and the "rule mentioned but not claimed" case that `Google.Passive` represents. - `go run ./scripts/styleclaims` is clean on the branch. - Regression check: reverting `Coder.FirstPersonSingular` to its pre-PR-1 wording reproduces 3 findings (the false claim plus the 2 count rows). Restoring it returns to clean. - `golangci-lint run scripts/styleclaims/...` reports 0 issues; `gofmt` clean. - `make lint/markdown`, `make lint/emdash` clean. <details> <summary>Design notes</summary> ### Why Go, not bash `scripts/` is mostly bash, but this parses Markdown tables and multi-line annotations, which bash does badly. `scripts/docshtmlcheck` sets the precedent for a Go checker invoked from a lint target, so this follows its layout: `main.go`, `main_test.go`, `README.md`, and a `go run ./scripts/...` target. ### Why not generate the tables Considered and rejected. Generation and checking need the same parse, so once the checker exists the table's correctness is guaranteed either way. Generation would only save a one-row edit in a PR that already edits the guide by doctrine, at the cost of a generated block inside prose contributors are invited to edit. The checker also catches a case generation can't: a rule enabled in `.vale.ini` with no style guide section at all. ### Parsing tradeoff The checker reads prose annotations, so it depends on their format staying regular: start with a recognized keyword, and mark each planned citation individually. That regularity is enforced by the checker itself, which is the point, but it does mean an author who invents a new annotation phrasing gets a failure rather than silent acceptance. If that proves annoying in practice, the fallback is a small YAML manifest of rule status that both the tables and the checker read. I'd rather not add that until the prose form actually hurts. ### Scope validation Scope cells are checked against `.vale.ini` section globs: every glob that sets a rule to `YES` must appear in the cell, and a rule with no `NO` toggle must claim `docs/**`. That's how the `OneSentencePerLine` row stays honest about running on contributor docs only. </details> --- Linear: [DOCS-887](https://linear.app/codercom/issue/DOCS-887/make-clear-that-vale-passing-is-not-style-guide-conformance) 🤖 Generated with Coder Agents on behalf of @nickvigilante.
styleclaims
styleclaims fails CI when the Coder documentation style guide claims a prose rule is enforced by tooling that isn't actually enabled.
It runs as make lint/style-claims (part of make lint and make lint-light).
Why it exists
Each rule section in docs/.style/style-guide/ ends with an annotation naming what enforces it.
Those annotations drifted from the configuration: 8 cited third-party Google.* rules that the repo-root .vale.ini never loads, and 2 cited Coder.* rules that have no YAML under docs/.style/styles/Coder/.
A reader who trusts an annotation believes a rule is checked when it isn't, which is worse than an unenforced rule the guide is honest about.
What it catches
- A
Coder.*rule cited as active with no rule file. The rule doesn't exist; the citation needs(planned). - A third-party rule cited as active.
.vale.inisetsBasedOnStyles = Coder, and third-party rules return one per PR after their corpus is clean, so anyGoogle.*,alex.*, orwrite-good.*citation is planned until that happens. - A rule with no style guide section. A rule file exists but no annotation cites it as active, so a rule landed without the section the per-rule PR pattern requires.
- Checks-table drift. A severity that disagrees with the rule file's
level:, a row for a rule that doesn't exist, a missing row for a rule that does, or a scope cell that doesn't read the way.vale.iniconfigures the rule. The scope check compares the rendered text, not the set of paths, so a cell that says "and" where the config says "except" is caught along with a cell that omits or invents a path. - A rule cited as enforcement while
.vale.inileaves it off. A rule file can exist and still run nowhere. - Coverage-table drift. A per-section or total count that disagrees with the annotations.
- A rule heading with no footer. A
##or###heading that states a rule but carries no annotation, so the coverage table can't count it.
The checker verifies a Coder.* citation against the rule files, so a typo is caught.
A third-party citation is verified only against the loaded styles: those rule files are vendored rather than kept in this repository, so the guarantee is weaker on that side.
The annotation format it relies on
An annotation starts with one of Enforced, Documentation-only, Adapted, or Vale rule, and runs to the line ending in *.
A citation counts as a claim only when its sentence contains enforc or names a Vale rule.
A sentence that mentions a rule without claiming it runs, such as the note explaining why Google.Passive stays out of the package, is ignored.
Mark each planned citation individually.
(planned) after the citation, or Planned Vale rule before it, both work:
*Enforced by `Coder.BrandNames`.*
*Enforced by `Coder.LearnMore` (planned).*
*Enforced by `Google.Gender` (planned) and `Google.GenderBias` (planned).*
*Enforced by `scripts/check_emdash.sh` (existing CI script) and `Coder.EmDash` (planned).*
*Documentation-only. No Vale rule.*
A trailing clause such as (both planned) that covers several citations at once isn't understood; mark each one.
A marker that appears before any citation introduces the list that follows, as in "Planned Vale rules" followed by names, and it carries until a later clause re-asserts enforcement.
So "Planned A, enforced by B." reads A planned and B active.
Coverage counts
Each annotation is classified once for the coverage table, so the table counts rule sections rather than linter rule names. One annotation footer is one rule section: a heading without a footer, such as an example, a navigation heading, or the out-of-scope Color contrast section, isn't a rule of the guide and isn't counted.
The checker also reports a ## or ### heading that states a rule but carries no footer, so a rule can't drop out of the table by never being annotated.
A footer belongs to the nearest heading above it, and a heading that has sub-headings is treated as covered by them, so a parent rule whose footer sits after a scoping sub-heading isn't flagged.
A heading is also exempt when it is the Learn more navigation section, or when it carries a note that opens with Out of scope or Not a rule, which is how a section that organizes a page without stating a rule declares itself.
Headings deeper than ### are detail inside a section, not rules.
A Markdown file that has no footers and no coverage row, such as editor-setup.md, isn't a rule section and isn't scanned.
- Documentation-only: the annotation declares the section
Documentation-only. This wins over everything else, so a section that only cross-references a rule another section owns counts under that other section and no rule is counted twice. The match is case-sensitive; a lowercase "documentation-only" describes part of a section, not the section. - Tool-checked: cites as active an enabled Coder rule, a third-party rule whose style
.vale.iniloads,markdownlint, anMD###rule, orscripts/check_emdash.sh. A Coder rule that exists but is disabled everywhere doesn't count: it checks no file. - Planned: cites only planned checkers.
Usage
go run ./scripts/styleclaims
It takes no arguments and runs from the repository root.
Findings print as file:line: message, or as file: message when the problem is the file as a whole, such as a rule with no style guide section or a missing coverage row.
Any finding exits 1.
The set of style guide sections comes from the directory, not from a list in the source. A Markdown file is a rule section when it carries at least one enforcement footer, or when the coverage table gives it a row, so a new subpage is validated as soon as either is true.