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
@@ -0,0 +1,54 @@
1
+ // A breakdown written in one sentence (「構成比は、Aが50%、Bが30%、Cが20%」, "By product, A made up 50% of sales, B 30% and
2
+ // C 20%") whose shares do not add up to 100%. The table and list form is structure/percent-sum.ts; the words are the same.
3
+ import type { Span } from "../plugin.ts";
4
+ import type { StructureIssue } from "./issues.ts";
5
+ import type { Amount } from "./total.ts";
6
+ import type { ShareWords } from "./percent-sum.ts";
7
+
8
+ const WHOLE = 100;
9
+ /** A sentence names two shares in passing (「AとBで50%」); a breakdown in prose names at least three parts. */
10
+ const MIN_PARTS = 3;
11
+ /**
12
+ * How far from 100% a sentence may add up and still read as one whole broken down. Far below it, the sentence names only the
13
+ * larger parts (「Aが50%、Bが20%、Cが10%など」); far above it, the percentages are of different wholes ("55% of directors,
14
+ * 48% of managers") or of two periods ("45% (40% last year)"), or the total is among them (「合計100%」).
15
+ */
16
+ const BAND = 10;
17
+ const HALF = 0.5;
18
+ const DECIMAL_BASE = 10;
19
+ const DECIMALS = /\.(\d+)/u;
20
+ /** A sign before the number (+3%, ▲2%): a change, not a part. */
21
+ const SIGNS = "-+−▲△";
22
+
23
+ const includesAny = (text: string, words: readonly string[]): boolean => {
24
+ const lowered = text.toLowerCase();
25
+ return words.some((word) => lowered.includes(word.toLowerCase()));
26
+ };
27
+
28
+ const decimalsOf = (source: string, amount: Amount): number => DECIMALS.exec(source.slice(amount.offset, amount.end))?.[1]?.length ?? 0;
29
+
30
+ const isSigned = (source: string, amount: Amount): boolean => SIGNS.includes(source.slice(0, amount.offset).trimEnd().at(-1) ?? " ");
31
+
32
+ /** The rounding the parts allow: half the last digit each, at least one point. */
33
+ const toleranceOf = (source: string, shares: readonly Amount[]): number =>
34
+ Math.max(1, shares.length * HALF * DECIMAL_BASE ** -Math.max(0, ...shares.map((share) => decimalsOf(source, share))));
35
+
36
+ const isBroken = (sum: number, tolerance: number): boolean => Math.abs(sum - WHOLE) > tolerance && Math.abs(sum - WHOLE) <= BAND;
37
+
38
+ const issueIn = (source: string, sentence: Span, percents: readonly Amount[], words: ShareWords): StructureIssue[] => {
39
+ const text = source.slice(sentence.start, sentence.end);
40
+ if (!includesAny(text, words.labels) || includesAny(text, words.exceptions)) return [];
41
+ const shares = percents.filter((amount) => amount.offset >= sentence.start && amount.end <= sentence.end);
42
+ if (shares.length < MIN_PARTS || shares.some((share) => isSigned(source, share))) return [];
43
+ const sum = shares.reduce((total, share) => total + share.value, 0);
44
+ const decimals = Math.max(0, ...shares.map((share) => decimalsOf(source, share)));
45
+ if (!isBroken(sum, toleranceOf(source, shares))) return [];
46
+ return [{ offset: shares[0]?.offset ?? sentence.start, values: { sum: `${sum.toFixed(decimals)}${shares[0]?.unit ?? ""}` } }];
47
+ };
48
+
49
+ /** Each sentence that names a share word and three or more unsigned percentages whose sum misses 100% by more than rounding, and by no more than BAND. */
50
+ export const proseShareMismatches = (source: string, sentences: readonly Span[], amounts: readonly Amount[], words: ShareWords): StructureIssue[] => {
51
+ if (words.labels.length === 0) return [];
52
+ const percents = amounts.filter((amount) => words.units.includes(amount.unit));
53
+ return sentences.flatMap((sentence) => issueIn(source, sentence, percents, words));
54
+ };
@@ -0,0 +1,135 @@
1
+ import type { StructureIssue } from "./issues.ts";
2
+ import { shownTotal, type Amount } from "./total.ts";
3
+
4
+ /**
5
+ * 文の中で書いた合計と内訳。表や箇条書きでなく、一つの文に並べた金額を読む。二つの形がある。
6
+ * 内訳のあとに合計: 「保守費60,000円、運用費30,000円とし、合計100,000円とする」 "$6,000 for hosting and $3,000 for support, for a total of $9,000"
7
+ * 合計のあとに内訳: 「月額150,000円(内訳:保守費90,000円、運用費60,000円)」 "costs $8 per month, made up of a base fee of $6 and a storage fee of $2"
8
+ * 合計の語(total-phrase)はすぐ後ろに合計の金額が来る語、内訳の語(breakdown-phrase)はすぐ後ろから内訳が始まる語。
9
+ * 内訳は合計と同じ単位の金額で、二つ以上。合計の語のすぐ前(内訳の語のすぐ後ろ)から続く並びのどれかの和が合計と合えば
10
+ * 合っているとみなし、どれとも合わないときだけ言う。文の中のほかの金額(単価・上限・遅延損害金)を内訳に数えて誤って言わないため。
11
+ * 値引きの語(discount-word)のすぐ後ろの金額は引く。内訳の語の前の金額は、あいだに並びをつなぐ語(breakdown-gap-joiner)が
12
+ * あれば、別の項目の金額で合計ではない(a $1,000 onboarding fee and a monthly subscription, consisting of …)。
13
+ */
14
+ export type TotalWords = {
15
+ readonly totals: readonly string[];
16
+ readonly breakdowns: readonly string[];
17
+ readonly discounts: readonly string[];
18
+ readonly joiners: readonly string[];
19
+ };
20
+
21
+ type Span = { readonly start: number; readonly end: number };
22
+
23
+ const CENTS = 100;
24
+ const MIN_PARTS = 2;
25
+
26
+ /** 合計の語と金額のあいだに書いてよいもの: 空白と、コロンか読点一つ。 */
27
+ const isTotalGap = (gap: string): boolean => /^[::、,,]?$/u.test(gap.trim());
28
+ const OPENERS = "((";
29
+ const CLOSERS = "))";
30
+ const CLOSER = /[))]/u;
31
+
32
+ const centsOf = (amount: Amount): number => Math.round(amount.value * CENTS);
33
+
34
+ const sumOf = (values: readonly number[]): number => values.reduce((sum, value) => sum + value, 0);
35
+
36
+ /** 英字で始まる語は、英字の続き(subtotal の total)では語と読まない。 */
37
+ const LATIN = /[A-Za-z0-9]/u;
38
+
39
+ const occurrences = (lower: string, phrase: string): number[] =>
40
+ [...lower.matchAll(new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "gu"))].map((match) => match.index);
41
+
42
+ /** 語の文の中の位置。重なるときは前から、同じ所から始まるなら長いほうを取る。大文字小文字は区別しない。 */
43
+ export const phraseSpans = (text: string, start: number, phrases: readonly string[]): Span[] => {
44
+ const lower = text.toLowerCase();
45
+ const found = phrases
46
+ .map((phrase) => phrase.toLowerCase())
47
+ .filter((phrase) => phrase !== "")
48
+ .flatMap((phrase) =>
49
+ occurrences(lower, phrase)
50
+ .filter((at) => !(LATIN.test(phrase[0] ?? "") && LATIN.test(lower[at - 1] ?? "")))
51
+ .map((at) => ({ start: at, end: at + phrase.length })),
52
+ )
53
+ .toSorted((left, right) => left.start - right.start || right.end - left.end);
54
+ return found
55
+ .filter((span, index) => found.slice(0, index).every((before) => before.end <= span.start))
56
+ .map((span) => ({ start: start + span.start, end: start + span.end }));
57
+ };
58
+
59
+ /** 金額の前に書いた単位($9,000 の $)を除いた、語と金額のあいだ。 */
60
+ const gapBefore = (source: string, from: number, amount: Amount): string => {
61
+ const gap = source.slice(from, amount.offset);
62
+ const trimmed = gap.trimEnd();
63
+ return trimmed.endsWith(amount.unit) ? trimmed.slice(0, trimmed.length - amount.unit.length) : gap;
64
+ };
65
+
66
+ /** 合計の語に近いほうから続く、二つ以上の並び(後ろ寄せ: 合計の前の内訳、前寄せ: 合計の後ろの内訳)。 */
67
+ const nearRuns = (parts: readonly number[], nearEnd: boolean): number[][] =>
68
+ parts.slice(MIN_PARTS - 1).map((_, index) => (nearEnd ? parts.slice(parts.length - MIN_PARTS - index) : parts.slice(0, MIN_PARTS + index)));
69
+
70
+ /** 内訳の金額。前の金額(並びの始まり)からこの金額までに値引きの語があれば、負の数。 */
71
+ const signedValues = (source: string, parts: readonly Amount[], from: number, discounts: readonly string[]): number[] =>
72
+ parts.map((part, index) => {
73
+ const start = parts[index - 1]?.end ?? from;
74
+ const discounted = phraseSpans(source.slice(start, part.offset), start, discounts).length > 0;
75
+ return discounted ? -centsOf(part) : centsOf(part);
76
+ });
77
+
78
+ type Breakdown = { readonly total: Amount; readonly parts: readonly Amount[]; readonly from: number; readonly nearEnd: boolean };
79
+
80
+ const mismatch = (source: string, breakdown: Breakdown, discounts: readonly string[]): StructureIssue[] => {
81
+ const { total, from, nearEnd } = breakdown;
82
+ const parts = breakdown.parts.filter((part) => part.unit === total.unit);
83
+ if (parts.length < MIN_PARTS) return [];
84
+ const values = signedValues(source, parts, from, discounts);
85
+ const written = centsOf(total);
86
+ if (nearRuns(values, nearEnd).some((run) => sumOf(run) === written)) return [];
87
+ return [{ offset: total.offset, values: shownTotal(source, total, sumOf(values)) }];
88
+ };
89
+
90
+ /** 内訳の語が括弧の中にあれば、その括弧の終わり。無ければ文の終わり。 */
91
+ const breakdownEnd = (source: string, total: Amount, marker: Span, sentence: Span): number => {
92
+ const between = source.slice(total.end, marker.start);
93
+ const opened = [...between].filter((char) => OPENERS.includes(char)).length > [...between].filter((char) => CLOSERS.includes(char)).length;
94
+ if (!opened) return sentence.end;
95
+ const close = CLOSER.exec(source.slice(marker.end, sentence.end));
96
+ return close === null ? sentence.end : marker.end + close.index;
97
+ };
98
+
99
+ const inside = (amounts: readonly Amount[], start: number, end: number): Amount[] => amounts.filter((amount) => amount.offset >= start && amount.end <= end);
100
+
101
+ /** 合計の語のすぐ後ろの金額を、その前の内訳と比べる。内訳は、前の合計の語か内訳の語より後ろから。 */
102
+ const totalsAfterParts = (source: string, sentence: Span, amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
103
+ const text = source.slice(sentence.start, sentence.end);
104
+ const markers = phraseSpans(text, sentence.start, words.totals);
105
+ const breakdowns = phraseSpans(text, sentence.start, words.breakdowns);
106
+ return markers.flatMap((marker, index) => {
107
+ const total = amounts.find((amount) => amount.offset >= marker.end && isTotalGap(gapBefore(source, marker.end, amount)));
108
+ if (total === undefined) return [];
109
+ const from = Math.max(sentence.start, markers[index - 1]?.end ?? 0, ...breakdowns.filter((span) => span.end <= marker.start).map((span) => span.end));
110
+ return mismatch(source, { total, parts: inside(amounts, from, marker.start), from, nearEnd: true }, words.discounts);
111
+ });
112
+ };
113
+
114
+ /** 内訳の語のすぐ前の金額を合計とし、内訳の語の後ろ(括弧の中なら括弧の終わりまで、次の合計の語まで)の金額と比べる。 */
115
+ const totalsBeforeParts = (source: string, sentence: Span, amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
116
+ const text = source.slice(sentence.start, sentence.end);
117
+ const totalMarkers = phraseSpans(text, sentence.start, words.totals);
118
+ return phraseSpans(text, sentence.start, words.breakdowns).flatMap((marker) => {
119
+ const total = amounts.findLast((amount) => amount.end <= marker.start);
120
+ if (total === undefined || phraseSpans(source.slice(total.end, marker.start), total.end, words.joiners).length > 0) return [];
121
+ const nextTotal = totalMarkers.find((span) => span.start >= marker.end)?.start ?? sentence.end;
122
+ const parts = inside(amounts, marker.end, Math.min(nextTotal, breakdownEnd(source, total, marker, sentence)));
123
+ return mismatch(source, { total, parts, from: marker.end, nearEnd: false }, words.discounts);
124
+ });
125
+ };
126
+
127
+ /** 文ごとに、書いた合計が内訳の和と合わない所。一つの金額は一度だけ言う。 */
128
+ export const proseTotalMismatches = (source: string, sentences: readonly Span[], amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
129
+ const issues = sentences.flatMap((sentence) => {
130
+ const inSentence = inside(amounts, sentence.start, sentence.end);
131
+ if (inSentence.length <= MIN_PARTS) return [];
132
+ return [...totalsAfterParts(source, sentence, inSentence, words), ...totalsBeforeParts(source, sentence, inSentence, words)];
133
+ });
134
+ return issues.filter((issue, index) => issues.findIndex((other) => other.offset === issue.offset) === index);
135
+ };
@@ -54,11 +54,24 @@ const nameAfter = (source: string, end: number): { name: string; at: number } |
54
54
  /** 項や号の札((a)、(ii)、(イ))。名前ではなく、参照の続き。 */
55
55
  const ITEM_LABEL = /^(?:[a-z]{1,2}|[ivxlcdm]{1,5}|.)$/iu;
56
56
 
57
+ const LATIN_ASIDE = /^[\p{Script=Latin}\s.]+$/u;
58
+
59
+ /**
60
+ * The name holds the aside. An English aside is a whole word or phrase: "or" in "Return of Information" is not the aside
61
+ * "or". Other asides are found anywhere (上記 in 上記参照).
62
+ */
63
+ export const holdsAside = (name: string, aside: string): boolean => {
64
+ if (aside.trim() === "") return false;
65
+ if (!LATIN_ASIDE.test(aside)) return normalized(name).includes(normalized(aside));
66
+ const words = ` ${name
67
+ .normalize("NFKC")
68
+ .toLowerCase()
69
+ .replaceAll(/[^\p{L}\p{N}]+/gu, " ")} `;
70
+ return aside.trim() !== "" && words.includes(` ${aside.trim().toLowerCase()} `);
71
+ };
72
+
57
73
  const isName = (name: string, words: ReferenceWords): boolean =>
58
- LETTER.test(name) &&
59
- !DIGIT.test(name) &&
60
- !ITEM_LABEL.test(name.normalize("NFKC")) &&
61
- !words.asides.some((aside) => normalized(name).includes(normalized(aside)));
74
+ LETTER.test(name) && !DIGIT.test(name) && !ITEM_LABEL.test(name.normalize("NFKC")) && !words.asides.some((aside) => holdsAside(name, aside));
62
75
 
63
76
  const agrees = (name: string, heading: string): boolean => {
64
77
  const [a, b] = [normalized(name), normalized(heading)];
@@ -0,0 +1,54 @@
1
+ import type { BodyText } from "./definition-use.ts";
2
+
3
+ /**
4
+ * 定義した語の頭の語(本・本件)を、同じ組のほかの語に替えた形。「本業務」と定義して「本件業務」と書くと、
5
+ * 読み手は別のものを指しているのかと迷う。組は言語パッケージの語彙表(defined-term-prefix)の group から取る。
6
+ */
7
+ export type PrefixGroups = readonly (readonly string[])[];
8
+
9
+ /** 語の前後の一字がこれなら、語は長い語の一部(見本物品の「本物品」、本業務委託契約の「本業務」)。 */
10
+ const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
11
+
12
+ /** 頭の語のあとがこれより短い語(本件人、本件日)を替えると、普通の語(本人、本日)になる。 */
13
+ const MIN_REST = 2;
14
+
15
+ /** 語彙表の項目を group ごとにまとめる。group の無い項目と、一つしかない組は外す。 */
16
+ export const prefixGroupsOf = (entries: readonly { readonly pattern: string; readonly group?: string | undefined }[]): string[][] => {
17
+ const groups = new Map<string, string[]>();
18
+ entries.forEach((entry) => {
19
+ if (entry.group === undefined || entry.pattern === "") return;
20
+ groups.set(entry.group, [...(groups.get(entry.group) ?? []), entry.pattern]);
21
+ });
22
+ return [...groups.values()].filter((group) => group.length > 1);
23
+ };
24
+
25
+ /** 語の頭の語を同じ組のほかの語に替えた形。頭の語は長いほうから当て、頭の語のあとが一字の語には形が無い。 */
26
+ export const prefixVariants = (term: string, groups: PrefixGroups): string[] =>
27
+ groups.flatMap((group) => {
28
+ const prefix = group
29
+ .filter((candidate) => term.startsWith(candidate) && term.length > candidate.length)
30
+ .toSorted((left, right) => right.length - left.length)[0];
31
+ const rest = prefix === undefined ? "" : term.slice(prefix.length);
32
+ if (prefix === undefined || [...rest].length < MIN_REST) return [];
33
+ return group.filter((other) => other !== prefix).map((other) => `${other}${rest}`);
34
+ });
35
+
36
+ /**
37
+ * 定義の後ろ(after より後)で、形の違う語を書いた位置。長い語の一部と、定義した語の始まり(本件物品明細と定義した所の本件物品)は除く。
38
+ */
39
+ export const variantUses = (source: string, texts: readonly BodyText[], variant: string, after: number, defined: readonly string[]): number[] =>
40
+ texts.flatMap((text) => {
41
+ const offsets: number[] = [];
42
+ let at = text.text.indexOf(variant);
43
+ while (at !== -1) {
44
+ offsets.push(text.start + at);
45
+ at = text.text.indexOf(variant, at + variant.length);
46
+ }
47
+ return offsets.filter(
48
+ (offset) =>
49
+ offset > after &&
50
+ !WORD_CHAR.test(source.charAt(offset - 1)) &&
51
+ !WORD_CHAR.test(source.charAt(offset + variant.length)) &&
52
+ !defined.some((term) => source.startsWith(term, offset)),
53
+ );
54
+ });
@@ -154,6 +154,10 @@ const shownAmounts = (source: string, total: Placed, sumCents: number): Record<s
154
154
  return { written, sum: `${sumSign}${sum}${written.endsWith(total.unit) ? total.unit : ""}` };
155
155
  };
156
156
 
157
+ /** 文の中の合計の金額(符号の無い、単位付きの金額)と和を、合計の書き方で見せる。 */
158
+ export const shownTotal = (source: string, total: Amount, sumCents: number): Record<string, string> =>
159
+ shownAmounts(source, { ...total, column: 0, cents: Math.round(total.value * CENTS) }, sumCents);
160
+
157
161
  /**
158
162
  * 数だけの列で、合計の行の数が項目のどれかより大きくなければ、和ではなく平均や率や年の列と読んで足さない。
159
163
  * 項目に負の数があれば、和が項目より小さいこともあるので足す。
@@ -0,0 +1,103 @@
1
+ // A version written in the lead-in to a code block ("Install version 2.3.0 from npm:") that the block itself does not
2
+ // show (npm install tidyq@2.4.0). Pure: reads the source, the code blocks, and the prose with code covered.
3
+ import type { Span } from "../plugin.ts";
4
+ import type { StructureIssue } from "./issues.ts";
5
+
6
+ /**
7
+ * A dotted version of three parts (2.4.0, v1.12.3, 3.0.0-beta.2). Two parts (22.04, 3.1) are too often a decimal, and a
8
+ * first part of four digits is too often a date (2024.01.15).
9
+ */
10
+ const VERSION = /(?<![\w.])v?(\d{1,3}\.\d{1,4}\.\d{1,4}(?:-[0-9A-Za-z.]+)?)(?![\w.]|\.\d)/gu;
11
+
12
+ /** A word that makes a version a bound or a starting point, not the version installed: 「2.3.0 から」 "from 2.3.0", "20.0.0 or later". */
13
+ export type RangeWord = { readonly word: string; readonly position: "before" | "after" };
14
+
15
+ /** What tells which versions are the block's: the bound words, and the Latin words that lead to the version installed. */
16
+ export type VersionWords = { readonly ranges: readonly RangeWord[]; readonly versionWords: readonly string[] };
17
+
18
+ /** One code block: where it starts and the text between its fences. */
19
+ export type CodeBlock = { readonly start: number; readonly code: string };
20
+
21
+ /** How far a range word may stand from the version (a space, a "v"). */
22
+ const RANGE_GAP = 2;
23
+
24
+ type Found = { readonly start: number; readonly end: number; readonly version: string };
25
+
26
+ const versionsIn = (text: string, offset = 0): Found[] =>
27
+ [...text.matchAll(VERSION)].map((match) => ({ start: offset + match.index, end: offset + match.index + match[0].length, version: match[1] ?? "" }));
28
+
29
+ const isBound = (prose: string, found: Span, words: readonly RangeWord[]): boolean =>
30
+ words.some(({ word, position }) => {
31
+ if (word === "") return false;
32
+ const near =
33
+ position === "after"
34
+ ? prose.slice(found.end, found.end + RANGE_GAP + word.length)
35
+ : prose.slice(Math.max(0, found.start - RANGE_GAP - word.length), found.start);
36
+ return near.toLowerCase().includes(word.toLowerCase());
37
+ });
38
+
39
+ /** The names a block pins its version to: tidyq in tidyq@2.4.0, tidyq==2.4.0, tidyq=2.4.0, tidyq:2.4.0, "tidyq": "2.4.0". */
40
+ const PINNED = /^([\w.-]+)(?:@|==|=|:)[v^~]?\d/u;
41
+ const QUOTES = /["']/gu;
42
+ const OPENS_WITH_NUMBER = /^[v^~]?\d/u;
43
+
44
+ /** Each whitespace-separated word of the code, quotes dropped; "tidyq": "2.4.0" is read as tidyq:2.4.0. */
45
+ const codeWords = (code: string): string[] => {
46
+ const words = code.split(/\s+/u).map((word) => word.replaceAll(QUOTES, "").replace(/,$/u, ""));
47
+ return words.map((word, index) => (word.endsWith(":") && OPENS_WITH_NUMBER.test(words[index + 1] ?? "") ? `${word}${words[index + 1] ?? ""}` : word));
48
+ };
49
+
50
+ const pinnedNames = (code: string): Set<string> => new Set(codeWords(code).flatMap((word) => PINNED.exec(word)?.[1]?.toLowerCase() ?? []));
51
+
52
+ const LATIN = /^[A-Za-z]/u;
53
+ const WORD_CHAR = /[\w.+-]/u;
54
+ /** How far back the word before a version is looked for. */
55
+ const WORD_REACH = 40;
56
+
57
+ /** The Latin word right before a position, if the word there is Latin: "Node.js" in "Requires Node.js 18.17.0". */
58
+ const latinWordBefore = (text: string): string | undefined => {
59
+ const last = [...(text.trimEnd().split(/\s/u).at(-1) ?? "")];
60
+ const word = last.slice(0, last.findLastIndex((char) => WORD_CHAR.test(char)) + 1).join("");
61
+ return LATIN.test(word) ? word.toLowerCase() : undefined;
62
+ };
63
+
64
+ /**
65
+ * Whether a lead-in version is about the block's software. A Latin word right before it must be one of the words that lead
66
+ * to the version installed (version, release, install; lexicon version-lead) or the pinned software: "Requires Node.js
67
+ * 18.17.0" over `npm install pkg@2.4.0` names another program. A version after no Latin word (「npm から 2.3.0」) is the block's.
68
+ */
69
+ const isAboutBlock = (prose: string, found: Found, names: ReadonlySet<string>, versionWords: readonly string[]): boolean => {
70
+ const word = latinWordBefore(prose.slice(Math.max(0, found.start - WORD_REACH), found.start));
71
+ return word === undefined || names.has(word) || versionWords.some((name) => name.toLowerCase() === word);
72
+ };
73
+
74
+ const BLANK_LINE = /\r?\n[ \t]*\r?\n/gu;
75
+ const NOT_PROSE = /^\s*(?:#|[-*+]\s|\d{1,3}[.)]\s|```|~~~|\|)/u;
76
+ const LETTER = /\p{L}/u;
77
+
78
+ /** The paragraph right above a code block: the lines up to the blank line before it. A heading, a list item, a table or a bare version above is not a lead-in. */
79
+ const leadInOf = (source: string, blockStart: number): Span | undefined => {
80
+ const lines = source.slice(0, blockStart).trimEnd();
81
+ const last = [...lines.matchAll(BLANK_LINE)].at(-1);
82
+ const start = last === undefined ? 0 : last.index + last[0].length;
83
+ const text = lines.slice(start);
84
+ const words = text.replace(VERSION, "");
85
+ return text === "" || NOT_PROSE.test(text) || !LETTER.test(words) ? undefined : { start, end: lines.length };
86
+ };
87
+
88
+ /**
89
+ * Each version in a code block's lead-in that differs from the one version the block shows. A block with no version, or
90
+ * with two (an upgrade from one to the other), says nothing to compare; a version marked as a bound, or named after another
91
+ * program, is not the one installed.
92
+ */
93
+ export const versionMismatches = (source: string, prose: string, blocks: readonly CodeBlock[], words: VersionWords): StructureIssue[] =>
94
+ blocks.flatMap((block) => {
95
+ const shown = [...new Set(versionsIn(block.code).map((found) => found.version))];
96
+ const leadIn = leadInOf(source, block.start);
97
+ const [code] = shown;
98
+ if (shown.length !== 1 || code === undefined || leadIn === undefined) return [];
99
+ const names = pinnedNames(block.code);
100
+ return versionsIn(prose.slice(leadIn.start, leadIn.end), leadIn.start)
101
+ .filter((found) => found.version !== code && !isBound(prose, found, words.ranges) && isAboutBlock(prose, found, names, words.versionWords))
102
+ .map((found) => ({ offset: found.start, values: { written: found.version, code } }));
103
+ });
@@ -5,6 +5,7 @@ import { inDocumentOrder } from "./issues.ts";
5
5
  // 日付と、その横に書いた曜日の食い違い。暦で決まるので、両方読めれば誤りは機械で言える。
6
6
 
7
7
  const FULL_DATE = /^(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})$/u;
8
+ const MONTH_DAY = /^(?<m>\d{2})-(?<d>\d{2})$/u;
8
9
 
9
10
  /** 暦の曜日。日曜日が 0。存在しない日(2月30日)は undefined。 */
10
11
  export const weekdayOf = (value: string): number | undefined => {
@@ -15,11 +16,87 @@ export const weekdayOf = (value: string): number | undefined => {
15
16
  return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? date.getUTCDay() : undefined;
16
17
  };
17
18
 
18
- /** 年まで書いた日付で、書いた曜日が暦と違うもの。年の無い日付は曜日を決められないので見ない。 */
19
- export const weekdayMismatches = (tree: StructureNode): StructureIssue[] =>
20
- inDocumentOrder(tree).flatMap((node) => {
19
+ const DAY_MS = 86_400_000;
20
+ /** 年を書いた日付から 5 か月以内なら、前後の年のどれか一つだけがそこに収まる。半年を超えると、どちらの年とも読める。 */
21
+ const YEAR_REACH_DAYS = 153;
22
+
23
+ const timeOf = (value: string): number | undefined => {
24
+ const groups = FULL_DATE.exec(value)?.groups;
25
+ return groups === undefined || weekdayOf(value) === undefined ? undefined : Date.UTC(Number(groups["y"]), Number(groups["m"]) - 1, Number(groups["d"]));
26
+ };
27
+
28
+ /**
29
+ * 年の無い月日(「10-09」)に、年を書いた日付(anchor、「2026-10-06」)の前後の年のうち、anchor から 5 か月以内に収まる年を付ける。
30
+ * メールや議事録は、文書の日付の年を書いて、そのあとの日付の年を省く。12月の文書の「1月5日」は翌年。どの年も収まらなければ undefined。
31
+ */
32
+ export const withNearestYear = (monthDay: string, anchor: string): string | undefined => {
33
+ const groups = MONTH_DAY.exec(monthDay)?.groups;
34
+ const anchorYear = FULL_DATE.exec(anchor)?.groups?.["y"];
35
+ const anchorTime = timeOf(anchor);
36
+ if (groups === undefined || anchorYear === undefined || anchorTime === undefined) return undefined;
37
+ return [-1, 0, 1]
38
+ .map((offset) => `${String(Number(anchorYear) + offset)}-${groups["m"] ?? ""}-${groups["d"] ?? ""}`)
39
+ .find((candidate) => {
40
+ const time = timeOf(candidate);
41
+ return time !== undefined && Math.abs(time - anchorTime) <= YEAR_REACH_DAYS * DAY_MS;
42
+ });
43
+ };
44
+
45
+ /**
46
+ * 年の無い日付の年を決める手がかり。stamp は文書の日付(「更新日:」の段落など)、sectionStarts は見出しで分けた節の始まり、
47
+ * yearHeadings はそのうち見出しが年を名指す節(「## 2024年度」)の始まり。anchorable は、年を決めるのに使ってよい日付か
48
+ * (引用符で日付だけを挙げた例は使わない)。
49
+ */
50
+ export type YearContext = {
51
+ readonly stamp?: string | undefined;
52
+ readonly sectionStarts: readonly number[];
53
+ readonly yearHeadings?: readonly number[];
54
+ readonly anchorable?: (node: StructureNode) => boolean;
55
+ };
56
+
57
+ const sectionOf = (starts: readonly number[], offset: number): number => starts.filter((start) => start <= offset).length;
58
+
59
+ /** 文書の書き出し: 最初の見出しより前と、最初の見出しの節。メールや議事録の頭の日付(日付:、日時:)が書かれる所。 */
60
+ const OPENING_SECTIONS = 1;
61
+
62
+ /**
63
+ * 年の無い日付の年を決める、年を書いた日付。同じ節のすぐ前のもの(前に無ければ後ろの最初のもの)。節に無ければ文書の日付、
64
+ * それも無ければ書き出しの最初の日付。年度ごとの見出しの下の「1月24日(火)」はその節の年で読み、別の節の日付の年では読まない。
65
+ */
66
+ const anchorOf = (dated: readonly StructureNode[], node: StructureNode, context: YearContext): string | undefined => {
67
+ const section = sectionOf(context.sectionStarts, node.span.start);
68
+ const inSection = dated.filter((other) => sectionOf(context.sectionStarts, other.span.start) === section);
69
+ const nearest = inSection.findLast((other) => other.span.start < node.span.start) ?? inSection[0];
70
+ // 見出しが年を名指す節(「## 2024年度」)の日付は、その年の中にある。節の外の日付からは決めない。
71
+ const sectionStart = context.sectionStarts[section - 1];
72
+ if (nearest === undefined && sectionStart !== undefined && (context.yearHeadings ?? []).includes(sectionStart)) return undefined;
73
+ const opening = dated.find((other) => sectionOf(context.sectionStarts, other.span.start) <= OPENING_SECTIONS);
74
+ const stamp = context.stamp !== undefined && FULL_DATE.test(context.stamp) ? context.stamp : undefined;
75
+ const fromTree = (found: StructureNode | undefined): string | undefined => (found === undefined ? undefined : String(found.attrs["value"]));
76
+ return fromTree(nearest) ?? stamp ?? fromTree(opening);
77
+ };
78
+
79
+ /** 日付の年月日。年の無い日付は、近くの年を書いた日付から年を決めて付ける。決まらなければ undefined。 */
80
+ const fullValueOf = (dated: readonly StructureNode[], node: StructureNode, context: YearContext): string | undefined => {
81
+ const value = String(node.attrs["value"]);
82
+ if (!MONTH_DAY.test(value)) return value;
83
+ const anchor = anchorOf(dated, node, context);
84
+ return anchor === undefined ? undefined : withNearestYear(value, anchor);
85
+ };
86
+
87
+ /**
88
+ * 書いた曜日が暦と違う日付。年の無い日付は、年を書いた日付(anchorOf)から 5 か月以内に収まる年として比べる。
89
+ * 年を書いた日付が文書に無いか、どの年も収まらなければ、曜日を決められないので見ない。
90
+ */
91
+ export const weekdayMismatches = (tree: StructureNode, context: YearContext = { sectionStarts: [] }): StructureIssue[] => {
92
+ const dates = inDocumentOrder(tree).filter((node) => node.kind === "date");
93
+ const anchorable = context.anchorable ?? (() => true);
94
+ const dated = dates.filter((node) => timeOf(String(node.attrs["value"])) !== undefined && anchorable(node));
95
+ return dates.flatMap((node) => {
21
96
  const written = node.attrs["weekday"];
22
- if (node.kind !== "date" || typeof written !== "number") return [];
23
- const actual = weekdayOf(String(node.attrs["value"]));
24
- return actual === undefined || actual === written ? [] : [{ offset: node.span.start, values: { date: String(node.attrs["value"]), written, actual } }];
97
+ if (typeof written !== "number") return [];
98
+ const value = fullValueOf(dated, node, context);
99
+ const actual = value === undefined ? undefined : weekdayOf(value);
100
+ return value === undefined || actual === undefined || actual === written ? [] : [{ offset: node.span.start, values: { date: value, written, actual } }];
25
101
  });
102
+ };
@@ -1,14 +1,13 @@
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";
3
+ import { PACKAGE_DIR, readText } from "../package-files.ts";
5
4
  import { parseStructureBaseline, type StructureBaseline } from "./baseline.ts";
6
5
 
7
- const BASELINE_FILE = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "structure-baseline.yaml");
6
+ const BASELINE_FILE = join(PACKAGE_DIR, "structure-baseline.yaml");
8
7
 
9
8
  const readBaseline = (): StructureBaseline => {
10
9
  try {
11
- return parseStructureBaseline(parse(readFileSync(BASELINE_FILE, "utf8")));
10
+ return parseStructureBaseline(parse(readText(BASELINE_FILE)));
12
11
  } catch (error) {
13
12
  throw new Error(`cannot read the structure baseline ${BASELINE_FILE}`, { cause: error });
14
13
  }
package/src/style-load.ts CHANGED
@@ -1,16 +1,15 @@
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 { parseStyle, type StyleDefinition } from "./style-parse.ts";
6
5
 
7
- const STYLES_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "styles");
6
+ const STYLES_DIR = join(PACKAGE_DIR, "styles");
8
7
 
9
8
  const loaded: { value: readonly StyleDefinition[] | undefined } = { value: undefined };
10
9
 
11
10
  const readStyle = (dir: string, file: string): StyleDefinition => {
12
11
  try {
13
- return parseStyle(parse(readFileSync(join(dir, file), "utf8")), file);
12
+ return parseStyle(parse(readText(join(dir, file))), file);
14
13
  } catch (error) {
15
14
  throw new Error(`cannot read ${join(dir, file)}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
16
15
  }
@@ -19,7 +18,7 @@ const readStyle = (dir: string, file: string): StyleDefinition => {
19
18
  /** The bundled house styles (styles/*.yaml), by file name. Read once. */
20
19
  export const loadStyles = (dir: string = STYLES_DIR): readonly StyleDefinition[] => {
21
20
  if (dir === STYLES_DIR && loaded.value !== undefined) return loaded.value;
22
- const styles = readdirSync(dir)
21
+ const styles = readDir(dir)
23
22
  .filter((file) => file.endsWith(".yaml"))
24
23
  .toSorted((left, right) => left.localeCompare(right, "en"))
25
24
  .map((file) => readStyle(dir, file));