chaffjs 0.24.0 → 0.26.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 (418) 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/cli-text.d.ts.map +1 -1
  90. package/dist/cli-text.js +2 -0
  91. package/dist/cli-text.js.map +1 -1
  92. package/dist/cli.d.ts.map +1 -1
  93. package/dist/cli.js +4 -1
  94. package/dist/cli.js.map +1 -1
  95. package/dist/commands/ai-score.d.ts +8 -0
  96. package/dist/commands/ai-score.d.ts.map +1 -0
  97. package/dist/commands/ai-score.js +59 -0
  98. package/dist/commands/ai-score.js.map +1 -0
  99. package/dist/commands/document-commands.d.ts.map +1 -1
  100. package/dist/commands/document-commands.js +2 -0
  101. package/dist/commands/document-commands.js.map +1 -1
  102. package/dist/config/by-path-problems.d.ts +5 -0
  103. package/dist/config/by-path-problems.d.ts.map +1 -0
  104. package/dist/config/by-path-problems.js +11 -0
  105. package/dist/config/by-path-problems.js.map +1 -0
  106. package/dist/config/load.d.ts +9 -2
  107. package/dist/config/load.d.ts.map +1 -1
  108. package/dist/config/load.js +21 -8
  109. package/dist/config/load.js.map +1 -1
  110. package/dist/config/read.d.ts +2 -0
  111. package/dist/config/read.d.ts.map +1 -1
  112. package/dist/config/read.js +4 -1
  113. package/dist/config/read.js.map +1 -1
  114. package/dist/config/warnings.d.ts +1 -1
  115. package/dist/config/warnings.d.ts.map +1 -1
  116. package/dist/config/warnings.js +3 -1
  117. package/dist/config/warnings.js.map +1 -1
  118. package/dist/config/working-dir.d.ts +2 -0
  119. package/dist/config/working-dir.d.ts.map +1 -0
  120. package/dist/config/working-dir.js +3 -0
  121. package/dist/config/working-dir.js.map +1 -0
  122. package/dist/custom/bounded-match.d.ts +1 -12
  123. package/dist/custom/bounded-match.d.ts.map +1 -1
  124. package/dist/custom/bounded-match.js +1 -11
  125. package/dist/custom/bounded-match.js.map +1 -1
  126. package/dist/custom/pattern-timeout.d.ts +13 -0
  127. package/dist/custom/pattern-timeout.d.ts.map +1 -0
  128. package/dist/custom/pattern-timeout.js +13 -0
  129. package/dist/custom/pattern-timeout.js.map +1 -0
  130. package/dist/detect.d.ts.map +1 -1
  131. package/dist/detect.js +4 -5
  132. package/dist/detect.js.map +1 -1
  133. package/dist/detectors/connective-hierarchy.d.ts +16 -0
  134. package/dist/detectors/connective-hierarchy.d.ts.map +1 -0
  135. package/dist/detectors/connective-hierarchy.js +68 -0
  136. package/dist/detectors/connective-hierarchy.js.map +1 -0
  137. package/dist/detectors/date-year.d.ts +10 -2
  138. package/dist/detectors/date-year.d.ts.map +1 -1
  139. package/dist/detectors/date-year.js +44 -8
  140. package/dist/detectors/date-year.js.map +1 -1
  141. package/dist/detectors/defined-term-form.d.ts +5 -0
  142. package/dist/detectors/defined-term-form.d.ts.map +1 -1
  143. package/dist/detectors/defined-term-form.js +22 -5
  144. package/dist/detectors/defined-term-form.js.map +1 -1
  145. package/dist/detectors/missing-final-period.d.ts +17 -0
  146. package/dist/detectors/missing-final-period.d.ts.map +1 -0
  147. package/dist/detectors/missing-final-period.js +62 -0
  148. package/dist/detectors/missing-final-period.js.map +1 -0
  149. package/dist/detectors/missing-space-after-comma.d.ts +10 -0
  150. package/dist/detectors/missing-space-after-comma.d.ts.map +1 -0
  151. package/dist/detectors/missing-space-after-comma.js +42 -0
  152. package/dist/detectors/missing-space-after-comma.js.map +1 -0
  153. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  154. package/dist/detectors/orthographic-variant.js +44 -17
  155. package/dist/detectors/orthographic-variant.js.map +1 -1
  156. package/dist/detectors/party-role-name.d.ts +3 -0
  157. package/dist/detectors/party-role-name.d.ts.map +1 -0
  158. package/dist/detectors/party-role-name.js +35 -0
  159. package/dist/detectors/party-role-name.js.map +1 -0
  160. package/dist/detectors/quote-without-source.d.ts +17 -0
  161. package/dist/detectors/quote-without-source.d.ts.map +1 -0
  162. package/dist/detectors/quote-without-source.js +64 -0
  163. package/dist/detectors/quote-without-source.js.map +1 -0
  164. package/dist/detectors/reference-text.js +1 -1
  165. package/dist/detectors/reference-text.js.map +1 -1
  166. package/dist/detectors/registry/connective-hierarchy.d.ts +3 -0
  167. package/dist/detectors/registry/connective-hierarchy.d.ts.map +1 -0
  168. package/dist/detectors/registry/connective-hierarchy.js +3 -0
  169. package/dist/detectors/registry/connective-hierarchy.js.map +1 -0
  170. package/dist/detectors/registry/missing-final-period.d.ts +3 -0
  171. package/dist/detectors/registry/missing-final-period.d.ts.map +1 -0
  172. package/dist/detectors/registry/missing-final-period.js +3 -0
  173. package/dist/detectors/registry/missing-final-period.js.map +1 -0
  174. package/dist/detectors/registry/missing-space-after-comma.d.ts +3 -0
  175. package/dist/detectors/registry/missing-space-after-comma.d.ts.map +1 -0
  176. package/dist/detectors/registry/missing-space-after-comma.js +3 -0
  177. package/dist/detectors/registry/missing-space-after-comma.js.map +1 -0
  178. package/dist/detectors/registry/party-role-name.d.ts +3 -0
  179. package/dist/detectors/registry/party-role-name.d.ts.map +1 -0
  180. package/dist/detectors/registry/party-role-name.js +3 -0
  181. package/dist/detectors/registry/party-role-name.js.map +1 -0
  182. package/dist/detectors/registry/quote-without-source.d.ts +3 -0
  183. package/dist/detectors/registry/quote-without-source.d.ts.map +1 -0
  184. package/dist/detectors/registry/quote-without-source.js +3 -0
  185. package/dist/detectors/registry/quote-without-source.js.map +1 -0
  186. package/dist/detectors/registry/vague-deadline.d.ts +3 -0
  187. package/dist/detectors/registry/vague-deadline.d.ts.map +1 -0
  188. package/dist/detectors/registry/vague-deadline.js +3 -0
  189. package/dist/detectors/registry/vague-deadline.js.map +1 -0
  190. package/dist/detectors/run-in-label.d.ts +7 -0
  191. package/dist/detectors/run-in-label.d.ts.map +1 -0
  192. package/dist/detectors/run-in-label.js +53 -0
  193. package/dist/detectors/run-in-label.js.map +1 -0
  194. package/dist/detectors/sentence-counts.d.ts +3 -1
  195. package/dist/detectors/sentence-counts.d.ts.map +1 -1
  196. package/dist/detectors/sentence-counts.js +11 -4
  197. package/dist/detectors/sentence-counts.js.map +1 -1
  198. package/dist/detectors/sentence-ending.d.ts +1 -1
  199. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  200. package/dist/detectors/sentence-ending.js +2 -1
  201. package/dist/detectors/sentence-ending.js.map +1 -1
  202. package/dist/detectors/structure-tree.d.ts +4 -1
  203. package/dist/detectors/structure-tree.d.ts.map +1 -1
  204. package/dist/detectors/structure-tree.js +21 -2
  205. package/dist/detectors/structure-tree.js.map +1 -1
  206. package/dist/detectors/unbalanced-bracket.d.ts +1 -1
  207. package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
  208. package/dist/detectors/unbalanced-bracket.js +23 -11
  209. package/dist/detectors/unbalanced-bracket.js.map +1 -1
  210. package/dist/detectors/vague-deadline.d.ts +25 -0
  211. package/dist/detectors/vague-deadline.d.ts.map +1 -0
  212. package/dist/detectors/vague-deadline.js +97 -0
  213. package/dist/detectors/vague-deadline.js.map +1 -0
  214. package/dist/furigana.d.ts +7 -0
  215. package/dist/furigana.d.ts.map +1 -0
  216. package/dist/furigana.js +16 -0
  217. package/dist/furigana.js.map +1 -0
  218. package/dist/genre-load.d.ts.map +1 -1
  219. package/dist/genre-load.js +5 -7
  220. package/dist/genre-load.js.map +1 -1
  221. package/dist/grade/ai-score.d.ts +16 -0
  222. package/dist/grade/ai-score.d.ts.map +1 -0
  223. package/dist/grade/ai-score.js +17 -0
  224. package/dist/grade/ai-score.js.map +1 -0
  225. package/dist/grade/finding.d.ts +6 -0
  226. package/dist/grade/finding.d.ts.map +1 -0
  227. package/dist/grade/finding.js +13 -0
  228. package/dist/grade/finding.js.map +1 -0
  229. package/dist/grade/grade-item.d.ts.map +1 -1
  230. package/dist/grade/grade-item.js +4 -11
  231. package/dist/grade/grade-item.js.map +1 -1
  232. package/dist/grade/render-variants.d.ts.map +1 -1
  233. package/dist/grade/render-variants.js +3 -0
  234. package/dist/grade/render-variants.js.map +1 -1
  235. package/dist/grade/render.d.ts.map +1 -1
  236. package/dist/grade/render.js +1 -0
  237. package/dist/grade/render.js.map +1 -1
  238. package/dist/grade/result.d.ts +3 -0
  239. package/dist/grade/result.d.ts.map +1 -1
  240. package/dist/grade/results-read.d.ts.map +1 -1
  241. package/dist/grade/results-read.js +13 -2
  242. package/dist/grade/results-read.js.map +1 -1
  243. package/dist/grade/summary.d.ts +3 -0
  244. package/dist/grade/summary.d.ts.map +1 -1
  245. package/dist/grade/summary.js +6 -0
  246. package/dist/grade/summary.js.map +1 -1
  247. package/dist/grade/text.d.ts +2 -0
  248. package/dist/grade/text.d.ts.map +1 -1
  249. package/dist/grade/text.js +2 -0
  250. package/dist/grade/text.js.map +1 -1
  251. package/dist/grade/variants-text.d.ts +3 -0
  252. package/dist/grade/variants-text.d.ts.map +1 -1
  253. package/dist/grade/variants-text.js +4 -0
  254. package/dist/grade/variants-text.js.map +1 -1
  255. package/dist/grade/variants.d.ts +3 -0
  256. package/dist/grade/variants.d.ts.map +1 -1
  257. package/dist/grade/variants.js +1 -0
  258. package/dist/grade/variants.js.map +1 -1
  259. package/dist/grade-api.d.ts +1 -0
  260. package/dist/grade-api.d.ts.map +1 -1
  261. package/dist/grade-api.js +2 -1
  262. package/dist/grade-api.js.map +1 -1
  263. package/dist/judge.d.ts +0 -1
  264. package/dist/judge.d.ts.map +1 -1
  265. package/dist/judge.js +0 -1
  266. package/dist/judge.js.map +1 -1
  267. package/dist/known-genres.js +2 -2
  268. package/dist/known-genres.js.map +1 -1
  269. package/dist/package-files.d.ts +6 -0
  270. package/dist/package-files.d.ts.map +1 -0
  271. package/dist/package-files.js +11 -0
  272. package/dist/package-files.js.map +1 -0
  273. package/dist/profile/load.d.ts.map +1 -1
  274. package/dist/profile/load.js +5 -6
  275. package/dist/profile/load.js.map +1 -1
  276. package/dist/rule-load.d.ts.map +1 -1
  277. package/dist/rule-load.js +5 -6
  278. package/dist/rule-load.js.map +1 -1
  279. package/dist/rule-offs.js +3 -3
  280. package/dist/rule-offs.js.map +1 -1
  281. package/dist/run.js +1 -1
  282. package/dist/run.js.map +1 -1
  283. package/dist/structure/date-range.d.ts +4 -0
  284. package/dist/structure/date-range.d.ts.map +1 -1
  285. package/dist/structure/date-range.js +23 -3
  286. package/dist/structure/date-range.js.map +1 -1
  287. package/dist/structure/issues.d.ts +17 -1
  288. package/dist/structure/issues.d.ts.map +1 -1
  289. package/dist/structure/issues.js +54 -5
  290. package/dist/structure/issues.js.map +1 -1
  291. package/dist/structure/party-role.d.ts +30 -0
  292. package/dist/structure/party-role.d.ts.map +1 -0
  293. package/dist/structure/party-role.js +58 -0
  294. package/dist/structure/party-role.js.map +1 -0
  295. package/dist/structure/prose-total.d.ts +28 -0
  296. package/dist/structure/prose-total.d.ts.map +1 -0
  297. package/dist/structure/prose-total.js +99 -0
  298. package/dist/structure/prose-total.js.map +1 -0
  299. package/dist/structure/reference-text.d.ts +5 -0
  300. package/dist/structure/reference-text.d.ts.map +1 -1
  301. package/dist/structure/reference-text.js +17 -4
  302. package/dist/structure/reference-text.js.map +1 -1
  303. package/dist/structure/term-prefix.d.ts +18 -0
  304. package/dist/structure/term-prefix.d.ts.map +1 -0
  305. package/dist/structure/term-prefix.js +40 -0
  306. package/dist/structure/term-prefix.js.map +1 -0
  307. package/dist/structure/total.d.ts +2 -0
  308. package/dist/structure/total.d.ts.map +1 -1
  309. package/dist/structure/total.js +2 -0
  310. package/dist/structure/total.js.map +1 -1
  311. package/dist/structure-shape/baseline-load.d.ts.map +1 -1
  312. package/dist/structure-shape/baseline-load.js +4 -5
  313. package/dist/structure-shape/baseline-load.js.map +1 -1
  314. package/dist/style-load.d.ts.map +1 -1
  315. package/dist/style-load.js +5 -6
  316. package/dist/style-load.js.map +1 -1
  317. package/package.json +24 -3
  318. package/rules/cliche.yaml +28 -8
  319. package/rules/connective-hierarchy.yaml +62 -0
  320. package/rules/date-without-year.yaml +28 -3
  321. package/rules/defined-term-form.yaml +9 -6
  322. package/rules/demonstrative-opener-run.yaml +24 -7
  323. package/rules/missing-final-period.yaml +76 -0
  324. package/rules/missing-space-after-comma.yaml +57 -0
  325. package/rules/no-mixed-desumasu.yaml +2 -2
  326. package/rules/orthographic-variant.yaml +2 -2
  327. package/rules/party-role-name.yaml +94 -0
  328. package/rules/quote-without-source.yaml +61 -0
  329. package/rules/total-mismatch.yaml +8 -8
  330. package/rules/vague-deadline.yaml +95 -0
  331. package/rules/weasel-word.yaml +25 -7
  332. package/skills/chaff/SKILL.md +32 -5
  333. package/src/adapter-import.ts +3 -0
  334. package/src/adapter-load.ts +2 -1
  335. package/src/ai-score/human-shares.ts +207 -0
  336. package/src/ai-score/lint-line.ts +10 -0
  337. package/src/ai-score/of-document.ts +46 -0
  338. package/src/ai-score/render.ts +124 -0
  339. package/src/ai-score/score.ts +121 -0
  340. package/src/ai-score/signals.ts +39 -0
  341. package/src/ai-score/text.ts +103 -0
  342. package/src/ai-score/view.ts +21 -0
  343. package/src/backends/anthropic.ts +0 -2
  344. package/src/backends/default-model.ts +9 -0
  345. package/src/backends/openai.ts +0 -2
  346. package/src/browser/adapter-import.ts +19 -0
  347. package/src/browser/bounded-match.ts +9 -0
  348. package/src/browser/check-text.ts +106 -0
  349. package/src/browser/detectors.ts +40 -0
  350. package/src/browser/import-meta-glob.d.ts +5 -0
  351. package/src/browser/package-files.ts +13 -0
  352. package/src/browser/path.ts +73 -0
  353. package/src/browser/working-dir.ts +3 -0
  354. package/src/browser-files.ts +87 -0
  355. package/src/browser.ts +19 -0
  356. package/src/cli-text.ts +2 -0
  357. package/src/cli.ts +4 -1
  358. package/src/commands/ai-score.ts +77 -0
  359. package/src/commands/document-commands.ts +2 -0
  360. package/src/config/by-path-problems.ts +17 -0
  361. package/src/config/load.ts +21 -8
  362. package/src/config/read.ts +5 -1
  363. package/src/config/warnings.ts +3 -1
  364. package/src/config/working-dir.ts +3 -0
  365. package/src/custom/bounded-match.ts +1 -16
  366. package/src/custom/pattern-timeout.ts +17 -0
  367. package/src/detect.ts +4 -5
  368. package/src/detectors/connective-hierarchy.ts +99 -0
  369. package/src/detectors/date-year.ts +51 -12
  370. package/src/detectors/defined-term-form.ts +28 -6
  371. package/src/detectors/missing-final-period.ts +79 -0
  372. package/src/detectors/missing-space-after-comma.ts +54 -0
  373. package/src/detectors/orthographic-variant.ts +59 -18
  374. package/src/detectors/party-role-name.ts +44 -0
  375. package/src/detectors/quote-without-source.ts +90 -0
  376. package/src/detectors/reference-text.ts +1 -1
  377. package/src/detectors/registry/connective-hierarchy.ts +4 -0
  378. package/src/detectors/registry/missing-final-period.ts +4 -0
  379. package/src/detectors/registry/missing-space-after-comma.ts +4 -0
  380. package/src/detectors/registry/party-role-name.ts +4 -0
  381. package/src/detectors/registry/quote-without-source.ts +4 -0
  382. package/src/detectors/registry/vague-deadline.ts +4 -0
  383. package/src/detectors/run-in-label.ts +60 -0
  384. package/src/detectors/sentence-counts.ts +13 -4
  385. package/src/detectors/sentence-ending.ts +3 -2
  386. package/src/detectors/structure-tree.ts +28 -6
  387. package/src/detectors/unbalanced-bracket.ts +25 -10
  388. package/src/detectors/vague-deadline.ts +132 -0
  389. package/src/furigana.ts +20 -0
  390. package/src/genre-load.ts +5 -7
  391. package/src/grade/ai-score.ts +32 -0
  392. package/src/grade/finding.ts +16 -0
  393. package/src/grade/grade-item.ts +4 -13
  394. package/src/grade/render-variants.ts +5 -0
  395. package/src/grade/render.ts +1 -0
  396. package/src/grade/result.ts +3 -0
  397. package/src/grade/results-read.ts +17 -2
  398. package/src/grade/summary.ts +9 -0
  399. package/src/grade/text.ts +6 -0
  400. package/src/grade/variants-text.ts +7 -0
  401. package/src/grade/variants.ts +4 -0
  402. package/src/grade-api.ts +3 -1
  403. package/src/judge.ts +0 -2
  404. package/src/known-genres.ts +2 -2
  405. package/src/package-files.ts +15 -0
  406. package/src/profile/load.ts +5 -6
  407. package/src/rule-load.ts +5 -6
  408. package/src/rule-offs.ts +3 -3
  409. package/src/run.ts +1 -1
  410. package/src/structure/date-range.ts +28 -3
  411. package/src/structure/issues.ts +61 -6
  412. package/src/structure/party-role.ts +94 -0
  413. package/src/structure/prose-total.ts +135 -0
  414. package/src/structure/reference-text.ts +17 -4
  415. package/src/structure/term-prefix.ts +54 -0
  416. package/src/structure/total.ts +4 -0
  417. package/src/structure-shape/baseline-load.ts +4 -5
  418. 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 }[])[];
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,99 @@
1
+ import type { Detector, Finding, LexiconEntry, Sentence, Span, Token } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { quotedSpans } from "../quoted-span.ts";
4
+
5
+ // A connective that joins only the outer level of a two-level list, in a sentence with no inner level under it. The
6
+ // lexicon holds both levels: an entry with a rewrite is an outer-level word (rewrite is the word a one-level list uses),
7
+ // and an entry with instead_of is an inner-level word that stands under that outer-level word.
8
+
9
+ /** One outer-level connective with nothing to stand above, and where it is in the document. */
10
+ export type LoneConnective = { readonly offset: number; readonly written: string; readonly usual: string };
11
+
12
+ const CONJUNCTION = "CCONJ";
13
+
14
+ /** The word lists the detector reads: the connectives, and the phrases that hold one's letters without being one (歯並びに). */
15
+ export type ConnectiveLists = { readonly connectives: readonly LexiconEntry[]; readonly lookalikes: readonly LexiconEntry[] };
16
+
17
+ /**
18
+ * The tagger does not always tell a connective from a noun and a particle: it reads 申請書並びに as 並び + に and
19
+ * 文字の並びに as the conjunction. After の, a determiner or an adjective (の並びに, この並びに, 正しい並びに) the word is
20
+ * a noun of its own. Where the tagger splits it, it is a connective only between two nouns: a noun, a number or a mark
21
+ * before it, and no verb right after it (一列並びに座る).
22
+ */
23
+ const MODIFIES_NOUN: ReadonlySet<string> = new Set(["DET", "ADJ"]);
24
+ const GENITIVE = "の";
25
+ const JOINS_AFTER: ReadonlySet<string> = new Set(["NOUN", "PROPN", "NUM", "PUNCT", "SYM"]);
26
+ const ENDS_CLAUSE: ReadonlySet<string> = new Set(["VERB", "AUX"]);
27
+
28
+ const modifiesNoun = (token: Token | undefined): boolean =>
29
+ token !== undefined && (MODIFIES_NOUN.has(token.pos) || (token.pos === "ADP" && token.surface === GENITIVE));
30
+
31
+ const joinsNouns = (previous: Token | undefined, next: Token | undefined): boolean =>
32
+ previous !== undefined && JOINS_AFTER.has(previous.pos) && next !== undefined && !ENDS_CLAUSE.has(next.pos);
33
+
34
+ /** A sentence's tokens by where they start and end, and the letters a connective there would not be read in. */
35
+ type Reading = { readonly byStart: ReadonlyMap<number, Token>; readonly byEnd: ReadonlyMap<number, Token>; readonly hidden: readonly boolean[] };
36
+
37
+ const placesOf = (text: string, word: string): number[] => [...text.matchAll(new RegExp(escapeRegExp(word), "gu"))].map((match) => match.index);
38
+
39
+ /** Inside 「」 or 『』 (a quoted phrase holds half a list) or inside a lookalike (歯並びに), by the sentence's own offsets. */
40
+ const hiddenLetters = (text: string, lookalikes: readonly LexiconEntry[]): boolean[] => {
41
+ const hidden = Array.from({ length: text.length }, () => false);
42
+ const hide = (start: number, end: number): boolean[] => hidden.fill(true, start, end);
43
+ quotedSpans(text).forEach((span) => hide(span.start, span.end));
44
+ lookalikes.forEach(({ pattern }) => placesOf(text, pattern).forEach((at) => hide(at, at + pattern.length)));
45
+ return hidden;
46
+ };
47
+
48
+ const readingOf = (sentence: Sentence, lookalikes: readonly LexiconEntry[]): Reading => {
49
+ const tokens = sentence.tokens ?? [];
50
+ return {
51
+ byStart: new Map(tokens.map((token) => [token.span.start, token])),
52
+ byEnd: new Map(tokens.map((token) => [token.span.end, token])),
53
+ hidden: hiddenLetters(sentence.text, lookalikes),
54
+ };
55
+ };
56
+
57
+ const standsAsConnective = (reading: Reading, span: Span): boolean => {
58
+ const previous = reading.byEnd.get(span.start);
59
+ if (modifiesNoun(previous)) return false;
60
+ const own = reading.byStart.get(span.start);
61
+ const tagged = own !== undefined && own.pos === CONJUNCTION && own.span.end === span.end;
62
+ return tagged || joinsNouns(previous, reading.byStart.get(span.end));
63
+ };
64
+
65
+ const hasInnerLevel = (sentence: Sentence, outer: LexiconEntry, connectives: readonly LexiconEntry[]): boolean =>
66
+ connectives.some((entry) => entry.instead_of === outer.pattern && sentence.text.includes(entry.pattern));
67
+
68
+ /** Where one outer-level word stands as a connective in the sentence. */
69
+ const connectiveSpans = (sentence: Sentence, outer: LexiconEntry, reading: Reading): Span[] =>
70
+ placesOf(sentence.text, outer.pattern)
71
+ .filter((at) => reading.hidden[at] !== true)
72
+ .map((at) => ({ start: sentence.span.start + at, end: sentence.span.start + at + outer.pattern.length }))
73
+ .filter((span) => standsAsConnective(reading, span));
74
+
75
+ /** The outer-level connectives of a sentence that stand over no inner level. */
76
+ export const loneConnectivesIn = (sentence: Sentence, lists: ConnectiveLists): LoneConnective[] => {
77
+ const lone = lists.connectives.filter(
78
+ (entry) => entry.rewrite !== undefined && sentence.text.includes(entry.pattern) && !hasInnerLevel(sentence, entry, lists.connectives),
79
+ );
80
+ if (lone.length === 0) return [];
81
+ const reading = readingOf(sentence, lists.lookalikes);
82
+ return lone
83
+ .flatMap((entry) => connectiveSpans(sentence, entry, reading).map((span) => ({ offset: span.start, written: entry.pattern, usual: entry.rewrite ?? "" })))
84
+ .sort((a, b) => a.offset - b.offset);
85
+ };
86
+
87
+ export const connectiveHierarchy: Detector = (doc, options): Finding[] => {
88
+ const lists = { connectives: options.lexicon ?? [], lookalikes: doc.lexicons["connective-lookalike"] ?? [] };
89
+ return doc.sentences.flatMap((sentence) =>
90
+ loneConnectivesIn(sentence, lists).map((lone) => ({
91
+ rule: "",
92
+ severity: "info",
93
+ line: 0,
94
+ column: 0,
95
+ quote: sentence.text.trim(),
96
+ values: { written: lone.written, usual: lone.usual, offset: lone.offset },
97
+ })),
98
+ );
99
+ };
@@ -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
  };
@@ -0,0 +1,79 @@
1
+ import type { Detector, Finding, LexiconEntry, Paragraph, ProseDocument, Token } from "../plugin.ts";
2
+ import { formMinority } from "./form-minority.ts";
3
+
4
+ // A paragraph whose last sentence stops without its closing mark, in a document whose other paragraphs end with one. It
5
+ // takes no side: a document that ends none of its paragraphs with the mark (slides, labels) is not reported. The marks
6
+ // come from the language's lexicon "final-period": group "end" is the period, group "other-end" closes a sentence another
7
+ // way (?, !) so the paragraph is not counted, and group "note" opens a note line. The
8
+ // lexicon is read directly rather than as the rule's word list, since its marks are what a fixed paragraph ends with.
9
+
10
+ /** A paragraph's end, and whether it closes with a sentence-ending mark. form is "closed" or "open". */
11
+ export type ParagraphEnd = { readonly offset: number; readonly form: string; readonly text: string };
12
+
13
+ /** The marks the detector reads, from the lexicon. */
14
+ export type PeriodMarks = { readonly ends: readonly string[]; readonly otherEnds: readonly string[]; readonly notes: readonly string[] };
15
+
16
+ /** Brackets and quotes that may follow a closing mark (〜です。」). */
17
+ const CLOSERS = '」』))"”’';
18
+ /** A line that is itself a quotation closes like a sentence. */
19
+ const QUOTE_CLOSERS = "」』";
20
+ /** A paragraph ending in a colon leads into a list or a block; it is not a sentence left open. */
21
+ const LEAD_IN = /[::]$/u;
22
+ /** Two spaces in a row, half- or full-width, lay a line out like a table (an ingredient, then its amount), so it is not a sentence. */
23
+ const LAYOUT = /[ \u3000]{2}/u;
24
+ /** Fewer characters than this is a label or a caption, not a sentence. */
25
+ const MIN_SENTENCE_CHARS = 15;
26
+ /** Words that end a clause: a verb, an adjective or an auxiliary in its dictionary form ends the sentence. */
27
+ const PREDICATE: ReadonlySet<string> = new Set(["VERB", "ADJ", "AUX"]);
28
+
29
+ /** The line's last mark before any closing brackets and quotes is one of these. */
30
+ const endsWithMark = (line: string, marks: readonly string[]): boolean => {
31
+ const chars = Array.from(line);
32
+ return marks.includes(chars[chars.findLastIndex((char) => !CLOSERS.includes(char))] ?? "");
33
+ };
34
+
35
+ const isClosed = (line: string, ends: readonly string[]): boolean => endsWithMark(line, ends) || QUOTE_CLOSERS.includes(line.at(-1) ?? "\0");
36
+
37
+ /** A note, a laid-out row, or a question or exclamation: not a sentence that could take the period. */
38
+ const isAside = (line: string, marks: PeriodMarks): boolean =>
39
+ marks.notes.some((note) => line.startsWith(note)) || LAYOUT.test(line) || endsWithMark(line, marks.otherEnds);
40
+
41
+ /** The paragraph's last word ends a sentence: a predicate written in its dictionary form (〜します, 〜である, 望ましい). */
42
+ const endsInPredicate = (paragraph: Paragraph): boolean => {
43
+ const last = (paragraph.sentences.at(-1)?.tokens ?? []).filter((token: Token) => token.surface.trim() !== "").at(-1);
44
+ return last !== undefined && PREDICATE.has(last.pos) && last.lemma === last.surface;
45
+ };
46
+
47
+ /** How a paragraph ends, or undefined when its last line is not a sentence (a label, a lead-in, a note, a laid-out row). */
48
+ export const paragraphEnd = (text: string, paragraph: Paragraph, marks: PeriodMarks): ParagraphEnd | undefined => {
49
+ const body = text.slice(paragraph.span.start, paragraph.span.end).trimEnd();
50
+ const last = body.split("\n").at(-1)?.trim() ?? "";
51
+ if (Array.from(last).length < MIN_SENTENCE_CHARS || LEAD_IN.test(last)) return undefined;
52
+ const offset = paragraph.span.start + body.length - 1;
53
+ if (isAside(last, marks)) return undefined;
54
+ if (isClosed(last, marks.ends)) return { offset, form: "closed", text: last };
55
+ // A closing parenthetical after the sentence (〜します(別紙を参照する)) is not where the period goes.
56
+ return !CLOSERS.includes(last.at(-1) ?? "\0") && endsInPredicate(paragraph) ? { offset, form: "open", text: last } : undefined;
57
+ };
58
+
59
+ const marksOf = (lexicon: readonly LexiconEntry[]): PeriodMarks => ({
60
+ ends: lexicon.filter((entry) => entry.group === "end").map((entry) => entry.pattern),
61
+ otherEnds: lexicon.filter((entry) => entry.group === "other-end").map((entry) => entry.pattern),
62
+ notes: lexicon.filter((entry) => entry.group === "note").map((entry) => entry.pattern),
63
+ });
64
+
65
+ export const missingFinalPeriod: Detector = (doc: ProseDocument, options): Finding[] => {
66
+ const text = doc.prose ?? doc.source;
67
+ const marks = marksOf(doc.lexicons["final-period"] ?? []);
68
+ const ends = doc.paragraphs.flatMap((paragraph) => paragraphEnd(text, paragraph, marks) ?? []);
69
+ const minority = formMinority(ends, options.limit);
70
+ if (minority === undefined || minority.usual.form !== "closed") return [];
71
+ return minority.odd.map((end) => ({
72
+ rule: "",
73
+ severity: "info",
74
+ line: 0,
75
+ column: 0,
76
+ quote: end.text,
77
+ values: { count: minority.odd.length, of: ends.length, offset: end.offset },
78
+ }));
79
+ };
@@ -0,0 +1,54 @@
1
+ import type { Detector, Finding } from "../plugin.ts";
2
+ import { quoteAround } from "./quote-around.ts";
3
+
4
+ // A comma between two words with no space after it ("address,and"): a slip in English prose. A run of values joined by
5
+ // commas ("blue,black,brown"), and anything inside an address, a path or an assignment, is data, not prose.
6
+
7
+ /** One comma with no space after it, where it is, and the two words it joins as written. */
8
+ export type TightComma = { readonly offset: number; readonly written: string };
9
+
10
+ /** A word of three letters or more (a possessive 's may follow), a comma, and a lower-case word of three letters or more. */
11
+ const JOINED = /(?<![\p{L}\p{N}])[A-Za-z]{3,}(?:['’]s)?,[a-z]{3,}(?![\p{L}\p{N}])/u;
12
+ /** Characters that make a run of text an address, a path, a parameter or data (a=b, a/b, a.b, @a, #a, a_b, 3,abc). */
13
+ const DATA_MARK = /[/\\=@#:.&?_|+*~$%^<>{}[\]\p{N}]/u;
14
+ /** Opening and closing marks a word of prose may carry ("(and,then)"), and the sentence's own end ("and,then."). */
15
+ const OPENERS = "(\"'“‘[";
16
+ const CLOSERS = ")\"'”’].,;!?";
17
+ /** A run quoted whole, inside brackets or not ("true,false", ("true,false")): a value written out, not two words of the sentence. */
18
+ const QUOTED_WHOLE = /^[([]*["'“‘].*["'”’][)\].,;!?]*$/u;
19
+ /** A run of text between whitespace. */
20
+ const RUN = /\S+/gu;
21
+ const COMMA = ",";
22
+
23
+ /** The run without the marks around it, trimmed by scanning rather than by a pattern that could backtrack. */
24
+ const bareOf = (run: string): string => {
25
+ const chars = run.split("");
26
+ const start = chars.findIndex((char) => !OPENERS.includes(char));
27
+ const end = chars.findLastIndex((char) => !CLOSERS.includes(char));
28
+ return start === -1 || end < start ? "" : run.slice(start, end + 1);
29
+ };
30
+
31
+ /** A run holding one comma and nothing that marks it as data. */
32
+ const isProseRun = (run: string): boolean => {
33
+ const bare = bareOf(run);
34
+ return bare.split(COMMA).length === 2 && !DATA_MARK.test(bare) && !QUOTED_WHOLE.test(run);
35
+ };
36
+
37
+ /** Each run is read once, so a long line of values costs one pass. */
38
+ export const tightCommasIn = (text: string): TightComma[] =>
39
+ [...text.matchAll(RUN)].flatMap((run) => {
40
+ const joined = isProseRun(run[0]) ? JOINED.exec(run[0]) : null;
41
+ return joined === null ? [] : [{ offset: run.index + joined.index + joined[0].indexOf(COMMA), written: joined[0] }];
42
+ });
43
+
44
+ export const missingSpaceAfterComma: Detector = (doc): Finding[] => {
45
+ const text = doc.prose ?? doc.source;
46
+ return tightCommasIn(text).map((comma) => ({
47
+ rule: "",
48
+ severity: "warning",
49
+ line: 0,
50
+ column: 0,
51
+ quote: quoteAround(text, comma.offset, comma.offset + 1),
52
+ values: { written: comma.written, offset: comma.offset },
53
+ }));
54
+ };