diff --git a/.changeset/vale-3-22-0.md b/.changeset/vale-3-22-0.md index 92359317..442de221 100644 --- a/.changeset/vale-3-22-0.md +++ b/.changeset/vale-3-22-0.md @@ -3,3 +3,20 @@ --- Update the bundled Vale to 3.22.0. + +For a rule under `.taskless/rules/vale/`, what you can now write: + +- `scope: text & ~link` (and `~strong`, `~emphasis`, `~code`) runs on the paragraph and blanks the element's text out of it before the rule sees it, so a wording or casing rule can leave link text and bold terms alone without giving up the sentence around them. Positions after the blanked element do not move. The release note's `text.raw` and `paragraph.link` spellings are not scopes; `verify` rejects them, so write the bare inline name. +- `split: true` on a `spelling` rule checks the parts of an identifier (`getHTTPResponsze_v2` reports `Responsze`) and places each at its own position. 3.21.0 accepted the key and did neither. +- A `frontmatter` or `frontmatter.` rule reports every occurrence in a field at the field's own position; a token appearing twice in one field was reported once. +- A one-line MDX element's text (`...`) is linted. + +What changes for a rule you already have: + +- **`BasedOnStyles =` is removed from every rule's `.vale.ini`, by migration 0008 on the next `init`, and `verify` now rejects the key with any value (`vale-config-no-based-on-styles`).** Every earlier version of the `create-vale-rule` recipe wrote the line into every matcher, where it was inert: no bundled style loads unless a run-level `BasedOnStyles` names one, and the assembled header names none. On 3.22.0 an empty value clears every setting a file inherited from an earlier matcher, and the assembled run config is every rule's matchers in id order, so a rule writing the line under `[docs/**]` silenced every alphabetically earlier rule under `docs/`, and `[*.md]` beside another rule's `[*.{md,markdown}]` silenced the first on every `.md` file, with nothing reported. `check` and `verify` refuse to run until `init` has migrated the configs, and name it; commit the rewritten files. A migrated config enables exactly what the old one did on 3.21.0. +- A rule whose `scope` negates `link`, `strong`, `emphasis` or `code` no longer sees that element's text inside a paragraph. Through 3.21.0 the paragraph still carried it, so findings inside those elements disappear. Drop the negation if the rule was meant to reach them. +- The isolating config `test` and `verify` build no longer writes `BasedOnStyles =`. Measured on both binaries, nothing fires without it and `Vale.Spelling` never could, so fixtures behave as before. + +Not changed for a rule this CLI assembles: a `[formats]` key may now be a file name or a glob, but no rule config can carry one and the assembled header writes none, so the extension still decides the parser. `UNSET` as a rule's value behaves as `NO` and is not accepted; `YES` and `NO` remain the two values. + +`taskless agent update` carries the same list, with what to do about each. diff --git a/.taskless/rules/vale/comments-record-not-forecast/.vale.ini b/.taskless/rules/vale/comments-record-not-forecast/.vale.ini index 48d29942..5d2d33b1 100644 --- a/.taskless/rules/vale/comments-record-not-forecast/.vale.ini +++ b/.taskless/rules/vale/comments-record-not-forecast/.vale.ini @@ -2,5 +2,4 @@ # code body is invisible, so this rule can never fire on an identifier. [packages/cli/src/**/*.ts] tskl) rule = comments-record-not-forecast -BasedOnStyles = comments-record-not-forecast.comments-record-not-forecast = YES diff --git a/.taskless/rules/vale/docs-npx-cli/.vale.ini b/.taskless/rules/vale/docs-npx-cli/.vale.ini index d1acb470..e1924e65 100644 --- a/.taskless/rules/vale/docs-npx-cli/.vale.ini +++ b/.taskless/rules/vale/docs-npx-cli/.vale.ini @@ -12,11 +12,9 @@ # invocation, and the rule keeps its full reach. [**/README.md] tskl) rule = docs-npx-cli -BasedOnStyles = docs-npx-cli.docs-npx-cli = YES # Test fixtures are inputs to the CLI's own suite, not documentation. [**/test/fixtures/**/README.md] tskl) rule = docs-npx-cli -BasedOnStyles = docs-npx-cli.docs-npx-cli = NO diff --git a/.taskless/rules/vale/no-blocklist-phrases/.vale.ini b/.taskless/rules/vale/no-blocklist-phrases/.vale.ini index 83d3b005..37b7bfce 100644 --- a/.taskless/rules/vale/no-blocklist-phrases/.vale.ini +++ b/.taskless/rules/vale/no-blocklist-phrases/.vale.ini @@ -2,19 +2,16 @@ # is a separate decision with a large remediation attached. [**/README.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = YES # Agent-facing instructions and the house conventions. Read as often as the # READMEs are, by both people and agents, and small enough to keep conforming. [CLAUDE.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = YES [.conventions/*.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = YES # Test fixtures are inputs to the CLI's own suite, not documentation. @@ -30,12 +27,10 @@ no-blocklist-phrases.no-blocklist-phrases = YES # the size of the thing that earned one. [packages/cli/src/agent/*.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = YES [**/test/fixtures/**/README.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = NO # Root prose docs that are not READMEs. `dotagents.md` explains the agent @@ -43,5 +38,4 @@ no-blocklist-phrases.no-blocklist-phrases = NO # outside the team exactly as a README is, and held to the same standard. [dotagents.md] tskl) rule = no-blocklist-phrases -BasedOnStyles = no-blocklist-phrases.no-blocklist-phrases = YES diff --git a/.taskless/rules/vale/no-em-dashes/.vale.ini b/.taskless/rules/vale/no-em-dashes/.vale.ini index 3271b045..e970eeea 100644 --- a/.taskless/rules/vale/no-em-dashes/.vale.ini +++ b/.taskless/rules/vale/no-em-dashes/.vale.ini @@ -2,19 +2,16 @@ # to source comments and openspec is a separate, much larger decision. [**/README.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = YES # Agent-facing instructions and the house conventions. Read as often as the # READMEs are, by both people and agents, and small enough to keep conforming. [CLAUDE.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = YES [.conventions/*.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = YES # Test fixtures are inputs to the CLI's own suite, not documentation. @@ -30,12 +27,10 @@ no-em-dashes.no-em-dashes = YES # the size of the thing that earned one. [packages/cli/src/agent/*.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = YES [**/test/fixtures/**/README.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = NO # Root prose docs that are not READMEs. `dotagents.md` explains the agent @@ -43,5 +38,4 @@ no-em-dashes.no-em-dashes = NO # outside the team exactly as a README is, and held to the same standard. [dotagents.md] tskl) rule = no-em-dashes -BasedOnStyles = no-em-dashes.no-em-dashes = YES diff --git a/.taskless/rules/vale/no-hedging/.vale.ini b/.taskless/rules/vale/no-hedging/.vale.ini index 0d55d3b9..9b5a1ffa 100644 --- a/.taskless/rules/vale/no-hedging/.vale.ini +++ b/.taskless/rules/vale/no-hedging/.vale.ini @@ -2,21 +2,17 @@ # blocks are not prose, so this rule never sees a command or an identifier. [**/README.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = YES [CLAUDE.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = YES [.conventions/*.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = YES [packages/cli/src/agent/*.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = YES @@ -56,7 +52,6 @@ no-hedging.no-hedging = YES [**/test/fixtures/**/README.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = NO # Root prose docs that are not READMEs. `dotagents.md` explains the agent @@ -64,5 +59,4 @@ no-hedging.no-hedging = NO # outside the team exactly as a README is, and held to the same standard. [dotagents.md] tskl) rule = no-hedging -BasedOnStyles = no-hedging.no-hedging = YES diff --git a/.taskless/taskless.json b/.taskless/taskless.json index 72bd9028..b00f7c3b 100644 --- a/.taskless/taskless.json +++ b/.taskless/taskless.json @@ -1,5 +1,5 @@ { - "version": 7, + "version": 8, "install": { "targets": { ".taskless": { @@ -21,7 +21,7 @@ "mode": "reference" } }, - "cliVersion": "0.11.0", + "cliVersion": "0.11.2", "onboarded": true }, "rules": { diff --git a/example/.taskless/rules/vale/no-simply/.vale.ini b/example/.taskless/rules/vale/no-simply/.vale.ini index a123e1f2..a97301bd 100644 --- a/example/.taskless/rules/vale/no-simply/.vale.ini +++ b/example/.taskless/rules/vale/no-simply/.vale.ini @@ -2,5 +2,4 @@ # directory. That's the point of the layout. [*.{html,md}] tskl) rule = no-simply -BasedOnStyles = no-simply.no-simply = YES diff --git a/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/proposal.md b/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/proposal.md new file mode 100644 index 00000000..c5e57d92 --- /dev/null +++ b/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/proposal.md @@ -0,0 +1,40 @@ +## Why + +Vale 3.22.0 gives an empty `BasedOnStyles` a meaning (upstream c2d62437): it clears every setting a file inherited from an earlier matcher. The `create-vale-rule` recipe has told every author to write `BasedOnStyles =` in every matcher since the Vale engine shipped, on the belief that it kept bundled styles from loading, and the assembled run config is every rule's matchers interleaved in id order. Measured on the 3.22.0 binary against the exact layout `assembleValeConfig` writes: a rule writing the line under `[docs/**]` silences every alphabetically earlier rule under `docs/`; `[*.md]` beside another rule's `[*.{md,markdown}]` silences the first on every `.md` file; and a rule's own later `[docs/**]` matcher carrying only the line turns the rule off there. Nothing is reported. Only two rules whose globs are byte-identical escape, because Vale merges those into one section, which is the only reason this repository's own five rules did not notice. On 3.21.0 every one of those configs fired both rules. + +The belief the line rested on was also wrong. Measured on both binaries with a document baited for `Vale.Spelling`, `Vale.Repetition` and `Vale.Terms`: nothing but the rule under test fires when the key is absent, and the control (`BasedOnStyles = Vale`) fires all of them. No bundled style loads unless a run-level `BasedOnStyles` names one, and the assembled header names none. + +## What Changes + +- **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; the id was never published in a release, so the rename is free). The message says what the line does on the pinned Vale and to delete it. +- **Migration 8 deletes the line** from every `.taskless/rules/vale//.vale.ini`, and nothing else: a line edit over the author's own bytes, never a parse and re-serialize, anchored on the key at line start so a comment survives, idempotent, and skipping a file without the line. `check` and `verify` refuse to run on a scaffold behind the current version and name `init`, which applies it; `init`, `demo`, `onboard`, the wizard and rule delivery run migrations directly. +- **The three isolating configs** (`buildIsolatingConfig`, the generator's `probe()`, `runOne` in the schema contract test) stop writing `BasedOnStyles =`, and their docblocks now carry the measurement instead of the belief. +- **Every fixture, corpus config, the demo asset and the recipe template drop the line.** The recipe explains why the key is rejected; the `update` ledger tells a project what `init` rewrites and that `verify` names anything left. +- **The 3.22.0 contract is pinned** in `vale-vendor-contract.test.ts` on the assembled layout: the `BasedOnStyles` table above, `UNSET`, that no bundled style loads without the key, negated inline scopes (`~link`, `~strong`, `~emphasis` blank the element's text out of the block; `text.raw` and `paragraph.link` from the release note are not operands), `[formats]` by file name and glob (and that the schema refuses a `[formats]` section in a rule config), front-matter placement, a rule file's `message` and `description` being the only prose in it, a one-line MDX element, and `split: true`. `VALE_VERSION` moves to 3.22.0 and the vocabulary is regenerated (unchanged beyond the stamp). + +## Delivery shape + +**Single PR**, merging down into the bot's pin-bump branch `vendor/vale/upgrade` (taskless/cli#368). The pin bump alone does not land green (`VALE_VERSION` disagrees with the pins) and, worse, would ship the silencing above to every project with two Vale rules on different globs; the schema rejection alone would turn every project's `check` red until someone deleted the lines by hand. Pin, rejection and migration are correct only together, so they reach `main` atomically through the bot's branch. + +Nothing here is **BREAKING**, and the bump is `patch`. A rule that carried the key is rewritten by the migration to a config that enables exactly what it enabled on 3.21.0, and a rule copied in afterwards with the key fails `verify` naming the line, where on 3.22.0 without this change it would have silenced a neighbour silently. + +## Capabilities + +### New Capabilities + +None. + +### Modified Capabilities + +- `cli-vale-rule-engine`: "Per-rule scoping is expressed in the rule's own Vale config" states that a rule's config carries no `BasedOnStyles` and why, with a scenario for overlapping matchers and one for the refusal; "A rule's Vale config is validated against a schema before it is assembled" moves the key from "must be empty" to "rejected with any value" and adds the `[formats]` scenario. +- `cli-rule-validation`: "Verify checks a rule's required components" gains the rejection scenario. +- `cli-taskless-bootstrap`: a new requirement for migration 8. + +## Impact + +- `packages/cli/src/schemas/vale-config.ts`, `src/rules/constraints.ts`, `assets/reference.json` (regenerated). +- `packages/cli/src/filesystem/migrations/0008-drop-based-on-styles.ts` (new), registered in `migrate.ts`; `LATEST_SCHEMA_VERSION` becomes 8 and this repository's own `.taskless/` is migrated and committed. +- `packages/cli/src/rules/vale/verify.ts`, `scripts/generate-vale-schema.ts`, `test/vale-schema-contract.test.ts`: isolating configs. +- `packages/cli/src/rules/capabilities.ts` (`VALE_VERSION`, the bump note in the tier table), `src/generated/vale-vocabulary*` (regenerated), `src/rules/assemble.ts` (docblock). +- `packages/cli/src/agent/create-vale-rule.md` (topic v11), `src/agent/update.md` (topic v9), `.changeset/vale-3-22-0.md` (grown in place). +- Tests: the schema fixture set, the constraint table, migration tests, the corpus, the vendor contract's new `Vale 3.22.0` block, and every inline config in the suite that carried the line. diff --git a/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/specs/cli-rule-validation/spec.md b/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/specs/cli-rule-validation/spec.md new file mode 100644 index 00000000..360b47a9 --- /dev/null +++ b/openspec/changes/archive/2026-09-21-vale-3-22-basedonstyles/specs/cli-rule-validation/spec.md @@ -0,0 +1,76 @@ +## MODIFIED Requirements + +### Requirement: Verify checks a rule's required components + +`verify` SHALL check that a rule has the components its engine requires and that they are well formed, and SHALL NOT require fixtures or test cases to exist. + +The two commands split because they have different preconditions. An agent part-way through authoring has a rule and no fixtures yet, and needs to know the rule itself is valid before it can write a meaningful test for it. + +Per engine, `verify` SHALL check: + +| Engine | Components | +| --------- | ------------------------------------------------------------------------------------------------------------------------------ | +| `sg` | `.yml` against the ast-grep schema and the Taskless required fields | +| `vale` | `.yml` against the Vale rule schema and the Taskless required fields, and the rule's `.vale.ini` against the config schema | +| `runtime` | `check.ts` present, and at least one capture rule under `captures/` | + +The `vale` row previously read "against Vale's own validation." Measured against the pinned 3.18.0 binary, that covers less than it claims: `level: bananas` is reported, while `extends: nonsense` and `scope: fenced` both verify clean and produce a rule that matches nothing. Vale validates a rule when it _runs_ one, and it runs one field at a time — so a name it does not recognize is not an error, it is a check that never fires. Schema validation is therefore its own layer for `vale`, as it already is for `sg`. + +#### Scenario: A rule with no fixtures still verifies + +- **WHEN** `verify` runs against a rule whose fixture buckets are empty or absent +- **THEN** it SHALL report on the rule's components only +- **AND** the absence of fixtures SHALL NOT be a verify failure + +#### Scenario: A malformed rule reports its own error + +- **WHEN** a Vale style declares a `level` outside `suggestion`/`warning`/`error` +- **THEN** `verify` SHALL report that error, naming the field + +#### Scenario: An unrecognized extension point is rejected + +- **WHEN** a Vale style declares an `extends` that is not one of Vale's check types +- **THEN** `verify` SHALL report it, naming the field and the accepted values +- **AND** it SHALL NOT report the rule as valid + +#### Scenario: An unrecognized scope is rejected + +- **WHEN** a Vale style declares a `scope` that is not one of Vale's scope values +- **THEN** `verify` SHALL report it, naming the field +- **AND** a scope using the `~` negation or `&` chaining syntax over recognized values SHALL be accepted + +#### Scenario: A field belonging to another check type is rejected + +- **WHEN** a Vale style declares a field its `extends` does not accept, such as `tokens` on an `occurrence` check +- **THEN** `verify` SHALL report it before Vale is invoked + +The failure it prevents is not a local one: Vale reports this as `E201: has invalid keys` and reads one assembled config per run, so a single rule with a stray field suppresses every other Vale rule's findings. + +#### Scenario: A rule config that never enables the rule is rejected + +- **WHEN** a Vale rule's `.vale.ini` declares matchers but no `. = YES` +- **THEN** `verify` SHALL report that the rule is present but off, naming the file + +#### Scenario: A rule config that carries BasedOnStyles is rejected + +- **WHEN** a Vale rule's `.vale.ini` assigns `BasedOnStyles` in any matcher, with any value +- **THEN** `verify` SHALL report it under `vale-config-no-based-on-styles`, naming the line +- **AND** it SHALL NOT report the rule as valid + +#### Scenario: A rule config that assigns a foreign key is rejected + +- **WHEN** a Vale rule's `.vale.ini` assigns a `