Skip to content

Validators

Every rule is declared as an attribute on a <block> tag inside a comment. One block can carry several attributes at once.

A start or end tag that fails to parse — most commonly a missing closing > — fails the whole run with Malformed block tag at line N, column N, rather than being silently ignored. Text counts as a tag only when whitespace follows <block or </block, or the comment ends there. So prose such as <block, filesystem> or <blockquote> is ignored.

Text inside backticks in a comment is ignored too, so a comment can quote a tag: // add a `<block keep-sorted>` tag. As in Markdown, the closing backticks must be on the same line, and there must be as many of them as opening ones.

A quoted value ends at the next quote of the same kind. To put that quote inside the value, write a backslash before it: keep-unique="\"(?P<value>[a-z]+)\"". The backslash stays in the value, so a regex reads \" as ". The other kind of quote needs no backslash: keep-unique='"(?P<value>[a-z]+)"'.

A tag can go on over several line comments in a row, such as // lines that rustfmt wraps. Only whitespace can come between them, so a comment after code on the next line does not continue the tag.

A pattern or a reference with a line break in it fails the run. A formatter that wraps the tag there may have dropped a space, so blockwatch can’t know what the value was. Keep each one on a single line. A line break after the comma between two references is fine.

I want to… Use
Force docs or config to be updated whenever some code changes affects
Assert two places still hold the same value same-as
Keep a list alphabetized or numerically ordered keep-sorted
Prevent duplicate entries in a list keep-unique
Require every line to match a format, or forbid some text line-pattern
Cap or fix the number of lines in a block line-count
Enforce a rule stated in plain English check-ai
Run arbitrary validation logic check-lua

Prefer the deterministic validators — affects, same-as, keep-sorted, keep-unique, line-pattern, line-count. They are fast, offline, and need no API key. Reserve check-ai for rules the others genuinely cannot express.

These apply to any block regardless of which validator it uses.

Names the block so other blocks can point at it with affects or same-as. Names also show up in blockwatch list.

# <block name="allowed-colors">
"red",
"green",
# </block>

A name must be unique within its file — a second block reusing one is a hard error, since every affects/same-as reference to it would be ambiguous.

Naming a block also makes its violations suppressible one at a time: only a named block’s violations carry the address that --suppress points at. An unnamed block’s violations can only be suppressed along with the rest of their file.

Controls how a violation is reported. Mirrors LSP diagnostic severities.

Value Reported Exit code
error (default) yes 1
warning yes 0
info yes 0
hint yes 0

Only error fails the run. This makes severity the way to introduce a rule without breaking CI on day one — land the block as a warning, clean up the existing violations, then promote it:

# <block keep-sorted severity="warning">
"cherry",
"apple",
# </block>

severity is declared beside the code and applies to every violation the block will ever produce. --suppress is passed on the command line and applies to violations that were already reported. Lower the severity when the rule is genuinely advisory here; suppress when the rule is right in general and wrong about this one case.

With a diff on stdin, a block is validated only if the diff touched its content or its start tag. Without a diff, every block in scope is validated.

affects is the exception in the other direction: it is inert without a diff, since it asks “was this edited without its counterpart?” — a question a full-tree run cannot answer. Every other validator runs in both modes.


← README