Skip to content

chore(vale): upgrade to Vale 3.22.0 - #368

Merged
theCodeDrift merged 6 commits into
mainfrom
vendor/vale/upgrade
Sep 22, 2026
Merged

theCodeDrift merged 6 commits into
mainfrom
vendor/vale/upgrade

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 21, 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.21.0-20260915061224 to
3.22.0-20260921180930 — Vale 3.22.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.22.0

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

Spelling

  • split: true checks the parts of an identifier (getHTTPResponse_v2 → get, Response) and reports each at its own position.
  • The engine now follows Hunspell's case model, compounding, and affix rules, and passes Hunspell's own test suite.
  • Suggestions come in Hunspell's order, and words from ignore files and vocabularies are offered ahead of dictionary words.
  • A loaded dictionary is shared across spelling rules.

Vocabularies

  • A section can set its own Vocab, which adds to the global one and gets Vale.<Name>.Terms and Vale.<Name>.Avoid rules.
  • A period in an entry is text, not a wildcard, and a phrase matches across a line wrap.
  • A Vale.Terms alert offers the entry's spelling as a fix.

Configuration

  • An empty BasedOnStyles clears a section's inherited settings; UNSET clears one rule or style.
  • A [formats] key can be a file name or a glob, not only an extension.
  • A scope can negate an inline element (text.raw, paragraph.link, and so on).
  • TokenIgnores and BlockIgnores apply to HTML and DITA.

Formats

  • MDX: indented blocks are never code, an expression followed by prose keeps the prose, one-line elements are linted, and alerts no longer land inside JSX tags.
  • DITA: all files in a run are converted in one toolkit call.
  • Jupyter notebooks are a format of their own.
  • Front-matter alerts are placed by the field's position.
  • URLs in code comments and RST are treated consistently.

Changelog

  • e109c062 fix: keep a URL out of prose in code comments and RST
  • 79a48752 feat: let a scope negate an inline element
  • cf161663 perf: convert a run's DITA files in one toolkit run
  • 68e022b4 feat: apply ignore patterns to DITA
  • f41bebb6 fix: cover a promoted RST title with the comment region written above it
  • 18dd8632 fix: apply a vale-off region to a footnote written inside it
  • 9bcbf9f9 test: pin a vale-off region after front matter and import lines (#1073)
  • 344fd9b0 fix: locate a block's later matches in that block, not in a later copy of its text
  • ec7e5f01 feat: match a vocabulary phrase across a line wrap, read a period as text, and offer the term as a fix
  • 0194fbbd feat: suggest from the project's own words
  • 49422de5 fix(mdx): lint a one-line element's text as prose, and keep alerts out of JSX tags
  • 5ab91a5d fix: place front-matter alerts by the field's position, not by searching the file
  • dcf7f18c fix(mdx): end an expression at its brace, keep prose after it, and parse indented blocks
  • 8a9fd38e fix: keep the whitespace before an inline comment, so the words around it stay apart
  • 7c61ce4f perf: bound the inline-markup lookahead by byte length, not a rune count of the whole context
  • 1e730195 fix: read a notebook cell's source with a tagless switch,
  • 9e661060 feat: let a section name its own vocabularies
  • fc513c5f perf: share a loaded dictionary across spelling rules, and take the regexp2 fold fix
  • f2785853 feat: lint a Vale rule by its message and description, not its patterns
  • 85992f2a feat: let a spelling rule split identifiers and check their parts
  • cb5885f4 feat: suggest in Hunspell's order, and score suggestions against its fixtures
  • 52094c78 fix: read affix headers with trailing fields, escaped slashes, any-script digits, and Turkic casing
  • 17417ac4 feat: honor IGNORE, convert input by the longest ICONV rule inside spell, and read ß as SS in all-caps under CHECKSHARPS
  • a4e2853d fix: read a COMPOUNDRULE as flags and operators, quote its words, and match a capitalized word lower-cased
  • bfd0b420 perf: resolve affixed words lazily from roots, and read dictionaries as Hunspell does: per SET, byte flags without FLAG, literal hyphens in classes
  • 06325d94 feat: adopt Hunspell's case model, and keep ignore-list words case-lenient
  • 54520592 feat: pass Hunspell's German compounding fixtures: CHECKCOMPOUNDREP, COMPOUNDWORDMAX, prefix strips, and circumfix pairing across levels
  • a770129c feat: apply CHECKCOMPOUNDPATTERN, with replacements, and keep an ONLYINCOMPOUND suffix off a compound's end
  • f1435080 feat: check compound position flags, permit and forbid flags, ONLYINCOMPOUND, FORCEUCASE, and word pairs
  • f9178fb7 feat: check compound boundaries, apply Hunspell's default BREAK rules, and forbid words per homonym
  • 25d8fbc7 test: run Hunspell's own fixtures as a conformance suite
  • c2d62437 feat: let an empty BasedOnStyles reset a file's inherited rule settings, and add UNSET
  • 6c2d99d9 feat: let a formats key name a file or a glob, not only an extension
  • 62d47db9 fix: accept the upper-cased form of a capitalized entry, and suggest in the word's case
  • f8ded8dd feat: let a caller observe each block and time each rule
  • cd86a3c6 perf: segment a file only when a sentence-scoped rule runs on it
  • cf649e6e chore: add commit msg hook
  • 2c3aa804 fix: hasCaptureGroup logic

Contains #372

#368 moves the six @taskless/vale-* pins to Vale 3.22.0. That commit alone does not land green (VALE_VERSION still says 3.21.0), and it would ship something worse than a red test: on 3.22.0 the BasedOnStyles = line every rule's .vale.ini carries, because the recipe told every author to write it, silences other rules. This PR is what makes the upgrade correct, measured against both binaries side by side, and it merges down into vendor/vale/upgrade so pin, schema rejection and migration reach main together.

What

  • The config schema rejects BasedOnStyles in a rule's config with any value, under vale-config-no-based-on-styles (renamed from vale-config-based-on-styles-empty, which accepted the empty form and was never in a release). The message for the empty form says what 3.22.0 does with it and to delete the line.
  • Migration 0008 deletes the line from every .taskless/rules/vale/<id>/.vale.ini and touches no other byte (a line edit anchored on the key, so a comment naming it survives; idempotent; a file without the line is not written). This repository's own five rules are migrated here by pnpm cli init.
  • The three isolating configs (buildIsolatingConfig, the generator's probe(), runOne in the schema contract test) stop writing the line. Their docblocks said it kept Vale.Spelling from firing on a fixture; measured, it never did (see the table).
  • Every fixture, corpus config, the demo asset, the example/ config and the recipe template drop the line.
  • VALE_VERSION → 3.22.0; the vocabulary re-derived against 3.22.0 changes only in its stamp; the tier table's docblock records the source check (no internal/lint/<format>.go added, so no tier moved).
  • A Vale 3.22.0 block in vale-vendor-contract.test.ts, one describe per release-note item, measured on the exact layout assembleValeConfig writes.

Measured

Both binaries, invoked directly. The C1 rows use two rules a-rule/b-rule on the assembled layout (StylesPath = rules/vale, breadcrumbs, id order), a fixture reading "Just alpha and bravo here." at doc.md, docs/inner.md, doc.markdown.

Case 3.21.0 3.22.0
a [*.md], b [*.md], both BasedOnStyles = both fire everywhere both fire everywhere (identical headers merge into one section)
a [*.md], b [*.{md,markdown}], both BasedOnStyles = both on every .md a silenced on every .md
a [*.md], b [**/*.md], both BasedOnStyles = both b only
a [*.md], b [docs/**], both BasedOnStyles = docs/inner.md: a+b docs/inner.md: b only
ids swapped: a [docs/**] first, b [*.md], both BasedOnStyles = docs/inner.md: a+b docs/inner.md: b only (the later section wins, so id order decides who survives)
a [*.md], b [docs/**], neither writes the key a+b a+b
single rule: [*.md] YES, then [docs/**] with only BasedOnStyles = fires under docs/ off under docs/
a and b [*.md], then a [docs/**] NO carrying BasedOnStyles = docs/: b docs/: nothing
[docs/**] a-rule.a-rule = UNSET docs/: b (any non-YES reads as off) docs/: b
[docs/**] a-rule = UNSET (style level) ignored, a+b ignored, a+b
no BasedOnStyles anywhere, document baited for Vale.Spelling/Repetition/Terms only the rule under test only the rule under test; BasedOnStyles = Vale control fires all of them
scope: ~link over prose with a link 5 findings (link text still in the paragraph) 4 (link text blanked; spans after it unchanged)
~strong & ~emphasis 5 3
~code 5 5 (inline code was never prose)
text.raw, paragraph.link (release-note spellings) bare: inert; negated: subtracts nothing same; the schema keeps rejecting them
[formats] NOTES = md / *.special = md, fixture with a fenced block accepted, ignored: fence fires (plain text) routed to Markdown: fence skipped
frontmatter rule, token twice in one field first occurrence only both, at the field's own span
a rule .yml as a lint target, bait in every field 6 findings (message, description, link, tokens, exceptions) 2 (message, description)
MDX one-line element <Note>bogus</Note> not linted linted
split: true spelling on getHTTPResponsze_v2 and recieveData recieveData whole; the identifier missed Responsze and recieve, each at its own span
notebook cells same same
timeout fixtures (80k / 350k / 1M repetitions) ~233 / ~935 / ~3130 ms ~240 / ~980 / ~2970 ms (fixtures unchanged)

This repository's own five rules use byte-identical globs across rules, so 3.21.0 and 3.22.0 agreed on its assembled config. That is a coincidence, not a defence.

Migration

0008-drop-based-on-styles.ts, LATEST_SCHEMA_VERSION 7 → 8. Which commands run migrations: init, demo, onboard, the bare-invocation wizard, and rule delivery (writeRule/ingest paths in rules/files.ts) call ensureTasklessDirectory and migrate. check and verify call requireCurrentSchema and refuse a scaffold behind the CLI with SCAFFOLD_MIGRATION_REQUIRED, naming init. So an upgraded project's first check says "run init", init deletes the lines, and check runs; a project already at 8 is untouched. The 0007 test pinned manifest.version to 7 after a runner pass and broke the day 8 existed; both it and the new 0008 test assert LATEST_SCHEMA_VERSION now.

Ledger

update.md gets a 3.22.0 addition to the existing "Migrating to 0.11.3" entry (topic v9), because two installed behaviours change: BasedOnStyles = is deleted by init and rejected by verify afterwards, and a rule whose scope negates link/strong/emphasis/code no longer sees that element's text. The additions ([formats], front matter, rule-file linting, MDX, split, UNSET) are listed with what they touch. create-vale-rule.md (topic v11) drops the line from its template, explains the rejection, and documents inline-element negation. .changeset/vale-3-22-0.md grown in place, still patch.

OpenSpec

vale-3-22-basedonstyles, single PR, archived here. Dry-run counted: cli-vale-rule-engine 33 → 37 scenarios, cli-rule-validation 31 → 32, cli-taskless-bootstrap 29 → 33 (9 → 10 requirements), nothing dropped.

Not pinned

  • MDX indented blocks and "an expression followed by prose": measured the same on both binaries for the shapes tried, so there was nothing to pin.
  • The DITA and Hunspell changes (compounding, affix rules, suggestion order, shared dictionaries): DITA needs a converter the platform packages do not ship, and the spelling engine's internals are not something a rule this CLI assembles can observe beyond split: true.
  • Per-section Vocab and Vale.<Name>.Terms: needs a <StylesPath>/config/vocabularies/ tree the rule layout has no home for, the same reason TextFSM Views were not pinned for 3.21.0.
  • lint.go's walk now skips the StylesPath tree unless a path inside it is named. check already excludes .taskless/ before Vale runs, so nothing this CLI does can observe it.

Stacked on #368, merges down into vendor/vale/upgrade. The bot branch already sits on origin/main (af9d00b → d04b14f), so no rebase was needed and the bot's tip is an ancestor of this branch.

The pin bump in #368 left VALE_VERSION at 3.21.0, which is what
engine-version-consistency fails on. The vocabulary re-derived against
the 3.22.0 binary changes only in its version stamp: no operand, prefix
or divergence moved. The v3.21.0...v3.22.0 tree adds no
internal/lint/<format>.go, so no tier row moves either; the docblock on
the tier table records the check and points at what did change.
…ed rules

Vale 3.22.0 gives an empty BasedOnStyles a meaning (upstream c2d62437):
it clears every setting a file inherited from an earlier matcher. The
assembled run config is every rule's matchers in id order, and the
recipe wrote BasedOnStyles = into every one of them, so on 3.22.0 a rule
under [docs/**] silences every alphabetically earlier rule under docs/,
and [*.md] beside another rule's [*.{md,markdown}] silences the first on
every .md file. Measured on both binaries against the exact layout
assembleValeConfig writes; only byte-identical globs, which Vale merges,
escape, which is why this repository's own five rules did not notice.

The schema now refuses the key with any value under
vale-config-no-based-on-styles (renamed from -based-on-styles-empty,
never released), and migration 0008 deletes the line from every
installed .taskless/rules/vale/*/.vale.ini, touching no other byte.
check and verify already refuse a scaffold behind the current version
and name init, which applies it. This repository's own .taskless/ is
migrated here by pnpm cli init.

The three isolating configs stop writing the line too. Their docblocks
claimed it kept Vale.Spelling from firing on a fixture; measured on both
binaries with a baited document, nothing fires without the key and the
control naming Vale fires all of it. No bundled style loads unless a
run-level BasedOnStyles names one.
A Vale 3.22.0 block in the vendor contract, every case run against
3.21.0 as well with that answer in the comment: the BasedOnStyles table
(identical globs survive, any other overlap silences the earlier rule,
id order decides, a NO matcher carrying the key reaches every rule),
UNSET as a rule value and at the style level, that no bundled style
loads without the key, negated inline scopes blanking the element's text
with positions held, the release note's dotted spellings being inert,
[formats] by file name and glob against a fenced-block discriminator,
front-matter placement reporting every occurrence in a field, a rule
file's message and description being its only prose, a one-line MDX
element, and split: true.

Corpus rows for ~link and ~strong & ~emphasis (accepted; the control
proves the paragraph is still reached) and for text.raw and
paragraph.link (ignored, as the dotted forms the release note uses).
The timeout fixtures re-timed on 3.22.0 land within noise of 3.21.0.
create-vale-rule (topic v11): the config template is three lines, a
paragraph says why BasedOnStyles is rejected with any value, and a
paragraph documents inline-element negation and warns off the release
note's text.raw and paragraph.link spellings. update (topic v9): the
0.11.3 entry gains the migration, the negation change, and the additions
a rule can use. The changeset grows in place, still patch. The OpenSpec
change restates the two modified requirements in full and adds the
migration-8 requirement; archived on this PR.
Dry-run counted first: cli-vale-rule-engine 33 to 37 scenarios,
cli-rule-validation 31 to 32, cli-taskless-bootstrap 29 to 33 and 9 to
10 requirements, nothing dropped.
@theCodeDrift

Copy link
Copy Markdown
Member

@claude /review

@claude

claude Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Claude finished @theCodeDrift's task in 5m 34s —— View job


Review complete

Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json contains only the original @claude /review trigger comment (no reviewThreads, reviews, or prior top-level comments beyond that trigger), so this is the first review of this PR. Everything below is [New].

Todo list

  • Read .prior-review.json (no prior findings — first review)
  • Fetch PR diff via gh pr diff and metadata via gh pr view
  • Review schema/migration changes (vale-config.ts, 0008-drop-based-on-styles.ts)
  • Review assemble/capabilities/constraints/verify changes
  • Review generator + vocabulary regeneration
  • Review test coverage (vendor contract, migration, schema tests) and OpenSpec archive correctness
  • Post final review comment

Summary

This is an unusually disciplined, internally consistent diff: the BasedOnStyles rename/rejection, migration 0008, the three isolating-config docblocks, every fixture, and the OpenSpec deltas all agree with each other, and I could not construct a runtime scenario where they diverge. Specific things I checked and found correct:

  • packages/cli/src/filesystem/migrations/0008-drop-based-on-styles.ts: the anchored regex ^[ \t]*BasedOnStyles[ \t]*=.*(?:\r?\n|$) correctly strips only assignment lines (leading whitespace included), preserves comments that merely mention BasedOnStyles, handles CRLF and a final line with no trailing newline, and is idempotent — all backed by migrate-drop-based-on-styles.test.ts.
  • packages/cli/src/schemas/vale-config.ts: the vale-config-no-based-on-styles rejection fires for both empty and non-empty values with distinct messages, and doesn't disturb the surrounding own-key/verdict logic.
  • Every .vale.ini fixture, corpus config, demo asset, example/ config, and the three isolating-config builders (buildIsolatingConfig, probe(), runOne) had the line removed consistently — a repo-wide grep confirms no leftover BasedOnStyles line survives except in the two fixtures that deliberately test the rejection.
  • The OpenSpec archive is one of the few places this class of PR usually goes wrong (per this repo's own CLAUDE.md warning that a MODIFIED Requirements block silently deletes anything it omits, and that a title rename silently no-ops the delta). I diffed the archived delta's restated requirements against openspec/specs/* and confirmed requirement titles are unchanged and every pre-existing scenario is preserved verbatim, with only the new BasedOnStyles/[formats]/overlap scenarios added.
  • Constraint id rename (vale-config-based-on-styles-empty → vale-config-no-based-on-styles) has no stale references anywhere in the tree (reference.json, constraints.ts, tests, docs all moved together).

[New] One real inconsistency, outside the diff itself:

.changeset/vale-config-schema.md (line 5, not touched by this PR) is still pending release and reads:

"...a key naming another rule, a value other than YES/NO, a non-empty BasedOnStyles, a config with no matcher or no YES..."

That sentence describes the pre-3.22.0 behavior (only a non-empty BasedOnStyles was rejected, under the old id vale-config-based-on-styles-empty). This PR's own changeset (.changeset/vale-3-22-0.md) now correctly says verify "rejects the key with any value." Since neither changeset has been consumed yet (nothing in packages/cli/CHANGELOG.md mentions BasedOnStyles), both will land in the same release and produce contradictory release notes — one bullet saying only a non-empty value is rejected, the other saying any value is. Worth updating the stale sentence in vale-config-schema.md (e.g., drop "non-empty" or fold it into the new changeset) before this ships, so 0.11.3's changelog doesn't ship two disagreeing statements about the same constraint.

No other correctness, security, performance, or test-adequacy issues found. I did not run git (not on the allowlist here); all findings are from gh pr diff/gh pr view plus Read on the working tree, and CI's own checks are left to CI as instructed.

@theCodeDrift
theCodeDrift merged commit a2d18a8 into main Sep 22, 2026
4 checks passed
theCodeDrift added a commit that referenced this pull request Sep 22, 2026
The pin bump in #368 left VALE_VERSION at 3.21.0, which is what
engine-version-consistency fails on. The vocabulary re-derived against
the 3.22.0 binary changes only in its version stamp: no operand, prefix
or divergence moved. The v3.21.0...v3.22.0 tree adds no
internal/lint/<format>.go, so no tier row moves either; the docblock on
the tier table records the check and points at what did change.
@theCodeDrift
theCodeDrift deleted the vendor/vale/upgrade branch September 22, 2026 00:09
@theCodeDrift

Copy link
Copy Markdown
Member

Re: @claude[bot] — "Claude finished @theCodeDrift's task in 5m 34s — Review complete…"
#368 (comment)

Read and verified. The one finding is real: .changeset/vale-config-schema.md still says "a non-empty BasedOnStyles" is rejected, while .changeset/vale-3-22-0.md says any value is, and both are pending for 0.11.3. Not changed here: this PR has merged, and reconciling the two pending changesets is a one-line follow-up on main that is being raised with the maintainer rather than pushed unasked.

— AI Coding Agent

theCodeDrift added a commit that referenced this pull request Sep 22, 2026
…pgrades

sg-detect.cjs --write and vale-upgrade-detect.cjs --write now also rewrite
AST_GREP_VERSION / VALE_VERSION in packages/cli/src/rules/capabilities.ts,
via a new bumpVersionConstant in pin-bump.cjs that anchors on the exact
declaration line, fails unless it is found exactly once, and leaves every
other byte alone. Vale's constant takes the base version the binary
reports, not the stamped npm version.

engine-version-consistency.test.ts holds the constant to the pins, so a
bot commit that moved only the pins failed Validate by construction
(#368). The test stays; the bot commit becomes consistent.
theCodeDrift added a commit that referenced this pull request Sep 22, 2026
…pgrades

sg-detect.cjs --write and vale-upgrade-detect.cjs --write now also rewrite
AST_GREP_VERSION / VALE_VERSION in packages/cli/src/rules/capabilities.ts,
via a new bumpVersionConstant in pin-bump.cjs that anchors on the exact
declaration line, fails unless it is found exactly once, and leaves every
other byte alone. Vale's constant takes the base version the binary
reports, not the stamped npm version.

engine-version-consistency.test.ts holds the constant to the pins, so a
bot commit that moved only the pins failed Validate by construction
(#368). The test stays; the bot commit becomes consistent.
theCodeDrift added a commit that referenced this pull request Sep 22, 2026
The vale-config-schema changeset still described the pre-3.22.0 rule
(a non-empty value is rejected) while vale-3-22-0.md says the key is
rejected outright. Both ship in 0.11.3; make them agree.

Raised in the #368 review.
theCodeDrift added a commit that referenced this pull request Sep 22, 2026
…pgrades

sg-detect.cjs --write and vale-upgrade-detect.cjs --write now also rewrite
AST_GREP_VERSION / VALE_VERSION in packages/cli/src/rules/capabilities.ts,
via a new bumpVersionConstant in pin-bump.cjs that anchors on the exact
declaration line, fails unless it is found exactly once, and leaves every
other byte alone. Vale's constant takes the base version the binary
reports, not the stamped npm version.

engine-version-consistency.test.ts holds the constant to the pins, so a
bot commit that moved only the pins failed Validate by construction
(#368). The test stays; the bot commit becomes consistent.
theCodeDrift added a commit that referenced this pull request Sep 22, 2026
…pgrades

sg-detect.cjs --write and vale-upgrade-detect.cjs --write now also rewrite
AST_GREP_VERSION / VALE_VERSION in packages/cli/src/rules/capabilities.ts,
via a new bumpVersionConstant in pin-bump.cjs that anchors on the exact
declaration line, fails unless it is found exactly once, and leaves every
other byte alone. Vale's constant takes the base version the binary
reports, not the stamped npm version.

engine-version-consistency.test.ts holds the constant to the pins, so a
bot commit that moved only the pins failed Validate by construction
(#368). The test stays; the bot commit becomes consistent.
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