Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/vale-3-19-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@taskless/cli": patch
---

Vale moves to 3.19.0.

Elixir (`.ex`, `.exs`) gained comment and `@doc` extraction, so it leaves
the plaintext fallback for the comment tier. This narrows what a Vale rule
matching Elixir reports: the code body is no longer prose, and findings that
came from it disappear silently. `taskless agent update` explains the drop and
where to take a rule that was relying on it.

MDX gained coverage in the other direction — a JSX element's children are read
as the Markdown they are, so prose inside a wrapping component such as
`<Steps>` or `<Aside>` is linted where it previously was skipped, and carries
the component name as a `text.class.<name>` scope a rule can target.

Every other row of the format-tier table was re-probed against the 3.19.0
binary and none of them moved.
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,11 @@
"@ast-grep/cli-win32-arm64-msvc": "0.45.2",
"@ast-grep/cli-win32-ia32-msvc": "0.45.2",
"@ast-grep/cli-win32-x64-msvc": "0.45.2",
"@taskless/vale-darwin-arm64": "3.18.0-20260824195610",
"@taskless/vale-darwin-x64": "3.18.0-20260824195610",
"@taskless/vale-linux-arm64": "3.18.0-20260824195610",
"@taskless/vale-linux-x64": "3.18.0-20260824195610",
"@taskless/vale-win32-arm64": "3.18.0-20260824195610",
"@taskless/vale-win32-x64": "3.18.0-20260824195610"
"@taskless/vale-darwin-arm64": "3.19.0-20260901000817",
"@taskless/vale-darwin-x64": "3.19.0-20260901000817",
"@taskless/vale-linux-arm64": "3.19.0-20260901000817",
"@taskless/vale-linux-x64": "3.19.0-20260901000817",
"@taskless/vale-win32-arm64": "3.19.0-20260901000817",
"@taskless/vale-win32-x64": "3.19.0-20260901000817"
}
}
32 changes: 24 additions & 8 deletions packages/cli/src/agent/create-vale-rule.txt
Original file line number Diff line number Diff line change
Expand Up @@ -532,18 +532,34 @@ it.
matcher that takes `check` down the first time the repo grows a
`.typ` file. Never put one of those extensions in a glob.

That example changed with Vale v%(VALE_VERSION)s, which is the point:
the dangerous extension is whichever one the list above says needs a
program, not the one you remember. `.mdx` was the example until this
That example changed with Vale v3.18.0, which is the point: the
dangerous extension is whichever one the list above says needs a
program, not the one you remember. `.mdx` was the example until that
release parsed it natively, and `.typ` took its place.

**`.mdx` is supported** as of Vale v%(VALE_VERSION)s, which parses it
natively rather than shelling out. `[*.{md,mdx}]` is a legitimate
matcher again, the example this recipe used to warn about is no
longer the broken one. Check the lists above rather than reaching for
that memory: `.typ` moved the opposite way in the same release, so a
**`.mdx` is supported** as of Vale v3.18.0, which parses it natively
rather than shelling out. `[*.{md,mdx}]` is a legitimate matcher
again, the example this recipe used to warn about is no longer the
broken one. Check the lists above rather than reaching for that
memory: `.typ` moved the opposite way in the same release, so a
matcher covering Typst is now the one that takes the run down.

**In `.mdx`, a component's children are prose as of v3.19.0.** Vale
reads a JSX element's children as the Markdown they are, so text
inside a wrapping component (`<Steps>`, `<Tabs>`, `<Aside>`) is
linted at its own source position. Only tags, attributes, `{...}`
expressions, self-closing elements, and an element opened and closed
on one standalone line are still treated as code.

Two consequences for a rule you write against MDX. Coverage grew,
so a rule can now fire in prose it never reached before, which is a
finding count that moves without the rule changing. And the children
carry the element's name as a class scope, exactly as MyST and
Quarto directives do, so `scope: text.class.Aside` targets one
component's content. That is an open family: `verify` accepts any
`text.class.<name>` tail, because the set of component names is the
author's, not Vale's.

5. **Write the fixtures.** Two directories inside the rule, both flat.
Vale lints the whole fixture tree, so a document nested a level
deeper would be linted and never checked against either bucket, which
Expand Down
15 changes: 10 additions & 5 deletions packages/cli/src/agent/route.txt
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,18 @@ answered together.
unreported. A matcher written as `[*.{md,typ}]` is not a wider
`[*.md]`. It is a broken one.

**`.mdx` is supported** as of Vale v%(VALE_VERSION)s, which
parses it natively. It needs no external program and belongs
with the other markup formats above. `.typ` moved the other way
in the same release: Typst now parses through `typst2vast`, so a
Typst file is excluded rather than read as prose the way it was
**`.mdx` is supported** as of Vale v3.18.0, which parses it
natively. It needs no external program and belongs with the
other markup formats above. `.typ` moved the other way in the
same release: Typst now parses through `typst2vast`, so a Typst
file is excluded rather than read as prose the way it was
before. Both are measured, not assumed.

As of v3.19.0 an MDX component's children are read as Markdown
too, so prose wrapped in `<Steps>` or `<Aside>` is linted where
it previously was not. That widens what a Vale rule covers in
`.mdx`; it does not change which extensions are safe to match.

**A language on neither list does not route to runtime by default.**
Check `create-legacy-rule` first: the repo may already run a linter
that speaks it, and that linter's own dialect is a local destination
Expand Down
17 changes: 15 additions & 2 deletions packages/cli/src/agent/update.txt
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,21 @@ that you finished.

### Migrating to 0.11.x

The vendored ast-grep moves from 0.41.0 to 0.45.2, and Vale is at
3.18.0. Four things follow for existing rules.
The vendored ast-grep moves from 0.41.0 to 0.45.2, and Vale from 3.18.0
to 3.19.0. Five things follow for existing rules.

**Elixir stopped being linted as prose.** Vale 3.19.0 reads `.ex` and
`.exs` through a real parser, so it now sees comments and `@doc`
attributes and nothing else. On 3.18.0 those files had no parser and
were linted whole, so a Vale rule matching `[*.ex]` fired on function
names, atoms, and string literals as well as on comments.

Those findings are gone, and nothing errors or warns. A rule that
reported on Elixir code bodies now reports less, which is a drop you
would otherwise notice only as a suspiciously clean run. If the rule was
meant for prose, this is the behaviour you always wanted. If it was
being used to catch something in the code itself, that is a job for an
`sg` rule, and `%(TASKLESS_CLI)s rule create` will route it there.

**A rewriter now requires `fix`.** `SerializableRewriter.required` goes
from `["rule","id"]` to `["id","fix","rule"]`, so a `rewriters:` entry
Expand Down
12 changes: 6 additions & 6 deletions packages/cli/src/generated/vale-vocabulary-report.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Vale 3.18.0 vocabulary: divergence report
# Vale 3.19.0 vocabulary: divergence report

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

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

Expand All @@ -28,25 +28,25 @@ the worse failure.

### `scope: meta`

Vale 3.18.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).

**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.

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

Vale 3.18.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).

**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.

### `scope: frontmatter`

This operand fired and Vale 3.18.0 documents it nowhere.
This operand fired and Vale 3.19.0 documents it nowhere.

**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.

### `scope: frontmatter.title`

This operand fired and Vale 3.18.0 documents it nowhere.
This operand fired and Vale 3.19.0 documents it nowhere.

**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.

Expand Down
14 changes: 7 additions & 7 deletions packages/cli/src/generated/vale-vocabulary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* `scripts/generate-vale-schema.ts`, and its header explains what each value
* below was measured with and what it is worth.
*
* Derived against Vale 3.18.0. Every value here is the recorded answer
* Derived against Vale 3.19.0. Every value here is the recorded answer
* of that binary to a rule the generator wrote and ran; nothing is transcribed
* from documentation. Where the binary and the documentation disagree, the
* disagreement is in `vale-vocabulary-report.md` rather than dropped.
Expand All @@ -15,7 +15,7 @@
*/

/** The binary this vocabulary was derived from. */
export const VALE_VOCABULARY_VERSION = "3.18.0";
export const VALE_VOCABULARY_VERSION = "3.19.0";

/**
* Vale's check types, self-enumerated: an unknown `extends` makes the binary
Expand Down Expand Up @@ -193,7 +193,7 @@ export const VALE_SCOPE_OPERANDS = [
export const VALE_SCOPE_PREFIXES = ["frontmatter.", "text.class."] as const;

/**
* Where Vale 3.18.0 and its documentation disagree.
* Where Vale 3.19.0 and its documentation disagree.
*
* Carried in the artifact rather than only in the report, so that a consumer
* can render them and a reviewer cannot miss them in a diff.
Expand All @@ -202,26 +202,26 @@ export const VALE_DIVERGENCES = [
{
subject: "scope: meta",
finding:
"Vale 3.18.0 documents this operand and it never fired, on any fixture probed (.md).",
"Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).",
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.",
},
{
subject: "scope: meta.class.title",
finding:
"Vale 3.18.0 documents this operand and it never fired, on any fixture probed (.md).",
"Vale 3.19.0 documents this operand and it never fired, on any fixture probed (.md).",
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.",
},
{
subject: "scope: frontmatter",
finding: "This operand fired and Vale 3.18.0 documents it nowhere.",
finding: "This operand fired and Vale 3.19.0 documents it nowhere.",
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.",
},
{
subject: "scope: frontmatter.title",
finding: "This operand fired and Vale 3.18.0 documents it nowhere.",
finding: "This operand fired and Vale 3.19.0 documents it nowhere.",
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.",
},
Expand Down
46 changes: 39 additions & 7 deletions packages/cli/src/rules/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,13 +195,21 @@ export const AST_GREP_TSX_SPLIT: Readonly<

/**
* The Vale release carried by the `@taskless/vale-<platform>` packages pinned
* in `packages/cli/package.json`. Their npm versions append a build stamp
* (`3.17.1-20260810052605`); this is the version Vale itself reports.
* in `packages/cli/package.json`. Their npm versions append a build stamp,
* `<valeVersion>-<yyyymmddhhmmss>`, minted per publish by
* `.github/workflows/release-vale.yml`; this is the version Vale itself
* reports.
*
* The SHAPE rather than an example, deliberately. A literal stamp here has to
* be hand-carried on every bump, and it has gone one release out of date twice
* now, which is worse than no example: it reads as the current pin and is not
* one. The real value lives in `packages/cli/package.json`, where a reader can
* see all six at once and cannot be told a stale one.
*
* Pinned against the binary by `test/vale-vendor-contract.test.ts`
* ("engine capabilities" → "reports the pinned version").
*/
export const VALE_VERSION = "3.18.0";
export const VALE_VERSION = "3.19.0";

/**
* Which tier Vale routes an extension to.
Expand Down Expand Up @@ -263,9 +271,31 @@ const CONVERTER_TIER_PREFIX = "converter:";
* plain text today, but the moment Vale routes it to a converter the same
* omission is a crash that takes down every Vale rule in the run.
*
* The 3.17.1 → 3.18.0 bump is what that warning looks like in practice. Every
* row below was re-probed against the 3.18.0 binary, and eight moved, in three
* different directions — which is why "re-measure" is not boilerplate here:
* THE TABLE HELD ACROSS 3.18.0 → 3.19.0, AND THAT IS NOT THE SAME AS THE BUMP
* BEING FREE. Every row below was re-probed against the 3.19.0 binary and not
* one of them moved. What moved was a format Vale *learned*, which is precisely
* the case a table of existing rows cannot report: `.ex` and `.exs` gained
* comment and doc-attribute extraction, so they left the unnamed plaintext
* fallback for the `comment` tier and are new rows below.
*
* Read that direction carefully, because it is a narrowing rather than a gain.
* On 3.18.0 an Elixir file was linted as one block of prose, so a rule matching
* `[*.ex]` fired on identifiers and string literals as readily as on comments.
* On 3.19.0 the code body is invisible and only comments and `@doc` attributes
* are read. Findings disappear, no error is raised, and nothing but a re-probe
* would have told us. A format Vale learns is never a no-op: the benign version
* of it is this one, and the dangerous version is a converter (see `.typ`).
*
* `.mdx` stayed `markup` and changed underneath the tier: a JSX element's
* children are now read as the Markdown they are, so a Vale rule covers prose
* inside `<Steps>` or `<Aside>` that it previously skipped, and those children
* carry the element name as a `text.class.<name>` scope. The tier is the wrong
* instrument for that kind of change — it says a parser exists, not what the
* parser sees — which is the second reason a bump needs more than this table.
*
* The 3.17.1 → 3.18.0 bump is what the dangerous case looks like in practice.
* Every row was re-probed against the 3.18.0 binary then too, and eight moved,
* in three different directions — which is why "re-measure" is not boilerplate:
*
* - `.mdx` gained a native parser: `converter:mdx2vast` → `markup`. It is
* supported now, and `[*.{md,mdx}]` is a legitimate matcher again.
Expand Down Expand Up @@ -315,6 +345,8 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".css": "comment",
".csx": "comment",
".cxx": "comment",
".ex": "comment",
".exs": "comment",
".go": "comment",
".h": "comment",
".h++": "comment",
Expand Down Expand Up @@ -353,7 +385,7 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".mkdn": "plaintext",
".tex": "plaintext",
// plaintext HERE, though Vale's own docs list it as comment-tier. Measured on
// the pinned 3.18.0 binary a bare non-comment line lints, which is the
// the pinned 3.19.0 binary a bare non-comment line lints, which is the
// plaintext signature. `.qml` and `.scss` sat here for the same reason until
// 3.18.0 made the docs true for them; `.pyi` is the row where transcribing
// the docs would still ship the wrong tier — the case for probing rather than
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/schemas/vale-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ const CHECK_FIELDS = VALE_CHECK_FIELDS;
*
* These are not field-table facts, which is why they sit apart from the union:
* every key involved is a legal field of its check. It is the *shape* that is
* fatal. Measured against Vale 3.18.0, each of the shapes below ends the
* fatal. Measured against Vale 3.19.0, each of the shapes below ends the
* process with a Go stack trace. The two `sequence` shapes fail while the rule
* is compiled:
*
Expand Down Expand Up @@ -511,7 +511,7 @@ function fatalShapeMessages(

if (rule.extends === "metric" && rule.formula !== undefined) {
// Structural, like the `sequence` arm above, because "was the key
// supplied" is the wrong question. Measured against 3.18.0, the panic
// supplied" is the wrong question. Measured against 3.19.0, the panic
// follows the *value*: an absent `condition`, a `condition:` written with
// no value (YAML parses that to null, which is not `undefined`), and a
// blank string all reach the same panic. Other types are not this guard's
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/reconcile-marker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ describe("recording a rules reconciliation", () => {
expect(rules?.reconciledTo).toBe(version);
// Engine versions are the input a later differential needs. Recorded here
// and nowhere else, so an upgrade cannot silently refresh them.
expect(rules?.engines).toEqual({ sg: "0.45.2", vale: "3.18.0" });
expect(rules?.engines).toEqual({ sg: "0.45.2", vale: "3.19.0" });
});

it("reports the marker through info", async () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/test/vale-vendor-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,8 @@ withVale("Vale vendor contract", () => {
*/
function comment(extension: string): string {
const HASH = new Set([
".ex",
".exs",
".jl",
".pl",
".pm",
Expand Down
Loading
Loading