Skip to content

Commit 5dae515

Browse files
theCodeDriftclaude
andcommitted
fix(cli): state converter-backed formats as unsupported, not as needing a tool
Maintainer decision: we do not offer support for any Vale format that parses through an external program. The skip notice used to end "Install it and put it on your PATH to have these files checked." That offered a path this build does not ship and does not test, and for `.xml` cannot deliver at all — an XSLT stylesheet is specific to the document, so no install makes it lintable. It also made behaviour host-dependent: macOS ships /usr/bin/xsltproc and typical Linux CI images do not, so the same repository checked differently depending on the machine. That split is what made this stack red in CI while green locally, which is the strongest argument for excluding unconditionally: a repository should check the same way everywhere. The exclusion mechanism is unchanged — it is what prevents the total-run crash. What changed is the promise. The programs are still named, as the reason rather than as a remedy, and the recipes now say plainly that installing them does not change the answer. The cost, accepted: a user who genuinely has asciidoctor installed loses `.adoc` checking that would have worked. Also reconciles the comment tier against Vale's own documentation, and sharpens the version-bump note now that the 3.18.0 picture is confirmed: `.mdx` gains a native parser and becomes supported, `.typ` gains one that shells out to typst2vast and so stays unsupported permanently, and MyST, Quarto and QDoc arrive needing no external program. All four require 3.18.0, so none is reachable from the pinned 3.17.1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Cyga14bww8rmazH2XrF8ms
1 parent 36dc592 commit 5dae515

7 files changed

Lines changed: 107 additions & 23 deletions

File tree

‎.changeset/vale-converter-formats.md‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,24 @@ expression goes path-wise, at which point a bare `*.adoc` stops matching
4848
tested. Vale's error output is also decoded now rather than forwarded verbatim,
4949
so a failure reads as a sentence naming the missing program instead of a
5050
five-field JSON object.
51+
52+
**These formats are now stated as unsupported rather than as needing a tool.**
53+
The notice used to end "Install it and put it on your PATH to have these files
54+
checked", which offered a path this build does not ship, does not test, and for
55+
`.xml` cannot deliver — an XSLT stylesheet is specific to the document, so no
56+
install makes it lintable. It also made behaviour host-dependent: macOS ships
57+
`/usr/bin/xsltproc` and typical Linux CI images do not, so the same repository
58+
checked differently depending on the machine. The exclusion is unconditional
59+
for that reason, and the programs are still named as the reason rather than as
60+
a remedy.
61+
62+
The comment tier was reconciled against Vale's own documentation at
63+
docs.vale.sh/formats/code, which adds `.bsh`, `.csx`, `.pod`, `.py3` and `.sbt`
64+
once measured. It also documents `.pyi`, `.qml` and `.scss` as comment-aware,
65+
and on the pinned 3.17.1 a bare non-comment line in each of them lints — so they
66+
stay in the plaintext tier. That divergence is the argument for probing rather
67+
than transcribing: the docs describe the current Vale, this build pins an older
68+
one, and copying the list would have shipped `.scss` as comment-aware and been
69+
wrong. `.pod` is a reminder of how easily this is misread — it lints Perl
70+
comments but not POD blocks, so probing it with `=head1` looks like no support
71+
at all.

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,11 +316,16 @@ it.
316316
not what the rule means, narrow the glob rather than accepting it.
317317
These land here despite reading like markup, so a `scope:` value
318318
has nothing to act on in them: %(VALE_PLAINTEXT_FORMATS)s
319-
- **cannot be read at all** — Vale supports these formats but shells
320-
out to an external converter to parse them, and this CLI ships none
321-
of those converters:
319+
- **not supported** — Vale parses these only by shelling out to an
320+
external program, and this build does not support any format that
321+
needs one:
322322
%(VALE_CONVERTER_FORMATS)s
323323

324+
Do not tell the user to install the program. Taskless excludes these
325+
files from the run whatever is installed, so that a repository
326+
checks the same way on every machine; `.xml` could not work anyway,
327+
since an XSLT stylesheet is specific to the document.
328+
324329
**A single unreadable file fails the whole Vale pass.** Vale exits 2
325330
with an `E100` runtime error and abandons the run — `--no-exit` does
326331
not suppress it — so every other Vale rule over every other file goes

‎packages/cli/src/agent/route.txt‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,11 +124,16 @@ answered together.
124124
scoped to YAML flags the code as readily as the comments. That is
125125
rarely what was asked for — say so before writing it. These read
126126
like markup and are not: %(VALE_PLAINTEXT_FORMATS)s
127-
- *cannot be read at all* — Vale supports these formats but shells
128-
out to an external converter to parse them, and this CLI ships
129-
none of those converters:
127+
- *not supported* — Vale parses these only by shelling out to an
128+
external program, and this build does not support any format that
129+
needs one:
130130
%(VALE_CONVERTER_FORMATS)s
131131

132+
Installing the program does not change this. Taskless excludes
133+
these files from the Vale run whatever is on the machine, so that
134+
a repository checks the same way everywhere rather than depending
135+
on what a given host happens to have available.
136+
132137
**One such file fails the entire Vale pass, not just that file.**
133138
Vale exits 2 with an `E100` runtime error, `--no-exit` does not
134139
suppress it, and every other Vale rule over every other file goes

‎packages/cli/src/rules/capabilities.ts‎

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,24 @@ const CONVERTER_TIER_PREFIX = "converter:";
156156
* are a property of {@link VALE_VERSION}'s binary, and the dangerous direction
157157
* is a format Vale *learns*: an extension missing from this table is read as
158158
* plain text today, but the moment Vale routes it to a converter the same
159-
* omission is a crash that takes down every Vale rule in the run. Vale 3.18.0
160-
* is the known incoming case in both directions — it parses MDX natively, and
161-
* it adds a Typst converter, so `.typ` moves from `plaintext` to
162-
* `converter:typst2vast` on that bump while `.mdx` moves to `markup`. Neither
163-
* happens on its own; re-probe every row.
159+
* omission is a crash that takes down every Vale rule in the run.
160+
*
161+
* Vale 3.18.0 is the known incoming bump, and it moves rows in three different
162+
* directions — which is why "re-measure" is not boilerplate here:
163+
*
164+
* - `.mdx` gains a native parser, so it moves `converter:mdx2vast` → `markup`
165+
* and becomes supported.
166+
* - `.typ` gains a parser that shells out to `typst2vast`
167+
* (https://docs.vale.sh/formats/typst), so it moves `plaintext` →
168+
* `converter:typst2vast`. That is the dangerous direction: today it is read
169+
* as prose, and after the bump the same row would crash the run. It also
170+
* stays unsupported permanently, since we do not support formats needing an
171+
* external program.
172+
* - MyST, Quarto and QDoc arrive with parsers needing no external program, so
173+
* they become genuinely supportable and want `markup` rows once measured.
174+
*
175+
* All four are documented as requiring v3.18.0 or later, so none of them is
176+
* reachable from {@link VALE_VERSION}. Re-probe every row on the bump.
164177
*/
165178
export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
166179
// markup — parsed, the format's own constructs skipped
@@ -174,11 +187,13 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
174187
// comment text only — the code body is invisible
175188
".c": "comment",
176189
".c++": "comment",
190+
".bsh": "comment",
177191
".cc": "comment",
178192
".clj": "comment",
179193
".cpp": "comment",
180194
".cs": "comment",
181195
".css": "comment",
196+
".csx": "comment",
182197
".cxx": "comment",
183198
".go": "comment",
184199
".h": "comment",
@@ -194,15 +209,18 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
194209
".php": "comment",
195210
".pl": "comment",
196211
".pm": "comment",
212+
".pod": "comment",
197213
".proto": "comment",
198214
".ps1": "comment",
199215
".py": "comment",
216+
".py3": "comment",
200217
".pyw": "comment",
201218
".r": "comment",
202219
".R": "comment",
203220
".rb": "comment",
204221
".rs": "comment",
205222
".sass": "comment",
223+
".sbt": "comment",
206224
".scala": "comment",
207225
".swift": "comment",
208226
".ts": "comment",
@@ -213,6 +231,14 @@ export const VALE_FORMAT_TIERS: Readonly<Record<string, ValeFormatTier>> = {
213231
".rmd": "plaintext",
214232
".tex": "plaintext",
215233
".typ": "plaintext",
234+
// plaintext HERE, though Vale's own docs list them as comment-tier. The docs
235+
// describe the CURRENT Vale; we pin 3.17.1. Measured on the pinned binary a
236+
// bare non-comment line lints, which is the plaintext signature. Transcribing
237+
// the docs would have shipped these as comment-tier and been wrong for this
238+
// build — the case for probing rather than copying.
239+
".pyi": "plaintext",
240+
".qml": "plaintext",
241+
".scss": "plaintext",
216242
// converter-dependent — Vale supports the format, we ship no converter
217243
".adoc": "converter:asciidoctor",
218244
".asc": "converter:asciidoctor",

‎packages/cli/src/rules/vale/formats.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,16 @@ export const TASKLESS_DIRECTORY = ".taskless";
8181
* Vale": the moment Vale learns a format, it starts routing that extension to a
8282
* parser, and if that parser shells out, an extension missing from the table is
8383
* a crash rather than a plain-text read. Vale 3.18.0 is the live example — it
84-
* adds Typst, which converts through `typst2vast`, so `.typ` stops being the
84+
* adds Typst, which parses through `typst2vast`, so `.typ` stops being the
8585
* plaintext read it is today and upgrading the `@taskless/vale-*` packages
8686
* without re-measuring would reintroduce exactly this bug under a new
8787
* extension. Re-measure the whole table on every bump; the per-extension cases
8888
* in `vale-vendor-contract.test.ts` are how.
89+
*
90+
* Note what the exclusion policy buys here. Because a format needing an
91+
* external program is never supported, a newly-converter-backed extension has
92+
* one correct destination rather than a judgement call, and the answer does not
93+
* depend on what happens to be installed on the machine running `check`.
8994
*/
9095

9196
/**
@@ -240,10 +245,10 @@ export function skippedFilesNotice(files: string[]): string | undefined {
240245
: sample.join(", ");
241246

242247
return (
243-
`Vale did not check ${String(files.length)} file(s): ${listed}. Vale ` +
244-
`supports these formats, but parsing them needs an external converter ` +
245-
`(${converters.join(", ")}) that this build does not ship. Install it and ` +
246-
`put it on your PATH to have these files checked; every other file was ` +
247-
`checked normally.`
248+
`Vale did not check ${String(files.length)} file(s): ${listed}. These ` +
249+
`formats are not supported by this build — Vale parses them only through ` +
250+
`an external program (${converters.join(", ")}), which this build does ` +
251+
`not ship and does not check for. Scope the rule to a supported format; ` +
252+
`every other file was checked normally.`
248253
);
249254
}

‎packages/cli/test/vale-formats.test.ts‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,16 +153,25 @@ describe("the skipped-files notice", () => {
153153
expect(skippedFilesNotice([])).toBeUndefined();
154154
});
155155

156-
it("names the files, the converter, and that the rest was checked", () => {
157-
const notice = skippedFilesNotice(["docs/guide.adoc", "spec/api.rst"]);
156+
it("names the files and says the format is unsupported, not that a tool is missing", () => {
157+
const notice =
158+
skippedFilesNotice(["docs/guide.adoc", "spec/api.rst"]) ?? "";
158159
expect(notice).toContain("docs/guide.adoc");
159160
expect(notice).toContain("spec/api.rst");
161+
expect(notice).toContain("every other file was checked normally");
162+
// The programs are still named — they are the REASON, and a user reading
163+
// "asciidoctor" understands what kind of gap this is.
160164
expect(notice).toContain("asciidoctor");
161165
expect(notice).toContain("rst2html");
162-
expect(notice).toContain("every other file was checked normally");
163-
// Vale supports these formats; this build cannot parse them. Telling a user
164-
// otherwise sends them to the wrong project's issue tracker.
165-
expect(notice).toContain("Vale supports these formats");
166+
// But the notice must not read as an offer. We do not support any format
167+
// that needs an external program, so telling a user to install one promises
168+
// a path that is untested, and for `.xml` impossible — an XSLT stylesheet is
169+
// specific to the document. It would also make behaviour host-dependent:
170+
// macOS ships /usr/bin/xsltproc and Linux CI images do not, so the same
171+
// repository would check differently per machine.
172+
expect(notice).toContain("not supported by this build");
173+
expect(notice).not.toMatch(/install/i);
174+
expect(notice).not.toMatch(/\bPATH\b/);
166175
});
167176

168177
it("summarizes rather than printing an unbounded file list", () => {

‎packages/cli/test/vale-vendor-contract.test.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,13 +395,21 @@ withVale("Vale vendor contract", () => {
395395
* See taskless/cli#151.
396396
*/
397397
/** The comment syntax Vale must see through, per extension. */
398+
/**
399+
* A correct comment for `extension`, which is the whole difficulty of this
400+
* file: a wrong delimiter reads exactly like absent support. `.css` looks
401+
* unsupported if fed `//` and is comment-aware with a block comment; `.pod` is
402+
* linted as Perl, so a `#` comment fires while a `=head1` POD block does not.
403+
*/
398404
function comment(extension: string): string {
399405
const HASH = new Set([
400406
".jl",
401407
".pl",
402408
".pm",
403409
".ps1",
410+
".pod",
404411
".py",
412+
".py3",
405413
".pyw",
406414
".r",
407415
".R",
@@ -546,6 +554,11 @@ withVale("Vale engine capabilities", () => {
546554
".rmd": "Fine.\n\n```{r}\nsimply <- 1\n```\n",
547555
".tex": "% simply in a comment\nFine.\n",
548556
".typ": "// simply\nFine.\n",
557+
// Documented by Vale as comment-aware, measured as plaintext on the pinned
558+
// binary — the construct here is the comment a parser would have skipped.
559+
".pyi": "# simply\nFine.\n",
560+
".qml": "// simply\nFine.\n",
561+
".scss": "// simply\nFine.\n",
549562
};
550563

551564
it("covers every extension in VALE_PLAINTEXT_EXTENSIONS", () => {

0 commit comments

Comments
 (0)