@redocly/recheck 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (595) hide show
  1. package/README.md +1023 -56
  2. package/dist/cli.js +40 -7
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/markdoc-schema.d.ts +17 -0
  5. package/dist/commands/markdoc-schema.d.ts.map +1 -0
  6. package/dist/commands/markdoc-schema.js +127 -0
  7. package/dist/commands/markdoc-schema.js.map +1 -0
  8. package/dist/commands/run.d.ts +2 -1
  9. package/dist/commands/run.d.ts.map +1 -1
  10. package/dist/commands/run.js +87 -10
  11. package/dist/commands/run.js.map +1 -1
  12. package/dist/config/load.d.ts +11 -0
  13. package/dist/config/load.d.ts.map +1 -1
  14. package/dist/config/load.js +12 -2
  15. package/dist/config/load.js.map +1 -1
  16. package/dist/config/presets/google.d.ts +3 -0
  17. package/dist/config/presets/google.d.ts.map +1 -0
  18. package/dist/config/presets/google.js +1671 -0
  19. package/dist/config/presets/google.js.map +1 -0
  20. package/dist/config/presets/inclusive-language.d.ts +3 -0
  21. package/dist/config/presets/inclusive-language.d.ts.map +1 -0
  22. package/dist/config/presets/inclusive-language.js +321 -0
  23. package/dist/config/presets/inclusive-language.js.map +1 -0
  24. package/dist/config/presets/index.d.ts +67 -0
  25. package/dist/config/presets/index.d.ts.map +1 -0
  26. package/dist/config/presets/index.js +137 -0
  27. package/dist/config/presets/index.js.map +1 -0
  28. package/dist/config/presets/markdoc.d.ts +21 -0
  29. package/dist/config/presets/markdoc.d.ts.map +1 -0
  30. package/dist/config/presets/markdoc.js +101 -0
  31. package/dist/config/presets/markdoc.js.map +1 -0
  32. package/dist/config/presets/markdown-relaxed.d.ts +3 -0
  33. package/dist/config/presets/markdown-relaxed.d.ts.map +1 -0
  34. package/dist/config/presets/markdown-relaxed.js +98 -0
  35. package/dist/config/presets/markdown-relaxed.js.map +1 -0
  36. package/dist/config/presets/markdown.d.ts +43 -0
  37. package/dist/config/presets/markdown.d.ts.map +1 -0
  38. package/dist/config/presets/markdown.js +132 -0
  39. package/dist/config/presets/markdown.js.map +1 -0
  40. package/dist/config/presets/microsoft.d.ts +3 -0
  41. package/dist/config/presets/microsoft.d.ts.map +1 -0
  42. package/dist/config/presets/microsoft.js +2268 -0
  43. package/dist/config/presets/microsoft.js.map +1 -0
  44. package/dist/config/presets/minimal.d.ts +3 -0
  45. package/dist/config/presets/minimal.d.ts.map +1 -0
  46. package/dist/config/presets/minimal.js +21 -0
  47. package/dist/config/presets/minimal.js.map +1 -0
  48. package/dist/config/presets/plain-language.d.ts +3 -0
  49. package/dist/config/presets/plain-language.d.ts.map +1 -0
  50. package/dist/config/presets/plain-language.js +351 -0
  51. package/dist/config/presets/plain-language.js.map +1 -0
  52. package/dist/config/presets/prose.d.ts +52 -0
  53. package/dist/config/presets/prose.d.ts.map +1 -0
  54. package/dist/config/presets/prose.js +138 -0
  55. package/dist/config/presets/prose.js.map +1 -0
  56. package/dist/config/schema.d.ts +128 -22
  57. package/dist/config/schema.d.ts.map +1 -1
  58. package/dist/config/schema.js +105 -21
  59. package/dist/config/schema.js.map +1 -1
  60. package/dist/config/validate.d.ts +12 -2
  61. package/dist/config/validate.d.ts.map +1 -1
  62. package/dist/config/validate.js +1200 -44
  63. package/dist/config/validate.js.map +1 -1
  64. package/dist/core/auto-fix.d.ts +8 -13
  65. package/dist/core/auto-fix.d.ts.map +1 -1
  66. package/dist/core/auto-fix.js +94 -75
  67. package/dist/core/auto-fix.js.map +1 -1
  68. package/dist/core/case-preserve.d.ts +46 -0
  69. package/dist/core/case-preserve.d.ts.map +1 -0
  70. package/dist/core/case-preserve.js +57 -0
  71. package/dist/core/case-preserve.js.map +1 -0
  72. package/dist/core/directives.d.ts +9 -0
  73. package/dist/core/directives.d.ts.map +1 -0
  74. package/dist/core/directives.js +73 -0
  75. package/dist/core/directives.js.map +1 -0
  76. package/dist/core/files.d.ts +63 -0
  77. package/dist/core/files.d.ts.map +1 -1
  78. package/dist/core/files.js +185 -0
  79. package/dist/core/files.js.map +1 -1
  80. package/dist/core/inline-code.d.ts +87 -0
  81. package/dist/core/inline-code.d.ts.map +1 -0
  82. package/dist/core/inline-code.js +104 -0
  83. package/dist/core/inline-code.js.map +1 -0
  84. package/dist/core/line-endings.d.ts +32 -0
  85. package/dist/core/line-endings.d.ts.map +1 -0
  86. package/dist/core/line-endings.js +65 -0
  87. package/dist/core/line-endings.js.map +1 -0
  88. package/dist/core/markdoc-tags.d.ts +79 -0
  89. package/dist/core/markdoc-tags.d.ts.map +1 -0
  90. package/dist/core/markdoc-tags.js +131 -0
  91. package/dist/core/markdoc-tags.js.map +1 -0
  92. package/dist/core/rule-filters.d.ts +17 -0
  93. package/dist/core/rule-filters.d.ts.map +1 -1
  94. package/dist/core/rule-filters.js +64 -0
  95. package/dist/core/rule-filters.js.map +1 -1
  96. package/dist/core/runner.d.ts +92 -3
  97. package/dist/core/runner.d.ts.map +1 -1
  98. package/dist/core/runner.js +348 -110
  99. package/dist/core/runner.js.map +1 -1
  100. package/dist/core/timing.d.ts.map +1 -1
  101. package/dist/data/markdoc-realm-schema.d.ts +3 -0
  102. package/dist/data/markdoc-realm-schema.d.ts.map +1 -0
  103. package/dist/data/markdoc-realm-schema.js +760 -0
  104. package/dist/data/markdoc-realm-schema.js.map +1 -0
  105. package/dist/data/proper-nouns.d.ts +2 -0
  106. package/dist/data/proper-nouns.d.ts.map +1 -0
  107. package/dist/data/proper-nouns.js +47 -0
  108. package/dist/data/proper-nouns.js.map +1 -0
  109. package/dist/index.d.ts +90 -0
  110. package/dist/index.d.ts.map +1 -0
  111. package/dist/index.js +152 -0
  112. package/dist/index.js.map +1 -0
  113. package/dist/metrics/formulas.d.ts +17 -0
  114. package/dist/metrics/formulas.d.ts.map +1 -0
  115. package/dist/metrics/formulas.js +70 -0
  116. package/dist/metrics/formulas.js.map +1 -0
  117. package/dist/metrics/index.d.ts +5 -0
  118. package/dist/metrics/index.d.ts.map +1 -0
  119. package/dist/metrics/index.js +3 -0
  120. package/dist/metrics/index.js.map +1 -0
  121. package/dist/metrics/statistics.d.ts +27 -0
  122. package/dist/metrics/statistics.d.ts.map +1 -0
  123. package/dist/metrics/statistics.js +56 -0
  124. package/dist/metrics/statistics.js.map +1 -0
  125. package/dist/parser/index.d.ts +18 -0
  126. package/dist/parser/index.d.ts.map +1 -0
  127. package/dist/parser/index.js +168 -0
  128. package/dist/parser/index.js.map +1 -0
  129. package/dist/parser/markdoc/extract-statics.d.ts +45 -0
  130. package/dist/parser/markdoc/extract-statics.d.ts.map +1 -0
  131. package/dist/parser/markdoc/extract-statics.js +139 -0
  132. package/dist/parser/markdoc/extract-statics.js.map +1 -0
  133. package/dist/parser/markdoc/pairing.d.ts +63 -0
  134. package/dist/parser/markdoc/pairing.d.ts.map +1 -0
  135. package/dist/parser/markdoc/pairing.js +94 -0
  136. package/dist/parser/markdoc/pairing.js.map +1 -0
  137. package/dist/parser/markdoc/schema.d.ts +85 -0
  138. package/dist/parser/markdoc/schema.d.ts.map +1 -0
  139. package/dist/parser/markdoc/schema.js +86 -0
  140. package/dist/parser/markdoc/schema.js.map +1 -0
  141. package/dist/parser/markdoc/span.d.ts +64 -0
  142. package/dist/parser/markdoc/span.d.ts.map +1 -0
  143. package/dist/parser/markdoc/span.js +729 -0
  144. package/dist/parser/markdoc/span.js.map +1 -0
  145. package/dist/parser/markdoc/structure.d.ts +28 -0
  146. package/dist/parser/markdoc/structure.d.ts.map +1 -0
  147. package/dist/parser/markdoc/structure.js +153 -0
  148. package/dist/parser/markdoc/structure.js.map +1 -0
  149. package/dist/parser/markdoc/syntax.d.ts +44 -0
  150. package/dist/parser/markdoc/syntax.d.ts.map +1 -0
  151. package/dist/parser/markdoc/syntax.js +317 -0
  152. package/dist/parser/markdoc/syntax.js.map +1 -0
  153. package/dist/parser/types.d.ts +18 -0
  154. package/dist/parser/types.d.ts.map +1 -0
  155. package/dist/parser/types.js.map +1 -0
  156. package/dist/reporter/fixes.d.ts.map +1 -1
  157. package/dist/reporter/fixes.js +22 -1
  158. package/dist/reporter/fixes.js.map +1 -1
  159. package/dist/reporter/statistics.d.ts +1 -8
  160. package/dist/reporter/statistics.d.ts.map +1 -1
  161. package/dist/reporter/statistics.js.map +1 -1
  162. package/dist/rules/registry.d.ts +15 -0
  163. package/dist/rules/registry.d.ts.map +1 -0
  164. package/dist/rules/registry.js +81 -0
  165. package/dist/rules/registry.js.map +1 -0
  166. package/dist/rules/scope/capitalization.d.ts +3 -0
  167. package/dist/rules/scope/capitalization.d.ts.map +1 -0
  168. package/dist/rules/scope/capitalization.js +163 -0
  169. package/dist/rules/scope/capitalization.js.map +1 -0
  170. package/dist/rules/scope/conditional.d.ts +3 -0
  171. package/dist/rules/scope/conditional.d.ts.map +1 -0
  172. package/dist/rules/scope/conditional.js +112 -0
  173. package/dist/rules/scope/conditional.js.map +1 -0
  174. package/dist/rules/scope/consistency.d.ts +3 -0
  175. package/dist/rules/scope/consistency.d.ts.map +1 -0
  176. package/dist/rules/scope/consistency.js +178 -0
  177. package/dist/rules/scope/consistency.js.map +1 -0
  178. package/dist/rules/scope/length.d.ts +3 -0
  179. package/dist/rules/scope/length.d.ts.map +1 -0
  180. package/dist/rules/scope/length.js +69 -0
  181. package/dist/rules/scope/length.js.map +1 -0
  182. package/dist/rules/scope/max-image-size.d.ts +3 -0
  183. package/dist/rules/scope/max-image-size.d.ts.map +1 -0
  184. package/dist/rules/scope/max-image-size.js +65 -0
  185. package/dist/rules/scope/max-image-size.js.map +1 -0
  186. package/dist/rules/scope/metric.d.ts +17 -0
  187. package/dist/rules/scope/metric.d.ts.map +1 -0
  188. package/dist/rules/scope/metric.js +216 -0
  189. package/dist/rules/scope/metric.js.map +1 -0
  190. package/dist/rules/scope/occurrence.d.ts +3 -0
  191. package/dist/rules/scope/occurrence.d.ts.map +1 -0
  192. package/dist/rules/scope/occurrence.js +48 -0
  193. package/dist/rules/scope/occurrence.js.map +1 -0
  194. package/dist/rules/scope/pattern.d.ts +3 -0
  195. package/dist/rules/scope/pattern.d.ts.map +1 -0
  196. package/dist/rules/scope/pattern.js +75 -0
  197. package/dist/rules/scope/pattern.js.map +1 -0
  198. package/dist/rules/scope/repetition.d.ts +3 -0
  199. package/dist/rules/scope/repetition.d.ts.map +1 -0
  200. package/dist/rules/scope/repetition.js +139 -0
  201. package/dist/rules/scope/repetition.js.map +1 -0
  202. package/dist/rules/scope/semantic-line-breaks.d.ts +3 -0
  203. package/dist/rules/scope/semantic-line-breaks.d.ts.map +1 -0
  204. package/dist/rules/scope/semantic-line-breaks.js +213 -0
  205. package/dist/rules/scope/semantic-line-breaks.js.map +1 -0
  206. package/dist/rules/scope/spelling.d.ts +27 -0
  207. package/dist/rules/scope/spelling.d.ts.map +1 -0
  208. package/dist/rules/scope/spelling.js +227 -0
  209. package/dist/rules/scope/spelling.js.map +1 -0
  210. package/dist/rules/scope/swap.d.ts +3 -0
  211. package/dist/rules/scope/swap.d.ts.map +1 -0
  212. package/dist/rules/scope/swap.js +149 -0
  213. package/dist/rules/scope/swap.js.map +1 -0
  214. package/dist/rules/scope/title-case.d.ts +46 -0
  215. package/dist/rules/scope/title-case.d.ts.map +1 -0
  216. package/dist/rules/scope/title-case.js +301 -0
  217. package/dist/rules/scope/title-case.js.map +1 -0
  218. package/dist/rules/token/blanks-around-fences.d.ts +3 -0
  219. package/dist/rules/token/blanks-around-fences.d.ts.map +1 -0
  220. package/dist/rules/token/blanks-around-fences.js +44 -0
  221. package/dist/rules/token/blanks-around-fences.js.map +1 -0
  222. package/dist/rules/token/blanks-around-headings.d.ts +3 -0
  223. package/dist/rules/token/blanks-around-headings.d.ts.map +1 -0
  224. package/dist/rules/token/blanks-around-headings.js +108 -0
  225. package/dist/rules/token/blanks-around-headings.js.map +1 -0
  226. package/dist/rules/token/blanks-around-lists.d.ts +3 -0
  227. package/dist/rules/token/blanks-around-lists.d.ts.map +1 -0
  228. package/dist/rules/token/blanks-around-lists.js +56 -0
  229. package/dist/rules/token/blanks-around-lists.js.map +1 -0
  230. package/dist/rules/token/blanks-around-tables.d.ts +3 -0
  231. package/dist/rules/token/blanks-around-tables.d.ts.map +1 -0
  232. package/dist/rules/token/blanks-around-tables.js +42 -0
  233. package/dist/rules/token/blanks-around-tables.js.map +1 -0
  234. package/dist/rules/token/code-block-style.d.ts +3 -0
  235. package/dist/rules/token/code-block-style.d.ts.map +1 -0
  236. package/dist/rules/token/code-block-style.js +30 -0
  237. package/dist/rules/token/code-block-style.js.map +1 -0
  238. package/dist/rules/token/code-fence-style.d.ts +3 -0
  239. package/dist/rules/token/code-fence-style.d.ts.map +1 -0
  240. package/dist/rules/token/code-fence-style.js +35 -0
  241. package/dist/rules/token/code-fence-style.js.map +1 -0
  242. package/dist/rules/token/commands-show-output.d.ts +3 -0
  243. package/dist/rules/token/commands-show-output.d.ts.map +1 -0
  244. package/dist/rules/token/commands-show-output.js +38 -0
  245. package/dist/rules/token/commands-show-output.js.map +1 -0
  246. package/dist/rules/token/descriptive-link-text.d.ts +3 -0
  247. package/dist/rules/token/descriptive-link-text.d.ts.map +1 -0
  248. package/dist/rules/token/descriptive-link-text.js +53 -0
  249. package/dist/rules/token/descriptive-link-text.js.map +1 -0
  250. package/dist/rules/token/emphasis-style.d.ts +3 -0
  251. package/dist/rules/token/emphasis-style.d.ts.map +1 -0
  252. package/dist/rules/token/emphasis-style.js +51 -0
  253. package/dist/rules/token/emphasis-style.js.map +1 -0
  254. package/dist/rules/token/fenced-code-language.d.ts +3 -0
  255. package/dist/rules/token/fenced-code-language.d.ts.map +1 -0
  256. package/dist/rules/token/fenced-code-language.js +35 -0
  257. package/dist/rules/token/fenced-code-language.js.map +1 -0
  258. package/dist/rules/token/first-line-h1.d.ts +3 -0
  259. package/dist/rules/token/first-line-h1.d.ts.map +1 -0
  260. package/dist/rules/token/first-line-h1.js +107 -0
  261. package/dist/rules/token/first-line-h1.js.map +1 -0
  262. package/dist/rules/token/heading-increment.d.ts +3 -0
  263. package/dist/rules/token/heading-increment.d.ts.map +1 -0
  264. package/dist/rules/token/heading-increment.js +27 -0
  265. package/dist/rules/token/heading-increment.js.map +1 -0
  266. package/dist/rules/token/heading-start-left.d.ts +3 -0
  267. package/dist/rules/token/heading-start-left.d.ts.map +1 -0
  268. package/dist/rules/token/heading-start-left.js +31 -0
  269. package/dist/rules/token/heading-start-left.js.map +1 -0
  270. package/dist/rules/token/heading-style.d.ts +3 -0
  271. package/dist/rules/token/heading-style.d.ts.map +1 -0
  272. package/dist/rules/token/heading-style.js +41 -0
  273. package/dist/rules/token/heading-style.js.map +1 -0
  274. package/dist/rules/token/helpers.d.ts +313 -0
  275. package/dist/rules/token/helpers.d.ts.map +1 -0
  276. package/dist/rules/token/helpers.js +746 -0
  277. package/dist/rules/token/helpers.js.map +1 -0
  278. package/dist/rules/token/hr-style.d.ts +3 -0
  279. package/dist/rules/token/hr-style.d.ts.map +1 -0
  280. package/dist/rules/token/hr-style.js +28 -0
  281. package/dist/rules/token/hr-style.js.map +1 -0
  282. package/dist/rules/token/index.d.ts +75 -0
  283. package/dist/rules/token/index.d.ts.map +1 -0
  284. package/dist/rules/token/index.js +226 -0
  285. package/dist/rules/token/index.js.map +1 -0
  286. package/dist/rules/token/line-length.d.ts +3 -0
  287. package/dist/rules/token/line-length.d.ts.map +1 -0
  288. package/dist/rules/token/line-length.js +120 -0
  289. package/dist/rules/token/line-length.js.map +1 -0
  290. package/dist/rules/token/link-fragments.d.ts +3 -0
  291. package/dist/rules/token/link-fragments.d.ts.map +1 -0
  292. package/dist/rules/token/link-fragments.js +145 -0
  293. package/dist/rules/token/link-fragments.js.map +1 -0
  294. package/dist/rules/token/link-image-reference-definitions.d.ts +3 -0
  295. package/dist/rules/token/link-image-reference-definitions.d.ts.map +1 -0
  296. package/dist/rules/token/link-image-reference-definitions.js +50 -0
  297. package/dist/rules/token/link-image-reference-definitions.js.map +1 -0
  298. package/dist/rules/token/link-image-style.d.ts +3 -0
  299. package/dist/rules/token/link-image-style.d.ts.map +1 -0
  300. package/dist/rules/token/link-image-style.js +131 -0
  301. package/dist/rules/token/link-image-style.js.map +1 -0
  302. package/dist/rules/token/list-indent.d.ts +3 -0
  303. package/dist/rules/token/list-indent.d.ts.map +1 -0
  304. package/dist/rules/token/list-indent.js +60 -0
  305. package/dist/rules/token/list-indent.js.map +1 -0
  306. package/dist/rules/token/list-length.d.ts +3 -0
  307. package/dist/rules/token/list-length.d.ts.map +1 -0
  308. package/dist/rules/token/list-length.js +55 -0
  309. package/dist/rules/token/list-length.js.map +1 -0
  310. package/dist/rules/token/list-marker-space.d.ts +3 -0
  311. package/dist/rules/token/list-marker-space.d.ts.map +1 -0
  312. package/dist/rules/token/list-marker-space.js +52 -0
  313. package/dist/rules/token/list-marker-space.js.map +1 -0
  314. package/dist/rules/token/markdoc-attributes.d.ts +3 -0
  315. package/dist/rules/token/markdoc-attributes.d.ts.map +1 -0
  316. package/dist/rules/token/markdoc-attributes.js +269 -0
  317. package/dist/rules/token/markdoc-attributes.js.map +1 -0
  318. package/dist/rules/token/markdoc-pairing.d.ts +3 -0
  319. package/dist/rules/token/markdoc-pairing.d.ts.map +1 -0
  320. package/dist/rules/token/markdoc-pairing.js +73 -0
  321. package/dist/rules/token/markdoc-pairing.js.map +1 -0
  322. package/dist/rules/token/markdoc-syntax.d.ts +3 -0
  323. package/dist/rules/token/markdoc-syntax.d.ts.map +1 -0
  324. package/dist/rules/token/markdoc-syntax.js +119 -0
  325. package/dist/rules/token/markdoc-syntax.js.map +1 -0
  326. package/dist/rules/token/markdoc-unknown-tag.d.ts +3 -0
  327. package/dist/rules/token/markdoc-unknown-tag.d.ts.map +1 -0
  328. package/dist/rules/token/markdoc-unknown-tag.js +64 -0
  329. package/dist/rules/token/markdoc-unknown-tag.js.map +1 -0
  330. package/dist/rules/token/messages.d.ts +4 -0
  331. package/dist/rules/token/messages.d.ts.map +1 -0
  332. package/dist/rules/token/messages.js +20 -0
  333. package/dist/rules/token/messages.js.map +1 -0
  334. package/dist/rules/token/no-alt-text.d.ts +3 -0
  335. package/dist/rules/token/no-alt-text.d.ts.map +1 -0
  336. package/dist/rules/token/no-alt-text.js +47 -0
  337. package/dist/rules/token/no-alt-text.js.map +1 -0
  338. package/dist/rules/token/no-bare-urls.d.ts +3 -0
  339. package/dist/rules/token/no-bare-urls.d.ts.map +1 -0
  340. package/dist/rules/token/no-bare-urls.js +88 -0
  341. package/dist/rules/token/no-bare-urls.js.map +1 -0
  342. package/dist/rules/token/no-blanks-blockquote.d.ts +3 -0
  343. package/dist/rules/token/no-blanks-blockquote.d.ts.map +1 -0
  344. package/dist/rules/token/no-blanks-blockquote.js +39 -0
  345. package/dist/rules/token/no-blanks-blockquote.js.map +1 -0
  346. package/dist/rules/token/no-duplicate-heading.d.ts +3 -0
  347. package/dist/rules/token/no-duplicate-heading.d.ts.map +1 -0
  348. package/dist/rules/token/no-duplicate-heading.js +101 -0
  349. package/dist/rules/token/no-duplicate-heading.js.map +1 -0
  350. package/dist/rules/token/no-duplicate-link-destinations.d.ts +3 -0
  351. package/dist/rules/token/no-duplicate-link-destinations.d.ts.map +1 -0
  352. package/dist/rules/token/no-duplicate-link-destinations.js +65 -0
  353. package/dist/rules/token/no-duplicate-link-destinations.js.map +1 -0
  354. package/dist/rules/token/no-emphasis-as-heading.d.ts +3 -0
  355. package/dist/rules/token/no-emphasis-as-heading.d.ts.map +1 -0
  356. package/dist/rules/token/no-emphasis-as-heading.js +44 -0
  357. package/dist/rules/token/no-emphasis-as-heading.js.map +1 -0
  358. package/dist/rules/token/no-empty-headings.d.ts +3 -0
  359. package/dist/rules/token/no-empty-headings.d.ts.map +1 -0
  360. package/dist/rules/token/no-empty-headings.js +28 -0
  361. package/dist/rules/token/no-empty-headings.js.map +1 -0
  362. package/dist/rules/token/no-empty-links.d.ts +3 -0
  363. package/dist/rules/token/no-empty-links.d.ts.map +1 -0
  364. package/dist/rules/token/no-empty-links.js +67 -0
  365. package/dist/rules/token/no-empty-links.js.map +1 -0
  366. package/dist/rules/token/no-hard-tabs.d.ts +3 -0
  367. package/dist/rules/token/no-hard-tabs.d.ts.map +1 -0
  368. package/dist/rules/token/no-hard-tabs.js +76 -0
  369. package/dist/rules/token/no-hard-tabs.js.map +1 -0
  370. package/dist/rules/token/no-inline-html.d.ts +3 -0
  371. package/dist/rules/token/no-inline-html.d.ts.map +1 -0
  372. package/dist/rules/token/no-inline-html.js +45 -0
  373. package/dist/rules/token/no-inline-html.js.map +1 -0
  374. package/dist/rules/token/no-missing-space-atx.d.ts +3 -0
  375. package/dist/rules/token/no-missing-space-atx.d.ts.map +1 -0
  376. package/dist/rules/token/no-missing-space-atx.js +36 -0
  377. package/dist/rules/token/no-missing-space-atx.js.map +1 -0
  378. package/dist/rules/token/no-missing-space-closed-atx.d.ts +3 -0
  379. package/dist/rules/token/no-missing-space-closed-atx.d.ts.map +1 -0
  380. package/dist/rules/token/no-missing-space-closed-atx.js +45 -0
  381. package/dist/rules/token/no-missing-space-closed-atx.js.map +1 -0
  382. package/dist/rules/token/no-multiple-blanks.d.ts +3 -0
  383. package/dist/rules/token/no-multiple-blanks.d.ts.map +1 -0
  384. package/dist/rules/token/no-multiple-blanks.js +35 -0
  385. package/dist/rules/token/no-multiple-blanks.js.map +1 -0
  386. package/dist/rules/token/no-multiple-space-atx.d.ts +13 -0
  387. package/dist/rules/token/no-multiple-space-atx.d.ts.map +1 -0
  388. package/dist/rules/token/no-multiple-space-atx.js +50 -0
  389. package/dist/rules/token/no-multiple-space-atx.js.map +1 -0
  390. package/dist/rules/token/no-multiple-space-blockquote.d.ts +3 -0
  391. package/dist/rules/token/no-multiple-space-blockquote.d.ts.map +1 -0
  392. package/dist/rules/token/no-multiple-space-blockquote.js +43 -0
  393. package/dist/rules/token/no-multiple-space-blockquote.js.map +1 -0
  394. package/dist/rules/token/no-multiple-space-closed-atx.d.ts +3 -0
  395. package/dist/rules/token/no-multiple-space-closed-atx.d.ts.map +1 -0
  396. package/dist/rules/token/no-multiple-space-closed-atx.js +19 -0
  397. package/dist/rules/token/no-multiple-space-closed-atx.js.map +1 -0
  398. package/dist/rules/token/no-reversed-links.d.ts +3 -0
  399. package/dist/rules/token/no-reversed-links.d.ts.map +1 -0
  400. package/dist/rules/token/no-reversed-links.js +52 -0
  401. package/dist/rules/token/no-reversed-links.js.map +1 -0
  402. package/dist/rules/token/no-space-in-code.d.ts +3 -0
  403. package/dist/rules/token/no-space-in-code.d.ts.map +1 -0
  404. package/dist/rules/token/no-space-in-code.js +75 -0
  405. package/dist/rules/token/no-space-in-code.js.map +1 -0
  406. package/dist/rules/token/no-space-in-emphasis.d.ts +3 -0
  407. package/dist/rules/token/no-space-in-emphasis.d.ts.map +1 -0
  408. package/dist/rules/token/no-space-in-emphasis.js +79 -0
  409. package/dist/rules/token/no-space-in-emphasis.js.map +1 -0
  410. package/dist/rules/token/no-space-in-links.d.ts +3 -0
  411. package/dist/rules/token/no-space-in-links.d.ts.map +1 -0
  412. package/dist/rules/token/no-space-in-links.js +48 -0
  413. package/dist/rules/token/no-space-in-links.js.map +1 -0
  414. package/dist/rules/token/no-trailing-punctuation.d.ts +3 -0
  415. package/dist/rules/token/no-trailing-punctuation.d.ts.map +1 -0
  416. package/dist/rules/token/no-trailing-punctuation.js +38 -0
  417. package/dist/rules/token/no-trailing-punctuation.js.map +1 -0
  418. package/dist/rules/token/no-trailing-spaces.d.ts +3 -0
  419. package/dist/rules/token/no-trailing-spaces.d.ts.map +1 -0
  420. package/dist/rules/token/no-trailing-spaces.js +89 -0
  421. package/dist/rules/token/no-trailing-spaces.js.map +1 -0
  422. package/dist/rules/token/ol-prefix.d.ts +3 -0
  423. package/dist/rules/token/ol-prefix.d.ts.map +1 -0
  424. package/dist/rules/token/ol-prefix.js +70 -0
  425. package/dist/rules/token/ol-prefix.js.map +1 -0
  426. package/dist/rules/token/proper-names.d.ts +3 -0
  427. package/dist/rules/token/proper-names.d.ts.map +1 -0
  428. package/dist/rules/token/proper-names.js +93 -0
  429. package/dist/rules/token/proper-names.js.map +1 -0
  430. package/dist/rules/token/reference-links-images.d.ts +3 -0
  431. package/dist/rules/token/reference-links-images.d.ts.map +1 -0
  432. package/dist/rules/token/reference-links-images.js +36 -0
  433. package/dist/rules/token/reference-links-images.js.map +1 -0
  434. package/dist/rules/token/required-headings.d.ts +3 -0
  435. package/dist/rules/token/required-headings.d.ts.map +1 -0
  436. package/dist/rules/token/required-headings.js +83 -0
  437. package/dist/rules/token/required-headings.js.map +1 -0
  438. package/dist/rules/token/single-h1.d.ts +3 -0
  439. package/dist/rules/token/single-h1.d.ts.map +1 -0
  440. package/dist/rules/token/single-h1.js +56 -0
  441. package/dist/rules/token/single-h1.js.map +1 -0
  442. package/dist/rules/token/single-trailing-newline.d.ts +3 -0
  443. package/dist/rules/token/single-trailing-newline.d.ts.map +1 -0
  444. package/dist/rules/token/single-trailing-newline.js +25 -0
  445. package/dist/rules/token/single-trailing-newline.js.map +1 -0
  446. package/dist/rules/token/strong-style.d.ts +3 -0
  447. package/dist/rules/token/strong-style.d.ts.map +1 -0
  448. package/dist/rules/token/strong-style.js +51 -0
  449. package/dist/rules/token/strong-style.js.map +1 -0
  450. package/dist/rules/token/table-column-count.d.ts +3 -0
  451. package/dist/rules/token/table-column-count.d.ts.map +1 -0
  452. package/dist/rules/token/table-column-count.js +44 -0
  453. package/dist/rules/token/table-column-count.js.map +1 -0
  454. package/dist/rules/token/table-column-style.d.ts +3 -0
  455. package/dist/rules/token/table-column-style.d.ts.map +1 -0
  456. package/dist/rules/token/table-column-style.js +179 -0
  457. package/dist/rules/token/table-column-style.js.map +1 -0
  458. package/dist/rules/token/table-pipe-style.d.ts +3 -0
  459. package/dist/rules/token/table-pipe-style.d.ts.map +1 -0
  460. package/dist/rules/token/table-pipe-style.js +54 -0
  461. package/dist/rules/token/table-pipe-style.js.map +1 -0
  462. package/dist/rules/token/ul-indent.d.ts +3 -0
  463. package/dist/rules/token/ul-indent.d.ts.map +1 -0
  464. package/dist/rules/token/ul-indent.js +72 -0
  465. package/dist/rules/token/ul-indent.js.map +1 -0
  466. package/dist/rules/token/ul-style.d.ts +3 -0
  467. package/dist/rules/token/ul-style.d.ts.map +1 -0
  468. package/dist/rules/token/ul-style.js +80 -0
  469. package/dist/rules/token/ul-style.js.map +1 -0
  470. package/dist/rules/types.d.ts +81 -0
  471. package/dist/rules/types.d.ts.map +1 -0
  472. package/dist/rules/types.js +2 -0
  473. package/dist/rules/types.js.map +1 -0
  474. package/dist/rules/utils.d.ts +29 -0
  475. package/dist/rules/utils.d.ts.map +1 -0
  476. package/dist/{assertions → rules}/utils.js +27 -0
  477. package/dist/rules/utils.js.map +1 -0
  478. package/dist/scopes/extractor.d.ts +7 -0
  479. package/dist/scopes/extractor.d.ts.map +1 -0
  480. package/dist/scopes/extractor.js +475 -0
  481. package/dist/scopes/extractor.js.map +1 -0
  482. package/dist/scopes/selector.d.ts +51 -0
  483. package/dist/scopes/selector.d.ts.map +1 -0
  484. package/dist/scopes/selector.js +121 -0
  485. package/dist/scopes/selector.js.map +1 -0
  486. package/dist/scopes/sentences.d.ts +15 -0
  487. package/dist/scopes/sentences.d.ts.map +1 -0
  488. package/dist/scopes/sentences.js +124 -0
  489. package/dist/scopes/sentences.js.map +1 -0
  490. package/dist/scopes/types.d.ts +44 -0
  491. package/dist/scopes/types.d.ts.map +1 -0
  492. package/dist/scopes/types.js +2 -0
  493. package/dist/scopes/types.js.map +1 -0
  494. package/dist/scopes/vocabulary.d.ts +16 -0
  495. package/dist/scopes/vocabulary.d.ts.map +1 -0
  496. package/dist/scopes/vocabulary.js +70 -0
  497. package/dist/scopes/vocabulary.js.map +1 -0
  498. package/dist/types/assertions.d.ts +95 -32
  499. package/dist/types/assertions.d.ts.map +1 -1
  500. package/dist/types/problems.d.ts +4 -5
  501. package/dist/types/problems.d.ts.map +1 -1
  502. package/dist/types/rules.d.ts +4 -6
  503. package/dist/types/rules.d.ts.map +1 -1
  504. package/examples/appendices/google.appendix.yaml +91 -0
  505. package/examples/appendices/inclusive-language.appendix.yaml +61 -0
  506. package/examples/appendices/microsoft.appendix.yaml +99 -0
  507. package/examples/appendices/plain-language.appendix.yaml +88 -0
  508. package/examples/google.yaml +1525 -0
  509. package/examples/inclusive-language.yaml +304 -0
  510. package/examples/microsoft.yaml +1542 -0
  511. package/examples/plain-language.yaml +325 -0
  512. package/package.json +49 -16
  513. package/presets/google/PROVENANCE.md +1022 -0
  514. package/presets/google/sources.json +192 -0
  515. package/presets/inclusive-language/PROVENANCE.md +174 -0
  516. package/presets/inclusive-language/sources.json +107 -0
  517. package/presets/microsoft/PROVENANCE.md +1555 -0
  518. package/presets/microsoft/sources.json +494 -0
  519. package/presets/plain-language/PROVENANCE.md +364 -0
  520. package/presets/plain-language/sources.json +108 -0
  521. package/dist/assertions/bullet-style.d.ts +0 -3
  522. package/dist/assertions/bullet-style.d.ts.map +0 -1
  523. package/dist/assertions/bullet-style.js +0 -60
  524. package/dist/assertions/bullet-style.js.map +0 -1
  525. package/dist/assertions/index.d.ts +0 -21
  526. package/dist/assertions/index.d.ts.map +0 -1
  527. package/dist/assertions/index.js +0 -30
  528. package/dist/assertions/index.js.map +0 -1
  529. package/dist/assertions/max-image-size.d.ts +0 -3
  530. package/dist/assertions/max-image-size.d.ts.map +0 -1
  531. package/dist/assertions/max-image-size.js +0 -73
  532. package/dist/assertions/max-image-size.js.map +0 -1
  533. package/dist/assertions/max-line-length.d.ts +0 -3
  534. package/dist/assertions/max-line-length.d.ts.map +0 -1
  535. package/dist/assertions/max-line-length.js +0 -68
  536. package/dist/assertions/max-line-length.js.map +0 -1
  537. package/dist/assertions/no-broken-fragment-links.d.ts +0 -3
  538. package/dist/assertions/no-broken-fragment-links.d.ts.map +0 -1
  539. package/dist/assertions/no-broken-fragment-links.js +0 -79
  540. package/dist/assertions/no-broken-fragment-links.js.map +0 -1
  541. package/dist/assertions/no-duplicate-headings.d.ts +0 -3
  542. package/dist/assertions/no-duplicate-headings.d.ts.map +0 -1
  543. package/dist/assertions/no-duplicate-headings.js +0 -66
  544. package/dist/assertions/no-duplicate-headings.js.map +0 -1
  545. package/dist/assertions/no-hard-tabs.d.ts +0 -3
  546. package/dist/assertions/no-hard-tabs.d.ts.map +0 -1
  547. package/dist/assertions/no-hard-tabs.js +0 -63
  548. package/dist/assertions/no-hard-tabs.js.map +0 -1
  549. package/dist/assertions/no-trailing-spaces.d.ts +0 -3
  550. package/dist/assertions/no-trailing-spaces.d.ts.map +0 -1
  551. package/dist/assertions/no-trailing-spaces.js +0 -72
  552. package/dist/assertions/no-trailing-spaces.js.map +0 -1
  553. package/dist/assertions/pattern.d.ts +0 -3
  554. package/dist/assertions/pattern.d.ts.map +0 -1
  555. package/dist/assertions/pattern.js +0 -39
  556. package/dist/assertions/pattern.js.map +0 -1
  557. package/dist/assertions/semantic-line-breaks.d.ts +0 -3
  558. package/dist/assertions/semantic-line-breaks.d.ts.map +0 -1
  559. package/dist/assertions/semantic-line-breaks.js +0 -152
  560. package/dist/assertions/semantic-line-breaks.js.map +0 -1
  561. package/dist/assertions/swap.d.ts +0 -3
  562. package/dist/assertions/swap.d.ts.map +0 -1
  563. package/dist/assertions/swap.js +0 -39
  564. package/dist/assertions/swap.js.map +0 -1
  565. package/dist/assertions/utils.d.ts +0 -8
  566. package/dist/assertions/utils.d.ts.map +0 -1
  567. package/dist/assertions/utils.js.map +0 -1
  568. package/dist/core/scope-parser.d.ts +0 -26
  569. package/dist/core/scope-parser.d.ts.map +0 -1
  570. package/dist/core/scope-parser.js +0 -110
  571. package/dist/core/scope-parser.js.map +0 -1
  572. package/dist/files.d.ts +0 -2
  573. package/dist/files.d.ts.map +0 -1
  574. package/dist/files.js +0 -39
  575. package/dist/files.js.map +0 -1
  576. package/dist/load-config.d.ts +0 -25
  577. package/dist/load-config.d.ts.map +0 -1
  578. package/dist/load-config.js +0 -104
  579. package/dist/load-config.js.map +0 -1
  580. package/dist/load.d.ts +0 -25
  581. package/dist/load.d.ts.map +0 -1
  582. package/dist/load.js +0 -112
  583. package/dist/load.js.map +0 -1
  584. package/dist/scope.d.ts +0 -26
  585. package/dist/scope.d.ts.map +0 -1
  586. package/dist/scope.js +0 -110
  587. package/dist/scope.js.map +0 -1
  588. package/dist/types.d.ts +0 -109
  589. package/dist/types.d.ts.map +0 -1
  590. package/dist/types.js.map +0 -1
  591. package/dist/validate.d.ts +0 -31
  592. package/dist/validate.d.ts.map +0 -1
  593. package/dist/validate.js +0 -154
  594. package/dist/validate.js.map +0 -1
  595. /package/dist/{types.js → parser/types.js} +0 -0
@@ -1,6 +1,15 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
1
3
  import Ajv from '@redocly/ajv';
2
4
  import addFormats from 'ajv-formats';
3
- import { RECHECK_CONFIG_SCHEMA } from './schema.js';
5
+ import * as yaml from 'js-yaml';
6
+ import { resolveMarkdocConfig } from '../parser/markdoc/schema.js';
7
+ import { RECHECK_CONFIG_SCHEMA, MARKDOC_TAG_SCHEMA } from './schema.js';
8
+ import { validateScopeSelector } from '../scopes/vocabulary.js';
9
+ import { tokenizeSelector, wholeDocumentKeywordProblems } from '../scopes/selector.js';
10
+ import { resolveExtends } from './presets/index.js';
11
+ import { resolveAssertion } from '../rules/registry.js';
12
+ import { resolveDictionaryPaths } from '../rules/scope/spelling.js';
4
13
  const ajv = new Ajv({
5
14
  useDefaults: true,
6
15
  allErrors: true,
@@ -8,6 +17,10 @@ const ajv = new Ajv({
8
17
  });
9
18
  addFormats(ajv); // mismatching AJV typing due to fork
10
19
  ajv.addSchema(RECHECK_CONFIG_SCHEMA, 'recheck-config');
20
+ // Compiled once so `markdoc.extend.tagsFile` entries get exactly the same
21
+ // per-tag shape check a config's own inline `extend.tags` gets from the
22
+ // schema above, without recompiling on every validate() call.
23
+ const validateMarkdocTagShape = ajv.compile(MARKDOC_TAG_SCHEMA);
11
24
  /**
12
25
  * Validates configuration structure using JSON Schema
13
26
  */
@@ -18,11 +31,22 @@ function validateStructure(config) {
18
31
  }
19
32
  const valid = validate(config);
20
33
  if (!valid && validate.errors) {
21
- return validate.errors.map((error) => ({
22
- message: `${error.instancePath || '/'}: ${error.message}`,
23
- path: error.instancePath,
24
- value: error.data,
25
- }));
34
+ return validate.errors.map((error) => {
35
+ // AJV's own `additionalProperties` message ("must NOT have additional
36
+ // properties") never names the offending key in `error.message`
37
+ // itself — it's only available on `error.params.additionalProperty`.
38
+ // Naming it here is what turns a schema-illegal key (e.g. the removed
39
+ // `autoFixable`) into an actionable, greppable error rather than a
40
+ // "which property?" guessing game.
41
+ const extra = error.keyword === 'additionalProperties' && error.params?.additionalProperty
42
+ ? ` (unknown property "${error.params.additionalProperty}")`
43
+ : '';
44
+ return {
45
+ message: `${error.instancePath || '/'}: ${error.message}${extra}`,
46
+ path: error.instancePath,
47
+ value: error.data,
48
+ };
49
+ });
26
50
  }
27
51
  return [];
28
52
  }
@@ -37,61 +61,1117 @@ function validateAssertions(rule, name, errors) {
37
61
  });
38
62
  return;
39
63
  }
40
- for (const [assertionType, _assertionConfig] of Object.entries(rule.assertions)) {
64
+ for (const assertionType of Object.keys(rule.assertions)) {
65
+ let resolved;
41
66
  try {
42
- switch (assertionType) {
43
- case 'swap':
44
- break;
45
- case 'pattern':
46
- break;
47
- case 'max-image-size':
48
- case 'max-line-length':
49
- case 'no-trailing-spaces':
50
- case 'bullet-style':
51
- case 'semantic-line-breaks':
52
- case 'no-hard-tabs':
53
- case 'no-duplicate-headings':
54
- case 'no-broken-fragment-links':
55
- break;
56
- default:
57
- errors.push({
58
- message: `Rule "${name}": unknown assertion type "${assertionType}"`,
59
- path: `${name}.assertions.${assertionType}`,
60
- });
61
- }
67
+ // Delegates to the same registry runRules() uses to dispatch
68
+ // assertions (rules/registry.ts resolveAssertion), rather than a
69
+ // hand-maintained list of known assertion ids duplicated here. Every
70
+ // scope AND token rule (including every markdownlint-ported rule
71
+ // registered via src/rules/token/index.ts) is "known" the moment
72
+ // it's registered, so this can't silently drift out of sync the way
73
+ // a hardcoded switch/case list did per rule-porting batch.
74
+ resolved = resolveAssertion(assertionType);
62
75
  }
63
- catch (error) {
76
+ catch {
64
77
  errors.push({
65
- message: `Rule "${name}": error validating assertion "${assertionType}": ${error.message}`,
78
+ message: `Rule "${name}": unknown assertion type "${assertionType}"`,
66
79
  path: `${name}.assertions.${assertionType}`,
67
80
  });
81
+ continue;
82
+ }
83
+ // Scope-rule assertions (pattern, occurrence, swap, ...) each have their
84
+ // own dedicated per-assertion validator below; token rules (the 53
85
+ // markdownlint-ported rules) had no option checking at all until now --
86
+ // see validateTokenRuleOptions.
87
+ if (resolved.kind === 'token') {
88
+ validateTokenRuleOptions(rule, name, assertionType, resolved.rule, errors);
89
+ }
90
+ }
91
+ }
92
+ // A misspelled option on a ported (token) rule used to validate clean and
93
+ // silently no-op -- invisible in a 100-rule style-guide config. Each token
94
+ // rule's own `defaults` object is the schema of record: it's the exact set
95
+ // of keys the rule reads off `ctx.config` (see e.g. rules/token/line-length.ts
96
+ // `defaults: { message, lineLength, codeBlocks, tables, headings, ... }`).
97
+ // `message` is always allowed because every token rule's `defaults` includes
98
+ // it (verified for all 53 ported rules) -- this is distinct from the
99
+ // RULE-level `message`/`severity`/`scope`/`fix`/`link`/`excludes`/
100
+ // `appliesTo`/`exceptions`/`assertions` keys, which are not assertion
101
+ // options and are validated elsewhere (schema.ts / validateAssertions).
102
+ function validateTokenRuleOptions(rule, name, id, tokenRule, errors) {
103
+ const optionsObject = requireOptionsObject(rule, name, id, errors);
104
+ if (!optionsObject)
105
+ return;
106
+ const allowed = new Set(Object.keys(tokenRule.defaults));
107
+ for (const key of Object.keys(optionsObject)) {
108
+ if (!allowed.has(key)) {
109
+ errors.push({
110
+ message: `Rule "${name}": unknown option "${key}" for assertion "${id}" (accepted: ${[...allowed].sort().join(', ')})`,
111
+ path: `${name}.assertions.${id}.${key}`,
112
+ value: key,
113
+ });
68
114
  }
69
115
  }
70
116
  }
71
117
  /**
72
- * Performs semantic validation and normalization
118
+ * Shared guard for every per-assertion option validator below: returns the
119
+ * assertion's options when they are a plain object, `undefined` when the
120
+ * assertion isn't configured on this rule, and pushes an "options must be
121
+ * an object" error for anything else. The JSON schema can't catch this
122
+ * shape mistake (`assertions` values are `additionalProperties: true`), so
123
+ * without it e.g. `occurrence: "oops"` validates cleanly and misbehaves at
124
+ * lint time.
73
125
  */
74
- function validateSemantics(config) {
126
+ function requireOptionsObject(rule, name, assertionId, errors) {
127
+ const assertions = rule.assertions;
128
+ if (!assertions || typeof assertions !== 'object' || !(assertionId in assertions)) {
129
+ return undefined;
130
+ }
131
+ const config = assertions[assertionId];
132
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
133
+ errors.push({
134
+ message: `Rule "${name}": ${assertionId} assertion options must be an object`,
135
+ path: `${name}.assertions.${assertionId}`,
136
+ });
137
+ return undefined;
138
+ }
139
+ return config;
140
+ }
141
+ // `negate` is included here (rather than left to fall through to the
142
+ // generic "unknown option" error below) so a config that sets it gets ONE
143
+ // specific, actionable message -- see the dedicated check in
144
+ // validatePatternOptions, not a redundant generic one alongside it.
145
+ const PATTERN_OPTION_KEYS = new Set(['tokens', 'ignoreCase', 'nonword', 'includeCode', 'negate']);
146
+ /**
147
+ * Rejects the removed `pattern` option `negate`. Git history shows it never
148
+ * functioned in ANY version of the engine — the check always sat inside the
149
+ * match-iteration loop, so `negate: true` reported nothing, ever, and a
150
+ * pattern's ABSENCE never reported either. Rather than silently ignoring a
151
+ * config key that reads like it inverts the rule, validation fails loudly;
152
+ * existence checks ("flag when a pattern is absent") are planned as a
153
+ * Vale-parity feature.
154
+ */
155
+ function validatePatternOptions(rule, name, errors) {
156
+ const patternConfig = requireOptionsObject(rule, name, 'pattern', errors);
157
+ if (!patternConfig)
158
+ return;
159
+ for (const key of Object.keys(patternConfig)) {
160
+ if (!PATTERN_OPTION_KEYS.has(key)) {
161
+ errors.push({
162
+ message: `Rule "${name}": unknown pattern option "${key}"`,
163
+ path: `${name}.assertions.pattern.${key}`,
164
+ });
165
+ }
166
+ }
167
+ if ('negate' in patternConfig) {
168
+ errors.push({
169
+ message: `Rule "${name}": pattern option "negate" was removed — it never worked ` +
170
+ `(it never reported anything); remove it from the config`,
171
+ path: `${name}.assertions.pattern.negate`,
172
+ });
173
+ }
174
+ // `tokens`, `ignoreCase`, and `nonword` used to have no type check at all --
175
+ // `tokens: "ab"` (a string, not an array) validated
176
+ // clean, then pattern.ts's `for (const token of tokens)` iterated the
177
+ // STRING CHARACTER BY CHARACTER ('a' and 'b' each compiled as their own
178
+ // regex), and `ignoreCase: "yes"` (any non-empty string is truthy)
179
+ // silently flipped case-sensitivity on a typo. `tokens` is required (not
180
+ // `tokens?:` -- see PatternAssertion in types/assertions.ts) and, like
181
+ // `swap`'s `pairs` and `consistency`'s `either`, an EMPTY tokens array can
182
+ // never report anything, so both shape and non-emptiness are checked here
183
+ // -- same reasoning as validateSwapOptions/validateConsistencyOptions.
184
+ const { tokens, ignoreCase, nonword } = patternConfig;
185
+ const isValidTokens = Array.isArray(tokens) &&
186
+ tokens.length > 0 &&
187
+ tokens.every((token) => typeof token === 'string');
188
+ if (!isValidTokens) {
189
+ errors.push({
190
+ message: `Rule "${name}": pattern requires "tokens" to be a non-empty array of strings`,
191
+ path: `${name}.assertions.pattern.tokens`,
192
+ });
193
+ }
194
+ if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
195
+ errors.push({
196
+ message: `Rule "${name}": pattern option "ignoreCase" must be a boolean`,
197
+ path: `${name}.assertions.pattern.ignoreCase`,
198
+ });
199
+ }
200
+ if (nonword !== undefined && typeof nonword !== 'boolean') {
201
+ errors.push({
202
+ message: `Rule "${name}": pattern option "nonword" must be a boolean`,
203
+ path: `${name}.assertions.pattern.nonword`,
204
+ });
205
+ }
206
+ // Default `false`: a match inside inline code is skipped, by range, not
207
+ // by masking the text (see rules/scope/pattern.ts), matching swap's
208
+ // `includeCode` option.
209
+ const includeCode = patternConfig.includeCode;
210
+ if (includeCode !== undefined && typeof includeCode !== 'boolean') {
211
+ errors.push({
212
+ message: `Rule "${name}": pattern option "includeCode" must be a boolean`,
213
+ path: `${name}.assertions.pattern.includeCode`,
214
+ });
215
+ }
216
+ }
217
+ /**
218
+ * Validates the `occurrence` assertion's options. Omitting BOTH `min` and
219
+ * `max` is an error — an occurrence assertion with no bound can never
220
+ * report anything — and so is an inverted range (`min` > `max`), which no
221
+ * count can satisfy.
222
+ */
223
+ const OCCURRENCE_OPTION_KEYS = new Set(['pattern', 'min', 'max', 'ignoreCase']);
224
+ function validateOccurrenceOptions(rule, name, errors) {
225
+ const occurrenceConfig = requireOptionsObject(rule, name, 'occurrence', errors);
226
+ if (!occurrenceConfig)
227
+ return;
228
+ for (const key of Object.keys(occurrenceConfig)) {
229
+ if (!OCCURRENCE_OPTION_KEYS.has(key)) {
230
+ errors.push({
231
+ message: `Rule "${name}": unknown occurrence option "${key}"`,
232
+ path: `${name}.assertions.occurrence.${key}`,
233
+ });
234
+ }
235
+ }
236
+ const { min, max, pattern } = occurrenceConfig;
237
+ if (min === undefined && max === undefined) {
238
+ errors.push({
239
+ message: `Rule "${name}": occurrence requires at least one of "min" or "max"`,
240
+ path: `${name}.assertions.occurrence`,
241
+ });
242
+ }
243
+ // `min`/`max` used to have no type check at all --
244
+ // `occurrence: { pattern: ",", max: "two" }` used to validate clean, then
245
+ // occurrence.ts's `count > "two"` is NaN-false (a number is never `>` a
246
+ // non-numeric string), so a max-bounded rule NEVER fires. Every sibling
247
+ // numeric validator (`metric`, `length`, `list-length`) already checks
248
+ // this; occurrence didn't.
249
+ if (min !== undefined && typeof min !== 'number') {
250
+ errors.push({
251
+ message: `Rule "${name}": occurrence option "min" must be a number`,
252
+ path: `${name}.assertions.occurrence.min`,
253
+ });
254
+ }
255
+ if (max !== undefined && typeof max !== 'number') {
256
+ errors.push({
257
+ message: `Rule "${name}": occurrence option "max" must be a number`,
258
+ path: `${name}.assertions.occurrence.max`,
259
+ });
260
+ }
261
+ if (typeof min === 'number' && typeof max === 'number' && min > max) {
262
+ errors.push({
263
+ message: `Rule "${name}": occurrence "min" (${min}) must not exceed "max" (${max})`,
264
+ path: `${name}.assertions.occurrence`,
265
+ });
266
+ }
267
+ // A missing/empty/non-string `pattern` silently compiles to an
268
+ // always-matching empty pattern in occurrence.ts's execute() — a
269
+ // max-bounded rule then floods every segment with false positives and a
270
+ // min-only rule can never fire. Reject loudly instead.
271
+ if (typeof pattern !== 'string' || pattern.length === 0) {
272
+ errors.push({
273
+ message: `Rule "${name}": occurrence requires a non-empty string "pattern"`,
274
+ path: `${name}.assertions.occurrence.pattern`,
275
+ });
276
+ }
277
+ }
278
+ /**
279
+ * Validates the `repetition` assertion's options. Both are optional
280
+ * (defaults `\w+` / `true` -- see rules/scope/repetition.ts), but when
281
+ * present `pattern` must be a non-empty string (an empty one compiles to an
282
+ * always-matching zero-width regex) and `ignoreCase` a boolean.
283
+ */
284
+ const REPETITION_OPTION_KEYS = new Set(['pattern', 'ignoreCase']);
285
+ function validateRepetitionOptions(rule, name, errors) {
286
+ const repetitionConfig = requireOptionsObject(rule, name, 'repetition', errors);
287
+ if (!repetitionConfig)
288
+ return;
289
+ for (const key of Object.keys(repetitionConfig)) {
290
+ if (!REPETITION_OPTION_KEYS.has(key)) {
291
+ errors.push({
292
+ message: `Rule "${name}": unknown repetition option "${key}"`,
293
+ path: `${name}.assertions.repetition.${key}`,
294
+ });
295
+ }
296
+ }
297
+ const { pattern, ignoreCase } = repetitionConfig;
298
+ if (pattern !== undefined && (typeof pattern !== 'string' || pattern.length === 0)) {
299
+ errors.push({
300
+ message: `Rule "${name}": repetition option "pattern" must be a non-empty string`,
301
+ path: `${name}.assertions.repetition.pattern`,
302
+ });
303
+ }
304
+ if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
305
+ errors.push({
306
+ message: `Rule "${name}": repetition option "ignoreCase" must be a boolean`,
307
+ path: `${name}.assertions.repetition.ignoreCase`,
308
+ });
309
+ }
310
+ }
311
+ /**
312
+ * Validates the `consistency` assertion's options. `either` is required and
313
+ * must be a non-empty object mapping one non-empty variant string to
314
+ * another -- with no pairs the assertion can never report anything, and an
315
+ * empty-string key would reach consistency.ts's scan loop as a zero-width
316
+ * regex.
317
+ */
318
+ const CONSISTENCY_OPTION_KEYS = new Set(['either', 'ignoreCase']);
319
+ function validateConsistencyOptions(rule, name, errors) {
320
+ const consistencyConfig = requireOptionsObject(rule, name, 'consistency', errors);
321
+ if (!consistencyConfig)
322
+ return;
323
+ for (const key of Object.keys(consistencyConfig)) {
324
+ if (!CONSISTENCY_OPTION_KEYS.has(key)) {
325
+ errors.push({
326
+ message: `Rule "${name}": unknown consistency option "${key}"`,
327
+ path: `${name}.assertions.consistency.${key}`,
328
+ });
329
+ }
330
+ }
331
+ const { either, ignoreCase } = consistencyConfig;
332
+ if (!either || typeof either !== 'object' || Array.isArray(either)) {
333
+ errors.push({
334
+ message: `Rule "${name}": consistency requires "either" to be an object mapping one variant to another (e.g. behavior: behaviour)`,
335
+ path: `${name}.assertions.consistency.either`,
336
+ });
337
+ }
338
+ else {
339
+ const entries = Object.entries(either);
340
+ if (entries.length === 0) {
341
+ errors.push({
342
+ message: `Rule "${name}": consistency "either" must declare at least one variant pair`,
343
+ path: `${name}.assertions.consistency.either`,
344
+ });
345
+ }
346
+ for (const [variant, alternative] of entries) {
347
+ if (variant.length === 0) {
348
+ errors.push({
349
+ message: `Rule "${name}": consistency "either" entry keys must be non-empty strings`,
350
+ path: `${name}.assertions.consistency.either`,
351
+ });
352
+ }
353
+ if (typeof alternative !== 'string' || alternative.length === 0) {
354
+ errors.push({
355
+ message: `Rule "${name}": consistency "either" entry "${variant}" must map to a non-empty string variant`,
356
+ path: `${name}.assertions.consistency.either.${variant}`,
357
+ });
358
+ }
359
+ }
360
+ }
361
+ if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
362
+ errors.push({
363
+ message: `Rule "${name}": consistency option "ignoreCase" must be a boolean`,
364
+ path: `${name}.assertions.consistency.ignoreCase`,
365
+ });
366
+ }
367
+ }
368
+ /**
369
+ * Validates the `conditional` assertion's options. Both `first` and
370
+ * `second` are required, non-empty strings. Deliberately does NOT check
371
+ * that they compile as regexes: like `pattern`'s `tokens`, they are raw
372
+ * user patterns and an invalid one silently produces zero problems at
373
+ * runtime (see conditional.ts).
374
+ */
375
+ const CONDITIONAL_OPTION_KEYS = new Set(['first', 'second', 'ignoreCase']);
376
+ function validateConditionalOptions(rule, name, errors) {
377
+ const conditionalConfig = requireOptionsObject(rule, name, 'conditional', errors);
378
+ if (!conditionalConfig)
379
+ return;
380
+ for (const key of Object.keys(conditionalConfig)) {
381
+ if (!CONDITIONAL_OPTION_KEYS.has(key)) {
382
+ errors.push({
383
+ message: `Rule "${name}": unknown conditional option "${key}"`,
384
+ path: `${name}.assertions.conditional.${key}`,
385
+ });
386
+ }
387
+ }
388
+ const { first, second, ignoreCase } = conditionalConfig;
389
+ if (typeof first !== 'string' || first.length === 0) {
390
+ errors.push({
391
+ message: `Rule "${name}": conditional requires a non-empty string "first"`,
392
+ path: `${name}.assertions.conditional.first`,
393
+ });
394
+ }
395
+ if (typeof second !== 'string' || second.length === 0) {
396
+ errors.push({
397
+ message: `Rule "${name}": conditional requires a non-empty string "second"`,
398
+ path: `${name}.assertions.conditional.second`,
399
+ });
400
+ }
401
+ if (ignoreCase !== undefined && typeof ignoreCase !== 'boolean') {
402
+ errors.push({
403
+ message: `Rule "${name}": conditional option "ignoreCase" must be a boolean`,
404
+ path: `${name}.assertions.conditional.ignoreCase`,
405
+ });
406
+ }
407
+ }
408
+ /**
409
+ * Validates the `capitalization` assertion's options. `match` is required
410
+ * and must be a non-empty string (a `$`-style or a custom regex — an
411
+ * invalid regex is deliberately NOT rejected here; it silently produces
412
+ * zero problems at runtime, like `pattern`'s `tokens`). `style` is accepted
413
+ * alongside ANY `match` value, not just `$title` (the only one it affects)
414
+ * — setting it elsewhere is a documented harmless no-op. `exceptions`
415
+ * entries must be non-empty strings or they'd silently never match in the
416
+ * exception lookup. `builtinVocabulary` (default `true`, see
417
+ * ../data/proper-nouns.ts) must be a boolean when present.
418
+ */
419
+ const CAPITALIZATION_OPTION_KEYS = new Set(['match', 'exceptions', 'style', 'builtinVocabulary']);
420
+ function validateCapitalizationOptions(rule, name, errors) {
421
+ const capitalizationConfig = requireOptionsObject(rule, name, 'capitalization', errors);
422
+ if (!capitalizationConfig)
423
+ return;
424
+ for (const key of Object.keys(capitalizationConfig)) {
425
+ if (!CAPITALIZATION_OPTION_KEYS.has(key)) {
426
+ errors.push({
427
+ message: `Rule "${name}": unknown capitalization option "${key}"`,
428
+ path: `${name}.assertions.capitalization.${key}`,
429
+ });
430
+ }
431
+ }
432
+ const { match, exceptions, style, builtinVocabulary } = capitalizationConfig;
433
+ if (builtinVocabulary !== undefined && typeof builtinVocabulary !== 'boolean') {
434
+ errors.push({
435
+ message: `Rule "${name}": capitalization option "builtinVocabulary" must be a boolean`,
436
+ path: `${name}.assertions.capitalization.builtinVocabulary`,
437
+ });
438
+ }
439
+ if (typeof match !== 'string' || match.length === 0) {
440
+ errors.push({
441
+ message: `Rule "${name}": capitalization requires a non-empty string "match" ` +
442
+ `($title, $sentence, $lower, $upper, or a regex pattern)`,
443
+ path: `${name}.assertions.capitalization.match`,
444
+ });
445
+ }
446
+ if (style !== undefined && style !== 'ap' && style !== 'chicago') {
447
+ errors.push({
448
+ message: `Rule "${name}": capitalization option "style" must be "ap" or "chicago"`,
449
+ path: `${name}.assertions.capitalization.style`,
450
+ });
451
+ }
452
+ if (exceptions !== undefined) {
453
+ const isValidExceptions = Array.isArray(exceptions) &&
454
+ exceptions.every((entry) => typeof entry === 'string' && entry.length > 0);
455
+ if (!isValidExceptions) {
456
+ errors.push({
457
+ message: `Rule "${name}": capitalization option "exceptions" must be an array of non-empty strings`,
458
+ path: `${name}.assertions.capitalization.exceptions`,
459
+ });
460
+ }
461
+ }
462
+ }
463
+ /**
464
+ * Validates the `metric` assertion's options. `formula` is required and
465
+ * must be one of the six formulas `computeReadability` supports -- an
466
+ * unrecognized value would otherwise throw at lint time. At least one of
467
+ * `min`/`max` is required, and an inverted range (`min` > `max`) is an
468
+ * error -- same reasoning as `occurrence` above.
469
+ */
470
+ const METRIC_OPTION_KEYS = new Set(['formula', 'min', 'max']);
471
+ const METRIC_FORMULAS = new Set([
472
+ 'flesch-reading-ease',
473
+ 'flesch-kincaid-grade',
474
+ 'gunning-fog',
475
+ 'smog',
476
+ 'coleman-liau',
477
+ 'automated-readability',
478
+ ]);
479
+ function validateMetricOptions(rule, name, errors) {
480
+ const metricConfig = requireOptionsObject(rule, name, 'metric', errors);
481
+ if (!metricConfig)
482
+ return;
483
+ for (const key of Object.keys(metricConfig)) {
484
+ if (!METRIC_OPTION_KEYS.has(key)) {
485
+ errors.push({
486
+ message: `Rule "${name}": unknown metric option "${key}"`,
487
+ path: `${name}.assertions.metric.${key}`,
488
+ });
489
+ }
490
+ }
491
+ const { formula, min, max } = metricConfig;
492
+ if (typeof formula !== 'string' || !METRIC_FORMULAS.has(formula)) {
493
+ errors.push({
494
+ message: `Rule "${name}": metric requires "formula" to be one of ${[...METRIC_FORMULAS].join(', ')}`,
495
+ path: `${name}.assertions.metric.formula`,
496
+ });
497
+ }
498
+ if (min === undefined && max === undefined) {
499
+ errors.push({
500
+ message: `Rule "${name}": metric requires at least one of "min" or "max"`,
501
+ path: `${name}.assertions.metric`,
502
+ });
503
+ }
504
+ if (min !== undefined && typeof min !== 'number') {
505
+ errors.push({
506
+ message: `Rule "${name}": metric option "min" must be a number`,
507
+ path: `${name}.assertions.metric.min`,
508
+ });
509
+ }
510
+ if (max !== undefined && typeof max !== 'number') {
511
+ errors.push({
512
+ message: `Rule "${name}": metric option "max" must be a number`,
513
+ path: `${name}.assertions.metric.max`,
514
+ });
515
+ }
516
+ if (typeof min === 'number' && typeof max === 'number' && min > max) {
517
+ errors.push({
518
+ message: `Rule "${name}": metric "min" (${min}) must not exceed "max" (${max})`,
519
+ path: `${name}.assertions.metric`,
520
+ });
521
+ }
522
+ }
523
+ /**
524
+ * Shared `min`/`max` integer-range check for `length` and `list-length`:
525
+ * both measure a COUNT
526
+ * that can never be negative (characters/words/sentences/list items), so a
527
+ * bound of `min: 0` (or any `min <= 0`) can NEVER be violated by a real
528
+ * count -- "must have at least 0 words" is vacuously true for every
529
+ * document -- and a negative `max` (e.g. `max: -1`) is violated by EVERY
530
+ * real count, since no count is ever less than a negative number. Neither is
531
+ * a meaningful bound; both are silent no-op/always-fire footguns. `min` must
532
+ * therefore be a positive integer and `max` a non-negative integer when
533
+ * present (`max: 0` is a real, meaningful "must be empty" bound, unlike a
534
+ * negative one). Fractional bounds (`min: 2.5`) are also rejected: both
535
+ * assertions always measure whole units, so a fractional bound could never
536
+ * be matched exactly either. Only runs when the value is ALREADY a number --
537
+ * a wrong-typed value is reported once by the caller's own type check, not
538
+ * duplicated here.
539
+ */
540
+ function validateCountBounds(name, assertionId, min, max, errors) {
541
+ if (typeof min === 'number' && (!Number.isInteger(min) || min < 1)) {
542
+ errors.push({
543
+ message: `Rule "${name}": ${assertionId} option "min" must be a positive integer ` +
544
+ `(${min} could never be violated by a real count)`,
545
+ path: `${name}.assertions.${assertionId}.min`,
546
+ });
547
+ }
548
+ if (typeof max === 'number' && (!Number.isInteger(max) || max < 0)) {
549
+ errors.push({
550
+ message: `Rule "${name}": ${assertionId} option "max" must be a non-negative integer ` +
551
+ `(${max} would be violated by every real count)`,
552
+ path: `${name}.assertions.${assertionId}.max`,
553
+ });
554
+ }
555
+ }
556
+ /**
557
+ * Validates the `list-length` assertion's options (rules/token/list-length.ts
558
+ * -- a Recheck-original TOKEN rule, not a markdownlint port). Unlike the
559
+ * scope-assertion validators above (occurrence, metric, ...), the generic
560
+ * `validateTokenRuleOptions` already rejects an unknown option name for every
561
+ * token rule -- derived from `Object.keys(rule.defaults)`, which declares
562
+ * both `min` and `max` (see list-length.ts's doc comment on its `max:
563
+ * undefined` default) -- so this only adds the type/range checks that
564
+ * mirror validateOccurrenceOptions/validateMetricOptions: `min`/`max` must be
565
+ * numbers when present, an inverted range (`min` > `max`) is an error -- no
566
+ * item count could ever satisfy it -- and `min`/`max`
567
+ * must additionally be a positive/non-negative INTEGER, per
568
+ * validateCountBounds above: unlike occurrence/metric, list-length's counts
569
+ * can never be negative, so `min: 0` can never fire and `max: -1` always
570
+ * fires, neither a meaningful bound. Omitting BOTH `min` and `max` is
571
+ * deliberately NOT an error here, unlike occurrence/metric: list-length's own
572
+ * `defaults.min` is 2, so an empty `list-length: {}` is already a complete,
573
+ * meaningful configuration (flag any list under 2 items), not a no-op
574
+ * assertion with nothing to check.
575
+ */
576
+ function validateListLengthOptions(rule, name, errors) {
577
+ const listLengthConfig = requireOptionsObject(rule, name, 'list-length', errors);
578
+ if (!listLengthConfig)
579
+ return;
580
+ const { min, max } = listLengthConfig;
581
+ if (min !== undefined && typeof min !== 'number') {
582
+ errors.push({
583
+ message: `Rule "${name}": list-length option "min" must be a number`,
584
+ path: `${name}.assertions.list-length.min`,
585
+ });
586
+ }
587
+ if (max !== undefined && typeof max !== 'number') {
588
+ errors.push({
589
+ message: `Rule "${name}": list-length option "max" must be a number`,
590
+ path: `${name}.assertions.list-length.max`,
591
+ });
592
+ }
593
+ if (typeof min === 'number' && typeof max === 'number' && min > max) {
594
+ errors.push({
595
+ message: `Rule "${name}": list-length "min" (${min}) must not exceed "max" (${max})`,
596
+ path: `${name}.assertions.list-length`,
597
+ });
598
+ }
599
+ validateCountBounds(name, 'list-length', min, max, errors);
600
+ }
601
+ /**
602
+ * Validates the `spelling` assertion's options. All are optional — an
603
+ * empty `spelling: {}` is valid (default dictionary). When present,
604
+ * `dictionary` must be a non-empty string, and `vocab`/`ignore` arrays of
605
+ * non-empty strings — an empty-string `ignore` pattern would compile to an
606
+ * always-matching regex, silencing every word. `builtinVocabulary` (default
607
+ * `true`, see ../data/proper-nouns.ts) must be a boolean when present.
608
+ */
609
+ const SPELLING_OPTION_KEYS = new Set(['dictionary', 'vocab', 'ignore', 'builtinVocabulary']);
610
+ function validateSpellingOptions(rule, name, errors) {
611
+ const spellingConfig = requireOptionsObject(rule, name, 'spelling', errors);
612
+ if (!spellingConfig)
613
+ return;
614
+ for (const key of Object.keys(spellingConfig)) {
615
+ if (!SPELLING_OPTION_KEYS.has(key)) {
616
+ errors.push({
617
+ message: `Rule "${name}": unknown spelling option "${key}"`,
618
+ path: `${name}.assertions.spelling.${key}`,
619
+ });
620
+ }
621
+ }
622
+ const { dictionary, vocab, ignore, builtinVocabulary } = spellingConfig;
623
+ if (builtinVocabulary !== undefined && typeof builtinVocabulary !== 'boolean') {
624
+ errors.push({
625
+ message: `Rule "${name}": spelling option "builtinVocabulary" must be a boolean`,
626
+ path: `${name}.assertions.spelling.builtinVocabulary`,
627
+ });
628
+ }
629
+ if (dictionary !== undefined && (typeof dictionary !== 'string' || dictionary.length === 0)) {
630
+ errors.push({
631
+ message: `Rule "${name}": spelling option "dictionary" must be a non-empty string`,
632
+ path: `${name}.assertions.spelling.dictionary`,
633
+ });
634
+ }
635
+ if (vocab !== undefined) {
636
+ const isValidVocab = Array.isArray(vocab) && vocab.every((word) => typeof word === 'string' && word.length > 0);
637
+ if (!isValidVocab) {
638
+ errors.push({
639
+ message: `Rule "${name}": spelling option "vocab" must be an array of non-empty strings`,
640
+ path: `${name}.assertions.spelling.vocab`,
641
+ });
642
+ }
643
+ }
644
+ if (ignore !== undefined) {
645
+ const isValidIgnore = Array.isArray(ignore) && ignore.every((word) => typeof word === 'string' && word.length > 0);
646
+ if (!isValidIgnore) {
647
+ errors.push({
648
+ message: `Rule "${name}": spelling option "ignore" must be an array of non-empty strings`,
649
+ path: `${name}.assertions.spelling.ignore`,
650
+ });
651
+ }
652
+ }
653
+ }
654
+ /**
655
+ * Validates the `length` assertion's options. `unit` is required and must be
656
+ * one of `'characters' | 'words' | 'sentences'` -- an unrecognized value
657
+ * would otherwise reach `length.ts`'s `measure()` and fall through to the
658
+ * word-tokenizer branch silently, scoring the wrong thing with no error.
659
+ * At least one of `min`/`max` is required, and an inverted range (`min` >
660
+ * `max`) is an error -- same reasoning as `occurrence`/`metric` above.
661
+ */
662
+ const LENGTH_OPTION_KEYS = new Set(['unit', 'min', 'max']);
663
+ const LENGTH_UNITS = new Set(['characters', 'words', 'sentences']);
664
+ function validateLengthOptions(rule, name, errors) {
665
+ const lengthConfig = requireOptionsObject(rule, name, 'length', errors);
666
+ if (!lengthConfig)
667
+ return;
668
+ for (const key of Object.keys(lengthConfig)) {
669
+ if (!LENGTH_OPTION_KEYS.has(key)) {
670
+ errors.push({
671
+ message: `Rule "${name}": unknown length option "${key}"`,
672
+ path: `${name}.assertions.length.${key}`,
673
+ });
674
+ }
675
+ }
676
+ const { unit, min, max } = lengthConfig;
677
+ if (typeof unit !== 'string' || !LENGTH_UNITS.has(unit)) {
678
+ errors.push({
679
+ message: `Rule "${name}": length requires "unit" to be one of ${[...LENGTH_UNITS].join(', ')}`,
680
+ path: `${name}.assertions.length.unit`,
681
+ });
682
+ }
683
+ if (min === undefined && max === undefined) {
684
+ errors.push({
685
+ message: `Rule "${name}": length requires at least one of "min" or "max"`,
686
+ path: `${name}.assertions.length`,
687
+ });
688
+ }
689
+ if (min !== undefined && typeof min !== 'number') {
690
+ errors.push({
691
+ message: `Rule "${name}": length option "min" must be a number`,
692
+ path: `${name}.assertions.length.min`,
693
+ });
694
+ }
695
+ if (max !== undefined && typeof max !== 'number') {
696
+ errors.push({
697
+ message: `Rule "${name}": length option "max" must be a number`,
698
+ path: `${name}.assertions.length.max`,
699
+ });
700
+ }
701
+ if (typeof min === 'number' && typeof max === 'number' && min > max) {
702
+ errors.push({
703
+ message: `Rule "${name}": length "min" (${min}) must not exceed "max" (${max})`,
704
+ path: `${name}.assertions.length`,
705
+ });
706
+ }
707
+ // `min: 0` (never
708
+ // violated -- a segment can't have fewer than 0 characters/words/
709
+ // sentences) and a negative `max` (always violated) are silent no-op/
710
+ // always-fire footguns, same reasoning as list-length's identical check
711
+ // above -- see validateCountBounds's doc comment.
712
+ validateCountBounds(name, 'length', min, max, errors);
713
+ }
714
+ /**
715
+ * Validates each find -> replace entry under `swap.pairs`: the KEY must be
716
+ * a non-empty string (an empty one escapes to a zero-width pattern in
717
+ * swap.ts's findMatches, same hazard as consistency's `either` keys); the
718
+ * VALUE must be a string, possibly empty -- an empty replacement is a
719
+ * legitimate "delete this word" swap.
720
+ */
721
+ function validateSwapPairEntries(entries, name, path, errors) {
722
+ for (const [key, value] of entries) {
723
+ const entryPath = `${path}.${key}`;
724
+ if (key.length === 0) {
725
+ errors.push({
726
+ message: `Rule "${name}": swap "pairs" entry keys must be non-empty strings`,
727
+ path: entryPath,
728
+ });
729
+ }
730
+ if (typeof value !== 'string') {
731
+ errors.push({
732
+ message: `Rule "${name}": swap "pairs" entry "${key}" must map to a string replacement`,
733
+ path: entryPath,
734
+ });
735
+ }
736
+ }
737
+ }
738
+ /**
739
+ * Validates the `swap` assertion's options. Exactly one shape is accepted
740
+ * -- the only one swap.ts's findMatches actually consumes:
741
+ *
742
+ * `{ ignoreCase?, wordBoundary?, keysAreRegex?, pairs: {find: replace} }`
743
+ *
744
+ * The legacy "direct" top-level shape (`swap: { he: they }`) is rejected
745
+ * with a migration hint: findMatches only ever reads `options.pairs`, so
746
+ * direct entries validated fine but were silently inert -- rejecting them
747
+ * turns that no-op into an actionable config error.
748
+ */
749
+ const SWAP_RESERVED_KEYS = new Set([
750
+ 'ignoreCase',
751
+ 'wordBoundary',
752
+ 'keysAreRegex',
753
+ 'pairs',
754
+ 'includeCode',
755
+ ]);
756
+ const SWAP_BOOLEAN_OPTION_KEYS = [
757
+ 'ignoreCase',
758
+ 'wordBoundary',
759
+ 'keysAreRegex',
760
+ 'includeCode',
761
+ ];
762
+ function validateSwapOptions(rule, name, errors) {
763
+ const swapConfig = requireOptionsObject(rule, name, 'swap', errors);
764
+ if (!swapConfig)
765
+ return;
766
+ for (const key of Object.keys(swapConfig)) {
767
+ if (!SWAP_RESERVED_KEYS.has(key)) {
768
+ errors.push({
769
+ message: `Rule "${name}": unknown swap option "${key}" -- swap does not accept ` +
770
+ `find -> replace entries at the top level; move find -> replace entries under "pairs:"`,
771
+ path: `${name}.assertions.swap.${key}`,
772
+ });
773
+ }
774
+ }
775
+ for (const key of SWAP_BOOLEAN_OPTION_KEYS) {
776
+ const value = swapConfig[key];
777
+ if (value !== undefined && typeof value !== 'boolean') {
778
+ errors.push({
779
+ message: `Rule "${name}": swap option "${key}" must be a boolean`,
780
+ path: `${name}.assertions.swap.${key}`,
781
+ });
782
+ }
783
+ }
784
+ if (!('pairs' in swapConfig)) {
785
+ errors.push({
786
+ message: `Rule "${name}": swap requires a "pairs" object mapping find -> replace strings`,
787
+ path: `${name}.assertions.swap.pairs`,
788
+ });
789
+ return;
790
+ }
791
+ const pairs = swapConfig.pairs;
792
+ if (!pairs ||
793
+ typeof pairs !== 'object' ||
794
+ Array.isArray(pairs) ||
795
+ Object.keys(pairs).length === 0) {
796
+ errors.push({
797
+ message: `Rule "${name}": swap option "pairs" must be a non-empty object mapping find -> replace strings`,
798
+ path: `${name}.assertions.swap.pairs`,
799
+ });
800
+ }
801
+ else {
802
+ validateSwapPairEntries(Object.entries(pairs), name, `${name}.assertions.swap.pairs`, errors);
803
+ }
804
+ }
805
+ /**
806
+ * Missing-peer validation for `spelling`: `nspell` and `dictionary-en` are
807
+ * OPTIONAL peer dependencies, so a config that enables `spelling` without
808
+ * them installed must fail here with an actionable install command rather
809
+ * than as a bare "Cannot find module" the first time a file is linted.
810
+ * Runs per spelling rule so a mix of default-dictionary and
811
+ * custom-dictionary rules gets the right install command for each; a
812
+ * config with no `spelling` assertion never reaches an `import()` call at
813
+ * all, keeping validate() lazy about the peers.
814
+ *
815
+ * ALSO validates that a custom `dictionary`
816
+ * path actually names a readable `.aff`/`.dic` pair: the check above only
817
+ * ever checked whether `nspell` itself imports, never whether the FILES a
818
+ * `dictionary` option points at exist — so a missing/unreadable custom
819
+ * dictionary used to pass validation cleanly and only fail (silently: see
820
+ * spelling.ts's `loadSpeller`/`spellerCache`) the first time a file was
821
+ * linted, disabling spelling for the rest of the process. Resolved via
822
+ * `resolveDictionaryPaths`, SHARED with spelling.ts's own
823
+ * `readCustomDictionary`, so validate() and the runtime can never disagree
824
+ * about which files a `dictionary` path names.
825
+ */
826
+ async function checkSpellingPeerDependencies(rules) {
827
+ const errors = [];
828
+ const reportedMessages = new Set();
829
+ const reportedDictionaryPaths = new Set();
830
+ for (const rule of rules) {
831
+ const spellingConfig = rule.assertions?.['spelling'];
832
+ if (!spellingConfig || typeof spellingConfig !== 'object')
833
+ continue;
834
+ const dictionaryPath = spellingConfig.dictionary;
835
+ const hasCustomDictionary = typeof dictionaryPath === 'string' && dictionaryPath.length > 0;
836
+ let missingPeer = false;
837
+ try {
838
+ await import('nspell');
839
+ }
840
+ catch {
841
+ missingPeer = true;
842
+ }
843
+ // A custom-dictionary rule never touches `dictionary-en` (see
844
+ // spelling.ts's loadDictionary), so its absence must not fail it.
845
+ if (!hasCustomDictionary) {
846
+ try {
847
+ await import('dictionary-en');
848
+ }
849
+ catch {
850
+ missingPeer = true;
851
+ }
852
+ }
853
+ if (missingPeer) {
854
+ const installCommand = hasCustomDictionary ? 'npm i nspell' : 'npm i nspell dictionary-en';
855
+ const peerNames = hasCustomDictionary ? '"nspell"' : '"nspell" and "dictionary-en"';
856
+ const message = `The spelling assertion requires the optional peer dependenc${hasCustomDictionary ? 'y' : 'ies'} ` +
857
+ `${peerNames} — run \`${installCommand}\` to enable it.`;
858
+ // Each distinct message is reported once, at the first offending
859
+ // rule's path; a mixed config still reports both install commands.
860
+ if (!reportedMessages.has(message)) {
861
+ reportedMessages.add(message);
862
+ errors.push({ message, path: `${rule.name}.assertions.spelling` });
863
+ }
864
+ }
865
+ // Independent of the peer-import check above: even when `nspell`
866
+ // imports fine, a custom `dictionary` option may still name files that
867
+ // don't exist or aren't readable. Dedupe by the raw dictionary path
868
+ // string so several rules sharing one bad path only report once.
869
+ if (hasCustomDictionary && !reportedDictionaryPaths.has(dictionaryPath)) {
870
+ reportedDictionaryPaths.add(dictionaryPath);
871
+ const { aff, dic } = resolveDictionaryPaths(dictionaryPath);
872
+ const unreadable = [];
873
+ for (const filePath of [aff, dic]) {
874
+ try {
875
+ await fs.access(filePath, fs.constants.R_OK);
876
+ }
877
+ catch {
878
+ unreadable.push(filePath);
879
+ }
880
+ }
881
+ if (unreadable.length > 0) {
882
+ errors.push({
883
+ message: `Rule "${rule.name}": spelling dictionary file${unreadable.length > 1 ? 's' : ''} ` +
884
+ `not found or not readable: ${unreadable.join(', ')}`,
885
+ path: `${rule.name}.assertions.spelling.dictionary`,
886
+ });
887
+ }
888
+ }
889
+ }
890
+ return errors;
891
+ }
892
+ /**
893
+ * Warns about a config that extends the `recheck/markdoc` preset without
894
+ * turning markdoc parsing on. The preset's four rules only look at
895
+ * `ctx.markdoc`, which the runner populates only when parsing is enabled, so
896
+ * such a config ships four rule entries that can never report. That is dead
897
+ * weight rather than a broken config, so this goes to `console.warn` (the
898
+ * validation result carries only errors, no warnings) and `isValid` stays
899
+ * `true`.
900
+ *
901
+ * Reads the raw, pre-`resolveExtends` `extends` array rather than the merged
902
+ * config: post-merge, the four rule keys the preset contributes are
903
+ * indistinguishable from a user hand-writing the same `recheck/markdoc-*` keys
904
+ * directly, which is a legitimate way to opt into only some of them and is not
905
+ * what this warning is about. The literal `"recheck/markdoc"` entry in
906
+ * `extends` is the one unambiguous signal that the preset itself was requested.
907
+ */
908
+ function warnStaleMarkdocPreset(extendsList, markdocEnabled, warnOnce) {
909
+ if (markdocEnabled || !Array.isArray(extendsList))
910
+ return;
911
+ if (!extendsList.includes('recheck/markdoc'))
912
+ return;
913
+ warnOnce('recheck: config extends "recheck/markdoc" but "markdoc" parsing is off — its four rules ' +
914
+ 'can never fire; set "markdoc: true" (or an object form) to enable them.');
915
+ }
916
+ /**
917
+ * Reads, parses, and shape-checks `markdoc.extend.tagsFile`, resolved
918
+ * relative to `configDir`. Returns the file's tags (already validated per
919
+ * entry against the same `MARKDOC_TAG_SCHEMA` inline `extend.tags` uses) plus
920
+ * any config errors; a non-empty error list means the caller must treat
921
+ * markdoc as disabled for this call, the same as any other structurally
922
+ * invalid markdoc shape.
923
+ *
924
+ * Deliberately does not touch the filesystem unless `raw` actually names a
925
+ * `tagsFile` -- a config with markdoc off, or with only inline `extend.tags`,
926
+ * must never probe for a file it never referenced.
927
+ */
928
+ async function loadMarkdocTagsFile(raw, configDir) {
929
+ const tagsFile = raw !== null && typeof raw === 'object' ? raw.extend?.tagsFile : undefined;
930
+ if (!tagsFile)
931
+ return { errors: [] };
932
+ const resolvedPath = path.resolve(configDir, tagsFile);
933
+ const errors = [];
934
+ let content;
935
+ try {
936
+ content = await fs.readFile(resolvedPath, 'utf8');
937
+ }
938
+ catch (error) {
939
+ errors.push({
940
+ path: '/markdoc/extend/tagsFile',
941
+ message: `markdoc.extend.tagsFile: could not read "${resolvedPath}": ${error.message}`,
942
+ });
943
+ return { errors };
944
+ }
945
+ let parsed;
946
+ try {
947
+ parsed = yaml.load(content);
948
+ }
949
+ catch (error) {
950
+ errors.push({
951
+ path: '/markdoc/extend/tagsFile',
952
+ message: `markdoc.extend.tagsFile: could not parse "${resolvedPath}" as YAML: ${error.message}`,
953
+ });
954
+ return { errors };
955
+ }
956
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
957
+ errors.push({
958
+ path: '/markdoc/extend/tagsFile',
959
+ message: `markdoc.extend.tagsFile: "${resolvedPath}" must be a YAML map of tag name to tag schema, got ${parsed === null ? 'null' : Array.isArray(parsed) ? 'an array' : typeof parsed}`,
960
+ });
961
+ return { errors };
962
+ }
963
+ const fileTags = {};
964
+ for (const [tagName, tagValue] of Object.entries(parsed)) {
965
+ if (!validateMarkdocTagShape(tagValue)) {
966
+ const detail = ajv.errorsText(validateMarkdocTagShape.errors, { separator: '; ' });
967
+ errors.push({
968
+ path: `/markdoc/extend/tagsFile/${tagName}`,
969
+ message: `markdoc.extend.tagsFile: "${resolvedPath}" tag "${tagName}" is invalid: ${detail}`,
970
+ });
971
+ continue;
972
+ }
973
+ fileTags[tagName] = tagValue;
974
+ }
975
+ // Any per-tag shape failure invalidates the whole file's contribution --
976
+ // partially trusting a file that failed its own shape check would silently
977
+ // merge an unvalidated tag into the resolved schema.
978
+ if (errors.length > 0)
979
+ return { errors };
980
+ return { fileTags, errors: [] };
981
+ }
982
+ /**
983
+ * Stale-pattern warning: a `pattern` assertion token that starts
984
+ * with the literal characters `^#` almost always indicates a config
985
+ * written for the pre-AST line-based scope extractor, where segment
986
+ * content still included the raw `#` heading marker. After the AST
987
+ * migration, non-`raw`/non-`all` scopes (e.g. `heading`, `sentence`,
988
+ * `paragraph`) hand `pattern` only the semantic TEXT of the segment — the
989
+ * literal markup is already stripped — so a token anchored on `^#` can
990
+ * never match and the rule silently does nothing. `scope: raw` (and the
991
+ * default `scope: all`, which also sees full raw file content) are exempt:
992
+ * both still see literal markup, so `^#` is a legitimate anchor there.
993
+ * This exact silent-death case was found in the repo's own recheck.yaml
994
+ * after the AST migration.
995
+ */
996
+ function warnStalePatternPrefix(rule, name, warnOnce) {
997
+ const patternConfig = rule.assertions?.['pattern'];
998
+ if (!patternConfig || !Array.isArray(patternConfig.tokens))
999
+ return;
1000
+ const scopeEntries = rule.scope === undefined ? [] : Array.isArray(rule.scope) ? rule.scope : [rule.scope];
1001
+ // Each scope entry may itself be a `&`-joined selector clause (e.g.
1002
+ // '~blockquote & ~heading') — split and check every term, since ANY
1003
+ // non-raw/non-all term in the selector means some matched segments will
1004
+ // be semantic-text-only. Parsed via the selector module's own tokenizer
1005
+ // so this check can't drift from how compileSelector reads the entry.
1006
+ const scopeTerms = scopeEntries.flatMap((entry) => typeof entry === 'string' ? tokenizeSelector(entry).map(({ term }) => term) : []);
1007
+ const hasRawOrAllScope = scopeEntries.length === 0 || scopeTerms.some((term) => term === 'raw' || term === 'all');
1008
+ if (hasRawOrAllScope)
1009
+ return;
1010
+ for (const token of patternConfig.tokens) {
1011
+ if (typeof token === 'string' && token.startsWith('^#')) {
1012
+ const scopeDisplay = Array.isArray(rule.scope) ? rule.scope.join(', ') : String(rule.scope);
1013
+ warnOnce(`recheck: Rule "${name}": pattern "${token}" starts with '^#' but scope "${scopeDisplay}" matches semantic text without markup — drop the '#' prefix or use scope: raw`);
1014
+ }
1015
+ }
1016
+ }
1017
+ /**
1018
+ * Vale parity: `metric` rules are ALWAYS summary-scoped — readability is a
1019
+ * whole-document score over the document's prose, which is exactly what the
1020
+ * `summary` scope segments carry (see scopes/extractor.ts). Any rule whose
1021
+ * assertions include `metric` gets `scope: 'summary'` forced here, so the
1022
+ * runner hands metric.ts the summary segments and the rule never re-extracts
1023
+ * scopes itself. A config that EXPLICITLY set some other scope gets a
1024
+ * warning that the scope is ignored — a warning, not an error, because the
1025
+ * rule still behaves correctly; ValidationResult has no warning channel
1026
+ * (only errors), so this uses console.warn via `warnOnce`, the
1027
+ * `warnStalePatternPrefix` precedent above. Explicitness comes from
1028
+ * `hasExplicitScope` (captured BEFORE schema validation): AJV `useDefaults`
1029
+ * injects `scope: 'all'` onto every rule that omitted it, so post-schema the
1030
+ * two cases are indistinguishable.
1031
+ */
1032
+ function normalizeMetricScope(rule, name, hasExplicitScope, warnOnce) {
1033
+ if (!rule.assertions || typeof rule.assertions !== 'object' || !('metric' in rule.assertions)) {
1034
+ return rule.scope;
1035
+ }
1036
+ if (hasExplicitScope) {
1037
+ const entries = Array.isArray(rule.scope) ? rule.scope : [rule.scope];
1038
+ const isSummary = entries.length === 1 && (entries[0] === 'summary' || entries[0] === 'default');
1039
+ if (!isSummary) {
1040
+ const scopeDisplay = Array.isArray(rule.scope) ? rule.scope.join(', ') : String(rule.scope);
1041
+ warnOnce(`recheck: Rule "${name}": metric is always summary-scoped; ignoring configured scope "${scopeDisplay}"`);
1042
+ }
1043
+ }
1044
+ return 'summary';
1045
+ }
1046
+ /**
1047
+ * Validates a rule's `scope` field against the full scope vocabulary and
1048
+ * selector syntax (optional `~` negation, `&`-joined terms). AJV only
1049
+ * checks the structural shape (string, or array of strings); this is the
1050
+ * term-level check that gives a helpful message naming the bad term.
1051
+ */
1052
+ function validateScope(rule, name, errors) {
1053
+ if (rule.scope === undefined)
1054
+ return;
1055
+ const entries = Array.isArray(rule.scope) ? rule.scope : [rule.scope];
1056
+ for (const entry of entries) {
1057
+ if (typeof entry !== 'string')
1058
+ continue; // caught by schema
1059
+ for (const problem of validateScopeSelector(entry)) {
1060
+ errors.push({
1061
+ message: `Rule "${name}": invalid scope — ${problem}`,
1062
+ path: `${name}.scope`,
1063
+ });
1064
+ }
1065
+ // `all`/`raw` as a TERM inside a compound or negated selector expression
1066
+ // (`heading & all`, `~all`, `~code & ~raw`) is the within-entry variant
1067
+ // of the array-mixing mistake rejected below: the conjunction form
1068
+ // compiles to a predicate that can never match (silently reporting
1069
+ // nothing), the negated form to one that matches every segment. The
1070
+ // shared helper (scopes/selector.ts) is also what makes compileSelector
1071
+ // throw on these shapes, so validation and compilation reject exactly
1072
+ // the same inputs.
1073
+ for (const problem of wholeDocumentKeywordProblems(entry)) {
1074
+ errors.push({
1075
+ message: `Rule "${name}": invalid scope — ${problem}`,
1076
+ path: `${name}.scope`,
1077
+ });
1078
+ }
1079
+ }
1080
+ // `all`/`raw` are whole-document keywords, not segment names — the
1081
+ // extractor never emits segments with those scopes, so combining either
1082
+ // with any other array entry (e.g. `scope: [all, code]`) can only ever
1083
+ // silently match nothing for the `all`/`raw` part. A single-element array
1084
+ // (`scope: ['all']`) is fine — compileSelector normalizes it to the bare
1085
+ // string's whole-document semantics — but a mix is a config mistake that
1086
+ // must fail loudly rather than validate and then report zero findings.
1087
+ if (entries.length > 1) {
1088
+ for (const entry of entries) {
1089
+ if (typeof entry !== 'string')
1090
+ continue; // caught by schema
1091
+ const term = entry.trim();
1092
+ if (term === 'all' || term === 'raw') {
1093
+ errors.push({
1094
+ message: `Rule "${name}": scope "${term}" covers the whole document and cannot be ` +
1095
+ `combined with other scopes — use \`scope: ${term}\` alone`,
1096
+ path: `${name}.scope`,
1097
+ });
1098
+ }
1099
+ }
1100
+ }
1101
+ }
1102
+ /**
1103
+ * Per-assertion `%s` message-placeholder caps. `metric` passes four values
1104
+ * (formula, score, min, max); `length` passes three (size, unit, bound --
1105
+ * see rules/scope/length.ts's FALLBACK_MAX/FALLBACK_MIN); every other
1106
+ * assertion passes at most two. A rule's cap is the largest among its
1107
+ * configured assertions, so a message can never declare more slots than its
1108
+ * assertion will ever fill.
1109
+ */
1110
+ const MESSAGE_PLACEHOLDER_CAPS = {
1111
+ metric: 4,
1112
+ length: 3,
1113
+ };
1114
+ const DEFAULT_MESSAGE_PLACEHOLDER_CAP = 2;
1115
+ function messagePlaceholderCap(rule) {
1116
+ const assertions = rule.assertions;
1117
+ const assertionIds = assertions && typeof assertions === 'object' ? Object.keys(assertions) : [];
1118
+ return assertionIds.reduce((cap, id) => Math.max(cap, MESSAGE_PLACEHOLDER_CAPS[id] ?? DEFAULT_MESSAGE_PLACEHOLDER_CAP), DEFAULT_MESSAGE_PLACEHOLDER_CAP);
1119
+ }
1120
+ function validateSemantics(config, rulesWithExplicitScope = new Set()) {
75
1121
  const errors = [];
76
1122
  const rules = [];
1123
+ // Dedupes the stale-pattern warning below to once per distinct message
1124
+ // for this whole validate() call — a config with the same stale pattern
1125
+ // shape on more than one rule only warns once per load.
1126
+ const warnedMessages = new Set();
1127
+ const warnOnce = (message) => {
1128
+ if (warnedMessages.has(message))
1129
+ return;
1130
+ warnedMessages.add(message);
1131
+ console.warn(message);
1132
+ };
77
1133
  for (const [key, rule] of Object.entries(config)) {
78
1134
  try {
79
1135
  // Derive name and shortName
80
1136
  const name = key;
81
1137
  const shortName = key.replace(/^recheck\//, '');
82
- // Validate message placeholder count
83
- const placeholderCount = (rule.message.match(/%s/g) || []).length;
84
- if (placeholderCount > 2) {
1138
+ // Validate message placeholder count against the rule's own
1139
+ // per-assertion cap (see MESSAGE_PLACEHOLDER_CAPS above). `rule.message`
1140
+ // is required by the JSON schema (see schema.ts `required`) so it is
1141
+ // always a string by the time a config passes AJV structural
1142
+ // validation; the `?? ''` only satisfies the now-optional
1143
+ // NormalizedRule/BaseRule type.
1144
+ const placeholderCount = ((rule.message ?? '').match(/%s/g) || []).length;
1145
+ const placeholderCap = messagePlaceholderCap(rule);
1146
+ if (placeholderCount > placeholderCap) {
85
1147
  errors.push({
86
- message: `Rule "${name}": message can have at most 2 %s placeholders, found ${placeholderCount}`,
1148
+ message: `Rule "${name}": message can have at most ${placeholderCap} %s placeholders, found ${placeholderCount}`,
87
1149
  path: `${name}.message`,
88
1150
  });
89
1151
  }
90
1152
  // Assertions validation
91
1153
  validateAssertions(rule, name, errors);
92
- // Create normalized rule
1154
+ // Removed `pattern` options (negate) must fail loudly, not no-op
1155
+ validatePatternOptions(rule, name, errors);
1156
+ validateOccurrenceOptions(rule, name, errors);
1157
+ validateRepetitionOptions(rule, name, errors);
1158
+ validateConsistencyOptions(rule, name, errors);
1159
+ validateConditionalOptions(rule, name, errors);
1160
+ validateCapitalizationOptions(rule, name, errors);
1161
+ validateMetricOptions(rule, name, errors);
1162
+ validateListLengthOptions(rule, name, errors);
1163
+ validateSpellingOptions(rule, name, errors);
1164
+ validateSwapOptions(rule, name, errors);
1165
+ validateLengthOptions(rule, name, errors);
1166
+ // Scope vocabulary/selector-syntax validation
1167
+ validateScope(rule, name, errors);
1168
+ // Stale `^#`-prefixed pattern token vs. non-raw/non-all scope
1169
+ warnStalePatternPrefix(rule, name, warnOnce);
1170
+ // Create normalized rule. `metric` rules are forced to
1171
+ // `scope: summary` (see normalizeMetricScope above).
93
1172
  const normalizedRule = {
94
1173
  ...rule,
1174
+ scope: normalizeMetricScope(rule, name, rulesWithExplicitScope.has(name), warnOnce),
95
1175
  name,
96
1176
  shortName,
97
1177
  };
@@ -107,23 +1187,99 @@ function validateSemantics(config) {
107
1187
  return { errors, rules };
108
1188
  }
109
1189
  /**
110
- * Full validation pipeline
1190
+ * Full validation pipeline. `options.configDir` (default `process.cwd()`) is
1191
+ * where a relative `markdoc.extend.tagsFile` resolves from -- the directory
1192
+ * containing the config file, so a project's `tagsFile: ./tags.yaml` behaves
1193
+ * the same regardless of the caller's own working directory.
111
1194
  */
112
- export async function validate(config) {
113
- const structureErrors = validateStructure(config);
1195
+ export async function validate(config, options) {
1196
+ // Resolve `extends` presets before schema validation of rules: the
1197
+ // merged (preset + user) config is what gets schema/semantic-validated,
1198
+ // so patternProperties only ever sees real `<namespace>/<rule>` rule keys
1199
+ // (`recheck/*` and, since the style-guide presets were added, `google/*`,
1200
+ // `microsoft/*`, and other preset-namespaced ids -- see schema.ts).
1201
+ // `extends` itself is schema-legal at the top level (see schema.ts) but
1202
+ // is stripped here — it is not a rule and must not reach rule iteration.
1203
+ // `resolveExtends` only fails to merge the UNRESOLVABLE preset name(s) it
1204
+ // reports in `extendsErrors` — every other preset and all of the user's
1205
+ // own top-level rule keys still land in `resolvedConfig` — so structure
1206
+ // and semantic validation below still run against everything that DID
1207
+ // resolve, instead of being skipped just because one `extends` entry
1208
+ // named an unknown preset. An unknown preset used to short-circuit semantic
1209
+ // validation entirely, hiding e.g. an unknown assertion id elsewhere in the
1210
+ // same config.
1211
+ const hasExtends = config && typeof config === 'object' && 'extends' in config;
1212
+ const { config: resolvedConfig, errors: extendsErrors } = hasExtends
1213
+ ? resolveExtends(config)
1214
+ : { config: config, errors: [] };
1215
+ // Which rules carry an EXPLICIT `scope`, recorded before validateStructure
1216
+ // runs: AJV `useDefaults` mutates the config in place, injecting
1217
+ // `scope: 'all'` onto every rule that omitted it, so this is the only
1218
+ // point where "configured" and "defaulted" scopes are distinguishable —
1219
+ // normalizeMetricScope needs the distinction to warn only about scopes a
1220
+ // user actually wrote.
1221
+ const rulesWithExplicitScope = new Set(resolvedConfig && typeof resolvedConfig === 'object'
1222
+ ? Object.entries(resolvedConfig)
1223
+ .filter(([key, rule]) => key !== 'extends' &&
1224
+ key !== 'markdoc' &&
1225
+ rule !== null &&
1226
+ typeof rule === 'object' &&
1227
+ 'scope' in rule)
1228
+ .map(([key]) => key)
1229
+ : []);
1230
+ const structureErrors = validateStructure(resolvedConfig);
1231
+ // `markdoc`, like `extends`, is an engine-level flag rather than a rule, so
1232
+ // it is read here before being stripped from rule iteration below.
1233
+ // `resolveMarkdocConfig` is defensive about the shape it is handed, so this
1234
+ // is safe to call even when `structureErrors` is about to report the same
1235
+ // value as invalid (e.g. `{ schema: 'bogus' }`).
1236
+ const rawMarkdoc = resolvedConfig?.markdoc;
1237
+ let { enabled: markdocEnabled, schema: markdocSchema } = resolveMarkdocConfig(rawMarkdoc);
1238
+ // The stale-preset warning is independent of structure and semantic
1239
+ // validity, so it runs here rather than after an error-return path below
1240
+ // could short-circuit it. A bare `console.warn` is enough: unlike
1241
+ // `warnStalePatternPrefix`, which runs once per rule and needs the deduping
1242
+ // `warnOnce`, this fires at most once per `validate()` call.
1243
+ warnStaleMarkdocPreset(hasExtends ? config.extends : undefined, markdocEnabled, (message) => console.warn(message));
114
1244
  if (structureErrors.length > 0) {
115
1245
  return {
116
1246
  isValid: false,
117
- errors: structureErrors,
1247
+ errors: [...extendsErrors, ...structureErrors],
118
1248
  rules: [],
1249
+ markdoc: { enabled: markdocEnabled, schema: markdocSchema },
1250
+ };
1251
+ }
1252
+ // Only reached once structural validation passed, so `rawMarkdoc`'s shape
1253
+ // (including `extend.tagsFile`, when present) is already known-good --
1254
+ // safe to resolve and read the file now. `loadMarkdocTagsFile` itself
1255
+ // never touches the filesystem when there's no `tagsFile` to load.
1256
+ const { fileTags, errors: tagsFileErrors } = await loadMarkdocTagsFile(rawMarkdoc, options?.configDir ?? process.cwd());
1257
+ if (tagsFileErrors.length > 0) {
1258
+ // Degrade exactly like any other invalid markdoc shape: a broken
1259
+ // tagsFile leaves no trustworthy schema for markdoc rules to run
1260
+ // against for this call.
1261
+ markdocEnabled = false;
1262
+ markdocSchema = null;
1263
+ }
1264
+ else if (fileTags) {
1265
+ const resolvedExtend = {
1266
+ fileTags,
1267
+ tags: rawMarkdoc && typeof rawMarkdoc === 'object' ? rawMarkdoc.extend?.tags : undefined,
119
1268
  };
1269
+ ({ enabled: markdocEnabled, schema: markdocSchema } = resolveMarkdocConfig(rawMarkdoc, resolvedExtend));
120
1270
  }
121
- // Then validate semantics
122
- const { errors: semanticErrors, rules } = validateSemantics(config);
1271
+ // Then validate semantics of everything that resolved successfully.
1272
+ // `markdoc` is stripped first, exactly as `extends` is stripped in
1273
+ // resolveExtends, so rule iteration in validateSemantics never sees it.
1274
+ const { markdoc: _markdoc, ...rulesOnlyConfig } = resolvedConfig;
1275
+ const { errors: semanticErrors, rules } = validateSemantics(rulesOnlyConfig, rulesWithExplicitScope);
1276
+ const peerErrors = await checkSpellingPeerDependencies(rules);
1277
+ const errors = [...extendsErrors, ...semanticErrors, ...peerErrors, ...tagsFileErrors];
123
1278
  return {
124
- isValid: semanticErrors.length === 0,
125
- errors: semanticErrors,
1279
+ isValid: errors.length === 0,
1280
+ errors,
126
1281
  rules,
1282
+ markdoc: { enabled: markdocEnabled, schema: markdocSchema },
127
1283
  };
128
1284
  }
129
1285
  //# sourceMappingURL=validate.js.map