Skip to content

chore(vale): upgrade to Vale 3.21.0 - #336

Merged
thecodedrift merged 6 commits into
mainfrom
vendor/vale/upgrade
Sep 16, 2026
Merged

thecodedrift merged 6 commits into
mainfrom
vendor/vale/upgrade

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 15, 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.20.0-20260907164938 to
3.21.0-20260915061224 — Vale 3.21.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.

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.


Upstream release notes — 3.21.0

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

doc(...) selections

A scope term that selects document elements by CSS selector, the same way in every format. Headings are wrapped in nested section elements first, so a rule can name a section:

extends: occurrence
message: "An ADR has a Decision section."
level: error
scope: 'doc(section:has(> h2:contains("Decision")))'
token: '(?s).'
min: 1

Alone, the selection is one block. Beside a scope, sentence & doc(...), it narrows that scope to the element. Negated, it excludes it. A metric rule with a scope now measures each block the scope names, so a section can have a word budget.

Docs: Selections, metric

TextFSM Views

A textfsm View reads plain text through a template in the form Google's TextFSM defined, run by Vale itself. What it captures becomes scopes placed at their line and column, so a rule can target a commit's subject or a transcript's turns. A conditional rule's new in key looks for second in another View scope, so a ! in a subject can require a BREAKING CHANGE: trailer.

Docs: TextFSM guide, conditional

Jupyter notebooks

.ipynb is a format: Markdown cells as Markdown, code cells as code in the kernel's language, outputs skipped. Alerts point into the notebook file itself.

Docs: Jupyter

Changelog

  • ed711769 perf: index the walker's context instead of scanning it
  • 252d0963 perf: index rune positions and resume column counts per block
  • 0dfd6e82 chore: accept configs and docstring in vocab
  • 9dfbc902 docs: add GitBook badge
  • 7285105b docs: README
  • 5992ffce docs: update README
  • d0ea8b46 feat: apply BlockIgnores and TokenIgnores to HTML
  • b4e2aa74 fix: keep the source's space between a dash and inline markup (#1177)
  • 4774cedd fix: a sequence rule with a negated scope double-reports every match (#1169)
  • 91af4046 fix: make the CLA bot signable
  • 0f8ed52a feat: lint Jupyter notebooks as a format of their own
  • 4da60aa6 feat: join a selected list into one value, so a notebook cell is one block
  • 683a2f65 feat: let a conditional look for its consequent in another View scope
  • 4c0f64f2 fix: apply a dasel View to any file its section matches
  • 1734e780 feat: give an unfilled textfsm value an empty scope, so min can report it
  • abbf2ec6 test: read past Git's comment lines in the commit View
  • 7ebd026d fix: name the file and both dasel causes when a View selector fails
  • 8aab425a fix: let a raw pattern contain a percent sign
  • 0c35215c fix: place a zero-width match, and report a rule error under a View
  • 1e4f6edc fix: keep a package's StylesPath out of the path list, and let the project's rule settings win
  • c18dcab0 ci: tell the docs about a release
  • e0a2250d feat: validate actions at load, carry their suggestions, and let edit run a pipeline
  • d039c952 feat: read plain text through TextFSM templates as a view engine
  • cfddd62e feat: select document elements with doc(...) and measure any block
  • 42c3ba6f fix: keep a line break after inline markup (#1174)

Contains #339

#336 moves the six @taskless/vale-* pins to Vale 3.21.0. That commit alone does not land green: VALE_VERSION still says 3.20.0, the generated vocabulary was derived against 3.20.0, and four things the binary changed underneath the pins fail the existing suites. This PR is what makes the upgrade correct, measured against both binaries side by side, and it merges down into vendor/vale/upgrade so the upgrade reaches main atomically.

What the pin bump alone left broken

  • VALE_VERSION → 3.21.0; vale-vocabulary.ts re-derived. The vocabulary diff is additive: in joins conditional's fields, doc( joins the scope families. Nothing removed.
  • Findings carry a new Suggestions field (mirrors a replace action's params). toFix keeps reading Action; the contract pins that the two agree.
  • A duplicated [glob] section or repeated key in a rule's .vale.ini now keeps its last assignment (upstream 1e4f6ed). The contract asserted first-wins; it now asserts last-wins in both orders, and assemble.ts says so.
  • Two upstream perf commits made the timeout fixtures ~20× faster, so the timeout test was passing without exercising the timeout. Re-measured and regrown (6.7 MB / 19 MB) to restore the headroom.
  • .ipynb is a format now, so it left the plaintext fallback: new tier row, new fixture, three pins on what the tier means.

Schema gaps the new vocabulary opened

  • Too strict: scope: doc(<selector>) was rejected as an unknown operand. verify now accepts the family, splitting a chain on & the way Vale does (a port of splitOutside) so a selector with & inside quotes is one term. The selector itself is left to Vale, which reports a bad one as E201 at load — the path a malformed regex already takes.
  • Too lax: an unknown action name. 3.20.0 loaded it and died on first match with a path-less E100; 3.21.0 refuses it at load as E201, and one config serves the run, so a typo silences every Vale rule on every check. verify now rejects a name outside replace, remove, suggest, convert, edit.

New contract pins (binary invoked directly)

Every doc(...) shape (chained, negated, standalone container, the inert standalone leaf, E201 for a bad selector, a metric over one section); a metric honoring an ordinary scope; BlockIgnores/TokenIgnores on HTML; unknown action refused at load. The sequence negated-scope double-report fix from the release notes did not reproduce on either binary through the CLI's path; it is pinned at the current count with that said.

Changeset and ledger

.changeset/vale-3-21-0.md is grown in place (still patch). update.md gains a Migrating to 0.11.2 entry because existing rules change behavior: a metric with a scope now measures per block, a bad action fails the run at load, duplicated ini keys flip to last-wins, and [*.ipynb] rules stop linting notebook JSON. create-vale-rule.md documents the doc(...) shapes and the inert-leaf trap.

Stacked on #336 — merges down into vendor/vale/upgrade.

github-actions Bot and others added 6 commits September 16, 2026 10:40
The bot moved the six platform pins; this is what the 3.21.0 binary
changed underneath them, measured against 3.20.0 side by side.

VALE_VERSION moves to 3.21.0 and the vocabulary is re-derived. The diff
is additive: `in` joins `conditional`'s field table, and `doc(` joins the
scope families. Nothing was removed. `in` names a View scope, and Views
live under a directory the rule layout has no home for, so the corpus
records it as a measured member every use of which fails at load.

Four things the pin alone left broken, each read rather than guessed:

- Findings carry a new `Suggestions` field. It mirrors a `replace`
  action's params and is `[]` otherwise; `toFix` keeps reading `Action`
  and the contract pins that the two agree.
- A duplicated `[glob]` section, or a repeated key in one, now keeps its
  LAST assignment (upstream 1e4f6ed, `ValueWithShadows`). The contract
  asserted first-wins in both orders; it now asserts last-wins in both,
  and assemble.ts's docstring says so.
- Two perf commits made the timeout fixtures ~20x faster: 152KB ran in
  ~45ms against a 100ms budget and the test reported `ok`. Re-measured
  (linear, ~2.8us per repetition); the fixtures grow to 6.7MB and 19MB
  to restore the headroom the old ones had.
- `.ipynb` is a format now (`internal/lint/notebook.go`), so it left the
  plaintext fallback for `markup`: a Markdown cell as Markdown, a code
  cell as its kernel's comments, raw cells and outputs unread. New tier
  row, new fixture, and three pins on what the tier means.

New contract cases for what a rule author can reach: every `doc(...)`
shape (chained, negated, standalone container, the inert standalone
leaf, the E201 for a bad selector, a metric over one section), a metric
honoring an ordinary scope, BlockIgnores/TokenIgnores on HTML, and an
unknown action name refused at load. The sequence negated-scope fix in
the release notes did not reproduce on either binary through the CLI's
path and is pinned at the current count with that said.
… verify

Two schema gaps the 3.21.0 vocabulary opened, one in each direction.

Too strict: `scope: doc(<selector>)` was rejected as an unknown operand.
The generator records the family as a `doc(` prefix; the schema now
knows the term ends in `)` with something between, and splits a chain
on `&` the way Vale does (a port of `splitOutside`), so a selector
carrying `&` inside quotes is one term rather than two fragments. What
is between the parens is not parsed: Vale compiles every selector at
load and reports a bad one as `E201 invalid selector in 'doc(...)'`,
which `test` surfaces, the path a malformed regex already takes.

Too lax: an unknown `action` name. Measured on 3.20.0 the rule loaded
and the run died only when it fired, as an E100 with no path; 3.21.0
refuses it at load as an E201 naming the file, and one config serves
the run, so a typo silences every Vale rule on every check. The five
accepted names are transcribed from upstream `checkAction` and held to
the binary by corpus rows.

Corpus: five accepted `doc(...)` shapes, the unclosed and empty forms
the schema rejects with the binary, and three recorded divergences
where the schema accepts what Vale does not honor because judging it
would mean parsing CSS against the document (a standalone leaf is
inert; a bad selector is Vale's to report; `in` needs a View the rule
layout cannot supply). Each mutation was run: the plain split fails the
quoted-ampersand row, dropping the paren check fails the empty and
unclosed rows, dropping the action check fails both rejected rows.
The changeset grows in place, for a rule author: what can now be
written (`doc(...)` scopes, a metric that honors its scope, `.ipynb`,
BlockIgnores on HTML) and what an existing rule will observe (a
duplicated matcher keeps its last assignment, an unknown action fails
at load, a scoped metric measures what it names, notebook JSON is no
longer prose). Only what the vendor contract pins end to end is listed;
`in` on `conditional` and TextFSM Views are not, since neither can be
reached through the rule layout.

`update.md` gains the 0.11.2 ledger entry with the same list and what to
do about each, every item measured against the 3.20.0 and 3.21.0
binaries side by side. The sequence negated-scope fix in the release
notes did not reproduce on either binary, and the entry says so the
way the ast-grep root-metavariable case does. `create-vale-rule.md`
adds `doc(...)` to the scope table with the one trap (a leaf on its own
is inert; chain it) and notes that `metric` takes a scope. Both topic
versions bump.
Review on #339: `ACTION_NAMES.includes(name)` compares the value
verbatim, and the only casing row (`action/mixed-case-name-key`) used
an already-invalid value, so it proved the key's case-insensitivity and
nothing about the value's.

Measured on 3.21.0 with a params shape that loads clean under
`replace`: `Replace` and `REPLACE` both draw `E201 unknown action
'Replace'`. The schema was already the binary's answer; the new row
`action/mixed-case-name-value` is what holds it there. Mutation run:
folding the value to lowercase before the compare fails that row as
"Vale rejected, schema accepted". The docstring says which half of the
action map is folded and which is not.
@thecodedrift
thecodedrift merged commit 452e6ec into main Sep 16, 2026
4 checks passed
@thecodedrift
thecodedrift deleted the vendor/vale/upgrade branch September 16, 2026 17:48
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