You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The code halves of #360 and #361. The recipe prose lands in a parallel PR; this one pins the underlying Vale behavior on the vendored binary and adds the verify advisory #360 asks for.
Vendor contract pins (packages/cli/test/vale-vendor-contract.test.ts, three new describe blocks under "Vale vendor contract"): how an existence rule's raw list is read, which key nonword governs, and which scopes see YAML front matter.
verify advisory (packages/cli/src/schemas/vale-rule.ts, wired in packages/cli/src/rules/inspect.ts): validateValeRule now returns advisories: string[] beside valid/errors, and verifyOneRule joins them with the config layer's advisories onto the rule's notice, in verify text output and --json.
Changeset.changeset/vale-raw-authoring.md, patch.
The exact advisory:
<id>: raw has N entries; Vale joins them into one pattern with no separator, so the second never matches on its own. Write one entry with (a|b) unless the join is intended.
Measured
Every row below was run on the vendored Vale 3.22.0 before it was written down, and each is now a test.
Two things the issues did not say, worth carrying into the recipe PR:
nonword: true is a tokens concern. Vale wraps the pattern in \b…\b only when the rule has tokens and nonword is unset; raw is inserted verbatim. The "Right" example in create-vale-rule: raw entries concatenate, they do not alternate #360 carries nonword: true beside a raw list, where it is harmless and does nothing.
The default scope reads front matter too.scope: raw is not what lets a casing rule into target: taskless; a rule with no scope key, and scope: text, see it as well. paragraph and sentence do not. So "drop scope: raw" is not a fix on its own.
Why advisory, not rejection
A multi-entry raw has a legitimate reading: it is how Vale lets an author split one long pattern across lines, and the join is exactly what such an author wants. The trap is the other reading, where the entries were meant as alternatives, and the schema cannot tell the two apart. So it is said rather than refused, the same line vale-config.ts holds for a repeated key or a [*] matcher: ok stays true, errors stays empty, and the message names the fix for the reading that is usually meant.
The corpus in vale-corpus.ts has three verdicts (accepted, ignored, rejected) and no "accepted, with a note", so the advisory is not a corpus row; its unit tests sit in vale-schema-contract.test.ts beside the other tests that ask the schema rather than the binary.
New tests: 12 vendor-contract pins (all measured first by hand on the binary, see the table), 7 schema unit tests for the advisory (two entries, three entries, one entry, tokens list, Raw: casing, beside a rejection, empty list), 3 CLI-level tests in verify-test-commands.test.ts (notice present on two entries in text and --json, absent on one entry, joined with a config advisory).
Untouched on purpose: packages/cli/src/agent/create-vale-rule.md (the recipe PR owns it).
Review mode: incremental — read 0 prior review thread(s) before reviewing. .prior-review.json contained no reviewThreads, reviews, or comments, so this is the first review of this PR; nothing was skipped as already-addressed.
[New] The PR's design is sound: raw really does concatenate with no separator on the vendored binary (matches the measured table in the PR body), the advisory is correctly scoped to existence rules with raw.length > 1 (confirmed against the generated VALE_CHECK_FIELDS vocabulary — raw belongs only to existence), and it correctly rides on notice rather than errors per the "said, not refused" precedent already established for the config layer. The 12 new vendor-contract pins, 7 schema tests, and 3 CLI tests are well targeted and I didn't find fabrication or escaping errors in the new YAML-building test fixtures.
Four items posted inline, ranked most notable first:
packages/cli/src/rules/inspect.ts:290 — Once a style-layer advisory (e.g. the new multi-entry raw warning) and a config-layer advisory both fire for the same rule, advisories.join("\n") embeds a real newline into notice, but commands/verify.ts's text-mode renderer prints it as a single notice: ... line — the second advisory loses its prefix and indentation. This exact combined case was already latently possible before this PR (adviseValeRuleConfig alone can push >1 advisory), but this PR is what documents and tests "joined... in verify text output and --json," and its own CLI-level test for the combined case only asserts --json, leaving the text-mode gap untested. Same line also reimplements an existing joinNotices helper (rules/dispatch.ts:262) — now a third copy of the identical idiom (the second being rules/vale/run.ts:401-402).
packages/cli/src/schemas/vale-rule.ts:831 — canonicalKeys is computed twice per validateValeRule call: once inside the schema pipeline (line 812, discarded by schemaLayer), once again in the new adviseValeRule. Harmless today, but a latent hazard if the pipeline ever grows a real pre-canonicalization step.
packages/cli/src/schemas/vale-rule.ts:842-850 — The new advisory is a single ad hoc if, not integrated into the table-driven pattern the rest of the module uses, nor the single-walk approach adviseValeRuleConfig uses. Fine for one advisory; the next one will likely be copy-pasted rather than generalized.
packages/cli/test/vale-vendor-contract.test.ts:167 — The new existenceOver test helper duplicates the pre-existing hedge helper's shape, and its extra string parameter silently requires a trailing \n from every caller.
No correctness bugs found in the core advisory logic, the canonicalKeys/literal-key handling, or the vendor-pin test assertions themselves. No CLAUDE.md/STYLEGUIDE-CODE.md violations found (import ordering, barrel exports, and type-export rules are all unaffected by the diff's hunks; the changeset's patch bump is correct for a 0.y.z package per CLAUDE.md's explicit guidance).
Read in full. The medium item (canonicalKeys computed twice) is answered on its thread: the advisory must read the raw input because it is specified to speak beside a rejection, where the pipeline yields no parsed value; canonicalKeys is pure and idempotent, so no divergence is possible today. The four low items (multi-line notice text rendering and joinNotices reuse, table-driven advisory shape, existenceOver helper ergonomics) are collected for the maintainer to decide on and left open.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The code halves of #360 and #361. The recipe prose lands in a parallel PR; this one pins the underlying Vale behavior on the vendored binary and adds the
verifyadvisory #360 asks for.packages/cli/test/vale-vendor-contract.test.ts, three newdescribeblocks under "Vale vendor contract"): how anexistencerule'srawlist is read, which keynonwordgoverns, and which scopes see YAML front matter.verifyadvisory (packages/cli/src/schemas/vale-rule.ts, wired inpackages/cli/src/rules/inspect.ts):validateValeRulenow returnsadvisories: string[]besidevalid/errors, andverifyOneRulejoins them with the config layer's advisories onto the rule'snotice, inverifytext output and--json..changeset/vale-raw-authoring.md,patch.The exact advisory:
Measured
Every row below was run on the vendored Vale 3.22.0 before it was written down, and each is now a test.
rawjoinsraw: ["\bstops being\b", "[^.]{0,20}\band becomes\b"]It and becomes fun.(second entry alone)It stops being fun.(first entry alone)It stops being dull and becomes fun.stops being dull and becomesraw: [alpha, bravo, charlie]bravo alone. alphabravocharlie together.alphabravocharlieraw: ["(\bstops being\b|[^.]{0,20}\band becomes\b)"]nonwordis fortokenstokens: ["—"]This is a sentence — with an em dash.tokens: ["—"],nonword: truenonworddoes not touchrawraw: ["—"], with and withoutnonwordraw: ["(The|That|This) is the twist\."], with and withoutnonwordThat is the twist. Yes.scope: raw, tokentaskless---\ntarget: taskless\n---\n\nSee taskless in the body.scope: text, and noscopeat allscope: paragraph,scope: sentencescope: frontmatter.targetscope: frontmatter.titlescope: frontmatteroccurrence,min: 1,scope: raw,token: '(?m)^description: .+$'description:description:Two things the issues did not say, worth carrying into the recipe PR:
nonword: trueis atokensconcern. Vale wraps the pattern in\b…\bonly when the rule hastokensandnonwordis unset;rawis inserted verbatim. The "Right" example in create-vale-rule: raw entries concatenate, they do not alternate #360 carriesnonword: truebeside arawlist, where it is harmless and does nothing.scope: rawis not what lets a casing rule intotarget: taskless; a rule with noscopekey, andscope: text, see it as well.paragraphandsentencedo not. So "dropscope: raw" is not a fix on its own.Why advisory, not rejection
A multi-entry
rawhas a legitimate reading: it is how Vale lets an author split one long pattern across lines, and the join is exactly what such an author wants. The trap is the other reading, where the entries were meant as alternatives, and the schema cannot tell the two apart. So it is said rather than refused, the same linevale-config.tsholds for a repeated key or a[*]matcher:okstays true,errorsstays empty, and the message names the fix for the reading that is usually meant.The corpus in
vale-corpus.tshas three verdicts (accepted,ignored,rejected) and no "accepted, with a note", so the advisory is not a corpus row; its unit tests sit invale-schema-contract.test.tsbeside the other tests that ask the schema rather than the binary.How verified
pnpm build,pnpm typecheck,pnpm lint,pnpm --filter @taskless/cli test: 98 files, 1645 tests, all passing.tokenslist,Raw:casing, beside a rejection, empty list), 3 CLI-level tests inverify-test-commands.test.ts(notice present on two entries in text and--json, absent on one entry, joined with a config advisory).packages/cli/src/agent/create-vale-rule.md(the recipe PR owns it).Refs #360
Refs #361