diff --git a/.changeset/vale-converter-formats.md b/.changeset/vale-converter-formats.md index 2725d48d..7a91db66 100644 --- a/.changeset/vale-converter-formats.md +++ b/.changeset/vale-converter-formats.md @@ -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 `> = { // markup — parsed, the format's own constructs skipped @@ -181,8 +192,12 @@ export const VALE_FORMAT_TIERS: Readonly> = { ".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", @@ -212,6 +227,8 @@ export const VALE_FORMAT_TIERS: Readonly> = { ".pod": "comment", ".proto": "comment", ".ps1": "comment", + ".qdoc": "comment", + ".qml": "comment", ".py": "comment", ".py3": "comment", ".pyw": "comment", @@ -220,6 +237,7 @@ export const VALE_FORMAT_TIERS: Readonly> = { ".rb": "comment", ".rs": "comment", ".sass": "comment", + ".scss": "comment", ".sbt": "comment", ".scala": "comment", ".swift": "comment", @@ -228,23 +246,20 @@ export const VALE_FORMAT_TIERS: Readonly> = { // 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", @@ -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(); @@ -369,7 +386,7 @@ export const VALE_CONVERTER_CHECKERS: Readonly> = { ".asc": "lintAdoc", ".xml": "lintXML", ".dita": "lintDITA", - ".mdx": "lintMDX", + ".typ": "lintTypst", }; /** Every converter-dependent extension, flattened. */ diff --git a/packages/cli/test/recipe-cross-references.test.ts b/packages/cli/test/recipe-cross-references.test.ts index 4319f727..966103c8 100644 --- a/packages/cli/test/recipe-cross-references.test.ts +++ b/packages/cli/test/recipe-cross-references.test.ts @@ -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 () => { @@ -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/); diff --git a/packages/cli/test/vale-formats.test.ts b/packages/cli/test/vale-formats.test.ts index 355407b4..27c2450c 100644 --- a/packages/cli/test/vale-formats.test.ts +++ b/packages/cli/test/vale-formats.test.ts @@ -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(); }); }); diff --git a/packages/cli/test/vale-vendor-contract.test.ts b/packages/cli/test/vale-vendor-contract.test.ts index 9079c2a8..649e6866 100644 --- a/packages/cli/test/vale-vendor-contract.test.ts +++ b/packages/cli/test/vale-vendor-contract.test.ts @@ -388,8 +388,10 @@ withVale("Vale vendor contract", () => { * - **plaintext** is the tier that needs no separating — a bare line fires — * but the entries listed in it do: each names a construct a parser WOULD have * skipped, and the probe asserts Vale lints it. That is the assertion that - * `.tex` and `.rmd` are not markup, and it is the one the first hand-written - * table got backwards. + * `.tex`, `.mkd` and `.mkdn` are not markup, and it is the one the first + * hand-written table got backwards. It is also the assertion that expires: on + * 3.17.1 `.rmd` belonged here, and 3.18.0 gave it a real parser and moved it + * into MARKUP_FIXTURES. * - **converter-dependent** is separated from everything by failing. * * See taskless/cli#151. @@ -417,6 +419,11 @@ function comment(extension: string): string { ]); if (HASH.has(extension)) return "# simply\n"; if (extension === ".css") return "/* simply */\n"; + // Vale 3.18.0 parses PHP with tree-sitter, so a comment only counts inside + // real PHP — on 3.17.1 a bare `// simply` was linted without the open tag. + if (extension === ".php") return " { skipped: "Fine.\n\n```\nsimply\n```\n", }, ".org": { prose: "We simply do it.\n", skipped: "# simply\nFine.\n" }, + // Native as of 3.18.0. `.mdx` arrived from the converter tier, `.rmd` from + // plaintext, and `.qmd` and `.myst` are new rows — four new claims, none of + // them carried over from the 3.17.1 table. + ".mdx": { + prose: "We simply do it.\n", + skipped: "Fine.\n\n```\nsimply\n```\n", + }, + ".myst": { + prose: "We simply do it.\n", + skipped: "Fine.\n\n```\nsimply\n```\n", + }, + ".qmd": { + prose: "We simply do it.\n", + skipped: "Fine.\n\n```{r}\nsimply <- 1\n```\n", + }, + ".rmd": { + prose: "We simply do it.\n", + skipped: "Fine.\n\n```{r}\nsimply <- 1\n```\n", + }, ".htm": { prose: "

We simply do it.

\n", skipped: "\n", @@ -479,11 +505,10 @@ withVale("Vale engine capabilities", () => { it("reports the pinned version", () => { // VALE_VERSION is rendered beside the reach lists in route.txt and // create-vale-rule.txt, so it is the attribution for every claim below. - // It also gates two of them, in opposite directions: Vale 3.18.0 parses MDX - // natively, so a bump past it moves `.mdx` from converter-dependent to - // markup — and the same release adds a Typst converter, so it moves `.typ` - // from plaintext to `converter:typst2vast`. A bump re-measures the whole - // table; these two are only the rows already known to move. + // 3.18.0 moved rows in both directions: `.mdx` gained a native parser and + // left the converter tier, while `.typ` gained a `typst2vast` converter and + // entered it. A bump re-measures the whole table — the fixtures below are + // the record of the last time that was done, not a forecast of the next. const result = spawnSync(binary as string, ["--version"], { encoding: "utf8", }); @@ -545,20 +570,19 @@ withVale("Vale engine capabilities", () => { * its spelling suggests would have skipped. * * Vale lints it, which is the whole finding: `.tex` is not TeX to Vale and - * `.rmd` is not R Markdown. The mirror image of the markup fixtures — same + * `.mkd` is not Markdown. The mirror image of the markup fixtures — same * documents, opposite expectation. */ const PLAINTEXT_FIXTURES: Record = { ".mkd": "Fine.\n\n```\nsimply\n```\n", ".mkdn": "Fine.\n\n```\nsimply\n```\n", - ".rmd": "Fine.\n\n```{r}\nsimply <- 1\n```\n", ".tex": "% simply in a comment\nFine.\n", - ".typ": "// simply\nFine.\n", // Documented by Vale as comment-aware, measured as plaintext on the pinned // binary — the construct here is the comment a parser would have skipped. + // `.qml` and `.scss` were here until 3.18.0 gave them real parsers, and + // `.rmd` left for MARKUP_FIXTURES in the same bump, which is why the tier is + // re-measured on every bump rather than carried over. ".pyi": "# simply\nFine.\n", - ".qml": "// simply\nFine.\n", - ".scss": "// simply\nFine.\n", }; it("covers every extension in VALE_PLAINTEXT_EXTENSIONS", () => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c0e399ae..97d57ab1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,23 +152,23 @@ importers: specifier: 0.41.0 version: 0.41.0 '@taskless/vale-darwin-arm64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 '@taskless/vale-darwin-x64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 '@taskless/vale-linux-arm64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 '@taskless/vale-linux-x64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 '@taskless/vale-win32-arm64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 '@taskless/vale-win32-x64': - specifier: 3.17.1-20260810052605 - version: 3.17.1-20260810052605 + specifier: 3.18.0-20260824195610 + version: 3.18.0-20260824195610 packages/vale-darwin-arm64: {} @@ -831,33 +831,33 @@ packages: '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - '@taskless/vale-darwin-arm64@3.17.1-20260810052605': - resolution: {integrity: sha512-182XIfZmbUaa8js7t11a7MB7Iw+tKsIUOXd8sDIKD7QhuEcdnrRt3dWHXRVR065tr86U5/z1tnHqenlWlx2BfA==} + '@taskless/vale-darwin-arm64@3.18.0-20260824195610': + resolution: {integrity: sha512-5CYshnFck8eFrvlDk33ObSFepFKlRJBeXZBLfDt5yLtjzHaBaOILwXlLMao4d1trbppN+ryb21btInJCteamVQ==} cpu: [arm64] os: [darwin] - '@taskless/vale-darwin-x64@3.17.1-20260810052605': - resolution: {integrity: sha512-NR3fWrTTu1Lj/nWOcKTO27pGv+wRTwbETcrxmDCxbsP5wNGPUHk/lhZIdvR4JE2PcIBg79mDOuE3CGt/G8nlnQ==} + '@taskless/vale-darwin-x64@3.18.0-20260824195610': + resolution: {integrity: sha512-WX2KWjE7/srXfIXrb2+p/KvB8wt76iQsB4r34c88GUiYidLmwjlN4sPcfCFyKgzcU9vsVwd9ycHpuACqtR51tw==} cpu: [x64] os: [darwin] - '@taskless/vale-linux-arm64@3.17.1-20260810052605': - resolution: {integrity: sha512-bxQgaTVxuxASzuLSVF/hm/blQnM8b3+2+y5swm91YbcCW8PW0/myjSCsNCazErfOwZzbdOdsSV4GJkNjmzSSsA==} + '@taskless/vale-linux-arm64@3.18.0-20260824195610': + resolution: {integrity: sha512-tAi4ys1wR+HJ6JQucExrGxg/DzVWc+Lx4+CixPLPgmIP+vw8E9DXdkY7vyIwMAMmxXUS3/cl7TNFv2J/Su3QhA==} cpu: [arm64] os: [linux] - '@taskless/vale-linux-x64@3.17.1-20260810052605': - resolution: {integrity: sha512-XByCGwrckU2vueP1XoqIaUD5sqPsgqhWAlVKHhTlTql/5wecQLT6c6/WGa8g8uN4uRYbl7MG12eJPBHWabOXiw==} + '@taskless/vale-linux-x64@3.18.0-20260824195610': + resolution: {integrity: sha512-Ae+fGfXGy6QlzuDLyh0FFkjrsbp52oxCQtRzKG6CfUIVc80O4Hh/yZIh+xi4UwTL9zJ69xS4LNDQ1MLoR/DQYQ==} cpu: [x64] os: [linux] - '@taskless/vale-win32-arm64@3.17.1-20260810052605': - resolution: {integrity: sha512-PxX0QZUTB1VR3h6T8A7/YJ8QtXCAEWnYifBrHHy0yYTPlgb7hQw69px26AAnlZy2ml7CClzt4LwF7Syq7gE+sw==} + '@taskless/vale-win32-arm64@3.18.0-20260824195610': + resolution: {integrity: sha512-FSYdG2KRuv2cI2ulJMVlRdtKI1rAd2ScqyERlF6MAnqCu/0TY95I/Po3YBpKA4Ms3l4YBk2dP1viphm68y/L1g==} cpu: [arm64] os: [win32] - '@taskless/vale-win32-x64@3.17.1-20260810052605': - resolution: {integrity: sha512-JBldnOID0Q7RM3jy9tr98ZI2DFav9UzLtRdR1bkoJ2i8X5XdkeEfKVD6Na5YNJ/C87I3z6tcZyoiIxcTKYcM9w==} + '@taskless/vale-win32-x64@3.18.0-20260824195610': + resolution: {integrity: sha512-xpIADyagcUUSL0+MXuR5557W2DzC7jYrttN6V6NqNjdYQ3/T2vxZ97B4+syMhbSgyfm9ratLLasuGfVNj+KS3w==} cpu: [x64] os: [win32] @@ -3087,22 +3087,22 @@ snapshots: '@standard-schema/spec@1.1.0': {} - '@taskless/vale-darwin-arm64@3.17.1-20260810052605': + '@taskless/vale-darwin-arm64@3.18.0-20260824195610': optional: true - '@taskless/vale-darwin-x64@3.17.1-20260810052605': + '@taskless/vale-darwin-x64@3.18.0-20260824195610': optional: true - '@taskless/vale-linux-arm64@3.17.1-20260810052605': + '@taskless/vale-linux-arm64@3.18.0-20260824195610': optional: true - '@taskless/vale-linux-x64@3.17.1-20260810052605': + '@taskless/vale-linux-x64@3.18.0-20260824195610': optional: true - '@taskless/vale-win32-arm64@3.17.1-20260810052605': + '@taskless/vale-win32-arm64@3.18.0-20260824195610': optional: true - '@taskless/vale-win32-x64@3.17.1-20260810052605': + '@taskless/vale-win32-x64@3.18.0-20260824195610': optional: true '@types/chai@5.2.3':