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
package/src/grade/text.ts CHANGED
@@ -13,6 +13,8 @@ export type GradeText = {
13
13
  readonly unknownRule: string;
14
14
  readonly rubricProblem: (problem: RubricProblem) => string;
15
15
  readonly penalty: (total: number) => string;
16
+ /** The outputs at each level of the AI-likeness quick score. */
17
+ readonly aiScore: (low: number, medium: number, high: number, notScored: number) => string;
16
18
  readonly noStructure: (source: string, language: string) => string;
17
19
  readonly unreadable: (path: string, why: string) => string;
18
20
  readonly problem: (problem: ItemProblem) => string;
@@ -110,6 +112,8 @@ export const GRADE_TEXT: Texts<GradeText> = {
110
112
  unknownRule: "grade: に書かれているが、chaff の知らないルール",
111
113
  rubricProblem: (problem) => `chaff.yaml の ${problem.path} は${EXPECTED_JA[problem.expected]}で書いてください(書かれていたのは ${problem.written})`,
112
114
  penalty: (total) => `減点の和: ${String(total)}`,
115
+ aiScore: (low, medium, high, notScored) =>
116
+ `AI らしさ: 低 ${String(low)}、中 ${String(medium)}、高 ${String(high)}、測っていない ${String(notScored)}(書いたのが AI かどうかの判定ではありません。合否には入りません)`,
113
117
  noStructure: (source, language) => `原文 ${source} の言語 ${language} のパッケージは文書の構造を読めない`,
114
118
  unreadable: (path, why) => `${path} を読めませんでした: ${why}`,
115
119
  problem: (problem) => PROBLEM_JA[problem.kind](problem),
@@ -142,6 +146,8 @@ export const GRADE_TEXT: Texts<GradeText> = {
142
146
  unknownRule: "named under grade: but not a rule chaff knows",
143
147
  rubricProblem: (problem) => `chaff.yaml: ${problem.path} must be ${EXPECTED_EN[problem.expected]} (found ${problem.written})`,
144
148
  penalty: (total) => `Penalty points: ${String(total)}`,
149
+ aiScore: (low, medium, high, notScored) =>
150
+ `AI-likeness: low ${String(low)}, medium ${String(medium)}, high ${String(high)}, not scored ${String(notScored)} (not a verdict on whether AI wrote them; not part of pass or fail)`,
145
151
  noStructure: (source, language) => `the ${language} package cannot read the structure of source ${source}`,
146
152
  unreadable: (path, why) => `Could not read ${path}: ${why}`,
147
153
  problem: (problem) => PROBLEM_EN[problem.kind](problem),
@@ -10,6 +10,9 @@ export type VariantText = {
10
10
  readonly citationsCell: (failed: number, checked: number) => string;
11
11
  readonly unsupportedFacts: string;
12
12
  readonly penalty: string;
13
+ /** The row of the AI-likeness quick score, and its cell: outputs at low / medium / high, then (not scored). */
14
+ readonly aiScore: string;
15
+ readonly aiScoreCell: (low: number, medium: number, high: number, notScored: number) => string;
13
16
  readonly ratesHeading: (unit: string) => string;
14
17
  readonly unit: Readonly<Record<"char" | "word", string>>;
15
18
  readonly disagreeHeading: (count: number) => string;
@@ -40,6 +43,8 @@ export const VARIANT_TEXT: Texts<VariantText> = {
40
43
  citationsCell: (failed, checked) => `${String(failed)}/${String(checked)}`,
41
44
  unsupportedFacts: "一節に無い事実",
42
45
  penalty: "減点の和",
46
+ aiScore: "AI らしさ 低/中/高",
47
+ aiScoreCell: (low, medium, high, notScored) => `${String(low)}/${String(medium)}/${String(high)}(${String(notScored)})`,
43
48
  ratesHeading: (unit) => `ルールごとの率(1,000 ${unit}あたり)`,
44
49
  unit: { char: "字", word: "語" },
45
50
  disagreeHeading: (count) => `合否が分かれた出力 ${String(count)} 件`,
@@ -68,6 +73,8 @@ export const VARIANT_TEXT: Texts<VariantText> = {
68
73
  citationsCell: (failed, checked) => `${String(failed)}/${String(checked)}`,
69
74
  unsupportedFacts: "Unsupported facts",
70
75
  penalty: "Penalty points",
76
+ aiScore: "AI low/med/high",
77
+ aiScoreCell: (low, medium, high, notScored) => `${String(low)}/${String(medium)}/${String(high)} (${String(notScored)})`,
71
78
  ratesHeading: (unit) => `Rule rates (per 1,000 ${unit})`,
72
79
  unit: { char: "characters", word: "words" },
73
80
  disagreeHeading: (count) => `${String(count)} ${count === 1 ? "output" : "outputs"} where pass or fail differs`,
@@ -1,6 +1,7 @@
1
1
  import type { LengthUnit } from "../plugin.ts";
2
2
  import { rateRows, sameReading } from "./pairing.ts";
3
3
  import type { GradeResult } from "./result.ts";
4
+ import type { AiLevelCounts } from "./ai-score.ts";
4
5
  import { summaryOf } from "./summary.ts";
5
6
  import { variantGroupsOf, type VariantGroups } from "./variants-input.ts";
6
7
 
@@ -21,6 +22,8 @@ export type VariantColumn = {
21
22
  readonly contexts?: { readonly checked: number; readonly unsupported: number } | undefined;
22
23
  /** Only with a `grade:` rubric. */
23
24
  readonly penalty?: number | undefined;
25
+ /** The outputs at each level of the AI-likeness quick score. Absent when no result carries one. */
26
+ readonly aiScore?: AiLevelCounts | undefined;
24
27
  };
25
28
 
26
29
  /** One rule's rate per 1,000 units in each variant, by variant label; 0 where the variant had no finding of it. */
@@ -66,6 +69,7 @@ const columnOf = (variant: string, results: readonly GradeResult[]): VariantColu
66
69
  citations: summary.citations,
67
70
  ...(summary.contexts === undefined ? {} : { contexts: { checked: summary.contexts.checked, unsupported: sum(summary.contexts.unsupported) } }),
68
71
  ...(summary.penalty === undefined ? {} : { penalty: summary.penalty }),
72
+ ...(summary.aiScore === undefined ? {} : { aiScore: summary.aiScore }),
69
73
  };
70
74
  };
71
75
 
package/src/grade-api.ts CHANGED
@@ -5,7 +5,8 @@ import { statSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { packageFor } from "./adapter-load.ts";
7
7
  import { CLI_TEXT } from "./cli-text.ts";
8
- import { EMPTY, loadConfig, type Config } from "./config/load.ts";
8
+ import { EMPTY, type Config } from "./config/load.ts";
9
+ import { loadConfig } from "./config/read.ts";
9
10
  import { withStyle } from "./config/style.ts";
10
11
  import { withExtensions } from "./extension/load.ts";
11
12
  import { GENRES } from "./genre.ts";
@@ -35,6 +36,7 @@ export type {
35
36
  SupportedFact,
36
37
  } from "./grade/result.ts";
37
38
  export type { OutputSize } from "./grade/rates.ts";
39
+ export type { AiLevelCounts, GradeAiScore } from "./grade/ai-score.ts";
38
40
  export { toScorer, type ChaffScore } from "./grade/scorer.ts";
39
41
  export type { Disagreement, VariantColumn, VariantComparison, VariantRates } from "./grade/variants.ts";
40
42
  export type { VariantInput } from "./grade/variants-input.ts";
package/src/judge.ts CHANGED
@@ -78,8 +78,6 @@ export type JudgeOptions = {
78
78
  const judgeFor = (options: JudgeOptions): Judge =>
79
79
  options.backend === "openai" ? openai.judge(options.openaiClient) : anthropic.judge(options.anthropicClient);
80
80
 
81
- export const defaultModel = (backend: BackendName): string => (backend === "openai" ? openai.DEFAULT_MODEL : anthropic.DEFAULT_MODEL);
82
-
83
81
  export const hasCredentials = (backend: BackendName): boolean => (backend === "openai" ? openai.hasCredentials() : anthropic.hasCredentials());
84
82
 
85
83
  export const isAuthFailure = (backend: BackendName, error: unknown): boolean =>
@@ -1,6 +1,6 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
  import { parse } from "yaml";
3
+ import { exists, readText } from "./package-files.ts";
4
4
  import { loadGenres } from "./genre-load.ts";
5
5
  import { parseGenres, type GenreData } from "./genre-parse.ts";
6
6
 
@@ -15,5 +15,5 @@ export const knownGenres = (): string[] => idsOf(loadGenres());
15
15
  */
16
16
  export const genresBeside = (rulesDir: string): string[] => {
17
17
  const file = join(rulesDir, "..", "genres.yaml");
18
- return existsSync(file) ? idsOf(parseGenres(parse(readFileSync(file, "utf8")))) : knownGenres();
18
+ return exists(file) ? idsOf(parseGenres(parse(readText(file)))) : knownGenres();
19
19
  };
@@ -0,0 +1,15 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // Every read of a file chaff ships (rules, genres, profiles, styles) goes through here, so a build without a file
6
+ // system can put its own reads in this module's place. The functions take any path, as the callers always have.
7
+
8
+ /** The package's folder: src/ and dist/ both sit right under it. */
9
+ export const PACKAGE_DIR = join(dirname(fileURLToPath(import.meta.url)), "..");
10
+
11
+ export const readText = (path: string): string => readFileSync(path, "utf8");
12
+
13
+ export const readDir = (path: string): string[] => readdirSync(path);
14
+
15
+ export const exists = (path: string): boolean => existsSync(path);
@@ -1,22 +1,21 @@
1
- import { readFileSync, readdirSync } 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";
3
+ import { PACKAGE_DIR, readDir, readText } from "../package-files.ts";
5
4
  import { parseProfile, type ProfileDefinition } from "./parse.ts";
6
5
 
7
- const PROFILES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "profiles");
6
+ const PROFILES_DIR = join(PACKAGE_DIR, "profiles");
8
7
 
9
8
  const loaded: { value: readonly ProfileDefinition[] | undefined } = { value: undefined };
10
9
 
11
10
  /** 同梱の profiles/*.yaml。一度だけ読む。 */
12
11
  export const loadProfiles = (): readonly ProfileDefinition[] => {
13
- loaded.value ??= readdirSync(PROFILES_DIR)
12
+ loaded.value ??= readDir(PROFILES_DIR)
14
13
  .filter((file) => file.endsWith(".yaml"))
15
14
  .toSorted((left, right) => left.localeCompare(right))
16
15
  .flatMap((file) => {
17
16
  const path = join(PROFILES_DIR, file);
18
17
  try {
19
- const definition = parseProfile(parse(readFileSync(path, "utf8")));
18
+ const definition = parseProfile(parse(readText(path)));
20
19
  return definition === undefined ? [] : [definition];
21
20
  } catch (error) {
22
21
  throw new Error(`cannot read the profile ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
package/src/rule-load.ts CHANGED
@@ -1,6 +1,4 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
- import { fileURLToPath } from "node:url";
3
- import { dirname, join } from "node:path";
1
+ import { join } from "node:path";
4
2
  import { parse } from "yaml";
5
3
  import type { LanguageLevels, LevelSets, LevelTable, RuleDefinition, Severity } from "./plugin.ts";
6
4
  import { rankOfSeverity, severityAt } from "./levels.ts";
@@ -8,8 +6,9 @@ import { optionsOf } from "./rule-options.ts";
8
6
  import { ruleGuideOf } from "./rule-guide.ts";
9
7
  import { fieldProblemSentence, fieldProblems } from "./rule-fields.ts";
10
8
  import { genresBeside } from "./known-genres.ts";
9
+ import { PACKAGE_DIR, readDir, readText } from "./package-files.ts";
11
10
 
12
- const RULES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "rules");
11
+ const RULES_DIR = join(PACKAGE_DIR, "rules");
13
12
 
14
13
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
15
14
 
@@ -191,7 +190,7 @@ const ruleOf = (raw: Record<string, unknown>, levels: LevelTable, levelSets: Lev
191
190
  */
192
191
  const parseRule = (dir: string, file: string): unknown => {
193
192
  try {
194
- return parse(readFileSync(join(dir, file), "utf8"));
193
+ return parse(readText(join(dir, file)));
195
194
  } catch (error) {
196
195
  throw new Error(`${file} を読めません: ${error instanceof Error ? error.message.split("\n")[0] : String(error)}`, { cause: error });
197
196
  }
@@ -199,7 +198,7 @@ const parseRule = (dir: string, file: string): unknown => {
199
198
 
200
199
  export const loadRules = (language: string, dir: string = RULES_DIR): RuleDefinition[] => {
201
200
  const genres = genresBeside(dir);
202
- return readdirSync(dir)
201
+ return readDir(dir)
203
202
  .filter((file) => file.endsWith(".yaml"))
204
203
  .toSorted((left, right) => left.localeCompare(right, "en"))
205
204
  .map((file) => toRule(parseRule(dir, file), language, file, genres));
package/src/rule-offs.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { readFileSync, readdirSync } from "node:fs";
2
1
  import { join } from "node:path";
3
2
  import { parse } from "yaml";
3
+ import { readDir, readText } from "./package-files.ts";
4
4
 
5
5
  /** One rule a group or genre turns off by default, from the rule file's off_for, with why. */
6
6
  export type RuleOff = { readonly rule: string; readonly target: string; readonly reason: string };
@@ -34,7 +34,7 @@ const offForBlock = (rule: string, text: string): string | undefined => {
34
34
  /** The offs of one rule file. The rule is named by the file, which is named by the rule's id. */
35
35
  const offsInFile = (dir: string, file: string): RuleOff[] => {
36
36
  const rule = file.replace(/\.yaml$/u, "");
37
- const block = offForBlock(rule, readFileSync(join(dir, file), "utf8").replaceAll("\r\n", "\n"));
37
+ const block = offForBlock(rule, readText(join(dir, file)).replaceAll("\r\n", "\n"));
38
38
  const parsed: unknown = block === undefined ? undefined : parse(block);
39
39
  return ruleOffsOf(rule, isRecord(parsed) ? parsed["off_for"] : undefined);
40
40
  };
@@ -44,7 +44,7 @@ const offsInFile = (dir: string, file: string): RuleOff[] => {
44
44
  * at startup, before any rule is.
45
45
  */
46
46
  export const loadRuleOffs = (dir: string): RuleOff[] =>
47
- readdirSync(dir)
47
+ readDir(dir)
48
48
  .filter((file) => file.endsWith(".yaml"))
49
49
  .toSorted((left, right) => left.localeCompare(right, "en"))
50
50
  .flatMap((file) => offsInFile(dir, file));
package/src/run.ts CHANGED
@@ -15,7 +15,7 @@ import { presetLevels } from "./genre-load.ts";
15
15
  import { bodySectionOf } from "./body-section.ts";
16
16
  import { optionValues, settleOptions, type OptionLayer } from "./rule-options.ts";
17
17
  import { byPosition } from "./finding-order.ts";
18
- import { PatternTimeout } from "./custom/bounded-match.ts";
18
+ import { PatternTimeout } from "./custom/pattern-timeout.ts";
19
19
  import { PluginRuleFailure } from "./extension/module-detector.ts";
20
20
  import { failureReason } from "./extension/failure-text.ts";
21
21
  import { tagCoverage } from "./tag-coverage.ts";
@@ -0,0 +1,103 @@
1
+ // A rate of change written beside the two values it is computed from (「前年同月の1,000社から20%増えました」 with 1,200社,
2
+ // "up 20% from 1,000 companies" with 1,200 companies) that the two values do not give. Pure; the detector reads the
3
+ // figures, the rate and the words from the tree and the language's lexicons.
4
+ import type { Span } from "../plugin.ts";
5
+ import type { StructureIssue } from "./issues.ts";
6
+
7
+ /** A value with its unit as the sentence compares it (円, 社, $, companies) and the step it is written to (1 for 1,200; 1億 for 12億). */
8
+ export type Figure = Span & { readonly value: number; readonly unit: string; readonly step: number };
9
+ /** A percentage, with the number of decimals written. */
10
+ export type Rate = Span & { readonly value: number; readonly decimals: number };
11
+ /** A word that says which way the value moved (増加, up: 1; 減少, down: -1). */
12
+ export type Direction = Span & { readonly sign: 1 | -1 };
13
+ /** A word that marks a value: the earlier one after it (「1,000社から」) or before it ("from 1,000"), the later one (「1,200社に」, "to 1,200"). */
14
+ export type BaseMark = Span & { readonly position: "before" | "after" };
15
+
16
+ export type ChangeText = {
17
+ readonly sentences: readonly Span[];
18
+ readonly figures: readonly Figure[];
19
+ readonly rates: readonly Rate[];
20
+ readonly directions: readonly Direction[];
21
+ readonly marks: readonly BaseMark[];
22
+ /** Words that mark the later value ("to", 「に」). */
23
+ readonly targets: readonly BaseMark[];
24
+ };
25
+
26
+ /** How far a direction word may stand from the rate (「20%の増加」「20%増えました」, "an increase of 20%", "up 20%"). */
27
+ const DIRECTION_REACH = 8;
28
+ /** How far a base mark may stand from its value ("from $10" has the currency sign between). */
29
+ const MARK_GAP = 3;
30
+ /**
31
+ * How far the earlier value may stand from the rate (「1,000社から20%」「10億円に比べて20%」"20% from $10 million"). Further
32
+ * away, the rate is of something else in the sentence (「100店から120店に増え、売上は15%増えた」).
33
+ */
34
+ const BASE_REACH = 12;
35
+ const PERCENT = 100;
36
+ const HALF = 0.5;
37
+ const DECIMAL_BASE = 10;
38
+
39
+ const within = (outer: Span, inner: Span): boolean => inner.start >= outer.start && inner.end <= outer.end;
40
+
41
+ const distance = (left: Span, right: Span): number => Math.max(left.start - right.end, right.start - left.end, 0);
42
+
43
+ const markOf = (figure: Figure, marks: readonly BaseMark[]): BaseMark | undefined =>
44
+ marks.find((mark) => {
45
+ const gap = mark.position === "after" ? mark.start - figure.end : figure.start - mark.end;
46
+ return gap >= 0 && gap <= MARK_GAP;
47
+ });
48
+
49
+ /** The earlier value: marked, with the value or its mark beside the rate ("up 25% compared with $1,000"). */
50
+ const isBaseFor = (figure: Figure, rate: Rate, marks: readonly BaseMark[]): boolean => {
51
+ const mark = markOf(figure, marks);
52
+ return mark !== undefined && Math.min(distance(figure, rate), distance(mark, rate)) <= BASE_REACH;
53
+ };
54
+
55
+ /**
56
+ * The later value is written before the rate (「1,200社となり、…20%増えました」"1,200 companies, up 20%") or marked as the
57
+ * value reached (「1,200社に」"to 1,200"). Another amount after the rate is about something else ("…, and revenue was $1,300").
58
+ */
59
+ const isCurrentFor = (figure: Figure, rate: Rate, targets: readonly BaseMark[]): boolean => figure.end <= rate.start || markOf(figure, targets) !== undefined;
60
+
61
+ /** The one sign the direction words beside the rate agree on. */
62
+ const signNear = (rate: Rate, directions: readonly Direction[]): 1 | -1 | undefined => {
63
+ const signs = new Set(directions.filter((direction) => distance(direction, rate) <= DIRECTION_REACH).map((direction) => direction.sign));
64
+ const [sign] = signs;
65
+ return signs.size === 1 ? sign : undefined;
66
+ };
67
+
68
+ /**
69
+ * The rates of change the two values allow. Each value may be off by half the step it is written to: 12億 is anything from
70
+ * 11.5億 to 12.5億, so 12億 from 10億 is a rise of 9.5% to 31.6%, and only a rate outside that is a mistake.
71
+ */
72
+ const rateRange = (base: Figure, current: Figure): readonly [number, number] | undefined => {
73
+ const [baseLow, baseHigh] = [base.value - base.step * HALF, base.value + base.step * HALF];
74
+ const [currentLow, currentHigh] = [current.value - current.step * HALF, current.value + current.step * HALF];
75
+ if (baseLow <= 0) return undefined;
76
+ return [(currentLow / baseHigh - 1) * PERCENT, (currentHigh / baseLow - 1) * PERCENT];
77
+ };
78
+
79
+ const issueIn = (text: ChangeText, sentence: Span): StructureIssue[] => {
80
+ const rates = text.rates.filter((rate) => within(sentence, rate));
81
+ const figures = text.figures.filter((figure) => within(sentence, figure));
82
+ const [rate] = rates;
83
+ if (rates.length !== 1 || rate === undefined) return [];
84
+ const sign = signNear(
85
+ rate,
86
+ text.directions.filter((direction) => within(sentence, direction)),
87
+ );
88
+ const bases = figures.filter((figure) => isBaseFor(figure, rate, text.marks));
89
+ const [base] = bases;
90
+ if (sign === undefined || bases.length !== 1 || base === undefined) return [];
91
+ const currents = figures.filter((figure) => figure !== base && figure.unit === base.unit && isCurrentFor(figure, rate, text.targets));
92
+ const [current] = currents;
93
+ const range = currents.length === 1 && current !== undefined ? rateRange(base, current) : undefined;
94
+ if (range === undefined || current === undefined) return [];
95
+ const written = sign * rate.value;
96
+ const slack = HALF * DECIMAL_BASE ** -rate.decimals;
97
+ if (written >= range[0] - slack && written <= range[1] + slack) return [];
98
+ const computed = Math.abs((current.value / base.value - 1) * PERCENT).toFixed(rate.decimals);
99
+ return [{ offset: rate.start, values: { rate: String(rate.value), computed } }];
100
+ };
101
+
102
+ /** Each sentence with one rate of change, one direction, one marked earlier value and one other value in its unit, that disagree. */
103
+ export const changeRateMismatches = (text: ChangeText): StructureIssue[] => text.sentences.flatMap((sentence) => issueIn(text, sentence));
@@ -18,6 +18,29 @@ export type RangeWords = {
18
18
  readonly frames: readonly RangeFrame[];
19
19
  /** 同じ文にあれば、組を期間ではなく日付の変更と読む語(moved from … to、… was postponed)。 */
20
20
  readonly changes: readonly string[];
21
+ /** 日付の前に添える曜日の名(Wednesday, April 1, 2026)。つなぎ目と前の語を読むときに除く。無ければ空。 */
22
+ readonly weekdays?: readonly string[];
23
+ };
24
+
25
+ /** 曜日の略し方の短いほう(Tue、Wed)。名の頭のこの字数以上が書いてあれば、その曜日と読む。 */
26
+ const WEEKDAY_ABBREVIATION = 3;
27
+ const LETTER = /\p{L}/u;
28
+
29
+ /** The text's last word (past a full stop) and where it starts. */
30
+ const lastWord = (text: string): { readonly word: string; readonly start: number } => {
31
+ const body = text.endsWith(".") ? text.slice(0, -1) : text;
32
+ const chars = [...body];
33
+ const start = chars.findLastIndex((char) => !LETTER.test(char)) + 1;
34
+ return { word: chars.slice(start).join(""), start: chars.slice(0, start).join("").length };
35
+ };
36
+
37
+ /** 終わりに付いた曜日の名か略し方とその後ろのコンマ("to tuesday," の tuesday,、"from wed." の wed.)を除く。 */
38
+ export const withoutTrailingWeekday = (text: string, weekdays: readonly string[]): string => {
39
+ const trimmed = text.replace(/,$/u, "").trimEnd();
40
+ const { word, start } = lastWord(trimmed);
41
+ const lower = word.toLowerCase();
42
+ const isWeekday = lower.length >= WEEKDAY_ABBREVIATION && weekdays.some((name) => name.toLowerCase().startsWith(lower));
43
+ return isWeekday ? trimmed.slice(0, start).trimEnd() : text;
21
44
  };
22
45
 
23
46
  export type RangeFrame = { readonly lead: string; readonly joint: string };
@@ -87,13 +110,15 @@ const sentenceAround = (source: string, start: DatedSpan, end: DatedSpan): { rea
87
110
  /** 前の語と間の語の組(from … to)。同じ文のどこかに変更の語(moved、postponed)があれば、日付を動かした文で、期間ではない。 */
88
111
  const framed = (source: string, start: DatedSpan, end: DatedSpan, joint: string, words: RangeWords): boolean => {
89
112
  const sentence = sentenceAround(source, start, end);
90
- const frame = words.frames.find((candidate) => candidate.joint === joint && endsWithWord(sentence.before, candidate.lead));
113
+ const before = withoutTrailingWeekday(sentence.before, words.weekdays ?? []);
114
+ const frame = words.frames.find((candidate) => candidate.joint === joint && endsWithWord(before, candidate.lead));
91
115
  return frame !== undefined && !words.changes.some((change) => hasWord(sentence.text, change.toLowerCase()));
92
116
  };
93
117
 
94
118
  const isRange = (source: string, start: DatedSpan, end: DatedSpan, words: RangeWords): boolean => {
95
- const joint = jointOf(source, start, end);
96
- if (joint === undefined) return false;
119
+ const written = jointOf(source, start, end);
120
+ if (written === undefined) return false;
121
+ const joint = withoutTrailingWeekday(written, words.weekdays ?? []);
97
122
  return (
98
123
  isOneOf(joint, words.connectors) || (isOneOf(joint, words.openers) && closedAfter(source, end, words.closers)) || framed(source, start, end, joint, words)
99
124
  );
@@ -31,8 +31,9 @@ const textOf = (node: StructureNode, key: string): string => {
31
31
  * 参照がこの文書のどこかを指しているか。fallback は言語パッケージが付ける別の行き先で、
32
32
  * 日本語の「第4条第1項」は、番号の無い第 1 項を持つ第4条を指しうる。
33
33
  */
34
- const resolves = (node: StructureNode, addresses: ReadonlySet<string>): boolean =>
35
- addresses.has(textOf(node, "target")) || (node.attrs["fallback"] !== undefined && addresses.has(fallbackKey(node)));
34
+ const resolves = (node: StructureNode, addresses: ReadonlySet<string>, chapters: Chapters): boolean =>
35
+ addresses.has(textOf(node, "target")) ||
36
+ (node.attrs["fallback"] !== undefined && chapterReading(textOf(node, "target"), chapters).fallback && addresses.has(fallbackKey(node)));
36
37
 
37
38
  /**
38
39
  * fallbackLabel があれば、fallback はその見出しの番号で書かれた節点だけに当たる。
@@ -48,6 +49,53 @@ const addressesOf = (node: StructureNode): string[] =>
48
49
 
49
50
  const keysOf = (node: StructureNode): string[] => addressesOf(node).flatMap((address) => [address, labelled(address, textOf(node, "label"))]);
50
51
 
52
+ /** An address without its numbers: the kind of unit it names (ch9 and ch10 are both ch). */
53
+ const unitOf = (address: string): string => address.replaceAll(/[\d.]+/gu, "");
54
+
55
+ /** The document's chapters: their addresses, and whether the one chapter is the document itself (its title, first of all). */
56
+ export type Chapters = { readonly addresses: readonly string[]; readonly titled: boolean };
57
+
58
+ /**
59
+ * How a document's chapters bear on a reference to a chapter (第9章, Chapter 9). Pure.
60
+ * fallback: whether the reference may fall back to a numbered section ("## 9. …"). Only in a document with no chapter:
61
+ * where chapters are written, a chapter reference names a chapter, and section 9 is not chapter 9.
62
+ * elsewhere: the reference names a chapter of another file. A document that is one chapter (第10章 as its title, a series
63
+ * of one chapter per file) holds no other chapter, so 第9章 is the next file's. Its own chapter is still its own.
64
+ */
65
+ export const chapterReading = (target: string, chapters: Chapters): { readonly fallback: boolean; readonly elsewhere: boolean } => {
66
+ const shortAddresses = chapters.addresses.map((address) => address.slice(address.lastIndexOf("/") + 1));
67
+ const toChapter = new Set(shortAddresses.map(unitOf)).has(unitOf(target));
68
+ const series = chapters.titled && chapters.addresses.length === 1 && !shortAddresses.includes(target);
69
+ return { fallback: !toChapter, elsewhere: toChapter && series };
70
+ };
71
+
72
+ /** A chapter is the document's title when it is not under another heading (ch10, not h1/ch1) and nothing numbered comes before it. */
73
+ const chaptersOf = (nodes: readonly StructureNode[]): Chapters => {
74
+ const chapters = nodes.filter((node) => node.kind === "chapter");
75
+ const first = nodes.find((node) => NUMBERED.has(node.kind));
76
+ const titled = chapters.length === 1 && first === chapters[0] && !(first?.address.includes("/") ?? true);
77
+ return { addresses: chapters.map((node) => node.address), titled };
78
+ };
79
+
80
+ /** What may stand between a chapter and the reference it qualifies: nothing (第1章第2条) or の (第1章の第2条). A list mark (第1章、第2条) names two. */
81
+ const QUALIFIER_GAPS: ReadonlySet<string> = new Set(["", "の"]);
82
+
83
+ /**
84
+ * The references that point into another file: a chapter reference read as elsewhere (chapterReading), and the reference
85
+ * right after one, which it qualifies (第1章の第2条 is article 2 of that other chapter).
86
+ */
87
+ const elsewhereReferences = (nodes: readonly StructureNode[], source: string): ReadonlySet<StructureNode> => {
88
+ const chapters = chaptersOf(nodes);
89
+ const references = nodes.filter((node) => node.kind === "reference");
90
+ const elsewhere = new Set<StructureNode>();
91
+ references.forEach((node, index) => {
92
+ const before = references[index - 1];
93
+ const qualified = before !== undefined && elsewhere.has(before) && QUALIFIER_GAPS.has(source.slice(before.span.end, node.span.start).trim());
94
+ if (qualified || chapterReading(textOf(node, "target"), chapters).elsewhere) elsewhere.add(node);
95
+ });
96
+ return elsewhere;
97
+ };
98
+
51
99
  /**
52
100
  * 他の文書を指す参照か。citedTag("[HTTP-CACHING]")は、書き方だけでは差し込み欄の "[BUYER-1]" と見分けられないので、
53
101
  * 文書がその語を一覧に載せているときだけ他の文書の名とみなす。
@@ -77,10 +125,12 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
77
125
  const otherNumbering = (node: StructureNode): boolean =>
78
126
  numberings.size > 0 && typeof numbering(node) === "string" && !numberings.has(String(numbering(node))) && citedElsewhere.has(numbering(node));
79
127
  const articleLabels = nodes.filter((node) => node.kind === "article").map((node) => textOf(node, "label"));
128
+ const chapters = chaptersOf(nodes);
129
+ const elsewhere = elsewhereReferences(nodes, source);
80
130
  return nodes
81
131
  .filter((node) => node.kind === "reference" && !citesOther(node) && !otherNumbering(node))
82
- .filter((node) => !namesAbsentUnit(node, articleLabels))
83
- .filter((node) => !resolves(node, addresses))
132
+ .filter((node) => !namesAbsentUnit(node, articleLabels) && !elsewhere.has(node))
133
+ .filter((node) => !resolves(node, addresses, chapters))
84
134
  .map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
85
135
  };
86
136
 
@@ -88,15 +138,20 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
88
138
  * 参照が指す節点。danglingReferences と同じ引き方(番地、章の短い番地、fallback)で、番号の付いたまとまりを引く。
89
139
  * 同じ鍵の節点が二つ以上ある(番号の振り直しや別表の条)ときは、どれを指すのか決まらないので undefined。
90
140
  */
91
- export const referenceResolver = (tree: StructureNode): ((reference: StructureNode) => StructureNode | undefined) => {
141
+ export const referenceResolver = (tree: StructureNode, source: string): ((reference: StructureNode) => StructureNode | undefined) => {
142
+ const nodes = inDocumentOrder(tree);
143
+ const chapters = chaptersOf(nodes);
144
+ const elsewhere = elsewhereReferences(nodes, source);
92
145
  const byKey = new Map<string, StructureNode | null>();
93
146
  inDocumentOrder(tree)
94
147
  .filter((node) => NUMBERED.has(node.kind))
95
148
  .forEach((node) => keysOf(node).forEach((key) => byKey.set(key, byKey.has(key) && byKey.get(key) !== node ? null : node)));
96
149
  const resolve = (reference: StructureNode): StructureNode | undefined => {
150
+ if (elsewhere.has(reference)) return undefined;
97
151
  const target = byKey.get(textOf(reference, "target"));
98
152
  if (target !== undefined) return target ?? undefined;
99
- return reference.attrs["fallback"] === undefined ? undefined : (byKey.get(fallbackKey(reference)) ?? undefined);
153
+ const fallback = reference.attrs["fallback"] !== undefined && chapterReading(textOf(reference, "target"), chapters).fallback;
154
+ return fallback ? (byKey.get(fallbackKey(reference)) ?? undefined) : undefined;
100
155
  };
101
156
  // Article 6 は Section 6 ではない。番号の書き方が両方にあって違えば、別の文書の条かもしれないので引かない。
102
157
  return (reference) => {
@@ -0,0 +1,34 @@
1
+ // The numbers written on the items of a Markdown ordered list (「1.」「2.」「4.」) that skip or repeat. Markdown renders an
2
+ // ordered list from its first number on, but the source shows the numbers as written, and a "see step 4" points at those.
3
+ import type { Span } from "../plugin.ts";
4
+ import type { StructureIssue } from "./issues.ts";
5
+
6
+ const ITEM_NUMBER = /^(?<number>\d{1,9})(?<delimiter>[.)])[ \t]/u;
7
+
8
+ /** One item's written number: where the item starts, the number and the number as written (「4.」). */
9
+ export type WrittenNumber = { readonly offset: number; readonly number: number; readonly label: string };
10
+
11
+ /** The number written at the start of each item, or undefined when an item has none (a bulleted list). */
12
+ export const writtenNumbers = (source: string, itemSpans: readonly Span[]): WrittenNumber[] | undefined => {
13
+ const numbers = itemSpans.map((span) => {
14
+ const found = ITEM_NUMBER.exec(source.slice(span.start, span.end))?.groups;
15
+ return found === undefined
16
+ ? undefined
17
+ : { offset: span.start, number: Number(found["number"]), label: `${found["number"] ?? ""}${found["delimiter"] ?? ""}` };
18
+ });
19
+ return numbers.every((entry) => entry !== undefined) ? numbers : undefined;
20
+ };
21
+
22
+ /**
23
+ * Each item whose number is not one more than the item before it. A list that writes one number on every item (「1.」 on
24
+ * each, which Markdown counts up) is numbered by the renderer, not by the writer, and is not read. An item numbered 1
25
+ * starts a new count, as the structure rule reads a 1 that follows other items.
26
+ */
27
+ export const listNumberBreaks = (items: readonly WrittenNumber[]): StructureIssue[] => {
28
+ if (new Set(items.map((item) => item.number)).size < 2) return [];
29
+ return items.flatMap((item, index) => {
30
+ const previous = items[index - 1];
31
+ if (previous === undefined || item.number === previous.number + 1 || item.number === 1) return [];
32
+ return [{ offset: item.offset, values: { previous: previous.label, label: item.label, expected: previous.number + 1, found: item.number } }];
33
+ });
34
+ };
@@ -0,0 +1,94 @@
1
+ import type { BodyText, DefinedTerm } from "./definition-use.ts";
2
+
3
+ /**
4
+ * 当事者を、定めた呼び名でなく立場の語で書いた所(「乙」と定めたあとの「受託者」、the "Supplier" のあとの the Vendor)。
5
+ * 当事者の定義は、文の途中で括弧に入れて短い呼び名を定めた所のうち、呼び名が当事者の呼び名の語(甲・乙・当社)か立場の語
6
+ * そのもの(Supplier)であるもの、または括弧のすぐ前に会社の形(株式会社、Ltd)を書いたもの。語は言語パッケージの語彙表から取る。
7
+ * 立場の語は、それ自体を定義した語や、定義した語の一部として書いた所(Receiving Party と定義した所の Party)なら指さない。
8
+ */
9
+ export type PartyWords = {
10
+ /** 当事者の立場の語(委託者、受託者、Supplier、Recipient)。 */
11
+ readonly roles: readonly string[];
12
+ /** 当事者の呼び名に使う語(甲、乙、当社、we)。 */
13
+ readonly shortNames: readonly string[];
14
+ /** 会社の形(株式会社、Ltd、Inc.)。 */
15
+ readonly companyForms: readonly string[];
16
+ };
17
+
18
+ export type Party = { readonly term: string; readonly end: number };
19
+ export type RoleUse = { readonly offset: number; readonly role: string };
20
+
21
+ /** 会社の形を探す、括弧の前の幅。名前一つ分。 */
22
+ const NAME_REACH = 40;
23
+ /** 語の前後の一字がこれなら、語は長い語の一部(再受託者、受託者側の「受託者」は語の一部でない側も含む)。 */
24
+ const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
25
+ const LATIN_START = /^\p{Script=Latin}/u;
26
+ /** 英語の立場の語は、冠詞か所有の語に続く大文字の語だけ(the Vendor)。小文字の the vendor は普通の名詞。 */
27
+ const ENGLISH_LEAD = /(?:^|[^\p{L}])(?:the|The|each|Each|any|Any|such|Such)\s+$/u;
28
+ const LEAD_REACH = 8;
29
+
30
+ const occurrences = (text: string, word: string): number[] => {
31
+ const found: number[] = [];
32
+ let at = text.indexOf(word);
33
+ while (at !== -1) {
34
+ found.push(at);
35
+ at = text.indexOf(word, at + word.length);
36
+ }
37
+ return found;
38
+ };
39
+
40
+ const LETTER = /\p{L}/u;
41
+
42
+ /** 英字の語は、英字の続き(Including の Inc)では語と読まない。 */
43
+ const wordAt = (text: string, word: string, at: number): boolean =>
44
+ !LATIN_START.test(word) || (!LETTER.test(text.charAt(at - 1)) && !LETTER.test(text.charAt(at + word.length)));
45
+
46
+ /** 括弧の前の名前に会社の形があるか。 */
47
+ const namedCompanyBefore = (source: string, definition: DefinedTerm, forms: readonly string[]): boolean => {
48
+ const before = source.slice(Math.max(0, definition.span.start - NAME_REACH), definition.span.start);
49
+ return forms.some((form) => form !== "" && occurrences(before, form).some((at) => wordAt(before, form, at)));
50
+ };
51
+
52
+ /** 文書が当事者として定めた呼び名と、その定義の終わり。 */
53
+ export const partiesOf = (source: string, terms: readonly DefinedTerm[], words: PartyWords): Party[] =>
54
+ terms
55
+ .filter((term) => term.inline)
56
+ .filter((term) => words.shortNames.includes(term.term) || words.roles.includes(term.term) || namedCompanyBefore(source, term, words.companyForms))
57
+ .map((term) => ({ term: term.term, end: term.span.end }));
58
+
59
+ /** 英語の立場の語のあとに付いてよいもの: 複数形と所有の形(Vendors、Vendor's)。 */
60
+ const ENGLISH_ENDING = /^(?:s|['’]s?)?(?![\p{L}\p{N}])/u;
61
+ /** 文や項目の頭("Vendor shall"、"1. Vendor shall")。 */
62
+ const CLAUSE_START = /^[\s\d.()*+-]*$/u;
63
+
64
+ /** 英語の立場の語は、冠詞か所有の語のあと(the Vendor)か、文の頭の大文字の語だけ。小文字の the vendor は普通の名詞。 */
65
+ const englishRoleAt = (text: BodyText, at: number, role: string): boolean => {
66
+ const before = text.text.slice(0, at);
67
+ return ENGLISH_ENDING.test(text.text.slice(at + role.length)) && (CLAUSE_START.test(before) || ENGLISH_LEAD.test(before.slice(-LEAD_REACH)));
68
+ };
69
+
70
+ const standsAlone = (source: string, text: BodyText, at: number, role: string): boolean => {
71
+ const offset = text.start + at;
72
+ if (WORD_CHAR.test(source.charAt(offset - 1))) return false;
73
+ return LATIN_START.test(role) ? englishRoleAt(text, at, role) : !/[\p{Script=Han}\p{Script=Katakana}ー]/u.test(source.charAt(offset + role.length));
74
+ };
75
+
76
+ /** 定義した語がこの位置を覆うか(Receiving Party の Party、本受託者規程の受託者)。 */
77
+ const insideDefined = (source: string, offset: number, role: string, defined: readonly string[]): boolean =>
78
+ defined.some((term) => term !== role && term.includes(role) && occurrences(term, role).some((at) => source.startsWith(term, offset - at)));
79
+
80
+ /**
81
+ * 当事者を定めた後(after より後)で、立場の語を書いた所。立場の語それ自体を定義した文書では、その語は呼び名なので指さない。
82
+ */
83
+ export const roleUses = (source: string, texts: readonly BodyText[], roles: readonly string[], defined: readonly string[], after: number): RoleUse[] =>
84
+ roles
85
+ .filter((role) => role !== "" && !defined.includes(role))
86
+ .flatMap((role) =>
87
+ texts.flatMap((text) =>
88
+ occurrences(text.text, role)
89
+ .filter((at) => text.start + at > after && standsAlone(source, text, at, role) && !insideDefined(source, text.start + at, role, defined))
90
+ .map((at) => ({ offset: text.start + at, role })),
91
+ ),
92
+ )
93
+ .toSorted((left, right) => left.offset - right.offset)
94
+ .filter((use, index, all) => all.findIndex((other) => other.offset <= use.offset && use.offset < other.offset + other.role.length) === index);