Skip to content

vale: parse per-rule .vale.ini into an AST, validate it with a schema, refuse a bad config at assembly #359

Description

@thecodedrift

What

A per-rule .taskless/rules/vale/<id>/.vale.ini is the one rule input with no schema. The style YAML goes through packages/cli/src/schemas/vale-rule.ts and the corpus; the ini is carried through packages/cli/src/rules/assemble.ts as verbatim lines (ruleConfigBody strips a copied-in StylesPath/MinAlertLevel with line.split("=")[0], sectionPatternsOf recovers matchers with /^\[(.+)\]$/). Nothing checks that a matcher names this rule, that the breadcrumb is present, that a value is YES/NO, or that a rule does not assign another rule's key, which the spec says it SHALL NOT be able to do.

Parse each rule config into an AST with @jedmao/ini-parser, validate the AST with a zod schema in packages/cli/src/schemas/vale-config.ts, run that in taskless verify and at assembly, and at assembly refuse the run on a failing config, consistent with how a bad ast-grep rule file aborts config parsing. A skipped-but-"passing" rule is the silent-disable failure this engine's design exists to catch.

Assembly stays byte concatenation of validated source: header, then per rule a # tskl) rule = <id> comment and the file verbatim. No serializer. The schema rejecting StylesPath/MinAlertLevel at rule level removes the only string edit assembly does today; sections comes from items.map((s) => s.name).

Parser bake-off (measured 2026-09-21, one config: three matchers, tskl) breadcrumb, a duplicated key, a [2024/**] glob)

package section names source order duplicate keys tskl) rule key comments types license
ini / @nodecraft/ini 2.5.0 nested on . ([*.md] → {"*":{md}}) lost ([2024/**] hoisted) last wins (inlineArrays keeps both) kept dropped no ISC
iniparser 1.0.5 literal kept last wins dropped dropped no MIT
config-ini-parser 1.6.1 literal kept kept, ordered pairs tskl) rule → rule dropped yes GPL-3.0
js-ini 1.6.0 literal kept array via keyMergeStrategy kept dropped yes MIT
@jedmao/ini-parser 0.2.4 literal kept kept kept kept yes MIT, zero deps

@jedmao/ini-parser is the pick: a lossless, ordered AST (Sections.items[] of Section { name, nodes: Property | Comment }), so every check is a refinement over items in source order and nothing is re-derived from text. Every Vale matcher glob contains a dot and positional order is the file's semantics, which rules out the ini lineage outright.

Things measured about it that the implementation must absorb:

  • Construct with resolve: false (default JSON-parses values: 2024 → number, true → boolean) and delimiter: /=/ (default also splits on :).
  • A blank line parses as Section { name: "", nodes: [] }. Filter those; never treat one as a matcher.
  • toString() is not round-trip safe (those blank-line sections emit as [], values gain a trailing space). Not a problem because assembly never serializes, but do not reach for it later.
  • 0.2.4, last published 2022, ~300 lines. Pin it; if it ever bites, vendor the file rather than write a parser.

Schema, keyed by the rule's directory id

Errors (fail verify, refuse assembly):

  • root properties: none allowed (StylesPath, MinAlertLevel, or anything else)
  • every section carries tskl) rule = <id> — recipe: without it "assembly attributes it to nobody"
  • assignment keys are <id>.<id> only; any other <style>.<check> key is a foreign-rule override (spec cli-vale-rule-engine: "A rule SHALL NOT be able to override another rule's matchers", currently unenforced)
  • assignment value is exactly YES or NO
  • BasedOnStyles, if present, is empty
  • at least one section, and at least one = YES somewhere

Warnings (reported, not fatal):

  • the same key assigned twice inside one section (3.21.0 flipped this from first-wins to last-wins; an author rarely means both)
  • a NO matcher that precedes the YES it narrows (reversal "silently re-enables the rule … and nothing reports that")
  • a [*] matcher (floods every file the walk reaches)
  • a .taskless/** matcher: check already excludes .taskless/ unconditionally (formats.ts), so this block only does work under a bare vale run

Where it runs

  • taskless verify: schema failure is a rule failure alongside fixture results; warnings printed.
  • assembly (check): validate every rule config; any error refuses the Vale run with the rule id and the failing line, the way an ast-grep config error refuses that engine. Warnings go to notices.
  • Spec: cli-vale-rule-engine gains a requirement for config validation and the refuse behavior; OpenSpec change, MODIFIED blocks restated in full. Pin "Assembly order is stable / byte-identical" against the new path.
  • Recipe create-vale-rule.md: say the config is schema-checked and what each error means; topic version bump.
  • patch changeset (pre-1.0). A previously-tolerated bad config now refuses the run, so the changeset and the update ledger say so.

Not this issue

Rule-YAML gotchas from the dogfood notes (raw concatenation, raw scope and front matter, corpus measurement, trusting notices) are recipe issues filed separately.

Activity

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

    CLIRelated to the taskless CLI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions