chaffjs 0.15.0 → 0.16.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 (427) 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 +26 -0
  19. package/dist/calendar-number.d.ts.map +1 -0
  20. package/dist/calendar-number.js +51 -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/commands/cite.d.ts.map +1 -1
  34. package/dist/commands/cite.js +4 -4
  35. package/dist/commands/cite.js.map +1 -1
  36. package/dist/commands/eval.js +1 -1
  37. package/dist/commands/eval.js.map +1 -1
  38. package/dist/commands/init-ask.d.ts +19 -0
  39. package/dist/commands/init-ask.d.ts.map +1 -0
  40. package/dist/commands/init-ask.js +43 -0
  41. package/dist/commands/init-ask.js.map +1 -0
  42. package/dist/commands/test.d.ts +8 -0
  43. package/dist/commands/test.d.ts.map +1 -1
  44. package/dist/commands/test.js +70 -38
  45. package/dist/commands/test.js.map +1 -1
  46. package/dist/commands/tree.d.ts +1 -8
  47. package/dist/commands/tree.d.ts.map +1 -1
  48. package/dist/commands/tree.js +7 -6
  49. package/dist/commands/tree.js.map +1 -1
  50. package/dist/config/by-path.d.ts.map +1 -1
  51. package/dist/config/by-path.js +10 -9
  52. package/dist/config/by-path.js.map +1 -1
  53. package/dist/config/load.d.ts +4 -0
  54. package/dist/config/load.d.ts.map +1 -1
  55. package/dist/config/load.js +19 -0
  56. package/dist/config/load.js.map +1 -1
  57. package/dist/config/name-problems.d.ts +5 -0
  58. package/dist/config/name-problems.d.ts.map +1 -0
  59. package/dist/config/name-problems.js +11 -0
  60. package/dist/config/name-problems.js.map +1 -0
  61. package/dist/date-stamp.js +1 -1
  62. package/dist/date-stamp.js.map +1 -1
  63. package/dist/detectors/char-shape.d.ts.map +1 -1
  64. package/dist/detectors/char-shape.js +16 -6
  65. package/dist/detectors/char-shape.js.map +1 -1
  66. package/dist/detectors/en-shape.d.ts.map +1 -1
  67. package/dist/detectors/en-shape.js +101 -7
  68. package/dist/detectors/en-shape.js.map +1 -1
  69. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  70. package/dist/detectors/enumerated-runs.js +4 -2
  71. package/dist/detectors/enumerated-runs.js.map +1 -1
  72. package/dist/detectors/gram-place.js +1 -1
  73. package/dist/detectors/gram-place.js.map +1 -1
  74. package/dist/detectors/heading-case.d.ts.map +1 -1
  75. package/dist/detectors/heading-case.js +3 -1
  76. package/dist/detectors/heading-case.js.map +1 -1
  77. package/dist/detectors/index.d.ts.map +1 -1
  78. package/dist/detectors/index.js +4 -3
  79. package/dist/detectors/index.js.map +1 -1
  80. package/dist/detectors/lettered-index.d.ts +9 -0
  81. package/dist/detectors/lettered-index.d.ts.map +1 -0
  82. package/dist/detectors/lettered-index.js +54 -0
  83. package/dist/detectors/lettered-index.js.map +1 -0
  84. package/dist/detectors/lexicon.d.ts +5 -1
  85. package/dist/detectors/lexicon.d.ts.map +1 -1
  86. package/dist/detectors/lexicon.js +46 -3
  87. package/dist/detectors/lexicon.js.map +1 -1
  88. package/dist/detectors/orthography.d.ts.map +1 -1
  89. package/dist/detectors/orthography.js +24 -6
  90. package/dist/detectors/orthography.js.map +1 -1
  91. package/dist/detectors/phrase-match.d.ts +3 -1
  92. package/dist/detectors/phrase-match.d.ts.map +1 -1
  93. package/dist/detectors/phrase-match.js +20 -15
  94. package/dist/detectors/phrase-match.js.map +1 -1
  95. package/dist/detectors/repeated-head.d.ts.map +1 -1
  96. package/dist/detectors/repeated-head.js +5 -4
  97. package/dist/detectors/repeated-head.js.map +1 -1
  98. package/dist/detectors/signals.d.ts.map +1 -1
  99. package/dist/detectors/signals.js +16 -6
  100. package/dist/detectors/signals.js.map +1 -1
  101. package/dist/detectors/stacked-hedge.d.ts +23 -0
  102. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  103. package/dist/detectors/stacked-hedge.js +64 -0
  104. package/dist/detectors/stacked-hedge.js.map +1 -0
  105. package/dist/detectors/superlative-amount.d.ts +9 -0
  106. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  107. package/dist/detectors/superlative-amount.js +20 -0
  108. package/dist/detectors/superlative-amount.js.map +1 -0
  109. package/dist/detectors/superlative-clause.d.ts +18 -0
  110. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  111. package/dist/detectors/superlative-clause.js +61 -0
  112. package/dist/detectors/superlative-clause.js.map +1 -0
  113. package/dist/detectors/superlative-scope.d.ts +6 -0
  114. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  115. package/dist/detectors/superlative-scope.js +3 -3
  116. package/dist/detectors/superlative-scope.js.map +1 -1
  117. package/dist/detectors/team.d.ts +0 -6
  118. package/dist/detectors/team.d.ts.map +1 -1
  119. package/dist/detectors/team.js +17 -1
  120. package/dist/detectors/team.js.map +1 -1
  121. package/dist/detectors/token-shape.d.ts.map +1 -1
  122. package/dist/detectors/token-shape.js +8 -6
  123. package/dist/detectors/token-shape.js.map +1 -1
  124. package/dist/document.d.ts +3 -0
  125. package/dist/document.d.ts.map +1 -1
  126. package/dist/document.js +18 -3
  127. package/dist/document.js.map +1 -1
  128. package/dist/eval.js +1 -1
  129. package/dist/eval.js.map +1 -1
  130. package/dist/feedback/draft.js +1 -1
  131. package/dist/feedback/draft.js.map +1 -1
  132. package/dist/feedback/excerpt.d.ts +0 -1
  133. package/dist/feedback/excerpt.d.ts.map +1 -1
  134. package/dist/feedback/excerpt.js +2 -2
  135. package/dist/feedback/excerpt.js.map +1 -1
  136. package/dist/file-header.d.ts +20 -0
  137. package/dist/file-header.d.ts.map +1 -0
  138. package/dist/file-header.js +21 -0
  139. package/dist/file-header.js.map +1 -0
  140. package/dist/files.js +3 -3
  141. package/dist/files.js.map +1 -1
  142. package/dist/genre-check.js +1 -1
  143. package/dist/genre-check.js.map +1 -1
  144. package/dist/genre-load.d.ts +6 -0
  145. package/dist/genre-load.d.ts.map +1 -0
  146. package/dist/genre-load.js +22 -0
  147. package/dist/genre-load.js.map +1 -0
  148. package/dist/genre-parse.d.ts +40 -0
  149. package/dist/genre-parse.d.ts.map +1 -0
  150. package/dist/genre-parse.js +104 -0
  151. package/dist/genre-parse.js.map +1 -0
  152. package/dist/genre-suggest.d.ts +15 -0
  153. package/dist/genre-suggest.d.ts.map +1 -0
  154. package/dist/genre-suggest.js +26 -0
  155. package/dist/genre-suggest.js.map +1 -0
  156. package/dist/genre.d.ts +1 -1
  157. package/dist/genre.d.ts.map +1 -1
  158. package/dist/genre.js +3 -13
  159. package/dist/genre.js.map +1 -1
  160. package/dist/init-choice.d.ts +12 -0
  161. package/dist/init-choice.d.ts.map +1 -0
  162. package/dist/init-choice.js +41 -0
  163. package/dist/init-choice.js.map +1 -0
  164. package/dist/init.d.ts.map +1 -1
  165. package/dist/init.js +16 -5
  166. package/dist/init.js.map +1 -1
  167. package/dist/judge.d.ts +2 -1
  168. package/dist/judge.d.ts.map +1 -1
  169. package/dist/judge.js +1 -1
  170. package/dist/judge.js.map +1 -1
  171. package/dist/line-paragraphs.d.ts +4 -0
  172. package/dist/line-paragraphs.d.ts.map +1 -0
  173. package/dist/line-paragraphs.js +69 -0
  174. package/dist/line-paragraphs.js.map +1 -0
  175. package/dist/mask.d.ts.map +1 -1
  176. package/dist/mask.js +7 -6
  177. package/dist/mask.js.map +1 -1
  178. package/dist/plugin.d.ts +6 -0
  179. package/dist/plugin.d.ts.map +1 -1
  180. package/dist/profile/check.d.ts +28 -0
  181. package/dist/profile/check.d.ts.map +1 -0
  182. package/dist/profile/check.js +12 -0
  183. package/dist/profile/check.js.map +1 -0
  184. package/dist/profile/for-file.d.ts +2 -2
  185. package/dist/profile/for-file.d.ts.map +1 -1
  186. package/dist/profile/for-file.js +9 -2
  187. package/dist/profile/for-file.js.map +1 -1
  188. package/dist/profile/load.js +1 -1
  189. package/dist/profile/load.js.map +1 -1
  190. package/dist/profile/select.d.ts +4 -2
  191. package/dist/profile/select.d.ts.map +1 -1
  192. package/dist/profile/select.js +8 -3
  193. package/dist/profile/select.js.map +1 -1
  194. package/dist/quoted-span.d.ts +15 -3
  195. package/dist/quoted-span.d.ts.map +1 -1
  196. package/dist/quoted-span.js +16 -11
  197. package/dist/quoted-span.js.map +1 -1
  198. package/dist/reasons.d.ts +1 -0
  199. package/dist/reasons.d.ts.map +1 -1
  200. package/dist/reasons.js +4 -1
  201. package/dist/reasons.js.map +1 -1
  202. package/dist/render/compact.d.ts.map +1 -1
  203. package/dist/render/compact.js +1 -2
  204. package/dist/render/compact.js.map +1 -1
  205. package/dist/render/eval.d.ts.map +1 -1
  206. package/dist/render/eval.js +4 -3
  207. package/dist/render/eval.js.map +1 -1
  208. package/dist/render/friendly.d.ts +2 -1
  209. package/dist/render/friendly.d.ts.map +1 -1
  210. package/dist/render/friendly.js +14 -6
  211. package/dist/render/friendly.js.map +1 -1
  212. package/dist/render/genres.d.ts +5 -0
  213. package/dist/render/genres.d.ts.map +1 -0
  214. package/dist/render/genres.js +28 -0
  215. package/dist/render/genres.js.map +1 -0
  216. package/dist/render/plural.d.ts +3 -0
  217. package/dist/render/plural.d.ts.map +1 -0
  218. package/dist/render/plural.js +5 -0
  219. package/dist/render/plural.js.map +1 -0
  220. package/dist/render/rules-json.d.ts.map +1 -1
  221. package/dist/render/rules-json.js +9 -3
  222. package/dist/render/rules-json.js.map +1 -1
  223. package/dist/render/semantic.d.ts +3 -2
  224. package/dist/render/semantic.d.ts.map +1 -1
  225. package/dist/render/semantic.js +94 -34
  226. package/dist/render/semantic.js.map +1 -1
  227. package/dist/render/summary.d.ts.map +1 -1
  228. package/dist/render/summary.js +4 -3
  229. package/dist/render/summary.js.map +1 -1
  230. package/dist/render/suppressions.d.ts.map +1 -1
  231. package/dist/render/suppressions.js +3 -2
  232. package/dist/render/suppressions.js.map +1 -1
  233. package/dist/render/text.d.ts +2 -0
  234. package/dist/render/text.d.ts.map +1 -1
  235. package/dist/render/text.js +11 -3
  236. package/dist/render/text.js.map +1 -1
  237. package/dist/rule-load.d.ts.map +1 -1
  238. package/dist/rule-load.js +4 -1
  239. package/dist/rule-load.js.map +1 -1
  240. package/dist/run-semantic.js +1 -1
  241. package/dist/run-semantic.js.map +1 -1
  242. package/dist/run.d.ts +2 -0
  243. package/dist/run.d.ts.map +1 -1
  244. package/dist/run.js +25 -15
  245. package/dist/run.js.map +1 -1
  246. package/dist/sentence-shape.d.ts +0 -1
  247. package/dist/sentence-shape.d.ts.map +1 -1
  248. package/dist/sentence-shape.js +3 -5
  249. package/dist/sentence-shape.js.map +1 -1
  250. package/dist/setting-problems.d.ts +5 -0
  251. package/dist/setting-problems.d.ts.map +1 -0
  252. package/dist/setting-problems.js +15 -0
  253. package/dist/setting-problems.js.map +1 -0
  254. package/dist/structure/build.js +5 -5
  255. package/dist/structure/build.js.map +1 -1
  256. package/dist/structure/cite.js +1 -1
  257. package/dist/structure/cite.js.map +1 -1
  258. package/dist/structure/issues.d.ts.map +1 -1
  259. package/dist/structure/issues.js +5 -5
  260. package/dist/structure/issues.js.map +1 -1
  261. package/dist/structure/markdown-path.d.ts.map +1 -1
  262. package/dist/structure/markdown-path.js +2 -2
  263. package/dist/structure/markdown-path.js.map +1 -1
  264. package/dist/structure/numbering.d.ts.map +1 -1
  265. package/dist/structure/numbering.js +1 -1
  266. package/dist/structure/numbering.js.map +1 -1
  267. package/dist/structure/relative-find.js +1 -1
  268. package/dist/structure/relative-find.js.map +1 -1
  269. package/dist/structure/sexp.js +1 -1
  270. package/dist/structure/sexp.js.map +1 -1
  271. package/dist/structure/unnumbered.js +2 -2
  272. package/dist/structure/unnumbered.js.map +1 -1
  273. package/dist/team-names.d.ts +9 -0
  274. package/dist/team-names.d.ts.map +1 -0
  275. package/dist/team-names.js +34 -0
  276. package/dist/team-names.js.map +1 -0
  277. package/dist/ui.d.ts +2 -0
  278. package/dist/ui.d.ts.map +1 -1
  279. package/dist/ui.js +6 -0
  280. package/dist/ui.js.map +1 -1
  281. package/dist/version.js +1 -1
  282. package/dist/version.js.map +1 -1
  283. package/dist/watch.d.ts +2 -2
  284. package/dist/watch.d.ts.map +1 -1
  285. package/dist/watch.js +5 -5
  286. package/dist/watch.js.map +1 -1
  287. package/genres.yaml +363 -0
  288. package/package.json +4 -3
  289. package/rules/adverb-overuse.yaml +2 -2
  290. package/rules/agentless-passive.yaml +1 -1
  291. package/rules/ai-generated-composite.yaml +2 -2
  292. package/rules/ai-tell.yaml +2 -2
  293. package/rules/bold-density.yaml +2 -2
  294. package/rules/closing-cliche.yaml +1 -1
  295. package/rules/concrete-evidence-density.yaml +2 -2
  296. package/rules/contraction-consistency.yaml +1 -1
  297. package/rules/cushion-phrase-density.yaml +1 -1
  298. package/rules/dangling-reference.yaml +1 -1
  299. package/rules/date-order.yaml +1 -1
  300. package/rules/date-weekday-mismatch.yaml +1 -1
  301. package/rules/double-keigo.yaml +1 -1
  302. package/rules/doubled-word.yaml +1 -1
  303. package/rules/duplicate-definition.yaml +1 -1
  304. package/rules/emoji-density.yaml +1 -1
  305. package/rules/empty-conclusion.yaml +2 -1
  306. package/rules/empty-intensifier.yaml +1 -1
  307. package/rules/excessive-hedging.yaml +16 -6
  308. package/rules/expletive-construction.yaml +2 -2
  309. package/rules/heading-echo.yaml +1 -1
  310. package/rules/hiragana-fukushi.yaml +1 -1
  311. package/rules/internal-jargon.yaml +2 -2
  312. package/rules/latin-spacing.yaml +5 -4
  313. package/rules/max-kanji-continuous.yaml +4 -3
  314. package/rules/max-paragraph-length.yaml +9 -2
  315. package/rules/max-sentence-length.yaml +14 -2
  316. package/rules/ngram-repetition.yaml +4 -4
  317. package/rules/no-doubled-joshi.yaml +2 -2
  318. package/rules/no-em-dash.yaml +2 -2
  319. package/rules/no-mixed-desumasu.yaml +1 -1
  320. package/rules/no-nakaguro-parallel.yaml +2 -2
  321. package/rules/numbering-gap.yaml +1 -1
  322. package/rules/oxford-comma-consistency.yaml +2 -1
  323. package/rules/padded-intro.yaml +2 -1
  324. package/rules/paragraph-length-variance.yaml +1 -1
  325. package/rules/preamble-length.yaml +2 -2
  326. package/rules/preferred-term.yaml +1 -1
  327. package/rules/proper-noun-density.yaml +3 -2
  328. package/rules/repeated-conjunction.yaml +2 -2
  329. package/rules/repeated-sentence-head.yaml +2 -2
  330. package/rules/required-sections.yaml +1 -1
  331. package/rules/rule-of-three.yaml +2 -2
  332. package/rules/sasete-itadaku.yaml +3 -3
  333. package/rules/section-length-uniformity.yaml +1 -1
  334. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  335. package/rules/sentence-rhythm.yaml +1 -1
  336. package/rules/taigen-dome-in-prose.yaml +2 -2
  337. package/rules/title-case-consistency.yaml +1 -1
  338. package/rules/total-mismatch.yaml +1 -1
  339. package/rules/undefined-acronym.yaml +4 -4
  340. package/rules/unqualified-superlative.yaml +2 -2
  341. package/skills/chaff/SKILL.md +28 -9
  342. package/src/address-chain.ts +1 -1
  343. package/src/backends/anthropic.ts +5 -1
  344. package/src/backends/openai.ts +2 -1
  345. package/src/backends/types.ts +3 -2
  346. package/src/baseline.ts +1 -1
  347. package/src/calendar-number.ts +75 -0
  348. package/src/cli-args.ts +2 -2
  349. package/src/cli-text.ts +29 -15
  350. package/src/cli.ts +42 -41
  351. package/src/commands/cite.ts +4 -4
  352. package/src/commands/eval.ts +1 -1
  353. package/src/commands/init-ask.ts +53 -0
  354. package/src/commands/test.ts +102 -45
  355. package/src/commands/tree.ts +7 -6
  356. package/src/config/by-path.ts +8 -9
  357. package/src/config/load.ts +24 -0
  358. package/src/config/name-problems.ts +15 -0
  359. package/src/date-stamp.ts +1 -1
  360. package/src/detectors/char-shape.ts +21 -9
  361. package/src/detectors/en-shape.ts +106 -8
  362. package/src/detectors/enumerated-runs.ts +3 -2
  363. package/src/detectors/gram-place.ts +1 -1
  364. package/src/detectors/heading-case.ts +4 -1
  365. package/src/detectors/index.ts +4 -3
  366. package/src/detectors/lettered-index.ts +78 -0
  367. package/src/detectors/lexicon.ts +63 -6
  368. package/src/detectors/orthography.ts +31 -7
  369. package/src/detectors/phrase-match.ts +23 -12
  370. package/src/detectors/repeated-head.ts +3 -4
  371. package/src/detectors/signals.ts +15 -6
  372. package/src/detectors/stacked-hedge.ts +93 -0
  373. package/src/detectors/superlative-amount.ts +24 -0
  374. package/src/detectors/superlative-clause.ts +82 -0
  375. package/src/detectors/superlative-scope.ts +3 -3
  376. package/src/detectors/team.ts +18 -2
  377. package/src/detectors/token-shape.ts +8 -10
  378. package/src/document.ts +29 -3
  379. package/src/eval.ts +1 -1
  380. package/src/feedback/draft.ts +1 -1
  381. package/src/feedback/excerpt.ts +2 -2
  382. package/src/file-header.ts +31 -0
  383. package/src/files.ts +3 -3
  384. package/src/genre-check.ts +1 -1
  385. package/src/genre-load.ts +24 -0
  386. package/src/genre-parse.ts +126 -0
  387. package/src/genre-suggest.ts +37 -0
  388. package/src/genre.ts +4 -13
  389. package/src/init-choice.ts +47 -0
  390. package/src/init.ts +20 -6
  391. package/src/judge.ts +3 -1
  392. package/src/line-paragraphs.ts +83 -0
  393. package/src/mask.ts +5 -6
  394. package/src/plugin.ts +6 -0
  395. package/src/profile/check.ts +34 -0
  396. package/src/profile/for-file.ts +10 -3
  397. package/src/profile/load.ts +1 -1
  398. package/src/profile/select.ts +11 -4
  399. package/src/quoted-span.ts +26 -12
  400. package/src/reasons.ts +5 -1
  401. package/src/render/compact.ts +1 -3
  402. package/src/render/eval.ts +4 -3
  403. package/src/render/friendly.ts +21 -6
  404. package/src/render/genres.ts +31 -0
  405. package/src/render/plural.ts +6 -0
  406. package/src/render/rules-json.ts +10 -3
  407. package/src/render/semantic.ts +117 -34
  408. package/src/render/summary.ts +4 -3
  409. package/src/render/suppressions.ts +3 -2
  410. package/src/render/text.ts +18 -3
  411. package/src/rule-load.ts +6 -1
  412. package/src/run-semantic.ts +1 -1
  413. package/src/run.ts +28 -15
  414. package/src/sentence-shape.ts +3 -7
  415. package/src/setting-problems.ts +24 -0
  416. package/src/structure/build.ts +5 -5
  417. package/src/structure/cite.ts +1 -1
  418. package/src/structure/issues.ts +6 -5
  419. package/src/structure/markdown-path.ts +2 -2
  420. package/src/structure/numbering.ts +1 -1
  421. package/src/structure/relative-find.ts +1 -1
  422. package/src/structure/sexp.ts +1 -1
  423. package/src/structure/unnumbered.ts +2 -2
  424. package/src/team-names.ts +39 -0
  425. package/src/ui.ts +7 -0
  426. package/src/version.ts +1 -1
  427. package/src/watch.ts +5 -5
@@ -61,8 +61,9 @@ export const enumeratedRuns = (paragraphs: readonly Span[], starts: readonly num
61
61
  const enumerated = paragraphs.filter((paragraph) => opensWithItem(paragraph, starts, source));
62
62
  const runs = enumerated.reduce<Span[]>((joined, paragraph) => {
63
63
  const last = joined.at(-1);
64
- if (last === undefined || !isBlank(source, last.end, paragraph.start)) return [...joined, paragraph];
65
- return [...joined.slice(0, -1), { start: last.start, end: paragraph.end }];
64
+ if (last === undefined || !isBlank(source, last.end, paragraph.start)) joined.push(paragraph);
65
+ else joined[joined.length - 1] = { start: last.start, end: paragraph.end };
66
+ return joined;
66
67
  }, []);
67
68
  return runs.filter((run) => itemsIn(run, starts, source) >= MIN_ITEMS);
68
69
  };
@@ -32,7 +32,7 @@ export const compacted = (text: string, unit: LengthUnit): Compacted => {
32
32
  }
33
33
  chars.push(char);
34
34
  // 位置は UTF-16 の単位で持つ。placeOf は indexOf の位置で引くので、絵文字(2 単位)には 2 つ置く。
35
- Array.from({ length: char.length }, (_, unit) => offsets.push(at + unit));
35
+ Array.from({ length: char.length }, (_, codeUnit) => offsets.push(at + codeUnit));
36
36
  });
37
37
  return { text: chars.join(""), offsets };
38
38
  };
@@ -3,11 +3,14 @@ const WORD = /[A-Za-z][A-Za-z'-]*/gu;
3
3
  /** 小さい語は Title Case でも小文字のままなので、大文字化の判定から外す。 */
4
4
  const MINOR = new Set(["a", "an", "the", "and", "or", "but", "of", "in", "on", "at", "to", "for", "with", "as", "by", "from", "is"]);
5
5
 
6
+ /** 大文字だけの語(PR・FCPs)は略語で、どちらの流儀でも大文字のまま。Title Case の証拠にならない。 */
7
+ const ACRONYM = /^[A-Z]{2,}s?$/u;
8
+
6
9
  const TITLE_DEPTH = 1;
7
10
 
8
11
  /** 見出しが Title Case か。判定できなければ undefined。 */
9
12
  export const isTitleCase = (heading: string): boolean | undefined => {
10
- const words = [...heading.matchAll(WORD)].map((match) => match[0]).filter((word) => !MINOR.has(word.toLowerCase()));
13
+ const words = [...heading.matchAll(WORD)].map((match) => match[0]).filter((word) => !MINOR.has(word.toLowerCase()) && !ACRONYM.test(word));
11
14
  // 1 語の見出しは、どちらの流儀でも先頭が大文字になる。判定できない。
12
15
  if (words.length < 2) return undefined;
13
16
  const capitalized = words.filter((word) => word[0] === word[0]?.toUpperCase()).length;
@@ -4,7 +4,7 @@ import { sentenceLength } from "./sentence-length.ts";
4
4
  import { headingEcho } from "./heading-echo.ts";
5
5
  import { repeatedHead } from "./repeated-head.ts";
6
6
  import { sentenceRhythm } from "./sentence-rhythm.ts";
7
- import { phraseMatch } from "./phrase-match.ts";
7
+ import { phraseCount, phraseMatch } from "./phrase-match.ts";
8
8
  import { agentlessPassive } from "./agentless-passive.ts";
9
9
  import { doubledWord } from "./doubled-word.ts";
10
10
  import { sentenceEnding } from "./sentence-ending.ts";
@@ -13,7 +13,7 @@ import { kanjiRun, middleDot } from "./char-shape.ts";
13
13
  import { adverbDensity, conjunctionRun, expletive, oxfordComma, titleCaseMix } from "./en-shape.ts";
14
14
  import { paragraphLength, paragraphVariance, preambleLength, ruleOfThree, sectionUniformity } from "./structure.ts";
15
15
  import { concreteEvidence, dashDensity, emojiDensity, ngramRepetition, undefinedAcronym } from "./signals.ts";
16
- import { aiTell, contractionMix, cushionDensity, hedgingDensity, repeatedConjunction, unqualifiedSuperlative } from "./lexicon.ts";
16
+ import { aiTell, contractionMix, cushionDensity, hedging, repeatedConjunction, unqualifiedSuperlative } from "./lexicon.ts";
17
17
  import { internalJargon, properNounDensity, requiredSections } from "./team.ts";
18
18
  import { latinSpacing, preferredTerm } from "./orthography.ts";
19
19
  import { danglingReference, dateOrder, dateWeekdayMismatch, duplicateDefinition, numberingGap, totalMismatch } from "./structure-tree.ts";
@@ -26,6 +26,7 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
26
26
  "repeated-head": repeatedHead,
27
27
  "sentence-rhythm": sentenceRhythm,
28
28
  "phrase-match": phraseMatch,
29
+ "phrase-count": phraseCount,
29
30
  "agentless-passive": agentlessPassive,
30
31
  "sentence-ending": sentenceEnding,
31
32
  "noun-ending": nounEnding,
@@ -48,7 +49,7 @@ export const DETECTORS: Readonly<Record<string, Detector>> = {
48
49
  "ngram-repetition": ngramRepetition,
49
50
  "undefined-acronym": undefinedAcronym,
50
51
  "concrete-evidence": concreteEvidence,
51
- "hedging-density": hedgingDensity,
52
+ hedging: hedging,
52
53
  "cushion-density": cushionDensity,
53
54
  "unqualified-superlative": unqualifiedSuperlative,
54
55
  "repeated-conjunction": repeatedConjunction,
@@ -0,0 +1,78 @@
1
+ import type { Section } from "../plugin.ts";
2
+
3
+ /** 索引の区切りは 1 文字の見出し(A, あ)で、それ自体は本文を持たない。 */
4
+ const SINGLE_LETTER = /^\p{L}$/u;
5
+ const FIRST_LETTER = /\p{L}/u;
6
+
7
+ /** 区切りが 1 つだけなら、たまたま 1 文字の見出しかもしれない。並んで初めて索引と読む。 */
8
+ const MIN_DIVIDERS = 2;
9
+
10
+ /** カタカナとひらがなは Unicode で同じ並びにあり、この差だけ離れている。 */
11
+ const KATAKANA = { first: 0x30a1, last: 0x30f6, toHiragana: 0x60 };
12
+
13
+ type Group = { readonly run: number; readonly letter: string; readonly entries: Section[] };
14
+
15
+ /** 索引で引くときの文字。最初の文字から濁点やアクセントを外し、カタカナはひらがなに、小文字は大文字にする。 */
16
+ export const indexLetterOf = (text: string): string => {
17
+ const letter = FIRST_LETTER.exec(text.normalize("NFD"))?.[0] ?? "";
18
+ const code = letter.codePointAt(0) ?? 0;
19
+ const base = code >= KATAKANA.first && code <= KATAKANA.last ? String.fromCodePoint(code - KATAKANA.toHiragana) : letter;
20
+ return base.toUpperCase();
21
+ };
22
+
23
+ const isDivider = (section: Section): boolean => SINGLE_LETTER.test(section.heading.normalize("NFC")) && section.sentences.length === 0;
24
+
25
+ /**
26
+ * ある深さの区切りごとに、その下の節を集める。区切りと同じ深さか浅い見出しで閉じる。
27
+ * 区切りでない見出しを挟んだ区切りは、別の並び(run)になる。別の親の下の A と B を 1 つの索引に綴じないため。
28
+ */
29
+ const groupsAt = (sections: readonly Section[], depth: number): Group[] => {
30
+ const groups: Group[] = [];
31
+ sections.reduce<{ run: number; open: Group | undefined }>(
32
+ ({ run, open }, section) => {
33
+ if (section.depth === depth && isDivider(section)) {
34
+ const group = { run, letter: indexLetterOf(section.heading), entries: [] };
35
+ groups.push(group);
36
+ return { run, open: group };
37
+ }
38
+ if (open !== undefined && section.depth > depth) {
39
+ open.entries.push(section);
40
+ return { run, open };
41
+ }
42
+ return { run: run + 1, open: undefined };
43
+ },
44
+ { run: 0, open: undefined },
45
+ );
46
+ return groups;
47
+ };
48
+
49
+ const runsOf = (groups: readonly Group[]): Group[][] =>
50
+ [...new Set(groups.map((group) => group.run))].map((run) => groups.filter((group) => group.run === run));
51
+
52
+ const ascending = (groups: readonly Group[]): boolean => groups.every((group, index) => index === 0 || (groups[index - 1]?.letter ?? "") < group.letter);
53
+
54
+ /** 区切りのすぐ下の見出し(見出し語)の過半が、区切りの文字で始まる。語りの節を 1 文字の見出しで束ねただけなら、こうはならない。 */
55
+ const headwordsFollowLetters = (groups: readonly Group[], depth: number): boolean => {
56
+ const matches = groups.flatMap((group) =>
57
+ group.entries.filter((entry) => entry.depth === depth + 1).map((entry) => indexLetterOf(entry.heading) === group.letter),
58
+ );
59
+ return matches.filter((match) => match).length * 2 > matches.length;
60
+ };
61
+
62
+ const isIndex = (groups: readonly Group[], depth: number): boolean =>
63
+ groups.length >= MIN_DIVIDERS && ascending(groups) && headwordsFollowLetters(groups, depth);
64
+
65
+ /**
66
+ * 文字で区切った索引(用語集・A to Z)の項目。区切りの見出しより深い節が、次の同じ深さ以上の見出しまで続く。
67
+ * 項目は語の定義や表記の決まりで、根拠の要る主張ではない。
68
+ */
69
+ export const letteredIndexEntries = (sections: readonly Section[]): ReadonlySet<Section> => {
70
+ const depths = new Set(sections.filter(isDivider).map((section) => section.depth));
71
+ return new Set(
72
+ [...depths].flatMap((depth) =>
73
+ runsOf(groupsAt(sections, depth))
74
+ .filter((run) => isIndex(run, depth))
75
+ .flatMap((run) => run.flatMap((group) => group.entries)),
76
+ ),
77
+ );
78
+ };
@@ -1,9 +1,12 @@
1
1
  import { joinWords } from "./word-list.ts";
2
2
  import { wordsOf } from "./structure.ts";
3
- import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence } from "../plugin.ts";
4
- import { entryIn, entryOpens, entryRanges } from "./lexicon-match.ts";
3
+ import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
4
+ import { entryIn, entryOpens, entryRanges, type TokenRange } from "./lexicon-match.ts";
5
5
  import { scoped, scopeMarkersOf, type ScopeMarkers } from "./superlative-scope.ts";
6
6
  import { namesQuantity } from "./superlative-name.ts";
7
+ import { namesAmount } from "./superlative-amount.ts";
8
+ import { restricted, type Restrictors } from "./superlative-clause.ts";
9
+ import { stackedHedges, type StackedHedge } from "./stacked-hedge.ts";
7
10
 
8
11
  const PER = 1000;
9
12
 
@@ -37,25 +40,79 @@ const densityRule =
37
40
  }));
38
41
  };
39
42
 
40
- export const hedgingDensity = densityRule("excessive-hedging");
43
+ const hedgingDensity = densityRule("excessive-hedging");
41
44
  export const cushionDensity = densityRule("cushion-phrase-density");
42
45
 
46
+ /** 書いたとおりの字。行の折り返しをまたいでいたら空白 1 つに。 */
47
+ const writtenAt = (source: string, span: Span): string => source.slice(span.start, span.end).replace(/\s+/gu, " ").trim();
48
+
49
+ const stackedFinding = (doc: ProseDocument, stacked: StackedHedge): Finding => ({
50
+ rule: "excessive-hedging",
51
+ severity: "warning",
52
+ line: 0,
53
+ column: 0,
54
+ quote: stacked.sentence.text.trim(),
55
+ variant: "stacked",
56
+ values: {
57
+ matched: joinWords(
58
+ stacked.spans.map((span) => writtenAt(doc.source, span)),
59
+ doc.language,
60
+ ),
61
+ count: stacked.spans.length,
62
+ offset: stacked.spans[0]?.start ?? stacked.sentence.span.start,
63
+ },
64
+ });
65
+
66
+ /**
67
+ * 逃げの表現は 2 つの見方で探す。1 つの文に重ねたもの(短い文書でも 1 文で分かる)と、文書全体の密度。
68
+ * 重ねた文は密度の側では出さない。同じ文を同じ rule で 2 度言わない。
69
+ */
70
+ export const hedging: Detector = (doc, options): Finding[] => {
71
+ const stacked = stackedHedges(doc.sentences, {
72
+ hedges: options.lexicon ?? [],
73
+ frames: doc.lexicons["hedge-frame"] ?? [],
74
+ scope: doc.lexicons["hedge-scope"] ?? [],
75
+ });
76
+ const reported = new Set(stacked.map((entry) => entry.sentence.span.start));
77
+ const density = hedgingDensity(doc, options).filter((finding) => !reported.has(Number(finding.values["offset"])));
78
+ return [...stacked.map((entry) => stackedFinding(doc, entry)), ...density];
79
+ };
80
+
43
81
  /** 数字は語ではないので言語を問わない。数があれば測った結果を言っている。品詞の数(NUM)は "the best one" の one まで含むので使わない。 */
44
82
  const DIGIT = /\d/u;
45
83
 
46
- type Qualifiers = { readonly comparison: Lexicon; readonly scope: ScopeMarkers; readonly quantityNouns: Lexicon };
84
+ type Qualifiers = {
85
+ readonly comparison: Lexicon;
86
+ readonly scope: ScopeMarkers;
87
+ readonly quantityNouns: Lexicon;
88
+ readonly amounts: Lexicon;
89
+ readonly restrictors: Restrictors;
90
+ };
47
91
 
48
92
  const qualifiersOf = (doc: ProseDocument): Qualifiers => ({
49
93
  comparison: doc.lexicons["comparison-marker"] ?? [],
50
94
  scope: scopeMarkersOf(doc.lexicons["superlative-scope"] ?? []),
51
95
  quantityNouns: doc.lexicons["quantity-noun"] ?? [],
96
+ amounts: doc.lexicons["superlative-amount"] ?? [],
97
+ restrictors: {
98
+ relatives: doc.lexicons["relative-word"] ?? [],
99
+ bounds: doc.lexicons["superlative-bound"] ?? [],
100
+ subjects: doc.lexicons["subject-pronoun"] ?? [],
101
+ },
52
102
  });
53
103
 
54
- /** どの出現も範囲を持つか量の名前のときだけ。1 つでもそうでない出現があれば、その文には限定の無い最上級がある。 */
104
+ /** 範囲・節・分詞・形容詞のどれかが最上級を限っているか、最上級が量か名前を言っているか。 */
105
+ const qualifiedAt = (tokens: readonly Token[], range: TokenRange, qualifiers: Qualifiers): boolean =>
106
+ scoped(tokens, range, qualifiers.scope) ||
107
+ restricted(tokens, range, qualifiers.restrictors) ||
108
+ namesQuantity(tokens, range, qualifiers.quantityNouns) ||
109
+ namesAmount(tokens, range, qualifiers.amounts);
110
+
111
+ /** どの出現も限られているか量を言うときだけ。1 つでもそうでない出現があれば、その文には限定の無い最上級がある。 */
55
112
  const everyQualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean => {
56
113
  const tokens = sentence.tokens ?? [];
57
114
  const ranges = entryRanges(sentence, entry);
58
- return ranges.length > 0 && ranges.every((range) => scoped(tokens, range, qualifiers.scope) || namesQuantity(tokens, range, qualifiers.quantityNouns));
115
+ return ranges.length > 0 && ranges.every((range) => qualifiedAt(tokens, range, qualifiers));
59
116
  };
60
117
 
61
118
  const qualified = (sentence: Sentence, entry: LexiconEntry, qualifiers: Qualifiers): boolean =>
@@ -1,4 +1,5 @@
1
- import type { Detector, Finding, Sentence, Span } from "../plugin.ts";
1
+ import type { Detector, Finding, ProseDocument, Sentence, Span } from "../plugin.ts";
2
+ import { calendarRuns, type CalendarRun, type CalendarUnits } from "../calendar-number.ts";
2
3
  import { latinBoundaries, minorityStyle, occurrencesOutside, type Boundary, type SpacingKind } from "../orthography.ts";
3
4
  import { isWithinAny, quotedSpans } from "../quoted-span.ts";
4
5
  import { digitRunAround, isNumberName, sequenceLabelStarts } from "../number-name.ts";
@@ -31,13 +32,32 @@ const digitBeside = (boundary: Boundary): number => {
31
32
  return boundary.spaced ? boundary.offset + 1 : boundary.offset;
32
33
  };
33
34
 
34
- /** 番号・識別子として書かれた数の境目は、空け方の好みではないので数えない(number-name.ts)。 */
35
- const isCounted = (sentence: Sentence, boundary: Boundary, sequence: ReadonlySet<number>, topUnits: ReadonlySet<string>): boolean => {
35
+ /** 文書全体で一度だけ読むもの。sequence は続き番号の行の頭、topUnits は都道府県の単位、calendar は日付・時刻の単位。 */
36
+ type NumberContext = { readonly sequence: ReadonlySet<number>; readonly topUnits: ReadonlySet<string>; readonly calendar: CalendarUnits };
37
+
38
+ /** 文の中の日付・時刻の数の、並びの頭の位置と、日付の中の位置。 */
39
+ const calendarPlaces = (sentence: Sentence, units: CalendarUnits): ReadonlyMap<number, CalendarRun["place"]> =>
40
+ new Map(calendarRuns(sentence.text, sentence.tokens, sentence.span.start, units).map(({ run, place }) => [run.start, place]));
41
+
42
+ /**
43
+ * 日付・時刻の中の境目(「9月」の数と単位のあいだ、「2026年9月」の年と 9 のあいだ)は、詰めて書く決まりで好みではない。
44
+ * 日付の前の境目(「は 9月」の空白)は、数の前をどう空けるかという書き手の書き方なので数える。
45
+ */
46
+ const isInsideCalendar = (boundary: Boundary, place: CalendarRun["place"] | undefined): boolean =>
47
+ place !== undefined && (boundary.kind === "after-digit" || place === "inner");
48
+
49
+ /** 番号・識別子として書かれた数(number-name.ts)と日付・時刻の中の境目は、空け方の好みではないので数えない。 */
50
+ const isCounted = (sentence: Sentence, boundary: Boundary, context: NumberContext, calendar: ReadonlyMap<number, CalendarRun["place"]>): boolean => {
36
51
  if (boundary.kind === "letter") return true;
37
52
  const run = digitRunAround(sentence.text, digitBeside(boundary));
38
- return run === undefined || !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start, sequence, topUnits);
53
+ if (run === undefined) return true;
54
+ if (isInsideCalendar(boundary, calendar.get(run.start))) return false;
55
+ return !isNumberName(sentence.text, run, sentence.tokens, sentence.span.start, context.sequence, context.topUnits);
39
56
  };
40
57
 
58
+ const patternList = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
59
+ const patternsOf = (doc: ProseDocument, id: string): ReadonlySet<string> => new Set(patternList(doc, id));
60
+
41
61
  /**
42
62
  * 鉤括弧で引いた題名や発言(「…ガイドライン ver. 1.1」)の中の境目。空け方は引いた元のもので、書き手の書き方ではない。
43
63
  * 括弧は日本語の字でも英数字でもないので、境目はまるごと括弧の中か外にある。
@@ -50,12 +70,16 @@ const isQuoted = (quoted: readonly Span[], boundary: Boundary): boolean => isWit
50
70
  */
51
71
  export const latinSpacing: Detector = (doc, options): Finding[] => {
52
72
  // 覆った文(prose)で探す。コードの中の「1 件」は並びに入れない。
53
- const sequence = sequenceLabelStarts(doc.prose ?? doc.source);
54
- const topUnits = new Set((doc.lexicons["prefecture-unit"] ?? []).map((entry) => entry.pattern));
73
+ const context: NumberContext = {
74
+ sequence: sequenceLabelStarts(doc.prose ?? doc.source),
75
+ topUnits: patternsOf(doc, "prefecture-unit"),
76
+ calendar: { chained: patternList(doc, "date-time-unit"), positional: patternsOf(doc, "calendar-unit"), year: patternsOf(doc, "calendar-year-unit") },
77
+ };
55
78
  const located: Located[] = doc.sentences.flatMap((sentence) => {
56
79
  const quoted = quotedSpans(sentence.text);
80
+ const calendar = calendarPlaces(sentence, context.calendar);
57
81
  return latinBoundaries(sentence.text, doc.source.slice(sentence.span.start, sentence.span.end))
58
- .filter((boundary) => !isQuoted(quoted, boundary) && isCounted(sentence, boundary, sequence, topUnits))
82
+ .filter((boundary) => !isQuoted(quoted, boundary) && isCounted(sentence, boundary, context, calendar))
59
83
  .map((boundary) => ({ sentence, ...boundary }));
60
84
  });
61
85
  return KINDS.flatMap((kind) => {
@@ -1,4 +1,4 @@
1
- import type { Detector, Finding, Lexicon, ProseDocument, Sentence } from "../plugin.ts";
1
+ import type { Detector, DetectorOptions, Finding, Lexicon, ProseDocument, Sentence } from "../plugin.ts";
2
2
  import { entryIn } from "./lexicon-match.ts";
3
3
 
4
4
  /** 冒頭は先頭 2 段落ぶん。「どの記事にも当てはまる書き出し」は冒頭にあるときだけ問題。 */
@@ -22,21 +22,32 @@ const hitsIn = (sentences: readonly Sentence[], lexicon: Lexicon): Hit[] =>
22
22
  return matched === undefined ? [] : [{ sentence, matched: matched.pattern }];
23
23
  });
24
24
 
25
- /**
26
- * 語彙表との照合。detector は共通で、語彙だけが言語別に差し替わる。spec §11。
27
- * これが L2 の全体像で、新しい言語は語彙表を書くだけで動く。
28
- */
29
- export const phraseMatch: Detector = (doc, options): Finding[] => {
30
- const lexicon = options.lexicon ?? [];
31
- if (lexicon.length === 0) return [];
32
- const hits = hitsIn(inScope(doc, options.where), lexicon);
33
- if (hits.length < options.limit) return [];
34
- return hits.map((hit) => ({
25
+ const findingsOf = (hits: readonly Hit[], limit: number): Finding[] =>
26
+ hits.map((hit) => ({
35
27
  rule: "",
36
28
  severity: "warning",
37
29
  line: 0,
38
30
  column: 0,
39
31
  quote: hit.sentence.text.trim(),
40
- values: { matched: hit.matched, count: hits.length, limit: options.limit, offset: hit.sentence.span.start },
32
+ values: { matched: hit.matched, count: hits.length, limit, offset: hit.sentence.span.start },
41
33
  }));
34
+
35
+ const phraseHits = (doc: ProseDocument, options: DetectorOptions): Hit[] => {
36
+ const lexicon = options.lexicon ?? [];
37
+ return lexicon.length === 0 ? [] : hitsIn(inScope(doc, options.where), lexicon);
38
+ };
39
+
40
+ /**
41
+ * 語彙表との照合。detector は共通で、語彙だけが言語別に差し替わる。spec §11。
42
+ * これが L2 の全体像で、新しい言語は語彙表を書くだけで動く。limit は指摘に要る数で、1 なら 1 つ目から言う。
43
+ */
44
+ export const phraseMatch: Detector = (doc, options): Finding[] => {
45
+ const hits = phraseHits(doc, options);
46
+ return hits.length < options.limit ? [] : findingsOf(hits, options.limit);
47
+ };
48
+
49
+ /** 回数に上限がある語。limit は許す回数で、それを超えたときだけ言う。message の「N 回まで」と同じ向き。 */
50
+ export const phraseCount: Detector = (doc, options): Finding[] => {
51
+ const hits = phraseHits(doc, options);
52
+ return hits.length <= options.limit ? [] : findingsOf(hits, options.limit);
42
53
  };
@@ -35,10 +35,9 @@ const runsOf = (sentences: readonly Sentence[], unit: LengthUnit, lists: readonl
35
35
  // 短すぎる書き出しは「同じ」と言えない。単位ぶん揃って初めて連なりと見る。
36
36
  const full = unit === "char" ? head.length === HEAD_CHARS : head.split(" ").length === HEAD_WORDS;
37
37
  const joins = last !== undefined && last.head === head && last.container === container && !listBetween(last.members.at(-1), sentence, lists);
38
- if (last !== undefined && joins && full) {
39
- return [...acc.slice(0, -1), { head, container, members: [...last.members, sentence] }];
40
- }
41
- return [...acc, { head, container, members: [sentence] }];
38
+ if (last !== undefined && joins && full) acc[acc.length - 1] = { head, container, members: [...last.members, sentence] };
39
+ else acc.push({ head, container, members: [sentence] });
40
+ return acc;
42
41
  }, []);
43
42
 
44
43
  export const repeatedHead: Detector = (doc, options): Finding[] =>
@@ -6,7 +6,9 @@ import { expansionAt, type ExpandedAt } from "./acronym-expansion.ts";
6
6
  import { isExplained } from "./acronym-compound.ts";
7
7
  import { conjugatedForms } from "./conjugated-form.ts";
8
8
  import { evidenceSpans, hasNumeral, startsWithin } from "./concrete-evidence.ts";
9
+ import { letteredIndexEntries } from "./lettered-index.ts";
9
10
  import { hasPredicateIn } from "./gram-predicate.ts";
11
+ import { nameSpans, touchesAny } from "../team-names.ts";
10
12
  import type { Detector, Finding, ProseDocument, Section, Sentence } from "../plugin.ts";
11
13
 
12
14
  const PER = 1000;
@@ -62,9 +64,12 @@ export const emojiDensity: Detector = (doc, options): Finding[] => {
62
64
  */
63
65
  const GRAM = { char: 8, word: 20 };
64
66
 
65
- const countGrams = (text: string, width: number): Map<string, number> => {
67
+ /** 並べた名前にかかる語句は数えない。名前が繰り返されれば、その前後(「the …」)も一緒に繰り返されるのは当たり前。 */
68
+ const countGrams = (text: string, width: number, names: readonly string[]): Map<string, number> => {
66
69
  const counts = new Map<string, number>();
70
+ const named = nameSpans(text, names);
67
71
  Array.from({ length: Math.max(0, text.length - width + 1) }).forEach((_, index) => {
72
+ if (touchesAny(named, index, index + width)) return;
68
73
  const gram = text.slice(index, index + width);
69
74
  counts.set(gram, (counts.get(gram) ?? 0) + 1);
70
75
  });
@@ -93,7 +98,8 @@ const gramsOf = (doc: ProseDocument): Map<string, number> => {
93
98
  const counts = new Map<string, number>();
94
99
  doc.sentences.forEach((sentence) => {
95
100
  const text = doc.lengthUnit === "char" ? proseText(sentence).replace(/\s+/gu, "") : proseText(sentence);
96
- countGrams(text, GRAM[doc.lengthUnit]).forEach((count, gram) => counts.set(gram, (counts.get(gram) ?? 0) + count));
101
+ const names = doc.lengthUnit === "char" ? (doc.names ?? []).map((name) => name.replace(/\s+/gu, "")) : (doc.names ?? []);
102
+ countGrams(text, GRAM[doc.lengthUnit], names).forEach((count, gram) => counts.set(gram, (counts.get(gram) ?? 0) + count));
97
103
  });
98
104
  return counts;
99
105
  };
@@ -117,7 +123,7 @@ export const ngramRepetition: Detector = (doc, options): Finding[] => {
117
123
  const sentenceWith = (gram: string): Sentence | undefined => doc.sentences.find((sentence) => gramText(sentence, doc.lengthUnit).includes(gram));
118
124
  const worst = [...gramsOf(doc).entries()]
119
125
  .filter(([gram, count]) => count > options.limit && isPhrasing(gram, doc.lengthUnit))
120
- .sort(([, left], [, right]) => right - left)
126
+ .toSorted(([, left], [, right]) => right - left)
121
127
  .map(([gram, count]) => ({ gram, count, at: sentenceWith(gram) }))
122
128
  .find(({ gram, at }) => at === undefined || hasPredicate(gram, at, doc.lengthUnit));
123
129
  if (worst === undefined) return [];
@@ -194,9 +200,10 @@ const spansOf = (text: string, notation: NotAcronymSpans): Span[] => [
194
200
 
195
201
  type AcronymHit = { readonly word: string; readonly hit: Hit };
196
202
 
203
+ /** チームが並べた名前の中の略語(NTT Docomo の NTT)は、名前の一部であって説明を待つ略語ではない。 */
197
204
  const acronymsOf = (doc: ProseDocument, notation: NotAcronymSpans): AcronymHit[] =>
198
205
  doc.sentences.flatMap((sentence) => {
199
- const excluded = spansOf(sentence.text, notation);
206
+ const excluded = [...spansOf(sentence.text, notation), ...nameSpans(sentence.text, doc.names ?? [])];
200
207
  return [...sentence.text.matchAll(ACRONYM)]
201
208
  .filter((match) => !excluded.some((span) => span.start <= match.index && match.index + match[0].length <= span.end))
202
209
  .map((match) => ({ word: match[0], hit: { sentence, offset: sentence.span.start + match.index } }));
@@ -232,7 +239,8 @@ const definitionVerbsOf = (doc: ProseDocument): string[] => {
232
239
  export const undefinedAcronym: Detector = (doc, options): Finding[] => {
233
240
  const body = bodyOf(doc);
234
241
  // HTTP のメソッド名(GET)は略語ではないので、通じる略語と同じく展開を求めない。
235
- const common = new Set([...(options.lexicon ?? []).map((entry) => entry.pattern), ...patternsOf(doc, "http-method")]);
242
+ // 並べた名前(JAXA)も、繋いだ略語(JAXA-ISAS)の片割れとして説明済みに数える。
243
+ const common = new Set([...(options.lexicon ?? []).map((entry) => entry.pattern), ...patternsOf(doc, "http-method"), ...(doc.names ?? [])]);
236
244
  const seen = new Map<string, Hit>();
237
245
  acronymsOf(doc, notationOf(doc)).forEach(({ word, hit }) => {
238
246
  if (!seen.has(word)) seen.set(word, hit);
@@ -273,7 +281,8 @@ const openingOf = (sentence: Sentence | undefined, unit: ProseDocument["lengthUn
273
281
  const hasNumeralIn = (section: Section): boolean => section.sentences.some((sentence) => hasNumeral(sentence.tokens ?? []));
274
282
 
275
283
  export const concreteEvidence: Detector = (doc, options): Finding[] => {
276
- const sections = doc.sections.filter((section) => section.sentences.length >= 3);
284
+ const entries = letteredIndexEntries(doc.sections);
285
+ const sections = doc.sections.filter((section) => section.sentences.length >= 3 && !entries.has(section));
277
286
  const bare = sections.filter(
278
287
  (section) => !CONCRETE.test(doc.source.slice(section.span.start, section.span.end)) && !hasNumeralIn(section) && !startsWithin(section.span, doc.links),
279
288
  );
@@ -0,0 +1,93 @@
1
+ import type { Lexicon, Sentence, Span, Token } from "../plugin.ts";
2
+ import { entryRanges, type TokenRange } from "./lexicon-match.ts";
3
+ import { isWithinAny, quotedIn, QUOTATION_MARKS } from "../quoted-span.ts";
4
+
5
+ /** 重ねたと言うのに要る数。1 つなら慎重さで、2 つ目からは読み手が何を言い切ったのか分からなくなる。 */
6
+ export const STACKED_AT = 2;
7
+
8
+ /**
9
+ * 語彙表 3 つ。hedges は逃げの表現そのもの、frames はそれだけでは逃げていない包む言い方(「という状況です」、may)、
10
+ * scope は hedges のうち、どれだけ確かかではなくどこで成り立つかを言うもの(「場合がある」、in some cases)。
11
+ */
12
+ export type HedgeWords = { readonly hedges: Lexicon; readonly frames: Lexicon; readonly scope: Lexicon };
13
+
14
+ type Device = { readonly range: TokenRange; readonly hedge: boolean };
15
+
16
+ export type StackedHedge = { readonly sentence: Sentence; readonly spans: readonly Span[] };
17
+
18
+ const rangesOf = (sentence: Sentence, lexicon: Lexicon): TokenRange[] => lexicon.flatMap((entry) => entryRanges(sentence, entry));
19
+
20
+ const contains = (outer: TokenRange, inner: TokenRange): boolean => outer.start <= inner.start && inner.end <= outer.end;
21
+
22
+ const sameRange = (left: TokenRange, right: TokenRange): boolean => left.start === right.start && left.end === right.end;
23
+
24
+ /**
25
+ * 同じ語に当たった見出しは 1 つに数える。「could」は「it could be」の一部。
26
+ * 同じ範囲に hedge と frame が両方当たったら hedge として残す(devices は hedge が先に並んでいる)。
27
+ */
28
+ const outermost = (devices: readonly Device[]): Device[] =>
29
+ devices.filter(
30
+ (device, index) =>
31
+ !devices.some((other, at) => at !== index && contains(other.range, device.range) && (!sameRange(other.range, device.range) || at < index)),
32
+ );
33
+
34
+ /** 語の並びの、文書全体の座標での範囲。 */
35
+ const spanOf = (tokens: readonly Token[], range: TokenRange): Span | undefined => {
36
+ const first = tokens[range.start];
37
+ const last = tokens[range.end - 1];
38
+ return first === undefined || last === undefined ? undefined : { start: first.span.start, end: last.span.end };
39
+ };
40
+
41
+ /** 句読点と等位接続詞(and、but、及び)は節を切る。別の節の逃げは、同じ主張を二重にぼかしていない。 */
42
+ const CLAUSE_BREAK = new Set(["PUNCT", "CCONJ"]);
43
+
44
+ /** 語ごとの、文の頭から数えた節の番号。 */
45
+ const clauseOf = (tokens: readonly Token[]): number[] =>
46
+ tokens.reduce<number[]>((clauses, token) => [...clauses, (clauses.at(-1) ?? 0) + (CLAUSE_BREAK.has(token.pos) ? 1 : 0)], []);
47
+
48
+ const PREDICATE = new Set(["VERB", "AUX"]);
49
+
50
+ /** 見せる範囲。活用する語で終わる見出し(と考えられる)は、後ろの助動詞(ます)まで含めて書いたとおりに見せる。 */
51
+ const shownEnd = (tokens: readonly Token[], range: TokenRange): number => {
52
+ const last = tokens[range.end - 1];
53
+ if (last === undefined || !PREDICATE.has(last.pos)) return range.end;
54
+ const stop = tokens.findIndex((token, at) => at >= range.end && token.pos !== "AUX");
55
+ return stop === -1 ? tokens.length : stop;
56
+ };
57
+
58
+ /** 書き手の言葉で、どこで成り立つかを言う語ではない見出し。引用(「」『』“” ")の中は話し手の言葉。 */
59
+ const devicesIn = (sentence: Sentence, words: HedgeWords): Device[] => {
60
+ const tokens = sentence.tokens ?? [];
61
+ const quoted = quotedIn(sentence, QUOTATION_MARKS);
62
+ const scope = rangesOf(sentence, words.scope);
63
+ const hedges = rangesOf(sentence, words.hedges).filter((range) => !scope.some((outer) => contains(outer, range)));
64
+ const devices = [...hedges.map((range) => ({ range, hedge: true })), ...rangesOf(sentence, words.frames).map((range) => ({ range, hedge: false }))];
65
+ return outermost(devices).filter((device) => {
66
+ const span = spanOf(tokens, device.range);
67
+ return span !== undefined && !isWithinAny(quoted, span);
68
+ });
69
+ };
70
+
71
+ /** 1 つの節に 2 つ以上あり、そのうち 1 つ以上が逃げの表現そのもの。包む言い方だけ(「という状況です」)は逃げていない。 */
72
+ const stackedClause = (devices: readonly Device[], clauses: readonly number[]): Device[] => {
73
+ const clauseAt = (device: Device): number => clauses[device.range.start] ?? 0;
74
+ const groups = [...new Set(devices.map(clauseAt))].map((clause) => devices.filter((device) => clauseAt(device) === clause));
75
+ return groups.find((group) => group.length >= STACKED_AT && group.some((device) => device.hedge)) ?? [];
76
+ };
77
+
78
+ /**
79
+ * 1 つの文の 1 つの節に、逃げの表現を重ねているか。business-blog-harness-spec §7.2。見せる範囲を文の中の順で返す。重ねていなければ空。
80
+ * 品詞が無ければ語の位置が分からないので何も言わない。
81
+ */
82
+ export const stackedHedgeIn = (sentence: Sentence, words: HedgeWords): Span[] => {
83
+ const tokens = sentence.tokens ?? [];
84
+ return stackedClause(devicesIn(sentence, words), clauseOf(tokens))
85
+ .toSorted((left, right) => left.range.start - right.range.start)
86
+ .flatMap((device) => spanOf(tokens, { start: device.range.start, end: shownEnd(tokens, device.range) }) ?? []);
87
+ };
88
+
89
+ export const stackedHedges = (sentences: readonly Sentence[], words: HedgeWords): StackedHedge[] =>
90
+ sentences.flatMap((sentence) => {
91
+ const spans = stackedHedgeIn(sentence, words);
92
+ return spans.length === 0 ? [] : [{ sentence, spans }];
93
+ });
@@ -0,0 +1,24 @@
1
+ import type { Lexicon, Token } from "../plugin.ts";
2
+ import type { TokenRange } from "./lexicon-match.ts";
3
+
4
+ /** 名詞の後ろにこれが続けば、その名詞は句の頭ではない。解析器は形容詞を名詞と読むことがある(the most frigid weather・the most cost-effective)。 */
5
+ const JOINED = new Set(["NOUN", "PROPN", "ADJ", "X"]);
6
+
7
+ const listed = (tokens: readonly Token[], range: TokenRange, amounts: Lexicon): boolean => {
8
+ const words = tokens
9
+ .slice(range.start, range.end)
10
+ .map((token) => token.surface.toLowerCase())
11
+ .join(" ");
12
+ return amounts.some((entry) => entry.pattern.toLowerCase() === words);
13
+ };
14
+
15
+ /**
16
+ * 量も言う最上級に、句の頭になる名詞がそのまま続くか(the most work・the most students・for the most part)。
17
+ * それは一番多いものを言っていて、何かが一番良いという主張ではない。形容詞を挟めば(the most powerful tool)主張のまま。
18
+ * どの最上級が量を言うかは言語パッケージの語彙表 superlative-amount が持つ。
19
+ */
20
+ export const namesAmount = (tokens: readonly Token[], range: TokenRange, amounts: Lexicon): boolean => {
21
+ const noun = tokens[range.end];
22
+ const after = tokens[range.end + 1];
23
+ return listed(tokens, range, amounts) && noun?.pos === "NOUN" && (after === undefined || !JOINED.has(after.pos));
24
+ };