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,125 @@
1
+ import type { Detector, Finding } from "../plugin.ts";
2
+ import { headingWords } from "../structure-shape/heading-words.ts";
3
+ import { quoteAround } from "./quote-around.ts";
4
+
5
+ // 目次の項目が、文書のどの見出しとも合わない所。目次の見出しは語彙表 toc-heading が言う。
6
+ // 項目と見出しは、空白・記号・大文字小文字を無視して比べ、頭の番号(1.2、2-1、第3章)を外しても比べる。
7
+
8
+ /** 見出しの無い目次の項目 1 つ。entry は項目の言葉(リンクなら字の部分)。 */
9
+ export type StrayTocEntry = { readonly offset: number; readonly entry: string };
10
+
11
+ type Line = { readonly text: string; readonly offset: number };
12
+ type Heading = { readonly title: string; readonly offset: number };
13
+
14
+ const HEADING = /^[ \t]{0,3}#{1,6}[ \t]/u;
15
+ const FENCE = /^[ \t]*(?:```|~~~)/u;
16
+ const LIST_ITEM = /^[ \t]*(?:[-*+]|\d{1,3}[.)])[ \t]/u;
17
+ const LINK_TEXT = /^\[(?<text>[^\]]*)\]\([^)]*\)/u;
18
+ const ATTRIBUTES = /\{#[^}]*\}/gu;
19
+ const DIGIT = /[\d0-9]/u;
20
+ const LEADER_DOTS = new Set([".", "…", "・"]);
21
+ const MIN_LEADER = 2;
22
+ /** 目次の項目のうち見出しに合わないものがこれより多ければ、目次は別の書き方の見出し(本文に無い章立て)と読んで言わない。 */
23
+ const MAX_STRAY_SHARE = 0.5;
24
+
25
+ const linesOf = (source: string): Line[] => [...source.matchAll(/^.*$/gmu)].map((match) => ({ text: match[0], offset: match.index }));
26
+
27
+ /** コードの囲みの外の行。 */
28
+ const outsideFences = (lines: readonly Line[]): Line[] => {
29
+ const fence = { open: false };
30
+ return lines.filter((line) => {
31
+ if (FENCE.test(line.text)) {
32
+ fence.open = !fence.open;
33
+ return false;
34
+ }
35
+ return !fence.open;
36
+ });
37
+ };
38
+
39
+ /** 比べるための形。全角を半角にそろえ(4 と 4)、空白・記号を消し、小文字にする。 */
40
+ const comparable = (text: string): string =>
41
+ text
42
+ .replace(ATTRIBUTES, "")
43
+ .normalize("NFKC")
44
+ .toLowerCase()
45
+ .replace(/[\s\p{P}\p{S}]/gu, "");
46
+
47
+ /** 頭の番号(1.2、2-1、第3章)と飾りを外した、比べるための形。 */
48
+ const withoutNumber = (text: string): string => comparable(headingWords(text));
49
+
50
+ /** text の終わりから、isTail に当たる字を外したもの。 */
51
+ const trimTail = (text: string, isTail: (char: string) => boolean): string => {
52
+ const chars = [...text];
53
+ return chars.slice(0, chars.findLastIndex((char) => !isTail(char)) + 1).join("");
54
+ };
55
+
56
+ /** 見出しの行の題(頭と終わりの # を外す)。見出しでなければ undefined。 */
57
+ const headingTitle = (line: string): string | undefined => {
58
+ if (!HEADING.test(line)) return undefined;
59
+ const withoutMarks = line.trim().replace(/^#+/u, "").trim();
60
+ return trimTail(withoutMarks, (char) => char === "#").trim();
61
+ };
62
+
63
+ /** 終わりのページ番号(「背景 ........ 3」の 3 と点線)。点線の無い数(Step 3)は題のうち。 */
64
+ const withoutPageNumber = (text: string): string => {
65
+ const before = trimTail(text, (char) => DIGIT.test(char)).trimEnd();
66
+ if (before === text.trimEnd()) return text;
67
+ const title = trimTail(before, (char) => LEADER_DOTS.has(char));
68
+ return before.length - title.length >= MIN_LEADER ? title.trim() : text;
69
+ };
70
+
71
+ /** 項目の言葉と、項目の行の中でそれが始まる所。リンクなら字の部分(番号の後ろのリンク「1. [概要](#…)」も)。 */
72
+ const entryOf = (item: string): { readonly text: string; readonly start: number } => {
73
+ const bracket = item.indexOf("[");
74
+ const link = bracket === -1 ? undefined : LINK_TEXT.exec(item.slice(bracket))?.groups?.["text"];
75
+ if (link !== undefined) return { text: withoutPageNumber(link), start: bracket + 1 };
76
+ return { text: withoutPageNumber(item.trim()), start: item.length - item.trimStart().length };
77
+ };
78
+
79
+ const matchesHeading = (entry: string, headings: readonly Heading[]): boolean =>
80
+ headings.some(
81
+ (heading) => comparable(heading.title) === comparable(entry) || (withoutNumber(entry) !== "" && withoutNumber(heading.title) === withoutNumber(entry)),
82
+ );
83
+
84
+ /** 目次の見出しの下(次の見出しまで)の箇条書きの項目。 */
85
+ const entriesUnder = (lines: readonly Line[], tocAt: number): Line[] => {
86
+ const after = lines.filter((line) => line.offset > tocAt);
87
+ const end = after.findIndex((line) => HEADING.test(line.text));
88
+ return (end === -1 ? after : after.slice(0, end)).flatMap((line) => {
89
+ const marker = LIST_ITEM.exec(line.text)?.[0];
90
+ if (marker === undefined) return [];
91
+ const entry = entryOf(line.text.slice(marker.length));
92
+ return [{ text: entry.text, offset: line.offset + marker.length + entry.start }];
93
+ });
94
+ };
95
+
96
+ /** 文書の目次の項目のうち、どの見出しとも合わないもの。 */
97
+ export const strayTocEntries = (source: string, tocWords: readonly string[]): StrayTocEntry[] => {
98
+ const lines = outsideFences(linesOf(source));
99
+ const headings = lines.flatMap((line) => {
100
+ const title = headingTitle(line.text);
101
+ return title === undefined ? [] : [{ title, offset: line.offset }];
102
+ });
103
+ const tocNames = new Set(tocWords.map(comparable));
104
+ return headings
105
+ .filter((heading) => tocNames.has(comparable(heading.title)))
106
+ .flatMap((toc) => {
107
+ const entries = entriesUnder(lines, toc.offset).filter((entry) => entry.text !== "");
108
+ const others = headings.filter((heading) => heading !== toc);
109
+ const stray = entries.filter((entry) => !matchesHeading(entry.text, others));
110
+ return stray.length > entries.length * MAX_STRAY_SHARE ? [] : stray.map((entry) => ({ offset: entry.offset, entry: entry.text }));
111
+ });
112
+ };
113
+
114
+ export const tocHeading: Detector = (doc): Finding[] =>
115
+ strayTocEntries(
116
+ doc.source,
117
+ (doc.lexicons["toc-heading"] ?? []).map((entry) => entry.pattern),
118
+ ).map((stray) => ({
119
+ rule: "",
120
+ severity: "warning",
121
+ line: 0,
122
+ column: 0,
123
+ quote: quoteAround(doc.source, stray.offset, stray.offset + stray.entry.length),
124
+ values: { entry: stray.entry, offset: stray.offset },
125
+ }));
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import type { Config } from "../config/load.ts";
3
3
  import type { Detector, Lexicon, RuleDefinition } from "../plugin.ts";
4
4
  import type { StyleDefinition } from "../style-parse.ts";
5
+ import type { GuideLayer } from "../genre-guide/layer.ts";
5
6
  import { customRulesOf } from "../custom/load.ts";
6
7
  import { withStyle } from "../config/style.ts";
7
8
  import { moduleDetector } from "./module-detector.ts";
@@ -33,6 +34,8 @@ export type Extensions = {
33
34
  readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
34
35
  /** The plugins' house styles. */
35
36
  readonly styles: readonly StyleDefinition[];
37
+ /** The plugins' changes to the genre guides, in the order chaff.yaml lists the plugins. */
38
+ readonly guides?: readonly GuideLayer[];
36
39
  readonly problems: readonly LoadProblem[];
37
40
  readonly pluginProblems: readonly PluginProblem[];
38
41
  };
@@ -85,6 +88,7 @@ const loadExtensions = async (config: Config): Promise<Extensions> => {
85
88
  rules: plugins.flatMap((plugin) => plugin.rules),
86
89
  lexicons: lexiconsByLanguage(plugins),
87
90
  styles: plugins.flatMap((plugin) => plugin.styles),
91
+ guides: plugins.flatMap((plugin) => (plugin.guide === undefined ? [] : [plugin.guide])),
88
92
  problems: modules.flatMap((entry) => ("problem" in entry.loaded ? [entry.loaded.problem] : [])),
89
93
  pluginProblems,
90
94
  };
@@ -123,6 +123,7 @@ export const readPack = (dir: string, manifest: Readonly<Record<string, unknown>
123
123
  rules: rules.value,
124
124
  lexicons: lexicons.value,
125
125
  styles: styles.value,
126
+ ...(manifest["guide"] === undefined ? {} : { guide: manifest["guide"] }),
126
127
  },
127
128
  };
128
129
  };
@@ -19,7 +19,7 @@ export type LoadedPlugins = { readonly plugins: readonly ParsedPlugin[]; readonl
19
19
  /** pack: the folder of a rule pack (YAML only), which is read rather than imported. */
20
20
  type Located = { readonly origin: PluginOrigin; readonly pack?: string } | { readonly problem: PluginProblem };
21
21
 
22
- const problemOf = (kind: Exclude<PluginProblem["kind"], "rule">, plugin: string, detail = ""): { problem: PluginProblem } => ({
22
+ const problemOf = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, plugin: string, detail = ""): { problem: PluginProblem } => ({
23
23
  problem: { kind, plugin, detail },
24
24
  });
25
25
 
@@ -7,6 +7,7 @@ import type { UntrustedDetector } from "./module-detector.ts";
7
7
  import { describeValue } from "./returned-findings.ts";
8
8
  import { isPluginName } from "./plugin-name.ts";
9
9
  import { knownGenres } from "../known-genres.ts";
10
+ import { guideLayerOf, type GuideLayer, type GuideProblem } from "../genre-guide/layer.ts";
10
11
 
11
12
  // What a plugin package exports (api.ts PluginSpec), read into what chaff runs: its rules with ids under the plugin's
12
13
  // name, their detectors, its word lists and its house styles. A rule is read exactly like a rule in custom_rules, with
@@ -32,7 +33,8 @@ type PluginProblemKind =
32
33
  /** plugin: as chaff.yaml writes it. detail: what was there instead, the name expected, the rule, list or style at fault. */
33
34
  export type PluginProblem =
34
35
  | { readonly kind: PluginProblemKind; readonly plugin: string; readonly detail: string }
35
- | { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem };
36
+ | { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem }
37
+ | { readonly kind: "guide"; readonly plugin: string; readonly problem: GuideProblem };
36
38
 
37
39
  /** Where a plugin came from: as chaff.yaml writes it, the file imported, and the name its package's name gives (none for a path). */
38
40
  export type PluginOrigin = { readonly written: string; readonly file: string; readonly expectedName: string | undefined };
@@ -47,11 +49,13 @@ export type ParsedPlugin = {
47
49
  /** Word lists by language, then by name with the plugin's prefix. */
48
50
  readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
49
51
  readonly styles: readonly StyleDefinition[];
52
+ /** Its changes to the genre guides, applied whenever the plugin is loaded: under a style's and chaff.yaml's. */
53
+ readonly guide?: GuideLayer | undefined;
50
54
  };
51
55
 
52
56
  export type PluginRead = { readonly plugin: ParsedPlugin | undefined; readonly problems: readonly PluginProblem[] };
53
57
 
54
- type Problem = (kind: Exclude<PluginProblem["kind"], "rule">, detail: string) => PluginProblem;
58
+ type Problem = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, detail: string) => PluginProblem;
55
59
 
56
60
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
57
61
 
@@ -215,6 +219,16 @@ const stylesOf = (raw: unknown, name: string, problem: Problem): Styles => {
215
219
  };
216
220
  };
217
221
 
222
+ // ───────── guide ─────────
223
+
224
+ type Guide = { readonly guide: GuideLayer | undefined; readonly problems: readonly PluginProblem[] };
225
+
226
+ const guideOf = (raw: unknown, name: string, written: string): Guide => {
227
+ if (raw === undefined) return { guide: undefined, problems: [] };
228
+ const read = guideLayerOf(raw, `plugins: ${name}`, knownGenres());
229
+ return { guide: read.layer, problems: read.problems.map((problem) => ({ kind: "guide", plugin: written, problem })) };
230
+ };
231
+
218
232
  // ───────── the plugin ─────────
219
233
 
220
234
  /** A plugin's default export, read. Every problem is reported; a plugin with any is left out whole, and the run is stopped. */
@@ -229,8 +243,17 @@ export const parsePlugin = (exported: unknown, origin: PluginOrigin, useFor: rea
229
243
  const lexicons = lexiconsOf(exported["lexicons"], name, problem);
230
244
  const rules = rulesOf(exported["rules"], { name, origin, useFor, lists: lexicons.lists });
231
245
  const styles = stylesOf(exported["styles"], name, problem);
232
- const problems = [...rules.problems, ...lexicons.problems, ...styles.problems];
246
+ const guide = guideOf(exported["guide"], name, origin.written);
247
+ const problems = [...rules.problems, ...lexicons.problems, ...styles.problems, ...guide.problems];
233
248
  if (problems.length > 0) return { plugin: undefined, problems };
234
- const plugin = { name, written: origin.written, rules: rules.rules, detectors: rules.detectors, lexicons: lexicons.lexicons, styles: styles.styles };
249
+ const plugin = {
250
+ name,
251
+ written: origin.written,
252
+ rules: rules.rules,
253
+ detectors: rules.detectors,
254
+ lexicons: lexicons.lexicons,
255
+ styles: styles.styles,
256
+ ...(guide.guide === undefined ? {} : { guide: guide.guide }),
257
+ };
235
258
  return { plugin, problems: [] };
236
259
  };
@@ -4,6 +4,7 @@ import type { Texts, UiLanguage } from "../ui.ts";
4
4
  import { customProblemSentence } from "../custom/problems.ts";
5
5
  import type { LoadProblem } from "./load.ts";
6
6
  import type { PluginProblem } from "./plugin-parse.ts";
7
+ import { guideProblemText } from "../genre-guide/problem-text.ts";
7
8
 
8
9
  // What in the code chaff.yaml names could not be loaded, as sentences. Each names the rule or plugin and the file or
9
10
  // package, as chaff.yaml writes it.
@@ -11,7 +12,9 @@ import type { PluginProblem } from "./plugin-parse.ts";
11
12
  type ModuleText = Readonly<Record<LoadProblem["kind"], (rule: string, file: string, detail: string) => string>>;
12
13
 
13
14
  type PluginText = {
14
- readonly problems: Readonly<Record<Exclude<PluginProblem["kind"], "rule">, (plugin: string, detail: string) => string>>;
15
+ readonly problems: Readonly<Record<Exclude<PluginProblem["kind"], "rule" | "guide">, (plugin: string, detail: string) => string>>;
16
+ /** Where a plugin's guide is, for a problem chaff.yaml's guide could have too. */
17
+ readonly guideScope: (plugin: string) => string;
15
18
  /** Where a plugin's rule is, for a problem a rule in custom_rules could have too. */
16
19
  readonly ruleScope: (plugin: string) => string;
17
20
  };
@@ -42,6 +45,7 @@ const MODULE_TEXT: Texts<ModuleText> = {
42
45
  const PLUGIN_TEXT: Texts<PluginText> = {
43
46
  ja: {
44
47
  ruleScope: (plugin) => `plugins の ${plugin} の rules`,
48
+ guideScope: (plugin) => `plugins の ${plugin}`,
45
49
  problems: {
46
50
  "not-a-list": (_plugin, detail) => `plugins はプラグインの並び(- chaff-plugin-foo や - ./local-plugin)で書いてください(${detail})`,
47
51
  "bad-specifier": (plugin) =>
@@ -65,6 +69,7 @@ const PLUGIN_TEXT: Texts<PluginText> = {
65
69
  },
66
70
  en: {
67
71
  ruleScope: (plugin) => `plugins ${plugin} rules`,
72
+ guideScope: (plugin) => `plugins ${plugin}`,
68
73
  problems: {
69
74
  "not-a-list": (_plugin, detail) => `write plugins as a list (- chaff-plugin-foo or - ./local-plugin), not ${detail}`,
70
75
  "bad-specifier": (plugin) =>
@@ -90,6 +95,7 @@ const PLUGIN_TEXT: Texts<PluginText> = {
90
95
  const pluginSentence = (problem: PluginProblem, ui: UiLanguage): string => {
91
96
  const text = PLUGIN_TEXT[ui];
92
97
  if (problem.kind === "rule") return customProblemSentence(problem.problem, ui, text.ruleScope(problem.plugin));
98
+ if (problem.kind === "guide") return `${text.guideScope(problem.plugin)}: ${guideProblemText(problem.problem, ui)}`;
93
99
  return text.problems[problem.kind](problem.plugin, problem.detail);
94
100
  };
95
101
 
@@ -8,6 +8,7 @@ import { COMPOSITE_RULE, recommendMode, type ModeChoice } from "./mode.ts";
8
8
  import type { StructureScore } from "../structure-shape/score.ts";
9
9
  import { structureTargetsOf, type StructureTarget } from "./structure-targets.ts";
10
10
  import { DEFAULT_DEPTH, depthIncludes, type RewriteDepth } from "../rewrite-depth.ts";
11
+ import type { ReportedGuide } from "../genre-guide/report.ts";
11
12
 
12
13
  /** One flagged spot, where it is and what chaff said about it. */
13
14
  export type PlanSpot = { readonly line: number; readonly column: number; readonly quote: string; readonly message: string };
@@ -46,6 +47,8 @@ export type FixPlan = {
46
47
  readonly rewrittenPath: string;
47
48
  readonly language: string;
48
49
  readonly genre: string;
50
+ /** What a good document of the genre does, to rewrite against; null when the genre was not set or the guide is off. */
51
+ readonly guide: ReportedGuide | null;
49
52
  readonly experimental: boolean;
50
53
  readonly mode: ModeChoice;
51
54
  /** None when chaff recommends the depth itself. */
@@ -80,6 +83,7 @@ export type PlanInput = {
80
83
  /** The ai-tell lexicon of the document's language, whose entries may carry their own hint. */
81
84
  readonly phrases: Lexicon;
82
85
  readonly chosenDepth?: ChosenDepth | undefined;
86
+ readonly guide?: ReportedGuide | undefined;
83
87
  };
84
88
 
85
89
  const BOLD_DENSITY_RULE = "bold-density";
@@ -220,6 +224,7 @@ export const buildFixPlan = (input: PlanInput): FixPlan => {
220
224
  rewrittenPath: rewrittenPathOf(input.path),
221
225
  language: input.language,
222
226
  genre: input.genre,
227
+ guide: input.guide ?? null,
223
228
  experimental: input.experimental,
224
229
  mode,
225
230
  chosenDepth: input.chosenDepth,
@@ -129,6 +129,14 @@ const checkLines = (plan: FixPlan, text: FixPlanText): string[] => [
129
129
  text.checksNote,
130
130
  ];
131
131
 
132
+ /** The genre's guide, first: the instruction the rewrite is held to. Nothing when the genre was not set or the guide is off. */
133
+ const guideLines = (plan: FixPlan, text: FixPlanText): string[] => {
134
+ const { guide } = plan;
135
+ if (guide === null) return [];
136
+ const rules = guide.rules.length === 0 ? [] : ["", text.guideRules(guide.rules.map((rule) => `\`${rule}\``).join(", "))];
137
+ return [`## ${text.guideHeading(guide.name, guide.genre)}`, "", text.guideLead, "", ...bullets(guide.lines), ...rules, ""];
138
+ };
139
+
132
140
  const textOf = (plan: FixPlan): FixPlanText => FIX_PLAN_TEXT[uiLanguageOf(plan.language)];
133
141
 
134
142
  /** The plan as Markdown, in the document's language: what an agent reads before rewriting. */
@@ -141,6 +149,7 @@ export const renderFixPlanMarkdown = (plan: FixPlan): string => {
141
149
  "",
142
150
  text.intro,
143
151
  "",
152
+ ...guideLines(plan, text),
144
153
  `## ${text.constraintsHeading}`,
145
154
  "",
146
155
  ...text.constraints.map((constraint, index) => `${String(index + 1)}. ${constraint}`),
@@ -7,6 +7,10 @@ export type FixPlanText = {
7
7
  readonly title: (path: string) => string;
8
8
  readonly about: (language: string, genre: string) => string;
9
9
  readonly intro: string;
10
+ /** The genre's guide, as the first section: what the rewrite is held to. */
11
+ readonly guideHeading: (name: string, genre: string) => string;
12
+ readonly guideLead: string;
13
+ readonly guideRules: (ids: string) => string;
10
14
  readonly constraintsHeading: string;
11
15
  readonly constraints: readonly string[];
12
16
  readonly modeHeading: string;
@@ -43,12 +47,15 @@ export type FixPlanText = {
43
47
 
44
48
  export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
45
49
  ja: {
46
- usage: "使い方: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <ジャンル>] [--json]",
50
+ usage: "使い方: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <ジャンル>] [--no-guide] [--json]",
47
51
  notFound: (path) => `${path} がありません。`,
48
52
  title: (path) => `直す計画: ${path}`,
49
53
  about: (language, genre) => `言語 ${language}、ジャンル ${genre}`,
50
54
  intro:
51
55
  "chaff が機械で見つけた箇所と、それぞれの直す方向です。chaff は書き直しません。書き直すのは、この計画を読む人か AI です。直したら、最後の確かめのコマンドを実行してください。",
56
+ guideHeading: (name, genre) => `指針: ${name}(${genre})`,
57
+ guideLead: "このジャンルのよい文書が満たすことです。書き直しはこれに沿わせ、書き終えたら一つずつ確かめてください。",
58
+ guideRules: (ids) => `このジャンルで特に効く rule: ${ids}`,
52
59
  constraintsHeading: "守ること",
53
60
  constraints: [
54
61
  "事実、数、日付、条件、名前を変えない。",
@@ -109,12 +116,15 @@ export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
109
116
  "1 つめは書き直した文書の指摘、2 つめは事実が落ちていないか・足されていないか、3 つめは構成の変化です。数は chaff の出力から取り、形容詞で言わないでください。",
110
117
  },
111
118
  en: {
112
- usage: "usage: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <genre>] [--json]",
119
+ usage: "usage: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <genre>] [--no-guide] [--json]",
113
120
  notFound: (path) => `${path} does not exist.`,
114
121
  title: (path) => `Fix plan: ${path}`,
115
122
  about: (language, genre) => `language ${language}, genre ${genre}`,
116
123
  intro:
117
124
  "What chaff found by machine, and how to rewrite each kind of spot. chaff does not rewrite; whoever reads this plan does, a person or an AI. When done, run the checks at the end.",
125
+ guideHeading: (name, genre) => `Guide: ${name} (${genre})`,
126
+ guideLead: "What a good document of this genre does. Rewrite toward it, and check the result against each line when done.",
127
+ guideRules: (ids) => `Rules that matter most for this genre: ${ids}`,
118
128
  constraintsHeading: "Constraints",
119
129
  constraints: [
120
130
  "Keep every fact, number, date, condition and name.",
@@ -0,0 +1,37 @@
1
+ import { presetLevelsOf, type GenreData } from "../genre-parse.ts";
2
+ import type { Level, RuleDefinition } from "../plugin.ts";
3
+ import { standingIn } from "../rule-genres.ts";
4
+
5
+ /** A rule that runs in at most this share of the genres speaks for the few it runs in. */
6
+ const RARE_SHARE = 0.25;
7
+
8
+ type RuleFacts = Pick<RuleDefinition, "id" | "status" | "use_for" | "languages">;
9
+
10
+ const runsIn = (rule: RuleFacts, data: GenreData, genre: string): boolean => standingIn(rule, genre, presetLevelsOf(data, genre)).kind === "on";
11
+
12
+ const isRare = (rule: RuleFacts, data: GenreData): boolean =>
13
+ data.genres.filter((entry) => runsIn(rule, data, entry.id)).length <= data.genres.length * RARE_SHARE;
14
+
15
+ const readsLanguage = (rule: RuleFacts, language: string): boolean => rule.languages === undefined || rule.languages.includes(language);
16
+
17
+ /**
18
+ * The rules that matter most for a genre, by id: those its preset turns on, and those that run in it but in few other
19
+ * genres. A rule the team's settings turn off is left out. Pure: the genres and the settings come in.
20
+ */
21
+ export const keyRulesOf = (
22
+ rules: readonly RuleFacts[],
23
+ data: GenreData,
24
+ genre: string,
25
+ language: string,
26
+ settings: Readonly<Record<string, Level>>,
27
+ ): string[] => {
28
+ const preset = presetLevelsOf(data, genre);
29
+ const matters = (rule: RuleFacts): boolean => {
30
+ const standing = standingIn(rule, genre, preset);
31
+ return standing.kind === "on" && (standing.byPreset || isRare(rule, data));
32
+ };
33
+ return rules
34
+ .filter((rule) => readsLanguage(rule, language) && settings[rule.id] !== "off" && matters(rule))
35
+ .map((rule) => rule.id)
36
+ .toSorted((left, right) => left.localeCompare(right, "en"));
37
+ };
@@ -0,0 +1,77 @@
1
+ import { guideLinesOf, type GuideLines } from "./lines.ts";
2
+
3
+ // A team's changes to the genre guides, as chaff.yaml, a style or a rule pack writes them under guide:
4
+ //
5
+ // guide:
6
+ // blog/tech: # a genre, or a group (legal) for every genre in it
7
+ // add: { ja: [...], en: [...] }
8
+ // legal/contract:
9
+ // replace: { ja: [...], en: [...] }
10
+ // business/email: off
11
+ //
12
+ // replace sets the lines for the languages it gives (the others keep theirs); add puts lines after them; off leaves the
13
+ // genre with no guide. A text or a list alone, without ja: or en:, is the same in every language. guide: off turns every
14
+ // genre's guide off; a stronger layer may still add lines back.
15
+
16
+ export type GuideEdit = { readonly kind: "off" } | { readonly kind: "edit"; readonly replace?: GuideLines; readonly add?: GuideLines };
17
+
18
+ /**
19
+ * One place's changes, by genre or group, and the place as a reader is told it (chaff.yaml, style: house, plugins: foo).
20
+ * allOff: guide: off, which empties every genre's guide before this layer's own entries.
21
+ */
22
+ export type GuideLayer = { readonly from: string; readonly allOff: boolean; readonly edits: Readonly<Record<string, GuideEdit>> };
23
+
24
+ /** What could not be read: the genre or group it was written under (none when guide: itself is wrong), and what was there. */
25
+ export type GuideProblem =
26
+ | { readonly kind: "not-a-map"; readonly written: string }
27
+ | { readonly kind: "unknown-genre"; readonly genre: string }
28
+ | { readonly kind: "bad-entry"; readonly genre: string; readonly written: string }
29
+ | { readonly kind: "bad-lines"; readonly genre: string; readonly field: "replace" | "add" };
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
32
+
33
+ const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
34
+
35
+ /** Whether guide: as written turns every guide off (guide: off, or false). */
36
+ export const turnsGuidesOff = (raw: unknown): boolean => raw === "off" || raw === false;
37
+
38
+ const EDIT_FIELDS: ReadonlySet<string> = new Set(["replace", "add"]);
39
+
40
+ type Read = { readonly edit: GuideEdit } | { readonly problem: GuideProblem };
41
+
42
+ type LinesRead = { readonly lines: GuideLines | undefined } | { readonly problem: GuideProblem };
43
+
44
+ const linesIn = (raw: Record<string, unknown>, field: "replace" | "add", genre: string): LinesRead => {
45
+ if (raw[field] === undefined) return { lines: undefined };
46
+ const lines = guideLinesOf(raw[field]);
47
+ return lines === undefined ? { problem: { kind: "bad-lines", genre, field } } : { lines };
48
+ };
49
+
50
+ const editOf = (genre: string, value: unknown): Read => {
51
+ if (turnsGuidesOff(value)) return { edit: { kind: "off" } };
52
+ const fields = isRecord(value) ? Object.keys(value) : [];
53
+ if (!isRecord(value) || fields.length === 0 || fields.some((field) => !EDIT_FIELDS.has(field))) {
54
+ return { problem: { kind: "bad-entry", genre, written: printed(value) } };
55
+ }
56
+ const replace = linesIn(value, "replace", genre);
57
+ if ("problem" in replace) return replace;
58
+ const add = linesIn(value, "add", genre);
59
+ if ("problem" in add) return add;
60
+ return { edit: { kind: "edit", ...(replace.lines === undefined ? {} : { replace: replace.lines }), ...(add.lines === undefined ? {} : { add: add.lines }) } };
61
+ };
62
+
63
+ /**
64
+ * guide: as written, read into a layer. known: the genres and groups an entry may name. An entry that cannot be read is
65
+ * left out and reported; the rest still apply. Nothing written is an empty layer.
66
+ */
67
+ export const guideLayerOf = (raw: unknown, from: string, known: readonly string[]): { layer: GuideLayer; problems: GuideProblem[] } => {
68
+ if (raw === undefined || raw === null || turnsGuidesOff(raw)) return { layer: { from, allOff: turnsGuidesOff(raw), edits: {} }, problems: [] };
69
+ if (!isRecord(raw)) return { layer: { from, allOff: false, edits: {} }, problems: [{ kind: "not-a-map", written: printed(raw) }] };
70
+ const read = Object.entries(raw).map(([genre, value]): readonly [string, Read] =>
71
+ known.includes(genre) ? [genre, editOf(genre, value)] : [genre, { problem: { kind: "unknown-genre", genre } }],
72
+ );
73
+ return {
74
+ layer: { from, allOff: false, edits: Object.fromEntries(read.flatMap(([genre, entry]) => ("edit" in entry ? [[genre, entry.edit]] : []))) },
75
+ problems: read.flatMap(([, entry]) => ("problem" in entry ? [entry.problem] : [])),
76
+ };
77
+ };
@@ -0,0 +1,41 @@
1
+ // What a good document of a genre looks like, as lines an AI or a person checks a draft against: genres.yaml's guide:,
2
+ // and the replace / add a team writes over it. Pure: the YAML comes in already parsed.
3
+
4
+ /** The guide's lines by language: { ja: [...], en: [...] }. */
5
+ export type GuideLines = Readonly<Record<string, readonly string[]>>;
6
+
7
+ /** The languages a guide is written in: chaff's. A text written without ja: or en: is given to each. */
8
+ export const GUIDE_LANGUAGES: readonly string[] = ["ja", "en"];
9
+
10
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
11
+
12
+ const isLine = (value: unknown): value is string => typeof value === "string" && value.trim() !== "";
13
+
14
+ /** One line, or a list of them. undefined when anything in it is not a line of text. */
15
+ export const linesOf = (value: unknown): string[] | undefined => {
16
+ if (isLine(value)) return [value.trim()];
17
+ if (!Array.isArray(value) || value.length === 0) return undefined;
18
+ const entries: unknown[] = value;
19
+ return entries.every(isLine) ? entries.map((entry) => entry.trim()) : undefined;
20
+ };
21
+
22
+ /**
23
+ * Lines by language as a team writes them: { ja: ..., en: ... }, each one line or a list; or one line or a list alone,
24
+ * which reads the same in every language. undefined when it cannot be read, a language chaff has no guide in included:
25
+ * lines in it would never be shown.
26
+ */
27
+ export const guideLinesOf = (value: unknown): GuideLines | undefined => {
28
+ const everywhere = linesOf(value);
29
+ if (everywhere !== undefined) return Object.fromEntries(GUIDE_LANGUAGES.map((language) => [language, everywhere]));
30
+ if (!isRecord(value) || Object.keys(value).length === 0 || Object.keys(value).some((key) => !GUIDE_LANGUAGES.includes(key))) return undefined;
31
+ const read = Object.entries(value).map(([language, lines]) => [language, linesOf(lines)] as const);
32
+ return read.every((entry): entry is readonly [string, string[]] => entry[1] !== undefined) ? Object.fromEntries(read) : undefined;
33
+ };
34
+
35
+ /** A bundled guide: a list of lines in ja and in en. Throws, since the file ships with chaff and a wrong entry is chaff's bug. */
36
+ export const bundledGuideOf = (value: unknown, where: string): GuideLines | undefined => {
37
+ if (value === undefined) return undefined;
38
+ const lines = isRecord(value) ? guideLinesOf(value) : undefined;
39
+ if (lines?.["ja"] === undefined || lines["en"] === undefined) throw new Error(`${where}: guide needs a list of lines in ja and in en`);
40
+ return lines;
41
+ };
@@ -0,0 +1,26 @@
1
+ import { CONFIG_FILE, type Config } from "../config/load.ts";
2
+ import { loadGenres } from "../genre-load.ts";
3
+ import { knownGenres } from "../known-genres.ts";
4
+ import type { UiLanguage } from "../ui.ts";
5
+ import { guideLayerOf, type GuideLayer } from "./layer.ts";
6
+ import { guideProblemText } from "./problem-text.ts";
7
+ import { resolveGuide, type ResolvedGuide } from "./resolve.ts";
8
+
9
+ type GuideSettings = Pick<Config, "guide" | "applied" | "extensions">;
10
+
11
+ const configLayer = (config: Pick<Config, "guide">): ReturnType<typeof guideLayerOf> => guideLayerOf(config.guide, CONFIG_FILE, knownGenres());
12
+
13
+ /** Every place that changes the bundled guides, weakest first: the rule packs, then the chosen style, then chaff.yaml. */
14
+ export const guideLayersOf = (config: GuideSettings): GuideLayer[] => [
15
+ ...(config.extensions?.guides ?? []),
16
+ ...(config.applied?.guide === undefined ? [] : [config.applied.guide]),
17
+ configLayer(config).layer,
18
+ ];
19
+
20
+ /** The guide a document of this genre is checked against, in its language; undefined when it is turned off or the genre has none. */
21
+ export const genreGuideFor = (config: GuideSettings, genre: string, language: UiLanguage): ResolvedGuide | undefined =>
22
+ resolveGuide(loadGenres(), genre, language, guideLayersOf(config));
23
+
24
+ /** What chaff.yaml's guide: says that cannot be read. The rest of it still applies, so these are warnings, not a stop. */
25
+ export const configGuideProblems = (config: Pick<Config, "guide" | "path">, ui: UiLanguage): string[] =>
26
+ configLayer(config).problems.map((problem) => `${config.path ?? CONFIG_FILE}: ${guideProblemText(problem, ui)}`);
@@ -0,0 +1,34 @@
1
+ import type { Texts, UiLanguage } from "../ui.ts";
2
+ import type { GuideProblem } from "./layer.ts";
3
+
4
+ type ProblemText = Readonly<{ [K in GuideProblem["kind"]]: (problem: Extract<GuideProblem, { kind: K }>) => string }>;
5
+
6
+ const TEXT: Texts<ProblemText> = {
7
+ ja: {
8
+ "not-a-map": (problem) => `guide の ${problem.written} は読めません。ジャンルか群の名前の下に replace・add・off を書くか、guide: off と書いてください`,
9
+ "unknown-genre": (problem) => `guide の ${problem.genre} というジャンルや群はありません(npx chaffjs genres で一覧が出ます)`,
10
+ "bad-entry": (problem) => `guide の ${problem.genre}: ${problem.written} は読めません。replace か add に行を書くか、off と書いてください`,
11
+ "bad-lines": (problem) => `guide の ${problem.genre}: ${problem.field} は 1 行か行の並びで、言語ごとなら ja: と en: の下に書いてください`,
12
+ },
13
+ en: {
14
+ "not-a-map": (problem) => `guide ${problem.written} cannot be read; write replace, add or off under a genre or group, or guide: off`,
15
+ "unknown-genre": (problem) => `guide names ${problem.genre}, which is no genre or group (npx chaffjs genres lists them)`,
16
+ "bad-entry": (problem) => `guide ${problem.genre}: ${problem.written} cannot be read; write lines under replace or add, or off`,
17
+ "bad-lines": (problem) => `guide ${problem.genre}: write ${problem.field} as a line or a list of lines, under ja: and en: for each language`,
18
+ },
19
+ };
20
+
21
+ /** One guide problem as a sentence, without the place it was written (the caller names chaff.yaml, a style or a plugin). */
22
+ export const guideProblemText = (problem: GuideProblem, ui: UiLanguage): string => {
23
+ const text = TEXT[ui];
24
+ switch (problem.kind) {
25
+ case "not-a-map":
26
+ return text["not-a-map"](problem);
27
+ case "unknown-genre":
28
+ return text["unknown-genre"](problem);
29
+ case "bad-entry":
30
+ return text["bad-entry"](problem);
31
+ case "bad-lines":
32
+ return text["bad-lines"](problem);
33
+ }
34
+ };