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
@@ -0,0 +1,50 @@
1
+ import type { Texts, UiLanguage } from "../ui.ts";
2
+ import type { ReportedGuide } from "./report.ts";
3
+ import { BUNDLED_GUIDES } from "./resolve.ts";
4
+
5
+ const TEXT: Texts<{
6
+ readonly heading: (name: string, genre: string) => string;
7
+ readonly lead: string;
8
+ readonly rules: (ids: string) => string;
9
+ readonly from: (places: string) => string;
10
+ readonly change: string;
11
+ }> = {
12
+ ja: {
13
+ heading: (name, genre) => `指針: ${name}(${genre})`,
14
+ lead: "下の指摘より先に、原稿がこれを満たしているかを確かめてください。",
15
+ rules: (ids) => `このジャンルで特に効く rule: ${ids}`,
16
+ from: (places) => `書いたところ: ${places}`,
17
+ change: "chaff.yaml の guide: で書き換えられます。出さないときは --no-guide。",
18
+ },
19
+ en: {
20
+ heading: (name, genre) => `Guide: ${name} (${genre})`,
21
+ lead: "Before the findings below, check the draft against these.",
22
+ rules: (ids) => `Rules that matter most for this genre: ${ids}`,
23
+ from: (places) => `Written in: ${places}`,
24
+ change: "Change it with guide: in chaff.yaml; leave it out with --no-guide.",
25
+ },
26
+ };
27
+
28
+ const RULE = 60;
29
+
30
+ /** Who wrote the guide, said only when a team changed it: the bundled text alone needs no source line. */
31
+ const fromLines = (guide: ReportedGuide, ui: UiLanguage): string[] =>
32
+ guide.from.length === 1 && guide.from[0] === BUNDLED_GUIDES ? [] : [` ${TEXT[ui].from(guide.from.join(" → "))}`];
33
+
34
+ /** The guide as the text report opens with it, in the document's language: read first, as the instruction to write against. */
35
+ export const renderGuideBlock = (guide: ReportedGuide, ui: UiLanguage): string[] => {
36
+ const text = TEXT[ui];
37
+ return [
38
+ "",
39
+ text.heading(guide.name, guide.genre),
40
+ "",
41
+ ` ${text.lead}`,
42
+ "",
43
+ ...guide.lines.map((line) => ` - ${line}`),
44
+ ...(guide.rules.length === 0 ? [] : ["", ` ${text.rules(guide.rules.join(", "))}`]),
45
+ ...fromLines(guide, ui),
46
+ ` ${text.change}`,
47
+ "",
48
+ "═".repeat(RULE),
49
+ ];
50
+ };
@@ -0,0 +1,63 @@
1
+ import { showsGuide } from "../cli-args.ts";
2
+ import type { GenreSource } from "../cli-text.ts";
3
+ import type { Config } from "../config/load.ts";
4
+ import { loadGenres } from "../genre-load.ts";
5
+ import type { RuleDefinition } from "../plugin.ts";
6
+ import { localized } from "../render/text.ts";
7
+ import { uiLanguageOf, type UiLanguage } from "../ui.ts";
8
+ import { keyRulesOf } from "./key-rules.ts";
9
+ import { genreGuideFor } from "./of-config.ts";
10
+
11
+ /** A genre's guide as chaff reports it: the genre and its name, the lines, the rules that matter most, and who wrote it. */
12
+ export type ReportedGuide = {
13
+ readonly genre: string;
14
+ readonly name: string;
15
+ readonly lines: readonly string[];
16
+ readonly rules: readonly string[];
17
+ readonly from: readonly string[];
18
+ };
19
+
20
+ /**
21
+ * Where a genre counts as set, so its guide is shown. A defaulted genre is not one, nor one guessed from the path or the
22
+ * text: a guide for the wrong kind of document misleads more than none.
23
+ */
24
+ const SET_BY: ReadonlySet<GenreSource> = new Set<GenreSource>(["--genre", "by_path", "config", "front-matter"]);
25
+
26
+ export const genreWasSet = (from: GenreSource): boolean => SET_BY.has(from);
27
+
28
+ type GuideSettings = Pick<Config, "guide" | "applied" | "extensions" | "rules">;
29
+
30
+ /** The guide for a genre in a document's language, with the rules that matter most; undefined when it is off or there is none. */
31
+ export const reportedGuide = (config: GuideSettings, genre: string, language: string, rules: readonly RuleDefinition[]): ReportedGuide | undefined => {
32
+ const ui = uiLanguageOf(language);
33
+ const guide = genreGuideFor(config, genre, ui);
34
+ if (guide === undefined) return undefined;
35
+ const data = loadGenres();
36
+ const name = data.genres.find((entry) => entry.id === genre)?.name;
37
+ return {
38
+ genre,
39
+ name: name === undefined ? genre : localized(name, ui),
40
+ lines: guide.lines,
41
+ rules: keyRulesOf(rules, data, genre, language, config.rules),
42
+ from: guide.from,
43
+ };
44
+ };
45
+
46
+ /** One checked document, as far as its guide goes. */
47
+ type GuidedDocument = { readonly genre: string; readonly genreFrom: GenreSource; readonly language: string; readonly rules: readonly RuleDefinition[] };
48
+
49
+ /** A guide with the language it is written in, for output that holds several documents. */
50
+ export type RunGuide = { readonly language: UiLanguage; readonly guide: ReportedGuide };
51
+
52
+ /** A run's guides: one per genre and language among the documents whose genre was set, first seen first. None with --no-guide. */
53
+ export const guidesOfRun = (documents: readonly GuidedDocument[], config: GuideSettings, args: readonly string[]): RunGuide[] => {
54
+ if (!showsGuide(args)) return [];
55
+ const keyed = documents
56
+ .filter((document) => genreWasSet(document.genreFrom))
57
+ .map((document) => ({ key: `${document.genre}\n${uiLanguageOf(document.language)}`, document }));
58
+ const firsts = keyed.filter((entry, index) => keyed.findIndex((other) => other.key === entry.key) === index);
59
+ return firsts.flatMap(({ document }) => {
60
+ const guide = reportedGuide(config, document.genre, document.language, document.rules);
61
+ return guide === undefined ? [] : [{ language: uiLanguageOf(document.language), guide }];
62
+ });
63
+ };
@@ -0,0 +1,36 @@
1
+ import type { GenreData } from "../genre-parse.ts";
2
+ import type { GuideEdit, GuideLayer } from "./layer.ts";
3
+
4
+ /** Where the bundled guides come from, as a resolved guide names it. */
5
+ export const BUNDLED_GUIDES = "genres.yaml";
6
+
7
+ /** A genre's guide in one language, and every place that wrote it, weakest first. */
8
+ export type ResolvedGuide = { readonly genre: string; readonly lines: readonly string[]; readonly from: readonly string[] };
9
+
10
+ type Current = { readonly lines: readonly string[]; readonly from: readonly string[] };
11
+
12
+ const applied = (current: Current, edit: GuideEdit | undefined, language: string, from: string): Current => {
13
+ if (edit === undefined) return current;
14
+ if (edit.kind === "off") return { lines: [], from: [...current.from, from] };
15
+ const replaced = edit.replace?.[language];
16
+ const added = edit.add?.[language] ?? [];
17
+ if (replaced === undefined && added.length === 0) return current;
18
+ return { lines: [...(replaced ?? current.lines), ...added], from: [...current.from, from] };
19
+ };
20
+
21
+ /**
22
+ * A genre's guide in a language: genres.yaml's for the genre, else its group's, then each layer in order (weakest first):
23
+ * its guide: off, then its group's entry, then its genre's. undefined for a genre chaff does not know, or when no lines are left.
24
+ */
25
+ export const resolveGuide = (data: GenreData, genre: string, language: string, layers: readonly GuideLayer[]): ResolvedGuide | undefined => {
26
+ const own = data.genres.find((entry) => entry.id === genre);
27
+ if (own === undefined) return undefined;
28
+ const group = data.groups.find((entry) => genre.startsWith(`${entry.id}/`));
29
+ const bundled = own.guide ?? group?.guide;
30
+ const start: Current = { lines: bundled?.[language] ?? [], from: bundled === undefined ? [] : [BUNDLED_GUIDES] };
31
+ const end = layers.reduce((current, layer) => {
32
+ const cleared = applied(current, layer.allOff ? { kind: "off" } : undefined, language, layer.from);
33
+ return applied(applied(cleared, group === undefined ? undefined : layer.edits[group.id], language, layer.from), layer.edits[genre], language, layer.from);
34
+ }, start);
35
+ return end.lines.length === 0 ? undefined : { genre, lines: end.lines, from: end.from };
36
+ };
@@ -1,12 +1,13 @@
1
1
  import { isLevel } from "./levels.ts";
2
2
  import type { Level, Localized } from "./plugin.ts";
3
3
  import type { RuleOff } from "./rule-offs.ts";
4
+ import { bundledGuideOf, type GuideLines } from "./genre-guide/lines.ts";
4
5
 
5
6
  /** A preset's rules: the level each named rule runs at, under whatever chaff.yaml's rules say. */
6
7
  export type PresetLevels = Readonly<Record<string, Level>>;
7
8
 
8
- /** A group of genres (legal, literature): its name, and the levels every genre in it shares. */
9
- export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels };
9
+ /** A group of genres (legal, literature): its name, the levels every genre in it shares, and the guide a genre without its own reads. */
10
+ export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels; readonly guide?: GuideLines | undefined };
10
11
 
11
12
  /** One genre of genres.yaml: what it is for, the levels it sets on top of its group's, and the profile it reads with. */
12
13
  export type GenreDefinition = {
@@ -16,6 +17,8 @@ export type GenreDefinition = {
16
17
  readonly rules: PresetLevels;
17
18
  readonly profile: string | undefined;
18
19
  readonly suggest: GenreSuggest;
20
+ /** What a good document of this genre does, as lines to check a draft against (genre-guide/). */
21
+ readonly guide?: GuideLines | undefined;
19
22
  };
20
23
 
21
24
  /** A line shape that marks the genre, and how many lines must have it. */
@@ -59,10 +62,16 @@ const idOf = (value: unknown, index: number): string => {
59
62
  return id;
60
63
  };
61
64
 
65
+ /** guide: as a field only when written, so an entry without one reads as it did before guides. */
66
+ const guideField = (raw: Record<string, unknown>, id: string): { guide?: GuideLines } => {
67
+ const guide = bundledGuideOf(raw["guide"], id);
68
+ return guide === undefined ? {} : { guide };
69
+ };
70
+
62
71
  const groupOf = (value: unknown, index: number): GenreGroup => {
63
72
  const id = idOf(value, index);
64
73
  const raw = isRecord(value) ? value : {};
65
- return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id) };
74
+ return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id), ...guideField(raw, id) };
66
75
  };
67
76
 
68
77
  const patternOf = (value: unknown, flags: string, where: string): RegExp => {
@@ -121,6 +130,7 @@ const genreOf = (value: unknown, index: number): GenreDefinition => {
121
130
  rules: levelsOf(raw["rules"], id),
122
131
  profile,
123
132
  suggest: suggestOf(raw["suggest"], id),
133
+ ...guideField(raw, id),
124
134
  };
125
135
  };
126
136
 
package/src/init.ts CHANGED
@@ -41,6 +41,13 @@ genre: ${genre}
41
41
  # names:
42
42
  # - 個人情報保護委員会
43
43
 
44
+ # このジャンルの指針(よい文書が満たすこと)。検査の結果より先に出て、AI が書き直すときの指示になる。
45
+ # 既定は chaff に入っている文。replace で置き換え、add で足し、off で出さない。
46
+ # guide:
47
+ # ${genre}:
48
+ # add:
49
+ # - 金額は税込みか税抜きかを書いているか
50
+
44
51
  # 既定から変えたものだけを書く。
45
52
  rules:
46
53
  `,
@@ -80,6 +87,13 @@ genre: ${genre}
80
87
  # names:
81
88
  # - Bank of England
82
89
 
90
+ # This genre's guide: what a good document of this kind does. It comes before the findings, and an AI
91
+ # rewriting the text works to it. chaff ships one; replace it, add lines, or turn it off.
92
+ # guide:
93
+ # ${genre}:
94
+ # add:
95
+ # - Does each amount say whether tax is included?
96
+
83
97
  # Only what differs from the defaults.
84
98
  rules:
85
99
  `,
@@ -0,0 +1,106 @@
1
+ // The bullets a document writes its lists with (`-`, `*`, `+`, and `・` typed at a line's start), where each list sits, and
2
+ // the `・` lines Markdown folds into the item above. Pure; reads the source's Markdown tree.
3
+ import { spanOf, type MarkdownNode } from "./markdown-node.ts";
4
+ import type { Span } from "./plugin.ts";
5
+
6
+ /** One bulleted list as the reader meets it: where its first item starts, how deep it sits among lists, and its bullet. */
7
+ export type ListMark = {
8
+ readonly start: number;
9
+ readonly depth: number;
10
+ readonly mark: string;
11
+ /** The list written right before it in the same parent, when there is one: Markdown splits a list where the bullet changes. */
12
+ readonly previous: string | undefined;
13
+ };
14
+
15
+ /** Bullets Markdown reads as a list. */
16
+ const MARKDOWN_BULLETS = new Set(["-", "*", "+"]);
17
+
18
+ /** A bullet a Japanese writer types at a line's start. Markdown does not read it as a list, so the lines run together. */
19
+ const TYPED_BULLET = "・";
20
+
21
+ /** Blocks whose lists are someone else's or are not prose: quotations, code and HTML. */
22
+ const SKIPPED = new Set(["blockquote", "code", "html"]);
23
+
24
+ const bulletOf = (source: string, list: MarkdownNode): { readonly start: number; readonly mark: string } | undefined => {
25
+ const first = list.children?.[0];
26
+ const span = first === undefined ? undefined : spanOf(first);
27
+ const mark = span === undefined ? undefined : source.charAt(span.start);
28
+ return span === undefined || mark === undefined || !MARKDOWN_BULLETS.has(mark) ? undefined : { start: span.start, mark };
29
+ };
30
+
31
+ /** Where each line of a span starts, with the line's text. */
32
+ const linesIn = (source: string, span: Span): { readonly start: number; readonly text: string }[] => {
33
+ const lines = source.slice(span.start, span.end).split("\n");
34
+ const read: { start: number; text: string }[] = [];
35
+ lines.forEach((text) => {
36
+ const previous = read.at(-1);
37
+ read.push({ start: previous === undefined ? span.start : previous.start + previous.text.length + 1, text });
38
+ });
39
+ return read;
40
+ };
41
+
42
+ /** The start of each line after the first that begins with a typed bullet. */
43
+ const typedLineStarts = (source: string, paragraph: Span): number[] =>
44
+ linesIn(source, paragraph).flatMap((line, at) => {
45
+ const indent = line.text.length - line.text.trimStart().length;
46
+ return at > 0 && line.text.trimStart().startsWith(TYPED_BULLET) ? [line.start + indent] : [];
47
+ });
48
+
49
+ /** Lines of a paragraph that start with a typed bullet; two or more in a row are a list the writer typed. */
50
+ const typedListStart = (source: string, paragraph: Span): number | undefined => {
51
+ const lines = linesIn(source, paragraph).filter((line) => line.text.trim() !== "");
52
+ const typed = lines.filter((line) => line.text.trimStart().startsWith(TYPED_BULLET));
53
+ const first = typed[0];
54
+ return typed.length >= 2 && first !== undefined ? first.start + first.text.length - first.text.trimStart().length : undefined;
55
+ };
56
+
57
+ type Frame = { readonly node: MarkdownNode; readonly depth: number; readonly inItem: boolean };
58
+
59
+ export type ListReading = {
60
+ readonly lists: readonly ListMark[];
61
+ /** `・` lines inside a Markdown list item's paragraph: Markdown joins them to the item above. */
62
+ readonly foldedTyped: readonly number[];
63
+ };
64
+
65
+ const listsOf = (source: string, node: MarkdownNode, depth: number): ListMark[] => {
66
+ const children = node.children ?? [];
67
+ return children.flatMap((child, at) => {
68
+ if (child.type !== "list") return [];
69
+ const bullet = bulletOf(source, child);
70
+ const before = children[at - 1];
71
+ const previous = before?.type === "list" ? bulletOf(source, before)?.mark : undefined;
72
+ return bullet === undefined ? [] : [{ ...bullet, depth, previous }];
73
+ });
74
+ };
75
+
76
+ const typedOf = (source: string, node: MarkdownNode, depth: number, inItem: boolean): { lists: ListMark[]; folded: number[] } => {
77
+ const span = spanOf(node);
78
+ if (node.type !== "paragraph" || span === undefined) return { lists: [], folded: [] };
79
+ if (inItem) return { lists: [], folded: typedLineStarts(source, span) };
80
+ const start = typedListStart(source, span);
81
+ return { lists: start === undefined ? [] : [{ start, depth, mark: TYPED_BULLET, previous: undefined }], folded: [] };
82
+ };
83
+
84
+ /**
85
+ * Every bulleted list outside quotations and code, with its depth among lists (a list in a list item is one deeper), in source
86
+ * order. A paragraph with two or more lines that start with `・` counts as a list at the paragraph's depth; the same lines in a
87
+ * list item's paragraph are folded into that item.
88
+ */
89
+ export const readLists = (source: string, root: MarkdownNode): ListReading => {
90
+ const lists: ListMark[] = [];
91
+ const folded: number[] = [];
92
+ const pending: Frame[] = [{ node: root, depth: 0, inItem: false }];
93
+ for (let frame = pending.pop(); frame !== undefined; frame = pending.pop()) {
94
+ const { node, depth, inItem } = frame;
95
+ if (SKIPPED.has(node.type)) continue;
96
+ lists.push(...listsOf(source, node, depth));
97
+ const typed = typedOf(source, node, depth, inItem);
98
+ lists.push(...typed.lists);
99
+ folded.push(...typed.folded);
100
+ const childDepth = node.type === "list" ? depth + 1 : depth;
101
+ const childInItem = node.type === "listItem" || (inItem && node.type !== "list");
102
+ (node.children ?? []).forEach((child) => pending.push({ node: child, depth: childDepth, inItem: childInItem }));
103
+ }
104
+ const byStart = (left: number, right: number): number => left - right;
105
+ return { lists: lists.toSorted((left, right) => byStart(left.start, right.start)), foldedTyped: folded.toSorted(byStart) };
106
+ };
@@ -14,6 +14,7 @@ import { loadStyles } from "../style-load.ts";
14
14
  import type { OptionLayer } from "../rule-options.ts";
15
15
  import { CUSTOM_TYPES } from "../custom/parse.ts";
16
16
  import { DEFAULT_DEPTH, REWRITE_DEPTHS, depthMeaning } from "../rewrite-depth.ts";
17
+ import type { ReportedGuide } from "../genre-guide/report.ts";
17
18
 
18
19
  const TEXT: Texts<{
19
20
  readonly offBySetting: string;
@@ -185,6 +186,7 @@ export const rulesJson = (
185
186
  language: string,
186
187
  genre: string,
187
188
  optionLayers: readonly OptionLayer[] = [],
189
+ guide?: ReportedGuide,
188
190
  ): string => {
189
191
  const text = TEXT[uiLanguageOf(language)];
190
192
  const preset = presetLevels(genre);
@@ -196,6 +198,8 @@ export const rulesJson = (
196
198
  schema_version: 2,
197
199
  config_file: "chaff.yaml",
198
200
  detected: { genre, language },
201
+ // What a good document of the genre does: null unless the genre was set (--genre, chaff.yaml) and its guide is on.
202
+ guide: guide ?? null,
199
203
  ...(config.applied === undefined ? {} : { style: styleOf(config) }),
200
204
  values_you_can_use: ["strict", "normal", "relaxed", "off"],
201
205
  values_note: text.valuesNote,
@@ -1,5 +1,6 @@
1
1
  import { messageOf, readableText } from "./text.ts";
2
2
  import type { Finding, RuleDefinition, Severity } from "../plugin.ts";
3
+ import type { RunGuide } from "../genre-guide/report.ts";
3
4
 
4
5
  /**
5
6
  * 指摘を SARIF 2.1.0 で出す。GitHub の code scanning に上げるため。
@@ -114,7 +115,8 @@ const notRunNotifications = (notRun: readonly NotRunOnFile[]): SarifNotification
114
115
  }));
115
116
  };
116
117
 
117
- export const renderSarif = (located: readonly Located[], version: string, notRun: readonly NotRunOnFile[] = []): string => {
118
+ /** guides: the guides of the genres this run set, each with its language; none adds no run properties. */
119
+ export const renderSarif = (located: readonly Located[], version: string, notRun: readonly NotRunOnFile[] = [], guides: readonly RunGuide[] = []): string => {
118
120
  return `${JSON.stringify(
119
121
  {
120
122
  $schema: SCHEMA,
@@ -124,6 +126,7 @@ export const renderSarif = (located: readonly Located[], version: string, notRun
124
126
  tool: { driver: { name: "chaff", version, informationUri: INFORMATION_URI, rules: usedRules(located) } },
125
127
  invocations: [{ executionSuccessful: true, toolConfigurationNotifications: notRunNotifications(notRun) }],
126
128
  results: located.map(resultOf),
129
+ ...(guides.length === 0 ? {} : { properties: { guides: guides.map((entry) => ({ ...entry.guide, language: entry.language })) } }),
127
130
  },
128
131
  ],
129
132
  },
@@ -11,10 +11,14 @@ export type FieldProblem =
11
11
  | { readonly kind: "bad-group"; readonly written: string }
12
12
  | { readonly kind: "bad-use-for"; readonly written: string }
13
13
  | { readonly kind: "bad-summary"; readonly written: string }
14
+ | { readonly kind: "bad-text"; readonly written: string }
14
15
  | { readonly kind: "bad-example"; readonly written: string };
15
16
 
16
17
  export type FieldKind = FieldProblem["kind"];
17
18
 
19
+ /** Every kind, for a caller that reports these among problems of its own. */
20
+ export const FIELD_KINDS: ReadonlySet<string> = new Set<FieldKind>(["bad-depth", "bad-group", "bad-use-for", "bad-summary", "bad-example", "bad-text"]);
21
+
18
22
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
19
23
 
20
24
  const isText = (value: unknown): value is string => typeof value === "string" && value.trim() !== "";
@@ -41,6 +45,25 @@ const useForProblems = (useFor: unknown, genres: readonly string[]): FieldProble
41
45
  const summaryProblems = (summary: unknown): FieldProblem[] =>
42
46
  summary === undefined || isTextByLanguage(summary) ? [] : [{ kind: "bad-summary", written: printed(summary) }];
43
47
 
48
+ /** A language as a key of a text by language: ja, en, pt-br. */
49
+ const LANGUAGE_KEY = /^[a-z]{2,3}(?:-[a-z0-9]+)*$/iu;
50
+
51
+ /** The texts a reader sees. Each may be one string or one per language. */
52
+ const TEXT_FIELDS = ["name", "why", "how_to_fix", "message", "summary"] as const;
53
+
54
+ /**
55
+ * A text by language whose key is not a language, or whose value is not a text. In YAML, { en: Write a date, or a
56
+ * number of days } is two entries, the second keyed " or a number of days": without this, the text is cut silently.
57
+ */
58
+ const textProblems = (raw: Readonly<Record<string, unknown>>): FieldProblem[] =>
59
+ TEXT_FIELDS.flatMap((field) => {
60
+ const value = raw[field];
61
+ if (!isRecord(value)) return [];
62
+ return Object.entries(value)
63
+ .filter(([key, text]) => !LANGUAGE_KEY.test(key) || typeof text !== "string")
64
+ .map(([key]) => ({ kind: "bad-text" as const, written: `${field}: ${JSON.stringify(key)}` }));
65
+ });
66
+
44
67
  /** One example: before and after, each a text in every language or by language. */
45
68
  const isExamplePair = (value: unknown): boolean => isRecord(value) && isTextByLanguage(value["before"]) && isTextByLanguage(value["after"]);
46
69
 
@@ -68,6 +91,7 @@ export const fieldProblems = (raw: Readonly<Record<string, unknown>>, genres: re
68
91
  ...useForProblems(raw["use_for"], genres),
69
92
  ...exampleProblems(raw["example"]),
70
93
  ...depthProblems(raw["rewrite"]),
94
+ ...textProblems(raw),
71
95
  ];
72
96
 
73
97
  const TEXT: Texts<Readonly<Record<FieldKind, (written: string) => string>>> = {
@@ -77,6 +101,8 @@ const TEXT: Texts<Readonly<Record<FieldKind, (written: string) => string>>> = {
77
101
  "bad-use-for": (written) =>
78
102
  `use_for: ${written} はジャンルではありません。ジャンルかその頭(business、business/report)を並べます。一覧は npx chaffjs genres。`,
79
103
  "bad-summary": (written) => `summary: ${written} は読めません。一つの文字列か、言語ごとの文字列({ ja: …, en: … })で書きます。`,
104
+ "bad-text": (written) =>
105
+ `${written} は言語ではありません。{ } の中のカンマは項目の区切りになるので、カンマのある文は引用符で囲みます(en: "Write a date, or a number of days.")。`,
80
106
  "bad-example": (written) =>
81
107
  `example: ${written} の before と after が揃っていません。{ before, after } か、言語ごとに { ja: { before, after }, en: { before, after } } と書きます。`,
82
108
  },
@@ -86,6 +112,8 @@ const TEXT: Texts<Readonly<Record<FieldKind, (written: string) => string>>> = {
86
112
  "bad-use-for": (written) =>
87
113
  `use_for: ${written} is not a genre. List genres or their first part (business, business/report); npx chaffjs genres lists them.`,
88
114
  "bad-summary": (written) => `summary: cannot read ${written}. Write one string, or one per language ({ ja: …, en: … }).`,
115
+ "bad-text": (written) =>
116
+ `${written} is not a language. Inside { }, a comma separates entries, so quote a text that has one (en: "Write a date, or a number of days.").`,
89
117
  "bad-example": (written) =>
90
118
  `example: ${written} lacks a before or an after. Write { before, after }, or one per language: { ja: { before, after }, en: { before, after } }.`,
91
119
  },
@@ -1,5 +1,8 @@
1
1
  import { isLevel } from "./levels.ts";
2
2
  import type { Level, Localized } from "./plugin.ts";
3
+ import { guideLayerOf, type GuideLayer } from "./genre-guide/layer.ts";
4
+ import { guideProblemText } from "./genre-guide/problem-text.ts";
5
+ import { knownGenres } from "./known-genres.ts";
3
6
 
4
7
  // A house style (styles/*.yaml): a well-known guideline a team picks with style: in chaff.yaml. It sets rule levels and
5
8
  // options, and says where they come from. Pure: the YAML comes in already parsed.
@@ -21,6 +24,8 @@ export type StyleDefinition = {
21
24
  readonly options: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
22
25
  /** The numbers the guideline sets, by language. The rule needs a level under rules too, or the limit decides nothing. */
23
26
  readonly limits: StyleLimits;
27
+ /** Its changes to the genre guides (genre-guide/layer.ts), between a rule pack's and chaff.yaml's. */
28
+ readonly guide?: GuideLayer | undefined;
24
29
  };
25
30
 
26
31
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -85,6 +90,15 @@ const limitsOf = (value: unknown, rules: Readonly<Record<string, Level>>, where:
85
90
  );
86
91
  };
87
92
 
93
+ /** The style's guide: as a layer named after the style. undefined when it writes none. */
94
+ const guideOf = (value: unknown, id: string, where: string): GuideLayer | undefined => {
95
+ if (value === undefined) return undefined;
96
+ const read = guideLayerOf(value, `style: ${id}`, knownGenres());
97
+ const [problem] = read.problems;
98
+ if (problem !== undefined) throw new Error(`${where}: ${guideProblemText(problem, "en")}`);
99
+ return read.layer;
100
+ };
101
+
88
102
  /** One style file. Throws on anything it cannot read: the file ships with chaff, so a wrong entry is chaff's bug. */
89
103
  export const parseStyle = (raw: unknown, file: string): StyleDefinition => {
90
104
  const id = isRecord(raw) ? raw["id"] : undefined;
@@ -99,7 +113,10 @@ export const styleOf = (raw: unknown, where: string): StyleDefinition => {
99
113
  if (typeof id !== "string" || id === "") throw new Error(`${where}: a style needs an id`);
100
114
  const rules = levelsOf(raw["rules"], where);
101
115
  const options = optionsOf(raw["options"], where);
102
- if (Object.keys(rules).length === 0 && Object.keys(options).length === 0) throw new Error(`${where}: a style must set rules or options`);
116
+ const guide = guideOf(raw["guide"], id, where);
117
+ if (Object.keys(rules).length === 0 && Object.keys(options).length === 0 && guide === undefined) {
118
+ throw new Error(`${where}: a style must set rules, options or guide`);
119
+ }
103
120
  return {
104
121
  id,
105
122
  name: localizedOf(raw["name"], "name", where),
@@ -108,5 +125,6 @@ export const styleOf = (raw: unknown, where: string): StyleDefinition => {
108
125
  rules,
109
126
  options,
110
127
  limits: limitsOf(raw["limits"], rules, where),
128
+ ...(guide === undefined ? {} : { guide }),
111
129
  };
112
130
  };