Explanation

The ruleset, and the defect each rule prevents

Every rule justified by the failure it stops, plus the rules that were considered and rejected.

This is a prose linting system: a Vale ruleset and a CI pipeline that enforce editorial standards on documentation the way a linter enforces them on code.

It exists because editorial review does not scale. One editor, applying good judgment one document at a time, is a bottleneck and a single point of failure. The rules below encode the judgments that can be encoded, so the editor's time goes to the ones that cannot.

The principle: every rule names its defect

A rule that cannot name the defect it prevents is a preference, and preferences do not belong in a build gate.

So every rule in this system carries the failure it stops, and naming that failure is how a rule gets in. A proposed rule that cannot complete the sentence "without this, a reader will…" is rejected, however tidy the writing it would produce.

The practical benefit is at the moment of failure. A writer who trips Docs.FutureTense sees the defect, not just the correction, and learns the standard. A writer who only sees avoid "will" learns to avoid the word. They do not learn why, so they rebuild the same problem somewhere the rule cannot reach it.

Severity policy

Two levels, and the split is deliberate.

error blocks the merge. Reserved for defects that mislead the reader, or that make the documentation wrong rather than merely worse. A wrong instruction and a broken cross-reference qualify. Awkward phrasing does not.

warning reports and does not block. For patterns that are usually worth changing and sometimes are not. The writer sees it and decides.

There is no suggestion level here, although Vale offers one. A third level invites a tier nobody reads, and a rule nobody reads is worse than absent: it adds noise that trains writers to skim the output, which is how the error lines get skimmed too.

The test for error: would a reviewer block a pull request over this? If no, it is a warning. Reviewers argue about where that line sits, which is why it is written down rather than left to whoever happens to be reviewing.

The rules

Correctness

These are error. They stop documentation that is wrong. Two of them run outside Vale, because Vale lints the text of one file at a time and these checks need more than that.

Internal links (lychee, offline) fails on a relative link or an anchor whose target does not exist.

Defect prevented: a reader following a cross-reference reaches a 404 and loses the thread. In any documentation set past a few dozen pages this is one of the most frequent defects, because renaming a file is easy and finding everything that pointed at it is not.

Required front matter (a yq check in CI) fails on a page missing title, description, or last_reviewed.

Defect prevented: a page with no description gets an auto-generated search snippet from its first sentence, which is usually a subordinate clause. A page with no last_reviewed date cannot be audited for staleness, so it never is.

Docs.VersionedCommands flags a version number in a command example that does not match the current release in docs/_data/versions.yml. Vale cannot read that file, so CI generates this rule from it before each run.

Defect prevented: a reader copies an install command that pins a release from eighteen months ago. Version numbers in prose rot silently; nothing fails until someone runs the command.

Docs.PlaceholderLeft flags TODO, TKTK, FIXME, XXX, lorem ipsum, and [insert …].

Defect prevented: a placeholder ships. This happens more than anyone admits, and it is the cheapest possible thing to catch automatically.

Clarity

Docs.DirectionalWords, Docs.ConditionAfterAction, and Docs.StepHasOneAction are error, because each pattern reliably misleads. Docs.AmbiguousPronoun is a warning, for the reason in its entry.

Docs.DirectionalWords flags see above, the table below, as shown on the right, click here, and similar phrases.

Defect prevented: the reference breaks on a narrow screen, in a single-page-per-topic export, or for a screen reader, where "below" has no meaning. Naming the target instead of its position survives every rendering.

Docs.ConditionAfterAction flags a list item that opens with a verb and states its condition (if, when, unless) after it.

Defect prevented: "Restart the service if the check fails." The reader has already restarted the service by the time they learn it was conditional. In a procedural step, the condition must precede the action.

Docs.StepHasOneAction flags a list item that joins two imperative verbs with and or then.

Defect prevented: the second action is skipped when the reader is interrupted between them, and interruption during a procedure is the normal case rather than the exception.

These two rules are not new standards. They are rules 1 and 7 of the Orchestrator manual, which states its writing rules in its front matter so a reviewer can check compliance. That manual relies on a human holding to them. This ruleset is the same two rules with the human taken out of the enforcement path, which is the only version that survives a team growing past one careful writer.

Docs.AmbiguousPronoun flags a sentence that opens with It, This, That, or They.

Defect prevented: "Restart the service and clear the cache. This can take a minute." Which one? The reader guesses, and half of them guess wrong. In a procedure, a wrong guess is a wrong action.

A regular expression cannot count the nouns in the previous sentence, so this rule fires on every sentence-initial pronoun, including the many that are unambiguous. As an error it would train writers to rephrase around it. As a warning it asks one question, and the writer answers it.

Consistency

These are warning. They are usually right.

Docs.TermSubstitutions maps informal or drifting terms to the approved one: log in for sign in and log on, uninstall for remove the app, unavailable for greyed out.

Defect prevented: a reader searching the help center for the word on the screen finds nothing, because three articles used three words for one action. Terminology drift is invisible to each individual author and obvious in aggregate.

Docs.FutureTense flags will before a verb that describes system behavior, such as will start or will return.

Defect prevented: "the service will start" reads as a prediction. "The service starts" reads as a fact. In reference documentation the difference matters, because the reader is deciding whether to depend on the behavior. Google's own Google.Will is turned off, because it flags every will and the two would report each hit twice.

Docs.Wordiness maps padding to its replacement: at this point in time to now, is able to to can, prior to to before. in order to is left to Google's word list, which already reports it.

Defect prevented: nothing, individually. In aggregate it is the difference between a page that is scanned and a page that is abandoned. Correctly a warning; a wordy sentence is not a wrong sentence.

Accessibility

Docs.AltText (error) flags an image with no alt text, or with alt text of fewer than 5 characters.

Defect prevented: a screen-reader user reaches a diagram and receives nothing, or receives img. Where a diagram carries information the prose does not, this is the whole page lost.

Docs.LinkText (error) flags link text of here, this, link, read more, or the URL itself.

Defect prevented: screen-reader users navigate by a list of links, stripped of surrounding prose. Nine links called "here" is nine identical entries. Sighted readers skimming a page get the same benefit from the fix.

Rules deliberately not written

This section matters as much as the rules, and it is the part most style systems omit.

Over-linting prose teaches writers to fight the tool. Once a writer starts rephrasing to satisfy a linter rather than to serve a reader, the system has inverted its purpose, and every rule after that point costs more than it returns.

Sentence length. Considered and rejected as a global rule. A 30-word sentence in a conceptual explanation can be correct; a 25-word sentence in a procedural step is not. The rule that survived is scoped: Docs.StepLength warns at 20 words inside list items only, where procedure steps live and the constraint has a reason.

Passive voice. Rejected, and Google.Passive is turned off. Passive voice is frequently the right choice in technical writing, because it puts the reader's object of interest in the subject position: "the invoice is finalized" is better than "the system finalizes the invoice" when the invoice is what the reader cares about. A passive-voice linter produces a large number of false positives, and a rule with a high false-positive rate trains writers to ignore the output.

Readability scores. Rejected. Flesch-Kincaid and its relatives measure syllables per word and words per sentence, which are proxies for difficulty rather than difficulty itself. "Set it to the one from before." scores as very easy and tells the reader nothing. A gate on a proxy optimizes the proxy.

Inclusive-language substitutions. Not rejected, but not automated here. These are handled by a shared, separately versioned Vale package maintained across the organization, because the list changes on a different cadence than this ruleset and should not require a pull request to this repository.

Headings must be sentence case. Rejected as an error, kept as a warning through Google.Headings. A heading in title case is inconsistent, but it misleads no one.

How it runs

Vale runs on every pull request that touches docs/, against the changed files only. Internal links are checked across the whole corpus on the same pull request, because a renamed page breaks links in files the pull request did not touch. A nightly run lints every page and checks external links, and it keeps one rolling issue up to date instead of opening a new one each night.

Results are posted as inline annotations on the diff, with file and line. This is not cosmetic. A CI failure that says only "Vale found 12 errors" requires the writer to reproduce locally to find them, and a check that is awkward to act on is a check that gets routed around.

The configuration and the workflow are in vale.ini and the custom styles and docs-quality.yml.

Measuring whether it works

A governance system that is never evaluated is a governance system that accumulates rules.

Three things are tracked:

Failures by rule, monthly. A rule that never fires is either fully internalized, in which case it is cheap to keep, or it is unnecessary. A rule that fires constantly is either catching a real and persistent problem, or it is wrong and is being worked around.

Suppression comments. Every vale off in the corpus is reviewed quarterly. A cluster of suppressions around one rule means the rule has false positives, and the correct response is to fix the rule rather than to accept the suppressions.

Time from pull request to merge on docs changes. If this rises after a rule lands, the rule is costing more than it returns, whatever its hit rate.

The ruleset has lost 3 rules to this review and gained 6. One of the three I had argued hard for. It fired so often on false positives that writers were suppressing it by reflex, which is worse than never having written it.

A ruleset only ever grows on its own. The review is what makes it shrink.

← Back to Portfolio