chaffjs 0.20.0 → 0.21.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 (567) hide show
  1. package/README.md +5 -4
  2. package/dist/api.d.ts +16 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-text.d.ts +2 -0
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +14 -5
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +4 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/command-help.d.ts +7 -0
  13. package/dist/command-help.d.ts.map +1 -0
  14. package/dist/command-help.js +18 -0
  15. package/dist/command-help.js.map +1 -0
  16. package/dist/commands/eval.js +2 -2
  17. package/dist/commands/eval.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +11 -1
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/help.d.ts +4 -0
  22. package/dist/commands/help.d.ts.map +1 -0
  23. package/dist/commands/help.js +7 -0
  24. package/dist/commands/help.js.map +1 -0
  25. package/dist/config/load.d.ts +2 -0
  26. package/dist/config/load.d.ts.map +1 -1
  27. package/dist/config/load.js +1 -0
  28. package/dist/config/load.js.map +1 -1
  29. package/dist/config/option-problems.js +2 -2
  30. package/dist/config/option-problems.js.map +1 -1
  31. package/dist/config/rule-problems.js +2 -2
  32. package/dist/config/rule-problems.js.map +1 -1
  33. package/dist/custom/checked.d.ts +9 -0
  34. package/dist/custom/checked.d.ts.map +1 -0
  35. package/dist/custom/checked.js +3 -0
  36. package/dist/custom/checked.js.map +1 -0
  37. package/dist/custom/dsl-fields.d.ts +27 -0
  38. package/dist/custom/dsl-fields.d.ts.map +1 -0
  39. package/dist/custom/dsl-fields.js +86 -0
  40. package/dist/custom/dsl-fields.js.map +1 -0
  41. package/dist/custom/load.d.ts.map +1 -1
  42. package/dist/custom/load.js +2 -0
  43. package/dist/custom/load.js.map +1 -1
  44. package/dist/custom/parse.d.ts +14 -3
  45. package/dist/custom/parse.d.ts.map +1 -1
  46. package/dist/custom/parse.js +44 -40
  47. package/dist/custom/parse.js.map +1 -1
  48. package/dist/custom/problems.d.ts.map +1 -1
  49. package/dist/custom/problems.js +11 -1
  50. package/dist/custom/problems.js.map +1 -1
  51. package/dist/detectors/blank-structure.d.ts.map +1 -1
  52. package/dist/detectors/blank-structure.js +6 -4
  53. package/dist/detectors/blank-structure.js.map +1 -1
  54. package/dist/detectors/clause-shape.d.ts +22 -0
  55. package/dist/detectors/clause-shape.d.ts.map +1 -0
  56. package/dist/detectors/clause-shape.js +149 -0
  57. package/dist/detectors/clause-shape.js.map +1 -0
  58. package/dist/detectors/code-fences.d.ts +34 -0
  59. package/dist/detectors/code-fences.d.ts.map +1 -0
  60. package/dist/detectors/code-fences.js +100 -0
  61. package/dist/detectors/code-fences.js.map +1 -0
  62. package/dist/detectors/cross-definitions.d.ts +4 -0
  63. package/dist/detectors/cross-definitions.d.ts.map +1 -0
  64. package/dist/detectors/cross-definitions.js +43 -0
  65. package/dist/detectors/cross-definitions.js.map +1 -0
  66. package/dist/detectors/cross-facts.d.ts +4 -0
  67. package/dist/detectors/cross-facts.d.ts.map +1 -0
  68. package/dist/detectors/cross-facts.js +53 -0
  69. package/dist/detectors/cross-facts.js.map +1 -0
  70. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts +3 -0
  71. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts.map +1 -0
  72. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js +3 -0
  73. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js.map +1 -0
  74. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts +3 -0
  75. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts.map +1 -0
  76. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js +3 -0
  77. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js.map +1 -0
  78. package/dist/detectors/custom.d.ts.map +1 -1
  79. package/dist/detectors/custom.js +4 -1
  80. package/dist/detectors/custom.js.map +1 -1
  81. package/dist/detectors/defined-term-form.d.ts +25 -0
  82. package/dist/detectors/defined-term-form.d.ts.map +1 -0
  83. package/dist/detectors/defined-term-form.js +170 -0
  84. package/dist/detectors/defined-term-form.js.map +1 -0
  85. package/dist/detectors/email-letter.d.ts +46 -0
  86. package/dist/detectors/email-letter.d.ts.map +1 -0
  87. package/dist/detectors/email-letter.js +234 -0
  88. package/dist/detectors/email-letter.js.map +1 -0
  89. package/dist/detectors/impossible-date.d.ts +20 -0
  90. package/dist/detectors/impossible-date.d.ts.map +1 -0
  91. package/dist/detectors/impossible-date.js +105 -0
  92. package/dist/detectors/impossible-date.js.map +1 -0
  93. package/dist/detectors/impossible-time.d.ts +18 -0
  94. package/dist/detectors/impossible-time.d.ts.map +1 -0
  95. package/dist/detectors/impossible-time.js +116 -0
  96. package/dist/detectors/impossible-time.js.map +1 -0
  97. package/dist/detectors/link-shape.d.ts +11 -0
  98. package/dist/detectors/link-shape.d.ts.map +1 -0
  99. package/dist/detectors/link-shape.js +56 -0
  100. package/dist/detectors/link-shape.js.map +1 -0
  101. package/dist/detectors/mark-shape.d.ts +53 -0
  102. package/dist/detectors/mark-shape.d.ts.map +1 -0
  103. package/dist/detectors/mark-shape.js +160 -0
  104. package/dist/detectors/mark-shape.js.map +1 -0
  105. package/dist/detectors/registry/adverb-polarity.d.ts +3 -0
  106. package/dist/detectors/registry/adverb-polarity.d.ts.map +1 -0
  107. package/dist/detectors/registry/adverb-polarity.js +3 -0
  108. package/dist/detectors/registry/adverb-polarity.js.map +1 -0
  109. package/dist/detectors/registry/attachment-missing.d.ts +3 -0
  110. package/dist/detectors/registry/attachment-missing.d.ts.map +1 -0
  111. package/dist/detectors/registry/attachment-missing.js +3 -0
  112. package/dist/detectors/registry/attachment-missing.js.map +1 -0
  113. package/dist/detectors/registry/bare-url-mix.d.ts +3 -0
  114. package/dist/detectors/registry/bare-url-mix.d.ts.map +1 -0
  115. package/dist/detectors/registry/bare-url-mix.js +3 -0
  116. package/dist/detectors/registry/bare-url-mix.js.map +1 -0
  117. package/dist/detectors/registry/caps-shouting.d.ts +3 -0
  118. package/dist/detectors/registry/caps-shouting.d.ts.map +1 -0
  119. package/dist/detectors/registry/caps-shouting.js +3 -0
  120. package/dist/detectors/registry/caps-shouting.js.map +1 -0
  121. package/dist/detectors/registry/code-fence-language.d.ts +3 -0
  122. package/dist/detectors/registry/code-fence-language.d.ts.map +1 -0
  123. package/dist/detectors/registry/code-fence-language.js +3 -0
  124. package/dist/detectors/registry/code-fence-language.js.map +1 -0
  125. package/dist/detectors/registry/defined-name-repeated.d.ts +3 -0
  126. package/dist/detectors/registry/defined-name-repeated.d.ts.map +1 -0
  127. package/dist/detectors/registry/defined-name-repeated.js +3 -0
  128. package/dist/detectors/registry/defined-name-repeated.js.map +1 -0
  129. package/dist/detectors/registry/defined-term-form.d.ts +3 -0
  130. package/dist/detectors/registry/defined-term-form.d.ts.map +1 -0
  131. package/dist/detectors/registry/defined-term-form.js +3 -0
  132. package/dist/detectors/registry/defined-term-form.js.map +1 -0
  133. package/dist/detectors/registry/email-frame.d.ts +3 -0
  134. package/dist/detectors/registry/email-frame.d.ts.map +1 -0
  135. package/dist/detectors/registry/email-frame.js +3 -0
  136. package/dist/detectors/registry/email-frame.js.map +1 -0
  137. package/dist/detectors/registry/email-subject.d.ts +3 -0
  138. package/dist/detectors/registry/email-subject.d.ts.map +1 -0
  139. package/dist/detectors/registry/email-subject.js +3 -0
  140. package/dist/detectors/registry/email-subject.js.map +1 -0
  141. package/dist/detectors/registry/ending-run.d.ts +3 -0
  142. package/dist/detectors/registry/ending-run.d.ts.map +1 -0
  143. package/dist/detectors/registry/ending-run.js +3 -0
  144. package/dist/detectors/registry/ending-run.js.map +1 -0
  145. package/dist/detectors/registry/heading-command.d.ts +3 -0
  146. package/dist/detectors/registry/heading-command.d.ts.map +1 -0
  147. package/dist/detectors/registry/heading-command.js +3 -0
  148. package/dist/detectors/registry/heading-command.js.map +1 -0
  149. package/dist/detectors/registry/hyphen-dash.d.ts +3 -0
  150. package/dist/detectors/registry/hyphen-dash.d.ts.map +1 -0
  151. package/dist/detectors/registry/hyphen-dash.js +3 -0
  152. package/dist/detectors/registry/hyphen-dash.js.map +1 -0
  153. package/dist/detectors/registry/image-file-name-alt.d.ts +3 -0
  154. package/dist/detectors/registry/image-file-name-alt.d.ts.map +1 -0
  155. package/dist/detectors/registry/image-file-name-alt.js +3 -0
  156. package/dist/detectors/registry/image-file-name-alt.js.map +1 -0
  157. package/dist/detectors/registry/impossible-date.d.ts +3 -0
  158. package/dist/detectors/registry/impossible-date.d.ts.map +1 -0
  159. package/dist/detectors/registry/impossible-date.js +3 -0
  160. package/dist/detectors/registry/impossible-date.js.map +1 -0
  161. package/dist/detectors/registry/impossible-time.d.ts +3 -0
  162. package/dist/detectors/registry/impossible-time.d.ts.map +1 -0
  163. package/dist/detectors/registry/impossible-time.js +3 -0
  164. package/dist/detectors/registry/impossible-time.js.map +1 -0
  165. package/dist/detectors/registry/latin-abbreviation.d.ts +3 -0
  166. package/dist/detectors/registry/latin-abbreviation.d.ts.map +1 -0
  167. package/dist/detectors/registry/latin-abbreviation.js +3 -0
  168. package/dist/detectors/registry/latin-abbreviation.js.map +1 -0
  169. package/dist/detectors/registry/link-text-url-mismatch.d.ts +3 -0
  170. package/dist/detectors/registry/link-text-url-mismatch.d.ts.map +1 -0
  171. package/dist/detectors/registry/link-text-url-mismatch.js +3 -0
  172. package/dist/detectors/registry/link-text-url-mismatch.js.map +1 -0
  173. package/dist/detectors/registry/mark-consistency.d.ts +3 -0
  174. package/dist/detectors/registry/mark-consistency.d.ts.map +1 -0
  175. package/dist/detectors/registry/mark-consistency.js +3 -0
  176. package/dist/detectors/registry/mark-consistency.js.map +1 -0
  177. package/dist/detectors/registry/origin-particle.d.ts +3 -0
  178. package/dist/detectors/registry/origin-particle.d.ts.map +1 -0
  179. package/dist/detectors/registry/origin-particle.js +3 -0
  180. package/dist/detectors/registry/origin-particle.js.map +1 -0
  181. package/dist/detectors/registry/request-without-deadline.d.ts +3 -0
  182. package/dist/detectors/registry/request-without-deadline.d.ts.map +1 -0
  183. package/dist/detectors/registry/request-without-deadline.js +3 -0
  184. package/dist/detectors/registry/request-without-deadline.js.map +1 -0
  185. package/dist/detectors/registry/sentence-spacing.d.ts +3 -0
  186. package/dist/detectors/registry/sentence-spacing.d.ts.map +1 -0
  187. package/dist/detectors/registry/sentence-spacing.js +3 -0
  188. package/dist/detectors/registry/sentence-spacing.js.map +1 -0
  189. package/dist/detectors/registry/shell-prompt.d.ts +3 -0
  190. package/dist/detectors/registry/shell-prompt.d.ts.map +1 -0
  191. package/dist/detectors/registry/shell-prompt.js +3 -0
  192. package/dist/detectors/registry/shell-prompt.js.map +1 -0
  193. package/dist/detectors/registry/step-statement.d.ts +3 -0
  194. package/dist/detectors/registry/step-statement.d.ts.map +1 -0
  195. package/dist/detectors/registry/step-statement.js +3 -0
  196. package/dist/detectors/registry/step-statement.js.map +1 -0
  197. package/dist/detectors/registry/unpaired-parallel.d.ts +3 -0
  198. package/dist/detectors/registry/unpaired-parallel.d.ts.map +1 -0
  199. package/dist/detectors/registry/unpaired-parallel.js +3 -0
  200. package/dist/detectors/registry/unpaired-parallel.js.map +1 -0
  201. package/dist/detectors/request-owner.d.ts +23 -0
  202. package/dist/detectors/request-owner.d.ts.map +1 -0
  203. package/dist/detectors/request-owner.js +110 -0
  204. package/dist/detectors/request-owner.js.map +1 -0
  205. package/dist/detectors/technical-docs.d.ts +32 -0
  206. package/dist/detectors/technical-docs.d.ts.map +1 -0
  207. package/dist/detectors/technical-docs.js +199 -0
  208. package/dist/detectors/technical-docs.js.map +1 -0
  209. package/dist/extension/pack-read.d.ts +21 -0
  210. package/dist/extension/pack-read.d.ts.map +1 -0
  211. package/dist/extension/pack-read.js +117 -0
  212. package/dist/extension/pack-read.js.map +1 -0
  213. package/dist/extension/plugin-load.d.ts.map +1 -1
  214. package/dist/extension/plugin-load.js +44 -10
  215. package/dist/extension/plugin-load.js.map +1 -1
  216. package/dist/extension/plugin-parse.d.ts.map +1 -1
  217. package/dist/extension/plugin-parse.js +10 -3
  218. package/dist/extension/plugin-parse.js.map +1 -1
  219. package/dist/facts/address-values.d.ts +5 -0
  220. package/dist/facts/address-values.d.ts.map +1 -0
  221. package/dist/facts/address-values.js +64 -0
  222. package/dist/facts/address-values.js.map +1 -0
  223. package/dist/facts/cross-facts.d.ts +20 -0
  224. package/dist/facts/cross-facts.d.ts.map +1 -0
  225. package/dist/facts/cross-facts.js +41 -0
  226. package/dist/facts/cross-facts.js.map +1 -0
  227. package/dist/fix-plan/chosen-depth.d.ts +10 -0
  228. package/dist/fix-plan/chosen-depth.d.ts.map +1 -0
  229. package/dist/fix-plan/chosen-depth.js +24 -0
  230. package/dist/fix-plan/chosen-depth.js.map +1 -0
  231. package/dist/fix-plan/mode.d.ts +13 -4
  232. package/dist/fix-plan/mode.d.ts.map +1 -1
  233. package/dist/fix-plan/mode.js +38 -7
  234. package/dist/fix-plan/mode.js.map +1 -1
  235. package/dist/fix-plan/plan.d.ts +20 -0
  236. package/dist/fix-plan/plan.d.ts.map +1 -1
  237. package/dist/fix-plan/plan.js +23 -5
  238. package/dist/fix-plan/plan.js.map +1 -1
  239. package/dist/fix-plan/render.d.ts.map +1 -1
  240. package/dist/fix-plan/render.js +24 -0
  241. package/dist/fix-plan/render.js.map +1 -1
  242. package/dist/fix-plan/text.d.ts +8 -0
  243. package/dist/fix-plan/text.d.ts.map +1 -1
  244. package/dist/fix-plan/text.js +36 -10
  245. package/dist/fix-plan/text.js.map +1 -1
  246. package/dist/genre-load.d.ts +1 -1
  247. package/dist/genre-load.d.ts.map +1 -1
  248. package/dist/genre-load.js +7 -4
  249. package/dist/genre-load.js.map +1 -1
  250. package/dist/genre-parse.d.ts +6 -0
  251. package/dist/genre-parse.d.ts.map +1 -1
  252. package/dist/genre-parse.js +18 -0
  253. package/dist/genre-parse.js.map +1 -1
  254. package/dist/init.js +10 -10
  255. package/dist/init.js.map +1 -1
  256. package/dist/known-genres.d.ts +8 -0
  257. package/dist/known-genres.d.ts.map +1 -0
  258. package/dist/known-genres.js +17 -0
  259. package/dist/known-genres.js.map +1 -0
  260. package/dist/link-shape.d.ts +20 -0
  261. package/dist/link-shape.d.ts.map +1 -0
  262. package/dist/link-shape.js +58 -0
  263. package/dist/link-shape.js.map +1 -0
  264. package/dist/reasons.d.ts.map +1 -1
  265. package/dist/reasons.js +15 -4
  266. package/dist/reasons.js.map +1 -1
  267. package/dist/render/explain.d.ts.map +1 -1
  268. package/dist/render/explain.js +11 -2
  269. package/dist/render/explain.js.map +1 -1
  270. package/dist/render/friendly.js +1 -1
  271. package/dist/render/friendly.js.map +1 -1
  272. package/dist/render/rules-json.d.ts.map +1 -1
  273. package/dist/render/rules-json.js +12 -3
  274. package/dist/render/rules-json.js.map +1 -1
  275. package/dist/render/rules-table.js +6 -6
  276. package/dist/render/rules-table.js.map +1 -1
  277. package/dist/render/semantic.js +2 -2
  278. package/dist/render/semantic.js.map +1 -1
  279. package/dist/render/suppressions.js +2 -2
  280. package/dist/render/suppressions.js.map +1 -1
  281. package/dist/rewrite-depth.d.ts +22 -0
  282. package/dist/rewrite-depth.d.ts.map +1 -0
  283. package/dist/rewrite-depth.js +42 -0
  284. package/dist/rewrite-depth.js.map +1 -0
  285. package/dist/rule-fields.d.ts +23 -0
  286. package/dist/rule-fields.d.ts.map +1 -0
  287. package/dist/rule-fields.js +64 -0
  288. package/dist/rule-fields.js.map +1 -0
  289. package/dist/rule-guide.d.ts +3 -0
  290. package/dist/rule-guide.d.ts.map +1 -1
  291. package/dist/rule-guide.js +6 -0
  292. package/dist/rule-guide.js.map +1 -1
  293. package/dist/rule-load.d.ts.map +1 -1
  294. package/dist/rule-load.js +13 -5
  295. package/dist/rule-load.js.map +1 -1
  296. package/dist/rule-offs.d.ts +14 -0
  297. package/dist/rule-offs.d.ts.map +1 -0
  298. package/dist/rule-offs.js +45 -0
  299. package/dist/rule-offs.js.map +1 -0
  300. package/dist/structure/build.d.ts.map +1 -1
  301. package/dist/structure/build.js +14 -1
  302. package/dist/structure/build.js.map +1 -1
  303. package/dist/structure/cross-definitions.d.ts +23 -0
  304. package/dist/structure/cross-definitions.d.ts.map +1 -0
  305. package/dist/structure/cross-definitions.js +34 -0
  306. package/dist/structure/cross-definitions.js.map +1 -0
  307. package/genres.yaml +14 -313
  308. package/package.json +3 -3
  309. package/rules/absolute-exception.yaml +3 -1
  310. package/rules/abstract-length.yaml +7 -1
  311. package/rules/acronym-expansion-conflict.yaml +2 -1
  312. package/rules/adverb-overuse.yaml +7 -1
  313. package/rules/adverb-without-negation.yaml +60 -0
  314. package/rules/adversative-ga-repeat.yaml +2 -1
  315. package/rules/agentless-passive.yaml +10 -1
  316. package/rules/agreement-slip.yaml +4 -1
  317. package/rules/ai-generated-composite.yaml +6 -1
  318. package/rules/ai-structure.yaml +8 -2
  319. package/rules/ai-tell.yaml +7 -1
  320. package/rules/all-caps-shouting.yaml +61 -0
  321. package/rules/announced-count-mismatch.yaml +3 -1
  322. package/rules/announcing-opener.yaml +7 -1
  323. package/rules/article-sound.yaml +6 -2
  324. package/rules/assistant-residue.yaml +4 -1
  325. package/rules/attachment-not-attached.yaml +112 -0
  326. package/rules/baito-keigo.yaml +64 -0
  327. package/rules/bare-url-mix.yaml +86 -0
  328. package/rules/bold-density.yaml +1 -0
  329. package/rules/bold-label-list.yaml +9 -1
  330. package/rules/bracket-nesting.yaml +2 -1
  331. package/rules/bracket-width-consistency.yaml +64 -0
  332. package/rules/broken-link.yaml +1 -1
  333. package/rules/chat-citation-residue.yaml +2 -1
  334. package/rules/citation-reference-mismatch.yaml +7 -1
  335. package/rules/citation-style-mix.yaml +7 -1
  336. package/rules/clause-chain.yaml +6 -1
  337. package/rules/closing-cliche.yaml +7 -0
  338. package/rules/code-fence-language-mix.yaml +176 -0
  339. package/rules/colloquial-opener.yaml +62 -0
  340. package/rules/colon-lead-in.yaml +7 -1
  341. package/rules/concrete-evidence-density.yaml +6 -0
  342. package/rules/contraction-consistency.yaml +4 -1
  343. package/rules/contrast-framing.yaml +7 -1
  344. package/rules/cross-doc-duplicate-definition.yaml +70 -0
  345. package/rules/cross-doc-fact-conflict.yaml +64 -0
  346. package/rules/cushion-phrase-density.yaml +8 -1
  347. package/rules/dangling-figure-reference.yaml +3 -1
  348. package/rules/dangling-reference.yaml +3 -1
  349. package/rules/date-format-consistency.yaml +3 -2
  350. package/rules/date-order.yaml +3 -1
  351. package/rules/date-range-reversed.yaml +3 -1
  352. package/rules/date-weekday-mismatch.yaml +3 -1
  353. package/rules/date-without-year.yaml +4 -3
  354. package/rules/defined-name-repeated.yaml +97 -0
  355. package/rules/defined-term-form.yaml +113 -0
  356. package/rules/demonstrative-opener-run.yaml +2 -1
  357. package/rules/double-keigo.yaml +6 -1
  358. package/rules/double-negative.yaml +5 -1
  359. package/rules/doubled-nado.yaml +2 -1
  360. package/rules/doubled-punctuation.yaml +1 -1
  361. package/rules/doubled-word.yaml +4 -1
  362. package/rules/duplicate-definition.yaml +3 -1
  363. package/rules/duplicate-heading.yaml +1 -1
  364. package/rules/duration-mismatch.yaml +2 -1
  365. package/rules/elapsed-years-mismatch.yaml +4 -1
  366. package/rules/email-greeting-closing.yaml +122 -0
  367. package/rules/email-subject-length.yaml +112 -0
  368. package/rules/emoji-density.yaml +3 -1
  369. package/rules/emoji-heading.yaml +7 -1
  370. package/rules/empty-intensifier.yaml +4 -0
  371. package/rules/empty-list-item.yaml +2 -1
  372. package/rules/empty-section.yaml +1 -1
  373. package/rules/empty-table-cell.yaml +2 -1
  374. package/rules/excessive-hedging.yaml +7 -1
  375. package/rules/exclamation-width-consistency.yaml +64 -0
  376. package/rules/expletive-construction.yaml +7 -1
  377. package/rules/fact-conflict.yaml +4 -1
  378. package/rules/figure-reference-order.yaml +7 -1
  379. package/rules/fullwidth-alnum-consistency.yaml +2 -2
  380. package/rules/hankaku-kana.yaml +1 -1
  381. package/rules/heading-command-missing.yaml +109 -0
  382. package/rules/heading-echo.yaml +6 -1
  383. package/rules/heading-level-skip.yaml +3 -3
  384. package/rules/heading-numbering-mix.yaml +4 -3
  385. package/rules/hiragana-fukushi.yaml +4 -1
  386. package/rules/homophone-slip.yaml +5 -1
  387. package/rules/humble-for-others.yaml +63 -0
  388. package/rules/hyphen-as-dash.yaml +59 -0
  389. package/rules/idiom-misuse.yaml +4 -1
  390. package/rules/image-file-name-alt.yaml +83 -0
  391. package/rules/impossible-date.yaml +79 -0
  392. package/rules/impossible-time.yaml +76 -0
  393. package/rules/internal-jargon.yaml +1 -1
  394. package/rules/invisible-character.yaml +1 -1
  395. package/rules/katakana-density.yaml +2 -1
  396. package/rules/katakana-long-vowel.yaml +1 -1
  397. package/rules/known-misspelling.yaml +2 -1
  398. package/rules/kutoten-consistency.yaml +1 -1
  399. package/rules/latin-abbreviation-form.yaml +58 -0
  400. package/rules/latin-spacing.yaml +6 -2
  401. package/rules/link-text-url-mismatch.yaml +72 -0
  402. package/rules/list-item-form-mix.yaml +7 -1
  403. package/rules/max-kanji-continuous.yaml +10 -2
  404. package/rules/max-paragraph-length.yaml +4 -1
  405. package/rules/max-sentence-length.yaml +29 -21
  406. package/rules/max-ten.yaml +4 -1
  407. package/rules/misconversion.yaml +2 -1
  408. package/rules/modal-conflict.yaml +2 -1
  409. package/rules/name-variant.yaml +4 -3
  410. package/rules/ngram-repetition.yaml +5 -0
  411. package/rules/no-doubled-joshi.yaml +5 -1
  412. package/rules/no-em-dash.yaml +4 -1
  413. package/rules/no-lead.yaml +6 -1
  414. package/rules/no-mixed-desumasu.yaml +5 -2
  415. package/rules/no-nakaguro-parallel.yaml +3 -0
  416. package/rules/nominalization.yaml +5 -1
  417. package/rules/number-style-consistency.yaml +6 -1
  418. package/rules/number-without-unit.yaml +2 -1
  419. package/rules/numbering-gap.yaml +1 -1
  420. package/rules/one-sentence-paragraph-run.yaml +5 -1
  421. package/rules/orthographic-variant.yaml +4 -1
  422. package/rules/outside-announced-list.yaml +2 -1
  423. package/rules/oxford-comma-consistency.yaml +3 -1
  424. package/rules/padded-intro.yaml +7 -0
  425. package/rules/paragraph-length-variance.yaml +7 -1
  426. package/rules/paragraph-restatement.yaml +6 -1
  427. package/rules/percent-sum-mismatch.yaml +3 -1
  428. package/rules/person-consistency.yaml +4 -1
  429. package/rules/preamble-length.yaml +6 -0
  430. package/rules/preferred-term.yaml +1 -1
  431. package/rules/proper-noun-density.yaml +8 -1
  432. package/rules/quote-style-consistency.yaml +66 -0
  433. package/rules/ra-nuki.yaml +4 -1
  434. package/rules/redundant-expression.yaml +4 -1
  435. package/rules/reference-title-mismatch.yaml +4 -3
  436. package/rules/reference-topic-missing.yaml +2 -1
  437. package/rules/relative-date-mismatch.yaml +2 -1
  438. package/rules/repeated-conjunction.yaml +4 -1
  439. package/rules/repeated-sentence-ending.yaml +62 -0
  440. package/rules/repeated-sentence-head.yaml +4 -0
  441. package/rules/request-without-deadline.yaml +92 -0
  442. package/rules/required-sections.yaml +1 -1
  443. package/rules/requirement-modal.yaml +1 -1
  444. package/rules/requirement-smell.yaml +12 -4
  445. package/rules/rule-of-three.yaml +8 -1
  446. package/rules/sasete-itadaku.yaml +7 -1
  447. package/rules/section-length-uniformity.yaml +7 -1
  448. package/rules/sentence-initial-conjunction-run.yaml +5 -1
  449. package/rules/sentence-initial-so.yaml +6 -1
  450. package/rules/sentence-rhythm.yaml +8 -1
  451. package/rules/sentence-spacing-consistency.yaml +63 -0
  452. package/rules/shell-prompt-mix.yaml +128 -0
  453. package/rules/space-before-punctuation.yaml +1 -1
  454. package/rules/spelling-consistency.yaml +3 -1
  455. package/rules/step-statement-mix.yaml +76 -0
  456. package/rules/stock-transition.yaml +8 -1
  457. package/rules/stray-space.yaml +2 -2
  458. package/rules/summary-fact-mismatch.yaml +4 -1
  459. package/rules/table-header-variant.yaml +2 -1
  460. package/rules/taigen-dome-in-prose.yaml +6 -0
  461. package/rules/tari-unpaired.yaml +58 -0
  462. package/rules/teki-overuse.yaml +2 -1
  463. package/rules/title-case-consistency.yaml +1 -1
  464. package/rules/title-length.yaml +7 -1
  465. package/rules/topic-predicate-distance.yaml +6 -1
  466. package/rules/total-mismatch.yaml +3 -1
  467. package/rules/translationese-density.yaml +4 -1
  468. package/rules/unbalanced-bracket.yaml +3 -3
  469. package/rules/undefined-acronym.yaml +8 -1
  470. package/rules/unfilled-placeholder.yaml +1 -1
  471. package/rules/unit-mismatch.yaml +2 -1
  472. package/rules/unknown-word.yaml +4 -1
  473. package/rules/unqualified-superlative.yaml +6 -1
  474. package/rules/unused-definition.yaml +4 -3
  475. package/rules/url-run-on.yaml +1 -1
  476. package/rules/use-before-definition.yaml +4 -3
  477. package/rules/vague-figure-reference.yaml +3 -1
  478. package/rules/vague-link-text.yaml +2 -1
  479. package/rules/vague-word-density.yaml +2 -1
  480. package/rules/very-adjective.yaml +5 -1
  481. package/rules/weasel-word.yaml +6 -1
  482. package/rules/wordy-phrase.yaml +6 -1
  483. package/rules/yori-as-from.yaml +61 -0
  484. package/src/api.ts +13 -1
  485. package/src/cli-text.ts +18 -7
  486. package/src/cli.ts +3 -0
  487. package/src/command-help.ts +26 -0
  488. package/src/commands/eval.ts +2 -2
  489. package/src/commands/fix-plan.ts +13 -1
  490. package/src/commands/help.ts +8 -0
  491. package/src/config/load.ts +3 -0
  492. package/src/config/option-problems.ts +2 -2
  493. package/src/config/rule-problems.ts +2 -2
  494. package/src/custom/checked.ts +8 -0
  495. package/src/custom/dsl-fields.ts +102 -0
  496. package/src/custom/load.ts +2 -0
  497. package/src/custom/parse.ts +72 -50
  498. package/src/custom/problems.ts +18 -2
  499. package/src/detectors/blank-structure.ts +12 -4
  500. package/src/detectors/clause-shape.ts +199 -0
  501. package/src/detectors/code-fences.ts +113 -0
  502. package/src/detectors/cross-definitions.ts +49 -0
  503. package/src/detectors/cross-facts.ts +60 -0
  504. package/src/detectors/cross-registry/cross-doc-duplicate-definition.ts +4 -0
  505. package/src/detectors/cross-registry/cross-doc-fact-conflict.ts +4 -0
  506. package/src/detectors/custom.ts +10 -2
  507. package/src/detectors/defined-term-form.ts +200 -0
  508. package/src/detectors/email-letter.ts +262 -0
  509. package/src/detectors/impossible-date.ts +141 -0
  510. package/src/detectors/impossible-time.ts +151 -0
  511. package/src/detectors/link-shape.ts +70 -0
  512. package/src/detectors/mark-shape.ts +202 -0
  513. package/src/detectors/registry/adverb-polarity.ts +4 -0
  514. package/src/detectors/registry/attachment-missing.ts +4 -0
  515. package/src/detectors/registry/bare-url-mix.ts +4 -0
  516. package/src/detectors/registry/caps-shouting.ts +4 -0
  517. package/src/detectors/registry/code-fence-language.ts +4 -0
  518. package/src/detectors/registry/defined-name-repeated.ts +4 -0
  519. package/src/detectors/registry/defined-term-form.ts +4 -0
  520. package/src/detectors/registry/email-frame.ts +4 -0
  521. package/src/detectors/registry/email-subject.ts +4 -0
  522. package/src/detectors/registry/ending-run.ts +4 -0
  523. package/src/detectors/registry/heading-command.ts +4 -0
  524. package/src/detectors/registry/hyphen-dash.ts +4 -0
  525. package/src/detectors/registry/image-file-name-alt.ts +4 -0
  526. package/src/detectors/registry/impossible-date.ts +4 -0
  527. package/src/detectors/registry/impossible-time.ts +4 -0
  528. package/src/detectors/registry/latin-abbreviation.ts +4 -0
  529. package/src/detectors/registry/link-text-url-mismatch.ts +4 -0
  530. package/src/detectors/registry/mark-consistency.ts +4 -0
  531. package/src/detectors/registry/origin-particle.ts +4 -0
  532. package/src/detectors/registry/request-without-deadline.ts +4 -0
  533. package/src/detectors/registry/sentence-spacing.ts +4 -0
  534. package/src/detectors/registry/shell-prompt.ts +4 -0
  535. package/src/detectors/registry/step-statement.ts +4 -0
  536. package/src/detectors/registry/unpaired-parallel.ts +4 -0
  537. package/src/detectors/request-owner.ts +141 -0
  538. package/src/detectors/technical-docs.ts +230 -0
  539. package/src/extension/pack-read.ts +128 -0
  540. package/src/extension/plugin-load.ts +49 -9
  541. package/src/extension/plugin-parse.ts +10 -3
  542. package/src/facts/address-values.ts +74 -0
  543. package/src/facts/cross-facts.ts +54 -0
  544. package/src/fix-plan/chosen-depth.ts +28 -0
  545. package/src/fix-plan/mode.ts +57 -11
  546. package/src/fix-plan/plan.ts +37 -5
  547. package/src/fix-plan/render.ts +24 -0
  548. package/src/fix-plan/text.ts +47 -10
  549. package/src/genre-load.ts +7 -4
  550. package/src/genre-parse.ts +19 -0
  551. package/src/init.ts +10 -10
  552. package/src/known-genres.ts +19 -0
  553. package/src/link-shape.ts +62 -0
  554. package/src/reasons.ts +16 -4
  555. package/src/render/explain.ts +14 -2
  556. package/src/render/friendly.ts +1 -1
  557. package/src/render/rules-json.ts +14 -3
  558. package/src/render/rules-table.ts +6 -6
  559. package/src/render/semantic.ts +2 -2
  560. package/src/render/suppressions.ts +2 -2
  561. package/src/rewrite-depth.ts +60 -0
  562. package/src/rule-fields.ts +95 -0
  563. package/src/rule-guide.ts +9 -0
  564. package/src/rule-load.ts +10 -4
  565. package/src/rule-offs.ts +50 -0
  566. package/src/structure/build.ts +21 -1
  567. package/src/structure/cross-definitions.ts +60 -0
@@ -0,0 +1,230 @@
1
+ // Checks a README or a manual needs: code fences tagged with a language or not, shell commands written with and without
2
+ // a prompt, a heading that names a command its section's code never shows, and a step of a numbered procedure written
3
+ // as a statement among instructions. Pure. Shell languages and prompt marks come from the shell-fence-language and
4
+ // shell-prompt lexicons.
5
+ import { codeFences, codeSpans, type CodeFence } from "./code-fences.ts";
6
+ import { quoteAt } from "./structure-tree.ts";
7
+ import type { Detector, Finding, ProseDocument, Span, Token } from "../plugin.ts";
8
+
9
+ const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
10
+
11
+ /** The members of the less used of two kinds: at most limit of them, and fewer than the other kind. */
12
+ export const minorityOf = <T>(first: readonly T[], second: readonly T[], limit: number): readonly T[] => {
13
+ const [fewer, more] = first.length <= second.length ? [first, second] : [second, first];
14
+ return fewer.length > 0 && fewer.length < more.length && fewer.length <= limit ? fewer : [];
15
+ };
16
+
17
+ const finding = (doc: ProseDocument, rule: string, offset: number, values: Readonly<Record<string, string | number>> = {}): Finding => ({
18
+ rule,
19
+ severity: "info",
20
+ line: 0,
21
+ column: 0,
22
+ quote: quoteAt(doc.source, offset),
23
+ values: { ...values, offset },
24
+ });
25
+
26
+ /** Fences without a language, when they are the fewer kind: a document that mostly leaves the language out is not reported. */
27
+ export const untaggedFewer = (fences: readonly CodeFence[], limit: number): readonly CodeFence[] => {
28
+ const untagged = fences.filter((fence) => fence.language === "");
29
+ const tagged = fences.filter((fence) => fence.language !== "");
30
+ return untagged.length > 0 && untagged.length < tagged.length && untagged.length <= limit ? untagged : [];
31
+ };
32
+
33
+ export const codeFenceLanguage: Detector = (doc, options): Finding[] => {
34
+ const fences = codeFences(doc.source);
35
+ const tagged = fences.filter((fence) => fence.language !== "").length;
36
+ return untaggedFewer(fences, options.limit).map((fence) =>
37
+ finding(doc, "code-fence-language-mix", fence.start, { tagged, untagged: fences.length - tagged }),
38
+ );
39
+ };
40
+
41
+ export type PromptStyle = "prompt" | "bare" | undefined;
42
+
43
+ /**
44
+ * Whether a shell block writes its commands after a prompt ($ npm install) or bare (npm install). A block that mixes
45
+ * prompted and unprompted lines shows a command with its output, and is neither. Comment lines (#) are skipped.
46
+ */
47
+ export const promptStyleOf = (fence: CodeFence, prompts: readonly string[]): PromptStyle => {
48
+ const lines = fence.lines.map((line) => line.trim()).filter((line) => line !== "" && !line.startsWith("#"));
49
+ if (lines.length === 0) return undefined;
50
+ const prompted = lines.filter((line) => prompts.some((prompt) => line.startsWith(`${prompt} `))).length;
51
+ if (prompted === lines.length) return "prompt";
52
+ return prompted === 0 ? "bare" : undefined;
53
+ };
54
+
55
+ export const shellPrompt: Detector = (doc, options): Finding[] => {
56
+ const languages = new Set(patternsOf(doc, "shell-fence-language").map((language) => language.toLowerCase()));
57
+ const prompts = patternsOf(doc, "shell-prompt");
58
+ const shells = codeFences(doc.source).filter((fence) => languages.has(fence.language));
59
+ const prompted = shells.filter((fence) => promptStyleOf(fence, prompts) === "prompt");
60
+ const bare = shells.filter((fence) => promptStyleOf(fence, prompts) === "bare");
61
+ const minority = minorityOf(prompted, bare, options.limit);
62
+ const style = minority === prompted ? "prompt" : "bare";
63
+ return minority.map((fence) => finding(doc, "shell-prompt-mix", fence.start, { majority: style === "prompt" ? bare.length : prompted.length }));
64
+ };
65
+
66
+ /**
67
+ * A code span that names a command or an option: a flag (--force, -f) or a command with its arguments (npm install). A
68
+ * single plain word (net, init) is as often a package, an endpoint or a type as a command, and is left out.
69
+ */
70
+ const COMMAND_WORD = /^[a-z][a-z0-9_-]*$/u;
71
+ const FLAG = /^--?[A-Za-z][\w-]*$/u;
72
+
73
+ export const isCommandName = (text: string): boolean => {
74
+ const words = text.trim().split(/\s+/u);
75
+ const first = words[0] ?? "";
76
+ return words.length === 1 ? FLAG.test(first) : COMMAND_WORD.test(first);
77
+ };
78
+
79
+ /** The heading without its links' text: [`net`](/pkg/net/) names the page it links to, not a command shown below. */
80
+ export const withoutLinks = (heading: string): string => {
81
+ const pieces: string[] = [];
82
+ const state = { at: 0 };
83
+ while (state.at < heading.length) {
84
+ const open = heading.indexOf("[", state.at);
85
+ const close = open === -1 ? -1 : heading.indexOf("]", open);
86
+ if (close === -1) break;
87
+ pieces.push(heading.slice(state.at, open), " ");
88
+ state.at = close + 1 + targetLength(heading, close + 1);
89
+ }
90
+ pieces.push(heading.slice(state.at));
91
+ return pieces.join("");
92
+ };
93
+
94
+ /** The length of a link's target right after its text: (url), [label] or [] (a reference link); 0 when there is none. */
95
+ const targetLength = (heading: string, at: number): number => {
96
+ const pair: Readonly<Record<string, string>> = { "(": ")", "[": "]" };
97
+ const closer = pair[heading.charAt(at)];
98
+ const end = closer === undefined ? -1 : heading.indexOf(closer, at);
99
+ return end === -1 ? 0 : end - at + 1;
100
+ };
101
+
102
+ /** The commands and options a heading names: its code spans that read as one, and bare flags (--force). */
103
+ export const commandsIn = (heading: string): string[] => {
104
+ const spans = codeSpans(withoutLinks(heading)).map((span) => span.text);
105
+ const flags = heading
106
+ .replace(/`[^`]*`/gu, " ")
107
+ .split(/\s+/u)
108
+ .filter((word) => FLAG.test(word) && word.startsWith("--"));
109
+ return [...new Set([...spans.filter(isCommandName), ...flags])];
110
+ };
111
+
112
+ const collapse = (text: string): string => text.replace(/\s+/gu, " ");
113
+ const OPTION_CHAR = /[A-Za-z0-9_-]/u;
114
+
115
+ /** Whether the code holds the command as a whole word: --force is not --force-with-lease, init is not initial. */
116
+ export const showsCommand = (code: string, command: string): boolean => {
117
+ const text = collapse(code);
118
+ const wanted = collapse(command.trim());
119
+ const state = { at: text.indexOf(wanted) };
120
+ while (state.at !== -1) {
121
+ const before = text.charAt(state.at - 1);
122
+ const after = text.charAt(state.at + wanted.length);
123
+ if (!OPTION_CHAR.test(before) && !OPTION_CHAR.test(after)) return true;
124
+ state.at = text.indexOf(wanted, state.at + 1);
125
+ }
126
+ return false;
127
+ };
128
+
129
+ type Section = { readonly heading: string; readonly start: number; readonly body: Span };
130
+
131
+ const sectionsOf = (doc: ProseDocument): Section[] => {
132
+ const headings = doc.markup?.headings ?? [];
133
+ return headings.map((heading, index) => {
134
+ const next = headings.slice(index + 1).find((later) => later.depth <= heading.depth);
135
+ return { heading: doc.source.slice(heading.start, heading.end), start: heading.start, body: { start: heading.end, end: next?.start ?? doc.source.length } };
136
+ });
137
+ };
138
+
139
+ /** The code a section shows: its fenced blocks and its inline code spans. Empty when it shows none. */
140
+ const codeOf = (doc: ProseDocument, fences: readonly CodeFence[], body: Span): string => {
141
+ const inFences = fences.filter((fence) => fence.start >= body.start && fence.start < body.end);
142
+ const outside = inFences.reduceRight(
143
+ (text, fence) => `${text.slice(0, fence.start - body.start)}${text.slice(Math.min(fence.end, body.end) - body.start)}`,
144
+ doc.source.slice(body.start, body.end),
145
+ );
146
+ return [...inFences.flatMap((fence) => fence.lines), ...codeSpans(outside).map((span) => span.text)].join("\n");
147
+ };
148
+
149
+ export const headingCommand: Detector = (doc): Finding[] => {
150
+ const fences = codeFences(doc.source);
151
+ return sectionsOf(doc).flatMap((section) => {
152
+ const commands = commandsIn(section.heading);
153
+ if (commands.length === 0) return [];
154
+ const code = codeOf(doc, fences, section.body);
155
+ if (code.trim() === "") return [];
156
+ return commands.filter((command) => !showsCommand(code, command)).map((command) => finding(doc, "heading-command-missing", section.start, { command }));
157
+ });
158
+ };
159
+
160
+ const ORDERED_ITEM = /^\s*\d{1,3}[.)]\s/u;
161
+ /** What opens a statement: an article or a pronoun (The installer asks, You click, It opens). */
162
+ const SUBJECT_OPENER = new Set(["DET", "PRON"]);
163
+ /** What a subject opens with when it is a name or a noun phrase (Docker builds, Each user gets). */
164
+ const SUBJECT_HEAD = new Set(["DET", "PRON", "PROPN", "NOUN", "ADJ"]);
165
+ /** What may stand between the opener and the verb: the rest of the subject. */
166
+ const SUBJECT_PART = new Set(["DET", "PRON", "PROPN", "NOUN", "ADJ", "ADP"]);
167
+ /** What opens an instruction. The tagger reads a sentence-initial imperative as a noun, an adjective or a name (Click, Open, Restart). */
168
+ const INSTRUCTION_OPENER = new Set(["VERB", "NOUN", "ADJ", "PROPN"]);
169
+ const PREDICATE = new Set(["VERB", "AUX"]);
170
+ const SKIPPED = new Set(["PUNCT", "NUM", "SYM", "X", "SPACE"]);
171
+ /** Steps shorter than this are labels (1. Setup), not instructions or statements. */
172
+ const MIN_STEP_TOKENS = 3;
173
+ /** A list needs this many steps to have a form to keep. */
174
+ const MIN_STEPS = 3;
175
+
176
+ /** A verb in a finite, inflected form (starts, built): it has a subject before it. A base form (Save) may be an imperative's object. */
177
+ const isInflected = (token: Token): boolean =>
178
+ token.lemma !== undefined && token.lemma !== "" && token.surface.toLowerCase() !== token.lemma && token.features?.["VerbForm"] !== "Part";
179
+
180
+ /**
181
+ * A step written as a statement: a subject, then its verb. The subject opens with an article or a pronoun (The installer
182
+ * asks, You click), or it is a name or a noun whose verb is inflected (Docker builds). A noun before a base-form verb is
183
+ * not enough: the tagger reads Click and Press there as nouns (Click Save).
184
+ */
185
+ export const isStatement = (tokens: readonly Token[]): boolean => {
186
+ const words = tokens.filter((token) => !SKIPPED.has(token.pos));
187
+ const [head, ...rest] = words;
188
+ if (head === undefined || words.length < MIN_STEP_TOKENS || !SUBJECT_HEAD.has(head.pos)) return false;
189
+ const verbAt = rest.findIndex((token) => PREDICATE.has(token.pos));
190
+ const verb = rest[verbAt];
191
+ if (verb === undefined || !rest.slice(0, verbAt).every((token) => SUBJECT_PART.has(token.pos))) return false;
192
+ // Punctuation before the verb is no subject: a label (Guests: the users who …) or an opening phrase (On the left bar, select …).
193
+ const interrupted = tokens.some((token) => token.pos === "PUNCT" && token.span.start > head.span.start && token.span.start < verb.span.start);
194
+ return !interrupted && (SUBJECT_OPENER.has(head.pos) || isInflected(verb));
195
+ };
196
+
197
+ type Step = { readonly start: number; readonly kind: "statement" | "instruction" | "other" };
198
+
199
+ /** A step that opens with its verb: an instruction (Run the installer). */
200
+ export const isInstruction = (tokens: readonly Token[]): boolean => INSTRUCTION_OPENER.has(tokens.find((token) => !SKIPPED.has(token.pos))?.pos ?? "");
201
+
202
+ const kindOf = (tokens: readonly Token[]): Step["kind"] => {
203
+ if (isStatement(tokens)) return "statement";
204
+ return isInstruction(tokens) ? "instruction" : "other";
205
+ };
206
+
207
+ /** The steps of each numbered list: each item's first sentence, read as a statement or not. */
208
+ const stepLists = (doc: ProseDocument): Step[][] =>
209
+ doc.lists
210
+ .filter((list) => ORDERED_ITEM.test(doc.source.slice(list.span.start, list.span.end)))
211
+ .map((list) =>
212
+ list.itemSpans.flatMap((item): Step[] => {
213
+ const sentence = doc.sentences.find((candidate) => candidate.span.start >= item.start && candidate.span.start < item.end);
214
+ // Only the sentence on the item's own line: a note paragraph under it (**NOTE:** …) is not the step.
215
+ const onItemLine = sentence !== undefined && !doc.source.slice(item.start, sentence.span.start).includes("\n");
216
+ return sentence?.tokens === undefined || !onItemLine ? [] : [{ start: sentence.span.start, kind: kindOf(sentence.tokens) }];
217
+ }),
218
+ )
219
+ .filter((steps) => steps.length >= MIN_STEPS);
220
+
221
+ export const stepStatement: Detector = (doc, options): Finding[] =>
222
+ stepLists(doc).flatMap((steps) => {
223
+ const statements = steps.filter((step) => step.kind === "statement");
224
+ const instructions = steps.filter((step) => step.kind === "instruction");
225
+ // A procedure is a list most of whose steps are instructions; a numbered list of terms and definitions is not one.
226
+ const isProcedure = instructions.length * 2 > steps.length;
227
+ return isProcedure && statements.length > 0 && statements.length < instructions.length && statements.length <= options.limit
228
+ ? statements.map((step) => finding(doc, "step-statement-mix", step.start, { count: instructions.length }))
229
+ : [];
230
+ });
@@ -0,0 +1,128 @@
1
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
2
+ import { basename, join } from "node:path";
3
+ import { parse } from "yaml";
4
+ import { API_VERSION } from "../api.ts";
5
+ import { isInside } from "../custom/module-path.ts";
6
+
7
+ // A rule pack: a plugin written in YAML alone. Its folder holds a manifest (chaff-plugin.yaml, or a chaff field in
8
+ // package.json), rules/*.yaml, lexicons/<language>/<list>.yaml and styles/*.yaml. Read here into the shape a code
9
+ // plugin's default export has, so plugin-parse.ts checks a pack exactly as it checks a code plugin. Nothing is run.
10
+
11
+ export const PACK_MANIFEST = "chaff-plugin.yaml";
12
+
13
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
14
+
15
+ const printed = (value: unknown): string => JSON.stringify(value) ?? typeof value;
16
+
17
+ const firstLine = (error: unknown): string => (error instanceof Error ? error.message : String(error)).split("\n")[0] ?? "";
18
+
19
+ /** A YAML file, or why it cannot be read, naming it as the pack's own path (rules/no-tbd.yaml). */
20
+ const readYaml = (dir: string, path: string): { readonly value: unknown } | { readonly message: string } => {
21
+ try {
22
+ // A link inside the pack that points out of it would read a file the pack does not hold.
23
+ if (!isInside(realpathSync(dir), realpathSync(join(dir, path)))) return { message: `${path}: it links to a file outside the pack` };
24
+ return { value: parse(readFileSync(join(dir, path), "utf8")) };
25
+ } catch (error) {
26
+ return { message: `${path}: ${firstLine(error)}` };
27
+ }
28
+ };
29
+
30
+ const yamlFilesIn = (dir: string, folder: string): string[] => {
31
+ const full = join(dir, folder);
32
+ if (!existsSync(full) || !statSync(full).isDirectory()) return [];
33
+ return readdirSync(full)
34
+ .filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"))
35
+ .toSorted((left, right) => left.localeCompare(right, "en"))
36
+ .map((file) => `${folder}/${file}`);
37
+ };
38
+
39
+ const foldersIn = (dir: string, folder: string): string[] => {
40
+ const full = join(dir, folder);
41
+ if (!existsSync(full) || !statSync(full).isDirectory()) return [];
42
+ return readdirSync(full)
43
+ .filter((name) => statSync(join(full, name)).isDirectory())
44
+ .toSorted((left, right) => left.localeCompare(right, "en"));
45
+ };
46
+
47
+ const packageJsonOf = (dir: string): Record<string, unknown> | undefined => {
48
+ const read = existsSync(join(dir, "package.json")) ? readYaml(dir, "package.json") : undefined;
49
+ return read !== undefined && "value" in read && isRecord(read.value) ? read.value : undefined;
50
+ };
51
+
52
+ /** The name in the folder's package.json, if it has one. */
53
+ export const packageNameOf = (dir: string): string | undefined => {
54
+ const name = packageJsonOf(dir)?.["name"];
55
+ return typeof name === "string" ? name : undefined;
56
+ };
57
+
58
+ /**
59
+ * The manifest of a pack folder: chaff-plugin.yaml, else package.json's chaff field. undefined when the folder is not
60
+ * a pack (a code plugin, loaded by importing it). A manifest that cannot be read is reported, not taken for "not a pack".
61
+ */
62
+ export const packManifestOf = (dir: string): { readonly manifest: Record<string, unknown> } | { readonly message: string } | undefined => {
63
+ if (existsSync(join(dir, PACK_MANIFEST))) {
64
+ const read = readYaml(dir, PACK_MANIFEST);
65
+ if ("message" in read) return read;
66
+ // An empty manifest only marks the folder; anything else must be a map.
67
+ if (read.value === null || read.value === undefined) return { manifest: {} };
68
+ return isRecord(read.value) ? { manifest: read.value } : { message: `${PACK_MANIFEST}: write a map (apiVersion: 1), not ${printed(read.value)}` };
69
+ }
70
+ const chaff = packageJsonOf(dir)?.["chaff"];
71
+ if (chaff === undefined) return undefined;
72
+ return isRecord(chaff) ? { manifest: chaff } : { message: `package.json: chaff must be a map ({ "apiVersion": 1 }), not ${printed(chaff)}` };
73
+ };
74
+
75
+ type Read<T> = { readonly value: T } | { readonly message: string };
76
+
77
+ /** Every file's value in the order of the files, or the first that cannot be read. */
78
+ const readAll = (dir: string, paths: readonly string[]): Read<unknown[]> =>
79
+ paths.reduce<Read<unknown[]>>(
80
+ (done, path) => {
81
+ if ("message" in done) return done;
82
+ const read = readYaml(dir, path);
83
+ return "message" in read ? read : { value: [...done.value, read.value] };
84
+ },
85
+ { value: [] },
86
+ );
87
+
88
+ /** rules/*.yaml: each file holds one rule, or a list of them. */
89
+ const rulesOf = (dir: string): Read<unknown[]> => {
90
+ const read = readAll(dir, yamlFilesIn(dir, "rules"));
91
+ return "message" in read ? read : { value: read.value.flatMap((value) => (Array.isArray(value) ? value.map((rule: unknown) => rule) : [value])) };
92
+ };
93
+
94
+ /** lexicons/<language>/<list>.yaml, as a code plugin writes its lexicons: { list: { language: entries } }. */
95
+ const lexiconsOf = (dir: string): Read<Record<string, Record<string, unknown>>> =>
96
+ foldersIn(dir, "lexicons").reduce<Read<Record<string, Record<string, unknown>>>>(
97
+ (done, language) => {
98
+ if ("message" in done) return done;
99
+ const paths = yamlFilesIn(dir, `lexicons/${language}`);
100
+ const read = readAll(dir, paths);
101
+ if ("message" in read) return read;
102
+ const merged = paths.reduce<Record<string, Record<string, unknown>>>((acc, path, index) => {
103
+ const list = basename(path).replace(/\.ya?ml$/u, "");
104
+ return { ...acc, [list]: { ...acc[list], [language]: read.value[index] } };
105
+ }, done.value);
106
+ return { value: merged };
107
+ },
108
+ { value: {} },
109
+ );
110
+
111
+ /** The pack as a plugin's default export: its name and API version from the manifest, its rules, lexicons and styles from its folders. */
112
+ export const readPack = (dir: string, manifest: Readonly<Record<string, unknown>>, expectedName: string | undefined): Read<Record<string, unknown>> => {
113
+ const rules = rulesOf(dir);
114
+ if ("message" in rules) return rules;
115
+ const lexicons = lexiconsOf(dir);
116
+ if ("message" in lexicons) return lexicons;
117
+ const styles = readAll(dir, yamlFilesIn(dir, "styles"));
118
+ if ("message" in styles) return styles;
119
+ return {
120
+ value: {
121
+ apiVersion: manifest["apiVersion"] ?? API_VERSION,
122
+ name: manifest["name"] ?? expectedName,
123
+ rules: rules.value,
124
+ lexicons: lexicons.value,
125
+ styles: styles.value,
126
+ },
127
+ };
128
+ };
@@ -1,3 +1,4 @@
1
+ import { existsSync, statSync } from "node:fs";
1
2
  import { createRequire } from "node:module";
2
3
  import { join } from "node:path";
3
4
  import { CONFIG_FILE, type Config } from "../config/load.ts";
@@ -7,6 +8,7 @@ import { importDefault } from "./import-default.ts";
7
8
  import { staysInside } from "./real-path.ts";
8
9
  import { isPluginPath, pluginNameOfPackage } from "./plugin-name.ts";
9
10
  import { parsePlugin, type ParsedPlugin, type PluginOrigin, type PluginProblem } from "./plugin-parse.ts";
11
+ import { PACK_MANIFEST, packManifestOf, packageNameOf, readPack } from "./pack-read.ts";
10
12
 
11
13
  // The plugins chaff.yaml lists under plugins:, found and loaded. A package is found from the folder chaff.yaml is in,
12
14
  // as Node finds it (node_modules); a path is the project's own plugin and, like a module rule, may not leave that
@@ -14,7 +16,8 @@ import { parsePlugin, type ParsedPlugin, type PluginOrigin, type PluginProblem }
14
16
 
15
17
  export type LoadedPlugins = { readonly plugins: readonly ParsedPlugin[]; readonly problems: readonly PluginProblem[] };
16
18
 
17
- type Located = { readonly origin: PluginOrigin } | { readonly problem: PluginProblem };
19
+ /** pack: the folder of a rule pack (YAML only), which is read rather than imported. */
20
+ type Located = { readonly origin: PluginOrigin; readonly pack?: string } | { readonly problem: PluginProblem };
18
21
 
19
22
  const problemOf = (kind: Exclude<PluginProblem["kind"], "rule">, plugin: string, detail = ""): { problem: PluginProblem } => ({
20
23
  problem: { kind, plugin, detail },
@@ -29,24 +32,61 @@ const resolveFrom = (baseDir: string, specifier: string): string | undefined =>
29
32
  }
30
33
  };
31
34
 
35
+ const isFolder = (path: string): boolean => existsSync(path) && statSync(path).isDirectory();
36
+
37
+ /** A folder that is a rule pack: it has a manifest. A folder that is a code plugin is imported as Node imports it. */
38
+ const isPack = (dir: string): boolean => packManifestOf(dir) !== undefined;
39
+
40
+ /** The folder Node would find package name in from the folder chaff.yaml is in, whatever the package exports. */
41
+ const packageFolder = (baseDir: string, name: string): string | undefined =>
42
+ (createRequire(join(baseDir, CONFIG_FILE)).resolve.paths(name) ?? [])
43
+ .map((nodeModules) => join(nodeModules, name))
44
+ .find((dir) => existsSync(join(dir, "package.json")));
45
+
46
+ const packAt = (written: string, dir: string, expectedName: string | undefined): Located => ({
47
+ origin: { written, file: join(dir, PACK_MANIFEST), expectedName },
48
+ pack: dir,
49
+ });
50
+
51
+ /** A pack of the project's own takes its name from its package.json (chaff-plugin-foo is foo) when its manifest does not give one. */
52
+ const localPackName = (dir: string): string | undefined => {
53
+ const name = packageNameOf(dir);
54
+ return name === undefined ? undefined : pluginNameOfPackage(name);
55
+ };
56
+
57
+ /** A plugin of the project's own, by its path: a pack folder, or a file (or folder) Node imports. Neither may leave the project. */
58
+ const locatePath = (written: string, baseDir: string): Located => {
59
+ const path = modulePathOf(written, baseDir);
60
+ if ("refusal" in path) return problemOf("outside", written);
61
+ if (isFolder(path.file) && isPack(path.file))
62
+ return staysInside(written, path.file, baseDir) ? packAt(written, path.file, localPackName(path.file)) : problemOf("outside", written);
63
+ const file = resolveFrom(baseDir, path.file);
64
+ if (file === undefined) return problemOf("not-found", written);
65
+ return staysInside(written, file, baseDir) ? { origin: { written, file, expectedName: undefined } } : problemOf("outside", written);
66
+ };
67
+
32
68
  const locate = (written: string, baseDir: string): Located => {
33
- if (isPluginPath(written)) {
34
- const path = modulePathOf(written, baseDir);
35
- if ("refusal" in path) return problemOf("outside", written);
36
- const file = resolveFrom(baseDir, path.file);
37
- if (file === undefined) return problemOf("not-found", written);
38
- return staysInside(written, file, baseDir) ? { origin: { written, file, expectedName: undefined } } : problemOf("outside", written);
39
- }
69
+ if (isPluginPath(written)) return locatePath(written, baseDir);
40
70
  const expectedName = pluginNameOfPackage(written);
41
71
  if (expectedName === undefined) return problemOf("bad-specifier", written);
72
+ const folder = packageFolder(baseDir, written);
73
+ if (folder !== undefined && isPack(folder)) return packAt(written, folder, expectedName);
42
74
  const file = resolveFrom(baseDir, written);
43
75
  return file === undefined ? problemOf("not-found", written) : { origin: { written, file, expectedName } };
44
76
  };
45
77
 
78
+ /** A rule pack read into what a code plugin's default export would be. Nothing in it runs. */
79
+ const readPackAt = (dir: string, expectedName: string | undefined): { readonly exported: unknown } | { readonly message: string } => {
80
+ const manifest = packManifestOf(dir);
81
+ if (manifest !== undefined && "message" in manifest) return manifest;
82
+ const read = readPack(dir, manifest?.manifest ?? {}, expectedName);
83
+ return "message" in read ? read : { exported: read.value };
84
+ };
85
+
46
86
  const loadPlugin = async (written: string, baseDir: string, useFor: readonly string[]): Promise<LoadedPlugins> => {
47
87
  const located = locate(written, baseDir);
48
88
  if ("problem" in located) return { plugins: [], problems: [located.problem] };
49
- const imported = await importDefault(located.origin.file);
89
+ const imported = located.pack === undefined ? await importDefault(located.origin.file) : readPackAt(located.pack, located.origin.expectedName);
50
90
  if ("message" in imported) return { plugins: [], problems: [problemOf("import-failed", written, imported.message).problem] };
51
91
  const read = parsePlugin(imported.exported, located.origin, useFor);
52
92
  return { plugins: read.plugin === undefined ? [] : [read.plugin], problems: read.problems };
@@ -6,6 +6,7 @@ import { styleOf, type StyleDefinition } from "../style-parse.ts";
6
6
  import type { UntrustedDetector } from "./module-detector.ts";
7
7
  import { describeValue } from "./returned-findings.ts";
8
8
  import { isPluginName } from "./plugin-name.ts";
9
+ import { knownGenres } from "../known-genres.ts";
9
10
 
10
11
  // What a plugin package exports (api.ts PluginSpec), read into what chaff runs: its rules with ids under the plugin's
11
12
  // name, their detectors, its word lists and its house styles. A rule is read exactly like a rule in custom_rules, with
@@ -109,6 +110,7 @@ const rulesOf = (raw: unknown, context: RulesContext): Rules => {
109
110
  const parsed = parseCustomRules(Array.isArray(raw) ? raw.map((entry: unknown) => asCustomRule(entry, origin.file)) : raw, {
110
111
  builtIn: new Set(),
111
112
  useFor: context.useFor,
113
+ genres: knownGenres(),
112
114
  baseDir: dirname(origin.file),
113
115
  });
114
116
  const detectors = detectorsByWrittenId(Array.isArray(raw) ? raw : []);
@@ -134,14 +136,19 @@ const rulesOf = (raw: unknown, context: RulesContext): Rules => {
134
136
  const lexiconEntryOf = (value: unknown): LexiconEntry | undefined => {
135
137
  if (typeof value === "string") return value.trim() === "" ? undefined : { pattern: value.trim() };
136
138
  if (!isRecord(value) || typeof value["pattern"] !== "string" || value["pattern"].trim() === "") return undefined;
137
- const insteadOf = value["instead_of"];
138
- return { pattern: value["pattern"].trim(), ...(typeof insteadOf === "string" && insteadOf.trim() !== "" ? { instead_of: insteadOf.trim() } : {}) };
139
+ const [insteadOf, rewrite] = [value["instead_of"], value["rewrite"]];
140
+ return {
141
+ pattern: value["pattern"].trim(),
142
+ ...(typeof insteadOf === "string" && insteadOf.trim() !== "" ? { instead_of: insteadOf.trim() } : {}),
143
+ ...(typeof rewrite === "string" && rewrite.trim() !== "" ? { rewrite: rewrite.trim() } : {}),
144
+ };
139
145
  };
140
146
 
141
147
  const lexiconOf = (words: unknown): Lexicon | undefined => {
142
148
  const entries = Array.isArray(words) ? words.map(lexiconEntryOf) : [undefined];
143
149
  const readable = entries.flatMap((entry) => (entry === undefined ? [] : [entry]));
144
- return readable.length === entries.length ? readable : undefined;
150
+ // An empty list would leave its rules running and finding nothing, which reads as a clean document.
151
+ return readable.length === entries.length && readable.length > 0 ? readable : undefined;
145
152
  };
146
153
 
147
154
  type ListInLanguage = { readonly language: string; readonly list: string; readonly lexicon: Lexicon };
@@ -0,0 +1,74 @@
1
+ import type { Span } from "../plugin.ts";
2
+
3
+ // Two kinds of value a docs site repeats from page to page, read by scanning rather than by a backtracking pattern:
4
+ // an email address (help@example.com) and a version of three parts or more, or one written with v (2.4.1, v3).
5
+
6
+ const LOCAL_CHAR = /[\p{L}\p{N}._%+-]/u;
7
+ const DOMAIN_CHAR = /[\p{L}\p{N}.-]/u;
8
+ const DOT = ".";
9
+
10
+ /** The longest address there is (RFC 5321), so a scan from one @ never reads the whole document. */
11
+ const MAX_ADDRESS = 254;
12
+
13
+ /** Where the run of isPart characters that ends at from begins (UTF-16 units, like the offsets). */
14
+ const extendLeft = (source: string, from: number, isPart: RegExp): number => {
15
+ const windowStart = Math.max(0, from - MAX_ADDRESS);
16
+ const stop = source
17
+ .slice(windowStart, from)
18
+ .split("")
19
+ .findLastIndex((char) => !isPart.test(char));
20
+ return windowStart + stop + 1;
21
+ };
22
+
23
+ /** Where the run of isPart characters that starts at from ends. */
24
+ const extendRight = (source: string, from: number, isPart: RegExp): number => {
25
+ const stop = source
26
+ .slice(from, from + MAX_ADDRESS)
27
+ .split("")
28
+ .findIndex((char) => !isPart.test(char));
29
+ return stop === -1 ? Math.min(source.length, from + MAX_ADDRESS) : from + stop;
30
+ };
31
+
32
+ /** A sentence's full stop after an address or a version is not part of it. */
33
+ const withoutTrailingDots = (text: string): string => text.slice(0, text.split("").findLastIndex((char) => char !== DOT) + 1);
34
+
35
+ /** The address around one @: a local part, and the labels of the domain up to the first empty one, two or more. */
36
+ const addressAt = (source: string, at: number): Span | undefined => {
37
+ const start = extendLeft(source, at, LOCAL_CHAR);
38
+ const written = source.slice(at + 1, extendRight(source, at + 1, DOMAIN_CHAR)).split(DOT);
39
+ const empty = written.indexOf("");
40
+ const labels = empty === -1 ? written : written.slice(0, empty);
41
+ if (start === at || labels.length < 2) return undefined;
42
+ return { start, end: at + 1 + labels.join(DOT).length };
43
+ };
44
+
45
+ /** The addresses in the text, left to right. An @ inside the address before it starts none. */
46
+ export const emailSpans = (source: string): Span[] =>
47
+ [...source.matchAll(/@/gu)].reduce<Span[]>((found, match) => {
48
+ const span = addressAt(source, match.index);
49
+ const previous = found.at(-1);
50
+ return span === undefined || (previous !== undefined && span.start < previous.end) ? found : [...found, span];
51
+ }, []);
52
+
53
+ /** A run of digits and dots, with a v before it or not, that starts a word. */
54
+ const VERSION_RUN = /(?<![\p{L}\p{N}.])v?\d[\d.]*/gu;
55
+ const WORD_CHAR = /[\p{L}\p{N}]/u;
56
+ const MIN_PARTS = 3;
57
+ const MAX_PARTS = 4;
58
+ const DIGITS = /^\d+$/u;
59
+
60
+ /** A version: v and one part or more (v3, v2.1), or three or four parts without it (2.4.1). Not a run that a word continues. */
61
+ const versionIn = (written: string, next: string): string | undefined => {
62
+ const run = withoutTrailingDots(written);
63
+ const prefixed = run.startsWith("v");
64
+ const parts = (prefixed ? run.slice(1) : run).split(DOT);
65
+ if (WORD_CHAR.test(next) && run === written) return undefined;
66
+ if (!parts.every((part) => DIGITS.test(part))) return undefined;
67
+ return prefixed || (parts.length >= MIN_PARTS && parts.length <= MAX_PARTS) ? run : undefined;
68
+ };
69
+
70
+ export const versionSpans = (source: string): Span[] =>
71
+ [...source.matchAll(VERSION_RUN)].flatMap((match) => {
72
+ const run = versionIn(match[0], source.charAt(match.index + match[0].length));
73
+ return run === undefined ? [] : [{ start: match.index, end: match.index + run.length }];
74
+ });
@@ -0,0 +1,54 @@
1
+ import { comparable, sameValue, type FactValue } from "./fact-values.ts";
2
+ import type { ScopedFact } from "./fact-scope.ts";
3
+ import { tallyVotes, type Vote } from "../cross-votes.ts";
4
+
5
+ // The same labelled fact given different values in different files of one run ("Monthly fee: $12" in one page, "$15" in
6
+ // another). Pure: the facts come in already read from each file. Within a file, fact-conflict compares section by section.
7
+
8
+ /** A file's labelled facts, by its path. */
9
+ export type FileFacts = { readonly path: string; readonly facts: readonly ScopedFact[] };
10
+
11
+ /** A file whose value for a label differs from the value most files give it: its first fact, and one file of the usual value. */
12
+ export type FactOutOfStep = { readonly path: string; readonly fact: ScopedFact; readonly usual: Vote<ScopedFact>; readonly files: number };
13
+
14
+ /** Two values the files can be said to agree or disagree on: comparable, and the same. */
15
+ const sameFact = (left: ScopedFact, right: ScopedFact): boolean => sameValue(left.value, right.value);
16
+
17
+ /**
18
+ * A file's vote for a label: its first fact, when every fact of the label in the file gives the same value. A file giving
19
+ * the label two values is either a list (a fee per plan) or a conflict within the file, which fact-conflict reports.
20
+ */
21
+ const voteOf = (file: FileFacts, key: string): Vote<ScopedFact>[] => {
22
+ const facts = file.facts.filter((fact) => fact.key === key && !fact.record);
23
+ const [first] = facts;
24
+ if (first === undefined) return [];
25
+ return facts.every((fact) => comparable(first.value, fact.value) && sameFact(first, fact)) ? [{ path: file.path, value: first }] : [];
26
+ };
27
+
28
+ /** The votes split into groups whose values can be compared at all (one unit, overlapping date precision). */
29
+ const comparableGroups = (votes: readonly Vote<ScopedFact>[]): Vote<ScopedFact>[][] =>
30
+ votes.reduce<Vote<ScopedFact>[][]>((groups, vote) => {
31
+ const home = groups.find((group) => group[0] !== undefined && comparable(group[0].value.value, vote.value.value));
32
+ if (home === undefined) groups.push([vote]);
33
+ else home.push(vote);
34
+ return groups;
35
+ }, []);
36
+
37
+ /** Two values across the files, as with fact-conflict in one section: three or more is read as a list, and left alone. */
38
+ const MAX_VALUES = 2;
39
+
40
+ const outOfStep = (votes: readonly Vote<ScopedFact>[]): FactOutOfStep[] => {
41
+ const tally = tallyVotes(votes, sameFact);
42
+ const [usual] = tally?.usual ?? [];
43
+ if (tally === undefined || usual === undefined || tally.values !== MAX_VALUES) return [];
44
+ return tally.odd.map((vote) => ({ path: vote.path, fact: vote.value, usual, files: tally.usual.length }));
45
+ };
46
+
47
+ /** The files whose value for a label differs from the value most files of the run give it. */
48
+ export const factsOutOfStep = (files: readonly FileFacts[]): FactOutOfStep[] => {
49
+ const keys = [...new Set(files.flatMap((file) => file.facts.map((fact) => fact.key)))];
50
+ return keys.flatMap((key) => comparableGroups(files.flatMap((file) => voteOf(file, key))).flatMap(outOfStep));
51
+ };
52
+
53
+ /** The value as written, for a message. */
54
+ export const writtenValue = (source: string, value: FactValue): string => source.slice(value.start, value.end);
@@ -0,0 +1,28 @@
1
+ import { readDepth, unknownDepthSentence } from "../rewrite-depth.ts";
2
+ import type { UiLanguage } from "../ui.ts";
3
+ import type { ChosenDepth } from "./plan.ts";
4
+
5
+ // The depth a fix plan goes to: --depth, else chaff.yaml's fix_plan.depth, else none (chaff recommends one). Pure.
6
+
7
+ export type DepthChoice = { readonly chosen: ChosenDepth | undefined } | { readonly problem: string };
8
+
9
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
10
+
11
+ const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
12
+
13
+ /** fix_plan as chaff.yaml writes it: nothing, or a map whose depth is a depth. */
14
+ const configured = (fixPlan: unknown, ui: UiLanguage): DepthChoice => {
15
+ if (fixPlan === undefined || fixPlan === null) return { chosen: undefined };
16
+ if (!isRecord(fixPlan)) return { problem: unknownDepthSentence("chaff.yaml fix_plan", printed(fixPlan), ui) };
17
+ const read = readDepth(fixPlan["depth"]);
18
+ if ("unknown" in read) return { problem: unknownDepthSentence("chaff.yaml fix_plan.depth", read.unknown, ui) };
19
+ return { chosen: read.depth === undefined ? undefined : { depth: read.depth, from: "config" } };
20
+ };
21
+
22
+ /** flag: --depth's value, or undefined when it is not given. An empty value is a value, and not a depth. */
23
+ export const chosenDepthOf = (flag: string | undefined, fixPlan: unknown, ui: UiLanguage): DepthChoice => {
24
+ if (flag === undefined) return configured(fixPlan, ui);
25
+ const read = readDepth(flag);
26
+ if ("depth" in read && read.depth !== undefined) return { chosen: { depth: read.depth, from: "flag" } };
27
+ return { problem: unknownDepthSentence("--depth", flag, ui) };
28
+ };