chaffjs 0.25.0 → 0.27.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 (468) hide show
  1. package/README.md +179 -42
  2. package/dist/adapter-import.d.ts +2 -0
  3. package/dist/adapter-import.d.ts.map +1 -0
  4. package/dist/adapter-import.js +11 -0
  5. package/dist/adapter-import.js.map +1 -0
  6. package/dist/adapter-load.d.ts.map +1 -1
  7. package/dist/adapter-load.js +2 -9
  8. package/dist/adapter-load.js.map +1 -1
  9. package/dist/ai-score/human-shares.d.ts +4 -0
  10. package/dist/ai-score/human-shares.d.ts.map +1 -0
  11. package/dist/ai-score/human-shares.js +205 -0
  12. package/dist/ai-score/human-shares.js.map +1 -0
  13. package/dist/ai-score/lint-line.d.ts +4 -0
  14. package/dist/ai-score/lint-line.d.ts.map +1 -0
  15. package/dist/ai-score/lint-line.js +9 -0
  16. package/dist/ai-score/lint-line.js.map +1 -0
  17. package/dist/ai-score/of-document.d.ts +7 -0
  18. package/dist/ai-score/of-document.d.ts.map +1 -0
  19. package/dist/ai-score/of-document.js +40 -0
  20. package/dist/ai-score/of-document.js.map +1 -0
  21. package/dist/ai-score/render.d.ts +24 -0
  22. package/dist/ai-score/render.d.ts.map +1 -0
  23. package/dist/ai-score/render.js +100 -0
  24. package/dist/ai-score/render.js.map +1 -0
  25. package/dist/ai-score/score.d.ts +77 -0
  26. package/dist/ai-score/score.d.ts.map +1 -0
  27. package/dist/ai-score/score.js +52 -0
  28. package/dist/ai-score/score.js.map +1 -0
  29. package/dist/ai-score/signals.d.ts +19 -0
  30. package/dist/ai-score/signals.d.ts.map +1 -0
  31. package/dist/ai-score/signals.js +30 -0
  32. package/dist/ai-score/signals.js.map +1 -0
  33. package/dist/ai-score/text.d.ts +37 -0
  34. package/dist/ai-score/text.d.ts.map +1 -0
  35. package/dist/ai-score/text.js +59 -0
  36. package/dist/ai-score/text.js.map +1 -0
  37. package/dist/ai-score/view.d.ts +5 -0
  38. package/dist/ai-score/view.d.ts.map +1 -0
  39. package/dist/ai-score/view.js +19 -0
  40. package/dist/ai-score/view.js.map +1 -0
  41. package/dist/backends/anthropic.d.ts +0 -1
  42. package/dist/backends/anthropic.d.ts.map +1 -1
  43. package/dist/backends/anthropic.js +0 -1
  44. package/dist/backends/anthropic.js.map +1 -1
  45. package/dist/backends/default-model.d.ts +3 -0
  46. package/dist/backends/default-model.d.ts.map +1 -0
  47. package/dist/backends/default-model.js +5 -0
  48. package/dist/backends/default-model.js.map +1 -0
  49. package/dist/backends/openai.d.ts +0 -1
  50. package/dist/backends/openai.d.ts.map +1 -1
  51. package/dist/backends/openai.js +0 -1
  52. package/dist/backends/openai.js.map +1 -1
  53. package/dist/browser/adapter-import.d.ts +2 -0
  54. package/dist/browser/adapter-import.d.ts.map +1 -0
  55. package/dist/browser/adapter-import.js +17 -0
  56. package/dist/browser/adapter-import.js.map +1 -0
  57. package/dist/browser/bounded-match.d.ts +3 -0
  58. package/dist/browser/bounded-match.d.ts.map +1 -0
  59. package/dist/browser/bounded-match.js +4 -0
  60. package/dist/browser/bounded-match.js.map +1 -0
  61. package/dist/browser/check-text.d.ts +43 -0
  62. package/dist/browser/check-text.d.ts.map +1 -0
  63. package/dist/browser/check-text.js +50 -0
  64. package/dist/browser/check-text.js.map +1 -0
  65. package/dist/browser/detectors.d.ts +4 -0
  66. package/dist/browser/detectors.d.ts.map +1 -0
  67. package/dist/browser/detectors.js +22 -0
  68. package/dist/browser/detectors.js.map +1 -0
  69. package/dist/browser/package-files.d.ts +5 -0
  70. package/dist/browser/package-files.d.ts.map +1 -0
  71. package/dist/browser/package-files.js +8 -0
  72. package/dist/browser/package-files.js.map +1 -0
  73. package/dist/browser/path.d.ts +9 -0
  74. package/dist/browser/path.d.ts.map +1 -0
  75. package/dist/browser/path.js +70 -0
  76. package/dist/browser/path.js.map +1 -0
  77. package/dist/browser/working-dir.d.ts +2 -0
  78. package/dist/browser/working-dir.d.ts.map +1 -0
  79. package/dist/browser/working-dir.js +3 -0
  80. package/dist/browser/working-dir.js.map +1 -0
  81. package/dist/browser-files.d.ts +21 -0
  82. package/dist/browser-files.d.ts.map +1 -0
  83. package/dist/browser-files.js +65 -0
  84. package/dist/browser-files.js.map +1 -0
  85. package/dist/browser.d.ts +9 -0
  86. package/dist/browser.d.ts.map +1 -0
  87. package/dist/browser.js +13 -0
  88. package/dist/browser.js.map +1 -0
  89. package/dist/capitals-with-small.d.ts +8 -0
  90. package/dist/capitals-with-small.d.ts.map +1 -0
  91. package/dist/capitals-with-small.js +20 -0
  92. package/dist/capitals-with-small.js.map +1 -0
  93. package/dist/cli-text.d.ts.map +1 -1
  94. package/dist/cli-text.js +2 -0
  95. package/dist/cli-text.js.map +1 -1
  96. package/dist/cli.d.ts.map +1 -1
  97. package/dist/cli.js +4 -1
  98. package/dist/cli.js.map +1 -1
  99. package/dist/commands/ai-score.d.ts +8 -0
  100. package/dist/commands/ai-score.d.ts.map +1 -0
  101. package/dist/commands/ai-score.js +59 -0
  102. package/dist/commands/ai-score.js.map +1 -0
  103. package/dist/commands/document-commands.d.ts.map +1 -1
  104. package/dist/commands/document-commands.js +2 -0
  105. package/dist/commands/document-commands.js.map +1 -1
  106. package/dist/config/by-path-problems.d.ts +5 -0
  107. package/dist/config/by-path-problems.d.ts.map +1 -0
  108. package/dist/config/by-path-problems.js +11 -0
  109. package/dist/config/by-path-problems.js.map +1 -0
  110. package/dist/config/load.d.ts +9 -2
  111. package/dist/config/load.d.ts.map +1 -1
  112. package/dist/config/load.js +21 -8
  113. package/dist/config/load.js.map +1 -1
  114. package/dist/config/read.d.ts +2 -0
  115. package/dist/config/read.d.ts.map +1 -1
  116. package/dist/config/read.js +4 -1
  117. package/dist/config/read.js.map +1 -1
  118. package/dist/config/warnings.d.ts +1 -1
  119. package/dist/config/warnings.d.ts.map +1 -1
  120. package/dist/config/warnings.js +3 -1
  121. package/dist/config/warnings.js.map +1 -1
  122. package/dist/config/working-dir.d.ts +2 -0
  123. package/dist/config/working-dir.d.ts.map +1 -0
  124. package/dist/config/working-dir.js +3 -0
  125. package/dist/config/working-dir.js.map +1 -0
  126. package/dist/custom/bounded-match.d.ts +1 -12
  127. package/dist/custom/bounded-match.d.ts.map +1 -1
  128. package/dist/custom/bounded-match.js +1 -11
  129. package/dist/custom/bounded-match.js.map +1 -1
  130. package/dist/custom/pattern-timeout.d.ts +13 -0
  131. package/dist/custom/pattern-timeout.d.ts.map +1 -0
  132. package/dist/custom/pattern-timeout.js +13 -0
  133. package/dist/custom/pattern-timeout.js.map +1 -0
  134. package/dist/derived/relative-weekdays.d.ts +16 -0
  135. package/dist/derived/relative-weekdays.d.ts.map +1 -0
  136. package/dist/derived/relative-weekdays.js +52 -0
  137. package/dist/derived/relative-weekdays.js.map +1 -0
  138. package/dist/detect.d.ts.map +1 -1
  139. package/dist/detect.js +4 -5
  140. package/dist/detect.js.map +1 -1
  141. package/dist/detectors/change-rate.d.ts +3 -0
  142. package/dist/detectors/change-rate.d.ts.map +1 -0
  143. package/dist/detectors/change-rate.js +102 -0
  144. package/dist/detectors/change-rate.js.map +1 -0
  145. package/dist/detectors/date-year.d.ts +10 -2
  146. package/dist/detectors/date-year.d.ts.map +1 -1
  147. package/dist/detectors/date-year.js +44 -8
  148. package/dist/detectors/date-year.js.map +1 -1
  149. package/dist/detectors/defined-term-form.d.ts +5 -0
  150. package/dist/detectors/defined-term-form.d.ts.map +1 -1
  151. package/dist/detectors/defined-term-form.js +22 -5
  152. package/dist/detectors/defined-term-form.js.map +1 -1
  153. package/dist/detectors/derived-numbers.d.ts.map +1 -1
  154. package/dist/detectors/derived-numbers.js +3 -7
  155. package/dist/detectors/derived-numbers.js.map +1 -1
  156. package/dist/detectors/document-date.d.ts +4 -0
  157. package/dist/detectors/document-date.d.ts.map +1 -0
  158. package/dist/detectors/document-date.js +12 -0
  159. package/dist/detectors/document-date.js.map +1 -0
  160. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  161. package/dist/detectors/orthographic-variant.js +60 -28
  162. package/dist/detectors/orthographic-variant.js.map +1 -1
  163. package/dist/detectors/party-role-name.d.ts +3 -0
  164. package/dist/detectors/party-role-name.d.ts.map +1 -0
  165. package/dist/detectors/party-role-name.js +35 -0
  166. package/dist/detectors/party-role-name.js.map +1 -0
  167. package/dist/detectors/quote-without-source.d.ts +17 -0
  168. package/dist/detectors/quote-without-source.d.ts.map +1 -0
  169. package/dist/detectors/quote-without-source.js +64 -0
  170. package/dist/detectors/quote-without-source.js.map +1 -0
  171. package/dist/detectors/reference-text.js +1 -1
  172. package/dist/detectors/reference-text.js.map +1 -1
  173. package/dist/detectors/registry/change-rate.d.ts +3 -0
  174. package/dist/detectors/registry/change-rate.d.ts.map +1 -0
  175. package/dist/detectors/registry/change-rate.js +3 -0
  176. package/dist/detectors/registry/change-rate.js.map +1 -0
  177. package/dist/detectors/registry/party-role-name.d.ts +3 -0
  178. package/dist/detectors/registry/party-role-name.d.ts.map +1 -0
  179. package/dist/detectors/registry/party-role-name.js +3 -0
  180. package/dist/detectors/registry/party-role-name.js.map +1 -0
  181. package/dist/detectors/registry/quote-without-source.d.ts +3 -0
  182. package/dist/detectors/registry/quote-without-source.d.ts.map +1 -0
  183. package/dist/detectors/registry/quote-without-source.js +3 -0
  184. package/dist/detectors/registry/quote-without-source.js.map +1 -0
  185. package/dist/detectors/registry/vague-deadline.d.ts +3 -0
  186. package/dist/detectors/registry/vague-deadline.d.ts.map +1 -0
  187. package/dist/detectors/registry/vague-deadline.js +3 -0
  188. package/dist/detectors/registry/vague-deadline.js.map +1 -0
  189. package/dist/detectors/registry/version-mismatch.d.ts +3 -0
  190. package/dist/detectors/registry/version-mismatch.d.ts.map +1 -0
  191. package/dist/detectors/registry/version-mismatch.js +3 -0
  192. package/dist/detectors/registry/version-mismatch.js.map +1 -0
  193. package/dist/detectors/relative-dates.d.ts.map +1 -1
  194. package/dist/detectors/relative-dates.js +3 -1
  195. package/dist/detectors/relative-dates.js.map +1 -1
  196. package/dist/detectors/relative-day-words.d.ts +4 -0
  197. package/dist/detectors/relative-day-words.d.ts.map +1 -0
  198. package/dist/detectors/relative-day-words.js +105 -0
  199. package/dist/detectors/relative-day-words.js.map +1 -0
  200. package/dist/detectors/run-in-label.d.ts +7 -0
  201. package/dist/detectors/run-in-label.d.ts.map +1 -0
  202. package/dist/detectors/run-in-label.js +53 -0
  203. package/dist/detectors/run-in-label.js.map +1 -0
  204. package/dist/detectors/sentence-counts.d.ts +3 -1
  205. package/dist/detectors/sentence-counts.d.ts.map +1 -1
  206. package/dist/detectors/sentence-counts.js +11 -4
  207. package/dist/detectors/sentence-counts.js.map +1 -1
  208. package/dist/detectors/sentence-ending.d.ts +1 -1
  209. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  210. package/dist/detectors/sentence-ending.js +2 -1
  211. package/dist/detectors/sentence-ending.js.map +1 -1
  212. package/dist/detectors/structure-tree.d.ts +8 -2
  213. package/dist/detectors/structure-tree.d.ts.map +1 -1
  214. package/dist/detectors/structure-tree.js +86 -5
  215. package/dist/detectors/structure-tree.js.map +1 -1
  216. package/dist/detectors/unbalanced-bracket.d.ts +1 -1
  217. package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
  218. package/dist/detectors/unbalanced-bracket.js +23 -11
  219. package/dist/detectors/unbalanced-bracket.js.map +1 -1
  220. package/dist/detectors/vague-deadline.d.ts +25 -0
  221. package/dist/detectors/vague-deadline.d.ts.map +1 -0
  222. package/dist/detectors/vague-deadline.js +97 -0
  223. package/dist/detectors/vague-deadline.js.map +1 -0
  224. package/dist/detectors/version-mismatch.d.ts +3 -0
  225. package/dist/detectors/version-mismatch.d.ts.map +1 -0
  226. package/dist/detectors/version-mismatch.js +19 -0
  227. package/dist/detectors/version-mismatch.js.map +1 -0
  228. package/dist/furigana.d.ts +7 -0
  229. package/dist/furigana.d.ts.map +1 -0
  230. package/dist/furigana.js +16 -0
  231. package/dist/furigana.js.map +1 -0
  232. package/dist/genre-load.d.ts.map +1 -1
  233. package/dist/genre-load.js +5 -7
  234. package/dist/genre-load.js.map +1 -1
  235. package/dist/grade/ai-score.d.ts +16 -0
  236. package/dist/grade/ai-score.d.ts.map +1 -0
  237. package/dist/grade/ai-score.js +17 -0
  238. package/dist/grade/ai-score.js.map +1 -0
  239. package/dist/grade/finding.d.ts +6 -0
  240. package/dist/grade/finding.d.ts.map +1 -0
  241. package/dist/grade/finding.js +13 -0
  242. package/dist/grade/finding.js.map +1 -0
  243. package/dist/grade/grade-item.d.ts.map +1 -1
  244. package/dist/grade/grade-item.js +4 -11
  245. package/dist/grade/grade-item.js.map +1 -1
  246. package/dist/grade/render-variants.d.ts.map +1 -1
  247. package/dist/grade/render-variants.js +3 -0
  248. package/dist/grade/render-variants.js.map +1 -1
  249. package/dist/grade/render.d.ts.map +1 -1
  250. package/dist/grade/render.js +1 -0
  251. package/dist/grade/render.js.map +1 -1
  252. package/dist/grade/result.d.ts +3 -0
  253. package/dist/grade/result.d.ts.map +1 -1
  254. package/dist/grade/results-read.d.ts.map +1 -1
  255. package/dist/grade/results-read.js +13 -2
  256. package/dist/grade/results-read.js.map +1 -1
  257. package/dist/grade/summary.d.ts +3 -0
  258. package/dist/grade/summary.d.ts.map +1 -1
  259. package/dist/grade/summary.js +6 -0
  260. package/dist/grade/summary.js.map +1 -1
  261. package/dist/grade/text.d.ts +2 -0
  262. package/dist/grade/text.d.ts.map +1 -1
  263. package/dist/grade/text.js +2 -0
  264. package/dist/grade/text.js.map +1 -1
  265. package/dist/grade/variants-text.d.ts +3 -0
  266. package/dist/grade/variants-text.d.ts.map +1 -1
  267. package/dist/grade/variants-text.js +4 -0
  268. package/dist/grade/variants-text.js.map +1 -1
  269. package/dist/grade/variants.d.ts +3 -0
  270. package/dist/grade/variants.d.ts.map +1 -1
  271. package/dist/grade/variants.js +1 -0
  272. package/dist/grade/variants.js.map +1 -1
  273. package/dist/grade-api.d.ts +1 -0
  274. package/dist/grade-api.d.ts.map +1 -1
  275. package/dist/grade-api.js +2 -1
  276. package/dist/grade-api.js.map +1 -1
  277. package/dist/judge.d.ts +0 -1
  278. package/dist/judge.d.ts.map +1 -1
  279. package/dist/judge.js +0 -1
  280. package/dist/judge.js.map +1 -1
  281. package/dist/known-genres.js +2 -2
  282. package/dist/known-genres.js.map +1 -1
  283. package/dist/package-files.d.ts +6 -0
  284. package/dist/package-files.d.ts.map +1 -0
  285. package/dist/package-files.js +11 -0
  286. package/dist/package-files.js.map +1 -0
  287. package/dist/profile/load.d.ts.map +1 -1
  288. package/dist/profile/load.js +5 -6
  289. package/dist/profile/load.js.map +1 -1
  290. package/dist/rule-load.d.ts.map +1 -1
  291. package/dist/rule-load.js +5 -6
  292. package/dist/rule-load.js.map +1 -1
  293. package/dist/rule-offs.js +3 -3
  294. package/dist/rule-offs.js.map +1 -1
  295. package/dist/run.js +1 -1
  296. package/dist/run.js.map +1 -1
  297. package/dist/structure/change-rate.d.ts +33 -0
  298. package/dist/structure/change-rate.d.ts.map +1 -0
  299. package/dist/structure/change-rate.js +71 -0
  300. package/dist/structure/change-rate.js.map +1 -0
  301. package/dist/structure/date-range.d.ts +4 -0
  302. package/dist/structure/date-range.d.ts.map +1 -1
  303. package/dist/structure/date-range.js +23 -3
  304. package/dist/structure/date-range.js.map +1 -1
  305. package/dist/structure/issues.d.ts +17 -1
  306. package/dist/structure/issues.d.ts.map +1 -1
  307. package/dist/structure/issues.js +54 -5
  308. package/dist/structure/issues.js.map +1 -1
  309. package/dist/structure/list-numbering.d.ts +17 -0
  310. package/dist/structure/list-numbering.d.ts.map +1 -0
  311. package/dist/structure/list-numbering.js +27 -0
  312. package/dist/structure/list-numbering.js.map +1 -0
  313. package/dist/structure/party-role.d.ts +30 -0
  314. package/dist/structure/party-role.d.ts.map +1 -0
  315. package/dist/structure/party-role.js +58 -0
  316. package/dist/structure/party-role.js.map +1 -0
  317. package/dist/structure/percent-sum-prose.d.ts +7 -0
  318. package/dist/structure/percent-sum-prose.d.ts.map +1 -0
  319. package/dist/structure/percent-sum-prose.js +44 -0
  320. package/dist/structure/percent-sum-prose.js.map +1 -0
  321. package/dist/structure/prose-total.d.ts +28 -0
  322. package/dist/structure/prose-total.d.ts.map +1 -0
  323. package/dist/structure/prose-total.js +99 -0
  324. package/dist/structure/prose-total.js.map +1 -0
  325. package/dist/structure/reference-text.d.ts +5 -0
  326. package/dist/structure/reference-text.d.ts.map +1 -1
  327. package/dist/structure/reference-text.js +17 -4
  328. package/dist/structure/reference-text.js.map +1 -1
  329. package/dist/structure/term-prefix.d.ts +18 -0
  330. package/dist/structure/term-prefix.d.ts.map +1 -0
  331. package/dist/structure/term-prefix.js +40 -0
  332. package/dist/structure/term-prefix.js.map +1 -0
  333. package/dist/structure/total.d.ts +2 -0
  334. package/dist/structure/total.d.ts.map +1 -1
  335. package/dist/structure/total.js +2 -0
  336. package/dist/structure/total.js.map +1 -1
  337. package/dist/structure/version-mismatch.d.ts +23 -0
  338. package/dist/structure/version-mismatch.d.ts.map +1 -0
  339. package/dist/structure/version-mismatch.js +74 -0
  340. package/dist/structure/version-mismatch.js.map +1 -0
  341. package/dist/structure/weekday.d.ts +21 -2
  342. package/dist/structure/weekday.d.ts.map +1 -1
  343. package/dist/structure/weekday.js +70 -8
  344. package/dist/structure/weekday.js.map +1 -1
  345. package/dist/structure-shape/baseline-load.d.ts.map +1 -1
  346. package/dist/structure-shape/baseline-load.js +4 -5
  347. package/dist/structure-shape/baseline-load.js.map +1 -1
  348. package/dist/style-load.d.ts.map +1 -1
  349. package/dist/style-load.js +5 -6
  350. package/dist/style-load.js.map +1 -1
  351. package/package.json +24 -3
  352. package/rules/agreement-slip.yaml +2 -2
  353. package/rules/change-rate-mismatch.yaml +44 -0
  354. package/rules/cliche.yaml +28 -8
  355. package/rules/date-weekday-mismatch.yaml +4 -4
  356. package/rules/date-without-year.yaml +28 -3
  357. package/rules/defined-term-form.yaml +9 -6
  358. package/rules/demonstrative-opener-run.yaml +24 -7
  359. package/rules/max-sentence-length.yaml +1 -0
  360. package/rules/no-mixed-desumasu.yaml +2 -2
  361. package/rules/numbering-gap.yaml +4 -4
  362. package/rules/one-sentence-paragraph-run.yaml +1 -0
  363. package/rules/orthographic-variant.yaml +3 -3
  364. package/rules/party-role-name.yaml +94 -0
  365. package/rules/percent-sum-mismatch.yaml +4 -4
  366. package/rules/quote-without-source.yaml +61 -0
  367. package/rules/relative-date-mismatch.yaml +4 -4
  368. package/rules/total-mismatch.yaml +8 -8
  369. package/rules/undefined-acronym.yaml +1 -0
  370. package/rules/vague-deadline.yaml +95 -0
  371. package/rules/version-mismatch.yaml +77 -0
  372. package/rules/weasel-word.yaml +25 -7
  373. package/skills/chaff/SKILL.md +12 -2
  374. package/src/adapter-import.ts +3 -0
  375. package/src/adapter-load.ts +2 -1
  376. package/src/ai-score/human-shares.ts +207 -0
  377. package/src/ai-score/lint-line.ts +10 -0
  378. package/src/ai-score/of-document.ts +46 -0
  379. package/src/ai-score/render.ts +124 -0
  380. package/src/ai-score/score.ts +121 -0
  381. package/src/ai-score/signals.ts +39 -0
  382. package/src/ai-score/text.ts +103 -0
  383. package/src/ai-score/view.ts +21 -0
  384. package/src/backends/anthropic.ts +0 -2
  385. package/src/backends/default-model.ts +9 -0
  386. package/src/backends/openai.ts +0 -2
  387. package/src/browser/adapter-import.ts +19 -0
  388. package/src/browser/bounded-match.ts +9 -0
  389. package/src/browser/check-text.ts +106 -0
  390. package/src/browser/detectors.ts +40 -0
  391. package/src/browser/import-meta-glob.d.ts +5 -0
  392. package/src/browser/package-files.ts +13 -0
  393. package/src/browser/path.ts +73 -0
  394. package/src/browser/working-dir.ts +3 -0
  395. package/src/browser-files.ts +87 -0
  396. package/src/browser.ts +19 -0
  397. package/src/capitals-with-small.ts +23 -0
  398. package/src/cli-text.ts +2 -0
  399. package/src/cli.ts +4 -1
  400. package/src/commands/ai-score.ts +77 -0
  401. package/src/commands/document-commands.ts +2 -0
  402. package/src/config/by-path-problems.ts +17 -0
  403. package/src/config/load.ts +21 -8
  404. package/src/config/read.ts +5 -1
  405. package/src/config/warnings.ts +3 -1
  406. package/src/config/working-dir.ts +3 -0
  407. package/src/custom/bounded-match.ts +1 -16
  408. package/src/custom/pattern-timeout.ts +17 -0
  409. package/src/derived/relative-weekdays.ts +65 -0
  410. package/src/detect.ts +4 -5
  411. package/src/detectors/change-rate.ts +122 -0
  412. package/src/detectors/date-year.ts +51 -12
  413. package/src/detectors/defined-term-form.ts +28 -6
  414. package/src/detectors/derived-numbers.ts +3 -11
  415. package/src/detectors/document-date.ts +18 -0
  416. package/src/detectors/orthographic-variant.ts +67 -22
  417. package/src/detectors/party-role-name.ts +44 -0
  418. package/src/detectors/quote-without-source.ts +90 -0
  419. package/src/detectors/reference-text.ts +1 -1
  420. package/src/detectors/registry/change-rate.ts +4 -0
  421. package/src/detectors/registry/party-role-name.ts +4 -0
  422. package/src/detectors/registry/quote-without-source.ts +4 -0
  423. package/src/detectors/registry/vague-deadline.ts +4 -0
  424. package/src/detectors/registry/version-mismatch.ts +4 -0
  425. package/src/detectors/relative-dates.ts +3 -1
  426. package/src/detectors/relative-day-words.ts +126 -0
  427. package/src/detectors/run-in-label.ts +60 -0
  428. package/src/detectors/sentence-counts.ts +13 -4
  429. package/src/detectors/sentence-ending.ts +3 -2
  430. package/src/detectors/structure-tree.ts +104 -10
  431. package/src/detectors/unbalanced-bracket.ts +25 -10
  432. package/src/detectors/vague-deadline.ts +132 -0
  433. package/src/detectors/version-mismatch.ts +23 -0
  434. package/src/furigana.ts +20 -0
  435. package/src/genre-load.ts +5 -7
  436. package/src/grade/ai-score.ts +32 -0
  437. package/src/grade/finding.ts +16 -0
  438. package/src/grade/grade-item.ts +4 -13
  439. package/src/grade/render-variants.ts +5 -0
  440. package/src/grade/render.ts +1 -0
  441. package/src/grade/result.ts +3 -0
  442. package/src/grade/results-read.ts +17 -2
  443. package/src/grade/summary.ts +9 -0
  444. package/src/grade/text.ts +6 -0
  445. package/src/grade/variants-text.ts +7 -0
  446. package/src/grade/variants.ts +4 -0
  447. package/src/grade-api.ts +3 -1
  448. package/src/judge.ts +0 -2
  449. package/src/known-genres.ts +2 -2
  450. package/src/package-files.ts +15 -0
  451. package/src/profile/load.ts +5 -6
  452. package/src/rule-load.ts +5 -6
  453. package/src/rule-offs.ts +3 -3
  454. package/src/run.ts +1 -1
  455. package/src/structure/change-rate.ts +103 -0
  456. package/src/structure/date-range.ts +28 -3
  457. package/src/structure/issues.ts +61 -6
  458. package/src/structure/list-numbering.ts +34 -0
  459. package/src/structure/party-role.ts +94 -0
  460. package/src/structure/percent-sum-prose.ts +54 -0
  461. package/src/structure/prose-total.ts +135 -0
  462. package/src/structure/reference-text.ts +17 -4
  463. package/src/structure/term-prefix.ts +54 -0
  464. package/src/structure/total.ts +4 -0
  465. package/src/structure/version-mismatch.ts +103 -0
  466. package/src/structure/weekday.ts +83 -6
  467. package/src/structure-shape/baseline-load.ts +4 -5
  468. package/src/style-load.ts +5 -6
@@ -1,7 +1,7 @@
1
1
  import { dirname } from "node:path";
2
- import { readYamlFile } from "./yaml-file.ts";
2
+ import { workingDir } from "./working-dir.ts";
3
3
  import { isLevel } from "../levels.ts";
4
- import { defaultModel } from "../judge.ts";
4
+ import { defaultModel } from "../backends/default-model.ts";
5
5
  import { isBackend, type BackendName } from "../backends/types.ts";
6
6
  import type { Level } from "../plugin.ts";
7
7
  import type { PathRule } from "./by-path.ts";
@@ -40,6 +40,8 @@ export type Config = {
40
40
  readonly include?: readonly string[];
41
41
  /** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
42
42
  readonly byPath: readonly PathRule[];
43
+ /** by_path entries that could not be read (a map instead of a list, an entry without files), as written. */
44
+ readonly unreadableByPath?: readonly string[];
43
45
  readonly baseDir: string;
44
46
  /** Options set on rules that take them: { rule id: { option: value } }, as written. rule-options.ts checks them against the rules. */
45
47
  readonly options?: Readonly<Record<string, unknown>>;
@@ -95,7 +97,7 @@ export const EMPTY: Config = {
95
97
  names: [],
96
98
  unreadableNames: [],
97
99
  byPath: [],
98
- baseDir: process.cwd(),
100
+ baseDir: workingDir(),
99
101
  };
100
102
 
101
103
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
@@ -169,15 +171,25 @@ const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable:
169
171
  };
170
172
  };
171
173
 
172
- const byPathOf = (raw: unknown): PathRule[] => (Array.isArray(raw) ? raw.map(toPathRule).filter((rule) => rule !== undefined) : []);
174
+ /** by_path は files を持つ項目の並び。並びでない値と、読めない項目は読めないものとして返す。黙って捨てると、書いた人は効いたつもりでいる。 */
175
+ export const byPathOf = (raw: unknown): { readonly rules: PathRule[]; readonly unreadable: string[] } => {
176
+ if (raw === undefined || raw === null) return { rules: [], unreadable: [] };
177
+ if (!Array.isArray(raw)) return { rules: [], unreadable: [printed(raw)] };
178
+ const entries: unknown[] = raw;
179
+ const read = entries.map((entry) => ({ entry, rule: toPathRule(entry) }));
180
+ return {
181
+ rules: read.flatMap(({ rule }) => (rule === undefined ? [] : [rule])),
182
+ unreadable: read.filter(({ rule }) => rule === undefined).map(({ entry }) => printed(entry)),
183
+ };
184
+ };
173
185
 
174
- /** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
175
- export const loadConfig = (path: string): Config => {
176
- const raw: unknown = readYamlFile(path);
186
+ /** 読んだ chaff.yaml の中身(raw)を設定にする。ファイルは読まない。path は raw を読んだ場所。 */
187
+ export const configOf = (raw: unknown, path: string): Config => {
177
188
  if (!isRecord(raw)) return { ...EMPTY, path };
178
189
  const declared: unknown = raw["ai_backend"];
179
190
  const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
180
191
  const names = namesOf(raw["names"]);
192
+ const byPath = byPathOf(raw["by_path"]);
181
193
  const options: unknown = raw["options"];
182
194
  return {
183
195
  genre: str(raw["genre"]),
@@ -197,7 +209,8 @@ export const loadConfig = (path: string): Config => {
197
209
  names: names.names,
198
210
  unreadableNames: names.unreadable,
199
211
  include: globsOf(raw["include"]),
200
- byPath: byPathOf(raw["by_path"]),
212
+ byPath: byPath.rules,
213
+ unreadableByPath: byPath.unreadable,
201
214
  baseDir: dirname(path),
202
215
  options: isRecord(options) ? options : {},
203
216
  unreadableOptions: options === undefined || options === null || isRecord(options) ? undefined : printed(options),
@@ -1,8 +1,12 @@
1
1
  import { existsSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { CONFIG_FILE, EMPTY, loadConfig, type Config } from "./load.ts";
3
+ import { CONFIG_FILE, EMPTY, configOf, type Config } from "./load.ts";
4
4
  import { withStyle } from "./style.ts";
5
+ import { readYamlFile } from "./yaml-file.ts";
5
6
  import { loadStyles } from "../style-load.ts";
6
7
 
8
+ /** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
9
+ export const loadConfig = (path: string): Config => configOf(readYamlFile(path), path);
10
+
7
11
  /** The chaff.yaml in dir with its style applied, or the defaults when there is none. */
8
12
  export const readConfigIn = (dir: string): Config => (existsSync(join(dir, CONFIG_FILE)) ? withStyle(loadConfig(join(dir, CONFIG_FILE)), loadStyles()) : EMPTY);
@@ -3,13 +3,15 @@ import type { RuleDefinition } from "../plugin.ts";
3
3
  import type { UiLanguage } from "../ui.ts";
4
4
  import { ruleProblems } from "./rule-problems.ts";
5
5
  import { nameProblems } from "./name-problems.ts";
6
+ import { byPathProblems } from "./by-path-problems.ts";
6
7
  import { configOptionProblems } from "./option-problems.ts";
7
8
  import { configGuideProblems } from "../genre-guide/of-config.ts";
8
9
 
9
- /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply, guides that cannot be read. Said before the results. */
10
+ /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names or by_path entries, options that do not apply, guides that cannot be read. Said before the results. */
10
11
  export const settingWarnings = (config: Config, rules: readonly RuleDefinition[], ui: UiLanguage): string[] => [
11
12
  ...ruleProblems(config, rules, ui),
12
13
  ...nameProblems(config, ui),
14
+ ...byPathProblems(config, ui),
13
15
  ...configOptionProblems(config, rules, ui),
14
16
  ...configGuideProblems(config, ui),
15
17
  ];
@@ -0,0 +1,3 @@
1
+ // The folder chaff runs in, where a run without chaff.yaml resolves paths. A build without a process puts its own module in this one's place.
2
+
3
+ export const workingDir = (): string => process.cwd();
@@ -1,25 +1,10 @@
1
1
  import { createContext, Script } from "node:vm";
2
+ import { PATTERN_BUDGET_MS, PatternTimeout, type TextMatches } from "./pattern-timeout.ts";
2
3
 
3
4
  // A team's regular expression, run with a time limit. regex-safety.ts refuses the shapes known to run away before a pattern
4
5
  // is used; no reading of the pattern can prove every shape safe, so this is the backstop. V8 can interrupt a regular
5
6
  // expression only through vm's timeout, so the matching runs in a vm context.
6
7
 
7
- /** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
8
- export const PATTERN_BUDGET_MS = 1000;
9
-
10
- /** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
11
- export class PatternTimeout extends Error {
12
- readonly budget_ms: number;
13
- constructor(budgetMs: number) {
14
- super(`the pattern did not finish within ${String(budgetMs)} ms`);
15
- this.name = "PatternTimeout";
16
- this.budget_ms = budgetMs;
17
- }
18
- }
19
-
20
- /** Each text's matches: where it starts and what it matched. Empty matches are left out. */
21
- export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
22
-
23
8
  const SCRIPT = new Script(
24
9
  `texts.map((text) => [...text.matchAll(new RegExp(source, flags))].filter((m) => m[0] !== "").map((m) => ({ index: m.index, text: m[0] })))`,
25
10
  );
@@ -0,0 +1,17 @@
1
+ // What a team's pattern may take, and what it is when it takes longer: the same wherever the pattern runs.
2
+
3
+ /** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
4
+ export const PATTERN_BUDGET_MS = 1000;
5
+
6
+ /** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
7
+ export class PatternTimeout extends Error {
8
+ readonly budget_ms: number;
9
+ constructor(budgetMs: number) {
10
+ super(`the pattern did not finish within ${String(budgetMs)} ms`);
11
+ this.name = "PatternTimeout";
12
+ this.budget_ms = budgetMs;
13
+ }
14
+ }
15
+
16
+ /** Each text's matches: where it starts and what it matched. Empty matches are left out. */
17
+ export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
@@ -0,0 +1,65 @@
1
+ // A date named by a weekday or a day relative to the document's date (「来週月曜(10月12日)」"next Monday (October 12)",
2
+ // 「明日(10月7日)」) that the absolute date beside it contradicts. Pure; the detector finds the words and the dates.
3
+ import type { Span } from "../plugin.ts";
4
+
5
+ /** 「来週月曜」"next Monday": weeks from the base's week (来週 is 1), and the weekday (Sunday is 0). */
6
+ export type WeekdayRelative = Span & { readonly kind: "weekday"; readonly weeks: number; readonly weekday: number };
7
+ /** 「明日」"tomorrow": days from the base. */
8
+ export type DayRelative = Span & { readonly kind: "day"; readonly days: number };
9
+ export type DayWord = WeekdayRelative | DayRelative;
10
+
11
+ const DAY_MS = 86_400_000;
12
+ const WEEK = 7;
13
+ const MONDAY = 1;
14
+ const FULL = /^(\d{4})-(\d{2})-(\d{2})$/u;
15
+ const MONTH_DAY = /^(\d{2})-(\d{2})$/u;
16
+
17
+ const timeOf = (value: string): number | undefined => {
18
+ const found = FULL.exec(value);
19
+ if (found === null) return undefined;
20
+ const [year, month, day] = [Number(found[1]), Number(found[2]), Number(found[3])];
21
+ const time = Date.UTC(year, month - 1, day);
22
+ const date = new Date(time);
23
+ return date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? time : undefined;
24
+ };
25
+
26
+ const valueOf = (time: number): string => new Date(time).toISOString().slice(0, "2026-10-06".length);
27
+
28
+ /**
29
+ * The days a weekday word can mean. Weeks are counted from Monday or from Sunday, and "next Monday" can also mean the
30
+ * coming Monday: every reading is allowed, so only a date that fits none of them is wrong.
31
+ */
32
+ const weekdayCandidates = (base: number, relative: WeekdayRelative): number[] => {
33
+ const baseDay = new Date(base).getUTCDay();
34
+ const fromMonday = base - ((baseDay - MONDAY + WEEK) % WEEK) * DAY_MS;
35
+ const fromSunday = base - baseDay * DAY_MS;
36
+ const inWeek = (weekStart: number, firstDay: number): number => weekStart + (relative.weeks * WEEK + ((relative.weekday - firstDay + WEEK) % WEEK)) * DAY_MS;
37
+ // "next Monday" and "this Monday" can mean the coming one; "last Monday" the one just past.
38
+ const coming = base + (((relative.weekday - baseDay + WEEK - 1) % WEEK) + 1) * DAY_MS;
39
+ const past = base - (((baseDay - relative.weekday + WEEK - 1) % WEEK) + 1) * DAY_MS;
40
+ const nearest = new Map([
41
+ [0, [coming]],
42
+ [1, [coming]],
43
+ [-1, [past]],
44
+ ]).get(relative.weeks);
45
+ return [inWeek(fromMonday, MONDAY), inWeek(fromSunday, 0), ...(nearest ?? [])];
46
+ };
47
+
48
+ const candidatesOf = (base: number, relative: DayWord): number[] =>
49
+ relative.kind === "day" ? [base + relative.days * DAY_MS] : weekdayCandidates(base, relative);
50
+
51
+ /** Whether the written date (2026-10-12, or 10-12 without its year) is one of the days the word can mean. */
52
+ const fits = (written: string, candidates: readonly number[]): boolean => {
53
+ const monthDay = MONTH_DAY.exec(written);
54
+ return candidates.some((time) => (monthDay === null ? valueOf(time) === written : valueOf(time).slice("2026-".length) === written));
55
+ };
56
+
57
+ /** The day the word means, as the message shows it: the first reading, written as the date was (with or without the year). */
58
+ export const relativeDayMismatch = (base: string, relative: DayWord, written: string): string | undefined => {
59
+ const baseTime = timeOf(base);
60
+ if (baseTime === undefined || (!FULL.test(written) && !MONTH_DAY.test(written))) return undefined;
61
+ const candidates = candidatesOf(baseTime, relative);
62
+ const [first] = candidates;
63
+ if (first === undefined || fits(written, candidates)) return undefined;
64
+ return MONTH_DAY.test(written) ? valueOf(first).slice("2026-".length) : valueOf(first);
65
+ };
package/src/detect.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { readFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { join } from "node:path";
4
2
  import { parse } from "yaml";
5
3
  import { japaneseRatio, latinRatio } from "./detect-language.ts";
6
4
  import { referenceListSpans } from "./reference-lists.ts";
7
5
  import { withoutSpans } from "./soft-break.ts";
8
6
  import { languageSample } from "./language-sample.ts";
7
+ import { PACKAGE_DIR, readText } from "./package-files.ts";
9
8
 
10
9
  export type LanguageGuess = { readonly language: string; readonly confidence: number; readonly from: string };
11
10
 
@@ -15,12 +14,12 @@ export type LanguageGuess = { readonly language: string; readonly confidence: nu
15
14
  */
16
15
  const JAPANESE_FLOOR = 0.15;
17
16
 
18
- const HEADINGS_FILE = join(dirname(fileURLToPath(import.meta.url)), "..", "reference-headings.yaml");
17
+ const HEADINGS_FILE = join(PACKAGE_DIR, "reference-headings.yaml");
19
18
 
20
19
  const isStringList = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string");
21
20
 
22
21
  const readHeadings = (): string[] => {
23
- const raw: unknown = parse(readFileSync(HEADINGS_FILE, "utf8"));
22
+ const raw: unknown = parse(readText(HEADINGS_FILE));
24
23
  const headings: unknown = typeof raw === "object" && raw !== null && "headings" in raw ? raw.headings : undefined;
25
24
  if (!isStringList(headings)) throw new Error(`${HEADINGS_FILE}: headings は文字列の並びであること`);
26
25
  return headings;
@@ -0,0 +1,122 @@
1
+ // change-rate-mismatch: the reading half. Builds the figures, the rates and the words of structure/change-rate.ts from the
2
+ // tree's quantities, the numbers counted with a word ("1,200 companies"), and the lexicons change-direction, change-base,
3
+ // percent-unit and amount-multiplier.
4
+ import type { Detector, Finding, ProseDocument, Span, StructureNode } from "../plugin.ts";
5
+ import { inDocumentOrder } from "../structure/issues.ts";
6
+ import { changeRateMismatches, type BaseMark, type Direction, type Figure, type Rate } from "../structure/change-rate.ts";
7
+ import { escapeRegExp } from "../orthography.ts";
8
+ import { quoteAt } from "./structure-tree.ts";
9
+
10
+ const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
11
+
12
+ const DIGITS = /\d[\d,]*(?:\.(\d+))?/u;
13
+ const DECIMAL_BASE = 10;
14
+ const LATIN = /^[A-Za-z]/u;
15
+
16
+ /** The step a number is written to: its last digit, times the magnitude the tree folded into the value (12億 is 12 × 1億). */
17
+ const stepOf = (written: string, value: number): number | undefined => {
18
+ const found = DIGITS.exec(written.normalize("NFKC"));
19
+ const shown = found === null ? Number.NaN : Number(found[0].replaceAll(",", ""));
20
+ if (!Number.isFinite(shown) || shown === 0) return undefined;
21
+ return (value / shown) * DECIMAL_BASE ** -(found?.[1]?.length ?? 0);
22
+ };
23
+
24
+ /** A word of magnitude right after the number ("$12 million"): the values compared must be written to the same one. */
25
+ const WORD_AFTER = /^\s+([A-Za-z]+)/u;
26
+ const WORDS_AFTER = /^\s+([A-Za-z]+)(?:\s+([A-Za-z]+))?/u;
27
+
28
+ const multiplierAfter = (source: string, end: number, multipliers: readonly string[]): string => {
29
+ const after = WORD_AFTER.exec(source.slice(end))?.[1]?.toLowerCase() ?? "";
30
+ return multipliers.includes(after) ? after : "";
31
+ };
32
+
33
+ const quantityNodes = (tree: StructureNode): StructureNode[] => inDocumentOrder(tree).filter((node) => node.kind === "quantity" || node.kind === "date");
34
+
35
+ const ratesOf = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[]): Rate[] =>
36
+ nodes.flatMap((node) => {
37
+ const unit = String(node.attrs["unit"] ?? "");
38
+ if (node.kind !== "quantity" || !units.includes(unit)) return [];
39
+ const decimals = DIGITS.exec(doc.source.slice(node.span.start, node.span.end).normalize("NFKC"))?.[1]?.length ?? 0;
40
+ return [{ start: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), decimals }];
41
+ });
42
+
43
+ const treeFigures = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[], multipliers: readonly string[]): Figure[] =>
44
+ nodes.flatMap((node) => {
45
+ const unit = String(node.attrs["unit"] ?? "");
46
+ const value = Number(node.attrs["value"]);
47
+ const step = stepOf(doc.source.slice(node.span.start, node.span.end), value);
48
+ if (node.kind !== "quantity" || units.includes(unit) || step === undefined) return [];
49
+ return [{ start: node.span.start, end: node.span.end, value, step, unit: `${unit}|${multiplierAfter(doc.source, node.span.end, multipliers)}` }];
50
+ });
51
+
52
+ /** A number with no unit the tree reads, counted with the word after it ("1,200 companies", "2.4 million users"). */
53
+ const COUNTED = /(?<![\w.,$€£¥])(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?(?=\s+[A-Za-z])/gu;
54
+ /** A year written alone ("2024 revenue"): a point in time, not a value. */
55
+ const YEAR = /^(?:1[89]|2[01])\d{2}$/u;
56
+
57
+ /** The unit a counted number is compared in: the word after it, with a word of magnitude before that word ("million users"). */
58
+ const countedUnit = (source: string, end: number, multipliers: readonly string[]): string | undefined => {
59
+ const words = WORDS_AFTER.exec(source.slice(end));
60
+ const first = words?.[1]?.toLowerCase() ?? "";
61
+ const multiplier = multipliers.includes(first) ? first : "";
62
+ const noun = multiplier === "" ? first : (words?.[2]?.toLowerCase() ?? "");
63
+ return LATIN.test(noun) ? `${noun}|${multiplier}` : undefined;
64
+ };
65
+
66
+ const countedFigures = (doc: ProseDocument, taken: readonly Span[], multipliers: readonly string[]): Figure[] =>
67
+ [...doc.source.matchAll(COUNTED)].flatMap((match) => {
68
+ const start = match.index;
69
+ const end = start + match[0].length;
70
+ const unit = countedUnit(doc.source, end, multipliers);
71
+ if (unit === undefined || taken.some((span) => span.start < end && start < span.end) || YEAR.test(match[0])) return [];
72
+ const decimals = match[2] ?? "";
73
+ const value = Number([(match[1] ?? "").replaceAll(",", ""), decimals].filter((part) => part !== "").join("."));
74
+ return [{ start, end, value, step: DECIMAL_BASE ** -decimals.length, unit }];
75
+ });
76
+
77
+ /** Where a word of the lexicon stands in the source: a Latin word as a whole word, anything else as written. */
78
+ const spansOf = (source: string, word: string): Span[] => {
79
+ const pattern = LATIN.test(word) ? new RegExp(`\\b${escapeRegExp(word)}\\b`, "giu") : new RegExp(escapeRegExp(word), "gu");
80
+ return [...source.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
81
+ };
82
+
83
+ const SIGNS: Readonly<Record<string, 1 | -1>> = { rise: 1, fall: -1 };
84
+
85
+ /** Phrases that hold a direction word but give no direction ("up to 25%" is a ceiling): lexicon change-direction-not. */
86
+ const directionsOf = (doc: ProseDocument): Direction[] => {
87
+ const not = patternsOf(doc, "change-direction-not").flatMap((phrase) => spansOf(doc.source, phrase));
88
+ return (doc.lexicons["change-direction"] ?? []).flatMap((entry) => {
89
+ const sign = SIGNS[entry.group ?? ""];
90
+ const spans = spansOf(doc.source, entry.pattern).filter((span) => !not.some((phrase) => phrase.start <= span.start && span.end <= phrase.end));
91
+ return sign === undefined ? [] : spans.map((span) => ({ ...span, sign }));
92
+ });
93
+ };
94
+
95
+ const marksOf = (doc: ProseDocument, lexicon: string): BaseMark[] =>
96
+ (doc.lexicons[lexicon] ?? []).flatMap((entry) =>
97
+ entry.position === undefined ? [] : spansOf(doc.source, entry.pattern).map((span) => ({ ...span, position: entry.position ?? "before" })),
98
+ );
99
+
100
+ export const changeRate: Detector = (doc): Finding[] => {
101
+ if (doc.structure === undefined) return [];
102
+ const nodes = quantityNodes(doc.structure);
103
+ const units = patternsOf(doc, "percent-unit");
104
+ const multipliers = patternsOf(doc, "amount-multiplier").map((word) => word.toLowerCase());
105
+ const taken = nodes.map((node) => node.span);
106
+ const text = {
107
+ sentences: doc.sentences.map((sentence) => sentence.span),
108
+ figures: [...treeFigures(doc, nodes, units, multipliers), ...countedFigures(doc, taken, multipliers)],
109
+ rates: ratesOf(doc, nodes, units),
110
+ directions: directionsOf(doc),
111
+ marks: marksOf(doc, "change-base"),
112
+ targets: marksOf(doc, "change-target"),
113
+ };
114
+ return changeRateMismatches(text).map((issue) => ({
115
+ rule: "change-rate-mismatch",
116
+ severity: "warning",
117
+ line: 0,
118
+ column: 0,
119
+ quote: quoteAt(doc.source, issue.offset),
120
+ values: { ...issue.values, offset: issue.offset },
121
+ }));
122
+ };
@@ -1,6 +1,6 @@
1
1
  // A date written without its year where the document's other dates carry one, or where the year cannot be read off
2
2
  // because the document's dates cross the turn of a year. Pure; reads the date nodes of the structure tree.
3
- import type { Detector, Finding, ProseDocument, StructureNode } from "../plugin.ts";
3
+ import type { Detector, Finding, LexiconEntry, ProseDocument, StructureNode } from "../plugin.ts";
4
4
  import { inDocumentOrder } from "../structure/issues.ts";
5
5
  import { quoteAt } from "./structure-tree.ts";
6
6
 
@@ -9,11 +9,19 @@ export type DatePoint = { readonly offset: number; readonly end: number; readonl
9
9
 
10
10
  export type YearGap = { readonly point: DatePoint; readonly kind: "minority" | "boundary"; readonly dated: number };
11
11
 
12
- /** The words just before a date that give its year in words (その年の, 同年, 毎年): the date has a year, or means every year. */
12
+ /**
13
+ * The words beside a date that give its year in words (その年の, 同年, 毎年; "of each year"): the date has a year, or means
14
+ * every year. An entry stands before the date unless it says `position: after`.
15
+ */
13
16
  export const YEAR_REFERENCE_LEXICON = "year-reference";
14
17
 
18
+ /** A year word as the lexicon gives it. `before`: the word ends within the reach before the date ("annually, by"). */
19
+ export type YearWord = { readonly word: string; readonly position?: "before" | "after" | undefined };
20
+
15
21
  const FULL = /^\d{4}-(\d{2})-\d{2}$/u;
16
22
  const MONTH_DAY = /^(\d{2})-\d{2}$/u;
23
+ /** The month of any date with one: 2026-12-01, 2026-12 or 12-01. */
24
+ const ANY_MONTH = /^(?:\d{4}-(\d{2})(?:-\d{2})?|(\d{2})-\d{2})$/u;
17
25
 
18
26
  /** How far before a date a year word may stand (「同年12月」「その年の12月」). */
19
27
  const YEAR_WORD_REACH = 6;
@@ -33,24 +41,53 @@ const sideOf = (month: number | undefined): "end" | "start" | undefined => {
33
41
  return YEAR_START.has(month) ? "start" : undefined;
34
42
  };
35
43
 
36
- /** A yearless date at one turn of the year while a dated one sits at the other: 1月15日 beside 2026年12月1日. */
37
- const crossesYear = (point: DatePoint, datedSides: ReadonlySet<string>): boolean => {
44
+ const anyMonthOf = (value: string): number | undefined => {
45
+ const found = ANY_MONTH.exec(value);
46
+ const month = found?.[1] ?? found?.[2];
47
+ return month === undefined ? undefined : Number(month);
48
+ };
49
+
50
+ const opposite = (side: "end" | "start"): "end" | "start" => (side === "end" ? "start" : "end");
51
+
52
+ /**
53
+ * A yearless date where the dates turn over the year: it sits at one turn, the date just before it at the other
54
+ * (12月10日 then 1月15日), and a dated one sits at that other turn too (2026年12月1日). A 2月15日 far from any 12月 is a
55
+ * day of the year, not a crossing.
56
+ */
57
+ const crossesYear = (point: DatePoint, previous: DatePoint | undefined, datedSides: ReadonlySet<string>): boolean => {
38
58
  const side = sideOf(monthOf(MONTH_DAY, point.value));
39
- return side !== undefined && datedSides.has(side === "end" ? "start" : "end");
59
+ if (side === undefined || !datedSides.has(opposite(side))) return false;
60
+ return previous !== undefined && sideOf(anyMonthOf(previous.value)) === opposite(side);
40
61
  };
41
62
 
42
63
  const lineStart = (source: string, offset: number): number => source.lastIndexOf("\n", offset - 1) + 1;
43
64
 
44
- const hasYearWord = (point: DatePoint, source: string, yearWords: readonly string[]): boolean => {
45
- const before = source.slice(Math.max(lineStart(source, point.offset), point.offset - YEAR_WORD_REACH), point.offset);
46
- return yearWords.some((word) => word !== "" && before.includes(word));
65
+ const lineEnd = (source: string, offset: number): number => {
66
+ const end = source.indexOf("\n", offset);
67
+ return end === -1 ? source.length : end;
68
+ };
69
+
70
+ /**
71
+ * A word without a position is found anywhere in the reach before the date (「同年12月」). One that says `before` may end
72
+ * there and start further back ("annually, by June 30"); one that says `after` starts within the reach after the date
73
+ * ("June 30 of each year").
74
+ */
75
+ const hasYearWord = (point: DatePoint, source: string, yearWords: readonly YearWord[]): boolean => {
76
+ const start = lineStart(source, point.offset);
77
+ const near = source.slice(Math.max(start, point.offset - YEAR_WORD_REACH), point.offset);
78
+ return yearWords.some(({ word, position }) => {
79
+ if (word === "") return false;
80
+ if (position === "after") return source.slice(point.end, Math.min(lineEnd(source, point.end), point.end + YEAR_WORD_REACH + word.length)).includes(word);
81
+ if (position === "before") return source.slice(Math.max(start, point.offset - YEAR_WORD_REACH - word.length), point.offset).includes(word);
82
+ return near.includes(word);
83
+ });
47
84
  };
48
85
 
49
86
  /**
50
87
  * A yearless date whose year the text gives anyway: a year word just before it (「同年12月28日」「その年の12月31日」), or,
51
88
  * earlier on the same line, a date with a year or with a year word (「2026年9月25日から10月7日まで」「その年の1月1日から12月31日まで」).
52
89
  */
53
- const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly string[]): boolean => {
90
+ const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly YearWord[]): boolean => {
54
91
  const start = lineStart(source, point.offset);
55
92
  return hasYearWord(point, source, yearWords) || anchors.some((other) => other.offset >= start && other.offset < point.offset);
56
93
  };
@@ -60,22 +97,24 @@ const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: stri
60
97
  * one is the odd one out. Otherwise writing the year once is the document's style, and only a date across the turn of a
61
98
  * year is reported. A date whose year the text gives in words or earlier on its line is never reported, nor counted.
62
99
  */
63
- export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly string[] = []): YearGap[] => {
100
+ export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly YearWord[] = []): YearGap[] => {
64
101
  const dated = points.filter((point) => FULL.test(point.value));
65
102
  const anchors = [...dated, ...points.filter((point) => hasYearWord(point, source, yearWords))];
66
103
  const yearless = points.filter((point) => MONTH_DAY.test(point.value) && !yearGiven(point, anchors, source, yearWords));
67
104
  const minority = dated.length >= 2 && yearless.length < dated.length;
68
105
  const datedSides = new Set(dated.flatMap((point) => sideOf(monthOf(FULL, point.value)) ?? []));
106
+ const previousOf = new Map(points.map((point, index) => [point, points[index - 1]]));
69
107
  return yearless.flatMap((point): YearGap[] => {
70
108
  if (minority) return [{ point, kind: "minority", dated: dated.length }];
71
- return crossesYear(point, datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
109
+ return crossesYear(point, previousOf.get(point), datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
72
110
  });
73
111
  };
74
112
 
75
113
  export const datePoints = (tree: StructureNode): DatePoint[] =>
76
114
  inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
77
115
 
78
- const yearWordsOf = (doc: ProseDocument): string[] => (doc.lexicons[YEAR_REFERENCE_LEXICON] ?? []).map((entry) => entry.pattern);
116
+ const yearWordsOf = (doc: ProseDocument): YearWord[] =>
117
+ (doc.lexicons[YEAR_REFERENCE_LEXICON] ?? []).map((entry: LexiconEntry) => ({ word: entry.pattern, position: entry.position }));
79
118
 
80
119
  export const dateWithoutYear: Detector = (doc: ProseDocument): Finding[] =>
81
120
  doc.structure === undefined
@@ -3,6 +3,7 @@
3
3
  // name is read from the tokens' parts of speech.
4
4
  import { definedTerms, usesOf, type BodyText, type DefinedTerm } from "../structure/definition-use.ts";
5
5
  import { escapeRegExp } from "../orthography.ts";
6
+ import { prefixGroupsOf, prefixVariants, variantUses } from "../structure/term-prefix.ts";
6
7
  import { quoteAt } from "./structure-tree.ts";
7
8
  import type { Detector, Finding, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
8
9
 
@@ -91,10 +92,22 @@ const isVerbAt = (doc: ProseDocument, offset: number): boolean => {
91
92
  return VERBAL.has(token.pos) || afterSubject;
92
93
  };
93
94
 
95
+ /** The term written with another prefix of its group (本件業務 where 本業務 is defined), after the sentence that defines it. */
96
+ const prefixFindings = (doc: ProseDocument, texts: readonly BodyText[], term: DefinedTerm, definingEnd: number, defined: readonly string[]): Finding[] =>
97
+ prefixVariants(term.term, prefixGroupsOf(doc.lexicons["defined-term-prefix"] ?? [])).flatMap((variant) =>
98
+ defined.includes(variant)
99
+ ? []
100
+ : variantUses(doc.source, texts, variant, definingEnd, defined).map((offset) =>
101
+ finding(doc, "defined-term-form", offset, "prefix", { term: term.term, written: variant, line: term.line }),
102
+ ),
103
+ );
104
+
94
105
  export const definedTermForm: Detector = (doc, options): Finding[] => {
95
106
  if (doc.structure === undefined) return [];
96
107
  const texts = bodyOf(doc);
97
- return firstDefinitions(definedTerms(doc.structure)).flatMap(({ term, spans }) => {
108
+ const terms = definedTerms(doc.structure);
109
+ const defined = terms.map((term) => term.term);
110
+ return firstDefinitions(terms).flatMap(({ term, spans }) => {
98
111
  const uses = usesOf(term.term, texts, spans, "loose");
99
112
  const definingEnd = definingSentenceEnd(doc, term.span);
100
113
  const quoted = quotedUses(doc.source, term.term, uses, definingEnd).map((offset) =>
@@ -104,7 +117,7 @@ export const definedTermForm: Detector = (doc, options): Finding[] => {
104
117
  const lower = lowerCaseUses(doc.source, term.term, nounUses, definingEnd, options.limit).map((offset) =>
105
118
  finding(doc, "defined-term-form", offset, "case", { term: term.term, line: term.line }),
106
119
  );
107
- return [...quoted, ...lower];
120
+ return [...quoted, ...lower, ...prefixFindings(doc, texts, term, definingEnd, defined)];
108
121
  });
109
122
  };
110
123
 
@@ -162,13 +175,21 @@ const isNameLine = (source: string, offset: number, name: string, term: string):
162
175
  const end = source.indexOf("\n", offset);
163
176
  const line = source.slice(start, end === -1 ? source.length : end).trim();
164
177
  const SLACK = 12;
165
- return line.includes(term) || [...line].length <= [...name].length + SLACK;
178
+ // The short name inside the long one ("Pinecone" in "Pinecone Software Ltd") is not the short name written beside it.
179
+ return line.replaceAll(name, "").includes(term) || [...line].length <= [...name].length + SLACK;
166
180
  };
167
181
 
168
182
  /** A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. */
169
183
  const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
170
- const standsAlone = (source: string, offset: number, name: string): boolean =>
171
- !WORD_CHAR.test(source.charAt(offset - 1)) && !WORD_CHAR.test(source.charAt(offset + name.length));
184
+ /**
185
+ * A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. A joining word right after
186
+ * it (北浜精機株式会社及び乙) starts the next item of a list, and the name stands alone.
187
+ */
188
+ export const standsAlone = (source: string, offset: number, name: string, joiners: readonly string[] = []): boolean => {
189
+ const after = offset + name.length;
190
+ const joined = joiners.some((joiner) => joiner !== "" && source.startsWith(joiner, after));
191
+ return !WORD_CHAR.test(source.charAt(offset - 1)) && (joined || !WORD_CHAR.test(source.charAt(after)));
192
+ };
172
193
 
173
194
  /**
174
195
  * Each use of a long name after the definition gave it a short name, outside a signature line. A name that is itself a
@@ -176,6 +197,7 @@ const standsAlone = (source: string, offset: number, name: string): boolean =>
176
197
  */
177
198
  export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[]): RepeatedName[] => {
178
199
  const defined = new Set(terms.map((term) => term.term));
200
+ const joiners = (doc.lexicons["enumeration-joiner"] ?? []).map((entry) => entry.pattern);
179
201
  return terms
180
202
  .filter((term) => term.inline)
181
203
  .flatMap((term) => {
@@ -187,7 +209,7 @@ export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[])
187
209
  return doc.sentences
188
210
  .filter((later) => later.span.start > term.span.end)
189
211
  .flatMap((later) => [...later.text.matchAll(pattern)].map((match) => later.span.start + match.index))
190
- .filter((offset) => standsAlone(doc.source, offset, name) && !isNameLine(doc.source, offset, name, term.term))
212
+ .filter((offset) => standsAlone(doc.source, offset, name, joiners) && !isNameLine(doc.source, offset, name, term.term))
191
213
  .map((offset) => ({ offset, name, term: term.term, line: term.line }));
192
214
  });
193
215
  };
@@ -1,6 +1,6 @@
1
1
  import type { Detector, Finding, ProseDocument, Span, StructureNode } from "../plugin.ts";
2
2
  import { inDocumentOrder } from "../structure/issues.ts";
3
- import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
3
+ import { documentDateOf } from "./document-date.ts";
4
4
  import { overlapsAny, spanIndex } from "../compare/spans.ts";
5
5
  import { yearOf, type DurationUnit } from "../derived/date-arithmetic.ts";
6
6
  import { durationMismatches, type DatedValue, type Duration } from "../derived/durations.ts";
@@ -142,16 +142,8 @@ const markedAges = (doc: ProseDocument): Elapsed[] =>
142
142
 
143
143
  /** 文書の日付の年: 日付だけの段落か「更新日:」の段落(date-stamp)の、最初の日付。 */
144
144
  const referenceYear = (doc: ProseDocument, tree: StructureNode): number | undefined => {
145
- const dates = datesOf(tree);
146
- const candidates = stampCandidates(
147
- doc.source,
148
- doc.paragraphs.map((paragraph) => paragraph.span),
149
- dates,
150
- );
151
- const stamps = dateStampIndexes(candidates, patternsOf(doc, "date-stamp-label"));
152
- const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
153
- const date = paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end);
154
- return date === undefined ? undefined : yearOf(date.value);
145
+ const date = documentDateOf(doc, tree);
146
+ return date === undefined ? undefined : yearOf(date);
155
147
  };
156
148
 
157
149
  /** 起点の年から数えた年数が、文書の日付と合わない。 */
@@ -0,0 +1,18 @@
1
+ // The document's own date: the first date of its date-stamp paragraph (a paragraph that is only a date, or 「更新日:」).
2
+ import type { ProseDocument, StructureNode } from "../plugin.ts";
3
+ import { inDocumentOrder } from "../structure/issues.ts";
4
+ import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
5
+
6
+ /** The date of the document's date stamp as the tree reads it ("2026-10-06", or "2026" for a year alone), when it has one. */
7
+ export const documentDateOf = (doc: ProseDocument, tree: StructureNode): string | undefined => {
8
+ const dates = inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ ...node.span, value: String(node.attrs["value"]) }] : []));
9
+ const candidates = stampCandidates(
10
+ doc.source,
11
+ doc.paragraphs.map((paragraph) => paragraph.span),
12
+ dates,
13
+ );
14
+ const labels = (doc.lexicons["date-stamp-label"] ?? []).map((entry) => entry.pattern);
15
+ const stamps = dateStampIndexes(candidates, labels);
16
+ const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
17
+ return paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end)?.value;
18
+ };