Skip to content

feat(vale): Vale 3.22.0 — reject BasedOnStyles in rule configs, migrate installed rules, pin the contract - #372

Merged
theCodeDrift merged 5 commits into
vendor/vale/upgradefrom
vendor/vale/3-22-0-contract
Sep 21, 2026
Merged

theCodeDrift merged 5 commits into
vendor/vale/upgradefrom
vendor/vale/3-22-0-contract

Conversation

@theCodeDrift

@theCodeDrift theCodeDrift commented Sep 21, 2026 •

Copy link
Copy Markdown
Member

Stack (root → tip):

#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 Author

@claude /review

@claude

claude Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Claude finished @theCodeDrift's task in 3m 31s —— View job


Review complete

Review mode: incremental — read 1 prior review thread(s) before reviewing. The .prior-review.json snapshot contains no review threads, no reviews, and its only "comment" is the trigger comment itself (@claude /review). There is nothing previously raised to classify as addressed or still-open, so this is effectively the first review of this PR.

  • Read .prior-review.json
  • Gather PR diff via gh pr diff
  • Review schema/rejection logic (vale-config.ts, constraints.ts, capabilities.ts)
  • Review migration 0008 (migrate.ts, 0008-drop-based-on-styles.ts)
  • Review assemble/generator changes (assemble.ts, generate-vale-schema.ts, verify.ts)
  • Review tests (contract test, migration test) and OpenSpec deltas
  • Post findings

Assessment

I found nothing NEW to flag as a correctness, security, or clarity problem. This is a well-scoped, carefully measured change and it holds together:

  • Schema rejection (packages/cli/src/schemas/vale-config.ts:354-364): BASED_ON_STYLES_KEY is now rejected under vale-config-no-based-on-styles for both the empty and named forms, with a message that differs by which case triggered it. Verified assemble.ts:200 calls validateValeRuleConfig before writing, so the rejection is enforced at assembly time too, not just at verify — matching the PR's claim that a rejected config "refuses the Vale run" rather than being silently assembled.
  • Migration 0008 (packages/cli/src/filesystem/migrations/0008-drop-based-on-styles.ts): the line-anchored regex (/^[ \t]*BasedOnStyles[ \t]*=.*(?:\r?\n|$)/gm) correctly handles CRLF, no-trailing-newline, and leaves comments mentioning the key alone — I traced the CRLF case by hand (.* absorbs a bare \r before end-of-line, but \r?\n still matches correctly since the \r? degrades to zero-width) and it produces the exact byte-for-byte result the tests assert. The readdir/readFile error-swallowing (treating any error as "nothing to migrate") matches the existing pattern in 0005-rule-directories.ts's entriesOf, so it's consistent with house style rather than a new gap.
  • Test coverage is thorough: migrate-drop-based-on-styles.test.ts covers idempotency, untouched-file byte comparison (via mtimeMs, not just content), comment preservation, CRLF, and the version-7-scaffold-through-the-runner path that guards against the exact LATEST_SCHEMA_VERSION literal-pinning trap called out in the commit history. vale-vendor-contract.test.ts's new "Vale 3.22.0" block pins the interleaving/silencing behavior against the real binary on the exact layout assembleValeConfig writes (not a synthetic shape), which is the right level for a vendor contract test.
  • OpenSpec deltas: checked both MODIFIED Requirements blocks (cli-vale-rule-engine, cli-rule-validation) against this repo's own documented archive hazard (a MODIFIED block replaces the full requirement, so a delta that silently drops a scenario deletes it from the spec). Diffed the final openspec/specs/*/spec.md changes directly: every hunk is additive (new scenarios/paragraphs), nothing that existed before was removed. The archived change directory tasks/proposal look consistent with the stated scenario counts (33→37, 31→32, 29→33).
  • Docs (create-vale-rule.md, update.md, changeset): consistent with each other and with the code — the recipe drops the four-line template to three, explains the rejection, and the changeset correctly keeps the bump at patch per this repo's 0.y.z convention.

Nothing here needed an inline comment — I didn't find a defect to anchor one to.

@theCodeDrift
theCodeDrift added this pull request to stack #373 September 21, 2026 23:09
@theCodeDrift
theCodeDrift removed this pull request from stack #373 September 21, 2026 23:33
@theCodeDrift
theCodeDrift merged commit b0dd977 into vendor/vale/upgrade Sep 21, 2026
8 checks passed
@theCodeDrift
theCodeDrift deleted the vendor/vale/3-22-0-contract branch September 21, 2026 23:33
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