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
21 changes: 21 additions & 0 deletions .changeset/vale-3-21-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@taskless/cli": patch
---

Update the bundled Vale to 3.21.0.

For a rule under `.taskless/rules/vale/`, what you can now write:

- `scope: doc(<selector>)` picks part of a document by CSS selector, where a heading and everything under it is a `section`: `text & doc(section:has(> h2:contains("Decision")))` is one section's prose, `~doc(...)` is everything outside it, and a `metric` scoped to `doc(...)` puts a word budget on that section alone. A leaf element on its own (`doc(h2)`) is inert; chain it (`text & doc(h2)`). `verify` accepts the family and leaves the selector to Vale, which rejects one it cannot compile at load.
- A `metric` honors its `scope`: `scope: sentence` measures each sentence rather than the whole document. An absent scope, or `scope: text`, still measures the document.
- `.ipynb` is a format: Markdown cells are read as Markdown, code cells as their kernel's comments, raw cells and outputs not at all. Findings point at the notebook file's own lines.
- `BlockIgnores` and `TokenIgnores` in a rule's `.vale.ini` now apply under an `[*.html]` matcher.

What changes for a rule you already have:

- A `[glob]` section repeated in a rule's `.vale.ini`, or a key repeated inside one, now keeps its last assignment rather than its first. Precedence is last-wins in both directions, so a disable placed after the enable it narrows works in every shape.
- An unknown `action` name is refused when the rule loads, and one config serves the whole run, so a typo there fails every Vale rule in the project. `verify` now rejects a name outside `replace`, `remove`, `suggest`, `convert`, `edit`.
- A `metric` that declared a `scope` was measuring the whole document anyway; it now measures what the scope names, so its findings move.
- A rule matching `[*.ipynb]` was linting the notebook's JSON; it now reads cells, so findings from outputs and metadata are gone.

`taskless agent update` carries the same list, with what to do about each.
12 changes: 6 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,11 @@
"@ast-grep/cli-win32-arm64-msvc": "0.45.3",
"@ast-grep/cli-win32-ia32-msvc": "0.45.3",
"@ast-grep/cli-win32-x64-msvc": "0.45.3",
"@taskless/vale-darwin-arm64": "3.20.0-20260907164938",
"@taskless/vale-darwin-x64": "3.20.0-20260907164938",
"@taskless/vale-linux-arm64": "3.20.0-20260907164938",
"@taskless/vale-linux-x64": "3.20.0-20260907164938",
"@taskless/vale-win32-arm64": "3.20.0-20260907164938",
"@taskless/vale-win32-x64": "3.20.0-20260907164938"
"@taskless/vale-darwin-arm64": "3.21.0-20260915061224",
"@taskless/vale-darwin-x64": "3.21.0-20260915061224",
"@taskless/vale-linux-arm64": "3.21.0-20260915061224",
"@taskless/vale-linux-x64": "3.21.0-20260915061224",
"@taskless/vale-win32-arm64": "3.21.0-20260915061224",
"@taskless/vale-win32-x64": "3.21.0-20260915061224"
}
}
20 changes: 20 additions & 0 deletions packages/cli/scripts/generate-vale-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,10 @@ const FIELD_CANDIDATES: readonly string[] = [
"chars",
"pattern",
"tag",
// New in Vale 3.21.0: `conditional` looks for its consequent in another
// View's scope. Offered to every check, like every other candidate, so the
// partition records which checks own it rather than assuming one does.
"in",
].toSorted();

/**
Expand Down Expand Up @@ -1022,6 +1026,22 @@ const SCOPE_CANDIDATES: readonly ScopeCandidate[] = [
documented: true,
note: "the same operand in the TypeScript tier.",
},
{
operand: "doc(section)",
fixture: "# Do bogus things\n\nFine.\n",
ext: "md",
documented: true,
prefix: "doc(",
note:
"new in Vale 3.21.0: a CSS selector over the document's elements. The " +
"tail is a selector, so this is a family. The schema owns the closing " +
"paren; the selector's syntax is Vale's to reject (E201 at load). " +
"Probed over `section` (a heading plus what follows it) rather than " +
"`h1` because a standalone term lints what is INSIDE the element as " +
"one block, and a leaf element has nothing inside it: `doc(h1)` alone " +
"is measured silent where `text & doc(h1)` fires. `test/vale-corpus.ts` " +
"carries both shapes.",
},
{
operand: "fenced",
fixture: "Prose bogus.\n\n```\nbogus fenced\n```\n",
Expand Down
31 changes: 29 additions & 2 deletions packages/cli/src/agent/create-vale-rule.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v7)
# Topic: create-vale-rule (CLI v%(CLI_VERSION)s / topic v8)

## You are here
This is `create-vale-rule`. It helps you write a Vale rule: a check over
Expand Down Expand Up @@ -82,7 +82,7 @@ it.
| a word repeated back to back | `repetition` |
| picking one of two acceptable spellings, consistently | `consistency` |
| "if X appears, Y must also appear" | `conditional` |
| a document-level length or ratio threshold | `metric` |
| a length or ratio threshold, over the document or one `scope` of it | `metric` |
| a readability grade, against a named formula | `readability` |
| a misspelling, against a dictionary | `spelling` |
| phrases that must appear in a fixed order | `sequence` |
Expand Down Expand Up @@ -185,6 +185,33 @@ it.
| `comment` | every comment, in a comment-tier format |
| `comment.line` | `//`-style comments |
| `comment.block` | `/* … */`-style comments |
| `doc(<selector>)` | elements matched by a CSS selector; see below |

**`doc(<selector>)` picks part of a document by CSS selector**, the
same way in every markup format, and a heading with everything under
it is a `section`, so one section of a document is
`doc(section:has(> h2:contains("Decision")))`. Chain it to narrow an
ordinary scope to that element: `text & doc(...)` is prose inside it,
`sentence & doc(...)` one sentence at a time inside it, `~doc(...)` is
everything outside it. On its own, `doc(...)` lints what is INSIDE the
element as one block, which is what `occurrence` (a section must say
"we will") and `metric` (a section runs over budget) want. Measured: a
`metric` with `scope: doc(section:has(> h2:contains("Consequences")))`
and `formula: words` counts that section's words, not the document's.

**A leaf element on its own is inert.** `doc(h2)` alone selects a
heading, and a heading has nothing inside it to lint as a block, so the
rule matches nothing, with no error anywhere. Write `text & doc(h2)`
for the heading's own text. The same holds for `doc(p)` and `doc(li)`.
`verify` accepts both spellings, because telling a leaf from a container
needs the document; `test` shows which one fires.

**The selector is Vale's to check, not `verify`'s.** A selector Vale
cannot compile (`doc(h2[)`) fails the whole run at load with
`E201 invalid selector in 'doc(...)'`, which `test` reports. A selector
that compiles and matches nothing is silent, like any scope with no
construct to find. `verify` checks that the term is `doc(` … `)` with
something between, and no more.

**`raw` subsumes `code` and `text`.** Measured on one document holding
the token in prose, in an inline span, and in a fenced block: `text`
Expand Down
68 changes: 67 additions & 1 deletion packages/cli/src/agent/update.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Topic: update (CLI v%(CLI_VERSION)s / topic v5)
# Topic: update (CLI v%(CLI_VERSION)s / topic v6)

## You are here
This is `update`. It tells you what an upgrade changed for the rules
Expand Down Expand Up @@ -227,6 +227,72 @@ leaked text into people's files.
(ast-grep/ast-grep#2868), but no shape we tried reproduced a difference,
including the TSX case that PR names. Nothing to do unless you see one.

### Migrating to 0.11.2

Vale moves from 3.20.0 to 3.21.0, and ast-grep from 0.45.2 to 0.45.3.
Nothing installed migrates for ast-grep: its one user-visible change is
that an inline `ast-grep-ignore` comment takes effect only as the
comment's first alphabetic text, which the 0.45.3 changeset describes
and no rule file carries. Six things follow for existing Vale rules,
every one measured against both binaries; the last is behaviour a rule
can now use, not a change to one it has.

**A duplicated matcher now keeps its LAST assignment.** Where a rule's
`.vale.ini` repeats a `[glob]` section, or repeats a key inside one,
3.20.0 kept the first value and 3.21.0 keeps the last (upstream 1e4f6ed,
"let the project's rule settings win"). Precedence across different
matchers was already last-wins, so the two directions now agree, and the
guidance to put a disable AFTER the enable it narrows is right for both
shapes. A rule that relied on the old order is one whose second
assignment was being ignored; it now takes effect, and the rule turns on
or off where it did not before. `git grep -c '^\[' .taskless/rules/vale`
finds a config with more than one section per rule to read.

**A `metric` rule's `scope` is honored.** Through 3.20.0 a `metric`
measured the whole document whatever its `scope` said; 3.21.0 measures
the blocks the scope names, and only an absent scope, or `scope: text`,
still means the document. A `metric` with `scope: sentence` or
`scope: paragraph` now reports per block, so its findings move, appear,
or disappear. If the document-wide reading was what you wanted, delete
the `scope`.

**Notebooks are read cell by cell.** `.ipynb` had no parser, so a rule
matching `[*.ipynb]` was linting the notebook's JSON: cell source,
outputs, and metadata alike. 3.21.0 reads a Markdown cell as Markdown
and a code cell as its kernel's comments, and reads raw cells and
outputs not at all. Findings drop, and `Line` now points into the
notebook file. Nothing warns; a suspiciously clean run over notebooks is
this.

**An unknown `action` name fails the run at load.** Measured on
3.20.0, a rule carrying `action: {name: bogus}` loaded, and the run
died only when that rule fired, as an `E100` with no file and no line.
3.21.0 refuses it when the rule loads, as an `E201` naming the file, so
a rule that has been carrying a bad action without ever matching now
fails every check, not just the ones where it matched. `verify` rejects
a name outside `replace`, `remove`, `suggest`, `convert`, `edit`, so run
it: the rule is named directly.

**`BlockIgnores` and `TokenIgnores` apply to HTML.** A rule's
`.vale.ini` carrying either key under an `[*.html]` matcher was ignored
through 3.20.0 and takes effect on 3.21.0. Findings inside the ignored
region disappear.

Upstream also says a `sequence` rule with a negated scope (`~list`,
`~code`) reported every match twice (vale-cli/vale#1169). No shape we
tried reproduced a difference between the two binaries, including the
one that PR's own test uses, so it is recorded the way the ast-grep
root-metavariable case is: nothing to do unless you see one.

**`scope: doc(<selector>)` selects part of a document by CSS selector**,
which a rule could not do before: `text & doc(section:has(> h2:contains("Decision")))`
is the prose of one section, `~doc(...)` is everything outside it, and
a `metric` scoped to `doc(...)` puts a word budget on that section
alone. `%(TASKLESS_CLI)s agent create-vale-rule` has the shapes and the
one trap (a leaf element on its own, `doc(h2)`, is inert; chain it).
No existing rule changes; this is a reason to revisit one that was
narrowed by hand.

## Errors

With `--json`, `--rules` failures emit `{ ok: false, code, message }`:
Expand Down
15 changes: 8 additions & 7 deletions packages/cli/src/generated/vale-vocabulary-report.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Vale 3.20.0 vocabulary: divergence report
# Vale 3.21.0 vocabulary: divergence report

GENERATED FILE — DO NOT EDIT. Produced by `pnpm generate:vale-schema`
alongside `vale-vocabulary.ts`.

Every value in the vocabulary is the recorded answer of the vendored Vale
3.20.0 binary. This file is what the binary said that its own
3.21.0 binary. This file is what the binary said that its own
documentation does not, in both directions. A generator that dropped these
would be quietly deciding which of the two to believe.

Expand All @@ -28,25 +28,25 @@ the worse failure.

### `scope: meta`

Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.21.0 documents this operand and it never fired, on any fixture probed (.md).

**Consequence.** It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.

### `scope: meta.class.title`

Vale 3.20.0 documents this operand and it never fired, on any fixture probed (.md).
Vale 3.21.0 documents this operand and it never fired, on any fixture probed (.md).

**Consequence.** It is omitted from the vocabulary, so `verify` rejects it. A rule written from the documentation would otherwise load, run, and match nothing, with no error reported anywhere.

### `scope: frontmatter`

This operand fired and Vale 3.20.0 documents it nowhere.
This operand fired and Vale 3.21.0 documents it nowhere.

**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.

### `scope: frontmatter.title`

This operand fired and Vale 3.20.0 documents it nowhere.
This operand fired and Vale 3.21.0 documents it nowhere.

**Consequence.** It is included in the vocabulary. It is also the standing counterexample to trusting the candidate list: a real operand nobody proposes is simply absent, and the schema then rejects a rule the binary honors.

Expand All @@ -64,7 +64,7 @@ This check accepted 'taskless_generator_sentinel', a key no check has. It does n

### `field probes: membership inferred from a type complaint`

10 probes drew an E201 that was not an invalid-key list: capitalization.action: expected a map, got 'bool'; conditional.action: expected a map, got 'bool'; existence.action: expected a map, got 'bool'; metric.action: expected a map, got 'bool'; occurrence.action: expected a map, got 'bool'; readability.action: expected a map, got 'bool'; repetition.action: expected a map, got 'bool'; script.action: expected a map, got 'bool'; sequence.action: expected a map, got 'bool'; substitution.action: expected a map, got 'bool'.
11 probes drew an E201 that was not an invalid-key list: capitalization.action: expected a map, got 'bool'; conditional.action: expected a map, got 'bool'; conditional.in: no View defines a scope named '1'; existence.action: expected a map, got 'bool'; metric.action: expected a map, got 'bool'; occurrence.action: expected a map, got 'bool'; readability.action: expected a map, got 'bool'; repetition.action: expected a map, got 'bool'; script.action: expected a map, got 'bool'; sequence.action: expected a map, got 'bool'; substitution.action: expected a map, got 'bool'.

**Consequence.** Each is recorded as a member: Vale recognized the key and objected to the probe's arbitrary value instead, which is membership evidence. They are listed so the inference is auditable rather than assumed.

Expand Down Expand Up @@ -108,5 +108,6 @@ This check accepted 'taskless_generator_sentinel', a key no check has. It does n
| `comment.block` | `.js` | yes | fires |
| `comment.line` | `.ts` | yes | fires |
| `comment.block` | `.ts` | yes | fires |
| `doc(section)` | `.md` | yes | fires |
| `fenced` | `.md` | no | silent |
| `banana` | `.md` | no | silent |
Loading