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
35 changes: 25 additions & 10 deletions .changeset/vale-converter-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,26 @@ the property only that tier has, since ordinary prose fires in all of them — s
a version bump that moves a format between tiers fails there instead of silently
turning the engine off again.

The recipes now say MDX is not supported _yet_, rather than unsupported: Vale
3.18.0 parses it natively and a CLI update carrying that Vale is expected to
bring it. The same release adds a Typst converter, which will move `.typ` out of
the plaintext tier, so the table carries a standing instruction to re-measure
every row on a version bump.
The engine moves to Vale 3.18.0 in the same release, and the table carries a
standing instruction to re-measure every row on a version bump — so every row
was re-probed against the new binary rather than carried over. Eight moved.
`.mdx` gains a native parser and leaves the unsupported tier, so a matcher like
`[*.{md,mdx}]` — the worked example above — is legitimate again, and `[*.{md,typ}]`
takes its place as the broken one. `.typ` moved the opposite way: Typst now
parses through `typst2vast`, an external program this build does not ship, so a
Typst file is excluded from the run rather than read as prose the way 3.17.1
read it. `.rmd` and the new `.qmd` and `.myst` are parsed as markup, and `.qml`,
`.scss` and the new `.qdoc` are comment-aware where they previously fell through
to plain text.

The `.typ` move is the one that mattered to get right. An extension missing from
the table is read as prose, which is harmless — but the moment Vale routes it to
a converter, that same omission is a crash that takes down every Vale rule in
the run. Bumping the binary without re-measuring would have introduced exactly
the failure this table exists to prevent, under an extension nobody was
watching. Re-probing also caught one change the release notes do not mention:
PHP comment extraction now requires a real `<?php` tag, where 3.17.1 linted a
bare `//` comment without one.

Two details are load-bearing and were both wrong on the first attempt. Vale
honours exactly one `--glob` and keeps the last, so the `.taskless/` exclusion
Expand All @@ -62,10 +77,10 @@ a remedy.
The comment tier was reconciled against Vale's own documentation at
docs.vale.sh/formats/code, which adds `.bsh`, `.csx`, `.pod`, `.py3` and `.sbt`
once measured. It also documents `.pyi`, `.qml` and `.scss` as comment-aware,
and on the pinned 3.17.1 a bare non-comment line in each of them lints — so they
stay in the plaintext tier. That divergence is the argument for probing rather
than transcribing: the docs describe the current Vale, this build pins an older
one, and copying the list would have shipped `.scss` as comment-aware and been
wrong. `.pod` is a reminder of how easily this is misread — it lints Perl
and on 3.17.1 a bare non-comment line in each of them lints. 3.18.0 makes the
claim true for `.qml` and `.scss` and still not for `.pyi`, which stays in the
plaintext tier. That divergence is the argument for probing rather than
transcribing: the docs describe whatever Vale is current, and copying the list
would have shipped `.pyi` as comment-aware and been wrong for both builds. `.pod` is a reminder of how easily this is misread — it lints Perl
comments but not POD blocks, so probing it with `=head1` looks like no support
at all.
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@
"@ast-grep/cli-win32-arm64-msvc": "0.41.0",
"@ast-grep/cli-win32-ia32-msvc": "0.41.0",
"@ast-grep/cli-win32-x64-msvc": "0.41.0",
"@taskless/vale-darwin-arm64": "3.17.1-20260810052605",
"@taskless/vale-darwin-x64": "3.17.1-20260810052605",
"@taskless/vale-linux-arm64": "3.17.1-20260810052605",
"@taskless/vale-linux-x64": "3.17.1-20260810052605",
"@taskless/vale-win32-arm64": "3.17.1-20260810052605",
"@taskless/vale-win32-x64": "3.17.1-20260810052605"
"@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"
}
}
25 changes: 15 additions & 10 deletions packages/cli/src/agent/create-vale-rule.txt
Original file line number Diff line number Diff line change
Expand Up @@ -329,16 +329,21 @@ it.
**A single unreadable file fails the whole Vale pass.** Vale exits 2
with an `E100` runtime error and abandons the run — `--no-exit` does
not suppress it — so every other Vale rule over every other file goes
unreported. `[*.{md,mdx}]` is not a slightly wider `[*.md]`; it is a
matcher that takes `check` down the first time the repo grows an
`.mdx` file. Never put one of those extensions in a glob.

**MDX is not supported yet.** Vale v%(VALE_VERSION)s reads `.mdx` only
through `mdx2vast`, which this CLI does not ship. Vale 3.18.0 parses
MDX natively, and a CLI update carrying that Vale is expected to
bring MDX support with it. Until then, scope the rule to `.md` and
say that `.mdx` is not supported yet rather than offering a matcher
that covers it.
unreported. `[*.{md,typ}]` is not a slightly wider `[*.md]`; it is a
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
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
matcher covering Typst is now the one that takes the run down.

5. **Write the fixtures.** Two directories inside the rule, both flat.
Vale lints the whole fixture tree, so a document nested a level
Expand Down
12 changes: 7 additions & 5 deletions packages/cli/src/agent/route.txt
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,15 @@ answered together.
**One such file fails the entire Vale pass, not just that file.**
Vale exits 2 with an `E100` runtime error, `--no-exit` does not
suppress it, and every other Vale rule over every other file goes
unreported. A matcher written as `[*.{md,mdx}]` is not a wider
unreported. A matcher written as `[*.{md,typ}]` is not a wider
`[*.md]` — it is a broken one.

**MDX is not supported yet.** Vale 3.18.0 parses MDX natively and
a CLI update carrying that Vale is expected to bring it; on
v%(VALE_VERSION)s, `.mdx` needs `mdx2vast` and this CLI ships
none. Tell the user MDX is not supported yet and scope to `.md`.
**`.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
before. Both are measured, not assumed.

**A language on neither list does not route to runtime by default.**
Check `create-legacy-rule` first: the repo may already run a linter
Expand Down
81 changes: 49 additions & 32 deletions packages/cli/src/rules/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const AST_GREP_LANGUAGES = [
* Pinned against the binary by `test/vale-vendor-contract.test.ts`
* ("engine capabilities" → "reports the pinned version").
*/
export const VALE_VERSION = "3.17.1";
export const VALE_VERSION = "3.18.0";
Comment thread
thecodedrift marked this conversation as resolved.

/**
* Which tier Vale routes an extension to.
Expand Down Expand Up @@ -139,9 +139,9 @@ const CONVERTER_TIER_PREFIX = "converter:";
* on a bare non-comment line yields ZERO. A type that fires on both is the
* plaintext fallback wearing a code extension.
* - **plaintext** — a bare line yields a finding. Listed only where the tier is
* surprising: `.tex`, `.rmd`, `.mkd` and `.mkdn` all look like markup and are
* not. Everything unnamed lands here too, which is why this tier does not
* need to be exhaustive.
* surprising: `.tex`, `.mkd` and `.mkdn` all look like markup and are not.
* Everything unnamed lands here too, which is why this tier does not need to
* be exhaustive.
* - **converter** — a non-zero exit whose output carries `E100` and the
* program's name.
*
Expand All @@ -158,31 +158,46 @@ 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.
*
* Vale 3.18.0 is the known incoming bump, and it moves rows in three different
* directions — which is why "re-measure" is not boilerplate here:
* 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:
*
* - `.mdx` gains a native parser, so it moves `converter:mdx2vast` → `markup`
* and becomes supported.
* - `.typ` gains a parser that shells out to `typst2vast`
* (https://docs.vale.sh/formats/typst), so it moves `plaintext` →
* `converter:typst2vast`. That is the dangerous direction: today it is read
* as prose, and after the bump the same row would crash the run. It also
* stays unsupported permanently, since we do not support formats needing an
* external program.
* - MyST, Quarto and QDoc arrive with parsers needing no external program, so
* they become genuinely supportable and want `markup` rows once measured.
* - `.mdx` gained a native parser: `converter:mdx2vast` → `markup`. It is
* supported now, and `[*.{md,mdx}]` is a legitimate matcher again.
* - `.typ` gained a parser that shells out to `typst2vast`
* (https://docs.vale.sh/formats/typst): `plaintext` →
* `converter:typst2vast`. That is the dangerous direction — 3.17.1 read it as
* prose, and the same row on 3.18.0 crashes the run. It stays unsupported
* permanently, since we do not support formats needing an external program.
* - `.rmd` gained a real Markdown parser, so it left the "looks like markup and
* is not" list above: `plaintext` → `markup`.
* - `.qml` and `.scss` gained parsers that see their comments: `plaintext` →
* `comment`. Vale's docs had claimed both for years; on 3.17.1 the claim was
* measurably false and on 3.18.0 it is true.
* - `.qmd` (Quarto) and `.myst` (MyST) measured as `markup`, and `.qdoc` as
* `comment` — QDoc documentation lives in a doc-comment block, so it is
* comment extraction rather than the markup tier a first reading of the
* release notes suggests, and probing it with bare prose reads as no support
* at all. All three are new rows, none of them reachable on 3.17.1.
*
* All four are documented as requiring v3.18.0 or later, so none of them is
* reachable from {@link VALE_VERSION}. Re-probe every row on the bump.
* PHP also changed without the release notes saying so: comment extraction now
* needs a real `<?php` tag, where 3.17.1 linted a bare `//` comment without
* one. The tier did not move, but the probe had to. Re-probe every row on the
* next bump, by the discriminating property and by each language's own comment
* syntax — a wrong delimiter reads exactly like absent support.
*/
export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
// markup — parsed, the format's own constructs skipped
".htm": "markup",
".html": "markup",
".markdown": "markup",
".md": "markup",
".mdx": "markup",
".mdown": "markup",
".myst": "markup",
".org": "markup",
".qmd": "markup",
".rmd": "markup",
".xhtml": "markup",
// comment text only — the code body is invisible
".c": "comment",
Expand Down Expand Up @@ -212,6 +227,8 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".pod": "comment",
".proto": "comment",
".ps1": "comment",
".qdoc": "comment",
".qml": "comment",
".py": "comment",
".py3": "comment",
".pyw": "comment",
Expand All @@ -220,6 +237,7 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
".rb": "comment",
".rs": "comment",
".sass": "comment",
".scss": "comment",
".sbt": "comment",
".scala": "comment",
".swift": "comment",
Expand All @@ -228,23 +246,20 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
// plaintext, and surprising about it — these look parsed and are not
".mkd": "plaintext",
".mkdn": "plaintext",
".rmd": "plaintext",
".tex": "plaintext",
".typ": "plaintext",
// plaintext HERE, though Vale's own docs list them as comment-tier. The docs
// describe the CURRENT Vale; we pin 3.17.1. Measured on the pinned binary a
// bare non-comment line lints, which is the plaintext signature. Transcribing
// the docs would have shipped these as comment-tier and been wrong for this
// build — the case for probing rather than copying.
// 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
// 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
// copying.
".pyi": "plaintext",
".qml": "plaintext",
".scss": "plaintext",
// converter-dependent — Vale supports the format, we ship no converter
".adoc": "converter:asciidoctor",
".asc": "converter:asciidoctor",
".asciidoc": "converter:asciidoctor",
".dita": "converter:dita",
".mdx": "converter:mdx2vast",
".typ": "converter:typst2vast",
".rest": "converter:rst2html",
".rst": "converter:rst2html",
".xml": "converter:xsltproc and an XSLT stylesheet",
Expand Down Expand Up @@ -337,10 +352,12 @@ function groupByConverter(): ValeConverterFormat[] {
*
* The blast radius is what makes this worth surfacing at routing time rather
* than at authoring time: Vale exits 2 with an `E100` runtime error and
* abandons the run, and `--no-exit` does not suppress it. One `.mdx` file
* abandons the run, and `--no-exit` does not suppress it. One `.typ` file
* caught by a rule's glob takes down the entire Vale pass, including every
* other rule and every other file — so `[*.{md,mdx}]` is not a slightly wider
* matcher than `[*.md]`, it is a broken one.
* other rule and every other file — so `[*.{md,typ}]` is not a slightly wider
* matcher than `[*.md]`, it is a broken one. (`[*.{md,mdx}]` was that example
* until 3.18.0 gave MDX a native parser — the membership of this tier is a
* property of {@link VALE_VERSION}, and so is the worked example.)
*/
export const VALE_CONVERTER_DEPENDENT: readonly ValeConverterFormat[] =
groupByConverter();
Expand Down Expand Up @@ -369,7 +386,7 @@ export const VALE_CONVERTER_CHECKERS: Readonly<Record<string, string>> = {
".asc": "lintAdoc",
".xml": "lintXML",
".dita": "lintDITA",
".mdx": "lintMDX",
".typ": "lintTypst",
Comment thread
thecodedrift marked this conversation as resolved.
};

/** Every converter-dependent extension, flattened. */
Expand Down
21 changes: 16 additions & 5 deletions packages/cli/test/recipe-cross-references.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -380,10 +380,16 @@ describe("recipes state engine reach from the pinned versions", () => {
// The consequence, not just the list. A recipe that names `.mdx` without
// saying it takes the whole pass down has not conveyed the hazard.
expect(route).toContain("E100");
// MDX is a "not yet", not a "never" — Vale 3.18.0 parses it natively. An
// agent told only that it is unreadable would tell a user MDX is
// unsupported, full stop.
expect(route).toContain("MDX is not supported yet");
// The pair that swapped in 3.18.0, asserted by name because getting either
// backwards is a user-visible error in opposite directions: telling an
// author `.mdx` is unsupported costs them a format Vale reads fine, and
// telling them `.typ` is readable takes their whole Vale pass down.
expect(route).toContain("`.mdx` is supported");
// `.typ` alone would pass on the converter list rendered above, which names
// it whatever the prose says. Pin the hazard sentence instead, so dropping
// the warning fails here even while the extension is still mentioned.
expect(route).toContain("`.typ` moved the other way");
expect(route).toContain("`[*.{md,typ}]`");
});

it("names ast-grep's languages in rendered create-sg-rule.txt", async () => {
Expand Down Expand Up @@ -414,7 +420,12 @@ describe("recipes state engine reach from the pinned versions", () => {
// this pins the warning rather than the absence of the string.
expect(recipe).toContain("Never put one of those extensions in a glob.");
expect(recipe).toContain(valePlaintextList());
expect(recipe).toContain("MDX is not supported yet");
expect(recipe).toContain("`.mdx` is supported");
// The cautionary glob must name a currently-dangerous extension. It said
// `[*.{md,mdx}]` until 3.18.0 made that matcher legitimate, which is
// exactly the kind of staleness a version bump introduces silently.
expect(recipe).toContain("[*.{md,typ}]");
expect(recipe).not.toContain("[*.{md,mdx}] is not");
// No date. The bump is expected, not scheduled, and a recipe that implies
// otherwise is stale the moment it slips.
expect(recipe).not.toMatch(/\b20\d\d-\d\d\b/);
Expand Down
10 changes: 9 additions & 1 deletion packages/cli/test/vale-formats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,17 @@ describe("the format tier table", () => {
// tier. Being wrong about the tier is survivable; being wrong about needing
// a converter is not, because it excludes a file Vale would have linted
// perfectly well. This is that half of the claim.
for (const extension of [".tex", ".rmd", ".mkd", ".mkdn", ".typ"]) {
for (const extension of [".tex", ".mkd", ".mkdn", ".pyi"]) {
expect(converterFor(`doc${extension}`)).toBeUndefined();
}
// The other half, and the reason this list is re-derived rather than
// remembered: `.typ` was plaintext until Vale 3.18.0 gave Typst a parser
// that shells out, so the same extension that must NOT be excluded on one
// version must be excluded on the next.
expect(converterFor("doc.typ")).toBe("typst2vast");
// `.mdx` went the other way in the same release — native now, so excluding
// it would drop a file Vale reads perfectly well.
expect(converterFor("doc.mdx")).toBeUndefined();
});
});

Expand Down
Loading
Loading