Skip to content

create-vale-rule: a rule about a command passes its fixtures and catches nothing #167

Description

@theCodeDrift

Summary

The create-vale-rule recipe leads an author straight into a rule that is green on verify, green on test, and catches zero real violations — because in markdown Vale skips code spans and fenced blocks by default, and a rule about a shell command is a rule about text that is almost always inside one.

What happened

Authoring docs-npx-cli (flag pnpm dlx @taskless/cli, prefer npx @taskless/cli) by following the recipe:

  1. Wrote the substitution rule and fixtures per the worked examples. verify ok, test ok.
  2. check against the real repository: zero findings, despite two READMEs containing the exact string — both inside ```bash blocks.
  3. Probed with a fixture holding the same command in three forms. Measured, with no scope: inline code not flagged, fenced block not flagged, plain prose flagged.
  4. scope: raw, scope: code, and scope: text each alone still missed at least one form. Only scope: [raw, code, text] caught all three — and then the two real violations surfaced.

Why the recipe does not prevent this

It does say "URLs and code spans are not prose" under the markup notes, which is accurate. But:

  • It is framed as a false-positive protection ("your rule won't fire on `Github/docs`"), not as the false-negative it becomes when the rule's whole subject is a command.
  • None of the nine worked rules is about a command, a flag, or a package name, so there is no example to copy for the case.
  • The step-1 table routes "preferring one term over another" to substitution with no scope guidance, and scope is introduced later as a way to narrow, never to widen into code.
  • The fixture advice ("the pass/ fixture must contain the same phrase outside that place") catches over-firing. Nothing prompts the author to put the phrase in a code span in fail/, which is what would have caught this.

The result is a rule that passes every local gate and is inert.

Suggested change

Add a worked rule for the command/identifier case — the one where a multi-scope value is required:

extends: substitution
message: "Use '%s' instead of '%s'"
level: error
scope: [raw, code, text]
swap:
  'pnpm dlx @taskless/cli': npx @taskless/cli

With the "goes wrong" line the other nine have: omitting the scope. Measured, the rule then passes its fixtures and reports nothing on a real repository, because every command in a README is in a code span or a fenced block.

Worth pairing with a fixture instruction: when a rule is about something that normally appears in code, the fail/ document must contain it inline, fenced, and in prose. That is the check that would have caught this before it shipped.

Activity

  1. theCodeDrift commented on Aug 25, 2026

    @theCodeDrift
    MemberAuthor

    Two measurements from using the rule in anger, both of which change what the recipe should recommend.

    scope is per-rule, and one raw rule does not affect the others

    Worth stating explicitly, because Vale assembles one config for the whole run and it is reasonable to assume the scopes interact. They do not. Same document, same run:

    rule scope prose inline code fenced block
    no-hedging default fires — quiet
    docs-npx-cli [raw, code, text] fires fires fires

    The scope table, measured

    raw subsumes the other two, so [raw, code, text] and a bare raw behave identically.

    scope prose inline code fenced block
    text ✓
    code ✓
    [code, text] ✓ ✓
    raw ✓ ✓ ✓

    raw and Vale's suppression comments are mutually exclusive

    This is the part worth documenting, because the obvious escape hatch is not available:

    scope <!-- vale Style.Rule = NO --> honored
    [raw, code, text] no
    [code, text] yes

    Under raw Vale reads the unparsed text, so a markup-level directive is invisible to it. A rule that reaches into fenced blocks therefore cannot be annotated away on a case-by-case basis, and a command rule needs raw precisely because every command in a README is fenced.

    The practical consequence, hit on taskless/cli itself: two READMEs documented npx and pnpm dlx side by side as deliberate alternatives. The rule fires on the pnpm dlx line, the directive cannot silence it, and the only resolutions are to change the docs or accept a permanent finding. We changed the docs, which was right here — but a repository that genuinely wants to document both package managers has no way to express that.

    So the worked rule this issue proposes should carry the trade-off, not just the scope value: raw buys fenced-block coverage and spends the ability to annotate an exception.

  2. theCodeDrift commented on Aug 25, 2026

    @theCodeDrift
    MemberAuthor

    Folded the general scope findings into #170, which collects Vale authoring pitfalls that pass verify and test and are only visible when someone notices a rule has never reported anything. This issue stays the specific case: a rule about a command, inert because commands are fenced.

  3. theCodeDrift commented on Aug 26, 2026

    @theCodeDrift
    MemberAuthor

    Delivered by #174 (a596e54, "docs(cli): close the create-vale-rule gaps that produce a silent rule"), merged to main. Verified against packages/cli/src/agent/create-vale-rule.txt on main rather than inferred from the PR title.

    Everything this issue asked for is in the recipe:

    • The false-negative framing. The markup notes no longer present code-span skipping only as false-positive protection. Step 2 now states that a rule about a command, flag, package name or env var has a subject that lives in fenced blocks in every real README, and that the default scope cannot see fenced blocks at all.
    • The fixture instruction. "When the rule's subject normally appears in code, the fail/ fixture must carry it three ways" — inline in a code span, inside a fenced block, and in ordinary prose, in one document. That is exactly the check this issue identified as the one that would have caught docs-npx-cli before it shipped.
    • The measurement, carried rather than described. "the default scope found one of three, raw found three", with the diagnostic rule stated in the form an author can act on: if the fixture fires on the prose line and not the other two, the answer is scope: raw.
    • The cost is stated too, which the issue did not ask for: a raw-scoped rule cannot be silenced by an in-document Vale directive, because directives are applied to the parsed document and raw reads the unparsed one.

    One refinement worth recording. This issue proposed scope: [raw, code, text], which is what docs-npx-cli was authored with. The recipe landed on scope: raw instead, because raw was measured to subsume code and text: on one document holding the token in all three places, text found one, code found one, [code, text] found two, and raw found all three. So the list form is not wrong, just redundant — raw is not "a bit wider", it is everything.

    The general scope findings live in #170 as noted above; this issue's specific case is closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions