Skip to content

Commit a38f6bb

Browse files
committed
feat(vale): take Vale 3.20.0, and spend the zones it makes affordable
#293 published the 3.20.0 platform packages; this pins them (3.20.0-20260907164938) and takes what the binary actually changed. THE RELEASE THAT MATTERS TO US IS NOT THE ONE ON THE TIN. 3.20.0's headline is style-library authoring — rule inheritance, nested directories, scalar tuning from config — and none of it is reachable here: our schema rejects any `extends` that is not a check type, and a rule lives alone in its own directory. What we get is one bugfix, `honor comment directives at any indent`, which retires a workaround we shipped guidance for eight commits ago. Measured, 3.19.0 vs 3.20.0, on the same fixture: inline pair at a continuation indent 3.19.0 reports 2, 3.20.0 reports 1 the same pair over a `scope: raw` rule 3.19.0 reports 2, 3.20.0 reports 1 The first is the case 6c99c41 measured and answered by deleting both directives: an inline pair failed CommonMark's HTML-block test, landed inside the paragraph, and the NO/YES cancelled out before the block was linted. It did nothing, silently. The second never worked at all, because `raw` rules run after ResetComments. Both are now givens in `vale-vendor-contract.test.ts`, which had no directive coverage before. SO THE EXAMPLES COME BACK. Both worked examples that were reworded to dodge `no-hedging` are restored to their natural wording, each inside a two-line zone at the continuation indent. The zone is load-bearing rather than decorative: the repo's own rule over the recipe reports 2 findings with the directive lines stripped and 0 with them in place. What used to cost 176 lines of prose to save two words now costs two lines. THE LEDGER IS ONE CROSSING, NOT TWO. v0.11.0 vendors 3.18.0, so 3.19.0 never reached a consumer and its Elixir and MDX changes arrive with these. The changeset grows in place rather than gaining a sibling, `update.md` reads 3.18.0 → 3.20.0 with nine items instead of five, and MDX joins the ledger, which it never did. Still `patch`: pre-1.0, and no released surface changes. The format-tier table held — 95 probes, no row moved. That table cannot report a format Vale LEARNS, so that was checked at the source instead: the v3.19.0...v3.20.0 tree adds no format handler and no comment-extraction language, and every existing one changes by a single import line from the org rename. The docblock now says to repeat that check, since a green table is silence about the dangerous direction rather than evidence against it. Two findings from doing the work, both now documented: - At `raw` scope the directive line is itself linted text, so a rule whose token appears in its own id reports a finding on the marker that silences it. `no-simply` looking for `simply` flags its own `= NO` line. - `sequence` accepts `exceptions` as of 3.20.0, which the derived vocabulary picked up on its own. Upstream also changed how a negated sequence token is satisfied at a sentence boundary. No shape I tried separated the two binaries, so it is recorded the way the ast-grep root-metavariable case already is: nothing to do unless you see one. pnpm cli check No issues found. 1325 tests pass, typecheck and lint clean.
1 parent 0553dfb commit a38f6bb

13 files changed

Lines changed: 283 additions & 128 deletions

‎.changeset/vale-3-19-0.md‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

‎.changeset/vale-3-20-0.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
"@taskless/cli": patch
3+
---
4+
5+
Vale moves to 3.20.0. The last release vendored 3.18.0, so 3.19.0 is part of
6+
this crossing too and no consumer sees it on its own.
7+
8+
An exception zone that did nothing now takes effect. Vale records the region
9+
each `<!-- vale <id>.<id> = NO -->` and `= YES` pair covers and suppresses the
10+
alerts inside it, where an inline pair at a continuation indent used to be read
11+
once per block and cancel itself out silently. A marker already sitting in a
12+
project's documents was inert and is now live, so findings can disappear with
13+
nothing reporting the drop. `taskless agent update` says how to tell that from
14+
a rule that broke.
15+
16+
Directives also reach `scope: raw` rules now, which they never did before. A
17+
rule about a shell command, a flag, or a package name was exempt-or-remove with
18+
nothing in between, and can be exempted case by case again. At `raw` scope the
19+
directive line is itself linted text, so a rule whose token appears in its own
20+
id reports a finding on the marker that silences it.
21+
22+
`sequence` accepts `exceptions`, which the derived vocabulary picked up and the
23+
schema now allows.
24+
25+
Carried over from 3.19.0: Elixir (`.ex`, `.exs`) gained comment and `@doc`
26+
extraction, so it leaves the plaintext fallback for the comment tier. This
27+
narrows what a Vale rule matching Elixir reports — the code body is no longer
28+
prose, and findings that came from it disappear silently. MDX gained coverage in
29+
the other direction, since a JSX element's children are read as the Markdown
30+
they are, so prose inside a wrapping component such as `<Steps>` or `<Aside>` is
31+
linted where it previously was skipped, and carries the component name as a
32+
`text.class.<name>` scope a rule can target.
33+
34+
Every row of the format-tier table was re-probed against the 3.20.0 binary and
35+
none of them moved. The direction that table cannot report was checked at the
36+
source instead: the v3.19.0...v3.20.0 tree adds no format handler, so no format
37+
was learned.

‎packages/cli/package.json‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,11 +89,11 @@
8989
"@ast-grep/cli-win32-arm64-msvc": "0.45.2",
9090
"@ast-grep/cli-win32-ia32-msvc": "0.45.2",
9191
"@ast-grep/cli-win32-x64-msvc": "0.45.2",
92-
"@taskless/vale-darwin-arm64": "3.19.0-20260901000817",
93-
"@taskless/vale-darwin-x64": "3.19.0-20260901000817",
94-
"@taskless/vale-linux-arm64": "3.19.0-20260901000817",
95-
"@taskless/vale-linux-x64": "3.19.0-20260901000817",
96-
"@taskless/vale-win32-arm64": "3.19.0-20260901000817",
97-
"@taskless/vale-win32-x64": "3.19.0-20260901000817"
92+
"@taskless/vale-darwin-arm64": "3.20.0-20260907164938",
93+
"@taskless/vale-darwin-x64": "3.20.0-20260907164938",
94+
"@taskless/vale-linux-arm64": "3.20.0-20260907164938",
95+
"@taskless/vale-linux-x64": "3.20.0-20260907164938",
96+
"@taskless/vale-win32-arm64": "3.20.0-20260907164938",
97+
"@taskless/vale-win32-x64": "3.20.0-20260907164938"
9898
}
9999
}

‎packages/cli/src/agent/create-vale-rule.md‎

Lines changed: 52 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v5)
1+
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v6)
22

33
## You are here
44
This is `create-vale-rule`. It helps you write a Vale rule: a check over
@@ -220,44 +220,49 @@ it.
220220
the cause is in that rule's own `scope` and glob, never in a
221221
neighbour's.
222222

223-
**A `raw`-scoped rule cannot be turned off in a document.** Vale's
224-
`<!-- vale <id>.<id> = NO -->` directive is applied to the *parsed*
225-
document, and `raw` reads the unparsed one. Measured: a `text`-scoped
226-
rule is silenced by the directive; the same rule at `raw` fires
227-
through it. So a rule about a shell command, a flag, or a package name
228-
needs `raw`, commands live in fenced blocks, which nothing else
229-
reaches, and takes that trade: it can no longer be exempted case by
230-
case, only removed.
231-
232-
**An exception zone is the size of a top-level block, and no smaller.**
233-
A pair of directives turns a rule off for a region and back on:
234-
`= NO` opens it and `= YES` closes it. Both lines have to start at
235-
column 0, because Vale reads them as HTML blocks and an HTML block
236-
begins at the left margin.
237-
238-
That constraint decides where a zone can go, and it is easy to get
239-
wrong twice. Measured on Vale 3.19.0, in this file:
240-
241-
- At a list item's continuation indent the directive is not
242-
recognised at all. The words stay reported and nothing says the
243-
marker was ignored.
244-
- At column 0 in the middle of a list, the directive is recognised
245-
and it ends the list. The blocks after it reparse as indented code,
246-
which are already outside a prose rule's reach, so coverage moves
247-
rather than being restored where you meant to restore it.
248-
249-
So a zone wraps a whole numbered step, a whole section, or a whole
250-
paragraph at the margin. Price it before reaching for one: exempting
251-
the two steps of this recipe that quote hedging words would have put
252-
176 lines of prose out of reach to keep two words of an example. Those
253-
two examples were rewritten to use words the rule does not carry,
254-
which cost three words and no coverage at all.
255-
256-
**Write a documented example indented, never at the margin.** A
257-
directive at column 0 in a recipe takes effect on the recipe, and the
258-
CLI strips it before serving so a reader never sees it. That is why
259-
every directive quoted in this file sits inside a sentence or an
260-
indented block: it is being shown, not run.
223+
**A directive turns any rule off, `raw` included, as of Vale 3.20.0.**
224+
`<!-- vale <id>.<id> = NO -->` opens a zone and `= YES` closes it.
225+
Vale records the region each directive covers and suppresses any
226+
alert located inside it, which reaches a `raw`-scoped rule too.
227+
Through 3.19.0 a directive was applied to the *parsed* document and
228+
`raw` reads the unparsed one, so a `raw` rule fired straight through
229+
every zone: a rule about a shell command, a flag, or a package name
230+
was exempt-or-remove with nothing in between. It is not any more.
231+
232+
One thing to know before writing one over a `raw` rule: at `raw`
233+
scope the directive line is itself linted text. A rule whose token
234+
appears in its own id matches the marker that silences it, and
235+
reports a finding on the directive. Name the rule so its own id does
236+
not contain the word it looks for.
237+
238+
**A zone is two lines, and no blank line may separate it from the
239+
prose it wraps.** Measured on Vale 3.20.0:
240+
241+
- Inline at a list item's continuation indent works, and this is the
242+
form to reach for. The pair covers the lines between it and
243+
nothing else.
244+
- At column 0 it works too, and it ends any list it interrupts.
245+
Blocks that then reparse as indented code are already outside a
246+
prose rule's reach, so a zone placed there can move coverage
247+
rather than restore it.
248+
- A pair separated from its prose by blank lines, at an indent past
249+
the code-block threshold, suppresses nothing: what it wraps is not
250+
prose any more. Nothing reports that, either.
251+
252+
Through 3.19.0 the first of those did nothing at all. An inline pair
253+
was read once per block, so the `NO` and the `YES` cancelled out
254+
before the paragraph was linted and the words stayed reported with
255+
no error and no warning. A zone had to wrap a whole step or a whole
256+
section at the margin, and exempting the two steps of this recipe
257+
that quote hedging words would have put 176 lines of prose out of
258+
reach to keep two words of an example. That price is gone.
259+
260+
**A shown directive has to be inline or fenced, never a bare line.**
261+
A directive on a line of its own now takes effect on the recipe at
262+
any indent, and the CLI strips it before serving so a reader never
263+
sees it. Measured: a directive inside backticks and a directive
264+
inside a fenced block are both inert, which is why every directive
265+
quoted in this file is one or the other.
261266

262267
**Then the fields the extension point adds**. This is where the rule
263268
actually lives, and each check reads only its own:
@@ -274,7 +279,7 @@ it.
274279
| `metric` | `formula`, `condition` |
275280
| `readability` | `metrics` (a list of formula names), `grade` |
276281
| `spelling` | `aff`, `dic`, `custom`, `filters`, `ignore`, `threshold` |
277-
| `sequence` | `tokens` (each a `pattern`/`tag` map); `ignorecase` |
282+
| `sequence` | `tokens` (each a `pattern`/`tag` map); `ignorecase`, `exceptions` |
278283
| `script` | `script` (Tengo source) |
279284

280285
The list above is measured, not transcribed: every entry was added to
@@ -437,8 +442,10 @@ it.
437442
needs `nonword: true`.
438443

439444
- **A bare word finds senses you did not mean.** `landed on` in a rule
440-
about jargon also matches "the plane landed on time"; `clearly` in a
441-
rule about hedging also matches "clearly labelled" in a spec.
445+
<!-- vale no-hedging.no-hedging = NO -->
446+
about jargon also matches "the plane landed on time"; `simply` in a
447+
rule about hedging also matches "simply connected" in a maths doc.
448+
<!-- vale no-hedging.no-hedging = YES -->
442449
Narrow the token to the **collocation** you actually object to
443450
(`landed on a decision`, not `landed on`), and check that you got it
444451
right by writing the `pass/` fixture from the literal sense *first*:
@@ -535,8 +542,10 @@ it.
535542
assumes; `scope: heading` has nothing to find outside it:
536543
%(VALE_MARKUP_FORMATS)s
537544
- **comment text only**: the comments are linted and the code body
545+
<!-- vale no-hedging.no-hedging = NO -->
538546
is invisible, which is exactly right for "comments must not say
539-
'TBD'":
547+
'obviously'":
548+
<!-- vale no-hedging.no-hedging = YES -->
540549
%(VALE_COMMENT_FORMATS)s
541550
- **plaintext fallback**: everything else, `.yml` `.toml` `.sh`
542551
`.sql` and every extension not named above included. There is no

‎packages/cli/src/agent/update.md‎

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Topic: update (CLI v%(CLI_VERSION)s / topic v4)
1+
# Topic: update (CLI v%(CLI_VERSION)s / topic v5)
22

33
## You are here
44
This is `update`. It tells you what an upgrade changed for the rules
@@ -76,7 +76,7 @@ that you finished.
7676
### Migrating to 0.11.x
7777

7878
The vendored ast-grep moves from 0.41.0 to 0.45.2, and Vale from 3.18.0
79-
to 3.19.0. Five things follow for existing rules.
79+
to 3.20.0. Nine things follow for existing rules.
8080

8181
**Elixir stopped being linted as prose.** Vale 3.19.0 reads `.ex` and
8282
`.exs` through a real parser, so it now sees comments and `@doc`
@@ -91,6 +91,52 @@ meant for prose, this is the behaviour you always wanted. If it was
9191
being used to catch something in the code itself, that is a job for an
9292
`sg` rule, and `%(TASKLESS_CLI)s rule create` will route it there.
9393

94+
**MDX components' children are prose now, and new findings appear.**
95+
As of Vale 3.19.0 a JSX element's children are read as the Markdown
96+
they are, so a rule over `.mdx` covers prose inside a wrapping
97+
component such as `<Steps>` or `<Aside>` that it previously skipped.
98+
Those children also carry the component name as a `text.class.<name>`
99+
scope, which a rule can target.
100+
101+
This is the opposite direction from Elixir: coverage grew, so a rule
102+
reports MORE than it did. Read the new findings as real, and narrow the
103+
rule's `scope` only if the component's prose was deliberately out of
104+
reach.
105+
106+
**An exception zone that did nothing now takes effect, and findings
107+
disappear.** Vale 3.20.0 records the region each `<!-- vale <id>.<id> =
108+
NO -->` and `= YES` pair covers, and suppresses any alert located
109+
inside it. Through 3.19.0 an *inline* pair (at a list item's
110+
continuation indent, with no blank line between it and the prose it
111+
wraps) was read once per block, so the two halves cancelled out and the
112+
zone did nothing at all.
113+
114+
So a marker already sitting in this project's documents was inert and
115+
is now live. Nothing errors: findings a document has reported for as
116+
long as the rule existed stop appearing, which reads as a rule that
117+
broke. Before treating a drop as a regression, look for the
118+
markers: `git grep -n '<!-- vale'`. Keep the zone if it was meant; if
119+
it was a half-finished experiment, delete the pair and the coverage
120+
comes back.
121+
122+
**A `raw`-scoped rule can be exempted in a document now.** Directives
123+
reach `raw` rules as of 3.20.0. Before, they were applied to the parsed
124+
document only and a `raw` rule fired straight through every zone, so a
125+
rule about a shell command, a flag, or a package name could only be
126+
removed, never exempted case by case. A rule that was narrowed or
127+
abandoned for that reason is worth revisiting.
128+
129+
One caveat comes with it: at `raw` scope the directive line is itself
130+
linted text, so a rule whose token appears in its own id reports a
131+
finding on the marker that silences it. Rename the rule, or scope it.
132+
133+
**`sequence` accepts `exceptions`.** The field is new in 3.20.0 and
134+
purely additive: no existing rule changes behaviour, and a sequence
135+
rule that was over-firing on a known phrase can now carry it. Upstream
136+
also changed how a negated sequence token is satisfied at a sentence
137+
boundary. No shape we tried reproduced a difference between 3.19.0 and
138+
3.20.0, so there is nothing to do unless you see one.
139+
94140
**A rewriter now requires `fix`.** `SerializableRewriter.required` goes
95141
from `["rule","id"]` to `["id","fix","rule"]`, so a `rewriters:` entry
96142
without a `fix:` is now rejected where it was accepted before.

‎packages/cli/src/generated/vale-vocabulary-report.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# Vale 3.19.0 vocabulary: divergence report
1+
# Vale 3.20.0 vocabulary: divergence report
22

33
GENERATED FILE — DO NOT EDIT. Produced by `pnpm generate:vale-schema`
44
alongside `vale-vocabulary.ts`.
55

66
Every value in the vocabulary is the recorded answer of the vendored Vale
7-
3.19.0 binary. This file is what the binary said that its own
7+
3.20.0 binary. This file is what the binary said that its own
88
documentation does not, in both directions. A generator that dropped these
99
would be quietly deciding which of the two to believe.
1010

@@ -28,25 +28,25 @@ the worse failure.
2828

2929
### `scope: meta`
3030

31-
Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).
31+
Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).
3232

3333
**Consequence.** It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.
3434

3535
### `scope: meta.class.title`
3636

37-
Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).
37+
Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).
3838

3939
**Consequence.** It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.
4040

4141
### `scope: frontmatter`
4242

43-
This operand fired and Vale 3.19.0 documents it nowhere.
43+
This operand fired and Vale 3.20.0 documents it nowhere.
4444

4545
**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.
4646

4747
### `scope: frontmatter.title`
4848

49-
This operand fired and Vale 3.19.0 documents it nowhere.
49+
This operand fired and Vale 3.20.0 documents it nowhere.
5050

5151
**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.
5252

‎packages/cli/src/generated/vale-vocabulary.ts‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* `scripts/generate-vale-schema.ts`, and its header explains what each value
77
* below was measured with and what it is worth.
88
*
9-
* Derived against Vale 3.19.0. Every value here is the recorded answer
9+
* Derived against Vale 3.20.0. Every value here is the recorded answer
1010
* of that binary to a rule the generator wrote and ran; nothing is transcribed
1111
* from documentation. Where the binary and the documentation disagree, the
1212
* disagreement is in `vale-vocabulary-report.md` rather than dropped.
@@ -15,7 +15,7 @@
1515
*/
1616

1717
/** The binary this vocabulary was derived from. */
18-
export const VALE_VOCABULARY_VERSION = "3.19.0";
18+
export const VALE_VOCABULARY_VERSION = "3.20.0";
1919

2020
/**
2121
* Vale's check types, self-enumerated: an unknown `extends` makes the binary
@@ -133,7 +133,7 @@ export const VALE_CHECK_FIELDS = {
133133
readability: ["grade", "metrics"],
134134
repetition: ["alpha", "exceptions", "ignorecase", "max", "tokens", "vocab"],
135135
script: ["script"],
136-
sequence: ["ignorecase", "tokens"],
136+
sequence: ["exceptions", "ignorecase", "tokens"],
137137
substitution: [
138138
"capitalize",
139139
"exceptions",
@@ -193,7 +193,7 @@ export const VALE_SCOPE_OPERANDS = [
193193
export const VALE_SCOPE_PREFIXES = ["frontmatter.", "text.class."] as const;
194194

195195
/**
196-
* Where Vale 3.19.0 and its documentation disagree.
196+
* Where Vale 3.20.0 and its documentation disagree.
197197
*
198198
* Carried in the artifact rather than only in the report, so that a consumer
199199
* can render them and a reviewer cannot miss them in a diff.
@@ -202,26 +202,26 @@ export const VALE_DIVERGENCES = [
202202
{
203203
subject: "scope: meta",
204204
finding:
205-
"Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).",
205+
"Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).",
206206
consequence:
207207
"It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.",
208208
},
209209
{
210210
subject: "scope: meta.class.title",
211211
finding:
212-
"Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).",
212+
"Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).",
213213
consequence:
214214
"It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.",
215215
},
216216
{
217217
subject: "scope: frontmatter",
218-
finding: "This operand fired and Vale 3.19.0 documents it nowhere.",
218+
finding: "This operand fired and Vale 3.20.0 documents it nowhere.",
219219
consequence:
220220
"It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.",
221221
},
222222
{
223223
subject: "scope: frontmatter.title",
224-
finding: "This operand fired and Vale 3.19.0 documents it nowhere.",
224+
finding: "This operand fired and Vale 3.20.0 documents it nowhere.",
225225
consequence:
226226
"It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.",
227227
},

0 commit comments

Comments
 (0)