Skip to content

feat(docs): add Coder.GerundHeading Vale rule to flag -ing-leading headings#25502

Draft
nickvigilante wants to merge 9 commits into
mainfrom
vigilante/docs-191-add-coder-gerundheading-rule
Draft

feat(docs): add Coder.GerundHeading Vale rule to flag -ing-leading headings#25502
nickvigilante wants to merge 9 commits into
mainfrom
vigilante/docs-191-add-coder-gerundheading-rule

Conversation

@nickvigilante

Copy link
Copy Markdown
Contributor

Summary

Adds Coder.GerundHeading, a warning-level Vale rule that flags headings beginning with an -ing word (typically a gerund or present participle, like Installing or Configuring).

Gerund-leading headings are a stylistic anti-pattern: imperative (Install Coder) reads better for task headings, and the noun form (Installation) reads better for concept headings. The right choice is context-dependent, so the rule lands at warning, not error.

Closes DOCS-191.

Corpus impact

  • 270 hits across the docs corpus.
  • 109 distinct -ing first words. All are real gerund or present-participle leads; no false positives in the flagged set.
  • 2 known false-positive words (Bring, String) are correctly skipped by the exception list. The list also seeds Spring, King, Ring, Sting, Sing, Thing, Wing for future content.
  • Baseline error count stays at 391 (make lint/prose). The rule adds 270 warnings.

Why existence + scope: heading and not sequence

The first prototype used Vale's sequence rule with tag: VBG (Penn Treebank gerund/present-participle). The rule fired zero times. The Vale docs explain why: every sequence rule requires at least one pattern token as an anchor, and sequence rules are sentence-scoped, not heading-scoped. So scope: heading had no effect.

Google and Microsoft style packages both use extends: existence + scope: heading + regex for every heading-targeted rule (HeadingAcronyms, HeadingColons, HeadingPunctuation, etc.). This rule follows the same pattern: ^[A-Z][a-z]+ing\b, anchored to the start of the heading text. The POS tagger is unused, but the regex is precise enough that we measured 100% true positives in the corpus.

Workflow disclosure

This is a docs-only change (one Vale rule, one style-guide section, one README ticket-list entry). Per the user's workflow rule, docs-only changes do not trigger /coder-agents-review. Human review only.

Implementation notes and decision log

Decisions

  • Severity = warning, not error. Many concept-noun gerunds (Logging, Networking, Monitoring, Troubleshooting) are legitimate heading leads. Flagging them as error would force rewrites that don't always improve readability. warning surfaces them for review and lets writers decide.
  • Anchored regex. Started without ^, which caught B in ## 🛡 Bulletproofing mid-heading. Anchoring to start dropped the corpus count from 320 to 270 and eliminated all mid-heading matches.
  • Exception list is conservative. Only includes -ing words that are demonstrably not gerunds (Bring, String, etc.). Concept-noun gerunds stay in the flagged set on purpose.
  • No POS tagger. Sequence rule type with tag: VBG would have been more principled, but Vale's sequence rules don't support scope: heading. The regex approach matches Google/Microsoft conventions.

Corpus sample (top 10 by frequency)

First word Hits
Troubleshooting 30
Using 19
Running 11
Configuring 11
Creating 9
Getting 9
Scaling 8
Testing 6
Monitoring 6
Managing 6

Files changed

  • docs/.style/styles/Coder/GerundHeading.yml (new): the rule.
  • docs/.style/style-guide.md: adds a ## Headings section with a ### Gerund headings subsection (rule ID, examples, exception-list policy, how to silence individual instances).
  • docs/.style/styles/Coder/README.md: adds DOCS-191 to the ticket list.

Verification

$ make lint/prose | tail -1
✖ 391 errors, 5471 warnings and 7688 suggestions in 458 files.

Baseline before this PR (PR #25467 tip):

✖ 391 errors, 5201 warnings and 7688 suggestions in 458 files.

Delta: 391 → 391 errors (unchanged, as expected for a warning-level rule), 5201 → 5471 warnings (+270 from the new rule).

Base

Based on PR #25467 (DOCS-40, Vale wiring). Will need to retarget to main after #25467 merges.


Filed via Coder Agents on Nick's behalf.

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.
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.
… 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.
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.
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 c3e9e2f (prose filter matches
  docs/.style paths) and 6054d9e (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).
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).
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`.
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.
@github-actions

Copy link
Copy Markdown

Docs preview

📖 View docs preview for docs/.style/style-guide.md

…-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.
@github-actions github-actions Bot added the stale This issue is like stale bread. label Jun 3, 2026
@github-actions github-actions Bot closed this Jun 7, 2026
@nickvigilante nickvigilante reopened this Jun 17, 2026
@nickvigilante nickvigilante removed the stale This issue is like stale bread. label Jun 17, 2026
@nickvigilante

Copy link
Copy Markdown
Contributor Author

Reopening. The stale bot auto-closed this on 2026-06-07 from inactivity, but the work is still active, tracked in Linear as DOCS-191 (In Review) under the Vale / docs style-guide initiative. This PR is stacked on vigilante/docs-40-... (the Vale CI PR, #25467), so it was sequenced behind that base rather than abandoned. I've removed the stale label and reset the timer; checks here may stay amber until the base (#25467) lands.

Reopened by Coder Agents on behalf of @nickvigilante.

Base automatically changed from vigilante/docs-40-add-vale-to-docs-ci-with-starter-microsoftgoogle-style-coder to main June 22, 2026 18:14
@datadog-coder

datadog-coder Bot commented Jun 22, 2026

Copy link
Copy Markdown

Pipelines

⚠️ Warnings

🚦 1 Pipeline job failed

contrib | title   View in Datadog   GitHub Actions

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: c6a5adb | Docs | Give us feedback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant