Repository navigation
vale: parse per-rule .vale.ini into an AST, validate it with a schema, refuse a bad config at assembly #359
Description
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) anddelimiter: /=/(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 (speccli-vale-rule-engine: "A rule SHALL NOT be able to override another rule's matchers", currently unenforced) - assignment value is exactly
YESorNO BasedOnStyles, if present, is empty- at least one section, and at least one
= YESsomewhere
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
NOmatcher that precedes theYESit narrows (reversal "silently re-enables the rule … and nothing reports that") - a
[*]matcher (floods every file the walk reaches) - a
.taskless/**matcher:checkalready excludes.taskless/unconditionally (formats.ts), so this block only does work under a barevalerun
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 tonotices. - Spec:
cli-vale-rule-enginegains 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. patchchangeset (pre-1.0). A previously-tolerated bad config now refuses the run, so the changeset and theupdateledger 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
spec(vale): apply plan review — reject NO-before-YES, re-point the W1…
spec(vale): propose schema validation of per-rule .vale.ini
spec(vale): apply plan review — reject NO-before-YES, re-point the W1…
spec(vale): propose schema validation of per-rule .vale.ini
spec(vale): apply plan review — reject NO-before-YES, re-point the W1…
theCodeDrift commentedon Sep 21, 2026
Landed on
mainat d04b14f via #365 (parser, schema,vale-config-*constraints,verifyattribution) and #366 (assembly refusal, dispatch, recipe v10, ledger, archive). OpenSpec change archived atopenspec/changes/archive/2026-09-21-vale-config-schema.Both PRs referenced this issue with
Refsrather thanFixes, so it did not auto-close; closing by hand. The 3.22.0 follow-up that rejectsBasedOnStylesoutright and migrates installed configs is #372.