docs: close the create-vale-rule gaps that produce a silent rule - #174
Open
thecodedrift wants to merge 5 commits into
Open
docs: close the create-vale-rule gaps that produce a silent rule#174thecodedrift wants to merge 5 commits into
thecodedrift wants to merge 5 commits into
Conversation
A Vale rule that is malformed fails loudly. A Vale rule that is wrong passes verify, passes test, and reports nothing forever. Authoring the first house-style rules against this repository produced three of those in a row, each green on every local gate. Measured against the pinned 3.18.0, the engine does not close them: `extends: nonsense` and `scope: fenced` both verify clean and match nothing, while a field belonging to another check type throws E201 and suppresses every other Vale rule, because Vale reads one assembled config per run. The change adds a schema layer to the Vale verify path and closes the recipe gaps that produce a silent rule. Vale publishes no JSON Schema — the machine-readable field knowledge sits behind its paid hosted MCP — so the schema is hand-authored, and a corpus of rule files run through both the binary and the schema is what keeps the transcription honest. Delivery shape: stacked, merging forward. The recipe text lands alone, then the schema and its verify layer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
Every addition is a behaviour measured against the pinned Vale 3.18.0 binary, not a caution in principle: the scope vocabulary and what each value reaches, `raw` subsuming `code` and `text`, `~`/`&` and the silent no-op a negation over an unknown operand becomes, `scope` being per-rule, a `raw` rule being unsuppressable by Vale's directive, `nonword: true` for a punctuation-only token, scoping a rule out with a second matcher, collocation narrowing, three-way fixtures for a subject that lives in code, `limit`/`vocab`, `.yml` over `.yaml`, and that a green `test` proves nothing about the matcher's reach. Check types are now enumerated from the binary's own rejection message. There are twelve, not eleven — the docs fold `readability` into `metric`. The per-check field tables are measured the same way, correcting two published claims. Refs #170 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
`scope: figure.caption` looked inert because the obvious control — a `<figcaption>` nested in `<figure>` — is invisible to every scope, `text` and `raw` included. A bare `<figcaption>` is linted and the scope fires on it. The operand is measured after all; the first reading would have dropped it from the vocabulary. This is the case a known-valid control alongside each measurement exists to catch, so it is recorded in the design as well as the recipe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
…on's prefix Two more measurements against the pinned binary. `capitalization` takes `prefix` — singular — and rejects both `prefixes` and `suffixes`; the first draft of this table dropped the field entirely on the strength of the plural's rejection. Field keys decode case-insensitively (`Tokens:`, `ignoreCase:` both work) while `extends`, `message` and `level` are read literally and their values are case-sensitive, so `level: WARNING` and `extends: Existence` are both rejected. Recorded because the schema in the next unit has to match that split exactly or it rejects rules the binary accepts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
The revised sections told the author that `verify` rejects a bad `extends`, an unrecognized `scope`, and a foreign field. None of that is true until the schema layer lands, and this stack merges forward — so this unit alone would ship a recipe promising a check that does not exist, which is the exact failure the recipe is about. Each promise is replaced by the instruction it was standing in for, and comes back in the unit that makes it true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1
Member
Author
|
@claude /review |
|
Claude finished @thecodedrift's task in 4m 2s —— View job I'll analyze this and get back to you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stack (root → tip):
What this is
Unit 1 of a two-PR stack for the
vale-authoring-polishchange. Text only — nobehavior changes, no new code paths. It carries the OpenSpec artifacts, the
changeset for the whole stack, and the
create-vale-rulerevisions.Unit 2 (the schema and its
verifylayer) stacks on top of this branch.Why
A malformed Vale rule fails loudly. A Vale rule that is merely wrong passes
verify, passestest, and reports nothing forever — and nobody re-checks agreen rule. Authoring house-style rules against this repository produced three
of those in a row. The recipe is the only place those can be prevented, and it
was incomplete in the specific places that produce them.
What I measured
Everything added to the recipe was measured against the vendored Vale
3.18.0, by authoring a rule in a temp project with
BasedOnStyles =(so nobundled style can contribute a finding) and running the binary directly. Where
a verdict is "did not fire", a known-valid control was run against the same
document first.
Twelve check types, not eleven — and the binary settles it
An
extendsoutside the set is not silently ignored. Vale rejects the file,exits 2, and enumerates the whole set in the error:
That is twelve. The docs' eleven omits
readability, folding it intometric;the binary treats them as separate checks with disjoint fields
(
metrics/gradeversusformula/condition), and each rejects the other's.The recipe now says twelve and quotes the message it came from.
The
scopevocabularyMeasured firing:
text,code,raw,heading,heading.h1–h6,paragraph,sentence,list,blockquote,link,alt,summary,strong,emphasis,table,table.header,table.cell,table.caption,comment,comment.line,comment.block,frontmatter,frontmatter.<key>,figure.caption,text.class.<name>.figure.captiontook two attempts and is the best argument for theknown-valid control this change insists on. The obvious control — a
<figcaption>nested in<figure>— never fires, and neither does ascope: textcontrol over the same document, because Vale dropseverything inside a
<figure>element before any check runs. A bare<figcaption>is linted normally and the scope fires on it. Read off thefirst result alone, the operand would have been recorded as one the binary
ignores and dropped from the enum.
Reach, on one document holding the token in prose, in an inline span, and in a
fenced block:
text→ 1,code→ 1,[code, text]→ 2,raw→ 3.rawsubsumes the other two, which is now stated rather than implied.
~negation,&chaining, and the list form all parse and behave — but anegation over an operand Vale does not recognize is a silent no-op.
~fencedand
text & ~fencedboth fire on everything, so a typo inside a~removes theexclusion the rule was written for. That one was not in the design and is new.
Corrections to the design's own draft
metascope. The design namedmetaandmeta.class.<kind>as the v3.18.0 addition. The binary's vocabulary is
text.frontmatter.<key>,and
frontmatter/frontmatter.<key>is what fires.metanever fired inMarkdown, MDX, or HTML.
extends: nonsensedoes not verify clean and match nothing. The design'sContext table claims it does. On 3.18.0 it is rejected outright, exit 2, whole
run suppressed — the same blast radius as
E201. The genuinely silent case isscope, alone. That does not weaken the case for a schema layer, itre-labels it: two of the three defects are blast-radius arguments and one is a
silence argument.
Both corrections are recorded in
design.mdunder Measured against Vale3.18.0, alongside the per-check field matrix.
Per-check fields
Measured by adding each candidate key to a minimal rule of each check and
watching for
E201. Two published claims are wrong:capitalizationrejectsprefixes,suffixesandignorecase;occurrencerejectsexceptionsandvocab.consistencyandspellingaccept any key at all —bananafield: trueoneither loads without complaint and is ignored. They do not use the strict decode
the other ten do, so neither Vale nor a schema can catch a typo in their fields.
The recipe says so.
What changed in the recipe
rawsubsumingcodeandtext.scopeis per-rule; the single assembled config does not make rules interact.raw-scoped rule cannot be suppressed by<!-- vale Rule = NO -->(itreads the unparsed document), so a rule about a command needs
rawand tradesaway per-case exemption. Measured both ways.
nonword: truefor a punctuation-only token, with the em-dash rule as theworked before/after.
NO— and thatprecedence is positional, so the exclusion goes second.
pass/fixture from the literalsense first.
fail/must carry itinline, fenced, and in prose in one document.
limitandvocabin the common-fields table, and that a style file renamedto
.yamlis silently not loaded — no error, no warning, zero findings.testis notevidence the rule's matcher glob reaches any real file. The only check for
that is a real
checkagainst a real file.Verification
pnpm typecheck,pnpm lint,pnpm testall pass (829 tests, 53 files).pnpm buildthenpnpm cli agent create-vale-rulerenders cleanly, with%(VALE_VERSION)sinterpolating. No cross-reference test needed updating —recipe-cross-references.test.tsasserts the rendered format lists, which thesesections do not touch.
Two further corrections, found while writing the schema
capitalizationtakesprefix— singular. It rejectsprefixesandsuffixes, both of which the docs list. An earlier draft of this table readthe plural's rejection as the field not existing and dropped it entirely,
which would have been a regression against the recipe's original text.
Tokens:andignoreCase:are read exactly as their lowercase spellings,while
EXTENDS:fails with "Missing the required 'extends' key", andlevel: WARNINGandextends: Existenceare both rejected on their values.Delivery shape
Stacked, merging forward. This unit is text only: landing it alone leaves
check,verify, and the test suite exactly as they are. The changeset liveshere, at the bottom, and unit 2 extends the same file.
One consequence of merging forward is worth flagging, because it caught me: the
first draft of these sections told the author that
verifyrejects a badextends, an unrecognizedscope, and a foreign field. None of that is trueuntil unit 2 lands, so this unit alone would have shipped a recipe promising a
check that does not exist — which is the exact failure the recipe is about.
Those promises were replaced by the instruction each stood in for, and come back
in #175.
Fixes #170
Refs #171