From cb723c3edbeb502ee3f0c27666380c99775b5b11 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Mon, 18 May 2026 21:38:15 +0000 Subject: [PATCH 01/10] docs: scaffold docs/.style for the prose style guide Adds a private contributor-tooling directory at docs/.style/ that holds: - README.md explaining the convention and the no-deploy guarantee - style-guide.md as a scaffold for the canonical prose style guide - styles/Coder/ as the home for custom Vale rules (filled by follow-ups) Defense-in-depth tweaks to .github/workflows/deploy-docs.yaml exclude docs/.style/** from the push trigger and from the surgical-reindex git diff. coder.com/docs route discovery is already manifest-driven, so nothing under docs/.style/ becomes a route or an Algolia record. Also: - .github/.linkspector.yml: skip external-link checks under docs/.style/ - AGENTS.md: point agents at the new style guide - .claude/docs/DOCS_STYLE_GUIDE.md: cross-link to the canonical prose guide; this file remains the structure/research companion The Vale configuration that consumes docs/.style/styles/ lands in a follow-up PR (DOCS-40). Closes DOCS-180. --- .claude/docs/DOCS_STYLE_GUIDE.md | 6 ++ .github/.linkspector.yml | 3 + .github/workflows/deploy-docs.yaml | 13 +++- AGENTS.md | 1 + docs/.style/README.md | 68 +++++++++++++++++++ docs/.style/style-guide.md | 105 +++++++++++++++++++++++++++++ docs/.style/styles/Coder/README.md | 45 +++++++++++++ 7 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 docs/.style/README.md create mode 100644 docs/.style/style-guide.md create mode 100644 docs/.style/styles/Coder/README.md diff --git a/.claude/docs/DOCS_STYLE_GUIDE.md b/.claude/docs/DOCS_STYLE_GUIDE.md index 70ffdb0b684..2dc5a6624db 100644 --- a/.claude/docs/DOCS_STYLE_GUIDE.md +++ b/.claude/docs/DOCS_STYLE_GUIDE.md @@ -1,5 +1,11 @@ # Documentation Style Guide +> **For prose rules**, read the canonical Coder documentation style guide at +> [`docs/.style/style-guide.md`](../../docs/.style/style-guide.md). That guide +> is what the Vale rules in `docs/.style/styles/Coder/` enforce. This file +> focuses on **structure, research, and content patterns** observed in the +> repo and is meant to ride alongside the prose guide, not replace it. + This guide documents documentation patterns observed in the Coder repository, based on analysis of existing admin guides, tutorials, and reference documentation. This is specifically for documentation files in the `docs/` directory - see [CONTRIBUTING.md](../../docs/about/contributing/CONTRIBUTING.md) for general contribution guidelines. ## Research Before Writing diff --git a/.github/.linkspector.yml b/.github/.linkspector.yml index 25af1ebe41b..012d9e76973 100644 --- a/.github/.linkspector.yml +++ b/.github/.linkspector.yml @@ -6,6 +6,9 @@ excludedDirs: - docs/reference # Older changelogs may contain broken links - docs/changelogs + # Contributor-facing style guide and Vale config. Not deployed to + # coder.com/docs; chasing external links here is overkill. + - docs/.style ignorePatterns: - pattern: "localhost" - pattern: "example.com" diff --git a/.github/workflows/deploy-docs.yaml b/.github/workflows/deploy-docs.yaml index abb07d65ad1..591ee0f71e4 100644 --- a/.github/workflows/deploy-docs.yaml +++ b/.github/workflows/deploy-docs.yaml @@ -34,7 +34,14 @@ on: # Intentionally only docs/**. Edits to this workflow file must not # auto-trigger a production reindex; use workflow_dispatch instead. # See DOCS-121 (incident) and DOCS-124 (fix). + # + # docs/.style/** is contributor tooling and never deploys to + # coder.com/docs. Negating it here skips the workflow on .style-only + # commits. GitHub Actions only suppresses when every changed file + # matches a negation, so mixed commits still trigger; the surgical + # diff step below drops .style paths from the payload. - "docs/**" + - "!docs/.style/**" workflow_dispatch: inputs: action: @@ -154,7 +161,11 @@ jobs: # + save) from deleted/renamed-old-side (delete only), and # so paths containing whitespace or quotes survive intact. DIFF_FILE=$(mktemp) - git diff --name-status -z "$BEFORE_SHA" "$AFTER_SHA" -- 'docs/**/*.md' > "$DIFF_FILE" + # 'docs/**/*.md' to keep markdown-only paths, ':(exclude)docs/.style/**' + # so contributor-tooling pages never reach the surgical-reindex payload + # on mixed commits. The trigger filter already short-circuits .style-only + # pushes; this is defense in depth. + git diff --name-status -z "$BEFORE_SHA" "$AFTER_SHA" -- 'docs/**/*.md' ':(exclude)docs/.style/**' > "$DIFF_FILE" # Parse the NUL-delimited diff into \t lines. # `--name-status -z` uses NUL between fields and between # records, with a special twist for renames: the record is diff --git a/AGENTS.md b/AGENTS.md index 4517ffe21c2..a436f76d76e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,6 +11,7 @@ Rule #1: If you want exception to ANY rule, YOU MUST STOP and get explicit permi - Language and area docs: Use [Modern Go](.claude/docs/GO.md), [Testing Patterns and Best Practices](.claude/docs/TESTING.md), [Database Development Patterns](.claude/docs/DATABASE.md), [OAuth2 Development Guide](.claude/docs/OAUTH2.md), [Coder Architecture](.claude/docs/ARCHITECTURE.md), [Troubleshooting Guide](.claude/docs/TROUBLESHOOTING.md), [Documentation Style Guide](.claude/docs/DOCS_STYLE_GUIDE.md), and [Pull Request Description Style Guide](.claude/docs/PR_STYLE_GUIDE.md) when that area is in scope. - Compatibility: `.agents/docs` symlinks to `.claude/docs` for agent runtimes that look there. - Frontend: Read [Frontend Development Guidelines](site/AGENTS.md) before changing anything under `site/`. +- Docs prose: When editing anything under `docs/`, read [Coder documentation style guide](docs/.style/style-guide.md). The repo-root [.claude/docs/DOCS_STYLE_GUIDE.md](.claude/docs/DOCS_STYLE_GUIDE.md) covers structure and research patterns; the new style guide covers prose rules. ## Foundational rules diff --git a/docs/.style/README.md b/docs/.style/README.md new file mode 100644 index 00000000000..d0642728502 --- /dev/null +++ b/docs/.style/README.md @@ -0,0 +1,68 @@ +# `docs/.style/` + +Contributor-facing style guide and prose-lint configuration for the Coder +documentation. Nothing under this directory is published to +[coder.com/docs](https://coder.com/docs). + +## What lives here + +| Path | Purpose | +|------------------|------------------------------------------------------------| +| `style-guide.md` | Canonical prose style guide for `docs/` | +| `styles/Coder/` | Custom Vale rules specific to Coder (product voice, terms) | + +See [`docs/.style/style-guide.md`](style-guide.md) for the style guide +itself. The `styles/Coder/` directory holds the custom Vale rules that +enforce parts of the guide. Vale's `StylesPath` in the repo-root +`.vale.ini` points at `docs/.style/styles/`. + +## Why a hidden directory + +The leading dot mirrors the `.github/`, `.vscode/`, and `.claude/` +convention already used in this repo for tooling-internal directories. +Vale and the structural Markdown linters still pick it up; coder.com's +docs site does not. + +## How exclusion from coder.com works + +[coder.com/docs](https://coder.com/docs) routes and search are +manifest-driven: + +- Route discovery lives in + [`coder/coder.com:src/utils/docs/docs.ts`](https://github.com/coder/coder.com/blob/master/src/utils/docs/docs.ts) + (`getDocsStaticPaths`). It iterates `routes` from `docs/manifest.json` + and emits one Next.js static path per entry. Files not in the manifest + do not become routes. +- The Algolia surgical indexer at + [`coder/coder.com:src/utils/algoliaDocs/surgical.ts`](https://github.com/coder/coder.com/blob/master/src/utils/algoliaDocs/surgical.ts) + explicitly skips paths that are not in the manifest, incrementing + `pathsSkipped`. + +Net result: not adding anything from `docs/.style/` to `docs/manifest.json` +gives us no route, no Algolia record, and no sidebar entry. Two +defense-in-depth changes in `.github/workflows/deploy-docs.yaml` keep the +deploy workflow from running on `.style`-only commits and exclude the +directory from the surgical-reindex payload on mixed commits. + +## What still runs against this directory + +- `make lint/markdown` (markdownlint-cli2) processes every Markdown file + here. The repo-root `package.json` invokes + `markdownlint-cli2 --fix $(find docs -name '*.md')`. +- `make fmt/markdown` (markdown-table-formatter) reflows tables here for + the same reason. +- Vale, once configured per + [DOCS-40](https://linear.app/codercom/issue/DOCS-40), lints the entire + `docs/**/*.md` set including `docs/.style/style-guide.md`. + +## What does not run against this directory + +- `linkspector`: excluded via `excludedDirs` in `.github/.linkspector.yml`. + External-link checking is overkill for contributor tooling. + +## Editing the style guide + +Open a PR against `docs/.style/style-guide.md`. The rule-specific tickets +in the +[Docs style guide](https://linear.app/codercom/project/docs-style-guide-7828445b9afc) +project fill in the body section by section. diff --git a/docs/.style/style-guide.md b/docs/.style/style-guide.md new file mode 100644 index 00000000000..19606684899 --- /dev/null +++ b/docs/.style/style-guide.md @@ -0,0 +1,105 @@ +# Coder documentation style guide + +This is the canonical style guide for the Coder documentation. It is the +source of truth that the Vale rules in `docs/.style/styles/Coder/` enforce. + +Status: scaffold. Sections below are populated by the rule-specific +tickets in the +[Docs style guide](https://linear.app/codercom/project/docs-style-guide-7828445b9afc) +project; this page starts as a table of contents and grows as those +tickets land. + +## How to use this guide + +- **Contributors**: read the section that matches what you are writing. + Each rule notes the Vale rule ID, if any, so you can reproduce the + warning locally. +- **Reviewers**: cite the section in a review comment. Reviews are easier + when the guidance is in one place. +- **AI agents**: read this page in full before editing anything under + `docs/`. The Coder Agents and Claude Code guides + ([`AGENTS.md`](../../AGENTS.md), + [`.claude/docs/DOCS_STYLE_GUIDE.md`](../../.claude/docs/DOCS_STYLE_GUIDE.md)) + link here. + +## Voice and tone + +To be filled in by rule-specific tickets. Planned coverage: + +- Active voice +- Second person +- Plural nouns and pronouns where number is uncertain +- Product voice (`stop` over `kill`, `turn off` over `disable` in + user-facing copy) - see + [DOCS-183](https://linear.app/codercom/issue/DOCS-183) +- Limiting "we" - see + [DOCS-35](https://linear.app/codercom/issue/DOCS-35) + +## Word choice + +To be filled in by rule-specific tickets. Planned coverage: + +- Inclusive language substitutions - see + [DOCS-182](https://linear.app/codercom/issue/DOCS-182) +- HashiCorp casing - see + [DOCS-34](https://linear.app/codercom/issue/DOCS-34) +- Dev Container terminology - see + [DOCS-33](https://linear.app/codercom/issue/DOCS-33) +- "Setup" vs "set up" and Quickstart casing - see + [DOCS-36](https://linear.app/codercom/issue/DOCS-36) +- "Next steps" vs "Learn more" - see + [DOCS-37](https://linear.app/codercom/issue/DOCS-37) +- Weasel words - see + [DOCS-42](https://linear.app/codercom/issue/DOCS-42) + +## Capitalization and punctuation + +To be filled in by rule-specific tickets. Planned coverage: + +- Sentence case in titles and headings +- General capitalization policy - see + [DOCS-38](https://linear.app/codercom/issue/DOCS-38) +- Em-dash and en-dash ban (use comma, semicolon, or period) - see + [DOCS-44](https://linear.app/codercom/issue/DOCS-44), origin tracked + in [DOCS-181](https://linear.app/codercom/issue/DOCS-181) + +## Formatting + +To be filled in by rule-specific tickets. Planned coverage: + +- Bold for UI elements +- Italics for parameter names and version variables +- Code font for user input, command-line utility names, filenames, + environment variables, HTTP verbs and status codes, placeholder + variables +- Code blocks with explicit language fences - see + [DOCS-43](https://linear.app/codercom/issue/DOCS-43) for MD040 + +## Vale enforcement + +The repo-root `.vale.ini` configures Vale to read styles from +`docs/.style/styles/`. The starter configuration combines: + +- Google's developer-docs base style +- A curated subset of `alex` (inclusive-language) +- A curated subset of `write-good` (wordiness) +- Coder-specific custom rules in `docs/.style/styles/Coder/` + +See [DOCS-40](https://linear.app/codercom/issue/DOCS-40) for the rationale +behind the cherry-picked base styles and the severity policy. + +## Editor setup + +To be filled in by +[DOCS-178](https://linear.app/codercom/issue/DOCS-178). Will cover VS +Code, Cursor, JetBrains, and Neovim. + +## Third-party references + +When this guide does not cover something, consult: + +| Type of guidance | Reference | +|----------------------|-----------------------------------------------------------------------------------------| +| Spelling | [Merriam-Webster](https://www.merriam-webster.com/) | +| Style - nontechnical | [The Chicago Manual of Style](https://www.chicagomanualofstyle.org/home.html) | +| Style - technical | [Microsoft Writing Style Guide](https://learn.microsoft.com/en-us/style-guide/welcome/) | diff --git a/docs/.style/styles/Coder/README.md b/docs/.style/styles/Coder/README.md new file mode 100644 index 00000000000..1fbfe3c6239 --- /dev/null +++ b/docs/.style/styles/Coder/README.md @@ -0,0 +1,45 @@ +# Coder custom Vale rules + +Custom Vale rules specific to Coder live here. Each rule is a YAML file +that Vale loads through the `BasedOnStyles = Coder` setting in the +repo-root `.vale.ini`. + +This directory is intentionally empty for now. The rule-specific tickets +in the +[Docs style guide](https://linear.app/codercom/project/docs-style-guide-7828445b9afc) +Linear project add rules incrementally: + +| Ticket | Rule | +|--------------------------------------------------------|------------------------------------| +| [DOCS-33](https://linear.app/codercom/issue/DOCS-33) | Dev Container terminology | +| [DOCS-34](https://linear.app/codercom/issue/DOCS-34) | HashiCorp casing | +| [DOCS-35](https://linear.app/codercom/issue/DOCS-35) | Limit "we" | +| [DOCS-36](https://linear.app/codercom/issue/DOCS-36) | Setup vs set up, Quickstart casing | +| [DOCS-37](https://linear.app/codercom/issue/DOCS-37) | Next steps vs Learn more | +| [DOCS-41](https://linear.app/codercom/issue/DOCS-41) | Vale substitution rule scaffold | +| [DOCS-42](https://linear.app/codercom/issue/DOCS-42) | Weasel words | +| [DOCS-44](https://linear.app/codercom/issue/DOCS-44) | Em-dash and en-dash mirror in Vale | +| [DOCS-182](https://linear.app/codercom/issue/DOCS-182) | Inclusive-language substitutions | +| [DOCS-183](https://linear.app/codercom/issue/DOCS-183) | Product-voice rules | + +## Authoring a new rule + +1. Write a YAML file under this directory. Name it after the rule's + intent, for example `InclusiveLanguage.yml` or `ProductVoice.yml`. +2. Each rule's `message:` should link to the matching section in + `docs/.style/style-guide.md`, ideally with a deep-link anchor, so a + contributor reading a Vale warning can jump straight to the guidance. +3. Land at `level: warning` first. Promote to `level: error` only after + both conditions hold: + - The rule is objectively correct (typo, brand-name casing, banned + substitution). + - The existing-content violation count for the rule reaches zero. +4. The + [parity CI check](https://linear.app/codercom/issue/DOCS-179) will + eventually verify that every rule here has a matching section in + `style-guide.md`. Add the section in the same PR as the rule. + +## Reference + +- Vale docs: +- Vale rule types: From 4cb4d63ce812869f739fc931d3c9202e398a45b2 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Mon, 18 May 2026 22:29:36 +0000 Subject: [PATCH 02/10] feat: wire Vale prose linter into docs CI Lands the Vale prose linter as a non-blocking docs CI step. Builds on the scaffold from DOCS-180 (#25466): - .vale.ini at the repo root configures Google + curated write-good + cherry-picked alex rules. Disables Google.Spacing (false positives on codersdk type names in the auto-generated API reference), Google.EmDash (conflicts with our em-dash ban), Google.Latin (i.e. and e.g. are fine), write-good.Passive and write-good.E-Prime (judgment-heavy). - mise.toml pins Vale 3.7.1 via aqua. - Makefile adds build/vale-$VERSION install, docs/.style/.vale-synced sentinel that runs 'vale sync' once per .vale.ini change, and a lint/prose target wrapped in '|| true' for v1 non-blocking severity. - .github/workflows/docs-ci.yaml adds a 'prose' step that lints only the changed Markdown files under docs/, with continue-on-error: true and a cache for the synced styles and binary. - .gitignore excludes the synced upstream styles and the sentinel. - .markdownlint-cli2.jsonc ignores the synced styles so local markdownlint runs do not lint upstream READMEs. - docs/.style/README.md and style-guide.md document how to run Vale locally and what the active rule set is. Severity policy (v1): every rule lands at 'warning'. CI is non-blocking through continue-on-error: true. A rule promotes to 'error' only when (a) it is objectively correct and (b) the existing-content violation count reaches zero. Judgment rules stay at 'suggestion'. Local run on the full docs corpus produces 0 blocking failures, 391 errors, 5202 warnings, 7682 suggestions across 458 files in ~20s. Refs DOCS-40. --- .github/workflows/docs-ci.yaml | 48 ++++++++++++++++++++ .gitignore | 9 ++++ .markdownlint-cli2.jsonc | 7 ++- .vale.ini | 80 ++++++++++++++++++++++++++++++++++ Makefile | 54 +++++++++++++++++++++++ docs/.style/README.md | 21 +++++++++ docs/.style/style-guide.md | 34 +++++++++++++++ mise.toml | 1 + 8 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 .vale.ini diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 8df9850f082..9403f7a3e5f 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -69,3 +69,51 @@ jobs: printf '%s\n' "$ALL_CHANGED_FILES" | tr ',' '\n' | xargs -d '\n' pnpm exec markdown-table-formatter --check env: ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }} + + # Separate changed-files filter for Vale prose linting. Vale's + # .vale.ini only configures rules under docs/, so a repo-wide **.md + # filter would feed Vale paths it has no opinions on. The + # docs/.style/styles/** subtree holds the synced upstream packages + # and must never go through Vale itself, so files_ignore strips them + # before the path list reaches the step. + - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v45.0.7 + id: changed-md-docs + with: + files: | + docs/**.md + files_ignore: | + docs/.style/styles/** + separator: "," + + # `make docs/.style/.vale-synced` downloads the pinned Vale binary + # into build/ and runs `vale sync` so the configured packages exist + # under StylesPath. The sync is idempotent across runs and gated by + # an mtime sentinel that the cache below preserves between workflow + # invocations. + - name: Cache Vale styles and binary + if: steps.changed-md-docs.outputs.any_changed == 'true' + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + docs/.style/styles/Google + docs/.style/styles/alex + docs/.style/styles/write-good + docs/.style/.vale-synced + build/vale-* + key: vale-${{ hashFiles('.vale.ini', 'mise.toml') }} + + # The prose step is non-blocking by design (continue-on-error: true). + # v1 lands every rule at `warning`, which makes Vale exit non-zero + # even on clean PRs that happen to touch many lines. Annotations + # stay visible in the step log; promote rules to `error` (and drop + # continue-on-error) once their existing-content violation count + # reaches zero. See DOCS-40. + - name: prose + if: steps.changed-md-docs.outputs.any_changed == 'true' + continue-on-error: true + run: | + make docs/.style/.vale-synced + vale=$(find build/ -maxdepth 1 -name 'vale-*' -type f -print -quit) + printf '%s\n' "$ALL_CHANGED_FILES" | tr ',' '\n' | xargs -d '\n' "$vale" + env: + ALL_CHANGED_FILES: ${{ steps.changed-md-docs.outputs.all_changed_files }} diff --git a/.gitignore b/.gitignore index 65dd97caf70..3d2563054d3 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,15 @@ node_modules/ vendor/ yarn-error.log +# Vale (prose linter) pulls package contents into docs/.style/styles/ on +# `vale sync`. Each package directory and the sync sentinel are gitignored +# so the synced content does not bloat the repo. The Coder/ directory next +# to them is tracked because it holds our custom rules. +docs/.style/.vale-synced +docs/.style/styles/Google/ +docs/.style/styles/alex/ +docs/.style/styles/write-good/ + # Test output files test-output/ diff --git a/.markdownlint-cli2.jsonc b/.markdownlint-cli2.jsonc index 0ce43e7cf9c..58b933d154c 100644 --- a/.markdownlint-cli2.jsonc +++ b/.markdownlint-cli2.jsonc @@ -1,3 +1,8 @@ { - "ignores": ["PLAN.md"], + "ignores": [ + "PLAN.md", + "docs/.style/styles/Google/**", + "docs/.style/styles/alex/**", + "docs/.style/styles/write-good/**" + ], } diff --git a/.vale.ini b/.vale.ini new file mode 100644 index 00000000000..2ec36e28b3a --- /dev/null +++ b/.vale.ini @@ -0,0 +1,80 @@ +# Vale configuration for Coder documentation. +# +# Curated cherry-pick of Google's developer-docs style, write-good (wordiness), +# and a hand-picked subset of alex (inclusive-language). The choice of base +# styles and disabled rules is documented in DOCS-40 and reproducible via +# `make lint/prose`. +# +# Severity policy (v1): every rule lands at `warning`. CI is non-blocking +# (Makefile wraps Vale in `|| true`). A rule promotes to `error` only after +# (a) it is objectively correct and (b) the existing-content violation count +# reaches zero. Judgment rules (Wordiness, Weasel, ThereIs) stay at +# `suggestion` and never promote to `error`. +# +# The styles themselves live under docs/.style/styles/ after `vale sync`, +# which the Makefile target invokes once per .vale.ini change. They are +# gitignored to keep the repo lean. + +StylesPath = docs/.style/styles +MinAlertLevel = suggestion + +# Packages drives `vale sync`. Pin upstream tags here when reproducibility +# matters more than getting upstream fixes; the unpinned form pulls the +# latest release of each package on `vale sync`. +Packages = Google, alex, write-good + +[*.md] +BasedOnStyles = Google, write-good + +# --- Google curation ------------------------------------------------------- +# Google.EmDash conflicts with our em-dash ban (see scripts/check_emdash.sh +# and DOCS-44). The repo-level ban covers Unicode U+2014/U+2013 plus the +# ` -- ` ASCII fallback; Google.EmDash would double-flag and use prose +# different from our policy. +Google.EmDash = NO + +# Google.Latin flags i.e. and e.g. for non-native readers. Coder docs assume +# a technical audience that reads these fluently; leaving the rule on +# produces noise without value. +Google.Latin = NO + +# Soften two high-volume Google rules. The signal-to-noise ratio is low +# at the default warning level. +Google.Parens = suggestion +Google.WordList = warning + +# Google.Spacing flags ASCII codepoint sequences like `k.R` or +# `codersdk.SomeType` as "should have one space." It produces ~4,500 +# errors against `docs/reference/api/schemas.md` alone (Vale spike, +# 2026-05-18) because codersdk type names match the pattern. The +# architectural decision for generated content is to fix the upstream +# Go generators (clidocgen, apidocgen, auditdocgen, metricsdocgen) +# rather than add Vale path exclusions; this disable buys time until +# those generator changes land. Re-enable then, ideally promoted to +# `error`. +Google.Spacing = NO + +# --- write-good curation --------------------------------------------------- +# Passive voice is contextually correct often enough that flagging every +# instance teaches nothing. E-Prime forbids forms of "to be" entirely, +# which is incompatible with normal technical writing. +write-good.Passive = NO +write-good.E-Prime = NO + +# Keep the three rules that catch real wordiness problems. Wordiness and +# ThereIs are judgment calls (suggestion); Weasel is sharper (warning). +write-good.TooWordy = suggestion +write-good.Weasel = warning +write-good.ThereIs = suggestion + +# --- alex curation --------------------------------------------------------- +# alex is loaded a la carte rather than via BasedOnStyles. ProfanityMaybe +# and ProfanityUnlikely fire on technical terms like `execute`, `kill`, +# `failed`, and `attack`; ProfanityLikely is much more conservative and +# safe to keep on. +alex.Ablist = warning +alex.Condescending = warning +alex.LGBTQ = warning +alex.ProfanityLikely = warning +alex.Race = warning +alex.Suicide = warning diff --git a/Makefile b/Makefile index 97238270118..3011b8a84a7 100644 --- a/Makefile +++ b/Makefile @@ -835,6 +835,60 @@ lint/typos: build/typos-$(TYPOS_VERSION) build/typos-$(TYPOS_VERSION) --config .github/workflows/typos.toml .PHONY: lint/typos +# Vale (prose linter). +# +# Pinned through mise.toml so dogfood image users and the Makefile install +# path stay in sync. The aqua identifier is `"aqua:errata-ai/vale" = "X.Y.Z"`; +# the grep below extracts the version. Keep that pin authoritative. +VALE_VERSION := $(shell grep -oP '"aqua:errata-ai/vale"\s*=\s*"\K[0-9.]+' mise.toml) + +# Map uname values to Vale release asset names. Vale ships: +# vale_X.Y.Z_Linux_64-bit.tar.gz (linux amd64) +# vale_X.Y.Z_Linux_arm64.tar.gz (linux arm64) +# vale_X.Y.Z_macOS_64-bit.tar.gz (macos amd64) +# vale_X.Y.Z_macOS_arm64.tar.gz (macos arm64) +VALE_ARCH := $(shell uname -m) +ifeq ($(VALE_ARCH),x86_64) +VALE_ARCH := 64-bit +endif +ifeq ($(VALE_ARCH),arm64) +VALE_ARCH := arm64 +endif +ifeq ($(VALE_ARCH),aarch64) +VALE_ARCH := arm64 +endif +ifeq ($(shell uname -s),Darwin) +VALE_OS := macOS +else +VALE_OS := Linux +endif + +build/vale-$(VALE_VERSION): + mkdir -p build/ + curl -sSfL "https://github.com/errata-ai/vale/releases/download/v$(VALE_VERSION)/vale_$(VALE_VERSION)_$(VALE_OS)_$(VALE_ARCH).tar.gz" \ + | tar -xzf - -C build/ ./vale + mv build/vale "$@" + +# `vale sync` pulls the packages listed in .vale.ini's Packages directive +# into StylesPath (docs/.style/styles/). The .vale-synced sentinel makes +# sync idempotent across `make lint/prose` calls and lets warm checkouts +# skip the re-sync entirely. Touch any time .vale.ini changes to force a +# re-sync. +docs/.style/.vale-synced: .vale.ini build/vale-$(VALE_VERSION) + @echo "$(GREEN)==>$(RESET) $(BOLD)vale sync$(RESET)" + build/vale-$(VALE_VERSION) sync + @touch $@ + +# v1 severity policy lands every rule at warning. Vale exits non-zero when +# alerts at MinAlertLevel or above are found, so the `|| true` keeps +# `make lint/prose` from failing while the cleanup PRs land. Promote rules +# to error (and remove the wrapper) when their existing-content violation +# count reaches zero. See DOCS-40. +lint/prose: docs/.style/.vale-synced + @echo "$(GREEN)==>$(RESET) $(BOLD)lint/prose$(RESET)" + build/vale-$(VALE_VERSION) docs/ || true +.PHONY: lint/prose + # pre-commit and pre-push mirror CI checks locally. # # pre-commit runs checks that don't need external services (Docker, diff --git a/docs/.style/README.md b/docs/.style/README.md index d0642728502..3679b947476 100644 --- a/docs/.style/README.md +++ b/docs/.style/README.md @@ -66,3 +66,24 @@ Open a PR against `docs/.style/style-guide.md`. The rule-specific tickets in the [Docs style guide](https://linear.app/codercom/project/docs-style-guide-7828445b9afc) project fill in the body section by section. + +## Running Vale locally + +The canonical entry point is `make lint/prose`. The first run downloads +the pinned Vale binary and the configured style packages (Google, alex, +write-good) into `docs/.style/styles/`; subsequent runs are fast. + +```shell +make lint/prose +``` + +The target wraps Vale in `|| true` so warnings do not fail the build. To +see Vale's raw exit code, invoke the binary directly: + +```shell +make docs/.style/.vale-synced +./build/vale-*/vale docs/ # or pass specific files +``` + +`.vale.ini` at the repo root selects the curated rule set. See its +inline comments for the rationale on each enabled or disabled rule. diff --git a/docs/.style/style-guide.md b/docs/.style/style-guide.md index 19606684899..8832a296223 100644 --- a/docs/.style/style-guide.md +++ b/docs/.style/style-guide.md @@ -88,6 +88,40 @@ The repo-root `.vale.ini` configures Vale to read styles from See [DOCS-40](https://linear.app/codercom/issue/DOCS-40) for the rationale behind the cherry-picked base styles and the severity policy. +### Running Vale locally + +The canonical entry point is `make lint/prose`. The first run downloads +the pinned Vale binary and the configured style packages; subsequent +runs reuse them. The target wraps Vale in `|| true` so warnings do not +break `make lint`, matching the v1 non-blocking policy. + +### Severity policy (v1) + +Every rule lands at `warning`. CI is non-blocking through +`continue-on-error: true` on the prose step. A rule promotes to `error` +only when (a) it is objectively correct (typo, brand-name casing, banned +substitution) and (b) the existing-content violation count reaches zero. +Judgment-based rules (Wordiness, Weasel, ThereIs) stay at `suggestion` +and never promote to `error`. + +### Active rule set + +The curated set documented in `.vale.ini`: + +- **Google** (base): all rules except `EmDash` (conflicts with our em-dash + ban), `Latin` (i.e. and e.g. are fine in technical writing), and `Spacing` + (fires aggressively on codersdk type names in the auto-generated API + reference; re-enable once the Go generators emit proper spacing). + `Parens` is softened to `suggestion` and `WordList` stays at `warning`. +- **write-good** (base, with disables): `Passive` and `E-Prime` are off. + `TooWordy` and `ThereIs` stay at `suggestion`; `Weasel` is at `warning`. +- **alex** (cherry-picked): `Ablist`, `Condescending`, `LGBTQ`, + `ProfanityLikely`, `Race`, `Suicide` at `warning`. The `ProfanityMaybe` + and `ProfanityUnlikely` rules fire on technical terms like `execute`, + `kill`, `failed`, and `attack`, so they are left off. +- **Coder** (custom): empty in v1. Rules land through the rule-specific + tickets in this project (see `docs/.style/styles/Coder/README.md`). + ## Editor setup To be filled in by diff --git a/mise.toml b/mise.toml index 4febc3f2631..afaaec08346 100644 --- a/mise.toml +++ b/mise.toml @@ -43,6 +43,7 @@ terraform = "1.15.2" # `coder external-auth` into `gh`, and a mise shim earlier in # PATH would bypass it. "aqua:crate-ci/typos" = "1.46.1" +"aqua:errata-ai/vale" = "3.7.1" "aqua:jj-vcs/jj" = "0.41.0" "aqua:watchexec/watchexec" = "2.5.1" doctl = "1.158.0" From c3e9e2ffd8e096ac2e145352dda07d0c2c156827 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Mon, 18 May 2026 22:35:58 +0000 Subject: [PATCH 03/10] fix(.github/workflows/docs-ci.yaml): match docs/.style paths in prose filter The 'docs/**.md' glob in tj-actions/changed-files skips dot-prefixed directories by default, so the changed-md-docs filter silently dropped docs/.style/README.md and docs/.style/style-guide.md. The Vale prose step never fired on PR #25467 as a result. Drop the second changed-files step and post-filter the changed-md output in shell. grep '^docs/' keeps only docs paths; grep -v '^docs/.style/styles/' excludes the synced upstream packages. The early exit handles PRs that only touched non-docs markdown. Refs DOCS-40. --- .github/workflows/docs-ci.yaml | 51 +++++++++++++++++----------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 9403f7a3e5f..7122bb154d4 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -70,28 +70,13 @@ jobs: env: ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }} - # Separate changed-files filter for Vale prose linting. Vale's - # .vale.ini only configures rules under docs/, so a repo-wide **.md - # filter would feed Vale paths it has no opinions on. The - # docs/.style/styles/** subtree holds the synced upstream packages - # and must never go through Vale itself, so files_ignore strips them - # before the path list reaches the step. - - uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v45.0.7 - id: changed-md-docs - with: - files: | - docs/**.md - files_ignore: | - docs/.style/styles/** - separator: "," - # `make docs/.style/.vale-synced` downloads the pinned Vale binary # into build/ and runs `vale sync` so the configured packages exist # under StylesPath. The sync is idempotent across runs and gated by # an mtime sentinel that the cache below preserves between workflow # invocations. - name: Cache Vale styles and binary - if: steps.changed-md-docs.outputs.any_changed == 'true' + if: steps.changed-md.outputs.any_changed == 'true' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | @@ -102,18 +87,34 @@ jobs: build/vale-* key: vale-${{ hashFiles('.vale.ini', 'mise.toml') }} - # The prose step is non-blocking by design (continue-on-error: true). - # v1 lands every rule at `warning`, which makes Vale exit non-zero - # even on clean PRs that happen to touch many lines. Annotations - # stay visible in the step log; promote rules to `error` (and drop - # continue-on-error) once their existing-content violation count - # reaches zero. See DOCS-40. + # Vale runs on the changed-md set filtered to docs/ paths only, + # excluding the synced upstream packages under docs/.style/styles/. + # We post-filter in shell rather than using a second + # tj-actions/changed-files step because the `docs/**.md` glob there + # silently skips dot-prefixed dirs (so it would miss our own + # docs/.style/style-guide.md while still treating the top-level + # `**.md` filter as authoritative). The early exit handles PRs that + # only touched non-docs markdown like AGENTS.md or README.md. + # + # The step is non-blocking by design (continue-on-error: true). v1 + # lands every rule at `warning`, which makes Vale exit non-zero on + # nearly every PR. Annotations stay visible in the step log; promote + # rules to `error` (and drop continue-on-error) once their + # existing-content violation count reaches zero. See DOCS-40. - name: prose - if: steps.changed-md-docs.outputs.any_changed == 'true' + if: steps.changed-md.outputs.any_changed == 'true' continue-on-error: true run: | + files=$(printf '%s\n' "$ALL_CHANGED_FILES" \ + | tr ',' '\n' \ + | grep -E '^docs/' \ + | grep -v '^docs/\.style/styles/' || true) + if [ -z "$files" ]; then + echo "No changed Markdown files under docs/ (excluding synced styles); skipping Vale." + exit 0 + fi make docs/.style/.vale-synced vale=$(find build/ -maxdepth 1 -name 'vale-*' -type f -print -quit) - printf '%s\n' "$ALL_CHANGED_FILES" | tr ',' '\n' | xargs -d '\n' "$vale" + printf '%s\n' "$files" | xargs -d '\n' "$vale" env: - ALL_CHANGED_FILES: ${{ steps.changed-md-docs.outputs.all_changed_files }} + ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }} From 6054d9ea0c883fec220c1dd58e5245a6a9fa724b Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Mon, 18 May 2026 22:39:27 +0000 Subject: [PATCH 04/10] fix(Makefile): match Vale's tar layout when extracting the binary Vale's release archive places 'vale' at the archive root with no leading ./ (unlike typos), so 'tar -xzf - ./vale' matched nothing and produced 'tar: ./vale: Not found in archive' on CI. Switch to 'vale'. The local Makefile invocation worked before because build/vale-3.7.1 was already present from the spike artifacts. CI hit the cold path and exposed the bug. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3011b8a84a7..c0d39f3b599 100644 --- a/Makefile +++ b/Makefile @@ -866,7 +866,7 @@ endif build/vale-$(VALE_VERSION): mkdir -p build/ curl -sSfL "https://github.com/errata-ai/vale/releases/download/v$(VALE_VERSION)/vale_$(VALE_VERSION)_$(VALE_OS)_$(VALE_ARCH).tar.gz" \ - | tar -xzf - -C build/ ./vale + | tar -xzf - -C build/ vale mv build/vale "$@" # `vale sync` pulls the packages listed in .vale.ini's Packages directive From e49b5ccedf81719641a33dc8b95ea2b2c7fd259b Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Tue, 19 May 2026 15:47:30 +0000 Subject: [PATCH 05/10] docs: address Coder Agents review on Vale wiring Resolves the 12 findings from review id 4314529409 on PR #25467. DEREM-1 (P2): fix `./build/vale-*/vale` glob in docs/.style/README.md; the binary is the file itself, not a directory. DEREM-2 (P3): drop the inaccurate `make lint` claim in style-guide.md; point readers at README's Running Vale section instead. DEREM-3 (P2): split the prose step into `Prepare Vale styles` (no continue-on-error) and `prose` (no continue-on-error; --no-exit), so the job fails on sync failures but lints non-blocking. DEREM-4 (P2): replace `|| true` with Vale's native --no-exit in the Makefile and rewrite the severity-policy comment to match the measured exit-code semantics (Vale exits non-zero only on error alerts, regardless of MinAlertLevel). DEREM-5 (P2): add `Coder` to BasedOnStyles so the empty starter style is loaded and ready to receive rule files. DEREM-6 (P3): replace hardcoded per-package paths in .gitignore and the workflow cache step with `styles/*` plus a `!styles/Coder` negation, reducing the places future packages need to be listed. DEREM-7 (P3): already addressed by c3e9e2ffd8 (prose filter matches docs/.style paths) and 6054d9ea0c (tar layout). DEREM-8 (P3): remove the duplicate Running Vale block in style-guide.md and cross-reference README.md as the single source. DEREM-9 (P3): add .github/vale-problem-matcher.json and wire the prose step with `::add-matcher::`/`--output=line`/`::remove-matcher::` so alerts surface as inline PR annotations. DEREM-10 (P4): trim the Active rule set section in style-guide.md to a policy summary plus a pointer to .vale.ini. DEREM-11 (Nit): rephrase the Makefile sync-sentinel comment to describe Make's behavior instead of suggesting `touch`. DEREM-12 (Nit): swap "(Vale spike, 2026-05-18)" for the more neutral "(measured 2026-05-18)" in the Google.Spacing rationale. Two-layer defense on the cache step protects the hand-authored Coder rules: the negation excludes styles/Coder from the cached paths, and the cache key hashes styles/Coder/** so any rule change invalidates the cache even if the negation behavior ever regresses (actions/toolkit#713, actions/cache#494). Verification: - make lint/actions: clean (zizmor, actionlint). - make fmt/markdown: no changes. - make lint/markdown: 0 errors across 463 files. - make lint/prose: exit 0; baseline 391 errors / 5201 warnings / 7673 suggestions across 458 files (consistent with the pre-review run). --- .github/vale-problem-matcher.json | 18 ++++++++ .github/workflows/docs-ci.yaml | 73 ++++++++++++++++++++----------- .gitignore | 11 +++-- .vale.ini | 24 +++++++--- Makefile | 17 +++---- docs/.style/README.md | 8 ++-- docs/.style/style-guide.md | 53 +++++++++++----------- 7 files changed, 129 insertions(+), 75 deletions(-) create mode 100644 .github/vale-problem-matcher.json diff --git a/.github/vale-problem-matcher.json b/.github/vale-problem-matcher.json new file mode 100644 index 00000000000..bedf0e0b3e3 --- /dev/null +++ b/.github/vale-problem-matcher.json @@ -0,0 +1,18 @@ +{ + "problemMatcher": [ + { + "owner": "vale", + "severity": "warning", + "pattern": [ + { + "regexp": "^(.+):(\\d+):(\\d+):([^:]+):(.+)$", + "file": 1, + "line": 2, + "column": 3, + "code": 4, + "message": 5 + } + ] + } + ] +} diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 7122bb154d4..3a2993ba34b 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -70,40 +70,60 @@ jobs: env: ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }} - # `make docs/.style/.vale-synced` downloads the pinned Vale binary - # into build/ and runs `vale sync` so the configured packages exist - # under StylesPath. The sync is idempotent across runs and gated by - # an mtime sentinel that the cache below preserves between workflow - # invocations. + # Vale's behavior on the next two steps: + # + # - `Prepare Vale styles` downloads the pinned Vale binary and runs + # `vale sync` to materialize the configured packages under + # StylesPath. This step has no `continue-on-error`: a download or + # sync failure should break the job. + # - `prose` runs Vale against the changed-md set filtered to docs/ + # paths only, excluding the synced upstream packages under + # docs/.style/styles/. We post-filter in shell rather than using a + # second tj-actions/changed-files step because the `docs/**.md` + # glob there silently skips dot-prefixed dirs (it would miss our + # own docs/.style/style-guide.md while still treating the + # top-level `**.md` filter as authoritative). The early exit + # handles PRs that only touched non-docs markdown like AGENTS.md + # or README.md. + # + # The step uses `vale --no-exit` so warning- and suggestion-level + # alerts do not fail CI, while real failures (missing binary, bad + # config) still propagate. The problem matcher at + # `.github/vale-problem-matcher.json` parses `--output=line` output + # and surfaces each alert as an inline PR annotation; remove the + # matcher after the run so it does not leak into later steps. + # + # The cache below covers the synced upstream packages and the Vale + # binary. Two layers of defense keep the hand-authored Coder rules + # under docs/.style/styles/Coder safe from a stale cache: + # + # 1. The negation excludes docs/.style/styles/Coder from the + # cached paths. actions/cache negation can be unreliable + # (actions/toolkit#713, actions/cache#494) when patterns mix + # directory levels, but works here because both patterns sit + # directly under docs/.style/styles/. + # 2. The cache key includes a hash of docs/.style/styles/Coder/**, + # so any rule change invalidates the cache even if a future + # cache release regresses the negation behavior. The key also + # hashes .vale.ini and mise.toml: either change forces a fresh + # `vale sync` and a fresh binary download. - name: Cache Vale styles and binary if: steps.changed-md.outputs.any_changed == 'true' uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | - docs/.style/styles/Google - docs/.style/styles/alex - docs/.style/styles/write-good + docs/.style/styles/* + !docs/.style/styles/Coder docs/.style/.vale-synced build/vale-* - key: vale-${{ hashFiles('.vale.ini', 'mise.toml') }} + key: vale-${{ hashFiles('.vale.ini', 'mise.toml', 'docs/.style/styles/Coder/**') }} + + - name: Prepare Vale styles + if: steps.changed-md.outputs.any_changed == 'true' + run: make docs/.style/.vale-synced - # Vale runs on the changed-md set filtered to docs/ paths only, - # excluding the synced upstream packages under docs/.style/styles/. - # We post-filter in shell rather than using a second - # tj-actions/changed-files step because the `docs/**.md` glob there - # silently skips dot-prefixed dirs (so it would miss our own - # docs/.style/style-guide.md while still treating the top-level - # `**.md` filter as authoritative). The early exit handles PRs that - # only touched non-docs markdown like AGENTS.md or README.md. - # - # The step is non-blocking by design (continue-on-error: true). v1 - # lands every rule at `warning`, which makes Vale exit non-zero on - # nearly every PR. Annotations stay visible in the step log; promote - # rules to `error` (and drop continue-on-error) once their - # existing-content violation count reaches zero. See DOCS-40. - name: prose if: steps.changed-md.outputs.any_changed == 'true' - continue-on-error: true run: | files=$(printf '%s\n' "$ALL_CHANGED_FILES" \ | tr ',' '\n' \ @@ -113,8 +133,9 @@ jobs: echo "No changed Markdown files under docs/ (excluding synced styles); skipping Vale." exit 0 fi - make docs/.style/.vale-synced vale=$(find build/ -maxdepth 1 -name 'vale-*' -type f -print -quit) - printf '%s\n' "$files" | xargs -d '\n' "$vale" + echo "::add-matcher::.github/vale-problem-matcher.json" + printf '%s\n' "$files" | xargs -d '\n' "$vale" --no-exit --output=line + echo "::remove-matcher owner=vale::" env: ALL_CHANGED_FILES: ${{ steps.changed-md.outputs.all_changed_files }} diff --git a/.gitignore b/.gitignore index 3d2563054d3..8cb191cfe49 100644 --- a/.gitignore +++ b/.gitignore @@ -14,13 +14,12 @@ vendor/ yarn-error.log # Vale (prose linter) pulls package contents into docs/.style/styles/ on -# `vale sync`. Each package directory and the sync sentinel are gitignored -# so the synced content does not bloat the repo. The Coder/ directory next -# to them is tracked because it holds our custom rules. +# `vale sync`. Each synced package directory and the sync sentinel are +# gitignored. The Coder/ directory next to them is tracked because it +# holds our custom rules. docs/.style/.vale-synced -docs/.style/styles/Google/ -docs/.style/styles/alex/ -docs/.style/styles/write-good/ +docs/.style/styles/*/ +!docs/.style/styles/Coder/ # Test output files test-output/ diff --git a/.vale.ini b/.vale.ini index 2ec36e28b3a..ca02386339a 100644 --- a/.vale.ini +++ b/.vale.ini @@ -5,11 +5,21 @@ # styles and disabled rules is documented in DOCS-40 and reproducible via # `make lint/prose`. # -# Severity policy (v1): every rule lands at `warning`. CI is non-blocking -# (Makefile wraps Vale in `|| true`). A rule promotes to `error` only after -# (a) it is objectively correct and (b) the existing-content violation count -# reaches zero. Judgment rules (Wordiness, Weasel, ThereIs) stay at -# `suggestion` and never promote to `error`. +# Severity policy. Rules sit at a level that reflects two things +# together: false-positive rate against real Coder docs and the gravity of +# the rule. Low FPs plus high gravity argues for `error`; lower gravity or +# more judgment calls argue for `warning` or `suggestion`. v1 lands most +# rules at `warning` and the wordiness rules at `suggestion`. Promote a +# rule to `error` only when (a) its false-positive rate against real +# content is effectively zero and (b) the existing-content violation count +# for that rule is also zero. +# +# About Vale's exit code: Vale exits non-zero only when error-level alerts +# are found, regardless of `MinAlertLevel`. The Makefile invokes Vale with +# `--no-exit` to suppress that exit while the un-overridden Google +# error-level rules still produce a baseline error count. Real failures +# (missing binary, bad config, missing files) still propagate. See +# DOCS-40 for the rollout plan. # # The styles themselves live under docs/.style/styles/ after `vale sync`, # which the Makefile target invokes once per .vale.ini change. They are @@ -24,7 +34,7 @@ MinAlertLevel = suggestion Packages = Google, alex, write-good [*.md] -BasedOnStyles = Google, write-good +BasedOnStyles = Google, write-good, Coder # --- Google curation ------------------------------------------------------- # Google.EmDash conflicts with our em-dash ban (see scripts/check_emdash.sh @@ -45,7 +55,7 @@ Google.WordList = warning # Google.Spacing flags ASCII codepoint sequences like `k.R` or # `codersdk.SomeType` as "should have one space." It produces ~4,500 -# errors against `docs/reference/api/schemas.md` alone (Vale spike, +# errors against `docs/reference/api/schemas.md` alone (measured # 2026-05-18) because codersdk type names match the pattern. The # architectural decision for generated content is to fix the upstream # Go generators (clidocgen, apidocgen, auditdocgen, metricsdocgen) diff --git a/Makefile b/Makefile index c0d39f3b599..66eb481e4c0 100644 --- a/Makefile +++ b/Makefile @@ -872,21 +872,22 @@ build/vale-$(VALE_VERSION): # `vale sync` pulls the packages listed in .vale.ini's Packages directive # into StylesPath (docs/.style/styles/). The .vale-synced sentinel makes # sync idempotent across `make lint/prose` calls and lets warm checkouts -# skip the re-sync entirely. Touch any time .vale.ini changes to force a -# re-sync. +# skip the re-sync entirely. Make rebuilds this target when `.vale.ini` +# changes. docs/.style/.vale-synced: .vale.ini build/vale-$(VALE_VERSION) @echo "$(GREEN)==>$(RESET) $(BOLD)vale sync$(RESET)" build/vale-$(VALE_VERSION) sync @touch $@ -# v1 severity policy lands every rule at warning. Vale exits non-zero when -# alerts at MinAlertLevel or above are found, so the `|| true` keeps -# `make lint/prose` from failing while the cleanup PRs land. Promote rules -# to error (and remove the wrapper) when their existing-content violation -# count reaches zero. See DOCS-40. +# Vale exits non-zero only on error-level alerts. `--no-exit` keeps the +# target green while the un-overridden Google error-level rules still +# produce a baseline error count; real failures (missing binary, bad +# config, missing files) still propagate. Once the baseline error count +# reaches zero, drop `--no-exit` and surface error-level violations as +# real failures. See DOCS-40. lint/prose: docs/.style/.vale-synced @echo "$(GREEN)==>$(RESET) $(BOLD)lint/prose$(RESET)" - build/vale-$(VALE_VERSION) docs/ || true + build/vale-$(VALE_VERSION) --no-exit docs/ .PHONY: lint/prose # pre-commit and pre-push mirror CI checks locally. diff --git a/docs/.style/README.md b/docs/.style/README.md index 3679b947476..1f9b6523461 100644 --- a/docs/.style/README.md +++ b/docs/.style/README.md @@ -77,12 +77,14 @@ write-good) into `docs/.style/styles/`; subsequent runs are fast. make lint/prose ``` -The target wraps Vale in `|| true` so warnings do not fail the build. To -see Vale's raw exit code, invoke the binary directly: +The target uses Vale's `--no-exit` flag so warning- and suggestion-level +alerts do not produce a non-zero exit; real failures (missing binary, +bad config) still propagate. To see Vale's raw exit code on errors, +drop `--no-exit` and invoke the binary directly: ```shell make docs/.style/.vale-synced -./build/vale-*/vale docs/ # or pass specific files +./build/vale-* docs/ # or pass specific files ``` `.vale.ini` at the repo root selects the curated rule set. See its diff --git a/docs/.style/style-guide.md b/docs/.style/style-guide.md index 8832a296223..0f32db727fb 100644 --- a/docs/.style/style-guide.md +++ b/docs/.style/style-guide.md @@ -90,37 +90,40 @@ behind the cherry-picked base styles and the severity policy. ### Running Vale locally -The canonical entry point is `make lint/prose`. The first run downloads -the pinned Vale binary and the configured style packages; subsequent -runs reuse them. The target wraps Vale in `|| true` so warnings do not -break `make lint`, matching the v1 non-blocking policy. +See [`docs/.style/README.md`](README.md#running-vale-locally) for the +make target, the `--no-exit` rationale, and the rule-set pointer. ### Severity policy (v1) -Every rule lands at `warning`. CI is non-blocking through -`continue-on-error: true` on the prose step. A rule promotes to `error` -only when (a) it is objectively correct (typo, brand-name casing, banned -substitution) and (b) the existing-content violation count reaches zero. -Judgment-based rules (Wordiness, Weasel, ThereIs) stay at `suggestion` -and never promote to `error`. +Rule severity sits at a level that reflects two things together: the +rule's false-positive rate against real Coder docs and the gravity of the +rule. Low FPs plus high gravity argues for `error`; lower gravity or more +judgment calls argue for `warning` or `suggestion`. + +v1 lands most rules at `warning` and the wordiness rules at `suggestion`. +A rule promotes to `error` only when (a) its false-positive rate against +real content is effectively zero and (b) the existing-content violation +count for that rule is also zero. + +Vale exits non-zero only on error-level alerts, regardless of +`MinAlertLevel`. The Makefile invokes Vale with `--no-exit` so the +baseline error count from un-overridden Google rules does not fail CI; +real failures (missing binary, bad config) still propagate. Drop +`--no-exit` once the baseline error count is zero. ### Active rule set -The curated set documented in `.vale.ini`: - -- **Google** (base): all rules except `EmDash` (conflicts with our em-dash - ban), `Latin` (i.e. and e.g. are fine in technical writing), and `Spacing` - (fires aggressively on codersdk type names in the auto-generated API - reference; re-enable once the Go generators emit proper spacing). - `Parens` is softened to `suggestion` and `WordList` stays at `warning`. -- **write-good** (base, with disables): `Passive` and `E-Prime` are off. - `TooWordy` and `ThereIs` stay at `suggestion`; `Weasel` is at `warning`. -- **alex** (cherry-picked): `Ablist`, `Condescending`, `LGBTQ`, - `ProfanityLikely`, `Race`, `Suicide` at `warning`. The `ProfanityMaybe` - and `ProfanityUnlikely` rules fire on technical terms like `execute`, - `kill`, `failed`, and `attack`, so they are left off. -- **Coder** (custom): empty in v1. Rules land through the rule-specific - tickets in this project (see `docs/.style/styles/Coder/README.md`). +The curated set lives in `.vale.ini`'s inline comments. Run +`make lint/prose` to see it in action. The high-level shape: + +- **Google** as the base, with a handful of disables and softer levels + on high-volume rules. +- **write-good** for wordiness, with passive voice and E-Prime off. +- **alex** loaded a la carte for the inclusive-language checks that do + not fire on technical vocabulary. +- **Coder** for custom rules. Empty in v1; rules land through the + rule-specific tickets in this project (see + `docs/.style/styles/Coder/README.md`). ## Editor setup From 0048444e9920340c06618e32eb83c9ed0165ca70 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Tue, 19 May 2026 17:05:06 +0000 Subject: [PATCH 06/10] docs: address Coder Agents R2 review on Vale wiring Three follow-ups from review id 4320840969. DEREM-13 (P3): drop the redundant `grep -v '^docs/\.style/styles/'` filter from the prose step. Synced upstream packages are gitignored, so tj-actions/changed-files never lists them; the filter only ever rejects tracked Coder/**.md files, which is the opposite of what we want. The inline comment now documents why no second filter exists. DEREM-14 (P3): fix the README's --no-exit explanation. The DEREM-4 fix chain reached .vale.ini, the Makefile, and style-guide.md but missed docs/.style/README.md. The README now matches the rest: --no-exit suppresses the exit from the baseline error count produced by un-overridden Google error-level rules, not from warnings/suggestions (which never trigger non-zero exit regardless of --no-exit). DEREM-15 (Nit): same model fix in the docs-ci.yaml prose-step comment. Verification: make lint/actions, make fmt/markdown, make lint/markdown, make lint/prose all clean. Vale baseline unchanged (391 errors / 5201 warnings / ~7673 suggestions across 458 files). --- .github/workflows/docs-ci.yaml | 29 +++++++++++++++-------------- docs/.style/README.md | 8 ++++---- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/docs-ci.yaml b/.github/workflows/docs-ci.yaml index 3a2993ba34b..8a05b78d11a 100644 --- a/.github/workflows/docs-ci.yaml +++ b/.github/workflows/docs-ci.yaml @@ -77,18 +77,20 @@ jobs: # StylesPath. This step has no `continue-on-error`: a download or # sync failure should break the job. # - `prose` runs Vale against the changed-md set filtered to docs/ - # paths only, excluding the synced upstream packages under - # docs/.style/styles/. We post-filter in shell rather than using a - # second tj-actions/changed-files step because the `docs/**.md` - # glob there silently skips dot-prefixed dirs (it would miss our - # own docs/.style/style-guide.md while still treating the - # top-level `**.md` filter as authoritative). The early exit - # handles PRs that only touched non-docs markdown like AGENTS.md - # or README.md. + # paths only. We post-filter in shell rather than using a second + # tj-actions/changed-files step because the `docs/**.md` glob + # there silently skips dot-prefixed dirs (it would miss our own + # docs/.style/style-guide.md while still treating the top-level + # `**.md` filter as authoritative). The early exit handles PRs + # that only touched non-docs markdown like AGENTS.md or README.md. + # No second filter excludes docs/.style/styles/: synced upstream + # packages are gitignored (so changed-files never lists them) and + # tracked Coder/**.md files should go through Vale. # - # The step uses `vale --no-exit` so warning- and suggestion-level - # alerts do not fail CI, while real failures (missing binary, bad - # config) still propagate. The problem matcher at + # The step uses `vale --no-exit` so the baseline error count from + # un-overridden Google error-level rules does not fail CI, while + # real failures (missing binary, bad config) still propagate. The + # problem matcher at # `.github/vale-problem-matcher.json` parses `--output=line` output # and surfaces each alert as an inline PR annotation; remove the # matcher after the run so it does not leak into later steps. @@ -127,10 +129,9 @@ jobs: run: | files=$(printf '%s\n' "$ALL_CHANGED_FILES" \ | tr ',' '\n' \ - | grep -E '^docs/' \ - | grep -v '^docs/\.style/styles/' || true) + | grep -E '^docs/' || true) if [ -z "$files" ]; then - echo "No changed Markdown files under docs/ (excluding synced styles); skipping Vale." + echo "No changed Markdown files under docs/; skipping Vale." exit 0 fi vale=$(find build/ -maxdepth 1 -name 'vale-*' -type f -print -quit) diff --git a/docs/.style/README.md b/docs/.style/README.md index 1f9b6523461..22b16e0e0fb 100644 --- a/docs/.style/README.md +++ b/docs/.style/README.md @@ -77,10 +77,10 @@ write-good) into `docs/.style/styles/`; subsequent runs are fast. make lint/prose ``` -The target uses Vale's `--no-exit` flag so warning- and suggestion-level -alerts do not produce a non-zero exit; real failures (missing binary, -bad config) still propagate. To see Vale's raw exit code on errors, -drop `--no-exit` and invoke the binary directly: +The target uses Vale's `--no-exit` flag so the baseline error count from +un-overridden Google rules does not produce a non-zero exit; real +failures (missing binary, bad config) still propagate. To see Vale's raw +exit code on errors, drop `--no-exit` and invoke the binary directly: ```shell make docs/.style/.vale-synced From d394b14876784eb41a385d04ef2db257b672a0e9 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Tue, 19 May 2026 18:04:15 +0000 Subject: [PATCH 07/10] docs(.style/README.md): note the build/vale-* glob gotcha DEREM-16 (Nit) from review id 4321386259. `./build/vale-* docs/` is fine when only one vale-X.Y.Z binary exists in build/, but if a developer bumps the version in mise.toml without running `make clean`, multiple binaries coexist and the glob expands to multiple positional args, breaking the command. Low-risk path because three conditions have to align (version bump + no clean + manual invocation), but worth a sentence so a developer who hits it knows to `make clean`. --- docs/.style/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/.style/README.md b/docs/.style/README.md index 22b16e0e0fb..c4e64d4bd78 100644 --- a/docs/.style/README.md +++ b/docs/.style/README.md @@ -87,5 +87,13 @@ make docs/.style/.vale-synced ./build/vale-* docs/ # or pass specific files ``` +The `./build/vale-*` glob expands to whatever Vale binaries exist under +`build/`. If you bump the version in `mise.toml` without running +`make clean`, multiple `vale-*` binaries can coexist and the glob expands +to multiple arguments, breaking the invocation. The canonical `make +lint/prose` path uses the exact versioned binary and is always correct; +run `make clean` before using the direct-invocation form if you've ever +changed the pinned version locally. + `.vale.ini` at the repo root selects the curated rule set. See its inline comments for the rationale on each enabled or disabled rule. From 5276b22f4723cbd72eb83abd63368967ee7c352d Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Tue, 19 May 2026 19:49:22 +0000 Subject: [PATCH 08/10] feat(docs/.style/styles/Coder): add Coder.GerundHeading Vale rule Adds a warning-level Vale rule that flags headings beginning with an -ing word (typically a gerund or present participle, like 'Installing' or 'Configuring'). The rule uses extends: existence with scope: heading and a regex anchored to the start of the heading text, since Vale's sequence rule type (which would let us condition on the POS tag VBG) is documented as sentence-scoped and does not honor scope: heading. Google and Microsoft style packages both use existence + scope: heading + regex for all their heading-targeted rules; this rule follows the same pattern. A small exceptions list covers words that end in 'ing' but are not gerunds (Bring, String, Spring, King, Ring, Sting, Sing, Thing, Wing). Concept-noun gerunds (Logging, Networking, Monitoring, Troubleshooting) are intentionally NOT in the exceptions list: those headings often read better as imperatives or full nouns and writers should see the warning and decide. Adds a 'Headings' section to docs/.style/style-guide.md with the 'Gerund headings' subsection explaining the rule, examples, and how to silence individual instances. Updates docs/.style/styles/Coder/README.md to list DOCS-191. Closes DOCS-191. --- docs/.style/style-guide.md | 44 ++++++++++++++++++++ docs/.style/styles/Coder/GerundHeading.yml | 48 ++++++++++++++++++++++ docs/.style/styles/Coder/README.md | 1 + 3 files changed, 93 insertions(+) create mode 100644 docs/.style/styles/Coder/GerundHeading.yml diff --git a/docs/.style/style-guide.md b/docs/.style/style-guide.md index 0f32db727fb..4d186e5712e 100644 --- a/docs/.style/style-guide.md +++ b/docs/.style/style-guide.md @@ -63,6 +63,50 @@ To be filled in by rule-specific tickets. Planned coverage: [DOCS-44](https://linear.app/codercom/issue/DOCS-44), origin tracked in [DOCS-181](https://linear.app/codercom/issue/DOCS-181) +## Headings + +To be filled in by rule-specific tickets. Planned coverage: + +- Sentence case in titles and headings - see + [DOCS-38](https://linear.app/codercom/issue/DOCS-38) + +### Gerund headings + +**Rule**: `Coder.GerundHeading` (warning). + +Avoid leading a heading with a gerund (an -ing word: "Installing," +"Configuring," "Setting up"). Two alternatives almost always read more +cleanly: + +1. **Imperative** for task headings. Use the bare verb instead of the + -ing form when the section is a step or how-to. +2. **Noun** for concept headings. Use the noun form when the section + describes a thing rather than an action. + +| Avoid | Prefer (imperative) | Prefer (noun) | +|------------------------------|----------------------------|---------------------| +| Installing Coder | Install Coder | Installation | +| Configuring authentication | Configure authentication | Authentication | +| Setting up your workspace | Set up your workspace | Workspace setup | +| Managing workspace schedules | Manage workspace schedules | Workspace schedules | + +The right choice depends on the page. Imperative reads well in tutorials +and reference sections that walk through tasks. Noun reads well in +overview, conceptual, and feature-list pages. Concept-noun gerunds like +"Logging," "Monitoring," "Networking," and "Troubleshooting" are flagged +by the rule. Promote them to imperative ("Monitor your deployment") or +convert them fully to nouns ("Network architecture") when the rewrite +reads better; leave them as-is when the gerund-form is the established +term and the alternatives feel forced. + +The rule fires on the first word of any heading or title that ends in +`-ing` and starts with a capital letter. A small exception list covers +non-gerund words that happen to end in `-ing` (`Bring`, `String`, +`Spring`, `King`, `Ring`, `Sting`, `Sing`, `Thing`, `Wing`). To silence a +specific instance that the exception list does not cover, wrap the +heading with `` and +``. Add a justifying comment. + ## Formatting To be filled in by rule-specific tickets. Planned coverage: diff --git a/docs/.style/styles/Coder/GerundHeading.yml b/docs/.style/styles/Coder/GerundHeading.yml new file mode 100644 index 00000000000..a45b4734f98 --- /dev/null +++ b/docs/.style/styles/Coder/GerundHeading.yml @@ -0,0 +1,48 @@ +# Coder.GerundHeading - flag titles and headings whose first word ends in +# -ing (typically a gerund or present participle, e.g., "Installing", +# "Configuring", "Setting"). +# +# Why: gerund-leading headings are a common stylistic anti-pattern. Two +# alternatives almost always read more cleanly: +# +# 1. Imperative: "Install Coder" instead of "Installing Coder". +# 2. Noun: "Installation" instead of "Installing". +# +# The right choice is context-dependent (imperative for task headings, +# noun for concept headings), so this rule lands at warning rather than +# error: it surfaces candidates for review without forcing a rewrite. +# +# Implementation: pure regex on a heading-scoped existence rule. Vale's +# `sequence` rule type would let us condition on the POS tag VBG, but +# sequence rules are sentence-scoped, not heading-scoped (see +# https://vale.sh/docs/checks/sequence). For heading-targeted rules, +# Google's and Microsoft's style packages both use existence + scope: +# heading + regex; we follow the same pattern. +# +# False positives: words that end in "ing" but aren't gerunds, like +# "Bring" or "String". The exceptions list below collects these. Concept +# nouns formed from gerunds ("Logging", "Networking", "Monitoring") are +# intentionally NOT in the exceptions list: those headings often read +# better as nouns or imperatives, and writers should see the warning and +# decide. +extends: existence +message: "Heading starts with an -ing word ('%s'); consider imperative ('Install') or noun ('Installation') instead. See style-guide.md." +link: https://github.com/coder/coder/blob/main/docs/.style/style-guide.md#gerund-headings +level: warning +scope: heading +nonword: false +# These words end in "ing" but are not gerunds (verb-base or non-verb +# nouns). Headings that begin with one of these are not the target of +# this rule. +exceptions: + - Bring + - Spring + - String + - King + - Ring + - Sting + - Sing + - Thing + - Wing +tokens: + - '^[A-Z][a-z]+ing\b' diff --git a/docs/.style/styles/Coder/README.md b/docs/.style/styles/Coder/README.md index 1fbfe3c6239..dc254b4b245 100644 --- a/docs/.style/styles/Coder/README.md +++ b/docs/.style/styles/Coder/README.md @@ -21,6 +21,7 @@ Linear project add rules incrementally: | [DOCS-44](https://linear.app/codercom/issue/DOCS-44) | Em-dash and en-dash mirror in Vale | | [DOCS-182](https://linear.app/codercom/issue/DOCS-182) | Inclusive-language substitutions | | [DOCS-183](https://linear.app/codercom/issue/DOCS-183) | Product-voice rules | +| [DOCS-191](https://linear.app/codercom/issue/DOCS-191) | Gerund-leading headings | ## Authoring a new rule From c6a5adb1e765b804c1ad6132251ea3719f9909d9 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Tue, 19 May 2026 20:46:33 +0000 Subject: [PATCH 09/10] docs(docs/.style/styles/Coder/GerundHeading.yml): document the source-code reason sequence rules cannot reach headings After a question from a reviewer about whether this rule should use `extends: sequence` with the VBG POS tag rather than a regex existence rule, dig into Vale 3.14.1 source and add the architectural reason to the rule's YAML comment. Two facts in Vale's source force the existence-based approach for heading-targeted rules: 1. internal/check/sequence.go:75 ends NewSequence with `rule.Definition.Scope = []string{"sentence"}`. The Run method's comment at line 247 reads "This is *always* sentence-scoped." Any user-supplied `scope:` on a sequence rule is silently overwritten. 2. internal/lint/ast.go::lintScope dispatches heading content with scope `text.heading.h2.md` to lintBlock directly, skipping the lintProse path. Only lintProse calls nlp.Compute, which is the function that produces `sentence.*`-scoped sub-blocks. Heading text therefore never appears as a block whose scope contains `sentence`. Empirically verified against Vale 3.14.1: an `extends: sequence` rule with `pattern: '\\w+ing'` + `tag: VBG` + `scope: heading` fires on paragraph text but is silent on H1-H6. Behavior of the rule is unchanged. Only the YAML comment is updated to capture the investigation so the next reviewer doesn't have to redo it. --- docs/.style/styles/Coder/GerundHeading.yml | 40 ++++++++++++++++++---- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/docs/.style/styles/Coder/GerundHeading.yml b/docs/.style/styles/Coder/GerundHeading.yml index a45b4734f98..cbe7bd0396d 100644 --- a/docs/.style/styles/Coder/GerundHeading.yml +++ b/docs/.style/styles/Coder/GerundHeading.yml @@ -12,12 +12,40 @@ # noun for concept headings), so this rule lands at warning rather than # error: it surfaces candidates for review without forcing a rewrite. # -# Implementation: pure regex on a heading-scoped existence rule. Vale's -# `sequence` rule type would let us condition on the POS tag VBG, but -# sequence rules are sentence-scoped, not heading-scoped (see -# https://vale.sh/docs/checks/sequence). For heading-targeted rules, -# Google's and Microsoft's style packages both use existence + scope: -# heading + regex; we follow the same pattern. +# Implementation: pure regex on a heading-scoped existence rule. +# +# Why not `extends: sequence` with `tag: VBG`? It cannot reach headings +# in Vale 3.14.x. Two architectural facts force this: +# +# 1. `internal/check/sequence.go` ends NewSequence with +# `rule.Definition.Scope = []string{"sentence"}` (and the Run +# method's comment reads "This is *always* sentence-scoped."), +# so any user-supplied `scope:` is overwritten. +# 2. `internal/lint/ast.go::lintScope` dispatches heading content +# with scope `text.heading.h2.md` straight to `lintBlock` and +# skips the `lintProse` path. Only `lintProse` calls +# `nlp.Compute`, which is the function that produces the +# `sentence.*`-scoped sub-blocks that sequence rules match. +# +# Net effect: heading text never appears as a block whose scope +# contains `sentence`, so a sequence rule's hardcoded `sentence` scope +# can never match a heading. Verified empirically against Vale 3.14.1 +# with `extends: sequence` + `pattern: '\w+ing'` + `tag: VBG` and +# `scope: heading`: fires on paragraph text, silent on H1-H6. +# +# For heading-targeted rules, Google's and Microsoft's style packages +# both use existence + scope: heading + regex; we follow the same +# pattern. The trade-off is that we maintain a small exceptions list +# for words like "Bring" and "String" that look like gerunds to a +# regex but aren't. POS tagging would catch those automatically, at +# the cost of (a) not running on headings at all and (b) the +# noun-vs-gerund ambiguity on terse heading text. The exceptions list +# is small and stable, so the maintenance cost is low. +# +# DOCS-185 tracks the broader investigation: filing an upstream Vale +# issue to make sequence rules honor user-supplied `scope:`, or +# alternatively exposing POS tags to script-based rules so a +# `extends: script` rule could implement this principled-ly. # # False positives: words that end in "ing" but aren't gerunds, like # "Bring" or "String". The exceptions list below collects these. Concept From ebe72fca1caf36787136dccbe5b00f0d66a302b9 Mon Sep 17 00:00:00 2001 From: Nick Vigilante Date: Wed, 8 Jul 2026 15:48:41 +0000 Subject: [PATCH 10/10] feat(docs/.style): enable Coder.GerundHeading --- docs/.style/styles/Coder/GerundHeading.yml | 67 ++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/.style/styles/Coder/GerundHeading.yml diff --git a/docs/.style/styles/Coder/GerundHeading.yml b/docs/.style/styles/Coder/GerundHeading.yml new file mode 100644 index 00000000000..7336bb3c906 --- /dev/null +++ b/docs/.style/styles/Coder/GerundHeading.yml @@ -0,0 +1,67 @@ +# Coder.GerundHeading - flag titles and headings whose first word ends in +# -ing (a gerund or present participle used as a verb form, e.g., +# "Installing", "Configuring", "Setting"). +# +# Prefer the imperative for task headings ("Install Coder") and the noun for +# concept headings ("Installation"). The full policy, including the exception +# categories mirrored below, lives in the style guide: +# docs/.style/style-guide/capitalization-and-punctuation.md#no-gerund-leading-headings +# +# Implementation: a heading-scoped existence rule with an anchored regex. +# Vale's POS-tagging `sequence` rules are sentence-scoped and never reach +# heading text, so Google's and Microsoft's heading rules use the same +# existence+regex approach. The trade-off is a small exceptions list for +# -ing words that are not verb forms. +# +# The exceptions fall into two groups: +# 1. Words that end in -ing but are not gerunds at all (Bring, String). +# 2. -ing words that name a feature, category, or attribute and read +# correctly as a heading lead (Logging, Monitoring, Networking, +# Troubleshooting). These match the style guide's Exceptions section. +extends: existence +message: "Heading starts with an -ing word ('%s'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings." +link: https://github.com/coder/coder/blob/main/docs/.style/style-guide/capitalization-and-punctuation.md#no-gerund-leading-headings +level: warning +scope: heading +nonword: false +exceptions: + # Not gerunds (verb-base or non-verb nouns). + - Bring + - King + - Ring + - Sing + - Spring + - Sting + - String + - Thing + - Wing + # Feature, category, or attribute nouns that read correctly as a heading + # lead. Mirrors the style guide's "Exceptions" section. + - Autoscaling + - Billing + - Breaking + - Caching + - Contributing + - Formatting + - Guiding + - Heading + - Licensing + - Logging + - Monitoring + - Naming + - Networking + - Ordering + - Pricing + - Provisioning + - Reading + - Rendering + - Routing + - Scaling + - Scheduling + - Styling + - Tracing + - Trailing + - Troubleshooting + - Versioning +tokens: + - '^[A-Z][a-z]+ing\b'