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.
What
A per-rule
.taskless/rules/vale/<id>/.vale.iniis the one rule input with no schema. The style YAML goes throughpackages/cli/src/schemas/vale-rule.tsand the corpus; the ini is carried throughpackages/cli/src/rules/assemble.tsas verbatim lines (ruleConfigBodystrips a copied-inStylesPath/MinAlertLevelwithline.split("=")[0],sectionPatternsOfrecovers matchers with/^\[(.+)\]$/). Nothing checks that a matcher names this rule, that the breadcrumb is present, that a value isYES/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 inpackages/cli/src/schemas/vale-config.ts, run that intaskless verifyand 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 rejectingStylesPath/MinAlertLevelat rule level removes the only string edit assembly does today;sectionscomes fromitems.map((s) => s.name).Parser bake-off (measured 2026-09-21, one config: three matchers,
tskl)breadcrumb, a duplicated key, a[2024/**]glob)tskl) rulekeyini/@nodecraft/ini2.5.0.([*.md]→{"*":{md}})[2024/**]hoisted)inlineArrayskeeps both)iniparser1.0.5config-ini-parser1.6.1tskl) rule→rulejs-ini1.6.0keyMergeStrategy@jedmao/ini-parser0.2.4@jedmao/ini-parseris the pick: a lossless, ordered AST (Sections.items[]ofSection { name, nodes: Property | Comment }), so every check is a refinement overitemsin 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 theinilineage outright.Things measured about it that the implementation must absorb:
resolve: false(default JSON-parses values:2024→ number,true→ boolean) anddelimiter: /=/(default also splits on:).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.Schema, keyed by the rule's directory id
Errors (fail
verify, refuse assembly):StylesPath,MinAlertLevel, or anything else)tskl) rule = <id>— recipe: without it "assembly attributes it to nobody"<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)YESorNOBasedOnStyles, if present, is empty= YESsomewhereWarnings (reported, not fatal):
NOmatcher that precedes theYESit narrows (reversal "silently re-enables the rule … and nothing reports that")[*]matcher (floods every file the walk reaches).taskless/**matcher:checkalready excludes.taskless/unconditionally (formats.ts), so this block only does work under a barevalerunWhere it runs
taskless verify: schema failure is a rule failure alongside fixture results; warnings printed.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.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.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.