chaffjs 0.20.0 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (567) hide show
  1. package/README.md +5 -4
  2. package/dist/api.d.ts +16 -1
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-text.d.ts +2 -0
  6. package/dist/cli-text.d.ts.map +1 -1
  7. package/dist/cli-text.js +14 -5
  8. package/dist/cli-text.js.map +1 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/cli.js +4 -0
  11. package/dist/cli.js.map +1 -1
  12. package/dist/command-help.d.ts +7 -0
  13. package/dist/command-help.d.ts.map +1 -0
  14. package/dist/command-help.js +18 -0
  15. package/dist/command-help.js.map +1 -0
  16. package/dist/commands/eval.js +2 -2
  17. package/dist/commands/eval.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +11 -1
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/help.d.ts +4 -0
  22. package/dist/commands/help.d.ts.map +1 -0
  23. package/dist/commands/help.js +7 -0
  24. package/dist/commands/help.js.map +1 -0
  25. package/dist/config/load.d.ts +2 -0
  26. package/dist/config/load.d.ts.map +1 -1
  27. package/dist/config/load.js +1 -0
  28. package/dist/config/load.js.map +1 -1
  29. package/dist/config/option-problems.js +2 -2
  30. package/dist/config/option-problems.js.map +1 -1
  31. package/dist/config/rule-problems.js +2 -2
  32. package/dist/config/rule-problems.js.map +1 -1
  33. package/dist/custom/checked.d.ts +9 -0
  34. package/dist/custom/checked.d.ts.map +1 -0
  35. package/dist/custom/checked.js +3 -0
  36. package/dist/custom/checked.js.map +1 -0
  37. package/dist/custom/dsl-fields.d.ts +27 -0
  38. package/dist/custom/dsl-fields.d.ts.map +1 -0
  39. package/dist/custom/dsl-fields.js +86 -0
  40. package/dist/custom/dsl-fields.js.map +1 -0
  41. package/dist/custom/load.d.ts.map +1 -1
  42. package/dist/custom/load.js +2 -0
  43. package/dist/custom/load.js.map +1 -1
  44. package/dist/custom/parse.d.ts +14 -3
  45. package/dist/custom/parse.d.ts.map +1 -1
  46. package/dist/custom/parse.js +44 -40
  47. package/dist/custom/parse.js.map +1 -1
  48. package/dist/custom/problems.d.ts.map +1 -1
  49. package/dist/custom/problems.js +11 -1
  50. package/dist/custom/problems.js.map +1 -1
  51. package/dist/detectors/blank-structure.d.ts.map +1 -1
  52. package/dist/detectors/blank-structure.js +6 -4
  53. package/dist/detectors/blank-structure.js.map +1 -1
  54. package/dist/detectors/clause-shape.d.ts +22 -0
  55. package/dist/detectors/clause-shape.d.ts.map +1 -0
  56. package/dist/detectors/clause-shape.js +149 -0
  57. package/dist/detectors/clause-shape.js.map +1 -0
  58. package/dist/detectors/code-fences.d.ts +34 -0
  59. package/dist/detectors/code-fences.d.ts.map +1 -0
  60. package/dist/detectors/code-fences.js +100 -0
  61. package/dist/detectors/code-fences.js.map +1 -0
  62. package/dist/detectors/cross-definitions.d.ts +4 -0
  63. package/dist/detectors/cross-definitions.d.ts.map +1 -0
  64. package/dist/detectors/cross-definitions.js +43 -0
  65. package/dist/detectors/cross-definitions.js.map +1 -0
  66. package/dist/detectors/cross-facts.d.ts +4 -0
  67. package/dist/detectors/cross-facts.d.ts.map +1 -0
  68. package/dist/detectors/cross-facts.js +53 -0
  69. package/dist/detectors/cross-facts.js.map +1 -0
  70. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts +3 -0
  71. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.d.ts.map +1 -0
  72. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js +3 -0
  73. package/dist/detectors/cross-registry/cross-doc-duplicate-definition.js.map +1 -0
  74. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts +3 -0
  75. package/dist/detectors/cross-registry/cross-doc-fact-conflict.d.ts.map +1 -0
  76. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js +3 -0
  77. package/dist/detectors/cross-registry/cross-doc-fact-conflict.js.map +1 -0
  78. package/dist/detectors/custom.d.ts.map +1 -1
  79. package/dist/detectors/custom.js +4 -1
  80. package/dist/detectors/custom.js.map +1 -1
  81. package/dist/detectors/defined-term-form.d.ts +25 -0
  82. package/dist/detectors/defined-term-form.d.ts.map +1 -0
  83. package/dist/detectors/defined-term-form.js +170 -0
  84. package/dist/detectors/defined-term-form.js.map +1 -0
  85. package/dist/detectors/email-letter.d.ts +46 -0
  86. package/dist/detectors/email-letter.d.ts.map +1 -0
  87. package/dist/detectors/email-letter.js +234 -0
  88. package/dist/detectors/email-letter.js.map +1 -0
  89. package/dist/detectors/impossible-date.d.ts +20 -0
  90. package/dist/detectors/impossible-date.d.ts.map +1 -0
  91. package/dist/detectors/impossible-date.js +105 -0
  92. package/dist/detectors/impossible-date.js.map +1 -0
  93. package/dist/detectors/impossible-time.d.ts +18 -0
  94. package/dist/detectors/impossible-time.d.ts.map +1 -0
  95. package/dist/detectors/impossible-time.js +116 -0
  96. package/dist/detectors/impossible-time.js.map +1 -0
  97. package/dist/detectors/link-shape.d.ts +11 -0
  98. package/dist/detectors/link-shape.d.ts.map +1 -0
  99. package/dist/detectors/link-shape.js +56 -0
  100. package/dist/detectors/link-shape.js.map +1 -0
  101. package/dist/detectors/mark-shape.d.ts +53 -0
  102. package/dist/detectors/mark-shape.d.ts.map +1 -0
  103. package/dist/detectors/mark-shape.js +160 -0
  104. package/dist/detectors/mark-shape.js.map +1 -0
  105. package/dist/detectors/registry/adverb-polarity.d.ts +3 -0
  106. package/dist/detectors/registry/adverb-polarity.d.ts.map +1 -0
  107. package/dist/detectors/registry/adverb-polarity.js +3 -0
  108. package/dist/detectors/registry/adverb-polarity.js.map +1 -0
  109. package/dist/detectors/registry/attachment-missing.d.ts +3 -0
  110. package/dist/detectors/registry/attachment-missing.d.ts.map +1 -0
  111. package/dist/detectors/registry/attachment-missing.js +3 -0
  112. package/dist/detectors/registry/attachment-missing.js.map +1 -0
  113. package/dist/detectors/registry/bare-url-mix.d.ts +3 -0
  114. package/dist/detectors/registry/bare-url-mix.d.ts.map +1 -0
  115. package/dist/detectors/registry/bare-url-mix.js +3 -0
  116. package/dist/detectors/registry/bare-url-mix.js.map +1 -0
  117. package/dist/detectors/registry/caps-shouting.d.ts +3 -0
  118. package/dist/detectors/registry/caps-shouting.d.ts.map +1 -0
  119. package/dist/detectors/registry/caps-shouting.js +3 -0
  120. package/dist/detectors/registry/caps-shouting.js.map +1 -0
  121. package/dist/detectors/registry/code-fence-language.d.ts +3 -0
  122. package/dist/detectors/registry/code-fence-language.d.ts.map +1 -0
  123. package/dist/detectors/registry/code-fence-language.js +3 -0
  124. package/dist/detectors/registry/code-fence-language.js.map +1 -0
  125. package/dist/detectors/registry/defined-name-repeated.d.ts +3 -0
  126. package/dist/detectors/registry/defined-name-repeated.d.ts.map +1 -0
  127. package/dist/detectors/registry/defined-name-repeated.js +3 -0
  128. package/dist/detectors/registry/defined-name-repeated.js.map +1 -0
  129. package/dist/detectors/registry/defined-term-form.d.ts +3 -0
  130. package/dist/detectors/registry/defined-term-form.d.ts.map +1 -0
  131. package/dist/detectors/registry/defined-term-form.js +3 -0
  132. package/dist/detectors/registry/defined-term-form.js.map +1 -0
  133. package/dist/detectors/registry/email-frame.d.ts +3 -0
  134. package/dist/detectors/registry/email-frame.d.ts.map +1 -0
  135. package/dist/detectors/registry/email-frame.js +3 -0
  136. package/dist/detectors/registry/email-frame.js.map +1 -0
  137. package/dist/detectors/registry/email-subject.d.ts +3 -0
  138. package/dist/detectors/registry/email-subject.d.ts.map +1 -0
  139. package/dist/detectors/registry/email-subject.js +3 -0
  140. package/dist/detectors/registry/email-subject.js.map +1 -0
  141. package/dist/detectors/registry/ending-run.d.ts +3 -0
  142. package/dist/detectors/registry/ending-run.d.ts.map +1 -0
  143. package/dist/detectors/registry/ending-run.js +3 -0
  144. package/dist/detectors/registry/ending-run.js.map +1 -0
  145. package/dist/detectors/registry/heading-command.d.ts +3 -0
  146. package/dist/detectors/registry/heading-command.d.ts.map +1 -0
  147. package/dist/detectors/registry/heading-command.js +3 -0
  148. package/dist/detectors/registry/heading-command.js.map +1 -0
  149. package/dist/detectors/registry/hyphen-dash.d.ts +3 -0
  150. package/dist/detectors/registry/hyphen-dash.d.ts.map +1 -0
  151. package/dist/detectors/registry/hyphen-dash.js +3 -0
  152. package/dist/detectors/registry/hyphen-dash.js.map +1 -0
  153. package/dist/detectors/registry/image-file-name-alt.d.ts +3 -0
  154. package/dist/detectors/registry/image-file-name-alt.d.ts.map +1 -0
  155. package/dist/detectors/registry/image-file-name-alt.js +3 -0
  156. package/dist/detectors/registry/image-file-name-alt.js.map +1 -0
  157. package/dist/detectors/registry/impossible-date.d.ts +3 -0
  158. package/dist/detectors/registry/impossible-date.d.ts.map +1 -0
  159. package/dist/detectors/registry/impossible-date.js +3 -0
  160. package/dist/detectors/registry/impossible-date.js.map +1 -0
  161. package/dist/detectors/registry/impossible-time.d.ts +3 -0
  162. package/dist/detectors/registry/impossible-time.d.ts.map +1 -0
  163. package/dist/detectors/registry/impossible-time.js +3 -0
  164. package/dist/detectors/registry/impossible-time.js.map +1 -0
  165. package/dist/detectors/registry/latin-abbreviation.d.ts +3 -0
  166. package/dist/detectors/registry/latin-abbreviation.d.ts.map +1 -0
  167. package/dist/detectors/registry/latin-abbreviation.js +3 -0
  168. package/dist/detectors/registry/latin-abbreviation.js.map +1 -0
  169. package/dist/detectors/registry/link-text-url-mismatch.d.ts +3 -0
  170. package/dist/detectors/registry/link-text-url-mismatch.d.ts.map +1 -0
  171. package/dist/detectors/registry/link-text-url-mismatch.js +3 -0
  172. package/dist/detectors/registry/link-text-url-mismatch.js.map +1 -0
  173. package/dist/detectors/registry/mark-consistency.d.ts +3 -0
  174. package/dist/detectors/registry/mark-consistency.d.ts.map +1 -0
  175. package/dist/detectors/registry/mark-consistency.js +3 -0
  176. package/dist/detectors/registry/mark-consistency.js.map +1 -0
  177. package/dist/detectors/registry/origin-particle.d.ts +3 -0
  178. package/dist/detectors/registry/origin-particle.d.ts.map +1 -0
  179. package/dist/detectors/registry/origin-particle.js +3 -0
  180. package/dist/detectors/registry/origin-particle.js.map +1 -0
  181. package/dist/detectors/registry/request-without-deadline.d.ts +3 -0
  182. package/dist/detectors/registry/request-without-deadline.d.ts.map +1 -0
  183. package/dist/detectors/registry/request-without-deadline.js +3 -0
  184. package/dist/detectors/registry/request-without-deadline.js.map +1 -0
  185. package/dist/detectors/registry/sentence-spacing.d.ts +3 -0
  186. package/dist/detectors/registry/sentence-spacing.d.ts.map +1 -0
  187. package/dist/detectors/registry/sentence-spacing.js +3 -0
  188. package/dist/detectors/registry/sentence-spacing.js.map +1 -0
  189. package/dist/detectors/registry/shell-prompt.d.ts +3 -0
  190. package/dist/detectors/registry/shell-prompt.d.ts.map +1 -0
  191. package/dist/detectors/registry/shell-prompt.js +3 -0
  192. package/dist/detectors/registry/shell-prompt.js.map +1 -0
  193. package/dist/detectors/registry/step-statement.d.ts +3 -0
  194. package/dist/detectors/registry/step-statement.d.ts.map +1 -0
  195. package/dist/detectors/registry/step-statement.js +3 -0
  196. package/dist/detectors/registry/step-statement.js.map +1 -0
  197. package/dist/detectors/registry/unpaired-parallel.d.ts +3 -0
  198. package/dist/detectors/registry/unpaired-parallel.d.ts.map +1 -0
  199. package/dist/detectors/registry/unpaired-parallel.js +3 -0
  200. package/dist/detectors/registry/unpaired-parallel.js.map +1 -0
  201. package/dist/detectors/request-owner.d.ts +23 -0
  202. package/dist/detectors/request-owner.d.ts.map +1 -0
  203. package/dist/detectors/request-owner.js +110 -0
  204. package/dist/detectors/request-owner.js.map +1 -0
  205. package/dist/detectors/technical-docs.d.ts +32 -0
  206. package/dist/detectors/technical-docs.d.ts.map +1 -0
  207. package/dist/detectors/technical-docs.js +199 -0
  208. package/dist/detectors/technical-docs.js.map +1 -0
  209. package/dist/extension/pack-read.d.ts +21 -0
  210. package/dist/extension/pack-read.d.ts.map +1 -0
  211. package/dist/extension/pack-read.js +117 -0
  212. package/dist/extension/pack-read.js.map +1 -0
  213. package/dist/extension/plugin-load.d.ts.map +1 -1
  214. package/dist/extension/plugin-load.js +44 -10
  215. package/dist/extension/plugin-load.js.map +1 -1
  216. package/dist/extension/plugin-parse.d.ts.map +1 -1
  217. package/dist/extension/plugin-parse.js +10 -3
  218. package/dist/extension/plugin-parse.js.map +1 -1
  219. package/dist/facts/address-values.d.ts +5 -0
  220. package/dist/facts/address-values.d.ts.map +1 -0
  221. package/dist/facts/address-values.js +64 -0
  222. package/dist/facts/address-values.js.map +1 -0
  223. package/dist/facts/cross-facts.d.ts +20 -0
  224. package/dist/facts/cross-facts.d.ts.map +1 -0
  225. package/dist/facts/cross-facts.js +41 -0
  226. package/dist/facts/cross-facts.js.map +1 -0
  227. package/dist/fix-plan/chosen-depth.d.ts +10 -0
  228. package/dist/fix-plan/chosen-depth.d.ts.map +1 -0
  229. package/dist/fix-plan/chosen-depth.js +24 -0
  230. package/dist/fix-plan/chosen-depth.js.map +1 -0
  231. package/dist/fix-plan/mode.d.ts +13 -4
  232. package/dist/fix-plan/mode.d.ts.map +1 -1
  233. package/dist/fix-plan/mode.js +38 -7
  234. package/dist/fix-plan/mode.js.map +1 -1
  235. package/dist/fix-plan/plan.d.ts +20 -0
  236. package/dist/fix-plan/plan.d.ts.map +1 -1
  237. package/dist/fix-plan/plan.js +23 -5
  238. package/dist/fix-plan/plan.js.map +1 -1
  239. package/dist/fix-plan/render.d.ts.map +1 -1
  240. package/dist/fix-plan/render.js +24 -0
  241. package/dist/fix-plan/render.js.map +1 -1
  242. package/dist/fix-plan/text.d.ts +8 -0
  243. package/dist/fix-plan/text.d.ts.map +1 -1
  244. package/dist/fix-plan/text.js +36 -10
  245. package/dist/fix-plan/text.js.map +1 -1
  246. package/dist/genre-load.d.ts +1 -1
  247. package/dist/genre-load.d.ts.map +1 -1
  248. package/dist/genre-load.js +7 -4
  249. package/dist/genre-load.js.map +1 -1
  250. package/dist/genre-parse.d.ts +6 -0
  251. package/dist/genre-parse.d.ts.map +1 -1
  252. package/dist/genre-parse.js +18 -0
  253. package/dist/genre-parse.js.map +1 -1
  254. package/dist/init.js +10 -10
  255. package/dist/init.js.map +1 -1
  256. package/dist/known-genres.d.ts +8 -0
  257. package/dist/known-genres.d.ts.map +1 -0
  258. package/dist/known-genres.js +17 -0
  259. package/dist/known-genres.js.map +1 -0
  260. package/dist/link-shape.d.ts +20 -0
  261. package/dist/link-shape.d.ts.map +1 -0
  262. package/dist/link-shape.js +58 -0
  263. package/dist/link-shape.js.map +1 -0
  264. package/dist/reasons.d.ts.map +1 -1
  265. package/dist/reasons.js +15 -4
  266. package/dist/reasons.js.map +1 -1
  267. package/dist/render/explain.d.ts.map +1 -1
  268. package/dist/render/explain.js +11 -2
  269. package/dist/render/explain.js.map +1 -1
  270. package/dist/render/friendly.js +1 -1
  271. package/dist/render/friendly.js.map +1 -1
  272. package/dist/render/rules-json.d.ts.map +1 -1
  273. package/dist/render/rules-json.js +12 -3
  274. package/dist/render/rules-json.js.map +1 -1
  275. package/dist/render/rules-table.js +6 -6
  276. package/dist/render/rules-table.js.map +1 -1
  277. package/dist/render/semantic.js +2 -2
  278. package/dist/render/semantic.js.map +1 -1
  279. package/dist/render/suppressions.js +2 -2
  280. package/dist/render/suppressions.js.map +1 -1
  281. package/dist/rewrite-depth.d.ts +22 -0
  282. package/dist/rewrite-depth.d.ts.map +1 -0
  283. package/dist/rewrite-depth.js +42 -0
  284. package/dist/rewrite-depth.js.map +1 -0
  285. package/dist/rule-fields.d.ts +23 -0
  286. package/dist/rule-fields.d.ts.map +1 -0
  287. package/dist/rule-fields.js +64 -0
  288. package/dist/rule-fields.js.map +1 -0
  289. package/dist/rule-guide.d.ts +3 -0
  290. package/dist/rule-guide.d.ts.map +1 -1
  291. package/dist/rule-guide.js +6 -0
  292. package/dist/rule-guide.js.map +1 -1
  293. package/dist/rule-load.d.ts.map +1 -1
  294. package/dist/rule-load.js +13 -5
  295. package/dist/rule-load.js.map +1 -1
  296. package/dist/rule-offs.d.ts +14 -0
  297. package/dist/rule-offs.d.ts.map +1 -0
  298. package/dist/rule-offs.js +45 -0
  299. package/dist/rule-offs.js.map +1 -0
  300. package/dist/structure/build.d.ts.map +1 -1
  301. package/dist/structure/build.js +14 -1
  302. package/dist/structure/build.js.map +1 -1
  303. package/dist/structure/cross-definitions.d.ts +23 -0
  304. package/dist/structure/cross-definitions.d.ts.map +1 -0
  305. package/dist/structure/cross-definitions.js +34 -0
  306. package/dist/structure/cross-definitions.js.map +1 -0
  307. package/genres.yaml +14 -313
  308. package/package.json +3 -3
  309. package/rules/absolute-exception.yaml +3 -1
  310. package/rules/abstract-length.yaml +7 -1
  311. package/rules/acronym-expansion-conflict.yaml +2 -1
  312. package/rules/adverb-overuse.yaml +7 -1
  313. package/rules/adverb-without-negation.yaml +60 -0
  314. package/rules/adversative-ga-repeat.yaml +2 -1
  315. package/rules/agentless-passive.yaml +10 -1
  316. package/rules/agreement-slip.yaml +4 -1
  317. package/rules/ai-generated-composite.yaml +6 -1
  318. package/rules/ai-structure.yaml +8 -2
  319. package/rules/ai-tell.yaml +7 -1
  320. package/rules/all-caps-shouting.yaml +61 -0
  321. package/rules/announced-count-mismatch.yaml +3 -1
  322. package/rules/announcing-opener.yaml +7 -1
  323. package/rules/article-sound.yaml +6 -2
  324. package/rules/assistant-residue.yaml +4 -1
  325. package/rules/attachment-not-attached.yaml +112 -0
  326. package/rules/baito-keigo.yaml +64 -0
  327. package/rules/bare-url-mix.yaml +86 -0
  328. package/rules/bold-density.yaml +1 -0
  329. package/rules/bold-label-list.yaml +9 -1
  330. package/rules/bracket-nesting.yaml +2 -1
  331. package/rules/bracket-width-consistency.yaml +64 -0
  332. package/rules/broken-link.yaml +1 -1
  333. package/rules/chat-citation-residue.yaml +2 -1
  334. package/rules/citation-reference-mismatch.yaml +7 -1
  335. package/rules/citation-style-mix.yaml +7 -1
  336. package/rules/clause-chain.yaml +6 -1
  337. package/rules/closing-cliche.yaml +7 -0
  338. package/rules/code-fence-language-mix.yaml +176 -0
  339. package/rules/colloquial-opener.yaml +62 -0
  340. package/rules/colon-lead-in.yaml +7 -1
  341. package/rules/concrete-evidence-density.yaml +6 -0
  342. package/rules/contraction-consistency.yaml +4 -1
  343. package/rules/contrast-framing.yaml +7 -1
  344. package/rules/cross-doc-duplicate-definition.yaml +70 -0
  345. package/rules/cross-doc-fact-conflict.yaml +64 -0
  346. package/rules/cushion-phrase-density.yaml +8 -1
  347. package/rules/dangling-figure-reference.yaml +3 -1
  348. package/rules/dangling-reference.yaml +3 -1
  349. package/rules/date-format-consistency.yaml +3 -2
  350. package/rules/date-order.yaml +3 -1
  351. package/rules/date-range-reversed.yaml +3 -1
  352. package/rules/date-weekday-mismatch.yaml +3 -1
  353. package/rules/date-without-year.yaml +4 -3
  354. package/rules/defined-name-repeated.yaml +97 -0
  355. package/rules/defined-term-form.yaml +113 -0
  356. package/rules/demonstrative-opener-run.yaml +2 -1
  357. package/rules/double-keigo.yaml +6 -1
  358. package/rules/double-negative.yaml +5 -1
  359. package/rules/doubled-nado.yaml +2 -1
  360. package/rules/doubled-punctuation.yaml +1 -1
  361. package/rules/doubled-word.yaml +4 -1
  362. package/rules/duplicate-definition.yaml +3 -1
  363. package/rules/duplicate-heading.yaml +1 -1
  364. package/rules/duration-mismatch.yaml +2 -1
  365. package/rules/elapsed-years-mismatch.yaml +4 -1
  366. package/rules/email-greeting-closing.yaml +122 -0
  367. package/rules/email-subject-length.yaml +112 -0
  368. package/rules/emoji-density.yaml +3 -1
  369. package/rules/emoji-heading.yaml +7 -1
  370. package/rules/empty-intensifier.yaml +4 -0
  371. package/rules/empty-list-item.yaml +2 -1
  372. package/rules/empty-section.yaml +1 -1
  373. package/rules/empty-table-cell.yaml +2 -1
  374. package/rules/excessive-hedging.yaml +7 -1
  375. package/rules/exclamation-width-consistency.yaml +64 -0
  376. package/rules/expletive-construction.yaml +7 -1
  377. package/rules/fact-conflict.yaml +4 -1
  378. package/rules/figure-reference-order.yaml +7 -1
  379. package/rules/fullwidth-alnum-consistency.yaml +2 -2
  380. package/rules/hankaku-kana.yaml +1 -1
  381. package/rules/heading-command-missing.yaml +109 -0
  382. package/rules/heading-echo.yaml +6 -1
  383. package/rules/heading-level-skip.yaml +3 -3
  384. package/rules/heading-numbering-mix.yaml +4 -3
  385. package/rules/hiragana-fukushi.yaml +4 -1
  386. package/rules/homophone-slip.yaml +5 -1
  387. package/rules/humble-for-others.yaml +63 -0
  388. package/rules/hyphen-as-dash.yaml +59 -0
  389. package/rules/idiom-misuse.yaml +4 -1
  390. package/rules/image-file-name-alt.yaml +83 -0
  391. package/rules/impossible-date.yaml +79 -0
  392. package/rules/impossible-time.yaml +76 -0
  393. package/rules/internal-jargon.yaml +1 -1
  394. package/rules/invisible-character.yaml +1 -1
  395. package/rules/katakana-density.yaml +2 -1
  396. package/rules/katakana-long-vowel.yaml +1 -1
  397. package/rules/known-misspelling.yaml +2 -1
  398. package/rules/kutoten-consistency.yaml +1 -1
  399. package/rules/latin-abbreviation-form.yaml +58 -0
  400. package/rules/latin-spacing.yaml +6 -2
  401. package/rules/link-text-url-mismatch.yaml +72 -0
  402. package/rules/list-item-form-mix.yaml +7 -1
  403. package/rules/max-kanji-continuous.yaml +10 -2
  404. package/rules/max-paragraph-length.yaml +4 -1
  405. package/rules/max-sentence-length.yaml +29 -21
  406. package/rules/max-ten.yaml +4 -1
  407. package/rules/misconversion.yaml +2 -1
  408. package/rules/modal-conflict.yaml +2 -1
  409. package/rules/name-variant.yaml +4 -3
  410. package/rules/ngram-repetition.yaml +5 -0
  411. package/rules/no-doubled-joshi.yaml +5 -1
  412. package/rules/no-em-dash.yaml +4 -1
  413. package/rules/no-lead.yaml +6 -1
  414. package/rules/no-mixed-desumasu.yaml +5 -2
  415. package/rules/no-nakaguro-parallel.yaml +3 -0
  416. package/rules/nominalization.yaml +5 -1
  417. package/rules/number-style-consistency.yaml +6 -1
  418. package/rules/number-without-unit.yaml +2 -1
  419. package/rules/numbering-gap.yaml +1 -1
  420. package/rules/one-sentence-paragraph-run.yaml +5 -1
  421. package/rules/orthographic-variant.yaml +4 -1
  422. package/rules/outside-announced-list.yaml +2 -1
  423. package/rules/oxford-comma-consistency.yaml +3 -1
  424. package/rules/padded-intro.yaml +7 -0
  425. package/rules/paragraph-length-variance.yaml +7 -1
  426. package/rules/paragraph-restatement.yaml +6 -1
  427. package/rules/percent-sum-mismatch.yaml +3 -1
  428. package/rules/person-consistency.yaml +4 -1
  429. package/rules/preamble-length.yaml +6 -0
  430. package/rules/preferred-term.yaml +1 -1
  431. package/rules/proper-noun-density.yaml +8 -1
  432. package/rules/quote-style-consistency.yaml +66 -0
  433. package/rules/ra-nuki.yaml +4 -1
  434. package/rules/redundant-expression.yaml +4 -1
  435. package/rules/reference-title-mismatch.yaml +4 -3
  436. package/rules/reference-topic-missing.yaml +2 -1
  437. package/rules/relative-date-mismatch.yaml +2 -1
  438. package/rules/repeated-conjunction.yaml +4 -1
  439. package/rules/repeated-sentence-ending.yaml +62 -0
  440. package/rules/repeated-sentence-head.yaml +4 -0
  441. package/rules/request-without-deadline.yaml +92 -0
  442. package/rules/required-sections.yaml +1 -1
  443. package/rules/requirement-modal.yaml +1 -1
  444. package/rules/requirement-smell.yaml +12 -4
  445. package/rules/rule-of-three.yaml +8 -1
  446. package/rules/sasete-itadaku.yaml +7 -1
  447. package/rules/section-length-uniformity.yaml +7 -1
  448. package/rules/sentence-initial-conjunction-run.yaml +5 -1
  449. package/rules/sentence-initial-so.yaml +6 -1
  450. package/rules/sentence-rhythm.yaml +8 -1
  451. package/rules/sentence-spacing-consistency.yaml +63 -0
  452. package/rules/shell-prompt-mix.yaml +128 -0
  453. package/rules/space-before-punctuation.yaml +1 -1
  454. package/rules/spelling-consistency.yaml +3 -1
  455. package/rules/step-statement-mix.yaml +76 -0
  456. package/rules/stock-transition.yaml +8 -1
  457. package/rules/stray-space.yaml +2 -2
  458. package/rules/summary-fact-mismatch.yaml +4 -1
  459. package/rules/table-header-variant.yaml +2 -1
  460. package/rules/taigen-dome-in-prose.yaml +6 -0
  461. package/rules/tari-unpaired.yaml +58 -0
  462. package/rules/teki-overuse.yaml +2 -1
  463. package/rules/title-case-consistency.yaml +1 -1
  464. package/rules/title-length.yaml +7 -1
  465. package/rules/topic-predicate-distance.yaml +6 -1
  466. package/rules/total-mismatch.yaml +3 -1
  467. package/rules/translationese-density.yaml +4 -1
  468. package/rules/unbalanced-bracket.yaml +3 -3
  469. package/rules/undefined-acronym.yaml +8 -1
  470. package/rules/unfilled-placeholder.yaml +1 -1
  471. package/rules/unit-mismatch.yaml +2 -1
  472. package/rules/unknown-word.yaml +4 -1
  473. package/rules/unqualified-superlative.yaml +6 -1
  474. package/rules/unused-definition.yaml +4 -3
  475. package/rules/url-run-on.yaml +1 -1
  476. package/rules/use-before-definition.yaml +4 -3
  477. package/rules/vague-figure-reference.yaml +3 -1
  478. package/rules/vague-link-text.yaml +2 -1
  479. package/rules/vague-word-density.yaml +2 -1
  480. package/rules/very-adjective.yaml +5 -1
  481. package/rules/weasel-word.yaml +6 -1
  482. package/rules/wordy-phrase.yaml +6 -1
  483. package/rules/yori-as-from.yaml +61 -0
  484. package/src/api.ts +13 -1
  485. package/src/cli-text.ts +18 -7
  486. package/src/cli.ts +3 -0
  487. package/src/command-help.ts +26 -0
  488. package/src/commands/eval.ts +2 -2
  489. package/src/commands/fix-plan.ts +13 -1
  490. package/src/commands/help.ts +8 -0
  491. package/src/config/load.ts +3 -0
  492. package/src/config/option-problems.ts +2 -2
  493. package/src/config/rule-problems.ts +2 -2
  494. package/src/custom/checked.ts +8 -0
  495. package/src/custom/dsl-fields.ts +102 -0
  496. package/src/custom/load.ts +2 -0
  497. package/src/custom/parse.ts +72 -50
  498. package/src/custom/problems.ts +18 -2
  499. package/src/detectors/blank-structure.ts +12 -4
  500. package/src/detectors/clause-shape.ts +199 -0
  501. package/src/detectors/code-fences.ts +113 -0
  502. package/src/detectors/cross-definitions.ts +49 -0
  503. package/src/detectors/cross-facts.ts +60 -0
  504. package/src/detectors/cross-registry/cross-doc-duplicate-definition.ts +4 -0
  505. package/src/detectors/cross-registry/cross-doc-fact-conflict.ts +4 -0
  506. package/src/detectors/custom.ts +10 -2
  507. package/src/detectors/defined-term-form.ts +200 -0
  508. package/src/detectors/email-letter.ts +262 -0
  509. package/src/detectors/impossible-date.ts +141 -0
  510. package/src/detectors/impossible-time.ts +151 -0
  511. package/src/detectors/link-shape.ts +70 -0
  512. package/src/detectors/mark-shape.ts +202 -0
  513. package/src/detectors/registry/adverb-polarity.ts +4 -0
  514. package/src/detectors/registry/attachment-missing.ts +4 -0
  515. package/src/detectors/registry/bare-url-mix.ts +4 -0
  516. package/src/detectors/registry/caps-shouting.ts +4 -0
  517. package/src/detectors/registry/code-fence-language.ts +4 -0
  518. package/src/detectors/registry/defined-name-repeated.ts +4 -0
  519. package/src/detectors/registry/defined-term-form.ts +4 -0
  520. package/src/detectors/registry/email-frame.ts +4 -0
  521. package/src/detectors/registry/email-subject.ts +4 -0
  522. package/src/detectors/registry/ending-run.ts +4 -0
  523. package/src/detectors/registry/heading-command.ts +4 -0
  524. package/src/detectors/registry/hyphen-dash.ts +4 -0
  525. package/src/detectors/registry/image-file-name-alt.ts +4 -0
  526. package/src/detectors/registry/impossible-date.ts +4 -0
  527. package/src/detectors/registry/impossible-time.ts +4 -0
  528. package/src/detectors/registry/latin-abbreviation.ts +4 -0
  529. package/src/detectors/registry/link-text-url-mismatch.ts +4 -0
  530. package/src/detectors/registry/mark-consistency.ts +4 -0
  531. package/src/detectors/registry/origin-particle.ts +4 -0
  532. package/src/detectors/registry/request-without-deadline.ts +4 -0
  533. package/src/detectors/registry/sentence-spacing.ts +4 -0
  534. package/src/detectors/registry/shell-prompt.ts +4 -0
  535. package/src/detectors/registry/step-statement.ts +4 -0
  536. package/src/detectors/registry/unpaired-parallel.ts +4 -0
  537. package/src/detectors/request-owner.ts +141 -0
  538. package/src/detectors/technical-docs.ts +230 -0
  539. package/src/extension/pack-read.ts +128 -0
  540. package/src/extension/plugin-load.ts +49 -9
  541. package/src/extension/plugin-parse.ts +10 -3
  542. package/src/facts/address-values.ts +74 -0
  543. package/src/facts/cross-facts.ts +54 -0
  544. package/src/fix-plan/chosen-depth.ts +28 -0
  545. package/src/fix-plan/mode.ts +57 -11
  546. package/src/fix-plan/plan.ts +37 -5
  547. package/src/fix-plan/render.ts +24 -0
  548. package/src/fix-plan/text.ts +47 -10
  549. package/src/genre-load.ts +7 -4
  550. package/src/genre-parse.ts +19 -0
  551. package/src/init.ts +10 -10
  552. package/src/known-genres.ts +19 -0
  553. package/src/link-shape.ts +62 -0
  554. package/src/reasons.ts +16 -4
  555. package/src/render/explain.ts +14 -2
  556. package/src/render/friendly.ts +1 -1
  557. package/src/render/rules-json.ts +14 -3
  558. package/src/render/rules-table.ts +6 -6
  559. package/src/render/semantic.ts +2 -2
  560. package/src/render/suppressions.ts +2 -2
  561. package/src/rewrite-depth.ts +60 -0
  562. package/src/rule-fields.ts +95 -0
  563. package/src/rule-guide.ts +9 -0
  564. package/src/rule-load.ts +10 -4
  565. package/src/rule-offs.ts +50 -0
  566. package/src/structure/build.ts +21 -1
  567. package/src/structure/cross-definitions.ts +60 -0
@@ -0,0 +1,8 @@
1
+ import type { CustomProblem } from "./parse.ts";
2
+
3
+ /** One field of one rule, read: its value when it can be used, and what is wrong with it otherwise. */
4
+ export type Checked<T> = { readonly value: T | undefined; readonly problems: readonly CustomProblem[] };
5
+
6
+ export const ok = <T>(value: T): Checked<T> => ({ value, problems: [] });
7
+
8
+ export const failed = <T>(...problems: CustomProblem[]): Checked<T> => ({ value: undefined, problems });
@@ -0,0 +1,102 @@
1
+ import type { LevelTable, Localized, Severity } from "../plugin.ts";
2
+ import { RULE_GROUPS, ruleGuideOf, type RuleGroup, type RuleGuide } from "../rule-guide.ts";
3
+ import { fieldProblems } from "../rule-fields.ts";
4
+ import { failed, ok, type Checked } from "./checked.ts";
5
+
6
+ // The fields a team's rule (custom_rules, or a plugin's rules) writes the way chaff's own rules/*.yaml do: levels,
7
+ // use_for, group, summary, the example by language and rewrite. Each is optional; without it the rule reads as a team
8
+ // rule always has. The shared fields are checked by rule-fields.ts, the one place chaff's own rules are checked too. Pure.
9
+
10
+ const SEVERITIES: readonly Severity[] = ["info", "warning", "error"];
11
+ const LEVEL_NAMES = ["strict", "normal", "relaxed"] as const;
12
+
13
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
14
+
15
+ const printed = (value: unknown): string => (typeof value === "string" ? value : (JSON.stringify(value) ?? typeof value));
16
+
17
+ const rankOf = (severity: Severity): number => SEVERITIES.indexOf(severity) + 1;
18
+
19
+ /** level: one severity; normal is the rule's own, relaxed one lower, strict one higher, where there is one. */
20
+ const tableFor = (severity: Severity): LevelTable => {
21
+ const rank = rankOf(severity);
22
+ return { ...(rank < SEVERITIES.length ? { strict: rank + 1 } : {}), normal: rank, ...(rank > 1 ? { relaxed: rank - 1 } : {}) };
23
+ };
24
+
25
+ const severityAt = (value: unknown): Severity | undefined => SEVERITIES.find((entry) => entry === value);
26
+
27
+ /**
28
+ * levels: a severity for each of strict, normal and relaxed, as chaff's rules with nothing to count write them. A
29
+ * team's detector reports places, not counts, so a number has nothing to be compared with and is refused.
30
+ */
31
+ const tableOf = (written: unknown): { table: LevelTable; severity: Severity } | undefined => {
32
+ if (!isRecord(written) || Object.keys(written).some((key) => !LEVEL_NAMES.some((name) => name === key))) return undefined;
33
+ const ranks = LEVEL_NAMES.flatMap((name) => {
34
+ const severity = severityAt(written[name]);
35
+ return severity === undefined ? [] : [[name, rankOf(severity)] as const];
36
+ });
37
+ const normal = severityAt(written["normal"]);
38
+ if (normal === undefined || ranks.length !== Object.keys(written).length) return undefined;
39
+ return { table: Object.fromEntries(ranks), severity: normal };
40
+ };
41
+
42
+ export type RuleLevels = { readonly table: LevelTable; readonly severity: Severity };
43
+
44
+ /** level (one severity, as before) or levels (a severity per level). Not both: they would disagree on normal. */
45
+ export const levelsOf = (raw: Readonly<Record<string, unknown>>, at: string): Checked<RuleLevels> => {
46
+ const [level, levels] = [raw["level"], raw["levels"]];
47
+ if (level !== undefined && levels !== undefined) return failed({ kind: "level-and-levels", at });
48
+ if (levels !== undefined) {
49
+ const read = tableOf(levels);
50
+ return read === undefined ? failed({ kind: "bad-levels", at, written: printed(levels) }) : ok(read);
51
+ }
52
+ if (level === undefined) return ok({ table: tableFor("warning"), severity: "warning" });
53
+ const severity = severityAt(level);
54
+ return severity === undefined ? failed({ kind: "bad-level", at, written: printed(level) }) : ok({ table: tableFor(severity), severity });
55
+ };
56
+
57
+ /** An example written by language ({ ja: { before, after } }) rather than as { before, after }. */
58
+ const isExampleByLanguage = (example: unknown): example is Record<string, unknown> =>
59
+ isRecord(example) && Object.keys(example).length > 0 && !("before" in example) && !("after" in example);
60
+
61
+ /** example's before and after, each by language, whichever form it is written in. */
62
+ export const exampleSides = (example: unknown): { readonly before: unknown; readonly after: unknown } => {
63
+ if (!isRecord(example)) return { before: undefined, after: undefined };
64
+ if (!isExampleByLanguage(example)) return { before: example["before"], after: example["after"] };
65
+ const pairs = Object.entries(example).flatMap(([language, pair]) => (isRecord(pair) ? [[language, pair] as const] : []));
66
+ return {
67
+ before: Object.fromEntries(pairs.map(([language, pair]) => [language, pair["before"]])),
68
+ after: Object.fromEntries(pairs.map(([language, pair]) => [language, pair["after"]])),
69
+ };
70
+ };
71
+
72
+ export type Described = Pick<RuleGuide, "rewrite" | "rewriteDepth"> & {
73
+ readonly group: RuleGroup;
74
+ /** undefined: the rule's name stands for its summary. */
75
+ readonly summary: Localized | undefined;
76
+ /** undefined: every genre, as a team's rule always has. */
77
+ readonly useFor: readonly string[] | undefined;
78
+ };
79
+
80
+ const textOf = (value: unknown): Localized | undefined => {
81
+ if (typeof value === "string") return value.trim() === "" ? undefined : { ja: value.trim(), en: value.trim() };
82
+ return isRecord(value) ? ruleGuideOf({ summary: value }).summary : undefined;
83
+ };
84
+
85
+ /**
86
+ * The fields a team's rule shares with chaff's own, checked in rule-fields.ts. An example written as { before, after }
87
+ * is left to the caller, which has always read and reported it.
88
+ */
89
+ export const describedOf = (raw: Readonly<Record<string, unknown>>, at: string, genres: readonly string[]): Checked<Described> => {
90
+ const shared = { ...raw, example: isExampleByLanguage(raw["example"]) ? raw["example"] : undefined };
91
+ const problems = fieldProblems(shared, genres).map((problem) => ({ kind: problem.kind, at, written: problem.written }));
92
+ if (problems.length > 0) return failed(...problems);
93
+ const { rewrite, rewriteDepth } = ruleGuideOf({ rewrite: raw["rewrite"] });
94
+ const useFor = raw["use_for"];
95
+ return ok({
96
+ group: RULE_GROUPS.find((group) => group === raw["group"]) ?? "team",
97
+ summary: textOf(raw["summary"]),
98
+ useFor: Array.isArray(useFor) ? useFor.map((entry) => String(entry)) : undefined,
99
+ rewrite,
100
+ rewriteDepth,
101
+ });
102
+ };
@@ -2,6 +2,7 @@ import type { Config } from "../config/load.ts";
2
2
  import type { RuleDefinition } from "../plugin.ts";
3
3
  import { loadRules } from "../rule-load.ts";
4
4
  import { loadGenres } from "../genre-load.ts";
5
+ import { knownGenres } from "../known-genres.ts";
5
6
  import { parseCustomRules, type CustomRules } from "./parse.ts";
6
7
 
7
8
  /** The team's custom_rules as rules, with what could not be read. A team's rule applies to every genre, like the team's words. */
@@ -9,6 +10,7 @@ export const customRulesOf = (config: Pick<Config, "customRules" | "baseDir">):
9
10
  parseCustomRules(config.customRules, {
10
11
  builtIn: new Set(loadRules("en").map((rule) => rule.id)),
11
12
  useFor: loadGenres().groups.map((group) => group.id),
13
+ genres: knownGenres(),
12
14
  baseDir: config.baseDir,
13
15
  });
14
16
 
@@ -1,7 +1,10 @@
1
- import type { CustomSpec, LevelTable, Localized, RuleDefinition, Severity, TokenCondition } from "../plugin.ts";
1
+ import type { CustomSpec, Localized, RuleDefinition, TokenCondition } from "../plugin.ts";
2
2
  import { regexRefusal, type RegexRefusal } from "./regex-safety.ts";
3
3
  import { posTags } from "./token-pattern.ts";
4
4
  import { modulePathOf, type ModulePathRefusal } from "./module-path.ts";
5
+ import type { FieldProblem } from "../rule-fields.ts";
6
+ import { failed, ok, type Checked } from "./checked.ts";
7
+ import { describedOf, exampleSides, levelsOf, type Described, type RuleLevels } from "./dsl-fields.ts";
5
8
 
6
9
  // custom_rules in chaff.yaml: a team's own deterministic rules, written without code. Each becomes a RuleDefinition like the
7
10
  // built-in ones, so findings, explain, rules --json, stet, the baseline and SARIF treat it the same. Pure: the YAML comes in parsed.
@@ -26,18 +29,25 @@ export type CustomProblem =
26
29
  | { readonly kind: "bad-pattern"; readonly at: string; readonly refusal: RegexRefusal }
27
30
  | { readonly kind: "no-tokens"; readonly at: string }
28
31
  | { readonly kind: "bad-token"; readonly at: string; readonly index: number }
29
- | { readonly kind: "unknown-pos"; readonly at: string; readonly written: string };
32
+ | { readonly kind: "unknown-pos"; readonly at: string; readonly written: string }
33
+ | { readonly kind: "level-and-levels"; readonly at: string }
34
+ | { readonly kind: "bad-levels"; readonly at: string; readonly written: string }
35
+ | (FieldProblem & { readonly at: string });
30
36
 
31
37
  export type CustomRules = { readonly rules: readonly RuleDefinition[]; readonly problems: readonly CustomProblem[] };
32
38
 
33
39
  /**
34
- * What parsing needs to know: the built-in rule ids (a team's rule may not take one), the genres a rule applies to, and
35
- * the folder a module's path is relative to (where chaff.yaml is).
40
+ * What parsing needs to know: the built-in rule ids (a team's rule may not take one), the genres a rule applies to when
41
+ * it does not say (useFor), every genre a use_for may name, and the folder a module's path is relative to (where chaff.yaml is).
36
42
  */
37
- export type CustomContext = { readonly builtIn: ReadonlySet<string>; readonly useFor: readonly string[]; readonly baseDir: string };
43
+ export type CustomContext = {
44
+ readonly builtIn: ReadonlySet<string>;
45
+ readonly useFor: readonly string[];
46
+ readonly genres: readonly string[];
47
+ readonly baseDir: string;
48
+ };
38
49
 
39
50
  const RULE_ID = /^[a-z][a-z0-9-]*$/u;
40
- const SEVERITIES: readonly Severity[] = ["info", "warning", "error"];
41
51
 
42
52
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
43
53
 
@@ -58,12 +68,6 @@ const EVERY_LANGUAGE = "*";
58
68
 
59
69
  const exampleSideOf = (value: unknown): Localized | undefined => (nonEmpty(value) ? { [EVERY_LANGUAGE]: value.trim() } : byLanguageOf(value));
60
70
 
61
- /** One rule's own problems, with the rule's id (or its place in the list) as where. */
62
- type Checked<T> = { readonly value: T | undefined; readonly problems: readonly CustomProblem[] };
63
-
64
- const ok = <T>(value: T): Checked<T> => ({ value, problems: [] });
65
- const failed = <T>(...problems: CustomProblem[]): Checked<T> => ({ value: undefined, problems });
66
-
67
71
  type Pair = { readonly avoid: string; readonly use: string };
68
72
 
69
73
  /** words as a map (avoid: use) or a list (words to point at, with nothing to use instead). */
@@ -125,7 +129,8 @@ const moduleOf = (raw: Record<string, unknown>, at: string, baseDir: string): Ch
125
129
  };
126
130
 
127
131
  const specOf = (raw: Record<string, unknown>, type: string, at: string, baseDir: string): Checked<CustomSpec> => {
128
- if (type === "words") return wordsOf(raw["words"], at);
132
+ // A words rule may take its words from a word list instead, one per language (a rule pack's lexicons/<language>/).
133
+ if (type === "words") return takesWordList(raw, type) ? ok({ type: "words", words: [] }) : wordsOf(raw["words"], at);
129
134
  if (type === "pattern") return patternOf(raw, at);
130
135
  if (type === "module") return moduleOf(raw, at, baseDir);
131
136
  return tokensOf(raw["tokens"], at);
@@ -137,28 +142,16 @@ const typeOf = (raw: Record<string, unknown>, at: string): Checked<string> => {
137
142
  return CUSTOM_TYPES.some((known) => known === type) ? ok(type) : failed({ kind: "unknown-type", at, written: type });
138
143
  };
139
144
 
140
- const severityOf = (raw: unknown, at: string): Checked<Severity> => {
141
- if (raw === undefined) return ok("warning");
142
- const severity = SEVERITIES.find((entry) => entry === raw);
143
- return severity === undefined ? failed({ kind: "bad-level", at, written: typeof raw === "string" ? raw : printed(raw) }) : ok(severity);
144
- };
145
-
146
- /** The four words as severities: normal is the rule's own level, relaxed one lower, strict one higher, where there is one. */
147
- const levelsFor = (severity: Severity): LevelTable => {
148
- const rank = SEVERITIES.indexOf(severity) + 1;
149
- return { ...(rank < SEVERITIES.length ? { strict: rank + 1 } : {}), normal: rank, ...(rank > 1 ? { relaxed: rank - 1 } : {}) };
150
- };
151
-
152
145
  type Texts = { readonly name: Localized; readonly why: Localized; readonly how_to_fix: Localized; readonly before: Localized; readonly after: Localized };
153
146
 
154
147
  const textsOf = (raw: Record<string, unknown>, at: string): Checked<Texts> => {
155
- const example = isRecord(raw["example"]) ? raw["example"] : {};
148
+ const example = exampleSides(raw["example"]);
156
149
  const found = {
157
150
  name: localizedOf(raw["name"]),
158
151
  why: localizedOf(raw["why"]),
159
152
  how_to_fix: localizedOf(raw["how_to_fix"]),
160
- before: exampleSideOf(example["before"]),
161
- after: exampleSideOf(example["after"]),
153
+ before: exampleSideOf(example.before),
154
+ after: exampleSideOf(example.after),
162
155
  };
163
156
  const missing = Object.entries(found).flatMap(([field, text]) =>
164
157
  text === undefined ? [{ kind: "missing" as const, at, field: field === "before" || field === "after" ? `example.${field}` : field }] : [],
@@ -179,9 +172,13 @@ const requiresOf = (raw: unknown, type: string, at: string): Checked<readonly st
179
172
  return unknown === undefined ? ok([...new Set(written)]) : failed({ kind: "bad-requires", at, written: unknown });
180
173
  };
181
174
 
182
- /** A module's word list by name; its detector gets it as options.lexicon. The other types carry their words themselves. */
183
- const wordListOf = (raw: unknown, type: string, at: string): Checked<string | undefined> => {
184
- if (raw === undefined || type !== "module") return ok(undefined);
175
+ /** A module reads a word list; so does a words rule that writes word_list in place of words. The others carry their words. */
176
+ const takesWordList = (raw: Record<string, unknown>, type: string): boolean =>
177
+ raw["word_list"] !== undefined && (type === "module" || (type === "words" && raw["words"] === undefined));
178
+
179
+ /** The word list by name; the detector gets it, in the document's language, as options.lexicon. */
180
+ const wordListOf = (raw: unknown, takesList: boolean, at: string): Checked<string | undefined> => {
181
+ if (raw === undefined || !takesList) return ok(undefined);
185
182
  return nonEmpty(raw) ? ok(raw.trim()) : failed({ kind: "bad-word-list", at, written: printed(raw) });
186
183
  };
187
184
 
@@ -190,7 +187,7 @@ type Needs = { readonly requires: readonly string[]; readonly wordList: string |
190
187
 
191
188
  const needsOf = (raw: Record<string, unknown>, type: string, at: string): Checked<Needs> => {
192
189
  const requires = requiresOf(raw["requires"], type, at);
193
- const wordList = wordListOf(raw["word_list"], type, at);
190
+ const wordList = wordListOf(raw["word_list"], takesWordList(raw, type), at);
194
191
  if (requires.value === undefined || wordList.problems.length > 0) return failed(...requires.problems, ...wordList.problems);
195
192
  return ok({ requires: requires.value, wordList: wordList.value });
196
193
  };
@@ -226,10 +223,11 @@ type Example = { readonly before: string; readonly after: string };
226
223
  type Parts = {
227
224
  readonly spec: CustomSpec;
228
225
  readonly needs: Needs;
229
- readonly severity: Severity;
226
+ readonly levels: RuleLevels;
230
227
  readonly texts: Texts;
231
228
  readonly languages: readonly string[] | undefined;
232
229
  readonly examples: Readonly<Record<string, Example>>;
230
+ readonly described: Described;
233
231
  };
234
232
 
235
233
  const READER_LANGUAGES: readonly string[] = ["ja", "en"];
@@ -253,7 +251,7 @@ const definitionOf = (id: string, raw: Record<string, unknown>, parts: Parts, us
253
251
  message: localizedOf(raw["message"]) ?? defaultMessage(parts.spec, parts.texts.name),
254
252
  messages: {},
255
253
  placeholders: PLACEHOLDERS,
256
- levels: levelsFor(parts.severity),
254
+ levels: parts.levels.table,
257
255
  level_sets: "severity",
258
256
  by_genre: {},
259
257
  how_to_find: HOW_TO_FIND[parts.spec.type],
@@ -266,18 +264,19 @@ const definitionOf = (id: string, raw: Record<string, unknown>, parts: Parts, us
266
264
  uses: [],
267
265
  from: [],
268
266
  languages: parts.languages,
269
- use_for: useFor,
270
- severity: parts.severity,
267
+ use_for: parts.described.useFor ?? useFor,
268
+ severity: parts.levels.severity,
271
269
  custom: parts.spec,
272
270
  // The reference and explain read a rule's plain-language fields from guide; a team's rule is listed with the team's words.
273
271
  guide: {
274
- group: "team",
275
- summary: parts.texts.name,
272
+ group: parts.described.group,
273
+ summary: parts.described.summary ?? parts.texts.name,
276
274
  examples: parts.examples,
277
275
  notFlagged: {},
278
276
  levelMeaning: {},
279
277
  sources: [],
280
- rewrite: {},
278
+ rewrite: parts.described.rewrite,
279
+ rewriteDepth: parts.described.rewriteDepth,
281
280
  },
282
281
  });
283
282
 
@@ -289,24 +288,47 @@ const idOf = (raw: Record<string, unknown>, index: number, seen: ReadonlySet<str
289
288
  return seen.has(id) ? failed({ kind: "duplicate-id", at }) : ok(id);
290
289
  };
291
290
 
291
+ /** Each field of one rule, checked. */
292
+ type Checks = {
293
+ readonly id: Checked<string>;
294
+ readonly spec: Checked<CustomSpec>;
295
+ readonly needs: Checked<Needs>;
296
+ readonly levels: Checked<RuleLevels>;
297
+ readonly texts: Checked<Texts>;
298
+ readonly languages: Checked<readonly string[] | undefined>;
299
+ readonly described: Checked<Described>;
300
+ };
301
+
302
+ /** The rule's parts once every check passed; undefined when one has no value. */
303
+ const partsOf = (checks: Checks, examples: Readonly<Record<string, Example>>): { readonly id: string; readonly parts: Parts } | undefined => {
304
+ const { id, spec, needs, levels, texts, described } = checks;
305
+ if (id.value === undefined || spec.value === undefined || needs.value === undefined) return undefined;
306
+ if (levels.value === undefined || texts.value === undefined || described.value === undefined) return undefined;
307
+ const parts = { spec: spec.value, needs: needs.value, levels: levels.value, texts: texts.value, languages: checks.languages.value, examples };
308
+ return { id: id.value, parts: { ...parts, described: described.value } };
309
+ };
310
+
292
311
  const ruleOf = (raw: unknown, index: number, seen: ReadonlySet<string>, context: CustomContext): Checked<RuleDefinition> => {
293
312
  if (!isRecord(raw)) return failed({ kind: "not-a-map", at: `#${String(index + 1)}` });
294
313
  const id = idOf(raw, index, seen, context.builtIn);
295
314
  const at = id.value ?? (nonEmpty(raw["id"]) ? raw["id"] : `#${String(index + 1)}`);
296
315
  const type = typeOf(raw, at);
297
- const spec = type.value === undefined ? failed<CustomSpec>() : specOf(raw, type.value, at, context.baseDir);
298
- const needs = needsOf(raw, type.value ?? "", at);
299
- const severity = severityOf(raw["level"], at);
300
- const texts = textsOf(raw, at);
301
- const languages = languagesOf(raw["languages"], at);
316
+ const checks: Checks = {
317
+ id,
318
+ spec: type.value === undefined ? failed<CustomSpec>() : specOf(raw, type.value, at, context.baseDir),
319
+ needs: needsOf(raw, type.value ?? "", at),
320
+ levels: levelsOf(raw, at),
321
+ texts: textsOf(raw, at),
322
+ languages: languagesOf(raw["languages"], at),
323
+ described: describedOf(raw, at, context.genres),
324
+ };
325
+ const { texts, languages } = checks;
302
326
  const examples = texts.value === undefined ? {} : examplesOf(texts.value, languages.value);
303
327
  const unpaired: CustomProblem[] = texts.value !== undefined && Object.keys(examples).length === 0 ? [{ kind: "unpaired-example", at }] : [];
304
- const problems = [...[id, type, spec, needs, severity, texts, languages].flatMap((checked) => checked.problems), ...unpaired];
305
- if (problems.length > 0) return failed(...problems);
306
- if (id.value === undefined || spec.value === undefined || needs.value === undefined || severity.value === undefined || texts.value === undefined)
307
- return failed();
308
- const parts = { spec: spec.value, needs: needs.value, severity: severity.value, texts: texts.value, languages: languages.value, examples };
309
- return ok(definitionOf(id.value, raw, parts, context.useFor));
328
+ const ordered: Checked<unknown>[] = [id, type, checks.spec, checks.needs, checks.levels, texts, languages, checks.described];
329
+ const problems = [...ordered.flatMap((checked) => checked.problems), ...unpaired];
330
+ const ready = problems.length > 0 ? undefined : partsOf(checks, examples);
331
+ return ready === undefined ? failed(...problems) : ok(definitionOf(ready.id, raw, ready.parts, context.useFor));
310
332
  };
311
333
 
312
334
  /** custom_rules as written. Every problem is reported; a rule with a problem is left out, and the run is stopped by the caller. */
@@ -5,10 +5,13 @@ import { customRulesOf } from "./load.ts";
5
5
  import { MAX_PATTERN_LENGTH, MAX_REPEATS, type RegexRefusal } from "./regex-safety.ts";
6
6
  import { POS_WRITTEN_NAMES } from "./token-pattern.ts";
7
7
  import type { ModulePathRefusal } from "./module-path.ts";
8
+ import { fieldProblemSentence, type FieldKind, type FieldProblem } from "../rule-fields.ts";
8
9
 
9
10
  /** Each problem's sentence, with {at}, {written}, {field}, {index}, {refusal} and {names} filled in from the problem. */
10
11
  type Text = {
11
- readonly problems: Readonly<Record<CustomProblem["kind"], string>>;
12
+ readonly problems: Readonly<Record<Exclude<CustomProblem["kind"], FieldKind>, string>>;
13
+ /** What comes before a shared field's own sentence (rule-fields.ts). */
14
+ readonly fieldAt: string;
12
15
  readonly refusal: Readonly<Record<RegexRefusal, string>>;
13
16
  readonly modulePath: Readonly<Record<ModulePathRefusal, string>>;
14
17
  };
@@ -45,7 +48,11 @@ const TEXT: Texts<Text> = {
45
48
  "bad-token": "{scope} の {at}: tokens の {index} 番目に pos・base・surface のどれもありません",
46
49
  "unknown-pos": "{scope} の {at}: 品詞 {written} は知りません({names})",
47
50
  "bad-pattern": "{scope} の {at}: pattern を使えません。{refusal}",
51
+ "level-and-levels": "{scope} の {at}: level と levels の両方があります。どちらか一つにしてください",
52
+ "bad-levels":
53
+ "{scope} の {at}: levels: {written} は読めません。strict・normal・relaxed に重さ(error / warning / info)を書き、normal は必ず書きます。チームのルールは箇所を出すので、数は書けません",
48
54
  },
55
+ fieldAt: "{scope} の {at}: ",
49
56
  },
50
57
  en: {
51
58
  modulePath: {
@@ -78,7 +85,11 @@ const TEXT: Texts<Text> = {
78
85
  "bad-token": "{scope} {at}: token {index} has none of pos, base and surface",
79
86
  "unknown-pos": "{scope} {at}: unknown part of speech {written} ({names})",
80
87
  "bad-pattern": "{scope} {at}: the pattern cannot be used: {refusal}",
88
+ "level-and-levels": "{scope} {at}: has both level and levels; keep one",
89
+ "bad-levels":
90
+ "{scope} {at}: cannot read levels: {written}. Give strict, normal and relaxed a severity (error / warning / info), normal always. A team's rule reports places, so a number has nothing to count",
81
91
  },
92
+ fieldAt: "{scope} {at}: ",
82
93
  },
83
94
  };
84
95
 
@@ -100,10 +111,15 @@ const valuesOf = (problem: CustomProblem, text: Text, scope: string): Readonly<R
100
111
  names: POS_WRITTEN_NAMES.join(" / "),
101
112
  });
102
113
 
114
+ const FIELD_KINDS: ReadonlySet<string> = new Set<FieldKind>(["bad-depth", "bad-group", "bad-use-for", "bad-summary", "bad-example"]);
115
+
116
+ const isFieldProblem = (problem: CustomProblem): problem is FieldProblem & { readonly at: string } => FIELD_KINDS.has(problem.kind);
117
+
103
118
  /** One problem as a sentence. scope names the list the rule is in: custom_rules, or a plugin's rules. */
104
119
  export const customProblemSentence = (problem: CustomProblem, ui: UiLanguage, scope = "custom_rules"): string => {
105
120
  const values = valuesOf(problem, TEXT[ui], scope);
106
- return TEXT[ui].problems[problem.kind].replace(PLACEHOLDER, (_whole, key: string) => values[key] ?? "");
121
+ const fill = (template: string): string => template.replace(PLACEHOLDER, (_whole, key: string) => values[key] ?? "");
122
+ return isFieldProblem(problem) ? `${fill(TEXT[ui].fieldAt)}${fieldProblemSentence(problem, ui)}` : fill(TEXT[ui].problems[problem.kind]);
107
123
  };
108
124
 
109
125
  /** What in custom_rules cannot run. Each stops the run: a team's rule that silently does not run looks like a clean document. */
@@ -3,7 +3,7 @@
3
3
  import type { Detector, Finding, ProseDocument, Span } from "../plugin.ts";
4
4
  import { readMarkdown } from "../markdown-read.ts";
5
5
  import { spanOf, type MarkdownNode } from "../markdown-node.ts";
6
- import { eachPreOrder } from "../tree-walk.ts";
6
+ import { eachPreOrder, foldPostOrder } from "../tree-walk.ts";
7
7
  import { quoteAt } from "./structure-tree.ts";
8
8
 
9
9
  /** Fewer body rows than this, and one blank is as likely a choice as a gap. */
@@ -21,14 +21,22 @@ const EMPHASIS = /[*_]/gu;
21
21
  const COMMENT = /^<!--[\s\S]*-->$/u;
22
22
  const VISIBLE_LEAVES = new Set(["inlineCode", "image", "imageReference"]);
23
23
 
24
- /** Whether a node shows the reader anything: text that is not blank, code, an image, or HTML that is not a comment. */
25
- const shows = (node: MarkdownNode): boolean => {
24
+ /** What a leaf shows on its own; undefined for a node whose children decide. */
25
+ const leafShows = (node: MarkdownNode): boolean | undefined => {
26
26
  if (node.type === "text") return (node.value ?? "").replace(EMPHASIS, "").trim() !== "";
27
27
  if (node.type === "html") return !COMMENT.test((node.value ?? "").trim());
28
28
  if (VISIBLE_LEAVES.has(node.type)) return true;
29
- return (node.children ?? []).some(shows);
29
+ return undefined;
30
30
  };
31
31
 
32
+ /** Whether a node shows the reader anything: text that is not blank, code, an image, or HTML that is not a comment. */
33
+ const shows = (node: MarkdownNode): boolean =>
34
+ foldPostOrder<MarkdownNode, boolean>(
35
+ node,
36
+ (each) => each.children ?? [],
37
+ (each, children) => leafShows(each) ?? children.some((child) => child),
38
+ );
39
+
32
40
  const sourceOf = (source: string, node: MarkdownNode | undefined): string => {
33
41
  const span = node === undefined ? undefined : spanOf(node);
34
42
  return span === undefined ? "" : source.slice(span.start, span.end).replace(/\|/gu, "").trim();
@@ -0,0 +1,199 @@
1
+ import type { Detector, DetectorOptions, Finding, Lexicon, ProseDocument, Sentence, Token } from "../plugin.ts";
2
+ import { isWithinAny, quotedIn } from "../quoted-span.ts";
3
+
4
+ // The shape of a clause read from its words: how it ends, a paired word left alone, an adverb that needs a
5
+ // negation, a particle used where another one belongs. Every word comes from a lexicon; this file knows parts of speech.
6
+
7
+ const findingAt = (sentence: Sentence, offset: number, values: Readonly<Record<string, string | number>>): Finding => ({
8
+ rule: "",
9
+ severity: "info",
10
+ line: 0,
11
+ column: 0,
12
+ quote: sentence.text.trim(),
13
+ values: { ...values, offset },
14
+ });
15
+
16
+ const patternsOf = (lexicon: Lexicon | undefined, group?: string): ReadonlySet<string> =>
17
+ new Set((lexicon ?? []).filter((entry) => group === undefined || entry.group === group).map((entry) => entry.pattern));
18
+
19
+ const isSpace = (token: Token | undefined): boolean => token !== undefined && token.surface.trim() === "";
20
+
21
+ /** The tokens of a sentence outside anything quoted in 「」『』, with their index in the sentence. */
22
+ const ownTokens = (sentence: Sentence): { readonly token: Token; readonly at: number }[] => {
23
+ const quoted = quotedIn(sentence);
24
+ return (sentence.tokens ?? []).flatMap((token, at) => (isWithinAny(quoted, token.span) ? [] : [{ token, at }]));
25
+ };
26
+
27
+ const wordOf = (token: Token): string => token.lemma ?? token.surface;
28
+
29
+ const isListed = (words: ReadonlySet<string>, token: Token | undefined): boolean =>
30
+ token !== undefined && (words.has(token.surface) || words.has(wordOf(token)));
31
+
32
+ // --- ending-run: sentences in a row that end with the same words ---------------------------------------------
33
+
34
+ const TRAILING_POS: ReadonlySet<string> = new Set(["PUNCT", "SYM"]);
35
+ const TAIL_POS: ReadonlySet<string> = new Set(["AUX", "PART"]);
36
+ const VERBAL_POS: ReadonlySet<string> = new Set(["VERB"]);
37
+
38
+ /**
39
+ * How a sentence ends: its closing auxiliaries, and the verb before them as written (しました, あります, 思います).
40
+ * A noun before them is left out, so 必要です and 重要です end the same way. No auxiliary, no ending to compare.
41
+ */
42
+ export const endingOf = (sentence: Sentence): string | undefined => {
43
+ const tokens = (sentence.tokens ?? []).filter((token) => !isSpace(token));
44
+ const last = tokens.findLastIndex((token) => !TRAILING_POS.has(token.pos));
45
+ const head = tokens.slice(0, last + 1).findLastIndex((token) => !TAIL_POS.has(token.pos));
46
+ if (head === last) return undefined;
47
+ const headToken = tokens[head];
48
+ const tail = tokens.slice(head + 1, last + 1).map((token) => token.surface);
49
+ return [headToken !== undefined && VERBAL_POS.has(headToken.pos) ? headToken.surface : "", ...tail].join("");
50
+ };
51
+
52
+ type Ended = { readonly sentence: Sentence; readonly ending: string | undefined };
53
+
54
+ /** Runs of consecutive sentences with the same ending. A sentence with no ending to compare closes a run. */
55
+ export const endingRuns = (sentences: readonly Sentence[]): Ended[][] =>
56
+ sentences
57
+ .map((sentence) => ({ sentence, ending: endingOf(sentence) }))
58
+ .reduce<Ended[][]>((runs, ended) => {
59
+ const last = runs.at(-1) ?? [];
60
+ const continues = ended.ending !== undefined && last[0]?.ending === ended.ending;
61
+ return continues ? [...runs.slice(0, -1), [...last, ended]] : [...runs, [ended]];
62
+ }, [])
63
+ .filter((run) => run[0]?.ending !== undefined);
64
+
65
+ const runFinding = (run: readonly Ended[], limit: number): Finding => {
66
+ const first = run[0];
67
+ return {
68
+ rule: "",
69
+ severity: "info",
70
+ line: 0,
71
+ column: 0,
72
+ quote: run.map(({ sentence }) => sentence.text.trim()).join(""),
73
+ values: { word: first?.ending ?? "", count: run.length, limit, offset: first?.sentence.span.start ?? 0 },
74
+ };
75
+ };
76
+
77
+ /** More than limit sentences in a row within one paragraph that end with the same words. */
78
+ export const endingRun: Detector = (doc, options): Finding[] =>
79
+ doc.paragraphs.flatMap((paragraph) =>
80
+ endingRuns(paragraph.sentences)
81
+ .filter((run) => run.length > options.limit)
82
+ .map((run) => runFinding(run, options.limit)),
83
+ );
84
+
85
+ // --- unpaired-parallel: a word that lists by pairs (〜たり) used once, followed by another clause -----------------
86
+
87
+ const CONTENT_POS: ReadonlySet<string> = new Set(["VERB", "ADJ", "NOUN", "PROPN"]);
88
+
89
+ const nextWord = (tokens: readonly Token[], at: number): Token | undefined => tokens.slice(at + 1).find((token) => !isSpace(token));
90
+
91
+ /**
92
+ * The marker is left alone when the sentence holds it once and what follows starts a new clause: a comma,
93
+ * or a content word other than the lexicon's closer (〜たり、映画を見る / 〜たり見る). 〜たりする and 〜たりもする are whole.
94
+ */
95
+ const leftAlone = (tokens: readonly Token[], at: number, closers: ReadonlySet<string>, commas: ReadonlySet<string>): boolean => {
96
+ const next = nextWord(tokens, at);
97
+ if (next === undefined) return false;
98
+ if (commas.has(next.surface)) return true;
99
+ return CONTENT_POS.has(next.pos) && !isListed(closers, next);
100
+ };
101
+
102
+ const CONJUNCTION = "CCONJ";
103
+
104
+ const markersIn = (sentence: Sentence, markers: ReadonlySet<string>): { readonly token: Token; readonly at: number }[] =>
105
+ ownTokens(sentence).filter(({ token }) => token.pos === CONJUNCTION && markers.has(token.surface));
106
+
107
+ /** A sentence that uses the lexicon's paired marker once and goes on without its partner. */
108
+ export const unpairedParallel: Detector = (doc, options): Finding[] => {
109
+ const markers = patternsOf(options.lexicon, "marker");
110
+ const closers = patternsOf(options.lexicon, "closer");
111
+ const commas = patternsOf(options.lexicon, "comma");
112
+ return doc.sentences.flatMap((sentence) => {
113
+ const found = markersIn(sentence, markers);
114
+ const only = found.length === 1 ? found[0] : undefined;
115
+ if (only === undefined || !leftAlone(sentence.tokens ?? [], only.at, closers, commas)) return [];
116
+ return [findingAt(sentence, only.token.span.start, { word: only.token.surface })];
117
+ });
118
+ };
119
+
120
+ // --- adverb-polarity: an adverb that wants a negation, in a sentence that has none ------------------------------
121
+
122
+ type Negations = { readonly words: ReadonlySet<string>; readonly prefixes: readonly string[] };
123
+
124
+ const negationsOf = (lexicon: Lexicon | undefined): Negations => ({
125
+ words: new Set((lexicon ?? []).filter((entry) => entry.group === undefined).map((entry) => entry.pattern)),
126
+ prefixes: [...patternsOf(lexicon, "prefix")],
127
+ });
128
+
129
+ /** A negating word (ない, 違う) or a word opening with a negating prefix (不十分, 無理). */
130
+ const isNegation = (token: Token, negations: Negations): boolean =>
131
+ isListed(negations.words, token) || negations.prefixes.some((prefix) => token.surface.length > prefix.length && token.surface.startsWith(prefix));
132
+
133
+ type Placed = { readonly token: Token; readonly at: number };
134
+
135
+ /** A negation of the writer's own after the adverb; one inside a quotation does not answer it. */
136
+ const hasNegation = (own: readonly Placed[], from: number, negations: Negations): boolean =>
137
+ own.some(({ token, at }) => at > from && isNegation(token, negations));
138
+
139
+ /** Each of the lexicon's adverbs (全然, 決して) with no word of the negation list after it in its sentence. */
140
+ export const adverbPolarity: Detector = (doc, options): Finding[] => {
141
+ const adverbs = patternsOf(options.lexicon);
142
+ const negations = negationsOf(doc.lexicons["negation-word"]);
143
+ return doc.sentences.flatMap((sentence) => {
144
+ const own = ownTokens(sentence);
145
+ return own
146
+ .filter(({ token, at }) => adverbs.has(token.surface) && !hasNegation(own, at, negations))
147
+ .map(({ token }) => findingAt(sentence, token.span.start, { word: token.surface }));
148
+ });
149
+ };
150
+
151
+ // --- origin-particle: a comparison particle (より) used for "from" -------------------------------------------------
152
+
153
+ /** Adverbs (順次) and punctuation (本日より、発送) stand between the particle and its head. */
154
+ const SKIPPED_POS: ReadonlySet<string> = new Set(["ADV", "PUNCT"]);
155
+ const PARTICLE = "ADP";
156
+ const NOMINAL_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN"]);
157
+
158
+ /** The first word after the particle, past adverbs (順次) and a bound honorific prefix (ご連絡). */
159
+ const headAfter = (tokens: readonly Token[], at: number): number =>
160
+ tokens.findIndex((token, index) => index > at && !isSpace(token) && !SKIPPED_POS.has(token.pos) && token.features?.["Bound"] !== "Yes");
161
+
162
+ /**
163
+ * The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself: followed by an object
164
+ * marker or a light verb (受付を開始, 開始します), not a noun being compared (昨年より販売が増えた).
165
+ */
166
+ const isOriginHead = (tokens: readonly Token[], head: number, heads: ReadonlySet<string>, objectMarkers: ReadonlySet<string>): boolean => {
167
+ const token = tokens[head];
168
+ if (token === undefined || !isListed(heads, token)) return false;
169
+ if (!NOMINAL_POS.has(token.pos)) return true;
170
+ const next = nextWord(tokens, head);
171
+ return next !== undefined && (objectMarkers.has(next.surface) || next.features?.["VerbType"] === "Light");
172
+ };
173
+
174
+ type ParticleWords = { readonly particles: ReadonlySet<string>; readonly heads: ReadonlySet<string>; readonly objectMarkers: ReadonlySet<string> };
175
+
176
+ const originsIn = (sentence: Sentence, words: ParticleWords): Token[] => {
177
+ const tokens = sentence.tokens ?? [];
178
+ return ownTokens(sentence)
179
+ .filter(
180
+ ({ token, at }) =>
181
+ token.pos === PARTICLE && words.particles.has(token.surface) && isOriginHead(tokens, headAfter(tokens, at), words.heads, words.objectMarkers),
182
+ )
183
+ .map(({ token }) => token);
184
+ };
185
+
186
+ const particleWords = (doc: ProseDocument, options: DetectorOptions): ParticleWords => ({
187
+ particles: patternsOf(options.lexicon),
188
+ heads: patternsOf(doc.lexicons["origin-head"], "head"),
189
+ objectMarkers: patternsOf(doc.lexicons["origin-head"], "object"),
190
+ });
191
+
192
+ /** The lexicon's particle where it marks a starting point (4月1日より開始), which the lexicon's rewrite says instead. */
193
+ export const originParticle: Detector = (doc, options): Finding[] => {
194
+ const words = particleWords(doc, options);
195
+ const rewrites = new Map((options.lexicon ?? []).map((entry) => [entry.pattern, entry.rewrite ?? ""]));
196
+ return doc.sentences.flatMap((sentence) =>
197
+ originsIn(sentence, words).map((token) => findingAt(sentence, token.span.start, { word: token.surface, suggestion: rewrites.get(token.surface) ?? "" })),
198
+ );
199
+ };