You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
An ast-grep rule is structurally validated before it ever reaches the binary. A Vale rule is not, and the failures that gap allows are silent ones: the rule parses, verify returns ok: true, and it reports nothing forever.
The template
The ast-grep path is the shape to copy:
packages/cli/scripts/fetch-ast-grep-schema.ts fetches schemas/rule.json from the ast-grep tag matching the pinned @ast-grep/cli version
output lands in packages/cli/src/generated/ast-grep-rule-schema.json
packages/cli/src/rules/verify.ts consumes it as the schema layer of VerifyResult, ahead of the requirements and test layers
packages/cli/src/rules/vale/verify.ts has no counterpart. All 318 lines are fixture behavior: bucket coverage, then which fixtures fired. Nothing reads the rule's own structure.
One thing to settle first
The ast-grep schema is generated, which is why it cannot drift from the binary. Vale is unlikely to publish an equivalent for its check types — worth confirming before designing around it. If it does not, the schema has to be hand-authored, and then drift is the whole risk.
The repository already has the answer to that: packages/cli/test/vale-vendor-contract.test.ts pins Vale's givens by invoking the pinned binary directly. A hand-authored schema should be held to the same standard, so a claim in the schema is one the binary was measured making, and a Vale upgrade fails the contract test rather than silently invalidating the schema.
What it would catch
Each of these is silent today. Measured on the pinned 3.18.0 binary unless noted.
Check
Failure it prevents
scope enum
scope: fenced (not a Vale scope) verifies clean. Only test catches it, reported as "fail fixture did not fire" — which points the author at their pattern, not at the invalid scope.
%s count per extends
substitution needs two, existence one. The recipe states outright that a wrong count "passes every check below" and only a human reading the message catches it. A schema can require two %s for substitution.
nonword: true for punctuation tokens
A token with no word characters can never match, because Vale wraps tokens in word boundaries. The em-dash rule in #104 has exactly this shape and would ship as a no-op. Expressible as if tokens contains /^\W+$/ then require nonword: true.
consistency id must be word characters only
The recipe records that a hyphen here fails with E201 invalid group name and takes every Vale rule in the project down, since Vale reads one config per run.
required fields per extends
swap for substitution, token+max/min for occurrence, first/second for conditional, and the singular/plural token vs tokens split. Untested: I did not measure whether Vale errors or silently ignores a mismatched field, so this row needs checking before it is designed.
Bare words vs collocations — judgment. landed on fires on "the plane landed on time".
Fixture design — when a rule's subject normally appears in code, the fail/ fixture must contain it inline, fenced, and in prose. That is a coverage check and belongs next to the existing bucket-coverage logic in rules/vale/verify.ts, not in a schema.
Note on the .vale.ini
Converting the ini to JSON and schema-validating it was considered and is not worth doing. verify already catches both of its silent failures, with good messages:
zz-probe has no .vale.ini, so nothing scopes it and it will never run.
zz-probe/.vale.ini never enables zz-probe.zz-probe, so the rule is present but off.
That is the missing-config case and the id-must-agree-in-four-places case, which are the two things a schema there would buy. The leverage is entirely on the rule file.
An ast-grep rule is structurally validated before it ever reaches the binary. A Vale rule is not, and the failures that gap allows are silent ones: the rule parses,
verifyreturnsok: true, and it reports nothing forever.The template
The ast-grep path is the shape to copy:
packages/cli/scripts/fetch-ast-grep-schema.tsfetchesschemas/rule.jsonfrom the ast-grep tag matching the pinned@ast-grep/cliversionpackages/cli/src/generated/ast-grep-rule-schema.jsonpackages/cli/src/rules/verify.tsconsumes it as theschemalayer ofVerifyResult, ahead of the requirements and test layerspackages/cli/src/rules/vale/verify.tshas no counterpart. All 318 lines are fixture behavior: bucket coverage, then which fixtures fired. Nothing reads the rule's own structure.One thing to settle first
The ast-grep schema is generated, which is why it cannot drift from the binary. Vale is unlikely to publish an equivalent for its check types — worth confirming before designing around it. If it does not, the schema has to be hand-authored, and then drift is the whole risk.
The repository already has the answer to that:
packages/cli/test/vale-vendor-contract.test.tspins Vale's givens by invoking the pinned binary directly. A hand-authored schema should be held to the same standard, so a claim in the schema is one the binary was measured making, and a Vale upgrade fails the contract test rather than silently invalidating the schema.What it would catch
Each of these is silent today. Measured on the pinned 3.18.0 binary unless noted.
scopeenumscope: fenced(not a Vale scope) verifies clean. Onlytestcatches it, reported as "fail fixture did not fire" — which points the author at their pattern, not at the invalid scope.%scount perextendssubstitutionneeds two,existenceone. The recipe states outright that a wrong count "passes every check below" and only a human reading the message catches it. A schema can require two%sforsubstitution.nonword: truefor punctuation tokensif tokens contains /^\W+$/ then require nonword: true.consistencyid must be word characters onlyE201 invalid group nameand takes every Vale rule in the project down, since Vale reads one config per run.extendsswapforsubstitution,token+max/minforoccurrence,first/secondforconditional, and the singular/pluraltokenvstokenssplit. Untested: I did not measure whether Vale errors or silently ignores a mismatched field, so this row needs checking before it is designed.What it would not catch, and where those belong
rawdisables Vale's suppression comments (create-vale-rule: a rule about a command passes its fixtures and catches nothing #167, and the comment there) — runtime behavior, not structure.landed onfires on "the plane landed on time".fail/fixture must contain it inline, fenced, and in prose. That is a coverage check and belongs next to the existing bucket-coverage logic inrules/vale/verify.ts, not in a schema.Note on the
.vale.iniConverting the ini to JSON and schema-validating it was considered and is not worth doing.
verifyalready catches both of its silent failures, with good messages:That is the missing-config case and the id-must-agree-in-four-places case, which are the two things a schema there would buy. The leverage is entirely on the rule file.
Refs #170
Refs #167
🤖 Generated with Claude Code
https://claude.ai/code/session_017cEN93Acyp4zBwP3oDnyy1