chaffjs 0.21.0 → 0.23.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 (360) hide show
  1. package/README.md +9 -2
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-args.d.ts +2 -0
  6. package/dist/cli-args.d.ts.map +1 -1
  7. package/dist/cli-args.js +2 -0
  8. package/dist/cli-args.js.map +1 -1
  9. package/dist/cli-text.d.ts.map +1 -1
  10. package/dist/cli-text.js +6 -0
  11. package/dist/cli-text.js.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +17 -30
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/feedback.d.ts +3 -0
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +5 -0
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/init-command.d.ts +4 -0
  22. package/dist/commands/init-command.d.ts.map +1 -0
  23. package/dist/commands/init-command.js +15 -0
  24. package/dist/commands/init-command.js.map +1 -0
  25. package/dist/commands/plugin-init.d.ts +20 -0
  26. package/dist/commands/plugin-init.d.ts.map +1 -0
  27. package/dist/commands/plugin-init.js +129 -0
  28. package/dist/commands/plugin-init.js.map +1 -0
  29. package/dist/commands/plugin-test.d.ts +11 -0
  30. package/dist/commands/plugin-test.d.ts.map +1 -0
  31. package/dist/commands/plugin-test.js +81 -0
  32. package/dist/commands/plugin-test.js.map +1 -0
  33. package/dist/commands/rules.d.ts +9 -0
  34. package/dist/commands/rules.d.ts.map +1 -0
  35. package/dist/commands/rules.js +26 -0
  36. package/dist/commands/rules.js.map +1 -0
  37. package/dist/config/load.d.ts +6 -1
  38. package/dist/config/load.d.ts.map +1 -1
  39. package/dist/config/load.js +1 -0
  40. package/dist/config/load.js.map +1 -1
  41. package/dist/config/style.js +1 -1
  42. package/dist/config/style.js.map +1 -1
  43. package/dist/config/warnings.d.ts +1 -1
  44. package/dist/config/warnings.d.ts.map +1 -1
  45. package/dist/config/warnings.js +3 -1
  46. package/dist/config/warnings.js.map +1 -1
  47. package/dist/custom/problems.d.ts.map +1 -1
  48. package/dist/custom/problems.js +1 -2
  49. package/dist/custom/problems.js.map +1 -1
  50. package/dist/detectors/amount-for-count.d.ts +21 -0
  51. package/dist/detectors/amount-for-count.d.ts.map +1 -0
  52. package/dist/detectors/amount-for-count.js +55 -0
  53. package/dist/detectors/amount-for-count.js.map +1 -0
  54. package/dist/detectors/currency-notation.d.ts +29 -0
  55. package/dist/detectors/currency-notation.d.ts.map +1 -0
  56. package/dist/detectors/currency-notation.js +58 -0
  57. package/dist/detectors/currency-notation.js.map +1 -0
  58. package/dist/detectors/dangling-opener.d.ts +14 -0
  59. package/dist/detectors/dangling-opener.d.ts.map +1 -0
  60. package/dist/detectors/dangling-opener.js +55 -0
  61. package/dist/detectors/dangling-opener.js.map +1 -0
  62. package/dist/detectors/ellipsis.d.ts +10 -0
  63. package/dist/detectors/ellipsis.d.ts.map +1 -0
  64. package/dist/detectors/ellipsis.js +56 -0
  65. package/dist/detectors/ellipsis.js.map +1 -0
  66. package/dist/detectors/footnote-mismatch.d.ts +11 -0
  67. package/dist/detectors/footnote-mismatch.d.ts.map +1 -0
  68. package/dist/detectors/footnote-mismatch.js +55 -0
  69. package/dist/detectors/footnote-mismatch.js.map +1 -0
  70. package/dist/detectors/form-minority.d.ts +13 -0
  71. package/dist/detectors/form-minority.d.ts.map +1 -0
  72. package/dist/detectors/form-minority.js +19 -0
  73. package/dist/detectors/form-minority.js.map +1 -0
  74. package/dist/detectors/heading-end-mark.d.ts +10 -0
  75. package/dist/detectors/heading-end-mark.d.ts.map +1 -0
  76. package/dist/detectors/heading-end-mark.js +59 -0
  77. package/dist/detectors/heading-end-mark.js.map +1 -0
  78. package/dist/detectors/list-marker.d.ts +7 -0
  79. package/dist/detectors/list-marker.d.ts.map +1 -0
  80. package/dist/detectors/list-marker.js +50 -0
  81. package/dist/detectors/list-marker.js.map +1 -0
  82. package/dist/detectors/markdown-slip.d.ts +19 -0
  83. package/dist/detectors/markdown-slip.d.ts.map +1 -0
  84. package/dist/detectors/markdown-slip.js +122 -0
  85. package/dist/detectors/markdown-slip.js.map +1 -0
  86. package/dist/detectors/numeral-opener.d.ts +10 -0
  87. package/dist/detectors/numeral-opener.d.ts.map +1 -0
  88. package/dist/detectors/numeral-opener.js +65 -0
  89. package/dist/detectors/numeral-opener.js.map +1 -0
  90. package/dist/detectors/ordinal-suffix.d.ts +12 -0
  91. package/dist/detectors/ordinal-suffix.d.ts.map +1 -0
  92. package/dist/detectors/ordinal-suffix.js +33 -0
  93. package/dist/detectors/ordinal-suffix.js.map +1 -0
  94. package/dist/detectors/range-notation.d.ts +15 -0
  95. package/dist/detectors/range-notation.d.ts.map +1 -0
  96. package/dist/detectors/range-notation.js +58 -0
  97. package/dist/detectors/range-notation.js.map +1 -0
  98. package/dist/detectors/registry/amount-for-count.d.ts +3 -0
  99. package/dist/detectors/registry/amount-for-count.d.ts.map +1 -0
  100. package/dist/detectors/registry/amount-for-count.js +3 -0
  101. package/dist/detectors/registry/amount-for-count.js.map +1 -0
  102. package/dist/detectors/registry/currency-notation.d.ts +3 -0
  103. package/dist/detectors/registry/currency-notation.d.ts.map +1 -0
  104. package/dist/detectors/registry/currency-notation.js +3 -0
  105. package/dist/detectors/registry/currency-notation.js.map +1 -0
  106. package/dist/detectors/registry/dangling-opener.d.ts +3 -0
  107. package/dist/detectors/registry/dangling-opener.d.ts.map +1 -0
  108. package/dist/detectors/registry/dangling-opener.js +3 -0
  109. package/dist/detectors/registry/dangling-opener.js.map +1 -0
  110. package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
  111. package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
  112. package/dist/detectors/registry/ellipsis-consistency.js +3 -0
  113. package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
  114. package/dist/detectors/registry/footnote-mismatch.d.ts +3 -0
  115. package/dist/detectors/registry/footnote-mismatch.d.ts.map +1 -0
  116. package/dist/detectors/registry/footnote-mismatch.js +3 -0
  117. package/dist/detectors/registry/footnote-mismatch.js.map +1 -0
  118. package/dist/detectors/registry/heading-end-punctuation.d.ts +3 -0
  119. package/dist/detectors/registry/heading-end-punctuation.d.ts.map +1 -0
  120. package/dist/detectors/registry/heading-end-punctuation.js +3 -0
  121. package/dist/detectors/registry/heading-end-punctuation.js.map +1 -0
  122. package/dist/detectors/registry/list-marker.d.ts +3 -0
  123. package/dist/detectors/registry/list-marker.d.ts.map +1 -0
  124. package/dist/detectors/registry/list-marker.js +3 -0
  125. package/dist/detectors/registry/list-marker.js.map +1 -0
  126. package/dist/detectors/registry/numeral-opener.d.ts +3 -0
  127. package/dist/detectors/registry/numeral-opener.d.ts.map +1 -0
  128. package/dist/detectors/registry/numeral-opener.js +3 -0
  129. package/dist/detectors/registry/numeral-opener.js.map +1 -0
  130. package/dist/detectors/registry/ordinal-suffix.d.ts +3 -0
  131. package/dist/detectors/registry/ordinal-suffix.d.ts.map +1 -0
  132. package/dist/detectors/registry/ordinal-suffix.js +3 -0
  133. package/dist/detectors/registry/ordinal-suffix.js.map +1 -0
  134. package/dist/detectors/registry/range-notation.d.ts +3 -0
  135. package/dist/detectors/registry/range-notation.d.ts.map +1 -0
  136. package/dist/detectors/registry/range-notation.js +3 -0
  137. package/dist/detectors/registry/range-notation.js.map +1 -0
  138. package/dist/detectors/registry/relative-pronoun-mix.d.ts +3 -0
  139. package/dist/detectors/registry/relative-pronoun-mix.d.ts.map +1 -0
  140. package/dist/detectors/registry/relative-pronoun-mix.js +3 -0
  141. package/dist/detectors/registry/relative-pronoun-mix.js.map +1 -0
  142. package/dist/detectors/registry/share-over-hundred.d.ts +3 -0
  143. package/dist/detectors/registry/share-over-hundred.d.ts.map +1 -0
  144. package/dist/detectors/registry/share-over-hundred.js +3 -0
  145. package/dist/detectors/registry/share-over-hundred.js.map +1 -0
  146. package/dist/detectors/registry/step-reference.d.ts +3 -0
  147. package/dist/detectors/registry/step-reference.d.ts.map +1 -0
  148. package/dist/detectors/registry/step-reference.js +3 -0
  149. package/dist/detectors/registry/step-reference.js.map +1 -0
  150. package/dist/detectors/registry/table-row-overflow.d.ts +3 -0
  151. package/dist/detectors/registry/table-row-overflow.d.ts.map +1 -0
  152. package/dist/detectors/registry/table-row-overflow.js +3 -0
  153. package/dist/detectors/registry/table-row-overflow.js.map +1 -0
  154. package/dist/detectors/registry/toc-heading.d.ts +3 -0
  155. package/dist/detectors/registry/toc-heading.d.ts.map +1 -0
  156. package/dist/detectors/registry/toc-heading.js +3 -0
  157. package/dist/detectors/registry/toc-heading.js.map +1 -0
  158. package/dist/detectors/registry/unclosed-code-fence.d.ts +3 -0
  159. package/dist/detectors/registry/unclosed-code-fence.d.ts.map +1 -0
  160. package/dist/detectors/registry/unclosed-code-fence.js +3 -0
  161. package/dist/detectors/registry/unclosed-code-fence.js.map +1 -0
  162. package/dist/detectors/registry/unrendered-emphasis.d.ts +3 -0
  163. package/dist/detectors/registry/unrendered-emphasis.d.ts.map +1 -0
  164. package/dist/detectors/registry/unrendered-emphasis.js +3 -0
  165. package/dist/detectors/registry/unrendered-emphasis.js.map +1 -0
  166. package/dist/detectors/relative-pronoun-mix.d.ts +19 -0
  167. package/dist/detectors/relative-pronoun-mix.d.ts.map +1 -0
  168. package/dist/detectors/relative-pronoun-mix.js +60 -0
  169. package/dist/detectors/relative-pronoun-mix.js.map +1 -0
  170. package/dist/detectors/share-over-hundred.d.ts +20 -0
  171. package/dist/detectors/share-over-hundred.d.ts.map +1 -0
  172. package/dist/detectors/share-over-hundred.js +55 -0
  173. package/dist/detectors/share-over-hundred.js.map +1 -0
  174. package/dist/detectors/step-reference.d.ts +20 -0
  175. package/dist/detectors/step-reference.d.ts.map +1 -0
  176. package/dist/detectors/step-reference.js +57 -0
  177. package/dist/detectors/step-reference.js.map +1 -0
  178. package/dist/detectors/toc-heading.d.ts +10 -0
  179. package/dist/detectors/toc-heading.d.ts.map +1 -0
  180. package/dist/detectors/toc-heading.js +99 -0
  181. package/dist/detectors/toc-heading.js.map +1 -0
  182. package/dist/extension/load.d.ts +3 -0
  183. package/dist/extension/load.d.ts.map +1 -1
  184. package/dist/extension/load.js +1 -0
  185. package/dist/extension/load.js.map +1 -1
  186. package/dist/extension/pack-read.d.ts.map +1 -1
  187. package/dist/extension/pack-read.js +1 -0
  188. package/dist/extension/pack-read.js.map +1 -1
  189. package/dist/extension/plugin-load.js.map +1 -1
  190. package/dist/extension/plugin-parse.d.ts +7 -0
  191. package/dist/extension/plugin-parse.d.ts.map +1 -1
  192. package/dist/extension/plugin-parse.js +18 -2
  193. package/dist/extension/plugin-parse.js.map +1 -1
  194. package/dist/extension/problem-text.d.ts.map +1 -1
  195. package/dist/extension/problem-text.js +5 -0
  196. package/dist/extension/problem-text.js.map +1 -1
  197. package/dist/fix-plan/plan.d.ts +4 -0
  198. package/dist/fix-plan/plan.d.ts.map +1 -1
  199. package/dist/fix-plan/plan.js +1 -0
  200. package/dist/fix-plan/plan.js.map +1 -1
  201. package/dist/fix-plan/render.d.ts.map +1 -1
  202. package/dist/fix-plan/render.js +9 -0
  203. package/dist/fix-plan/render.js.map +1 -1
  204. package/dist/fix-plan/text.d.ts +4 -0
  205. package/dist/fix-plan/text.d.ts.map +1 -1
  206. package/dist/fix-plan/text.js +8 -2
  207. package/dist/fix-plan/text.js.map +1 -1
  208. package/dist/genre-guide/key-rules.d.ts +10 -0
  209. package/dist/genre-guide/key-rules.d.ts.map +1 -0
  210. package/dist/genre-guide/key-rules.js +23 -0
  211. package/dist/genre-guide/key-rules.js.map +1 -0
  212. package/dist/genre-guide/layer.d.ts +44 -0
  213. package/dist/genre-guide/layer.d.ts.map +1 -0
  214. package/dist/genre-guide/layer.js +43 -0
  215. package/dist/genre-guide/layer.js.map +1 -0
  216. package/dist/genre-guide/lines.d.ts +15 -0
  217. package/dist/genre-guide/lines.d.ts.map +1 -0
  218. package/dist/genre-guide/lines.js +39 -0
  219. package/dist/genre-guide/lines.js.map +1 -0
  220. package/dist/genre-guide/of-config.d.ts +13 -0
  221. package/dist/genre-guide/of-config.d.ts.map +1 -0
  222. package/dist/genre-guide/of-config.js +18 -0
  223. package/dist/genre-guide/of-config.js.map +1 -0
  224. package/dist/genre-guide/problem-text.d.ts +5 -0
  225. package/dist/genre-guide/problem-text.d.ts.map +1 -0
  226. package/dist/genre-guide/problem-text.js +29 -0
  227. package/dist/genre-guide/problem-text.js.map +1 -0
  228. package/dist/genre-guide/render.d.ts +5 -0
  229. package/dist/genre-guide/render.d.ts.map +1 -0
  230. package/dist/genre-guide/render.js +38 -0
  231. package/dist/genre-guide/render.js.map +1 -0
  232. package/dist/genre-guide/report.d.ts +32 -0
  233. package/dist/genre-guide/report.d.ts.map +1 -0
  234. package/dist/genre-guide/report.js +42 -0
  235. package/dist/genre-guide/report.js.map +1 -0
  236. package/dist/genre-guide/resolve.d.ts +16 -0
  237. package/dist/genre-guide/resolve.d.ts.map +1 -0
  238. package/dist/genre-guide/resolve.js +31 -0
  239. package/dist/genre-guide/resolve.js.map +1 -0
  240. package/dist/genre-parse.d.ts +5 -1
  241. package/dist/genre-parse.d.ts.map +1 -1
  242. package/dist/genre-parse.js +8 -1
  243. package/dist/genre-parse.js.map +1 -1
  244. package/dist/init.d.ts.map +1 -1
  245. package/dist/init.js +14 -0
  246. package/dist/init.js.map +1 -1
  247. package/dist/list-marks.d.ts +21 -0
  248. package/dist/list-marks.d.ts.map +1 -0
  249. package/dist/list-marks.js +82 -0
  250. package/dist/list-marks.js.map +1 -0
  251. package/dist/render/rules-json.d.ts +2 -1
  252. package/dist/render/rules-json.d.ts.map +1 -1
  253. package/dist/render/rules-json.js +3 -1
  254. package/dist/render/rules-json.js.map +1 -1
  255. package/dist/render/sarif.d.ts +3 -1
  256. package/dist/render/sarif.d.ts.map +1 -1
  257. package/dist/render/sarif.js +3 -1
  258. package/dist/render/sarif.js.map +1 -1
  259. package/dist/rule-fields.d.ts +5 -0
  260. package/dist/rule-fields.d.ts.map +1 -1
  261. package/dist/rule-fields.js +21 -0
  262. package/dist/rule-fields.js.map +1 -1
  263. package/dist/style-parse.d.ts +3 -0
  264. package/dist/style-parse.d.ts.map +1 -1
  265. package/dist/style-parse.js +18 -2
  266. package/dist/style-parse.js.map +1 -1
  267. package/genres.yaml +329 -0
  268. package/package.json +3 -3
  269. package/rules/cliche.yaml +67 -0
  270. package/rules/currency-notation-consistency.yaml +77 -0
  271. package/rules/dangling-participle.yaml +58 -0
  272. package/rules/ellipsis-consistency.yaml +73 -0
  273. package/rules/figurative-density.yaml +84 -0
  274. package/rules/footnote-mismatch.yaml +91 -0
  275. package/rules/heading-end-punctuation-mix.yaml +158 -0
  276. package/rules/less-for-fewer.yaml +59 -0
  277. package/rules/list-marker-mix.yaml +111 -0
  278. package/rules/nonstandard-word.yaml +80 -0
  279. package/rules/numeral-sentence-start.yaml +56 -0
  280. package/rules/ordinal-suffix-mismatch.yaml +65 -0
  281. package/rules/range-notation-consistency.yaml +73 -0
  282. package/rules/share-over-hundred.yaml +65 -0
  283. package/rules/step-reference-missing.yaml +85 -0
  284. package/rules/table-row-overflow.yaml +90 -0
  285. package/rules/toc-heading-mismatch.yaml +136 -0
  286. package/rules/unclosed-code-fence.yaml +107 -0
  287. package/rules/uncomparable-graded.yaml +65 -0
  288. package/rules/unrendered-emphasis.yaml +73 -0
  289. package/rules/which-that-consistency.yaml +64 -0
  290. package/skills/chaff/SKILL.md +13 -7
  291. package/src/api.ts +13 -0
  292. package/src/cli-args.ts +3 -0
  293. package/src/cli-text.ts +6 -0
  294. package/src/cli.ts +21 -29
  295. package/src/commands/feedback.ts +3 -0
  296. package/src/commands/fix-plan.ts +8 -0
  297. package/src/commands/init-command.ts +13 -0
  298. package/src/commands/plugin-init.ts +150 -0
  299. package/src/commands/plugin-test.ts +111 -0
  300. package/src/commands/rules.ts +34 -0
  301. package/src/config/load.ts +7 -1
  302. package/src/config/style.ts +1 -1
  303. package/src/config/warnings.ts +3 -1
  304. package/src/custom/problems.ts +1 -3
  305. package/src/detectors/amount-for-count.ts +90 -0
  306. package/src/detectors/currency-notation.ts +90 -0
  307. package/src/detectors/dangling-opener.ts +74 -0
  308. package/src/detectors/ellipsis.ts +74 -0
  309. package/src/detectors/footnote-mismatch.ts +72 -0
  310. package/src/detectors/form-minority.ts +21 -0
  311. package/src/detectors/heading-end-mark.ts +82 -0
  312. package/src/detectors/list-marker.ts +60 -0
  313. package/src/detectors/markdown-slip.ts +140 -0
  314. package/src/detectors/numeral-opener.ts +72 -0
  315. package/src/detectors/ordinal-suffix.ts +40 -0
  316. package/src/detectors/range-notation.ts +76 -0
  317. package/src/detectors/registry/amount-for-count.ts +4 -0
  318. package/src/detectors/registry/currency-notation.ts +4 -0
  319. package/src/detectors/registry/dangling-opener.ts +4 -0
  320. package/src/detectors/registry/ellipsis-consistency.ts +4 -0
  321. package/src/detectors/registry/footnote-mismatch.ts +4 -0
  322. package/src/detectors/registry/heading-end-punctuation.ts +4 -0
  323. package/src/detectors/registry/list-marker.ts +4 -0
  324. package/src/detectors/registry/numeral-opener.ts +4 -0
  325. package/src/detectors/registry/ordinal-suffix.ts +4 -0
  326. package/src/detectors/registry/range-notation.ts +4 -0
  327. package/src/detectors/registry/relative-pronoun-mix.ts +4 -0
  328. package/src/detectors/registry/share-over-hundred.ts +4 -0
  329. package/src/detectors/registry/step-reference.ts +4 -0
  330. package/src/detectors/registry/table-row-overflow.ts +4 -0
  331. package/src/detectors/registry/toc-heading.ts +4 -0
  332. package/src/detectors/registry/unclosed-code-fence.ts +4 -0
  333. package/src/detectors/registry/unrendered-emphasis.ts +4 -0
  334. package/src/detectors/relative-pronoun-mix.ts +79 -0
  335. package/src/detectors/share-over-hundred.ts +75 -0
  336. package/src/detectors/step-reference.ts +83 -0
  337. package/src/detectors/toc-heading.ts +125 -0
  338. package/src/extension/load.ts +4 -0
  339. package/src/extension/pack-read.ts +1 -0
  340. package/src/extension/plugin-load.ts +1 -1
  341. package/src/extension/plugin-parse.ts +27 -4
  342. package/src/extension/problem-text.ts +7 -1
  343. package/src/fix-plan/plan.ts +5 -0
  344. package/src/fix-plan/render.ts +9 -0
  345. package/src/fix-plan/text.ts +12 -2
  346. package/src/genre-guide/key-rules.ts +37 -0
  347. package/src/genre-guide/layer.ts +77 -0
  348. package/src/genre-guide/lines.ts +41 -0
  349. package/src/genre-guide/of-config.ts +26 -0
  350. package/src/genre-guide/problem-text.ts +34 -0
  351. package/src/genre-guide/render.ts +50 -0
  352. package/src/genre-guide/report.ts +63 -0
  353. package/src/genre-guide/resolve.ts +36 -0
  354. package/src/genre-parse.ts +13 -3
  355. package/src/init.ts +14 -0
  356. package/src/list-marks.ts +106 -0
  357. package/src/render/rules-json.ts +4 -0
  358. package/src/render/sarif.ts +4 -1
  359. package/src/rule-fields.ts +28 -0
  360. package/src/style-parse.ts +19 -1
package/src/cli.ts CHANGED
@@ -13,9 +13,12 @@ import { runEval } from "./commands/eval.ts";
13
13
  import { runTest } from "./commands/test.ts";
14
14
  import { GENRES } from "./genre.ts";
15
15
  import { resolveGenre } from "./resolve-genre.ts";
16
- import { runInit } from "./init.ts";
17
- import { initGenre } from "./commands/init-ask.ts";
18
- import { targetsOf, withExperimental, withIncludes } from "./cli-args.ts";
16
+ import { runInitCommand } from "./commands/init-command.ts";
17
+ import { runPluginTest } from "./commands/plugin-test.ts";
18
+ import { targetsOf, withIncludes } from "./cli-args.ts";
19
+ import { guidesOfRun, type RunGuide } from "./genre-guide/report.ts";
20
+ import { showRules } from "./commands/rules.ts";
21
+ import { renderGuideBlock } from "./genre-guide/render.ts";
19
22
  import { rulesOf } from "./custom/load.ts";
20
23
  import { renderCompact } from "./render/compact.ts";
21
24
  import { renderExplain } from "./render/explain.ts";
@@ -23,8 +26,6 @@ import { renderFriendly } from "./render/friendly.ts";
23
26
  import { renderGenres } from "./render/genres.ts";
24
27
  import { loadGenres, presetLevels } from "./genre-load.ts";
25
28
  import { fileHeader } from "./file-header.ts";
26
- import { rulesJson } from "./render/rules-json.ts";
27
- import { rulesTable } from "./render/rules-table.ts";
28
29
  import { renderSarif } from "./render/sarif.ts";
29
30
  import { VERSION, VERSION_LINES } from "./version.ts";
30
31
  import type { TreeContext } from "./commands/tree.ts";
@@ -38,7 +39,7 @@ import { runConditions, runFeedback, settingsOf, type Checked } from "./commands
38
39
  import { homedir } from "node:os";
39
40
  import { settingWarnings } from "./config/warnings.ts";
40
41
  import { readConfigIn } from "./config/read.ts";
41
- import { optionLayersOf, settingSourcesOf } from "./config/option-problems.ts";
42
+ import { settingSourcesOf } from "./config/option-problems.ts";
42
43
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
43
44
 
44
45
  import type { Finding, Level, RuleDefinition } from "./plugin.ts";
@@ -117,7 +118,7 @@ const present = (check: SourceCheck, argv: readonly string[]): Inspected => {
117
118
  genre,
118
119
  outcome: { path, findings: split.fresh, notRun: raw.skipped.length },
119
120
  perFile: { path, suppressed: applied.suppressed, reasonless: applied.unusedReasonless, notRun },
120
- checked: { findings: split.fresh, rules, language, genre, skipped: raw.skipped },
121
+ checked: { findings: split.fresh, rules, language, genre, genreFrom: from, skipped: raw.skipped },
121
122
  kept: applied.kept,
122
123
  };
123
124
  };
@@ -132,7 +133,7 @@ const inspectRun = async (paths: readonly string[], config: Config, argv: readon
132
133
  * 指摘を PR の変更行に出すための出口。--sarif <path> を書いたときだけ作る。
133
134
  * 端末の出力は変えない。CI で上げるためのファイルが増えるだけ。
134
135
  */
135
- const writeSarif = (results: readonly Inspected[], argv: readonly string[], config: Config): void => {
136
+ const writeSarif = (results: readonly Inspected[], argv: readonly string[], config: Config, guides: readonly RunGuide[]): void => {
136
137
  const path = flag(argv, "--sarif");
137
138
  if (path === undefined) return;
138
139
  // 文言はファイルの言語で描く。by_path で 1 つの repo に 2 言語が混ざるため。
@@ -141,7 +142,7 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[], conf
141
142
  );
142
143
  const notRun = results.flatMap((result) => result.checked.skipped.map((entry) => ({ path: result.outcome.path, rule: entry.rule, why: entry.why })));
143
144
  mkdirSync(dirname(path), { recursive: true });
144
- writeFileSync(path, renderSarif(located, VERSION, notRun), "utf8");
145
+ writeFileSync(path, renderSarif(located, VERSION, notRun, guides), "utf8");
145
146
  console.log(hostText(config).sarifWritten(path, located.length));
146
147
  };
147
148
 
@@ -169,7 +170,13 @@ const lint = async (targets: readonly string[], argv: readonly string[], config:
169
170
  }
170
171
  warnRuleProblems(config, language);
171
172
  const results = await inspectRun(paths, config, argv);
172
- writeSarif(results, argv, config);
173
+ const guides = guidesOfRun(
174
+ results.map((result) => result.checked),
175
+ config,
176
+ argv,
177
+ );
178
+ writeSarif(results, argv, config, guides);
179
+ if (!argv.includes("--compact")) guides.forEach((entry) => console.log(renderGuideBlock(entry.guide, entry.language).join("\n")));
173
180
  results.filter((result) => result.outcome.findings.length > 0 || paths.length === 1).forEach((result) => console.log(result.text));
174
181
  renderSummary(
175
182
  results.map((result) => result.outcome),
@@ -255,17 +262,6 @@ type Handler = (argv: readonly string[], config: Config) => number | Promise<num
255
262
  /** `--` で始まらない引数。対象のパス。 */
256
263
  const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
257
264
 
258
- /** `rules --json` for an AI to read; `rules` alone, a table for a person. */
259
- const showRules = (argv: readonly string[], written: Config): number => {
260
- const config = withExperimental(written, argv);
261
- const language = config.language ?? hostLanguage(undefined, process.env);
262
- warnRuleProblems(config, language);
263
- const genre = flag(argv, "--genre") ?? config.genre ?? "blog/tech";
264
- const rules = rulesOf(language, config);
265
- console.log(argv.includes("--json") ? rulesJson(rules, config, language, genre, optionLayersOf(config)) : rulesTable(rules, config, language, genre));
266
- return 0;
267
- };
268
-
269
265
  const showGenres = (config: Config): number => {
270
266
  console.log(renderGenres(loadGenres(), hostLanguage(config.language, process.env)));
271
267
  return 0;
@@ -282,19 +278,15 @@ const measureContext = (argv: readonly string[], config: Config): { config: Conf
282
278
 
283
279
  /** 分岐を数珠つなぎにせず表にする。足すときに main を太らせない。 */
284
280
  const HANDLERS: Readonly<Record<string, Handler>> = {
285
- init: async (argv, config) => {
286
- const ui = hostLanguage(config.language, process.env);
287
- const chosen = await initGenre(flag(argv, "--genre"), ui, process.cwd());
288
- if ("error" in chosen) console.error(chosen.error);
289
- else runInit(process.cwd(), chosen.genre, ui).forEach((line) => console.log(line));
290
- return "error" in chosen ? 1 : 0;
291
- },
281
+ init: (argv, config) => runInitCommand((name) => flag(argv, name), argv.includes("--plugin"), hostLanguage(config.language, process.env), process.cwd()),
292
282
  genres: (_argv, config) => showGenres(config),
293
- rules: showRules,
283
+ rules: (argv, config) => showRules(argv, config, { flag, warn: warnRuleProblems }),
294
284
  explain: (argv, config) => explain(config, argv[1], flag(argv, "--genre")),
295
285
  eval: (argv, config) => runEval(positional(argv), argv, { ...measureContext(argv, config), flag }),
296
286
  ...Object.fromEntries(Object.entries(DOCUMENT_COMMANDS).map(([name, run]): [string, Handler] => [name, (argv, config) => run(argv, treeContext(config))])),
297
287
  test: (argv, config) => runTest(positional(argv), argv, { ...measureContext(argv, config), inspect }),
288
+ "plugin-test": (argv, config) =>
289
+ runPluginTest(positional(argv)[0], { ui: hostLanguage(config.language, process.env), check: async (...args) => (await inspect(...args)).checked }),
298
290
  "fix-plan": (argv, config) =>
299
291
  runFixPlan(fixPlanTargets(argv), argv, { ...treeContext(config), check: async (path) => (await inspectAll(config, argv)(path)).checked }),
300
292
  baseline: (argv, config) => runBaseline(positional(argv), argv, config),
@@ -9,6 +9,7 @@ import { excerptsAround } from "../feedback/excerpt.ts";
9
9
  import { feedbackDraft, type FeedbackDraft, type FeedbackKind, type ReportedFinding } from "../feedback/draft.ts";
10
10
  import { notRunAmong } from "../not-run.ts";
11
11
  import type { Skipped } from "../run.ts";
12
+ import type { GenreSource } from "../cli-text.ts";
12
13
 
13
14
  export const FEEDBACK_FILE = ".chaff-feedback.md";
14
15
  const NEW_ISSUE = "https://github.com/isamu/lab/issues/new";
@@ -20,6 +21,8 @@ export type Checked = {
20
21
  readonly rules: readonly RuleDefinition[];
21
22
  readonly language: string;
22
23
  readonly genre: string;
24
+ /** Where the genre came from: set (--genre, chaff.yaml, by_path, front matter), guessed or defaulted. */
25
+ readonly genreFrom: GenreSource;
23
26
  /** The rules this check did not run, and why. */
24
27
  readonly skipped: readonly Skipped[];
25
28
  /** How the check was run, as the reader of a report would repeat it (runConditions). */
@@ -8,6 +8,9 @@ import { chosenDepthOf } from "../fix-plan/chosen-depth.ts";
8
8
  import type { Checked } from "./feedback.ts";
9
9
  import { readDocument } from "./read-document.ts";
10
10
  import type { TreeContext } from "./tree.ts";
11
+ import { genreWasSet, reportedGuide, type ReportedGuide } from "../genre-guide/report.ts";
12
+ import { showsGuide } from "../cli-args.ts";
13
+ import type { Config } from "../config/load.ts";
11
14
 
12
15
  /** Options whose value is the next argument. That value is not a file. */
13
16
  const VALUED: ReadonlySet<string> = new Set(["--language", "--genre", "--depth"]);
@@ -24,6 +27,10 @@ export type FixPlanContext = TreeContext & {
24
27
  readonly check: (path: string) => Promise<Omit<Checked, "conditions">>;
25
28
  };
26
29
 
30
+ /** The genre's guide, the plan's first section; none when the genre was not set or the guide is off. */
31
+ const guideOf = (checked: Omit<Checked, "conditions">, config: Config, argv: readonly string[]): ReportedGuide | undefined =>
32
+ genreWasSet(checked.genreFrom) && showsGuide(argv) ? reportedGuide(config, checked.genre, checked.language, checked.rules) : undefined;
33
+
27
34
  /**
28
35
  * An instruction document for whoever rewrites the file: the findings grouped by rule, each rule's rewrite direction,
29
36
  * the document-level signals, the structure targets from human articles, a recommended mode and the checks to run after. Deterministic; sends nothing anywhere.
@@ -61,6 +68,7 @@ export const runFixPlan = async (targets: readonly string[], argv: readonly stri
61
68
  structure: structureOf(prose.doc),
62
69
  phrases: prose.doc.lexicons["ai-tell"] ?? [],
63
70
  chosenDepth: depth.chosen,
71
+ guide: guideOf(checked, context.config, argv),
64
72
  });
65
73
  console.log(argv.includes("--json") ? renderFixPlanJson(plan) : renderFixPlanMarkdown(plan));
66
74
  return 0;
@@ -0,0 +1,13 @@
1
+ import { runInit } from "../init.ts";
2
+ import type { UiLanguage } from "../ui.ts";
3
+ import { initGenre } from "./init-ask.ts";
4
+ import { runInitPlugin } from "./plugin-init.ts";
5
+
6
+ /** chaff init: chaff.yaml for a project, or with --plugin, a rule pack to start from. flag reads an option's value. */
7
+ export const runInitCommand = async (flag: (name: string) => string | undefined, plugin: boolean, ui: UiLanguage, dir: string): Promise<number> => {
8
+ if (plugin) return runInitPlugin(dir, flag("--plugin"), ui);
9
+ const chosen = await initGenre(flag("--genre"), ui, dir);
10
+ if ("error" in chosen) console.error(chosen.error);
11
+ else runInit(dir, chosen.genre, ui).forEach((line) => console.log(line));
12
+ return "error" in chosen ? 1 : 0;
13
+ };
@@ -0,0 +1,150 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { API_VERSION } from "../api.ts";
4
+ import { VERSION } from "../version.ts";
5
+ import type { Texts, UiLanguage } from "../ui.ts";
6
+
7
+ // chaff init --plugin <name>: a rule pack to start from, written in YAML alone. One rule, its word list in Japanese and
8
+ // English, and a test command (chaff plugin-test) that checks each rule's example against the rule.
9
+
10
+ /** A pack's own name: lowercase letters, digits and hyphens, optionally under a scope (@team/house). */
11
+ const PACK_NAME = /^(?:(@[a-z0-9][a-z0-9._-]*)\/)?([a-z][a-z0-9-]*)$/u;
12
+
13
+ export type PackName = { readonly scope: string | undefined; readonly name: string };
14
+
15
+ /** The name as written after --plugin, or undefined when it cannot be a plugin's name. "chaff-plugin-" in front is allowed and dropped. */
16
+ export const packNameOf = (written: string | undefined): PackName | undefined => {
17
+ const match = PACK_NAME.exec((written ?? "").replace(/(^|\/)chaff-plugin-/u, "$1"));
18
+ if (match === null) return undefined;
19
+ const [, scope, name] = match;
20
+ return name === undefined ? undefined : { scope, name };
21
+ };
22
+
23
+ /** The package's name and the folder it is made in: chaff-plugin-house, or @team/chaff-plugin-house in chaff-plugin-house. */
24
+ export const packageOf = (pack: PackName): { readonly packageName: string; readonly folder: string } => ({
25
+ packageName: pack.scope === undefined ? `chaff-plugin-${pack.name}` : `${pack.scope}/chaff-plugin-${pack.name}`,
26
+ folder: `chaff-plugin-${pack.name}`,
27
+ });
28
+
29
+ const RULE = `# One rule of the pack. It is written as a rule under custom_rules in chaff.yaml is; see
30
+ # https://isamu.github.io/lab/en/guide/adding-rules for every field.
31
+ id: avoid-words
32
+ type: words
33
+ # The words come from lexicons/<language>/avoid-words.yaml, in the document's language.
34
+ word_list: avoid-words
35
+ name: { ja: チームで使わない言葉, en: A word the team does not use }
36
+ why:
37
+ ja: このチームでは、同じものを決まった言葉で呼びます。別の言葉が混ざると、読む人は別のものかと迷います。
38
+ en: The team calls each thing by one word. Another word makes a reader wonder whether it is another thing.
39
+ how_to_fix: { ja: チームの言葉に置き換えます。, en: Use the team's word. }
40
+ message:
41
+ ja: 「{matched}」ではなく「{preferred}」と書きます
42
+ en: 'Write "{preferred}", not "{matched}"'
43
+ level: warning
44
+ example:
45
+ ja: { before: 弊社の新しい窓口をご案内します。, after: 当社の新しい窓口をご案内します。 }
46
+ en: { before: We utilize the new form for every request., after: We use the new form for every request. }
47
+ rewrite:
48
+ depth: light
49
+ ja:
50
+ direction: 指された言葉だけを、チームの言葉に置き換えます。
51
+ pairs:
52
+ - { before: 弊社の新しい窓口をご案内します。, after: 当社の新しい窓口をご案内します。 }
53
+ - { before: 弊社では毎月点検しています。, after: 当社では毎月点検しています。 }
54
+ keep: [文のほかの部分]
55
+ avoid: [言葉と一緒に文の意味を変える]
56
+ en:
57
+ direction: Replace only the flagged word with the team's word.
58
+ pairs:
59
+ - { before: We utilize the new form for every request., after: We use the new form for every request. }
60
+ - { before: Utilize the search box first., after: Use the search box first. }
61
+ keep: [the rest of the sentence]
62
+ avoid: [changing what the sentence says along with the word]
63
+ `;
64
+
65
+ const LEXICON_JA = `# 使わない言葉(pattern)と、使う言葉(rewrite)。
66
+ - { pattern: 弊社, rewrite: 当社 }
67
+ `;
68
+
69
+ const LEXICON_EN = `# The word not to use (pattern) and the word to use (rewrite).
70
+ - { pattern: utilize, rewrite: use }
71
+ `;
72
+
73
+ const packageJson = (packageName: string): string =>
74
+ `${JSON.stringify(
75
+ {
76
+ name: packageName,
77
+ version: "0.1.0",
78
+ description: "Rules for chaff, written in YAML.",
79
+ files: ["rules", "lexicons", "styles"],
80
+ chaff: { apiVersion: API_VERSION },
81
+ scripts: { test: "chaffjs plugin-test ." },
82
+ peerDependencies: { chaffjs: `>=${VERSION}` },
83
+ },
84
+ null,
85
+ 2,
86
+ )}\n`;
87
+
88
+ const FILES = (packageName: string): Readonly<Record<string, string>> => ({
89
+ "package.json": packageJson(packageName),
90
+ "rules/avoid-words.yaml": RULE,
91
+ "lexicons/ja/avoid-words.yaml": LEXICON_JA,
92
+ "lexicons/en/avoid-words.yaml": LEXICON_EN,
93
+ });
94
+
95
+ const TEXT: Texts<{
96
+ readonly badName: (written: string) => string;
97
+ readonly exists: (folder: string) => string;
98
+ readonly made: (folder: string, packageName: string) => string;
99
+ readonly next: (folder: string, name: string) => readonly string[];
100
+ }> = {
101
+ ja: {
102
+ badName: (written) => `--plugin ${written}: プラグインの名前は英小文字・数字・ハイフンで書きます(house、@team/house)`,
103
+ exists: (folder) => `${folder} はもうあります。上書きしないので、別の名前にするか消してから実行してください`,
104
+ made: (folder, packageName) => `${folder}/ に YAML だけのルールの束 ${packageName} を作りました。`,
105
+ next: (folder, name) => [
106
+ "",
107
+ "次に:",
108
+ ` cd ${folder} && npx chaffjs plugin-test . ルールの例をルールにかけて確かめる`,
109
+ ` chaff.yaml に plugins: [./${folder}] と書くと、${name}/avoid-words として動きます`,
110
+ "",
111
+ ],
112
+ },
113
+ en: {
114
+ badName: (written) => `--plugin ${written}: a plugin's name is lowercase letters, digits and hyphens (house, @team/house)`,
115
+ exists: (folder) => `${folder} already exists. Nothing is overwritten; choose another name or remove it first`,
116
+ made: (folder, packageName) => `Made the YAML rule pack ${packageName} in ${folder}/.`,
117
+ next: (folder, name) => [
118
+ "",
119
+ "Next:",
120
+ ` cd ${folder} && npx chaffjs plugin-test . check each rule against its own example`,
121
+ ` add plugins: [./${folder}] to chaff.yaml and it runs as ${name}/avoid-words`,
122
+ "",
123
+ ],
124
+ },
125
+ };
126
+
127
+ /** Makes the pack in dir. Lines to print, and whether it was made; nothing is written when the folder is already there. */
128
+ export const initPlugin = (dir: string, written: string | undefined, ui: UiLanguage): { readonly ok: boolean; readonly lines: readonly string[] } => {
129
+ const text = TEXT[ui];
130
+ const pack = packNameOf(written);
131
+ if (pack === undefined) return { ok: false, lines: [text.badName(written ?? "")] };
132
+ const { packageName, folder } = packageOf(pack);
133
+ if (existsSync(join(dir, folder))) return { ok: false, lines: [text.exists(folder)] };
134
+ Object.entries(FILES(packageName)).forEach(([path, body]) => {
135
+ mkdirSync(dirname(join(dir, folder, path)), { recursive: true });
136
+ writeFileSync(join(dir, folder, path), body, "utf8");
137
+ });
138
+ const name = pack.scope === undefined ? pack.name : `${pack.scope}/${pack.name}`;
139
+ return {
140
+ ok: true,
141
+ lines: [text.made(folder, packageName), ...Object.keys(FILES(packageName)).map((path) => ` ${folder}/${path}`), ...text.next(folder, name)],
142
+ };
143
+ };
144
+
145
+ /** chaff init --plugin <name>: makes the pack in dir and says what it made, or why it did not. */
146
+ export const runInitPlugin = (dir: string, written: string | undefined, ui: UiLanguage): number => {
147
+ const made = initPlugin(dir, written, ui);
148
+ made.lines.forEach((line) => (made.ok ? console.log(line) : console.error(line)));
149
+ return made.ok ? 0 : 1;
150
+ };
@@ -0,0 +1,111 @@
1
+ import { mkdtempSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join, resolve } from "node:path";
4
+ import { EMPTY, type Config } from "../config/load.ts";
5
+ import { withExtensions } from "../extension/load.ts";
6
+ import { extensionProblems } from "../extension/problem-text.ts";
7
+ import { loadGenres } from "../genre-load.ts";
8
+ import type { RuleDefinition } from "../plugin.ts";
9
+ import type { Skipped } from "../run.ts";
10
+ import type { Texts, UiLanguage } from "../ui.ts";
11
+ import type { Checked } from "./feedback.ts";
12
+
13
+ // chaff plugin-test [folder]: loads a plugin as chaff.yaml would, then runs each of its rules on the rule's own example,
14
+ // in every language the example is written in. The before must give the rule a finding and the after must give it
15
+ // none. A rule that does not run there (no word list for the language) fails, with the reason.
16
+
17
+ export type PluginTestContext = {
18
+ readonly ui: UiLanguage;
19
+ /** Lints one file with these settings, as chaff on the command line does. */
20
+ readonly check: (path: string, config: Config, argv: readonly string[]) => Promise<Pick<Checked, "findings" | "skipped">>;
21
+ };
22
+
23
+ type Outcome = { readonly rule: string; readonly language: string; readonly failure: string | undefined };
24
+
25
+ const TEXT: Texts<{
26
+ readonly noRules: (dir: string) => string;
27
+ readonly beforeQuiet: string;
28
+ readonly afterFlagged: (count: number) => string;
29
+ readonly notRun: (why: string) => string;
30
+ readonly noExample: string;
31
+ readonly summary: (passed: number, total: number) => string;
32
+ }> = {
33
+ ja: {
34
+ noRules: (dir) => `${dir} にルールがありません。`,
35
+ beforeQuiet: "例の before で指摘が出ません",
36
+ afterFlagged: (count) => `例の after で指摘が ${String(count)} 件出ます`,
37
+ notRun: (why) => `動きません(${why})`,
38
+ noExample: "例がありません",
39
+ summary: (passed, total) => `${String(total)} 件のうち ${String(passed)} 件が通りました。`,
40
+ },
41
+ en: {
42
+ noRules: (dir) => `${dir} has no rules.`,
43
+ beforeQuiet: "the example's before gets no finding",
44
+ afterFlagged: (count) => `the example's after gets ${String(count)} ${count === 1 ? "finding" : "findings"}`,
45
+ notRun: (why) => `it does not run (${why})`,
46
+ noExample: "it has no example",
47
+ summary: (passed, total) => `${String(passed)} of ${String(total)} passed.`,
48
+ },
49
+ };
50
+
51
+ /** The first genre the rule is for: the example runs where the rule does. */
52
+ const genreFor = (rule: RuleDefinition): string => {
53
+ const genres = loadGenres().genres.map((genre) => genre.id);
54
+ return genres.find((genre) => rule.use_for.some((target) => genre === target || genre.startsWith(`${target}/`))) ?? genres[0] ?? "business/report";
55
+ };
56
+
57
+ const skippedWhy = (skipped: readonly Skipped[], rule: string): string | undefined => skipped.find((entry) => entry.rule === rule)?.why;
58
+
59
+ /** One rule in one language: before flagged, after clean. */
60
+ const testExample = async (rule: RuleDefinition, language: string, config: Config, context: PluginTestContext): Promise<Outcome> => {
61
+ const text = TEXT[context.ui];
62
+ const example = rule.guide?.examples[language];
63
+ if (example === undefined) return { rule: rule.id, language, failure: text.noExample };
64
+ const dir = mkdtempSync(join(tmpdir(), "chaff-plugin-test-"));
65
+ const argv = ["--language", language, "--genre", genreFor(rule)];
66
+ const run = async (name: string, body: string): Promise<Pick<Checked, "findings" | "skipped">> => {
67
+ writeFileSync(join(dir, name), `${body.trimEnd()}\n`, "utf8");
68
+ return context.check(join(dir, name), config, argv);
69
+ };
70
+ const before = await run("before.md", example.before);
71
+ const why = skippedWhy(before.skipped, rule.id);
72
+ if (why !== undefined) return { rule: rule.id, language, failure: text.notRun(why) };
73
+ if (!before.findings.some((finding) => finding.rule === rule.id)) return { rule: rule.id, language, failure: text.beforeQuiet };
74
+ const after = (await run("after.md", example.after)).findings.filter((finding) => finding.rule === rule.id).length;
75
+ return { rule: rule.id, language, failure: after === 0 ? undefined : text.afterFlagged(after) };
76
+ };
77
+
78
+ const lineOf = (outcome: Outcome): string =>
79
+ outcome.failure === undefined ? ` ✓ ${outcome.rule} (${outcome.language})` : ` ✗ ${outcome.rule} (${outcome.language}): ${outcome.failure}`;
80
+
81
+ /** The languages a rule's example is written in, or the rule's languages when it has none, so a missing example is said. */
82
+ const languagesOf = (rule: RuleDefinition): readonly string[] => {
83
+ const written = Object.keys(rule.guide?.examples ?? {});
84
+ return written.length > 0 ? written : (rule.languages ?? ["ja", "en"]);
85
+ };
86
+
87
+ /** Exit 0 when every rule passes on every example; 1 when the plugin cannot be loaded or a rule fails. */
88
+ export const runPluginTest = async (folder: string | undefined, context: PluginTestContext): Promise<number> => {
89
+ const text = TEXT[context.ui];
90
+ const dir = resolve(folder ?? ".");
91
+ const config = await withExtensions({ ...EMPTY, plugins: [dir], baseDir: dir, path: dir });
92
+ const problems = extensionProblems(config, context.ui);
93
+ if (problems.length > 0) {
94
+ problems.forEach((problem) => console.error(problem));
95
+ return 1;
96
+ }
97
+ const rules = config.extensions?.rules ?? [];
98
+ if (rules.length === 0) {
99
+ console.error(text.noRules(dir));
100
+ return 1;
101
+ }
102
+ const cases = rules.flatMap((rule) => languagesOf(rule).map((language) => ({ rule, language })));
103
+ const outcomes = await cases.reduce<Promise<Outcome[]>>(
104
+ async (done, entry) => [...(await done), await testExample(entry.rule, entry.language, config, context)],
105
+ Promise.resolve([]),
106
+ );
107
+ outcomes.map(lineOf).forEach((line) => console.log(line));
108
+ const passed = outcomes.filter((outcome) => outcome.failure === undefined).length;
109
+ console.log(`\n${text.summary(passed, outcomes.length)}`);
110
+ return passed === outcomes.length ? 0 : 1;
111
+ };
@@ -0,0 +1,34 @@
1
+ import { showsGuide, withExperimental } from "../cli-args.ts";
2
+ import type { Config } from "../config/load.ts";
3
+ import { optionLayersOf } from "../config/option-problems.ts";
4
+ import { rulesOf } from "../custom/load.ts";
5
+ import { reportedGuide } from "../genre-guide/report.ts";
6
+ import { rulesJson } from "../render/rules-json.ts";
7
+ import { rulesTable } from "../render/rules-table.ts";
8
+ import { hostLanguage } from "../ui.ts";
9
+
10
+ /** The genre the rules are shown for when none is set. */
11
+ const SHOWN_GENRE = "blog/tech";
12
+
13
+ export type RulesContext = {
14
+ readonly flag: (argv: readonly string[], name: string) => string | undefined;
15
+ /** Says once, on stderr, what in the settings has no effect. */
16
+ readonly warn: (config: Config, language: string) => void;
17
+ };
18
+
19
+ /** `rules --json` for an AI to read, with the guide of a genre that was set; `rules` alone, a table for a person. */
20
+ export const showRules = (argv: readonly string[], written: Config, context: RulesContext): number => {
21
+ const config = withExperimental(written, argv);
22
+ const language = config.language ?? hostLanguage(undefined, process.env);
23
+ context.warn(config, language);
24
+ const setGenre = context.flag(argv, "--genre") ?? config.genre;
25
+ const genre = setGenre ?? SHOWN_GENRE;
26
+ const rules = rulesOf(language, config);
27
+ if (!argv.includes("--json")) {
28
+ console.log(rulesTable(rules, config, language, genre));
29
+ return 0;
30
+ }
31
+ const guide = setGenre !== undefined && showsGuide(argv) ? reportedGuide(config, genre, language, rules) : undefined;
32
+ console.log(rulesJson(rules, config, language, genre, optionLayersOf(config), guide));
33
+ return 0;
34
+ };
@@ -7,6 +7,7 @@ import type { Level } from "../plugin.ts";
7
7
  import type { PathRule } from "./by-path.ts";
8
8
  import type { Extensions } from "../extension/load.ts";
9
9
  import type { StyleLimits } from "../style-parse.ts";
10
+ import type { GuideLayer } from "../genre-guide/layer.ts";
10
11
 
11
12
  export const CONFIG_FILE = "chaff.yaml";
12
13
 
@@ -46,7 +47,7 @@ export type Config = {
46
47
  readonly unreadableOptions?: string | undefined;
47
48
  /** The house style chaff.yaml names (styles/*.yaml), as written. A value that is not a name is kept printed, to be reported. */
48
49
  readonly style?: string | undefined;
49
- /** The style once applied (config/style.ts): its id, the rules whose level it decided, and its options. */
50
+ /** The style once applied (config/style.ts): its id, the rules whose level it decided, its options and its guide. */
50
51
  readonly applied?: AppliedStyle | undefined;
51
52
  /** custom_rules as written: the team's own rules. custom/parse.ts reads and checks them. */
52
53
  readonly customRules?: unknown;
@@ -54,6 +55,8 @@ export type Config = {
54
55
  readonly grade?: unknown;
55
56
  /** fix_plan as written: the depth chaff fix-plan rewrites to (fix-plan/chosen-depth.ts reads and checks it). */
56
57
  readonly fixPlan?: unknown;
58
+ /** guide as written: the team's changes to the genre guides, or off (genre-guide/of-config.ts reads it). */
59
+ readonly guide?: unknown;
57
60
  /** plugins as written: package names and paths. extension/plugin-load.ts reads and checks them. */
58
61
  readonly plugins?: unknown;
59
62
  /** The code chaff.yaml names, once loaded (extension/load.ts). Reading chaff.yaml does not load it. */
@@ -66,6 +69,8 @@ export type AppliedStyle = {
66
69
  readonly options: Readonly<Record<string, unknown>>;
67
70
  /** The style's numbers, by rule and language, for the rules whose level it decided. */
68
71
  readonly limits: StyleLimits;
72
+ /** The style's changes to the genre guides, when it has any. */
73
+ readonly guide?: GuideLayer | undefined;
69
74
  };
70
75
 
71
76
  /** 判定の質が誤検知に直結するので、既定は最上位のモデル。cost は絞り込みで削る。spec §14。 */
@@ -200,6 +205,7 @@ export const loadConfig = (path: string): Config => {
200
205
  customRules: raw["custom_rules"],
201
206
  grade: raw["grade"],
202
207
  fixPlan: raw["fix_plan"],
208
+ guide: raw["guide"],
203
209
  plugins: raw["plugins"],
204
210
  };
205
211
  };
@@ -22,7 +22,7 @@ export const withStyle = (config: Config, styles: readonly StyleDefinition[]): C
22
22
  return {
23
23
  ...config,
24
24
  rules: { ...style.rules, ...config.rules },
25
- applied: { style: style.id, levelsFrom: fromStyle, options: style.options, limits },
25
+ applied: { style: style.id, levelsFrom: fromStyle, options: style.options, limits, guide: style.guide },
26
26
  };
27
27
  };
28
28
 
@@ -4,10 +4,12 @@ import type { UiLanguage } from "../ui.ts";
4
4
  import { ruleProblems } from "./rule-problems.ts";
5
5
  import { nameProblems } from "./name-problems.ts";
6
6
  import { configOptionProblems } from "./option-problems.ts";
7
+ import { configGuideProblems } from "../genre-guide/of-config.ts";
7
8
 
8
- /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply. Said before the results. */
9
+ /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply, guides that cannot be read. Said before the results. */
9
10
  export const settingWarnings = (config: Config, rules: readonly RuleDefinition[], ui: UiLanguage): string[] => [
10
11
  ...ruleProblems(config, rules, ui),
11
12
  ...nameProblems(config, ui),
12
13
  ...configOptionProblems(config, rules, ui),
14
+ ...configGuideProblems(config, ui),
13
15
  ];
@@ -5,7 +5,7 @@ import { customRulesOf } from "./load.ts";
5
5
  import { MAX_PATTERN_LENGTH, MAX_REPEATS, type RegexRefusal } from "./regex-safety.ts";
6
6
  import { POS_WRITTEN_NAMES } from "./token-pattern.ts";
7
7
  import type { ModulePathRefusal } from "./module-path.ts";
8
- import { fieldProblemSentence, type FieldKind, type FieldProblem } from "../rule-fields.ts";
8
+ import { FIELD_KINDS, fieldProblemSentence, type FieldKind, type FieldProblem } from "../rule-fields.ts";
9
9
 
10
10
  /** Each problem's sentence, with {at}, {written}, {field}, {index}, {refusal} and {names} filled in from the problem. */
11
11
  type Text = {
@@ -111,8 +111,6 @@ const valuesOf = (problem: CustomProblem, text: Text, scope: string): Readonly<R
111
111
  names: POS_WRITTEN_NAMES.join(" / "),
112
112
  });
113
113
 
114
- const FIELD_KINDS: ReadonlySet<string> = new Set<FieldKind>(["bad-depth", "bad-group", "bad-use-for", "bad-summary", "bad-example"]);
115
-
116
114
  const isFieldProblem = (problem: CustomProblem): problem is FieldProblem & { readonly at: string } => FIELD_KINDS.has(problem.kind);
117
115
 
118
116
  /** One problem as a sentence. scope names the list the rule is in: custom_rules, or a plugin's rules. */
@@ -0,0 +1,90 @@
1
+ import type { Detector, Finding, Lexicon, Token } from "../plugin.ts";
2
+ import { endsNounPhrase } from "./agreement-slip.ts";
3
+ import { isPartOfLongerWord } from "./doubled-word.ts";
4
+
5
+ /**
6
+ * 量を言う語を、数えられる名詞の複数に付けた所(less errors、less customer complaints)。語は語彙表 amount-quantifier が言う。
7
+ * 前が名詞か数なら「引く」の less(revenue less costs、$500 less fees)なので読まない。
8
+ */
9
+
10
+ export type AmountLists = {
11
+ /** 量を言う語(less)と、数を言う語(fewer, rewrite)。 */
12
+ readonly quantifiers: Lexicon;
13
+ /** 複数の形でも数えない名詞。単数と複数が同じ綴りの語(series)と、金額を言う複数(earnings)。 */
14
+ readonly uncounted: ReadonlySet<string>;
15
+ /** 名詞にならない be / have の形。名詞句がそこで終わる。 */
16
+ readonly finite: ReadonlySet<string>;
17
+ };
18
+
19
+ export type AmountSlip = { readonly quantifier: Token; readonly noun: Token; readonly rewrite: string };
20
+
21
+ const UPPER_START = /^\p{Lu}/u;
22
+
23
+ /** 前に立てば、量の語が限定詞として読める品詞。名詞・数・記号の後ろは「引く」の less。 */
24
+ const OPENS_PHRASE = new Set(["VERB", "AUX", "ADP", "CCONJ", "SCONJ", "ADV", "PART"]);
25
+ const OPENING_MARK = new Set(["(", "[", "{", '"', "'", "“", "‘"]);
26
+
27
+ const lower = (token: Token): string => token.surface.toLowerCase();
28
+
29
+ /** 解析器は三人称の動詞を複数の名詞と読むことがある(The parser reports less errors の reports)。 */
30
+ const mayBeVerb = (token: Token): boolean => token.pos === "NOUN" && token.features?.["AlsoVerb"] === "Yes" && token.features["Number"] === "Plur";
31
+
32
+ const opensPhrase = (previous: Token | undefined): boolean =>
33
+ previous === undefined || OPENS_PHRASE.has(previous.pos) || OPENING_MARK.has(previous.surface) || mayBeVerb(previous);
34
+
35
+ const isCommonNoun = (token: Token | undefined): token is Token =>
36
+ token?.pos === "NOUN" && token.features?.["Guess"] !== "Yes" && !UPPER_START.test(token.surface);
37
+
38
+ const isPlural = (token: Token): boolean => token.features?.["Number"] === "Plur";
39
+
40
+ /** 複数の形がはっきりした、数えられる普通名詞。形から当てた語(stimuli)と、数えない語は読まない。 */
41
+ const isCountedPlural = (noun: Token | undefined, uncounted: ReadonlySet<string>): noun is Token =>
42
+ isCommonNoun(noun) && isPlural(noun) && !uncounted.has(lower(noun));
43
+
44
+ /** less の後ろの名詞の頭。単数の名詞を一つ重ねた語(less customer complaints)は、その後ろの語。 */
45
+ const headAt = (tokens: readonly Token[], at: number): number => {
46
+ const next = tokens[at + 1];
47
+ return isCommonNoun(next) && !isPlural(next) ? at + 2 : at + 1;
48
+ };
49
+
50
+ const slipAt = (source: string, tokens: readonly Token[], at: number, lists: AmountLists): AmountSlip | undefined => {
51
+ const quantifier = tokens[at];
52
+ const entry = quantifier === undefined ? undefined : lists.quantifiers.find((candidate) => candidate.pattern.toLowerCase() === lower(quantifier));
53
+ if (quantifier?.pos !== "ADJ" || entry === undefined || !opensPhrase(tokens[at - 1])) return undefined;
54
+ const head = headAt(tokens, at);
55
+ const noun = tokens[head];
56
+ if (!isCountedPlural(noun, lists.uncounted) || !endsNounPhrase(tokens[head + 1], lists.finite) || isPartOfLongerWord(source, quantifier, noun))
57
+ return undefined;
58
+ return { quantifier, noun, rewrite: entry.rewrite ?? "" };
59
+ };
60
+
61
+ export const amountSlipsIn = (source: string, tokens: readonly Token[], lists: AmountLists): AmountSlip[] =>
62
+ tokens.flatMap((_, at) => slipAt(source, tokens, at, lists) ?? []);
63
+
64
+ const patternsOf = (lexicon: Lexicon): string[] => lexicon.map((entry) => entry.pattern.toLowerCase());
65
+
66
+ const listsOf = (lexicons: Readonly<Record<string, Lexicon>>): AmountLists => {
67
+ const words = lexicons["amount-quantifier"] ?? [];
68
+ return {
69
+ quantifiers: words.filter((entry) => entry.group === "quantifier"),
70
+ uncounted: new Set([...patternsOf(lexicons["invariant-noun"] ?? []), ...patternsOf(words.filter((entry) => entry.group === "amount"))]),
71
+ finite: new Set(patternsOf(lexicons["finite-auxiliary"] ?? [])),
72
+ };
73
+ };
74
+
75
+ export const amountForCount: Detector = (doc): Finding[] => {
76
+ const lists = listsOf(doc.lexicons);
77
+ return doc.sentences.flatMap((sentence) =>
78
+ amountSlipsIn(doc.source, sentence.tokens ?? [], lists).map((slip): Finding => {
79
+ const rest = doc.source.slice(slip.quantifier.span.end, slip.noun.span.end).replace(/\s+/gu, " ");
80
+ return {
81
+ rule: "",
82
+ severity: "warning",
83
+ line: 0,
84
+ column: 0,
85
+ quote: sentence.text.trim(),
86
+ values: { word: `${slip.quantifier.surface}${rest}`, suggestion: `${slip.rewrite}${rest}`, offset: slip.quantifier.span.start },
87
+ };
88
+ }),
89
+ );
90
+ };