Skip to content

chore(vale): upgrade to Vale 3.24.0 - #546

Merged
theCodeDrift merged 3 commits into
mainfrom
vendor/vale/upgrade
Oct 21, 2026
Merged

theCodeDrift merged 3 commits into
mainfrom
vendor/vale/upgrade

Conversation

@github-actions

@github-actions github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Stack (root → tip):

The @taskless/vale-* packages are published ahead of what
@taskless/cli pins.

This moves all six pins from 3.23.0-20260930045834 to
3.24.0-20261005234241 — Vale 3.24.0 — and regenerates
pnpm-lock.yaml. The pins move together on purpose: the platform
packages are selected by optional dependency, so a straggler left at
the old version is a different Vale on one platform than on the others.

It also moves VALE_VERSION in capabilities.ts to
3.24.0 and regenerates src/generated/vale-vocabulary.ts
from the new binary, and the workflow ran
engine-version-consistency against the result before pushing. What
this does not carry is judgment: if the contract suites below
report a failure, Vale 3.24.0 changed a measured behaviour,
and that edit, the update ledger, any migration, and the release
note's prose belong in a child pull request. So does a row for each
format reader listed below.

This is the half that reaches a user. Republishing the platform
packages changes nobody's install, because the CLI pins each one
exactly; merging this is what ships the new Vale.

This pull request rolls: if another set is published before it merges,
the branch, title, and body are rewritten to the newer version rather
than a second pull request being opened. Push a commit to the branch
and that stops — the workflow will not force-push over a commit it did
not write.

Measured against Vale 3.24.0

Compared with 3.23.0, which is what main pins. Validate on a pull request this workflow opens waits for a maintainer to approve it, so this section is the check that ran.

Contract suites

8 of 211 failed. The new Vale changed a recorded behaviour. The measured edit, the update ledger, and any migration belong in a child pull request that merges down into this branch.

vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .adoc, needing asciidoctor
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintAdoc]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .asc, needing asciidoctor
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintAdoc]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .asciidoc, needing asciidoctor
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintAdoc]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .dita, needing dita
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintDITA]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .typ, needing typst2vast
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintTypst]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .rest, needing rst2html
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintRST]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .rst, needing rst2html
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintRST]'
vale-vendor-contract.test.ts > Vale engine capabilities fails the whole run on .xml, needing xsltproc and an XSLT stylesheet
  AssertionError: expected '{\n  "Line": 0,\n  "Path": "",\n  "Te…' to contain '[lintXML]'

Format readers upstream added

  • internal/lint/code/cs.go

Each can route an extension that VALE_FORMAT_TIERS in capabilities.ts has no row for, and an unlisted extension is never probed. Find where internal/core/format.go sends it, probe the extension against the new binary, and add a row. A reader that needs an external converter is the dangerous case: the missing row becomes a crash that aborts every Vale rule in the run. (diff)


Upstream release notes — 3.24.0

https://github.com/vale-cli/vale/releases/tag/v3.24.0

This release adds vale test, a test runner for your rules. A rule that matches nothing still loads, runs, and reports success. You only find out it's broken when it misses something. Now each rule can carry its own test cases: a short document, and the alerts linting it should produce.

extends: existence
message: "Consider removing '%s'."
tokens:
  - perhaps
tests:
  - name: flags a hedge
    input: Perhaps we should ship.
    want: |
      1:1:House.Hedging:Consider removing 'Perhaps'.

Running vale test lints each case and shows a diff when the output doesn't match. A case can pin the exact output (want), check for part of it (contains), or check that something is left alone (absent). It can run one rule on its own or go through your project's configuration, and it can read its input as another format or through a View. script rules are tested the same way. --coverage fails any rule that no case makes fire, and the exit codes are built for CI.

See Testing and the announcement post.

Changelog

  • 2ee78171 fix: A rule whose tests don't parse fails vale test instead of vanishing
  • 77da0b08 refactor: Typed errors, pattern errors at the token, and readable contexts
  • 634b321c fix: Errors show the real exit code, honor --no-color, and point at the config
  • db39c406 feat: BasedOnStyles = NONE, the spelled-out empty value
  • bc8b91d8 perf: Don't parse a file no rule can run on
  • c283a4dc feat: Conditional sections, [glob if expr]
  • 08923bdf feat: vale test, and install a package's own dependencies for it
  • 424f6e62 fix: Keep a fenced block skipped when both pre and code are in SkippedScopes
  • 84deb6c3 fix: Keep non-breaking and narrow spaces beside inline markup
  • 8cbda873 feat(code): parse C# comments with the C# grammar (#1196)
  • 24f277bb test: add vale test cases

Contains #549

#546 moves the six @taskless/vale-* pins to Vale 3.24.0. Run locally it has 10 failures, not the 8 its report lists: the report runs only the contract suites, and the two it missed were a real break for users. This PR makes the upgrade correct and pins what 3.24.0 changed. It merges down into vendor/vale/upgrade, so the pin, the fixes and the guidance reach main together.

The break the report missed

Vale 3.24.0 warns about every key it does not recognise inside a matcher (upstream validateLevel). That includes the tskl) rule = <id> breadcrumb every rule's .vale.ini carries:

W101 'tskl) rule' isn't a section option; Vale is ignoring it.

Two effects:

  • Every check printed one notice per matcher.
  • The warning lands on stderr ahead of a fatal error's JSON, so the JSON no longer parsed. That threw away the error's Path and with it the One unparseable file zeroes findings for the whole check run #300 narrowing. One file with bad front matter took every other file's Vale findings down again (mixed-engine-check caught it).

Fix, at assembly, with no migration. valeRuleBlock puts # in front of each breadcrumb line, at the lines the config schema already located (breadcrumbLines on the verdict). Nothing searches the text. Line counts are kept, so a line Vale reports is still the author's. Rule files on disk and recipes are unchanged.

And in runVale. Vale's leading W\d{3} lines are peeled off before its error is parsed, so a user's own warning (3.24.0 also warns on <id>.<id> = maybe) cannot block the narrowing either. Each warning is now its own notice.

The 8 reported failures

The converter E100 dropped its checker tag. [lintAdoc] Runtime error is now asciidoctor not found; it's needed to read AsciiDoc. The run still dies the same way (exit 2, --no-exit does not help), and only the test read the tag. VALE_CONVERTER_CHECKERS becomes VALE_CONVERTER_READERS, keyed to the format Vale names. .xml keeps its host split: where xsltproc is present it reports the missing stylesheet instead.

What changed that no test caught

A new Vale 3.24.0 block in vale-vendor-contract.test.ts, each case run against 3.22.0 and 3.24.0 on the same fixture:

3.22.0 / 3.23.0 3.24.0
unknown key in a matcher silent W101
# tskl) rule comment in a matcher silent silent
warnings, then a fatal error (no warnings) W101 lines, then the JSON
C# // ReSharper disable, // NOSONAR linted not read
word after a link and U+00A0 fused, unmatched matched
.adoc where every rule is NO aborts the run not parsed

The new C# reader the report flagged (code/cs.go) moves no tier. .cs and .csx were already comment rows, read with the C grammar. Conditional sections ([*.md if .Meta.draft]) pass validate and work in check as-is.

tests: is refused

Vale 3.24.0 ships vale test, which runs cases written in a rule's tests: list. Taskless never runs it: verify judges a Vale rule by its .tests/pass and .tests/fail documents. validate and verify now reject the key rather than let a rule read as tested by cases nothing runs. check still runs such a rule. The vocabulary keeps tests, since Vale reads it, and the corpus row moves to DIVERGENCES so the disagreement is counted.

A deterministic rule migration that turns each case into a fixture follows as its own PR on #548's update --rules mechanism, now on main. Until then the ledger gives the steps by hand.

Guidance

Checks

pnpm typecheck, pnpm lint and pnpm test (2497 passed) are green locally on this branch, rebased onto main with ast-grep 0.51.0 and built.

Not in this PR

  • The upgrade report could also run vale-run and mixed-engine-check, which would have caught the two failures it missed.

Refs #546

Contains #551

#549 makes validate and verify refuse a Vale rule's tests: key. 0.12.0 had accepted it, so a rule written since then would stop validating on upgrade. This adds a rule migration on #548's mechanism, so update --rules moves those cases into fixtures instead of leaving the author a by-hand list. It merges down into vendor/vale/3-24-0-contract.

What it does

src/rules/rule-migrations/vale-3-24-0-tests-to-fixtures.ts, registered in RULE_MIGRATIONS, keyed on Vale 3.24.0. It runs when the recorded Vale engine is below 3.24.0.

A case is a document and whether the rule fires on it, which is exactly a fixture. An in-rule case loads only its own rule, so any alert it expects is that rule's:

the case becomes
want with any text .tests/fail/
want that is empty .tests/pass/
no want, but contains .tests/fail/
only absent .tests/pass/
  • The fixture: the case's input, byte for byte. It is named for the case (flags a hedge → flags-a-hedge.md), with format as the extension (md by default, as Vale reads it). What want pins beyond that (line, column, message) is not something verification needs, and it is not carried.
  • The rule file: the tests: block is removed by its byte range from the YAML parser. Nothing is re-serialized, so comments and every other line stay as written.
  • Safe to rerun: fixtures are written before the rule file. A fixture with identical bytes counts as done, and a different file with the same name gets -2. A rerun, or a run that died between the two steps, changes nothing it already did.
  • Left alone: a rule with a case that reads through a view (this layout defines none) or names another rule file in rule:. Moving half its cases would be harder to finish than none. validate keeps naming that rule.

Issued rules

Per the rule-migration contract, the rewrite only applies to shapes the service never issued. Nothing in the Cloud corpus carries tests: (confirmed 2026-10-20), so no signed rule is touched. Rules users wrote themselves are what this is for.

Guidance

  • update (topic v18): the 0.13.3 opener is back to "Nothing here needs work by hand." The tests: paragraph now reads like the Haskell one: update --rules moves them for you, so don't do it by hand. It names the view and rule: cases as the exception.
  • The changeset on chore(vale): upgrade to Vale 3.24.0 #546 gains one sentence.

Tests

  • rule-migrate.test.ts: block removal for an indented list, a list at the key's column, a blank line inside, and a block ending the file with no newline. The bucket for each kind of case, naming and extensions, the rules it leaves alone, idempotence, finishing an interrupted run, never overwriting, failing on an unreadable rules tree, and keying on 3.24.0. A migrated rule passes validateValeRule.
  • reconcile-marker.test.ts: update --rules through the built CLI moves the cases, reports the files, and stamps the engines.
  • Two existing Haskell migration tests now record the current Vale version. They recorded only sg, so the Vale migration also read as pending in them.

pnpm typecheck, pnpm lint and pnpm test (2526 passed) are green locally, built.

Refs #549

…'s tests: key

Vale 3.24.0 warns W101 about every key it does not recognise inside a
matcher, including the `tskl) rule` breadcrumb. That printed one notice
per matcher on every check, and the warning ahead of a fatal error's
JSON broke the per-file narrowing from #300. Assembly now
comments the breadcrumb out of the config Vale reads, and runVale peels
Vale's warning lines off before parsing its error, one notice each.

The converter E100 dropped its `[lintAdoc]` checker tag, so the
contract asserts the format Vale now names instead. New 3.24.0 pins
cover the W101, warning-then-error order, C# tool directives, a
non-breaking space after a link, and files no rule runs on.

`validate` and `verify` now reject a Vale rule's `tests:` key: Taskless
verifies Vale rules by their .tests/pass and .tests/fail documents and
never runs `vale test`.
`validate` refuses a Vale rule's `tests:` key from 3.24.0 on, and 0.12.0
had accepted it. A rule migration keyed on Vale 3.24.0 now writes each
case's input as a fixture under .tests/fail when it expects an alert and
.tests/pass when it expects none, named for the case with its format as
the extension, then deletes the block by its byte range. It never
overwrites a fixture, reruns as a no-op, and leaves a rule whose cases
read through a view or name another rule file untouched. No issued rule
carries the key.
@theCodeDrift

Copy link
Copy Markdown
Member

@claude /review

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

Claude finished @theCodeDrift's task in 4m 20s —— View job


Review complete

  • Read .prior-review.json (no prior threads/reviews found — this is the first review)
  • Fetch PR diff and metadata
  • Review changed files for correctness, security, performance, test adequacy, clarity
  • Post inline comments for findings (none warranted — see below)
  • Post final summary

Review mode: incremental — read 0 prior review thread(s) before reviewing. The only prior content was the @claude /review trigger comment itself; there are no prior review threads, reviews, or findings to classify, so this is effectively the first review and covers the whole diff.

Summary

This is a well-scoped, carefully-reasoned upgrade PR (the branch already carries the two stacked fix/feature commits from #549 and #551, so the diff reviewed here is the full combined change). I read all 22 changed files plus the surrounding source (vale-config.ts, rule-migrate.ts, vale-rule.ts) for context the diff hunks don't show. I did not find any correctness, security, or performance bugs.

Specific things I verified rather than assumed:

  • Breadcrumb commenting (assemble.ts / vale-config.ts): valeRuleBlock comments out tskl) rule lines in place using 1-based line numbers from breadcrumbLines, which are only populated when locateLines could actually match the source line to the parsed node. assembleValeConfig only calls valeRuleBlock after checking verdict.rejections.length === 0, so a config with a root-level (unscoped) breadcrumb — which the schema rejects — never reaches this commenting logic. CRLF line endings are preserved correctly since the split/join round-trips on "\n" only (confirmed against the CRLF case added in assemble.test.ts).
  • splitValeWarnings (run.ts): only peels off a leading run of W\d{3} lines, so a pretty-printed JSON error object (which spans multiple lines but never starts a line with W\d{3}) is never split mid-object — JSON.parse still gets the whole object back intact. The ordering difference between the failure path (error first, then warnings) and the success path (warnings, then trailing text) is intentional and documented, not an inconsistency.
  • vale-3-24-0-tests-to-fixtures.ts migration: byte-range removal of the tests: block, fixture bucketing (want/contains/absent priority), slug collision handling (taken set shared per rule file, -2 suffix), and the fixtures-before-rule-file write order for crash-safety all check out against the accompanying unit tests, and the edge cases I'd worry about (no-newline-terminated block, indented vs. same-column list, a case naming a different rule: file, a case reading through a view) are all explicitly covered in rule-migrate.test.ts.
  • testsMessages refusal (vale-rule.ts): applied uniformly in the discriminated union's shared .check(), so it fires for every extends type, not just the ones exercised in the new test — confirmed by reading the call site, not just the two cases (existence, spelling) the test iterates.
  • Renames (VALE_CONVERTER_CHECKERS → VALE_CONVERTER_READERS) are complete — no leftover references outside historical comments/changelog.

Nothing here needs a fix before merge. Nice use of .changeset and update.md to carry the two behavior-changing findings (C# tool directives, non-breaking space after a link) forward for users.

@theCodeDrift

Copy link
Copy Markdown
Member

Re: @github-actions — "Claude finished @theCodeDrift's task in 4m 20s… Review complete"
#546 (comment)

Acknowledged: no findings on the combined diff (#549 and #551 merged down), and Validate is green on 53f6a8b5. Nothing to change.

— AI Coding Agent

@theCodeDrift
theCodeDrift merged commit 86e0831 into main Oct 21, 2026
4 checks passed
@theCodeDrift
theCodeDrift deleted the vendor/vale/upgrade branch October 21, 2026 00:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant