Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/vale-3-22-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<key>` 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 (`<Note>...</Note>`) 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.
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 0 additions & 2 deletions .taskless/rules/vale/docs-npx-cli/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 0 additions & 6 deletions .taskless/rules/vale/no-blocklist-phrases/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,18 +27,15 @@ 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
# configuration to people adopting these skills elsewhere, so it is read from
# 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
6 changes: 0 additions & 6 deletions .taskless/rules/vale/no-em-dashes/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -30,18 +27,15 @@ 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
# configuration to people adopting these skills elsewhere, so it is read from
# 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
6 changes: 0 additions & 6 deletions .taskless/rules/vale/no-hedging/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -56,13 +52,11 @@ 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
# configuration to people adopting these skills elsewhere, so it is read from
# 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
4 changes: 2 additions & 2 deletions .taskless/taskless.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": 7,
"version": 8,
"install": {
"targets": {
".taskless": {
Expand All @@ -21,7 +21,7 @@
"mode": "reference"
}
},
"cliVersion": "0.11.0",
"cliVersion": "0.11.2",
"onboarded": true
},
"rules": {
Expand Down
1 change: 0 additions & 1 deletion example/.taskless/rules/vale/no-simply/.vale.ini
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,4 @@
# directory. That's the point of the layout.
[*.{html,md}]
tskl) rule = no-simply
BasedOnStyles =
no-simply.no-simply = YES
Original file line number Diff line number Diff line change
@@ -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/<id>/.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.
Original file line number Diff line number Diff line change
@@ -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` | `<id>.yml` against the ast-grep schema and the Taskless required fields |
| `vale` | `<id>.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 `<id>.<id> = 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 `<style>.<check>` key naming a different rule
- **THEN** `verify` SHALL report it, naming the key and the line
- **AND** it SHALL NOT report the rule as valid

#### Scenario: A rule config advisory does not fail verify

- **WHEN** a Vale rule's `.vale.ini` assigns the same key twice inside one matcher, and another matcher still enables the rule
- **THEN** `verify` SHALL report the rule as valid
- **AND** the repeat SHALL be printed as a notice on that rule

#### Scenario: A rule config whose only enable is overridden is rejected

- **WHEN** a Vale rule's `.vale.ini` assigns `<id>.<id> = YES` and then `<id>.<id> = NO` in its only matcher
- **THEN** `verify` SHALL report that the rule is present but off, under `vale-config-enabled-somewhere`
- **AND** the repeat SHALL still be printed as a notice on that rule
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
## ADDED Requirements

### Requirement: Migration 8 drops BasedOnStyles from Vale rule configs

Migration `8` SHALL delete every `BasedOnStyles` assignment line from each `.taskless/rules/vale/<id>/.vale.ini`, whatever the value, and SHALL change nothing else in the file: every other line, comment, blank line and line ending SHALL be preserved byte for byte, and a comment that mentions `BasedOnStyles` SHALL be left alone. A config without the line SHALL NOT be rewritten. A project with no `rules/vale/` tree SHALL be left as it is. It SHALL NOT modify any shipped migration.

The migration exists because the `create-vale-rule` recipe wrote `BasedOnStyles =` into every matcher through the release before this one, the config schema now rejects the key, and `check` and `verify` refuse to run on a scaffold behind the current version. Without the rewrite every upgraded project's first `check` would refuse the Vale engine over a line the CLI itself wrote.

#### Scenario: An existing scaffold loses the line from every matcher

- **WHEN** `taskless.json` records version 7, a rule's `.vale.ini` carries `BasedOnStyles =` in three matchers, and the CLI bootstraps `.taskless/`
- **THEN** migration 8 SHALL run
- **AND** the config SHALL contain no `BasedOnStyles` line and every other byte unchanged
- **AND** the rewritten config SHALL pass the config schema
- **AND** `taskless.json` SHALL record the latest schema version

#### Scenario: A config without the line is untouched

- **WHEN** a rule's `.vale.ini` carries no `BasedOnStyles` line and migration 8 runs
- **THEN** the file SHALL NOT be written

#### Scenario: A comment naming the key survives

- **WHEN** a rule's `.vale.ini` carries a comment line that mentions `BasedOnStyles` and migration 8 runs
- **THEN** the comment SHALL remain and only assignment lines SHALL be removed

#### Scenario: Migration 8 is idempotent

- **WHEN** migration 8 runs twice over the same scaffold
- **THEN** the second run SHALL change nothing
Loading
Loading