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
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { loadAdapter } from "../adapter-load.ts";
4
4
  import { applyByPath } from "../config/by-path.ts";
5
5
  import { profileFor } from "../profile/for-file.ts";
6
+ import { resolveGenre } from "../resolve-genre.ts";
6
7
  import { guessLanguage } from "../detect.ts";
7
8
  import { isMarkdownPath } from "../structure/markdown-path.ts";
8
9
  import { buildStructure } from "../structure/of.ts";
@@ -35,15 +36,15 @@ const TEXT: Texts<{
35
36
  },
36
37
  };
37
38
 
38
- export const treeText = (context: TreeContext): (typeof TEXT)["ja"] => TEXT[context.ui ?? "ja"];
39
+ const treeText = (context: TreeContext): (typeof TEXT)["ja"] => TEXT[context.ui ?? "ja"];
39
40
 
40
- const FORMATS = ["sexp", "json"];
41
+ const FORMATS: ReadonlySet<string> = new Set(["sexp", "json"]);
41
42
 
42
43
  /** 値を取るフラグ。その次の引数は値で、対象のファイルではない。 */
43
- const VALUED = ["--format", "--language"];
44
+ const VALUED: ReadonlySet<string> = new Set(["--format", "--language", "--genre"]);
44
45
 
45
46
  export const treeTargets = (argv: readonly string[]): string[] =>
46
- argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.includes(all[index - 1] ?? ""));
47
+ argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
47
48
 
48
49
  export const readSource = async (path: string, context: TreeContext): Promise<string | undefined> => {
49
50
  try {
@@ -81,7 +82,7 @@ export const readTree = async (path: string, argv: readonly string[], context: T
81
82
  }
82
83
  // 日本語は形態素で数量と日付を読む。解析器が無ければ単位の表で読むので、木は作れる。
83
84
  await adapter.prepare?.({ pos: true });
84
- const profile = profileFor(context.config, path, source, language);
85
+ const profile = profileFor(context.config, path, source, language, resolveGenre(path, source, context.config, context.flag(argv, "--genre")).genre);
85
86
  return { source, tree: buildStructure({ path, source, language, markdown: isMarkdownPath(path), profile }, adapter.structure) };
86
87
  };
87
88
 
@@ -108,7 +109,7 @@ export const inOrder = async (paths: readonly string[], each: (path: string) =>
108
109
  */
109
110
  export const runTree = async (paths: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
110
111
  const format = context.flag(argv, "--format") ?? "sexp";
111
- if (paths.length === 0 || !FORMATS.includes(format)) {
112
+ if (paths.length === 0 || !FORMATS.has(format)) {
112
113
  console.error(treeText(context).usage);
113
114
  return 1;
114
115
  }
@@ -44,12 +44,11 @@ export const matches = (glob: string, base: string, path: string): boolean => to
44
44
  type PathSettings = { genre?: string; language?: string; profile?: string };
45
45
 
46
46
  export const applyByPath = (rules: readonly PathRule[], base: string, path: string): PathSettings =>
47
- rules.reduce<PathSettings>((acc, rule) => {
48
- if (!rule.files.some((glob) => matches(glob, base, path))) return acc;
49
- return {
50
- ...acc,
51
- ...(rule.genre === undefined ? {} : { genre: rule.genre }),
52
- ...(rule.language === undefined ? {} : { language: rule.language }),
53
- ...(rule.profile === undefined ? {} : { profile: rule.profile }),
54
- };
55
- }, {});
47
+ rules
48
+ .filter((rule) => rule.files.some((glob) => matches(glob, base, path)))
49
+ .reduce<PathSettings>((settings, rule) => {
50
+ if (rule.genre !== undefined) settings.genre = rule.genre;
51
+ if (rule.language !== undefined) settings.language = rule.language;
52
+ if (rule.profile !== undefined) settings.profile = rule.profile;
53
+ return settings;
54
+ }, {});
@@ -30,6 +30,10 @@ export type Config = {
30
30
  readonly prefer: Readonly<Record<string, string>>;
31
31
  /** この種類の文書に無いと困る見出し。チームが自分で決める。 */
32
32
  readonly requiredSections: readonly string[];
33
+ /** チームの固有名詞(組織名・製品名)。1 つの名前として読み、漢字の連なりや繰り返しに数えない。 */
34
+ readonly names: readonly string[];
35
+ /** names に書かれていたが名前として読めなかった値。黙って捨てると、並べたつもりの名前が効いていないことに気づけない。 */
36
+ readonly unreadableNames: readonly string[];
33
37
  /** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
34
38
  readonly byPath: readonly PathRule[];
35
39
  readonly baseDir: string;
@@ -54,6 +58,8 @@ export const EMPTY: Config = {
54
58
  jargon: [],
55
59
  prefer: {},
56
60
  requiredSections: [],
61
+ names: [],
62
+ unreadableNames: [],
57
63
  byPath: [],
58
64
  baseDir: process.cwd(),
59
65
  };
@@ -114,6 +120,21 @@ const preferOf = (raw: unknown): Record<string, string> =>
114
120
  )
115
121
  : {};
116
122
 
123
+ const isNameEntry = (value: unknown): value is string | number => typeof value === "string" || typeof value === "number";
124
+
125
+ const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
126
+
127
+ /** 名前は文字列か数(2025 のような名前)の並び。並びでない値と、並びの中の名前でない項目は読めないものとして返す。 */
128
+ const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable: string[] } => {
129
+ if (raw === undefined || raw === null) return { names: [], unreadable: [] };
130
+ if (!Array.isArray(raw)) return { names: [], unreadable: [printed(raw)] };
131
+ const entries: unknown[] = raw;
132
+ return {
133
+ names: wordsOf(entries.filter(isNameEntry)),
134
+ unreadable: entries.filter((entry) => !isNameEntry(entry)).map(printed),
135
+ };
136
+ };
137
+
117
138
  const byPathOf = (raw: unknown): PathRule[] => (Array.isArray(raw) ? raw.map(toPathRule).filter((rule) => rule !== undefined) : []);
118
139
 
119
140
  /** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
@@ -122,6 +143,7 @@ export const loadConfig = (path: string): Config => {
122
143
  if (!isRecord(raw)) return { ...EMPTY, path };
123
144
  const declared: unknown = raw["ai_backend"];
124
145
  const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
146
+ const names = namesOf(raw["names"]);
125
147
  return {
126
148
  genre: str(raw["genre"]),
127
149
  profile: str(raw["profile"]),
@@ -137,6 +159,8 @@ export const loadConfig = (path: string): Config => {
137
159
  jargon: wordsOf(raw["jargon"]),
138
160
  prefer: preferOf(raw["prefer"]),
139
161
  requiredSections: wordsOf(raw["required_sections"]),
162
+ names: names.names,
163
+ unreadableNames: names.unreadable,
140
164
  byPath: byPathOf(raw["by_path"]),
141
165
  baseDir: dirname(path),
142
166
  };
@@ -0,0 +1,15 @@
1
+ import type { Config } from "./load.ts";
2
+ import type { Texts, UiLanguage } from "../ui.ts";
3
+
4
+ const TEXT: Texts<{ readonly unreadable: (where: string, value: string) => string }> = {
5
+ ja: {
6
+ unreadable: (where, value) => `${where}: names の ${value} は名前として読めません。names には名前を 1 行に 1 つずつ並べてください(- 個人情報保護委員会)`,
7
+ },
8
+ en: {
9
+ unreadable: (where, value) => `${where}: cannot read ${value} under names as a name. Write names as a list, one name per line (- Bank of England)`,
10
+ },
11
+ };
12
+
13
+ /** chaff.yaml の names のうち、名前として読めなかったもの。読めないまま黙ると、書いた人は名前を並べたつもりで何も変わっていない。 */
14
+ export const nameProblems = (config: Pick<Config, "unreadableNames" | "path">, ui: UiLanguage = "ja"): string[] =>
15
+ config.unreadableNames.map((value) => TEXT[ui].unreadable(config.path ?? "chaff.yaml", value));
package/src/date-stamp.ts CHANGED
@@ -21,7 +21,7 @@ const LABEL_EDGE = /[\s::]/u;
21
21
  const datesOf = (candidate: StampCandidate): Span[] => candidate.dates.filter((date) => date.end > date.start);
22
22
 
23
23
  const withoutDates = (candidate: StampCandidate): string => {
24
- const ordered = datesOf(candidate).sort((left, right) => left.start - right.start);
24
+ const ordered = datesOf(candidate).toSorted((left, right) => left.start - right.start);
25
25
  const gaps = ordered.reduce<{ readonly parts: readonly string[]; readonly from: number }>(
26
26
  (acc, date) => ({ parts: [...acc.parts, candidate.text.slice(acc.from, date.start)], from: Math.max(acc.from, date.end) }),
27
27
  { parts: [], from: 0 },
@@ -2,6 +2,7 @@ import { escapeRegExp } from "../orthography.ts";
2
2
  import { ROMAN_NUMERAL } from "./roman-numeral.ts";
3
3
  import { citationKeySpans } from "./citation-key.ts";
4
4
  import { dottedNameSpans } from "./dotted-name.ts";
5
+ import { letterJoinedNameSpans } from "./letter-joined-name.ts";
5
6
  import { loneNumeralSpans, nameNumeralSpans } from "./name-numeral.ts";
6
7
  import { placeholderSpans } from "./placeholder.ts";
7
8
  import { seriesLabelSpans } from "./series-label.ts";
@@ -25,6 +26,8 @@ export type NotationWords = {
25
26
  readonly divisions: readonly string[];
26
27
  /** 速記録が大文字で書く発言者の姓の前に置く敬称(Mr.、Mrs.、Madam)。 */
27
28
  readonly honorifics: readonly string[];
29
+ /** 大文字で書く姓の前に置く肩書き(Prime Minister、Governor)。後ろに略語も来る(the President NASA memo)ので、名前の形を求める。 */
30
+ readonly titles: readonly string[];
28
31
  /** 日付・時刻の数字の後ろに書く単位(年、月、日、時、分)。その前の同じ大文字の繰り返し(YY年)は書式の置き場所。 */
29
32
  readonly dateTimeUnits: readonly string[];
30
33
  };
@@ -49,6 +52,17 @@ const divisionOf = (words: readonly string[]): string => oneOf(words.flatMap((wo
49
52
  /** 敬称のすぐ後ろの大文字の姓(Mr. HAWLEY、Dr. O'NEIL)。速記録は発言者をこう書き、略語ではない。 */
50
53
  const SURNAME_IN_CAPITALS = String.raw`[A-Z]+(?:['’-][A-Z]+)*(?![\p{L}\p{N}_])`;
51
54
 
55
+ /**
56
+ * 肩書きの後ろの大文字の語が姓と読めるのは、名前の形のときだけ。行頭の発言者の印(Senator HAWLEY.、Chairman JORDAN:)か、
57
+ * 日本政府の英文が「姓 名」の順に書くローマ字の名前(Prime Minister ABE Shinzo)。the President NASA memo と
58
+ * The Secretary GDPR: Article 5 の略語は数える。
59
+ */
60
+ const GIVEN_NAME_AFTER = String.raw`(?=\s+\p{Lu}\p{Ll}+(?![\p{L}\p{N}_]))`;
61
+ const titledName = (titles: string): readonly RegExp[] => [
62
+ new RegExp(String.raw`(?<=(?:^|\n)[ \t]*)${titles}\s+${SURNAME_IN_CAPITALS}(?=[.:])`, "gu"),
63
+ new RegExp(String.raw`(?<![\p{L}\p{N}_])${titles}\s+${SURNAME_IN_CAPITALS}${GIVEN_NAME_AFTER}`, "gu"),
64
+ ];
65
+
52
66
  // 強調の記号は空白に置き換えてある(**3:30** PM)ので、部品の間の空白は数を問わない。
53
67
  const patternsOf = (words: NotationWords): readonly RegExp[] => [
54
68
  new RegExp(String.raw`${CLOCK_12}\s*${oneOf(words.meridiem)}`, "gu"),
@@ -59,6 +73,7 @@ const patternsOf = (words: NotationWords): readonly RegExp[] => [
59
73
  new RegExp(oneOf(words.emphasis), "gu"),
60
74
  new RegExp(String.raw`(?<![\p{L}\p{N}_])${divisionOf(words.divisions)}\s+${ROMAN_NUMERAL}(?![\p{L}\p{N}_&])`, "gu"),
61
75
  new RegExp(String.raw`(?<![\p{L}\p{N}_])${oneOf(words.honorifics)}\s+${SURNAME_IN_CAPITALS}`, "gu"),
76
+ ...titledName(oneOf(words.titles)),
62
77
  ];
63
78
 
64
79
  export type NotAcronymSpans = (text: string) => Span[];
@@ -73,6 +88,7 @@ export const notAcronymSpansOf = (words: NotationWords): NotAcronymSpans => {
73
88
  ...patterns.flatMap((pattern) => [...text.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }))),
74
89
  ...citationKeySpans(text),
75
90
  ...dottedNameSpans(text),
91
+ ...letterJoinedNameSpans(text),
76
92
  ...nameNumeralSpans(text),
77
93
  ...loneNumeralSpans(text),
78
94
  ...seriesLabelSpans(text),
@@ -12,6 +12,7 @@ import { escapeRegExp } from "../orthography.ts";
12
12
  * (single nucleotide polymorphism:SNP) 括弧の最後の項目が略語で、コロンの前の語の頭文字と揃う
13
13
  * 人事部(以下「HR」という。) 括弧の中が定義の語と略語だけ
14
14
  * overnight reverse repurchase agreement (ON RRP) 括弧の中が空白で繋いだ略語だけで、前の名前から文字が順に拾える
15
+ * Partnership On Wide Energy and Resources Resilience Asia (POWERR Asia) 括弧の中が略語と添えた語で、前の名前の頭文字と揃う
15
16
  * 括弧と略語の間には、引用符((“MNDA”)、(「MNDA」))と空白だけを許す。
16
17
  */
17
18
  const WRAP = String.raw`[\s"“”'‘’「」『』]*`;
@@ -149,12 +150,28 @@ const isJointBracketedAt = (body: string, acronym: string, at: number): boolean
149
150
  return abbreviates(body.slice(0, start), body.slice(start, close));
150
151
  };
151
152
 
153
+ /** 括弧の中で略語に添えた、大文字で始まる語((POWERR Asia)、(ZEC Initiative))と、閉じる括弧。 */
154
+ const QUALIFIER = new RegExp(String.raw`^(?<words>(?: [A-Z][a-z]+)+)${WRAP}[))]`, "u");
155
+
156
+ /**
157
+ * 括弧の中が略語と、それに添えた語だけの形。括弧の前の名前の頭文字が略語と揃うときだけ展開と見なす((NASA Goddard) は揃わない)。
158
+ * 添えた語が名前の終わりにもあれば(… Resilience Asia (POWERR Asia))、それを除いて比べる。
159
+ */
160
+ const isQualifiedAt = (body: string, acronym: string, at: number): boolean => {
161
+ if (!OPENED.test(body.slice(Math.max(0, at - NEAR), at))) return false;
162
+ const end = at + acronym.length;
163
+ const words = QUALIFIER.exec(body.slice(end, end + DEFINITION_REACH))?.groups?.["words"];
164
+ if (words === undefined) return false;
165
+ const name = body.slice(0, at).replace(ANY_OPENED, "").trimEnd();
166
+ return spellsOut(name.endsWith(words) ? name.slice(0, -words.length) : name, acronym);
167
+ };
168
+
152
169
  const isBracketedAt = (body: string, acronym: string, at: number): boolean => {
153
170
  const after = body.slice(at + acronym.length, at + acronym.length + NEAR);
154
171
  const before = body.slice(Math.max(0, at - NEAR), at);
155
172
  if (OPENS.test(after) || (OPENED.test(before) && CLOSES.test(after))) return true;
156
173
  if (SQUARE_OPENED.test(before) && SQUARE_CLOSES.test(after) && spellsOut(body.slice(0, at), acronym)) return true;
157
- return isSeparatedAt(body, acronym, at) || isNamedBeforeAt(body, acronym, at) || isJointBracketedAt(body, acronym, at);
174
+ return isSeparatedAt(body, acronym, at) || isNamedBeforeAt(body, acronym, at) || isJointBracketedAt(body, acronym, at) || isQualifiedAt(body, acronym, at);
158
175
  };
159
176
 
160
177
  /**
@@ -167,6 +184,120 @@ const EQUALS_DEFINITION = /^\s*[==]\s*(?=\p{L})\S*?[\p{Ll}\p{Lo}]/u;
167
184
  const isEqualsDefinedAt = (body: string, acronym: string, at: number): boolean =>
168
185
  EQUALS_DEFINITION.test(body.slice(at + acronym.length, at + acronym.length + DEFINITION_REACH));
169
186
 
187
+ /**
188
+ * 用語集の見出し語としての略語(## AFD、| AFD |、**AFD**:、- AFD —)。名前は見出し語のすぐ後ろ、つまり次の行・隣のセル・
189
+ * 区切りの後ろに書く。見出しと表は文にならないので、文をつないだ本文ではなく source の行を見る。
190
+ */
191
+ const TERM = String.raw`[A-Z][A-Z&-]*[A-Z]`;
192
+ const STRONG = /\*\*|__/gu;
193
+ const LIST_MARK = String.raw`(?:[-*+]|\d+[.)])\s+`;
194
+ const TERM_SEPARATOR = String.raw`(?:[::]|\s[-–—]|[–—])`;
195
+ /** 行が略語だけの見出し・段落(## AFD、AFD:、**AFD**)。 */
196
+ const TERM_LINE = new RegExp(String.raw`^(?:#{1,6}\s+)?(?<term>${TERM})\s*(?<colon>[::])?(?:\s+#+)?$`, "u");
197
+ /** 見出しか強調の印。印の無い「RTO:」の行はメモの見出しにも書くので、コロンを付けた略語だけの行は印があるときだけ見出し語と読む。 */
198
+ const MARKED_LINE = /^(?:#|\*\*|__)/u;
199
+ /**
200
+ * 箇条書きの頭の略語と区切りの後ろの名前(- AFD — Area…)か、強調した略語とその後ろの名前(**AFD**: Area…、**AFD** Area…)。
201
+ * 印の無い行頭の略語とコロン(RTO: Review ticket ownership.)は、メモの見出しにも書くので見出し語と読まない。
202
+ */
203
+ const TERM_LEAD = new RegExp(
204
+ String.raw`^(?:(?:${LIST_MARK})?(?<strong>\*\*|__)(?<strongTerm>${TERM})\s*(?:[::]\s*)?\k<strong>\s*${TERM_SEPARATOR}?|${LIST_MARK}(?<term>${TERM})\s*${TERM_SEPARATOR})\s*(?<rest>\S.*)$`,
205
+ "u",
206
+ );
207
+ const TABLE_ROW = /^\s*\|.*\|\s*$/u;
208
+ const WHOLE_TERM = new RegExp(String.raw`^${TERM}$`, "u");
209
+ /** 名前の終わり。文の終わり、区切り、括弧、空白で挟んだハイフン。読点は名前の中にも書く(Aviation, Range, and Aerospace Meteorology)ので、その前でも比べる。 */
210
+ const NAME_END = /[.;::((。|]|\s[-–—]\s|[–—]/u;
211
+ const NAME_COMMA = /[,,、]/u;
212
+ const LEADING_MARKS = /^[\s::>*+-]+/u;
213
+
214
+ /** 項目の頭の名前と、その読点の前まで(Service Level Agreement, a contract → Service Level Agreement)。 */
215
+ const namesOf = (text: string): string[] => {
216
+ const clause = (text.replaceAll(STRONG, "").replace(LEADING_MARKS, "").split(NAME_END)[0] ?? "").trim();
217
+ return [clause, (clause.split(NAME_COMMA)[0] ?? "").trim()].filter((name) => name !== "");
218
+ };
219
+
220
+ /** 1 語の名前を縮めた略語(ABV → Above、ABNDT → Abundant)。名前は小文字を含む語で、略語そのもの(SEO.)ではない。 */
221
+ const shortensWord = (name: string, acronym: string): boolean => !/\s/u.test(name) && /\p{Ll}/u.test(name) && abbreviates(name, acronym);
222
+
223
+ const spellsName = (text: string, acronym: string): boolean => namesOf(text).some((name) => namesAcronym(name, acronym) || shortensWord(name, acronym));
224
+
225
+ const cellsOf = (row: string): string[] =>
226
+ row
227
+ .trim()
228
+ .replace(/^\||\|$/gu, "")
229
+ .split("|")
230
+ .map((cell) => cell.replaceAll(STRONG, "").trim());
231
+
232
+ /** 表の行で、略語だけのセルの隣(後ろか前)のセルが名前のもの。 */
233
+ const tableTerms = (row: string): string[] => {
234
+ const cells = cellsOf(row);
235
+ return cells.filter(
236
+ (cell, index) => WHOLE_TERM.test(cell) && [cells[index + 1], cells[index - 1]].some((next) => next !== undefined && spellsName(next, cell)),
237
+ );
238
+ };
239
+
240
+ const leadTerm = (line: string): string[] => {
241
+ const lead = TERM_LEAD.exec(line.trim());
242
+ const term = lead?.groups?.["strongTerm"] ?? lead?.groups?.["term"];
243
+ return term !== undefined && spellsName(lead?.groups?.["rest"] ?? "", term) ? [term] : [];
244
+ };
245
+
246
+ /** 見出し語から名前までに見る行の数。空行が 2 つ挟まっても、次の段落を見る。 */
247
+ const NEXT_LINE_REACH = 3;
248
+
249
+ /** 略語だけの行と、その次の空でない行が名前のもの。 */
250
+ const lineTerm = (lines: readonly string[], index: number): string[] => {
251
+ const line = (lines[index] ?? "").trim();
252
+ const groups = TERM_LINE.exec(line.replaceAll(STRONG, ""))?.groups;
253
+ const term = groups?.["term"];
254
+ if (term === undefined || (groups?.["colon"] !== undefined && !MARKED_LINE.test(line))) return [];
255
+ const next = lines.slice(index + 1, index + 1 + NEXT_LINE_REACH).find((line) => line.trim() !== "");
256
+ return next !== undefined && spellsName(next, term) ? [term] : [];
257
+ };
258
+
259
+ const FENCE = /^ {0,3}(?<fence>`{3,}|~{3,})/u;
260
+ const FRONT_MATTER_EDGE = /^(?:---|\.\.\.)\s*$/u;
261
+
262
+ type FenceScan = { readonly open: string | undefined; readonly kept: string[] };
263
+
264
+ /** 閉じる行は記号の並びだけ。後ろに語がある行(```ts)は囲みの中の行。 */
265
+ const CLOSING_FENCE = /^ {0,3}(?<fence>`{3,}|~{3,})\s*$/u;
266
+
267
+ const closes = (line: string, open: string): boolean => CLOSING_FENCE.exec(line)?.groups?.["fence"]?.startsWith(open) === true;
268
+
269
+ /** コードの囲みの中の行(開き・閉じの行も)を空にする。閉じるのは同じ記号で、開きと同じか長い並び。 */
270
+ const withoutFences = (lines: readonly string[]): string[] =>
271
+ lines.reduce<FenceScan>(
272
+ (scan, line) => {
273
+ const fence = FENCE.exec(line)?.groups?.["fence"];
274
+ const inside = scan.open !== undefined || fence !== undefined;
275
+ scan.kept.push(inside ? "" : line);
276
+ if (scan.open === undefined) return { open: fence, kept: scan.kept };
277
+ return { open: closes(line, scan.open) ? undefined : scan.open, kept: scan.kept };
278
+ },
279
+ { open: undefined, kept: [] },
280
+ ).kept;
281
+
282
+ /** 文書の頭の front matter(--- で始まり --- か ... で閉じる)の行数。無ければ 0。 */
283
+ const frontMatterLength = (lines: readonly string[]): number =>
284
+ lines[0]?.trim() === "---" ? lines.findIndex((line, index) => index > 0 && FRONT_MATTER_EDGE.test(line)) + 1 : 0;
285
+
286
+ /** 4 桁以上の字下げはコードとして書ける。入れ子の箇条書きと見分けずに外す(外しても、略語が報告されるほうへ倒れるだけ)。 */
287
+ const INDENTED = /^(?: {4}|\t)/u;
288
+
289
+ /** 本文として読まない行(front matter、コードの囲み、字下げしたコード)を空にした行。行の位置は変えない。 */
290
+ const proseLines = (lines: readonly string[]): string[] => {
291
+ const skipped = frontMatterLength(lines);
292
+ return withoutFences(lines.map((line, index) => (index < skipped || INDENTED.test(line) ? "" : line)));
293
+ };
294
+
295
+ /** 用語集の見出し語として、すぐ後ろに名前を書いた略語。 */
296
+ export const termEntryAcronyms = (source: string): ReadonlySet<string> => {
297
+ const lines = proseLines(source.split(/\r?\n/u));
298
+ return new Set(lines.flatMap((line, index) => (TABLE_ROW.test(line) ? tableTerms(line) : [...lineTerm(lines, index), ...leadTerm(line)])));
299
+ };
300
+
170
301
  export type ExpandedAt = (body: string, acronym: string, at: number) => boolean;
171
302
 
172
303
  /** body の at にある略語が、その場で展開・定義されているか。語彙表から一度だけ組み立てて、略語ごとに呼ぶ。 */
@@ -0,0 +1,191 @@
1
+ import type { Detector, Finding, Lexicon, Token } from "../plugin.ts";
2
+ import { isPartOfLongerWord } from "./doubled-word.ts";
3
+
4
+ /**
5
+ * 語の形が合っていない(a significant changes / one of the most important feature / Your can check)。
6
+ * 書き換えで片方の語だけ直した跡。解析器の読みが一通りに決まる形だけを数える。語はすべてアダプタの語彙表から来る。
7
+ */
8
+
9
+ export type AgreementLists = {
10
+ /** 一つを数える限定詞(a / each)。 */
11
+ readonly singular: Lexicon;
12
+ /** 複数を数える限定詞と句(these / one of the)。 */
13
+ readonly plural: Lexicon;
14
+ /** 単数と複数が同じ綴りの名詞(series / data)。 */
15
+ readonly invariant: ReadonlySet<string>;
16
+ /** 単数の限定詞で複数を数える形容詞(a few days)。 */
17
+ readonly count: ReadonlySet<string>;
18
+ /** 名詞の前にしか立たない所有の語(your / their)。 */
19
+ readonly possessive: ReadonlySet<string>;
20
+ /** 名詞にならない be / have の形(is / has)。 */
21
+ readonly finite: ReadonlySet<string>;
22
+ /** 単数で書く所を複数で書いた句(ones of the most)。 */
23
+ readonly misnumbered: Lexicon;
24
+ };
25
+
26
+ export type Slip = { readonly variant: "number" | "possessive"; readonly first: Token; readonly last: Token; readonly word: string };
27
+
28
+ const SPACE_ONLY = /^\s+$/u;
29
+ const UPPER_START = /^\p{Lu}/u;
30
+ const LETTER = /\p{L}/u;
31
+ const LOWER = /\p{Ll}/u;
32
+ /** 名詞句を閉じる記号。読点・括弧の始まり・ハイフンは、名詞を重ねた語(a sales, marketing and support team)の途中にも立つ。 */
33
+ const CLOSING_MARK = new Set([".", ";", ":", "?", "!", ")"]);
34
+ /**
35
+ * 名詞の後ろに来れば、名詞句はそこで終わっている品詞。名詞・形容詞・接続詞・所有の印は、名詞を重ねた語の途中。
36
+ * 動詞は、名詞にならない be / have の形(is / has)だけ。解析器は複数の名詞の後ろの名詞を動詞と読む(a sales team の team が VBP)。
37
+ */
38
+ const PHRASE_END = new Set(["AUX", "ADP", "SCONJ", "PRON", "DET", "ADV"]);
39
+ const PREDICATE_TAIL = new Set(["DET", "ADJ", "PRON", "NUM"]);
40
+
41
+ const lower = (token: Token): string => token.surface.toLowerCase();
42
+
43
+ const isParticiple = (token: Token): boolean => token.features?.["VerbForm"] === "Part" || token.features?.["VerbForm"] === "Ger";
44
+
45
+ const isArticle = (token: Token): boolean => token.features?.["PronType"] === "Art";
46
+
47
+ const isPlural = (token: Token): boolean => token.features?.["Number"] === "Plur";
48
+
49
+ /** 名詞の後ろの語で、名詞句が終わるか。分詞(a data driven approach)も名詞を重ねた語の途中。 */
50
+ export const endsNounPhrase = (next: Token | undefined, finite: ReadonlySet<string>): boolean => {
51
+ if (next === undefined || finite.has(lower(next))) return true;
52
+ return next.pos === "PUNCT" ? CLOSING_MARK.has(next.surface) : PHRASE_END.has(next.pos);
53
+ };
54
+
55
+ const wordsOf = (entry: Lexicon[number]): readonly string[] =>
56
+ entry.tokens === undefined ? entry.pattern.toLowerCase().split(/\s+/u) : entry.tokens.map((token) => token.surface.toLowerCase());
57
+
58
+ /** tokens の at 語目から始まる語彙表の句の長さ。無ければ 0。 */
59
+ const phraseAt = (tokens: readonly Token[], at: number, phrases: Lexicon): number => {
60
+ const matched = phrases.map(wordsOf).find((words) => words.every((word, k) => tokens[at + k]?.surface.toLowerCase() === word));
61
+ return matched?.length ?? 0;
62
+ };
63
+
64
+ /** first から last まで、語のあいだが空白だけ。コードの印やリンクを挟めば別々の語。 */
65
+ const joinedBySpace = (source: string, tokens: readonly Token[]): boolean =>
66
+ tokens.slice(1).every((token, k) => SPACE_ONLY.test(source.slice(tokens[k]?.span.end ?? token.span.start, token.span.start)));
67
+
68
+ const opensSentence = (tokens: readonly Token[], at: number): boolean => !tokens.slice(0, at).some((token) => LETTER.test(token.surface));
69
+
70
+ /** 文の途中の大文字の限定詞は、名前の一部(Plan A users)。 */
71
+ const isNamePart = (tokens: readonly Token[], at: number): boolean => UPPER_START.test(tokens[at]?.surface ?? "") && !opensSentence(tokens, at);
72
+
73
+ const isModifier = (token: Token): boolean => token.pos === "ADJ" || token.pos === "ADV" || (token.pos === "VERB" && isParticiple(token));
74
+
75
+ /** 限定詞の後ろの修飾語。数の形容詞(a few days)が挟まれば、単数の限定詞でも複数を数えるので undefined。 */
76
+ const modifiersAfter = (tokens: readonly Token[], from: number, count: ReadonlySet<string>): readonly Token[] | undefined => {
77
+ const end = tokens.findIndex((token, index) => index >= from && !isModifier(token));
78
+ const modifiers = tokens.slice(from, end === -1 ? tokens.length : end);
79
+ return modifiers.some((token) => count.has(lower(token))) ? undefined : modifiers;
80
+ };
81
+
82
+ const isGuessed = (token: Token): boolean => token.features?.["Guess"] === "Yes";
83
+
84
+ /** 単数と複数が同じ綴りの語、複数と付いても原形が同じ語(lens)、解析器が形から当てた語(stimuli)は、どちらの数とも決まらない。 */
85
+ const hasOneNumber = (noun: Token, invariant: ReadonlySet<string>): boolean =>
86
+ !invariant.has(lower(noun)) && !isGuessed(noun) && !(isPlural(noun) && noun.lemma?.toLowerCase() === lower(noun));
87
+
88
+ const isCommonNoun = (token: Token | undefined): token is Token => token?.pos === "NOUN" && !UPPER_START.test(token.surface);
89
+
90
+ type Determiner = { readonly head: Token; readonly length: number; readonly plural: boolean };
91
+
92
+ const determinerAt = (tokens: readonly Token[], at: number, lists: AgreementLists): Determiner | undefined => {
93
+ const head = tokens[at];
94
+ const plural = phraseAt(tokens, at, lists.plural);
95
+ const singular = phraseAt(tokens, at, lists.singular);
96
+ if (head === undefined || plural + singular === 0) return undefined;
97
+ return plural > 0 ? { head, length: plural, plural: true } : { head, length: singular, plural: false };
98
+ };
99
+
100
+ /**
101
+ * 冠詞と句でない限定詞(this / these)は代名詞にもなる。This results in a loss の results は動詞なので、
102
+ * 形容詞を挟んだ名詞(this new rules)だけを数える。
103
+ */
104
+ const standsAlone = (determiner: Determiner): boolean => determiner.length === 1 && !isArticle(determiner.head);
105
+
106
+ const readsAsDeterminer = (determiner: Determiner, modifiers: readonly Token[]): boolean =>
107
+ !standsAlone(determiner) || modifiers.some((token) => token.pos === "ADJ");
108
+
109
+ const mayBeNoun = (token: Token): boolean => token.features?.["AlsoNoun"] === "Yes" || isGuessed(token);
110
+
111
+ /**
112
+ * 名詞が動詞にも読め(works)、その前に名詞句の頭になれる語があれば、限定詞と名詞ではなく主語と動詞かもしれない。
113
+ * an individual works(individual は名詞にもなる)/ Those responsible report to the board(those は代名詞にもなる)。
114
+ * 後ろが is / has なら、名詞は動詞ではない(these key result is)。
115
+ */
116
+ const mayBeVerb = (determiner: Determiner, modifiers: readonly Token[], noun: Token, next: Token | undefined, finite: ReadonlySet<string>): boolean =>
117
+ noun.features?.["AlsoVerb"] === "Yes" && !(next !== undefined && finite.has(lower(next))) && (standsAlone(determiner) || modifiers.some(mayBeNoun));
118
+
119
+ const numberSlipAt = (source: string, tokens: readonly Token[], at: number, lists: AgreementLists): Slip | undefined => {
120
+ const determiner = determinerAt(tokens, at, lists);
121
+ if (determiner === undefined || isNamePart(tokens, at)) return undefined;
122
+ const modifiers = modifiersAfter(tokens, at + determiner.length, lists.count);
123
+ if (modifiers === undefined || !readsAsDeterminer(determiner, modifiers)) return undefined;
124
+ const nounAt = at + determiner.length + modifiers.length;
125
+ const noun = tokens[nounAt];
126
+ if (!isCommonNoun(noun) || !hasOneNumber(noun, lists.invariant) || isPlural(noun) === determiner.plural) return undefined;
127
+ if (mayBeVerb(determiner, modifiers, noun, tokens[nounAt + 1], lists.finite)) return undefined;
128
+ const span = tokens.slice(at, nounAt + 1);
129
+ if (!endsNounPhrase(tokens[nounAt + 1], lists.finite) || !joinedBySpace(source, span) || isPartOfLongerWord(source, determiner.head, noun)) return undefined;
130
+ return slipOf("number", source, determiner.head, noun);
131
+ };
132
+
133
+ const slipOf = (variant: Slip["variant"], source: string, first: Token, last: Token): Slip => ({
134
+ variant,
135
+ first,
136
+ last,
137
+ word: source.slice(first.span.start, last.span.end).replace(/\s+/gu, " "),
138
+ });
139
+
140
+ const isBaseVerb = (token: Token | undefined): boolean => token?.pos === "VERB" && token.features?.["VerbForm"] === undefined;
141
+
142
+ /** 所有の語の直後の be(their is)か、助動詞 + 動詞の原形(Your can check)。名詞にもなる助動詞(your will)は動詞が続くときだけ。 */
143
+ const possessiveSlipAt = (source: string, tokens: readonly Token[], at: number, lists: AgreementLists): Slip | undefined => {
144
+ const [possessive, verb] = [tokens[at], tokens[at + 1]];
145
+ if (possessive === undefined || verb === undefined || !lists.possessive.has(lower(possessive))) return undefined;
146
+ // 大文字だけの語は略語(Your AM = account manager)。
147
+ const verbal = LOWER.test(verb.surface) && (lists.finite.has(lower(verb)) || (verb.pos === "AUX" && isBaseVerb(tokens[at + 2])));
148
+ if (!verbal || !joinedBySpace(source, [possessive, verb]) || isPartOfLongerWord(source, possessive, verb)) return undefined;
149
+ return slipOf("possessive", source, possessive, verb);
150
+ };
151
+
152
+ /** ones of the most。前に限定詞・形容詞・代名詞があれば、ones は代名詞(the ones of the most use)。 */
153
+ const misnumberedAt = (source: string, tokens: readonly Token[], at: number, lists: AgreementLists): Slip | undefined => {
154
+ const span = tokens.slice(at, at + phraseAt(tokens, at, lists.misnumbered));
155
+ const [first, last, previous] = [span[0], span.at(-1), tokens[at - 1]];
156
+ if (first === undefined || last === undefined || (previous !== undefined && PREDICATE_TAIL.has(previous.pos))) return undefined;
157
+ return joinedBySpace(source, span) ? slipOf("number", source, first, last) : undefined;
158
+ };
159
+
160
+ export const agreementIn = (source: string, tokens: readonly Token[], lists: AgreementLists): Slip[] =>
161
+ tokens.flatMap((_, at) => {
162
+ const slip = numberSlipAt(source, tokens, at, lists) ?? possessiveSlipAt(source, tokens, at, lists) ?? misnumberedAt(source, tokens, at, lists);
163
+ return slip === undefined ? [] : [slip];
164
+ });
165
+
166
+ const setOf = (lexicon: Lexicon | undefined): ReadonlySet<string> => new Set((lexicon ?? []).map((entry) => entry.pattern.toLowerCase()));
167
+
168
+ const listsOf = (lexicons: Readonly<Record<string, Lexicon>>): AgreementLists => ({
169
+ singular: lexicons["singular-determiner"] ?? [],
170
+ plural: lexicons["plural-determiner"] ?? [],
171
+ invariant: setOf(lexicons["invariant-noun"]),
172
+ count: setOf(lexicons["count-adjective"]),
173
+ possessive: setOf(lexicons["dependent-possessive"]),
174
+ finite: setOf(lexicons["finite-auxiliary"]),
175
+ misnumbered: lexicons["misnumbered-phrase"] ?? [],
176
+ });
177
+
178
+ export const agreementSlip: Detector = (doc): Finding[] => {
179
+ const lists = listsOf(doc.lexicons);
180
+ return doc.sentences.flatMap((sentence) =>
181
+ agreementIn(doc.source, sentence.tokens ?? [], lists).map((slip): Finding => ({
182
+ rule: "agreement-slip",
183
+ severity: "warning",
184
+ line: 0,
185
+ column: 0,
186
+ quote: sentence.text.trim(),
187
+ values: { word: slip.word, offset: slip.first.span.start },
188
+ variant: slip.variant,
189
+ })),
190
+ );
191
+ };
@@ -6,6 +6,9 @@ import { isAddressRun } from "./place-run.ts";
6
6
  import { isOneName } from "./name-run.ts";
7
7
  import { proseText } from "../measure.ts";
8
8
  import { parallelDotCount } from "./middle-dot.ts";
9
+ import { maskSpans } from "../mask.ts";
10
+ import { quotedSpans } from "../quoted-span.ts";
11
+ import { nameSpans } from "../team-names.ts";
9
12
 
10
13
  /**
11
14
  * 文字の並びを見る検出。漢字の連なりが住所か名前かだけは、形態素解析の固有名詞と数で決める。
@@ -13,8 +16,8 @@ import { parallelDotCount } from "./middle-dot.ts";
13
16
  */
14
17
  const KANJI_RUN = /[一-鿿]+/gu;
15
18
 
16
- /** 漢字の連なりと、その文書全体の座標での範囲。 */
17
- type KanjiRun = { readonly text: string; readonly span: Span | undefined };
19
+ /** 漢字の連なりと、その文書全体の座標での範囲。quoted は鉤括弧の中身がまるごとこの連なりであること。 */
20
+ type KanjiRun = { readonly text: string; readonly span: Span | undefined; readonly quoted: boolean };
18
21
 
19
22
  const SPACE = /\s/gu;
20
23
 
@@ -22,14 +25,22 @@ const SPACE = /\s/gu;
22
25
  * 同じ連なりが文に二度出ても、それぞれの位置で読む。proseText は空白を詰めたり除いたりするだけなので、
23
26
  * 空白でない k 文字目は元の文でも空白でない k 文字目。番地の覆いは長さを保つので、proseText の位置がそのまま使える。
24
27
  */
25
- const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined): KanjiRun[] => {
28
+ /** 番地と、チームが並べた名前を空白で覆う。名前は読み手が 1 語と知っているので、前後は別の連なりになる。 */
29
+ const maskedProse = (prose: string, profile: DocumentProfile | undefined, names: readonly string[]): string =>
30
+ maskSpans(maskAddresses(prose, profile), nameSpans(prose, names));
31
+
32
+ /** 鉤括弧の中身が、ちょうどこの範囲(文の先頭からの位置)か。括弧でくくった漢字だけの語は、書き手が 1 つの名前として示したもの。 */
33
+ const fillsQuote = (sentence: Sentence, start: number, end: number): boolean =>
34
+ quotedSpans(sentence.text).some((span) => span.start === start && span.end === end);
35
+
36
+ const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined, names: readonly string[]): KanjiRun[] => {
26
37
  const prose = proseText(sentence);
27
38
  const printed = compacted(sentence.text, "char").offsets;
28
- return [...maskAddresses(prose, profile).matchAll(KANJI_RUN)].map((match) => {
39
+ return [...maskedProse(prose, profile, names).matchAll(KANJI_RUN)].map((match) => {
29
40
  const before = prose.slice(0, match.index).replace(SPACE, "").length;
30
41
  const [start, last] = [printed[before], printed[before + match[0].length - 1]];
31
- const span = start === undefined || last === undefined ? undefined : { start: sentence.span.start + start, end: sentence.span.start + last + 1 };
32
- return { text: match[0], span };
42
+ if (start === undefined || last === undefined) return { text: match[0], span: undefined, quoted: false };
43
+ return { text: match[0], span: { start: sentence.span.start + start, end: sentence.span.start + last + 1 }, quoted: fillsQuote(sentence, start, last + 1) };
33
44
  });
34
45
  };
35
46
 
@@ -44,14 +55,15 @@ const isPlaceName = (tokens: readonly Token[], span: Span, topUnits: ReadonlySet
44
55
  * 住所か名前かは、助数詞を外した残りで決める(1日日本銀行の「日本銀行」)。品詞が無ければ判定しない。
45
56
  */
46
57
  const measuredRun = (tokens: readonly Token[] | undefined, run: KanjiRun, topUnits: ReadonlySet<string>): string => {
58
+ if (run.quoted) return "";
47
59
  if (tokens === undefined || run.span === undefined) return run.text;
48
60
  const counter = leadingCounter(tokens, run.span.start, run.text);
49
61
  const rest = counter === undefined ? run.span : { start: counter.span.end, end: run.span.end };
50
62
  return isPlaceName(tokens, rest, topUnits) ? "" : run.text.slice(counter?.surface.length ?? 0);
51
63
  };
52
64
 
53
- const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string>): string =>
54
- runsOf(sentence, profile)
65
+ const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string>, names: readonly string[]): string =>
66
+ runsOf(sentence, profile, names)
55
67
  .map((run) => measuredRun(sentence.tokens, run, topUnits))
56
68
  .reduce((longest, run) => (run.length > longest.length ? run : longest), "");
57
69
 
@@ -62,7 +74,7 @@ const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined,
62
74
  export const kanjiRun: Detector = (doc, options): Finding[] => {
63
75
  const topUnits = new Set((doc.lexicons["prefecture-unit"] ?? []).map((entry) => entry.pattern));
64
76
  return doc.sentences
65
- .map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits) }))
77
+ .map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits, doc.names ?? []) }))
66
78
  .filter(({ run }) => run.length > options.limit)
67
79
  .map(({ sentence, run }) => ({
68
80
  rule: "max-kanji-continuous",