chaffjs 0.15.0 → 0.17.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 (528) hide show
  1. package/README.md +27 -2
  2. package/dist/address-chain.js +1 -1
  3. package/dist/address-chain.js.map +1 -1
  4. package/dist/backends/anthropic.d.ts +2 -1
  5. package/dist/backends/anthropic.d.ts.map +1 -1
  6. package/dist/backends/anthropic.js +4 -1
  7. package/dist/backends/anthropic.js.map +1 -1
  8. package/dist/backends/openai.d.ts +2 -1
  9. package/dist/backends/openai.d.ts.map +1 -1
  10. package/dist/backends/openai.js +1 -1
  11. package/dist/backends/openai.js.map +1 -1
  12. package/dist/backends/types.d.ts +2 -1
  13. package/dist/backends/types.d.ts.map +1 -1
  14. package/dist/backends/types.js +1 -1
  15. package/dist/backends/types.js.map +1 -1
  16. package/dist/baseline.js +1 -1
  17. package/dist/baseline.js.map +1 -1
  18. package/dist/calendar-number.d.ts +31 -0
  19. package/dist/calendar-number.d.ts.map +1 -0
  20. package/dist/calendar-number.js +58 -0
  21. package/dist/calendar-number.js.map +1 -0
  22. package/dist/cli-args.d.ts +0 -5
  23. package/dist/cli-args.d.ts.map +1 -1
  24. package/dist/cli-args.js +2 -2
  25. package/dist/cli-args.js.map +1 -1
  26. package/dist/cli-text.d.ts +6 -2
  27. package/dist/cli-text.d.ts.map +1 -1
  28. package/dist/cli-text.js +19 -12
  29. package/dist/cli-text.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +43 -37
  32. package/dist/cli.js.map +1 -1
  33. package/dist/closing-numbers.d.ts +20 -0
  34. package/dist/closing-numbers.d.ts.map +1 -0
  35. package/dist/closing-numbers.js +56 -0
  36. package/dist/closing-numbers.js.map +1 -0
  37. package/dist/commands/cite.d.ts.map +1 -1
  38. package/dist/commands/cite.js +4 -4
  39. package/dist/commands/cite.js.map +1 -1
  40. package/dist/commands/eval.js +1 -1
  41. package/dist/commands/eval.js.map +1 -1
  42. package/dist/commands/init-ask.d.ts +19 -0
  43. package/dist/commands/init-ask.d.ts.map +1 -0
  44. package/dist/commands/init-ask.js +43 -0
  45. package/dist/commands/init-ask.js.map +1 -0
  46. package/dist/commands/test.d.ts +8 -0
  47. package/dist/commands/test.d.ts.map +1 -1
  48. package/dist/commands/test.js +78 -44
  49. package/dist/commands/test.js.map +1 -1
  50. package/dist/commands/tree.d.ts +1 -8
  51. package/dist/commands/tree.d.ts.map +1 -1
  52. package/dist/commands/tree.js +7 -6
  53. package/dist/commands/tree.js.map +1 -1
  54. package/dist/config/by-path.d.ts.map +1 -1
  55. package/dist/config/by-path.js +10 -9
  56. package/dist/config/by-path.js.map +1 -1
  57. package/dist/config/load.d.ts +4 -0
  58. package/dist/config/load.d.ts.map +1 -1
  59. package/dist/config/load.js +19 -0
  60. package/dist/config/load.js.map +1 -1
  61. package/dist/config/name-problems.d.ts +5 -0
  62. package/dist/config/name-problems.d.ts.map +1 -0
  63. package/dist/config/name-problems.js +11 -0
  64. package/dist/config/name-problems.js.map +1 -0
  65. package/dist/date-stamp.js +1 -1
  66. package/dist/date-stamp.js.map +1 -1
  67. package/dist/detectors/acronym-context.d.ts +2 -0
  68. package/dist/detectors/acronym-context.d.ts.map +1 -1
  69. package/dist/detectors/acronym-context.js +13 -0
  70. package/dist/detectors/acronym-context.js.map +1 -1
  71. package/dist/detectors/acronym-expansion.d.ts +2 -0
  72. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  73. package/dist/detectors/acronym-expansion.js +104 -1
  74. package/dist/detectors/acronym-expansion.js.map +1 -1
  75. package/dist/detectors/agreement-slip.d.ts +32 -0
  76. package/dist/detectors/agreement-slip.d.ts.map +1 -0
  77. package/dist/detectors/agreement-slip.js +137 -0
  78. package/dist/detectors/agreement-slip.js.map +1 -0
  79. package/dist/detectors/char-shape.d.ts.map +1 -1
  80. package/dist/detectors/char-shape.js +16 -6
  81. package/dist/detectors/char-shape.js.map +1 -1
  82. package/dist/detectors/content-morphemes.d.ts +4 -0
  83. package/dist/detectors/content-morphemes.d.ts.map +1 -0
  84. package/dist/detectors/content-morphemes.js +12 -0
  85. package/dist/detectors/content-morphemes.js.map +1 -0
  86. package/dist/detectors/en-shape.d.ts +0 -1
  87. package/dist/detectors/en-shape.d.ts.map +1 -1
  88. package/dist/detectors/en-shape.js +2 -143
  89. package/dist/detectors/en-shape.js.map +1 -1
  90. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  91. package/dist/detectors/enumerated-runs.js +4 -2
  92. package/dist/detectors/enumerated-runs.js.map +1 -1
  93. package/dist/detectors/gram-place.js +1 -1
  94. package/dist/detectors/gram-place.js.map +1 -1
  95. package/dist/detectors/heading-case.d.ts.map +1 -1
  96. package/dist/detectors/heading-case.js +3 -1
  97. package/dist/detectors/heading-case.js.map +1 -1
  98. package/dist/detectors/heading-echo.d.ts.map +1 -1
  99. package/dist/detectors/heading-echo.js +12 -18
  100. package/dist/detectors/heading-echo.js.map +1 -1
  101. package/dist/detectors/heading-overlap.d.ts +13 -0
  102. package/dist/detectors/heading-overlap.d.ts.map +1 -0
  103. package/dist/detectors/heading-overlap.js +52 -0
  104. package/dist/detectors/heading-overlap.js.map +1 -0
  105. package/dist/detectors/index.d.ts.map +1 -1
  106. package/dist/detectors/index.js +10 -4
  107. package/dist/detectors/index.js.map +1 -1
  108. package/dist/detectors/letter-joined-name.d.ts +13 -0
  109. package/dist/detectors/letter-joined-name.d.ts.map +1 -0
  110. package/dist/detectors/letter-joined-name.js +11 -0
  111. package/dist/detectors/letter-joined-name.js.map +1 -0
  112. package/dist/detectors/lettered-index.d.ts +9 -0
  113. package/dist/detectors/lettered-index.d.ts.map +1 -0
  114. package/dist/detectors/lettered-index.js +54 -0
  115. package/dist/detectors/lettered-index.js.map +1 -0
  116. package/dist/detectors/lexicon.d.ts +5 -1
  117. package/dist/detectors/lexicon.d.ts.map +1 -1
  118. package/dist/detectors/lexicon.js +51 -5
  119. package/dist/detectors/lexicon.js.map +1 -1
  120. package/dist/detectors/orthography.d.ts.map +1 -1
  121. package/dist/detectors/orthography.js +24 -6
  122. package/dist/detectors/orthography.js.map +1 -1
  123. package/dist/detectors/oxford-comma.d.ts +3 -0
  124. package/dist/detectors/oxford-comma.d.ts.map +1 -0
  125. package/dist/detectors/oxford-comma.js +340 -0
  126. package/dist/detectors/oxford-comma.js.map +1 -0
  127. package/dist/detectors/phrase-match.d.ts +3 -1
  128. package/dist/detectors/phrase-match.d.ts.map +1 -1
  129. package/dist/detectors/phrase-match.js +20 -15
  130. package/dist/detectors/phrase-match.js.map +1 -1
  131. package/dist/detectors/repeated-head.d.ts.map +1 -1
  132. package/dist/detectors/repeated-head.js +5 -4
  133. package/dist/detectors/repeated-head.js.map +1 -1
  134. package/dist/detectors/signals.d.ts.map +1 -1
  135. package/dist/detectors/signals.js +45 -16
  136. package/dist/detectors/signals.js.map +1 -1
  137. package/dist/detectors/stacked-hedge.d.ts +16 -0
  138. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  139. package/dist/detectors/stacked-hedge.js +67 -0
  140. package/dist/detectors/stacked-hedge.js.map +1 -0
  141. package/dist/detectors/stray-space.d.ts +7 -0
  142. package/dist/detectors/stray-space.d.ts.map +1 -0
  143. package/dist/detectors/stray-space.js +34 -0
  144. package/dist/detectors/stray-space.js.map +1 -0
  145. package/dist/detectors/structure.d.ts +3 -1
  146. package/dist/detectors/structure.d.ts.map +1 -1
  147. package/dist/detectors/structure.js +4 -2
  148. package/dist/detectors/structure.js.map +1 -1
  149. package/dist/detectors/superlative-amount.d.ts +9 -0
  150. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  151. package/dist/detectors/superlative-amount.js +20 -0
  152. package/dist/detectors/superlative-amount.js.map +1 -0
  153. package/dist/detectors/superlative-clause.d.ts +18 -0
  154. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  155. package/dist/detectors/superlative-clause.js +61 -0
  156. package/dist/detectors/superlative-clause.js.map +1 -0
  157. package/dist/detectors/superlative-scope.d.ts +6 -0
  158. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  159. package/dist/detectors/superlative-scope.js +3 -3
  160. package/dist/detectors/superlative-scope.js.map +1 -1
  161. package/dist/detectors/team.d.ts +0 -6
  162. package/dist/detectors/team.d.ts.map +1 -1
  163. package/dist/detectors/team.js +17 -1
  164. package/dist/detectors/team.js.map +1 -1
  165. package/dist/detectors/token-shape.d.ts.map +1 -1
  166. package/dist/detectors/token-shape.js +8 -6
  167. package/dist/detectors/token-shape.js.map +1 -1
  168. package/dist/document.d.ts +3 -0
  169. package/dist/document.d.ts.map +1 -1
  170. package/dist/document.js +33 -6
  171. package/dist/document.js.map +1 -1
  172. package/dist/eval.js +1 -1
  173. package/dist/eval.js.map +1 -1
  174. package/dist/feedback/draft.js +1 -1
  175. package/dist/feedback/draft.js.map +1 -1
  176. package/dist/feedback/excerpt.d.ts +0 -1
  177. package/dist/feedback/excerpt.d.ts.map +1 -1
  178. package/dist/feedback/excerpt.js +2 -2
  179. package/dist/feedback/excerpt.js.map +1 -1
  180. package/dist/file-header.d.ts +20 -0
  181. package/dist/file-header.d.ts.map +1 -0
  182. package/dist/file-header.js +21 -0
  183. package/dist/file-header.js.map +1 -0
  184. package/dist/files.js +3 -3
  185. package/dist/files.js.map +1 -1
  186. package/dist/genre-check.js +1 -1
  187. package/dist/genre-check.js.map +1 -1
  188. package/dist/genre-load.d.ts +6 -0
  189. package/dist/genre-load.d.ts.map +1 -0
  190. package/dist/genre-load.js +22 -0
  191. package/dist/genre-load.js.map +1 -0
  192. package/dist/genre-parse.d.ts +44 -0
  193. package/dist/genre-parse.d.ts.map +1 -0
  194. package/dist/genre-parse.js +117 -0
  195. package/dist/genre-parse.js.map +1 -0
  196. package/dist/genre-suggest.d.ts +15 -0
  197. package/dist/genre-suggest.d.ts.map +1 -0
  198. package/dist/genre-suggest.js +40 -0
  199. package/dist/genre-suggest.js.map +1 -0
  200. package/dist/genre.d.ts +1 -1
  201. package/dist/genre.d.ts.map +1 -1
  202. package/dist/genre.js +3 -13
  203. package/dist/genre.js.map +1 -1
  204. package/dist/init-choice.d.ts +12 -0
  205. package/dist/init-choice.d.ts.map +1 -0
  206. package/dist/init-choice.js +41 -0
  207. package/dist/init-choice.js.map +1 -0
  208. package/dist/init.d.ts.map +1 -1
  209. package/dist/init.js +16 -5
  210. package/dist/init.js.map +1 -1
  211. package/dist/judge.d.ts +2 -1
  212. package/dist/judge.d.ts.map +1 -1
  213. package/dist/judge.js +1 -1
  214. package/dist/judge.js.map +1 -1
  215. package/dist/line-paragraphs.d.ts +4 -0
  216. package/dist/line-paragraphs.d.ts.map +1 -0
  217. package/dist/line-paragraphs.js +71 -0
  218. package/dist/line-paragraphs.js.map +1 -0
  219. package/dist/mask.d.ts.map +1 -1
  220. package/dist/mask.js +2 -10
  221. package/dist/mask.js.map +1 -1
  222. package/dist/number-name.js +1 -1
  223. package/dist/number-name.js.map +1 -1
  224. package/dist/opening-line.d.ts +3 -0
  225. package/dist/opening-line.d.ts.map +1 -0
  226. package/dist/opening-line.js +14 -0
  227. package/dist/opening-line.js.map +1 -0
  228. package/dist/orthography.d.ts +5 -1
  229. package/dist/orthography.d.ts.map +1 -1
  230. package/dist/orthography.js +7 -1
  231. package/dist/orthography.js.map +1 -1
  232. package/dist/paragraph-length.d.ts +7 -0
  233. package/dist/paragraph-length.d.ts.map +1 -0
  234. package/dist/paragraph-length.js +10 -0
  235. package/dist/paragraph-length.js.map +1 -0
  236. package/dist/plugin.d.ts +12 -0
  237. package/dist/plugin.d.ts.map +1 -1
  238. package/dist/profile/check.d.ts +28 -0
  239. package/dist/profile/check.d.ts.map +1 -0
  240. package/dist/profile/check.js +12 -0
  241. package/dist/profile/check.js.map +1 -0
  242. package/dist/profile/for-file.d.ts +2 -2
  243. package/dist/profile/for-file.d.ts.map +1 -1
  244. package/dist/profile/for-file.js +9 -2
  245. package/dist/profile/for-file.js.map +1 -1
  246. package/dist/profile/load.js +1 -1
  247. package/dist/profile/load.js.map +1 -1
  248. package/dist/profile/select.d.ts +4 -2
  249. package/dist/profile/select.d.ts.map +1 -1
  250. package/dist/profile/select.js +8 -3
  251. package/dist/profile/select.js.map +1 -1
  252. package/dist/quoted-span.d.ts +13 -3
  253. package/dist/quoted-span.d.ts.map +1 -1
  254. package/dist/quoted-span.js +16 -11
  255. package/dist/quoted-span.js.map +1 -1
  256. package/dist/reasons.d.ts +1 -0
  257. package/dist/reasons.d.ts.map +1 -1
  258. package/dist/reasons.js +4 -1
  259. package/dist/reasons.js.map +1 -1
  260. package/dist/render/compact.d.ts.map +1 -1
  261. package/dist/render/compact.js +1 -2
  262. package/dist/render/compact.js.map +1 -1
  263. package/dist/render/eval.d.ts.map +1 -1
  264. package/dist/render/eval.js +4 -3
  265. package/dist/render/eval.js.map +1 -1
  266. package/dist/render/friendly.d.ts +2 -1
  267. package/dist/render/friendly.d.ts.map +1 -1
  268. package/dist/render/friendly.js +14 -6
  269. package/dist/render/friendly.js.map +1 -1
  270. package/dist/render/genres.d.ts +5 -0
  271. package/dist/render/genres.d.ts.map +1 -0
  272. package/dist/render/genres.js +28 -0
  273. package/dist/render/genres.js.map +1 -0
  274. package/dist/render/plural.d.ts +3 -0
  275. package/dist/render/plural.d.ts.map +1 -0
  276. package/dist/render/plural.js +5 -0
  277. package/dist/render/plural.js.map +1 -0
  278. package/dist/render/rules-json.d.ts.map +1 -1
  279. package/dist/render/rules-json.js +9 -3
  280. package/dist/render/rules-json.js.map +1 -1
  281. package/dist/render/semantic.d.ts +3 -2
  282. package/dist/render/semantic.d.ts.map +1 -1
  283. package/dist/render/semantic.js +94 -34
  284. package/dist/render/semantic.js.map +1 -1
  285. package/dist/render/summary.d.ts.map +1 -1
  286. package/dist/render/summary.js +4 -3
  287. package/dist/render/summary.js.map +1 -1
  288. package/dist/render/suppressions.d.ts.map +1 -1
  289. package/dist/render/suppressions.js +3 -2
  290. package/dist/render/suppressions.js.map +1 -1
  291. package/dist/render/text.d.ts +2 -0
  292. package/dist/render/text.d.ts.map +1 -1
  293. package/dist/render/text.js +11 -3
  294. package/dist/render/text.js.map +1 -1
  295. package/dist/rule-load.d.ts.map +1 -1
  296. package/dist/rule-load.js +12 -1
  297. package/dist/rule-load.js.map +1 -1
  298. package/dist/run-semantic.d.ts +3 -1
  299. package/dist/run-semantic.d.ts.map +1 -1
  300. package/dist/run-semantic.js +9 -4
  301. package/dist/run-semantic.js.map +1 -1
  302. package/dist/run.d.ts +4 -0
  303. package/dist/run.d.ts.map +1 -1
  304. package/dist/run.js +29 -16
  305. package/dist/run.js.map +1 -1
  306. package/dist/semantic.d.ts +1 -1
  307. package/dist/semantic.d.ts.map +1 -1
  308. package/dist/semantic.js +10 -3
  309. package/dist/semantic.js.map +1 -1
  310. package/dist/sentence-shape.d.ts +0 -1
  311. package/dist/sentence-shape.d.ts.map +1 -1
  312. package/dist/sentence-shape.js +3 -5
  313. package/dist/sentence-shape.js.map +1 -1
  314. package/dist/setting-problems.d.ts +5 -0
  315. package/dist/setting-problems.d.ts.map +1 -0
  316. package/dist/setting-problems.js +15 -0
  317. package/dist/setting-problems.js.map +1 -0
  318. package/dist/span-merge.d.ts +4 -0
  319. package/dist/span-merge.d.ts.map +1 -0
  320. package/dist/span-merge.js +12 -0
  321. package/dist/span-merge.js.map +1 -0
  322. package/dist/speaker-labels.d.ts +4 -0
  323. package/dist/speaker-labels.d.ts.map +1 -0
  324. package/dist/speaker-labels.js +106 -0
  325. package/dist/speaker-labels.js.map +1 -0
  326. package/dist/stray-space.d.ts +17 -0
  327. package/dist/stray-space.d.ts.map +1 -0
  328. package/dist/stray-space.js +66 -0
  329. package/dist/stray-space.js.map +1 -0
  330. package/dist/structure/build.js +5 -5
  331. package/dist/structure/build.js.map +1 -1
  332. package/dist/structure/cite.js +1 -1
  333. package/dist/structure/cite.js.map +1 -1
  334. package/dist/structure/issues.d.ts.map +1 -1
  335. package/dist/structure/issues.js +5 -5
  336. package/dist/structure/issues.js.map +1 -1
  337. package/dist/structure/markdown-path.d.ts.map +1 -1
  338. package/dist/structure/markdown-path.js +2 -2
  339. package/dist/structure/markdown-path.js.map +1 -1
  340. package/dist/structure/numbering.d.ts.map +1 -1
  341. package/dist/structure/numbering.js +1 -1
  342. package/dist/structure/numbering.js.map +1 -1
  343. package/dist/structure/relative-find.js +1 -1
  344. package/dist/structure/relative-find.js.map +1 -1
  345. package/dist/structure/relative-resolve.d.ts.map +1 -1
  346. package/dist/structure/relative-resolve.js +26 -20
  347. package/dist/structure/relative-resolve.js.map +1 -1
  348. package/dist/structure/sexp.js +1 -1
  349. package/dist/structure/sexp.js.map +1 -1
  350. package/dist/structure/unnumbered.js +2 -2
  351. package/dist/structure/unnumbered.js.map +1 -1
  352. package/dist/team-names.d.ts +9 -0
  353. package/dist/team-names.d.ts.map +1 -0
  354. package/dist/team-names.js +31 -0
  355. package/dist/team-names.js.map +1 -0
  356. package/dist/ui.d.ts +2 -0
  357. package/dist/ui.d.ts.map +1 -1
  358. package/dist/ui.js +6 -0
  359. package/dist/ui.js.map +1 -1
  360. package/dist/version.js +1 -1
  361. package/dist/version.js.map +1 -1
  362. package/dist/watch.d.ts +2 -2
  363. package/dist/watch.d.ts.map +1 -1
  364. package/dist/watch.js +5 -5
  365. package/dist/watch.js.map +1 -1
  366. package/genres.yaml +395 -0
  367. package/package.json +4 -3
  368. package/rules/adverb-overuse.yaml +2 -2
  369. package/rules/agentless-passive.yaml +1 -1
  370. package/rules/agreement-slip.yaml +36 -0
  371. package/rules/ai-generated-composite.yaml +2 -2
  372. package/rules/ai-tell.yaml +2 -2
  373. package/rules/bold-density.yaml +2 -2
  374. package/rules/closing-cliche.yaml +1 -1
  375. package/rules/concrete-evidence-density.yaml +2 -2
  376. package/rules/contraction-consistency.yaml +1 -1
  377. package/rules/cushion-phrase-density.yaml +1 -1
  378. package/rules/dangling-reference.yaml +1 -1
  379. package/rules/date-order.yaml +1 -1
  380. package/rules/date-weekday-mismatch.yaml +1 -1
  381. package/rules/double-keigo.yaml +1 -1
  382. package/rules/doubled-word.yaml +1 -1
  383. package/rules/duplicate-definition.yaml +1 -1
  384. package/rules/emoji-density.yaml +1 -1
  385. package/rules/empty-conclusion.yaml +3 -1
  386. package/rules/empty-intensifier.yaml +1 -1
  387. package/rules/excessive-hedging.yaml +16 -6
  388. package/rules/expletive-construction.yaml +2 -2
  389. package/rules/heading-echo.yaml +2 -1
  390. package/rules/hiragana-fukushi.yaml +1 -1
  391. package/rules/internal-jargon.yaml +2 -2
  392. package/rules/latin-spacing.yaml +5 -4
  393. package/rules/max-kanji-continuous.yaml +4 -3
  394. package/rules/max-paragraph-length.yaml +18 -4
  395. package/rules/max-sentence-length.yaml +18 -2
  396. package/rules/ngram-repetition.yaml +4 -4
  397. package/rules/no-doubled-joshi.yaml +2 -2
  398. package/rules/no-em-dash.yaml +2 -2
  399. package/rules/no-mixed-desumasu.yaml +1 -1
  400. package/rules/no-nakaguro-parallel.yaml +2 -2
  401. package/rules/numbering-gap.yaml +1 -1
  402. package/rules/oxford-comma-consistency.yaml +2 -1
  403. package/rules/padded-intro.yaml +2 -1
  404. package/rules/paragraph-length-variance.yaml +1 -1
  405. package/rules/preamble-length.yaml +2 -2
  406. package/rules/preferred-term.yaml +1 -1
  407. package/rules/proper-noun-density.yaml +3 -2
  408. package/rules/repeated-conjunction.yaml +2 -2
  409. package/rules/repeated-sentence-head.yaml +2 -2
  410. package/rules/required-sections.yaml +1 -1
  411. package/rules/rule-of-three.yaml +2 -2
  412. package/rules/sasete-itadaku.yaml +3 -3
  413. package/rules/section-length-uniformity.yaml +1 -1
  414. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  415. package/rules/sentence-rhythm.yaml +1 -1
  416. package/rules/stray-space.yaml +38 -0
  417. package/rules/taigen-dome-in-prose.yaml +2 -2
  418. package/rules/title-case-consistency.yaml +1 -1
  419. package/rules/total-mismatch.yaml +1 -1
  420. package/rules/undefined-acronym.yaml +5 -4
  421. package/rules/unqualified-superlative.yaml +2 -2
  422. package/skills/chaff/SKILL.md +28 -9
  423. package/src/address-chain.ts +1 -1
  424. package/src/backends/anthropic.ts +5 -1
  425. package/src/backends/openai.ts +2 -1
  426. package/src/backends/types.ts +3 -2
  427. package/src/baseline.ts +1 -1
  428. package/src/calendar-number.ts +84 -0
  429. package/src/cli-args.ts +2 -2
  430. package/src/cli-text.ts +29 -15
  431. package/src/cli.ts +42 -41
  432. package/src/closing-numbers.ts +72 -0
  433. package/src/commands/cite.ts +4 -4
  434. package/src/commands/eval.ts +1 -1
  435. package/src/commands/init-ask.ts +53 -0
  436. package/src/commands/test.ts +118 -56
  437. package/src/commands/tree.ts +7 -6
  438. package/src/config/by-path.ts +8 -9
  439. package/src/config/load.ts +24 -0
  440. package/src/config/name-problems.ts +15 -0
  441. package/src/date-stamp.ts +1 -1
  442. package/src/detectors/acronym-context.ts +16 -0
  443. package/src/detectors/acronym-expansion.ts +132 -1
  444. package/src/detectors/agreement-slip.ts +191 -0
  445. package/src/detectors/char-shape.ts +21 -9
  446. package/src/detectors/content-morphemes.ts +19 -0
  447. package/src/detectors/en-shape.ts +2 -173
  448. package/src/detectors/enumerated-runs.ts +3 -2
  449. package/src/detectors/gram-place.ts +1 -1
  450. package/src/detectors/heading-case.ts +4 -1
  451. package/src/detectors/heading-echo.ts +12 -20
  452. package/src/detectors/heading-overlap.ts +65 -0
  453. package/src/detectors/index.ts +10 -4
  454. package/src/detectors/letter-joined-name.ts +15 -0
  455. package/src/detectors/lettered-index.ts +78 -0
  456. package/src/detectors/lexicon.ts +69 -8
  457. package/src/detectors/orthography.ts +31 -7
  458. package/src/detectors/oxford-comma.ts +402 -0
  459. package/src/detectors/phrase-match.ts +23 -12
  460. package/src/detectors/repeated-head.ts +3 -4
  461. package/src/detectors/signals.ts +47 -15
  462. package/src/detectors/stacked-hedge.ts +96 -0
  463. package/src/detectors/stray-space.ts +40 -0
  464. package/src/detectors/structure.ts +10 -2
  465. package/src/detectors/superlative-amount.ts +24 -0
  466. package/src/detectors/superlative-clause.ts +82 -0
  467. package/src/detectors/superlative-scope.ts +3 -3
  468. package/src/detectors/team.ts +18 -2
  469. package/src/detectors/token-shape.ts +8 -10
  470. package/src/document.ts +54 -7
  471. package/src/eval.ts +1 -1
  472. package/src/feedback/draft.ts +1 -1
  473. package/src/feedback/excerpt.ts +2 -2
  474. package/src/file-header.ts +31 -0
  475. package/src/files.ts +3 -3
  476. package/src/genre-check.ts +1 -1
  477. package/src/genre-load.ts +24 -0
  478. package/src/genre-parse.ts +151 -0
  479. package/src/genre-suggest.ts +50 -0
  480. package/src/genre.ts +4 -13
  481. package/src/init-choice.ts +47 -0
  482. package/src/init.ts +20 -6
  483. package/src/judge.ts +3 -1
  484. package/src/line-paragraphs.ts +86 -0
  485. package/src/mask.ts +2 -12
  486. package/src/number-name.ts +1 -1
  487. package/src/opening-line.ts +17 -0
  488. package/src/orthography.ts +10 -2
  489. package/src/paragraph-length.ts +9 -0
  490. package/src/plugin.ts +12 -0
  491. package/src/profile/check.ts +34 -0
  492. package/src/profile/for-file.ts +10 -3
  493. package/src/profile/load.ts +1 -1
  494. package/src/profile/select.ts +11 -4
  495. package/src/quoted-span.ts +26 -12
  496. package/src/reasons.ts +5 -1
  497. package/src/render/compact.ts +1 -3
  498. package/src/render/eval.ts +4 -3
  499. package/src/render/friendly.ts +21 -6
  500. package/src/render/genres.ts +31 -0
  501. package/src/render/plural.ts +6 -0
  502. package/src/render/rules-json.ts +10 -3
  503. package/src/render/semantic.ts +117 -34
  504. package/src/render/summary.ts +4 -3
  505. package/src/render/suppressions.ts +3 -2
  506. package/src/render/text.ts +18 -3
  507. package/src/rule-load.ts +14 -1
  508. package/src/run-semantic.ts +29 -21
  509. package/src/run.ts +33 -16
  510. package/src/semantic.ts +11 -3
  511. package/src/sentence-shape.ts +3 -7
  512. package/src/setting-problems.ts +24 -0
  513. package/src/span-merge.ts +13 -0
  514. package/src/speaker-labels.ts +129 -0
  515. package/src/stray-space.ts +81 -0
  516. package/src/structure/build.ts +5 -5
  517. package/src/structure/cite.ts +1 -1
  518. package/src/structure/issues.ts +6 -5
  519. package/src/structure/markdown-path.ts +2 -2
  520. package/src/structure/numbering.ts +1 -1
  521. package/src/structure/relative-find.ts +1 -1
  522. package/src/structure/relative-resolve.ts +34 -19
  523. package/src/structure/sexp.ts +1 -1
  524. package/src/structure/unnumbered.ts +2 -2
  525. package/src/team-names.ts +39 -0
  526. package/src/ui.ts +7 -0
  527. package/src/version.ts +1 -1
  528. package/src/watch.ts +5 -5
@@ -11,10 +11,6 @@ const ENDS = /[。..!?!?]["))」』\s]*$/u;
11
11
 
12
12
  export const isClosed = (sentence: Sentence): boolean => ENDS.test(sentence.text);
13
13
 
14
- const PREDICATE = new Set(["AUX", "VERB", "ADJ"]);
15
-
16
- export const hasPredicate = (sentence: Sentence): boolean => (sentence.tokens ?? []).some((token) => PREDICATE.has(token.pos));
17
-
18
14
  export const hasParticle = (sentence: Sentence): boolean => (sentence.tokens ?? []).some((token) => token.pos === "ADP" || token.pos === "SCONJ");
19
15
 
20
16
  const SKIP = new Set(["PUNCT", "PART", "SYM"]);
@@ -46,11 +42,11 @@ const CLOSING_QUOTES = ["」", "』", "\u201d"];
46
42
  * 引用で終わる文(画面には「確認します。」。)は、書き手の文末が無いので空。
47
43
  */
48
44
  export const endingTokens = (sentence: Sentence): Token[] => {
49
- const reversed = [...beforeTrailing(sentence)].reverse();
45
+ const reversed = beforeTrailing(sentence).toReversed();
50
46
  const last = reversed.findIndex((token) => !SKIP.has(token.pos));
51
47
  if (last === -1 || reversed.slice(0, last).some((token) => CLOSING_QUOTES.some((quote) => token.surface.includes(quote)))) return [];
52
48
  const head = reversed.findIndex((token, index) => index >= last && token.pos !== "AUX");
53
- return reversed.slice(last, head === -1 ? reversed.length : head + 1).reverse();
49
+ return reversed.slice(last, head === -1 ? reversed.length : head + 1).toReversed();
54
50
  };
55
51
 
56
- export const lastContent = (sentence: Sentence): Token | undefined => [...beforeTrailing(sentence)].reverse().find((token) => !SKIP.has(token.pos));
52
+ export const lastContent = (sentence: Sentence): Token | undefined => beforeTrailing(sentence).findLast((token) => !SKIP.has(token.pos));
@@ -0,0 +1,24 @@
1
+ import type { Config } from "./config/load.ts";
2
+ import type { CliText } from "./cli-text.ts";
3
+ import { GENRES } from "./genre.ts";
4
+ import { unknownGenres, writtenGenres } from "./genre-check.ts";
5
+ import { loadProfiles } from "./profile/load.ts";
6
+ import { unknownProfiles, writtenProfiles } from "./profile/check.ts";
7
+
8
+ const genreProblems = (command: string, genreFlag: string | undefined, config: Config, text: CliText): string[] =>
9
+ unknownGenres(writtenGenres(command, genreFlag, config), GENRES).map((entry) =>
10
+ text.unknownGenre(entry.genre, text.genreWhere(entry.where, entry.files), GENRES),
11
+ );
12
+
13
+ const profileProblems = (command: string, config: Config, text: CliText): string[] => {
14
+ const known = loadProfiles().map((definition) => definition.id);
15
+ return unknownProfiles(writtenProfiles(command, config), known).map((entry) =>
16
+ text.unknownProfile(entry.profile, text.profileWhere(entry.where, entry.files), known),
17
+ );
18
+ };
19
+
20
+ /** Settings that name something chaff does not have. Each would silently check less than was asked, so the run stops on them. */
21
+ export const settingProblems = (command: string, genreFlag: string | undefined, config: Config, text: CliText): string[] => [
22
+ ...genreProblems(command, genreFlag, config, text),
23
+ ...profileProblems(command, config, text),
24
+ ];
@@ -0,0 +1,13 @@
1
+ import type { Span } from "./plugin.ts";
2
+
3
+ /** 重なる範囲を 1 つにまとめ、始まりの順に並べる。joinTouching なら、接するだけの範囲(前の end が次の start)もまとめる。 */
4
+ export const mergeSpans = (spans: readonly Span[], joinTouching: boolean): Span[] =>
5
+ spans
6
+ .toSorted((left, right) => left.start - right.start)
7
+ .reduce<Span[]>((acc, span) => {
8
+ const last = acc.at(-1);
9
+ if (last !== undefined && (joinTouching ? span.start <= last.end : span.start < last.end))
10
+ acc[acc.length - 1] = { start: last.start, end: Math.max(last.end, span.end) };
11
+ else acc.push(span);
12
+ return acc;
13
+ }, []);
@@ -0,0 +1,129 @@
1
+ import type { Span } from "./plugin.ts";
2
+
3
+ /**
4
+ * 戯曲・台本・議事録・書き起こしの、話し手の名前(`ALGERNON.`、行頭の `夫` と全角空白、`○事務局`)。
5
+ * 名前は誰が話すかの印であって本文ではない。文として数えると、名前だけの文が増え、名前が固有名詞と言い回しの繰り返しに入る。
6
+ *
7
+ * 形だけで決める。同じ形の行頭の名前が、MIN_SPEAKERS 人以上それぞれ MIN_TURNS 回以上繰り返され、どれも後に発言が続き、
8
+ * その形の行が文書の段落の多くを始める。1 つの名前の繰り返しは見出し(○ と全角空白の後の「注意事項」)、
9
+ * 繰り返されない名前は偶然、ときどき出るだけの `NOTE:` `WARNING:` は注記なので、話し手と読まない。
10
+ * そう読めた形では、1 度しか話さない人の名前も話し手の名前とする(議事録の委員)。
11
+ */
12
+
13
+ /** 話し手と読むのに要る、繰り返す名前の数。 */
14
+ const MIN_SPEAKERS = 2;
15
+ /** 名前が繰り返されたと読む回数。 */
16
+ const MIN_TURNS = 3;
17
+ /** 対話と読むのに要る、その形の行の数。数回の注記(NOTE: と WARNING:)は対話ではない。 */
18
+ const MIN_DIALOGUE_TURNS = 10;
19
+ /** その形の行が始める段落の割合の下限。会議録は長い発言が何段落も続くので、戯曲より低い。 */
20
+ const MIN_TURN_SHARE = 0.1;
21
+
22
+ type Candidate = { readonly shape: string; readonly name: string; readonly span: Span };
23
+
24
+ /** 名前の後の発言が、同じ行にあるか(inline)、次の行にあるか(alone)。 */
25
+ type Shape = { readonly id: string; readonly pattern: RegExp; readonly alone: boolean };
26
+
27
+ /**
28
+ * 英語の名前は大文字で書いたものだけ(ALGERNON、LADY BRACKNELL、Mr. SMITH)。
29
+ * 大文字小文字まじりの「Title:」「Notes:」「Content-Type:」は、記録や表の項目の名前と形が同じで、見分けられない。
30
+ */
31
+ const CAPS_WORD = String.raw`[A-Z][A-Z'’&-]*\.?`;
32
+ const CAPS_NAME = String.raw`(?:[A-Z][a-z]{1,3}\. )?${CAPS_WORD}(?: ${CAPS_WORD}){0,3}`;
33
+ /** 日本語の名前に入らない文字。空白、括弧、句読点、かぎ括弧、コロン。 */
34
+ const JA_NAME = String.raw`[^\s\u3000()()、。,.「」『』::]`;
35
+
36
+ /** 各 pattern の 1 番目の組が行頭の空白、2 番目が覆う範囲、`name` の組が名前。 */
37
+ const SHAPES: readonly Shape[] = [
38
+ { id: "caps", pattern: new RegExp(String.raw`^(\s*)((?<name>${CAPS_NAME})[.:]?)\s*$`, "u"), alone: true },
39
+ { id: "caps", pattern: new RegExp(String.raw`^(\s*)((?<name>${CAPS_NAME})[.:])\s+\S`, "u"), alone: false },
40
+ { id: "mark", pattern: new RegExp(String.raw`^([ \t\u3000]*)([○◯〇](?<name>${JA_NAME}{1,20})(?:([^)\n]*))?)[ \t\u3000]*$`, "u"), alone: true },
41
+ { id: "mark", pattern: new RegExp(String.raw`^([ \t\u3000]*)([○◯〇](?<name>${JA_NAME}{1,20})(?:([^)\n]*))?\u3000+)\S`, "u"), alone: false },
42
+ { id: "spaced", pattern: new RegExp(String.raw`^([ \t]*)((?<name>${JA_NAME}{1,10})\u3000+)\S`, "u"), alone: false },
43
+ { id: "quote", pattern: new RegExp(String.raw`^([ \t]*)((?<name>${JA_NAME}{1,10}))[ \t]*「`, "u"), alone: false },
44
+ ];
45
+
46
+ /**
47
+ * 名前でないもの。番号(数字・漢数字・ローマ数字・「第」で始まる序数・1 字の仮名)は見出しや項目の印で、話し手ではない。
48
+ * 助詞で終わる語は地の文の主語(「私は「…」」)。英字 1 字は、名前より略語や項目の印が多い。
49
+ */
50
+ const NUMERAL = /^[\d0-9一二三四五六七八九十百千〇IVXLCDM]+(?:の[\d0-9一二三四五六七八九十百千〇]+)*$/u;
51
+ const ORDINAL = /^第[\d0-9一二三四五六七八九十百千〇]/u;
52
+ const DIGIT = /[\d0-9]/u;
53
+ const SINGLE_KANA = /^[\p{Script=Hiragana}\p{Script=Katakana}]$/u;
54
+ const LETTER = /\p{L}/u;
55
+ const PARTICLE_END = /[はがもをにへとでやの]$/u;
56
+
57
+ const isName = (shape: string, name: string): boolean =>
58
+ LETTER.test(name) &&
59
+ !NUMERAL.test(name.replace(/[\s.'’&-]/gu, "")) &&
60
+ !ORDINAL.test(name) &&
61
+ !DIGIT.test(name) &&
62
+ !SINGLE_KANA.test(name) &&
63
+ !(shape === "quote" && PARTICLE_END.test(name)) &&
64
+ !(shape === "caps" && name.replace(/[^A-Z]/gu, "").length < 2);
65
+
66
+ type Line = { readonly start: number; readonly text: string };
67
+
68
+ const linesOf = (text: string): Line[] => {
69
+ const scan = { start: 0 };
70
+ return text.split("\n").map((raw) => {
71
+ const line = { start: scan.start, text: raw.replace(/\r$/u, "") };
72
+ scan.start += raw.length + 1;
73
+ return line;
74
+ });
75
+ };
76
+
77
+ const isBlank = (line: Line | undefined): boolean => line === undefined || line.text.trim() === "";
78
+
79
+ /** 名前だけの行は、次の行に発言があるときだけ話し手の名前(空行の後の大文字の行は見出し)。 */
80
+ const candidateOf = (lines: readonly Line[], index: number): Candidate | undefined => {
81
+ const line = lines[index];
82
+ if (line === undefined) return undefined;
83
+ return SHAPES.flatMap((shape) => {
84
+ const match = shape.pattern.exec(line.text);
85
+ const lead = match?.[1];
86
+ const label = match?.[2];
87
+ const name = match?.groups?.["name"]?.replace(/[.:]$/u, "");
88
+ if (lead === undefined || label === undefined || name === undefined || !isName(shape.id, name)) return [];
89
+ if (shape.alone && isBlank(lines[index + 1])) return [];
90
+ const start = line.start + lead.length;
91
+ return [{ shape: shape.id, name, span: { start, end: start + label.length } }];
92
+ })[0];
93
+ };
94
+
95
+ const countBy = <T>(items: readonly T[], keyOf: (item: T) => string): Map<string, number> =>
96
+ items.reduce((counts, item) => counts.set(keyOf(item), (counts.get(keyOf(item)) ?? 0) + 1), new Map<string, number>());
97
+
98
+ const speakerKey = (candidate: Candidate): string => `${candidate.shape}\n${candidate.name}`;
99
+
100
+ /** 形ごとに、MIN_TURNS 回以上出る名前が MIN_SPEAKERS 人以上いるか。 */
101
+ const speakerShapes = (candidates: readonly Candidate[]): Set<string> => {
102
+ const turns = countBy(candidates, speakerKey);
103
+ const speakers = [...new Map(candidates.map((candidate) => [speakerKey(candidate), candidate])).values()];
104
+ const repeated = speakers.filter((speaker) => (turns.get(speakerKey(speaker)) ?? 0) >= MIN_TURNS);
105
+ const perShape = countBy(repeated, (speaker) => speaker.shape);
106
+ return new Set([...perShape].filter(([, count]) => count >= MIN_SPEAKERS).map(([shape]) => shape));
107
+ };
108
+
109
+ /** 空行の後から始まる、空でない行のまとまり(段落)の数。 */
110
+ const paragraphCount = (lines: readonly Line[]): number => lines.filter((line, index) => !isBlank(line) && (index === 0 || isBlank(lines[index - 1]))).length;
111
+
112
+ /** 話し手の形のうち、その形の行が MIN_DIALOGUE_TURNS 以上あり、段落の MIN_TURN_SHARE 以上を始めるもの。 */
113
+ const dialogueShapes = (candidates: readonly Candidate[], paragraphs: number): Set<string> => {
114
+ const turns = countBy(candidates, (candidate) => candidate.shape);
115
+ return new Set(
116
+ [...speakerShapes(candidates)].filter((shape) => {
117
+ const count = turns.get(shape) ?? 0;
118
+ return count >= MIN_DIALOGUE_TURNS && count >= paragraphs * MIN_TURN_SHARE;
119
+ }),
120
+ );
121
+ };
122
+
123
+ /** text の中の話し手の名前の範囲。発言は含まない。 */
124
+ export const speakerLabels = (text: string): Span[] => {
125
+ const lines = linesOf(text);
126
+ const candidates = lines.flatMap((_, index) => candidateOf(lines, index) ?? []);
127
+ const shapes = dialogueShapes(candidates, paragraphCount(lines));
128
+ return candidates.filter((candidate) => shapes.has(candidate.shape)).map((candidate) => candidate.span);
129
+ };
@@ -0,0 +1,81 @@
1
+ // 日本語の語と語のあいだの空白。どの境目を数えるかを品詞で決める。
2
+ import type { Sentence, Span, Token } from "./plugin.ts";
3
+ import { isJapanese } from "./orthography.ts";
4
+ import { isWithinAny, quotedIn } from "./quoted-span.ts";
5
+
6
+ /** between-phrases は助詞の後ろ(文節の切れ目)、inside-phrase は文節を始められない語の前(助詞・助動詞・名詞に続く「する」)。 */
7
+ export type JointKind = "between-phrases" | "inside-phrase";
8
+
9
+ /** offset は前の語の終わり(文書の座標)。before / after は境目の両側の語。 */
10
+ export type Joint = { readonly offset: number; readonly kind: JointKind; readonly spaced: boolean; readonly before: string; readonly after: string };
11
+
12
+ const SPACES = /^[ \u3000]+$/u;
13
+ const PARTICLE = new Set(["ADP", "SCONJ"]);
14
+ // 文節の頭に立てない語。「確認 しました」の「する」は名詞の後ろのときだけ(「し始める」の「し」は動詞の頭に立つ)。
15
+ const BOUND = new Set(["ADP", "SCONJ", "PART", "AUX"]);
16
+ const LIGHT_VERB = "する";
17
+
18
+ /**
19
+ * 空白の後ろの「が」を、解析器は文頭の接続詞(「が、」)と読む。一字の接続詞(が・と・や・か)は助詞として扱う。
20
+ * 二字以上(また・なお)は文節を始める接続詞。
21
+ */
22
+ const isBound = (token: Token): boolean => BOUND.has(token.pos) || (token.pos === "CCONJ" && [...token.surface].length === 1);
23
+
24
+ const kindOf = (left: Token, right: Token): JointKind | undefined => {
25
+ if (isBound(right) || (right.lemma === LIGHT_VERB && left.pos === "NOUN")) return "inside-phrase";
26
+ // 名詞どうし(担当 田中、赤坂 文弥、フルデスクトップ モード)は名札・名前・複合語の書き方で、数えない。
27
+ return PARTICLE.has(left.pos) ? "between-phrases" : undefined;
28
+ };
29
+
30
+ /**
31
+ * 行(か文)の頭の語か。助詞は前の語に付くので、行頭の「助詞」(「ヘ」)は項目の記号を読み違えたもので、後ろの空白は記号と本文の区切り。
32
+ * 行頭の内容語(折り返した「確認 しました」の「確認」)は語句の頭なので、ここでは外さない。
33
+ */
34
+ const isLineHead = (text: string, at: number): boolean => text.slice(text.lastIndexOf("\n", at - 1) + 1, at).trim() === "";
35
+
36
+ const isWord = (token: Token): boolean => token.surface.trim() !== "";
37
+
38
+ type Pair = { readonly left: Token; readonly right: Token };
39
+
40
+ const pairsOf = (tokens: readonly Token[]): Pair[] =>
41
+ tokens.filter(isWord).flatMap((right, index, words) => {
42
+ const left = words[index - 1];
43
+ return left === undefined ? [] : [{ left, right }];
44
+ });
45
+
46
+ // 強調とリンクの括弧。読み手には見えない。
47
+ const CHROME = /[*_~[\]]/gu;
48
+
49
+ /**
50
+ * 空けたか詰めたか。間が何も無いか強調・リンクの括弧だけなら詰めた、書き手が書いた空白(半角・全角)だけなら空けた。
51
+ * 括弧の隣の空白(「**…こと** とする」「手順は [README]」)は、日本語の隣で強調が効かない Markdown のための空白なので数えない。
52
+ * 改行(Markdown の改行や、行の折り返し)や、見える字を覆った空白(コード)をまたぐ語も、境目に数えない。
53
+ */
54
+ const spacing = (sentence: Sentence, written: string, pair: Pair): boolean | undefined => {
55
+ const [from, to] = [pair.left.span.end - sentence.span.start, pair.right.span.start - sentence.span.start];
56
+ const gap = written.slice(from, to);
57
+ if (gap.replace(CHROME, "") === "") return false;
58
+ return SPACES.test(gap) ? true : undefined;
59
+ };
60
+
61
+ const jointOf = (sentence: Sentence, written: string, quoted: readonly Span[], pair: Pair): Joint | undefined => {
62
+ const { left, right } = pair;
63
+ if (!isJapanese(left.surface.at(-1)) || !isJapanese(right.surface.at(0))) return undefined;
64
+ const spaced = spacing(sentence, written, pair);
65
+ if (spaced === undefined || isWithinAny(quoted, { start: left.span.start, end: right.span.end })) return undefined;
66
+ const kind = kindOf(left, right);
67
+ if (kind === undefined || (kind === "between-phrases" && isLineHead(sentence.text, left.span.start - sentence.span.start))) return undefined;
68
+ return { offset: left.span.end, kind, spaced, before: left.surface, after: right.surface };
69
+ };
70
+
71
+ /**
72
+ * 文の中の、日本語の語と語の境目のうち、空けるか詰めるかを数えるもの。written は本文でないものを覆う前の、文と同じ範囲。
73
+ * 英字・数字との境目は latin-spacing が見るので入れない。
74
+ */
75
+ export const phraseJoints = (sentence: Sentence, written: string): Joint[] => {
76
+ const quoted = quotedIn(sentence);
77
+ return pairsOf(sentence.tokens ?? []).flatMap((pair) => {
78
+ const joint = jointOf(sentence, written, quoted, pair);
79
+ return joint === undefined ? [] : [joint];
80
+ });
81
+ };
@@ -73,9 +73,9 @@ const open = (state: State, frame: Frame): void => {
73
73
  state.stack.push(frame);
74
74
  };
75
75
 
76
- const nearestNumbered = (state: State): Frame | undefined => [...state.stack].reverse().find((frame) => frame.numbered !== undefined);
76
+ const nearestNumbered = (state: State): Frame | undefined => state.stack.findLast((frame) => frame.numbered !== undefined);
77
77
 
78
- const nearestSection = (state: State): Frame | undefined => [...state.stack].reverse().find((frame) => frame.draft.kind === "section");
78
+ const nearestSection = (state: State): Frame | undefined => state.stack.findLast((frame) => frame.draft.kind === "section");
79
79
 
80
80
  /**
81
81
  * 条番号や 4.2 はそれだけで番地になる。項・号・(a) は親の番地に続ける。親が無ければ節の中の番号とする。
@@ -160,12 +160,12 @@ const universalNumber = (patterns: StructurePatterns, text: string, context: Num
160
160
  };
161
161
 
162
162
  /** 行の中の定義・参照・義務・数量を、いま開いている最も内側の節点の子にする。 */
163
- const enclosingArticle = (state: State): Draft | undefined => [...state.stack].reverse().find((frame) => frame.draft.kind === "article")?.draft;
163
+ const enclosingArticle = (state: State): Draft | undefined => state.stack.findLast((frame) => frame.draft.kind === "article")?.draft;
164
164
 
165
165
  /** 条より広い範囲(「In this Part」)なら、その深さの開いているまとまり。開いていなければ(Part の見出しの無い文書)無い。 */
166
166
  const widerScope = (state: State, patterns: StructurePatterns, text: string): Draft | undefined => {
167
167
  const depth = patterns.definitionScopeDepth?.(text);
168
- return depth === undefined ? undefined : [...state.stack].reverse().find((frame) => frame.numbered?.depth === depth)?.draft;
168
+ return depth === undefined ? undefined : state.stack.findLast((frame) => frame.numbered?.depth === depth)?.draft;
169
169
  };
170
170
 
171
171
  /** 範囲を宣言した行を持つまとまり。条の中なら条、条の外(Markdown の見出しの下、Part の直下)なら開いている一番内側のもの。 */
@@ -200,7 +200,7 @@ const addLeaves = (state: State, patterns: StructurePatterns, line: Line, wrappe
200
200
  attrs: mention.attrs,
201
201
  })),
202
202
  )
203
- .sort((left, right) => left.start - right.start);
203
+ .toSorted((left, right) => left.start - right.start);
204
204
  leaves.forEach((leaf) =>
205
205
  parent.children.push({
206
206
  kind: leaf.kind,
@@ -73,7 +73,7 @@ const addressedNodes = (tree: StructureNode): Addressed[] => {
73
73
  const current = pending.pop();
74
74
  if (current === undefined) break;
75
75
  if (current.node.address !== "") found.push(current);
76
- [...current.node.children].reverse().forEach((child) => pending.push({ node: child, depth: current.depth + 1 }));
76
+ current.node.children.toReversed().forEach((child) => pending.push({ node: child, depth: current.depth + 1 }));
77
77
  }
78
78
  return found;
79
79
  };
@@ -7,7 +7,7 @@ import { namesAbsentUnit } from "./unit-word.ts";
7
7
 
8
8
  export type StructureIssue = { readonly offset: number; readonly values: Readonly<Record<string, string | number>> };
9
9
 
10
- const NUMBERED: readonly string[] = ["chapter", "article", "item"];
10
+ const NUMBERED: ReadonlySet<string> = new Set(["chapter", "article", "item"]);
11
11
 
12
12
  /** 木を上から順に平らにする。文書の中での順番と同じになる。再帰にしないのは、深い木でスタックを使い切らないため。 */
13
13
  export const inDocumentOrder = (tree: StructureNode): StructureNode[] => {
@@ -17,7 +17,7 @@ export const inDocumentOrder = (tree: StructureNode): StructureNode[] => {
17
17
  const node = pending.pop();
18
18
  if (node === undefined) break;
19
19
  order.push(node);
20
- pending.push(...[...node.children].reverse());
20
+ pending.push(...node.children.toReversed());
21
21
  }
22
22
  return order;
23
23
  };
@@ -57,6 +57,8 @@ const citesOtherDocument =
57
57
  (node: StructureNode): boolean =>
58
58
  node.attrs["document"] !== undefined || (node.attrs["citedTag"] !== undefined && listed.has(textOf(node, "citedTag")));
59
59
 
60
+ const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
61
+
60
62
  /**
61
63
  * 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
62
64
  * 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
@@ -67,10 +69,9 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
67
69
  if (!nodes.some((node) => node.kind === "article")) return [];
68
70
  const tagged = nodes.some((node) => node.attrs["citedTag"] !== undefined);
69
71
  const citesOther = citesOtherDocument(tagged ? listedTags(source) : new Set());
70
- const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).flatMap(keysOf));
72
+ const addresses = new Set(nodes.filter((node) => NUMBERED.has(node.kind)).flatMap(keysOf));
71
73
  // Section で組んだ法令が "Articles 13 to 21 of the UK GDPR" と別の文書の Article を名指ししていれば、名の無い "Article 6(3)" もそちら。
72
74
  // 名指しがあれば、名の無い "Article 9" が書き間違いか向こうの条かは区別できないので黙る。名指しが無い文書では報告する。
73
- const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
74
75
  const numberings = new Set(nodes.flatMap((node) => (node.kind === "article" && node.numbering !== undefined ? [node.numbering] : [])));
75
76
  const citedElsewhere = new Set(nodes.filter((node) => node.kind === "reference" && citesOther(node)).map(numbering));
76
77
  const otherNumbering = (node: StructureNode): boolean =>
@@ -94,7 +95,7 @@ const definitionsInOrder = (tree: StructureNode): Definition[] => {
94
95
  if (current === undefined) break;
95
96
  if (current.node.kind === "definition") found.push(current);
96
97
  const article = current.node.kind === "article" ? current.node.address : current.article;
97
- [...current.node.children].reverse().forEach((child) => pending.push({ node: child, article }));
98
+ current.node.children.toReversed().forEach((child) => pending.push({ node: child, article }));
98
99
  }
99
100
  return found;
100
101
  };
@@ -1,6 +1,6 @@
1
1
  import { extname } from "node:path";
2
2
 
3
- const MARKDOWN = [".md", ".markdown", ".mdx"];
3
+ const MARKDOWN: ReadonlySet<string> = new Set([".md", ".markdown", ".mdx"]);
4
4
 
5
5
  /** 見出しとコードの範囲を Markdown として読むか。.txt の契約書は行頭の番号だけで木にする。 */
6
- export const isMarkdownPath = (path: string): boolean => MARKDOWN.includes(extname(path).toLowerCase());
6
+ export const isMarkdownPath = (path: string): boolean => MARKDOWN.has(extname(path).toLowerCase());
@@ -46,4 +46,4 @@ export const numberingBreaks = (tree: StructureNode): StructureIssue[] =>
46
46
  inDocumentOrder(tree)
47
47
  .flatMap(sequencesOf)
48
48
  .flatMap(breaksIn)
49
- .sort((left, right) => left.offset - right.offset);
49
+ .toSorted((left, right) => left.offset - right.offset);
@@ -233,5 +233,5 @@ export const relativeMentions = (
233
233
  const bare = bareMentions(text, patterns, vocabulary, number, [...absolute, ...named]);
234
234
  return [...named, ...bare]
235
235
  .filter((mention) => !substituted.some((span) => span.start <= mention.start && mention.start < span.end))
236
- .sort((left, right) => left.start - right.start);
236
+ .toSorted((left, right) => left.start - right.start);
237
237
  };
@@ -10,7 +10,13 @@ import type { StructureNode } from "../plugin.ts";
10
10
  /** range は「第四十三条から第五十五条まで 削除」のように、一行で並びをまとめたもの。番地は最初のものしか持たない。 */
11
11
  type Place = { readonly address: string; readonly level: number; readonly range?: boolean };
12
12
 
13
- type Last = { readonly target: string; readonly document: string | undefined };
13
+ /**
14
+ * 参照が指した文書: 他の文書の名前と、番号を名指した単位の語。
15
+ * 単位の語は、見出しに「1 目的」と番号を振った指針の「法第16条」が、条で数える別の文書の条だと決めるのに使う(unit-word.ts)。
16
+ */
17
+ type Cited = { readonly document: string | undefined; readonly unitWord: string | undefined };
18
+
19
+ type Last = Cited & { readonly target: string };
14
20
 
15
21
  const NUMBERED = new Set(["chapter", "article", "item"]);
16
22
 
@@ -100,21 +106,21 @@ const continued = (level: number, node: StructureNode, context: Context): Last |
100
106
  if (origin === undefined || context.articleLevel === undefined || level < context.articleLevel) return undefined;
101
107
  const parts = origin.target.split(".");
102
108
  const kept = level - context.articleLevel + 1;
103
- return parts.length < kept ? undefined : { target: parts.slice(0, kept).join("."), document: origin.document };
109
+ return parts.length < kept ? undefined : { ...origin, target: parts.slice(0, kept).join(".") };
104
110
  };
105
111
 
106
112
  /**
107
113
  * 参照が名指しした深さを覚える。名指しは、書き出しの深さ(第〇条なら条、前項なら項)から番地の深さまで。
108
114
  * 番地を深さで切る起点は条。条より外のまとまりは番地の形が違うので覚えない。
109
115
  */
110
- const remember = (context: Context, target: string, document: string | undefined, from: number): void => {
116
+ const remember = (context: Context, reference: Last, from: number): void => {
111
117
  const articleLevel = context.articleLevel;
112
118
  // 章・節は番地の形(ch1)が条から始まらないので覚えない。覚えると「同条」が章を指してしまう。
113
119
  if (articleLevel === undefined || from < articleLevel) return;
114
- const parts = target.split(".");
120
+ const parts = reference.target.split(".");
115
121
  parts.forEach((_, index) => {
116
122
  const level = articleLevel + index;
117
- if (level >= from) context.last.set(level, { target: parts.slice(0, index + 1).join("."), document });
123
+ if (level >= from) context.last.set(level, { ...reference, target: parts.slice(0, index + 1).join(".") });
118
124
  });
119
125
  };
120
126
 
@@ -145,12 +151,17 @@ const baseOf = (way: string, node: StructureNode, anchor: Anchor | undefined, la
145
151
  return anchor === undefined ? undefined : byPosition(way, Number(node.attrs["count"]), anchor);
146
152
  };
147
153
 
148
- /** 同と並びの続きは、元の参照が他の文書を指していれば、それを引き継ぐ。前・次・本はこの文書の中。 */
149
- const documentOf = (way: string, node: StructureNode, last: Last | undefined, context: Context): string | undefined => {
150
- if (way === "same") return last?.document;
151
- return way === "continue" ? continued(Number(node.attrs["level"]), node, context)?.document : undefined;
154
+ /** 同と並びの続きは、元の参照が指した文書を引き継ぐ。前・次・本はこの文書の中。 */
155
+ const citedOf = (way: string, node: StructureNode, last: Last | undefined, context: Context): Cited | undefined => {
156
+ if (way === "same") return last;
157
+ return way === "continue" ? continued(Number(node.attrs["level"]), node, context) : undefined;
152
158
  };
153
159
 
160
+ const citedAttrs = (cited: Cited | undefined): Record<string, string> => ({
161
+ ...(cited?.document === undefined ? {} : { document: cited.document }),
162
+ ...(cited?.unitWord === undefined ? {} : { unitWord: cited.unitWord }),
163
+ });
164
+
154
165
  const resolveOne = (node: StructureNode, path: readonly StructureNode[], context: Context): StructureNode | undefined => {
155
166
  const way = String(node.attrs["relative"]);
156
167
  const level = Number(node.attrs["level"]);
@@ -160,12 +171,11 @@ const resolveOne = (node: StructureNode, path: readonly StructureNode[], context
160
171
  const place = base === undefined ? undefined : withSuffix(base, String(node.attrs["suffix"] ?? ""), context.implicitLevel);
161
172
  if (place === undefined) return undefined;
162
173
  const fallback = fallbackOf(place, context.implicitLevel);
163
- const document = documentOf(way, node, last, context);
164
174
  const attrs = {
165
175
  target: place.address,
166
176
  label: String(node.attrs["label"]),
167
177
  ...(fallback === undefined ? {} : { fallback }),
168
- ...(document === undefined ? {} : { document }),
178
+ ...citedAttrs(citedOf(way, node, last, context)),
169
179
  };
170
180
  return { ...node, attrs };
171
181
  };
@@ -176,17 +186,16 @@ const isRelative = (node: StructureNode): boolean => node.kind === "reference" &
176
186
  const walk = (node: StructureNode, path: readonly StructureNode[], context: Context): StructureNode | undefined => {
177
187
  if (isRelative(node)) {
178
188
  const resolved = resolveOne(node, path, context);
179
- const target = resolved === undefined ? undefined : String(resolved.attrs["target"]);
180
- const document = optional(resolved?.attrs["document"]);
181
- if (target !== undefined) context.byStart.set(node.span.start, { target, document });
182
- if (target !== undefined) remember(context, target, document, Number(node.attrs["names"] ?? node.attrs["level"]));
189
+ if (resolved === undefined) return undefined;
190
+ const reference = lastOf(resolved);
191
+ context.byStart.set(node.span.start, reference);
192
+ remember(context, reference, Number(node.attrs["names"] ?? node.attrs["level"]));
183
193
  return resolved;
184
194
  }
185
195
  if (node.kind === "reference") {
186
- const target = String(node.attrs["target"]);
187
- const document = optional(node.attrs["document"]);
188
- context.byStart.set(node.span.start, { target, document });
189
- remember(context, target, document, context.articleLevel ?? 0);
196
+ const reference = lastOf(node);
197
+ context.byStart.set(node.span.start, reference);
198
+ remember(context, reference, context.articleLevel ?? 0);
190
199
  }
191
200
  const inside = [...path, node];
192
201
  const children = node.children.flatMap((child) => {
@@ -198,6 +207,12 @@ const walk = (node: StructureNode, path: readonly StructureNode[], context: Cont
198
207
 
199
208
  const optional = (value: string | number | undefined): string | undefined => (value === undefined ? undefined : String(value));
200
209
 
210
+ const lastOf = (reference: StructureNode): Last => ({
211
+ target: String(reference.attrs["target"]),
212
+ document: optional(reference.attrs["document"]),
213
+ unitWord: optional(reference.attrs["unitWord"]),
214
+ });
215
+
201
216
  const articlesOf = (node: StructureNode): Place[] => {
202
217
  const place = node.kind === "article" ? placeOf(node) : undefined;
203
218
  return [...(place === undefined ? [] : [place]), ...node.children.flatMap(articlesOf)];
@@ -8,7 +8,7 @@ const atom = (value: string | number): string => (typeof value === "number" ? St
8
8
  /** 属性はキーの順に並べる。同じ木から毎回同じ文字列を出すため。 */
9
9
  const keywords = (node: StructureNode): string[] => [
10
10
  ...Object.keys(node.attrs)
11
- .sort((left, right) => left.localeCompare(right, "en"))
11
+ .toSorted((left, right) => left.localeCompare(right, "en"))
12
12
  .flatMap((key) => {
13
13
  const value = node.attrs[key];
14
14
  return value === undefined ? [] : [`:${key} ${atom(value)}`];
@@ -24,9 +24,9 @@ export const unnumberedUnit = (line: string, open: readonly OpenUnit[], rule: Ru
24
24
  if (rule === undefined) return undefined;
25
25
  const rest = patternOf(rule.indent).exec(line)?.groups?.["rest"];
26
26
  if (rest === undefined) return undefined;
27
- const holder = [...open].reverse().find((unit) => unit.numbered.kind === rule.inside);
27
+ const holder = open.findLast((unit) => unit.numbered.kind === rule.inside);
28
28
  if (holder === undefined || !holder.bodyOnLine) return undefined;
29
- const previous = [...open].reverse().find((unit) => unit.numbered.depth === rule.depth)?.numbered;
29
+ const previous = open.findLast((unit) => unit.numbered.depth === rule.depth)?.numbered;
30
30
  if (previous !== undefined && previous.label !== "") return undefined;
31
31
  const number = String((previous === undefined ? 1 : Number(previous.number)) + 1);
32
32
  return { kind: "item", depth: rule.depth, number, absolute: false, label: "", heading: "", rest: rest.trim(), ordinal: Number(number) };
@@ -0,0 +1,39 @@
1
+ import type { Span } from "./plugin.ts";
2
+ import { mergeSpans } from "./span-merge.ts";
3
+
4
+ const SPACE = /\s/u;
5
+ const ESCAPE = /[.*+?^${}()|[\]\\]/gu;
6
+ const WHITESPACE_RUN = String.raw`\s+`;
7
+ const WRAP = String.raw`(?:\r?\n[ \t]*)?`;
8
+
9
+ /** 名前の中の空白は空白の並びに当て、字と字の間には折り返しの改行を許す。本文の行は好きな所で折れる。 */
10
+ const patternOf = (name: string): RegExp => {
11
+ const pieces = Array.from(name.trim()).reduce<string[]>((acc, char) => {
12
+ const previous = acc.at(-1);
13
+ if (SPACE.test(char)) {
14
+ if (previous !== WHITESPACE_RUN) acc.push(WHITESPACE_RUN);
15
+ return acc;
16
+ }
17
+ const escaped = char.replace(ESCAPE, "\\$&");
18
+ if (previous !== undefined && previous !== WHITESPACE_RUN) acc.push(WRAP);
19
+ acc.push(escaped);
20
+ return acc;
21
+ }, []);
22
+ return new RegExp(pieces.join(""), "gu");
23
+ };
24
+
25
+ const occurrences = (text: string, name: string): Span[] =>
26
+ name.trim().length === 0 ? [] : [...text.matchAll(patternOf(name))].map((match) => ({ start: match.index, end: match.index + match[0].length }));
27
+
28
+ /**
29
+ * チームが chaff.yaml の names に並べた名前が、text のどこにあるか。綴りは書かれたとおり(Apple と apple は別)。
30
+ * 重なる出現は 1 つの範囲にまとめる。接するだけなら別の名前(並んだ 2 つの名前は 2 つと数える)。位置は text の先頭を 0 とする。
31
+ */
32
+ export const nameSpans = (text: string, names: readonly string[]): Span[] =>
33
+ mergeSpans(
34
+ names.flatMap((name) => occurrences(text, name)),
35
+ false,
36
+ );
37
+
38
+ /** [start, end) が names の範囲のどれかと重なるか。 */
39
+ export const touchesAny = (spans: readonly Span[], start: number, end: number): boolean => spans.some((span) => start < span.end && span.start < end);
package/src/ui.ts CHANGED
@@ -19,5 +19,12 @@ export const hostLanguage = (configLanguage: string | undefined, env: Readonly<R
19
19
  return locale !== undefined && locale.toLowerCase().startsWith("ja") ? "ja" : "en";
20
20
  };
21
21
 
22
+ /** Several documents end with one closing: in their language when they share one, else the host's. */
23
+ export const sharedLanguage = (languages: readonly string[], host: UiLanguage): UiLanguage => {
24
+ const distinct = new Set(languages.map((language) => uiLanguageOf(language)));
25
+ const [only] = [...distinct];
26
+ return distinct.size === 1 && only !== undefined ? only : host;
27
+ };
28
+
22
29
  /** Text in both languages. Each module keeps its own, next to where it is used. */
23
30
  export type Texts<T> = Readonly<Record<UiLanguage, T>>;
package/src/version.ts CHANGED
@@ -21,7 +21,7 @@ export const versionLines = (raw: unknown): string[] => {
21
21
  const dependencies = isRecord(raw) && isRecord(raw["dependencies"]) ? raw["dependencies"] : {};
22
22
  const bundled = Object.entries(dependencies)
23
23
  .filter((entry): entry is [string, string] => BUNDLED.test(entry[0]) && typeof entry[1] === "string")
24
- .sort(([left], [right]) => left.localeCompare(right))
24
+ .toSorted(([left], [right]) => left.localeCompare(right))
25
25
  .map(([name, version]) => `${name} ${version}`);
26
26
  return [`chaffjs ${versionOf(raw)}`, ...bundled];
27
27
  };
package/src/watch.ts CHANGED
@@ -7,24 +7,24 @@ const SETTLE_MS = 120;
7
7
  export type Snapshot = Readonly<Record<string, number>>;
8
8
 
9
9
  export const snapshotOf = (findings: readonly Finding[]): Snapshot =>
10
- findings.reduce<Record<string, number>>((acc, finding) => ({ ...acc, [finding.rule]: (acc[finding.rule] ?? 0) + 1 }), {});
10
+ Object.fromEntries(findings.reduce((counts, finding) => counts.set(finding.rule, (counts.get(finding.rule) ?? 0) + 1), new Map<string, number>()));
11
11
 
12
12
  const total = (snapshot: Snapshot): number => Object.values(snapshot).reduce((sum, count) => sum + count, 0);
13
13
 
14
14
  /**
15
15
  * 差分だけを出す。書いている最中に全件を出し直されると、何が変わったのか分からない。
16
- * workflow spec §11。
16
+ * workflow spec §11。`unit` takes the count after the change, which sets the English noun ("0 → 1 finding").
17
17
  */
18
- export const describeChange = (before: Snapshot, after: Snapshot, unit = " 件"): string | undefined => {
18
+ export const describeChange = (before: Snapshot, after: Snapshot, unit: (count: number) => string = () => " 件"): string | undefined => {
19
19
  const rules = [...new Set([...Object.keys(before), ...Object.keys(after)])];
20
20
  const moved = rules
21
21
  .map((rule) => ({ rule, delta: (after[rule] ?? 0) - (before[rule] ?? 0) }))
22
22
  .filter((entry) => entry.delta !== 0)
23
- .sort((left, right) => left.rule.localeCompare(right.rule, "en"));
23
+ .toSorted((left, right) => left.rule.localeCompare(right.rule, "en"));
24
24
  if (moved.length === 0) return undefined;
25
25
  const detail = moved.map((entry) => `${entry.delta > 0 ? "+" : ""}${entry.delta} ${entry.rule}`).join(", ");
26
26
  const mark = total(after) < total(before) ? "✓" : "✗";
27
- return `${mark} ${total(before)} → ${total(after)}${unit} (${detail})`;
27
+ return `${mark} ${total(before)} → ${total(after)}${unit(total(after))} (${detail})`;
28
28
  };
29
29
 
30
30
  export const clock = (): string => new Date().toTimeString().slice(0, 8);