chaffjs 0.34.0 → 0.35.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 (390) hide show
  1. package/dist/capitals-passage.d.ts +3 -0
  2. package/dist/capitals-passage.d.ts.map +1 -0
  3. package/dist/capitals-passage.js +26 -0
  4. package/dist/capitals-passage.js.map +1 -0
  5. package/dist/coined-hyphen-name.d.ts +13 -0
  6. package/dist/coined-hyphen-name.d.ts.map +1 -0
  7. package/dist/coined-hyphen-name.js +21 -0
  8. package/dist/coined-hyphen-name.js.map +1 -0
  9. package/dist/company-names.d.ts +6 -1
  10. package/dist/company-names.d.ts.map +1 -1
  11. package/dist/company-names.js +22 -3
  12. package/dist/company-names.js.map +1 -1
  13. package/dist/detectors/acronym-context.d.ts +1 -1
  14. package/dist/detectors/acronym-context.d.ts.map +1 -1
  15. package/dist/detectors/acronym-context.js +4 -4
  16. package/dist/detectors/acronym-context.js.map +1 -1
  17. package/dist/detectors/acronym-definition.d.ts +13 -0
  18. package/dist/detectors/acronym-definition.d.ts.map +1 -0
  19. package/dist/detectors/acronym-definition.js +90 -0
  20. package/dist/detectors/acronym-definition.js.map +1 -0
  21. package/dist/detectors/acronym-lone.d.ts +13 -0
  22. package/dist/detectors/acronym-lone.d.ts.map +1 -0
  23. package/dist/detectors/acronym-lone.js +22 -0
  24. package/dist/detectors/acronym-lone.js.map +1 -0
  25. package/dist/detectors/acronym-name.d.ts +55 -0
  26. package/dist/detectors/acronym-name.d.ts.map +1 -0
  27. package/dist/detectors/acronym-name.js +75 -0
  28. package/dist/detectors/acronym-name.js.map +1 -0
  29. package/dist/detectors/capitalised-term.d.ts +14 -2
  30. package/dist/detectors/capitalised-term.d.ts.map +1 -1
  31. package/dist/detectors/capitalised-term.js +115 -4
  32. package/dist/detectors/capitalised-term.js.map +1 -1
  33. package/dist/detectors/caps-caption.d.ts +11 -0
  34. package/dist/detectors/caps-caption.d.ts.map +1 -0
  35. package/dist/detectors/caps-caption.js +51 -0
  36. package/dist/detectors/caps-caption.js.map +1 -0
  37. package/dist/detectors/char-shape.d.ts.map +1 -1
  38. package/dist/detectors/char-shape.js +47 -18
  39. package/dist/detectors/char-shape.js.map +1 -1
  40. package/dist/detectors/clause-shape.d.ts +1 -1
  41. package/dist/detectors/clause-shape.d.ts.map +1 -1
  42. package/dist/detectors/clause-shape.js +9 -4
  43. package/dist/detectors/clause-shape.js.map +1 -1
  44. package/dist/detectors/concrete-evidence.d.ts +13 -1
  45. package/dist/detectors/concrete-evidence.d.ts.map +1 -1
  46. package/dist/detectors/concrete-evidence.js +59 -0
  47. package/dist/detectors/concrete-evidence.js.map +1 -1
  48. package/dist/detectors/contraction-minority.d.ts +11 -0
  49. package/dist/detectors/contraction-minority.d.ts.map +1 -0
  50. package/dist/detectors/contraction-minority.js +10 -0
  51. package/dist/detectors/contraction-minority.js.map +1 -0
  52. package/dist/detectors/counter-edge.d.ts +5 -0
  53. package/dist/detectors/counter-edge.d.ts.map +1 -1
  54. package/dist/detectors/counter-edge.js +23 -0
  55. package/dist/detectors/counter-edge.js.map +1 -1
  56. package/dist/detectors/date-any-year.d.ts +6 -3
  57. package/dist/detectors/date-any-year.d.ts.map +1 -1
  58. package/dist/detectors/date-any-year.js +47 -10
  59. package/dist/detectors/date-any-year.js.map +1 -1
  60. package/dist/detectors/date-heading-year.d.ts +18 -0
  61. package/dist/detectors/date-heading-year.d.ts.map +1 -0
  62. package/dist/detectors/date-heading-year.js +38 -0
  63. package/dist/detectors/date-heading-year.js.map +1 -0
  64. package/dist/detectors/date-year.d.ts +12 -2
  65. package/dist/detectors/date-year.d.ts.map +1 -1
  66. package/dist/detectors/date-year.js +31 -6
  67. package/dist/detectors/date-year.js.map +1 -1
  68. package/dist/detectors/defined-term-form.d.ts.map +1 -1
  69. package/dist/detectors/defined-term-form.js +26 -5
  70. package/dist/detectors/defined-term-form.js.map +1 -1
  71. package/dist/detectors/due-date.d.ts.map +1 -1
  72. package/dist/detectors/due-date.js +6 -0
  73. package/dist/detectors/due-date.js.map +1 -1
  74. package/dist/detectors/en-shape.d.ts.map +1 -1
  75. package/dist/detectors/en-shape.js +5 -1
  76. package/dist/detectors/en-shape.js.map +1 -1
  77. package/dist/detectors/fact-consistency.d.ts.map +1 -1
  78. package/dist/detectors/fact-consistency.js +3 -2
  79. package/dist/detectors/fact-consistency.js.map +1 -1
  80. package/dist/detectors/gram-passages.d.ts +11 -0
  81. package/dist/detectors/gram-passages.d.ts.map +1 -0
  82. package/dist/detectors/gram-passages.js +26 -0
  83. package/dist/detectors/gram-passages.js.map +1 -0
  84. package/dist/detectors/heading-end-mark.d.ts +8 -3
  85. package/dist/detectors/heading-end-mark.d.ts.map +1 -1
  86. package/dist/detectors/heading-end-mark.js +18 -8
  87. package/dist/detectors/heading-end-mark.js.map +1 -1
  88. package/dist/detectors/heading-names.d.ts +7 -0
  89. package/dist/detectors/heading-names.d.ts.map +1 -0
  90. package/dist/detectors/heading-names.js +24 -0
  91. package/dist/detectors/heading-names.js.map +1 -0
  92. package/dist/detectors/heading-numbering.d.ts.map +1 -1
  93. package/dist/detectors/heading-numbering.js +7 -2
  94. package/dist/detectors/heading-numbering.js.map +1 -1
  95. package/dist/detectors/inch-mark.d.ts +10 -0
  96. package/dist/detectors/inch-mark.d.ts.map +1 -0
  97. package/dist/detectors/inch-mark.js +48 -0
  98. package/dist/detectors/inch-mark.js.map +1 -0
  99. package/dist/detectors/lexicon.d.ts +0 -1
  100. package/dist/detectors/lexicon.d.ts.map +1 -1
  101. package/dist/detectors/lexicon.js +29 -23
  102. package/dist/detectors/lexicon.js.map +1 -1
  103. package/dist/detectors/mark-shape.d.ts +6 -3
  104. package/dist/detectors/mark-shape.d.ts.map +1 -1
  105. package/dist/detectors/mark-shape.js +12 -6
  106. package/dist/detectors/mark-shape.js.map +1 -1
  107. package/dist/detectors/name-variant-apart.d.ts +27 -0
  108. package/dist/detectors/name-variant-apart.d.ts.map +1 -0
  109. package/dist/detectors/name-variant-apart.js +51 -0
  110. package/dist/detectors/name-variant-apart.js.map +1 -0
  111. package/dist/detectors/name-variant.d.ts.map +1 -1
  112. package/dist/detectors/name-variant.js +18 -11
  113. package/dist/detectors/name-variant.js.map +1 -1
  114. package/dist/detectors/numeral-word.d.ts +15 -0
  115. package/dist/detectors/numeral-word.d.ts.map +1 -0
  116. package/dist/detectors/numeral-word.js +26 -0
  117. package/dist/detectors/numeral-word.js.map +1 -0
  118. package/dist/detectors/organisation-run.d.ts +14 -0
  119. package/dist/detectors/organisation-run.d.ts.map +1 -0
  120. package/dist/detectors/organisation-run.js +73 -0
  121. package/dist/detectors/organisation-run.js.map +1 -0
  122. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  123. package/dist/detectors/orthographic-variant.js +13 -5
  124. package/dist/detectors/orthographic-variant.js.map +1 -1
  125. package/dist/detectors/outline-label.d.ts +14 -0
  126. package/dist/detectors/outline-label.d.ts.map +1 -0
  127. package/dist/detectors/outline-label.js +32 -0
  128. package/dist/detectors/outline-label.js.map +1 -0
  129. package/dist/detectors/page-marker.d.ts +6 -0
  130. package/dist/detectors/page-marker.d.ts.map +1 -0
  131. package/dist/detectors/page-marker.js +19 -0
  132. package/dist/detectors/page-marker.js.map +1 -0
  133. package/dist/detectors/parallel-definition.d.ts +16 -0
  134. package/dist/detectors/parallel-definition.d.ts.map +1 -0
  135. package/dist/detectors/parallel-definition.js +51 -0
  136. package/dist/detectors/parallel-definition.js.map +1 -0
  137. package/dist/detectors/reference-text.d.ts.map +1 -1
  138. package/dist/detectors/reference-text.js +1 -0
  139. package/dist/detectors/reference-text.js.map +1 -1
  140. package/dist/detectors/registry/unfilled-placeholder.d.ts.map +1 -1
  141. package/dist/detectors/registry/unfilled-placeholder.js +2 -2
  142. package/dist/detectors/registry/unfilled-placeholder.js.map +1 -1
  143. package/dist/detectors/registry/vague-density.js +1 -1
  144. package/dist/detectors/registry/vague-density.js.map +1 -1
  145. package/dist/detectors/request-owner.d.ts +6 -2
  146. package/dist/detectors/request-owner.d.ts.map +1 -1
  147. package/dist/detectors/request-owner.js +57 -7
  148. package/dist/detectors/request-owner.js.map +1 -1
  149. package/dist/detectors/requirement-smell.d.ts.map +1 -1
  150. package/dist/detectors/requirement-smell.js +10 -2
  151. package/dist/detectors/requirement-smell.js.map +1 -1
  152. package/dist/detectors/sentence-length.d.ts.map +1 -1
  153. package/dist/detectors/sentence-length.js +24 -16
  154. package/dist/detectors/sentence-length.js.map +1 -1
  155. package/dist/detectors/sentence-rhythm.d.ts.map +1 -1
  156. package/dist/detectors/sentence-rhythm.js +6 -3
  157. package/dist/detectors/sentence-rhythm.js.map +1 -1
  158. package/dist/detectors/series-label.d.ts +2 -1
  159. package/dist/detectors/series-label.d.ts.map +1 -1
  160. package/dist/detectors/series-label.js +10 -4
  161. package/dist/detectors/series-label.js.map +1 -1
  162. package/dist/detectors/signals.d.ts +4 -0
  163. package/dist/detectors/signals.d.ts.map +1 -1
  164. package/dist/detectors/signals.js +95 -36
  165. package/dist/detectors/signals.js.map +1 -1
  166. package/dist/detectors/structure-tree.d.ts.map +1 -1
  167. package/dist/detectors/structure-tree.js +8 -7
  168. package/dist/detectors/structure-tree.js.map +1 -1
  169. package/dist/detectors/template-sections.d.ts +14 -0
  170. package/dist/detectors/template-sections.d.ts.map +1 -0
  171. package/dist/detectors/template-sections.js +25 -0
  172. package/dist/detectors/template-sections.js.map +1 -0
  173. package/dist/detectors/titled-run.d.ts +22 -0
  174. package/dist/detectors/titled-run.d.ts.map +1 -0
  175. package/dist/detectors/titled-run.js +34 -0
  176. package/dist/detectors/titled-run.js.map +1 -0
  177. package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
  178. package/dist/detectors/unbalanced-bracket.js +4 -0
  179. package/dist/detectors/unbalanced-bracket.js.map +1 -1
  180. package/dist/detectors/undefined-term.d.ts.map +1 -1
  181. package/dist/detectors/undefined-term.js +8 -0
  182. package/dist/detectors/undefined-term.js.map +1 -1
  183. package/dist/detectors/unfilled-placeholder.d.ts +4 -0
  184. package/dist/detectors/unfilled-placeholder.d.ts.map +1 -0
  185. package/dist/detectors/unfilled-placeholder.js +9 -0
  186. package/dist/detectors/unfilled-placeholder.js.map +1 -0
  187. package/dist/detectors/vague-density.d.ts +5 -0
  188. package/dist/detectors/vague-density.d.ts.map +1 -0
  189. package/dist/detectors/vague-density.js +52 -0
  190. package/dist/detectors/vague-density.js.map +1 -0
  191. package/dist/facts/fact-scope.d.ts +10 -1
  192. package/dist/facts/fact-scope.d.ts.map +1 -1
  193. package/dist/facts/fact-scope.js +34 -3
  194. package/dist/facts/fact-scope.js.map +1 -1
  195. package/dist/facts/table-facts.d.ts +12 -2
  196. package/dist/facts/table-facts.d.ts.map +1 -1
  197. package/dist/facts/table-facts.js +22 -4
  198. package/dist/facts/table-facts.js.map +1 -1
  199. package/dist/grade-api.d.ts +9 -2
  200. package/dist/grade-api.d.ts.map +1 -1
  201. package/dist/grade-api.js +31 -15
  202. package/dist/grade-api.js.map +1 -1
  203. package/dist/heading-numbering.d.ts +5 -2
  204. package/dist/heading-numbering.d.ts.map +1 -1
  205. package/dist/heading-numbering.js +27 -6
  206. package/dist/heading-numbering.js.map +1 -1
  207. package/dist/name-end-letter.d.ts +5 -0
  208. package/dist/name-end-letter.d.ts.map +1 -1
  209. package/dist/name-end-letter.js +8 -0
  210. package/dist/name-end-letter.js.map +1 -1
  211. package/dist/name-given-before.d.ts +15 -0
  212. package/dist/name-given-before.d.ts.map +1 -0
  213. package/dist/name-given-before.js +30 -0
  214. package/dist/name-given-before.js.map +1 -0
  215. package/dist/name-separator.d.ts +2 -2
  216. package/dist/name-separator.d.ts.map +1 -1
  217. package/dist/name-separator.js +56 -5
  218. package/dist/name-separator.js.map +1 -1
  219. package/dist/name-variants.d.ts +3 -1
  220. package/dist/name-variants.d.ts.map +1 -1
  221. package/dist/name-variants.js +8 -7
  222. package/dist/name-variants.js.map +1 -1
  223. package/dist/name-word-order.d.ts +9 -5
  224. package/dist/name-word-order.d.ts.map +1 -1
  225. package/dist/name-word-order.js +47 -15
  226. package/dist/name-word-order.js.map +1 -1
  227. package/dist/nominal-stem.d.ts +13 -0
  228. package/dist/nominal-stem.d.ts.map +1 -0
  229. package/dist/nominal-stem.js +32 -0
  230. package/dist/nominal-stem.js.map +1 -0
  231. package/dist/sentence-load.d.ts +2 -1
  232. package/dist/sentence-load.d.ts.map +1 -1
  233. package/dist/sentence-load.js +6 -2
  234. package/dist/sentence-load.js.map +1 -1
  235. package/dist/structure/cited-document-section.d.ts +7 -0
  236. package/dist/structure/cited-document-section.d.ts.map +1 -0
  237. package/dist/structure/cited-document-section.js +33 -0
  238. package/dist/structure/cited-document-section.js.map +1 -0
  239. package/dist/structure/html-text-blocks.d.ts +8 -0
  240. package/dist/structure/html-text-blocks.d.ts.map +1 -0
  241. package/dist/structure/html-text-blocks.js +47 -0
  242. package/dist/structure/html-text-blocks.js.map +1 -0
  243. package/dist/structure/issues.d.ts +1 -1
  244. package/dist/structure/issues.d.ts.map +1 -1
  245. package/dist/structure/issues.js +19 -4
  246. package/dist/structure/issues.js.map +1 -1
  247. package/dist/structure/labelled-date-order.d.ts +4 -1
  248. package/dist/structure/labelled-date-order.d.ts.map +1 -1
  249. package/dist/structure/labelled-date-order.js +2 -2
  250. package/dist/structure/labelled-date-order.js.map +1 -1
  251. package/dist/structure/labelled-order.d.ts +3 -0
  252. package/dist/structure/labelled-order.d.ts.map +1 -1
  253. package/dist/structure/labelled-order.js +8 -3
  254. package/dist/structure/labelled-order.js.map +1 -1
  255. package/dist/structure/list-numbering.d.ts +20 -4
  256. package/dist/structure/list-numbering.d.ts.map +1 -1
  257. package/dist/structure/list-numbering.js +65 -9
  258. package/dist/structure/list-numbering.js.map +1 -1
  259. package/dist/structure/listed-paragraph.d.ts +16 -0
  260. package/dist/structure/listed-paragraph.d.ts.map +1 -0
  261. package/dist/structure/listed-paragraph.js +37 -0
  262. package/dist/structure/listed-paragraph.js.map +1 -0
  263. package/dist/structure/numbering.d.ts +3 -2
  264. package/dist/structure/numbering.d.ts.map +1 -1
  265. package/dist/structure/numbering.js +31 -6
  266. package/dist/structure/numbering.js.map +1 -1
  267. package/dist/structure/reference-text.d.ts +9 -1
  268. package/dist/structure/reference-text.d.ts.map +1 -1
  269. package/dist/structure/reference-text.js +21 -3
  270. package/dist/structure/reference-text.js.map +1 -1
  271. package/dist/structure/section-names.d.ts +12 -0
  272. package/dist/structure/section-names.d.ts.map +1 -0
  273. package/dist/structure/section-names.js +26 -0
  274. package/dist/structure/section-names.js.map +1 -0
  275. package/dist/structure/versioned-label.d.ts +16 -0
  276. package/dist/structure/versioned-label.d.ts.map +1 -0
  277. package/dist/structure/versioned-label.js +48 -0
  278. package/dist/structure/versioned-label.js.map +1 -0
  279. package/dist/structure/written-headings.d.ts +31 -0
  280. package/dist/structure/written-headings.d.ts.map +1 -0
  281. package/dist/structure/written-headings.js +119 -0
  282. package/dist/structure/written-headings.js.map +1 -0
  283. package/dist/structure/written-resolve.d.ts +12 -0
  284. package/dist/structure/written-resolve.d.ts.map +1 -0
  285. package/dist/structure/written-resolve.js +49 -0
  286. package/dist/structure/written-resolve.js.map +1 -0
  287. package/package.json +3 -3
  288. package/rules/all-caps-shouting.yaml +2 -2
  289. package/rules/concrete-evidence-density.yaml +2 -2
  290. package/rules/contraction-consistency.yaml +13 -13
  291. package/rules/dangling-reference.yaml +2 -2
  292. package/rules/date-without-year.yaml +2 -2
  293. package/rules/defined-name-repeated.yaml +2 -2
  294. package/rules/fact-conflict.yaml +2 -2
  295. package/rules/heading-end-punctuation-mix.yaml +2 -2
  296. package/rules/heading-numbering-mix.yaml +2 -2
  297. package/rules/latin-spacing.yaml +2 -2
  298. package/rules/max-kanji-continuous.yaml +5 -3
  299. package/rules/max-sentence-length.yaml +3 -2
  300. package/rules/name-variant.yaml +2 -2
  301. package/rules/ngram-repetition.yaml +2 -2
  302. package/rules/numbering-gap.yaml +2 -2
  303. package/rules/orthographic-variant.yaml +2 -2
  304. package/rules/reference-title-mismatch.yaml +2 -2
  305. package/rules/repeated-sentence-ending.yaml +3 -2
  306. package/rules/request-without-deadline.yaml +2 -2
  307. package/rules/requirement-smell.yaml +2 -2
  308. package/rules/sentence-rhythm.yaml +2 -2
  309. package/rules/title-case-consistency.yaml +2 -2
  310. package/rules/unbalanced-bracket.yaml +2 -2
  311. package/rules/undefined-acronym.yaml +11 -2
  312. package/rules/undefined-term.yaml +2 -2
  313. package/rules/unfilled-placeholder.yaml +2 -2
  314. package/rules/vague-word-density.yaml +2 -2
  315. package/src/capitals-passage.ts +31 -0
  316. package/src/coined-hyphen-name.ts +27 -0
  317. package/src/company-names.ts +24 -3
  318. package/src/detectors/acronym-context.ts +5 -6
  319. package/src/detectors/acronym-definition.ts +123 -0
  320. package/src/detectors/acronym-lone.ts +26 -0
  321. package/src/detectors/acronym-name.ts +117 -0
  322. package/src/detectors/capitalised-term.ts +154 -4
  323. package/src/detectors/caps-caption.ts +61 -0
  324. package/src/detectors/char-shape.ts +74 -22
  325. package/src/detectors/clause-shape.ts +14 -4
  326. package/src/detectors/concrete-evidence.ts +77 -1
  327. package/src/detectors/contraction-minority.ts +11 -0
  328. package/src/detectors/counter-edge.ts +23 -0
  329. package/src/detectors/date-any-year.ts +56 -10
  330. package/src/detectors/date-heading-year.ts +55 -0
  331. package/src/detectors/date-year.ts +54 -9
  332. package/src/detectors/defined-term-form.ts +26 -5
  333. package/src/detectors/due-date.ts +10 -1
  334. package/src/detectors/en-shape.ts +5 -1
  335. package/src/detectors/fact-consistency.ts +3 -2
  336. package/src/detectors/gram-passages.ts +36 -0
  337. package/src/detectors/heading-end-mark.ts +23 -10
  338. package/src/detectors/heading-names.ts +31 -0
  339. package/src/detectors/heading-numbering.ts +7 -2
  340. package/src/detectors/inch-mark.ts +49 -0
  341. package/src/detectors/lexicon.ts +32 -21
  342. package/src/detectors/mark-shape.ts +12 -6
  343. package/src/detectors/name-variant-apart.ts +63 -0
  344. package/src/detectors/name-variant.ts +30 -12
  345. package/src/detectors/numeral-word.ts +33 -0
  346. package/src/detectors/organisation-run.ts +86 -0
  347. package/src/detectors/orthographic-variant.ts +22 -6
  348. package/src/detectors/outline-label.ts +48 -0
  349. package/src/detectors/page-marker.ts +26 -0
  350. package/src/detectors/parallel-definition.ts +64 -0
  351. package/src/detectors/reference-text.ts +1 -0
  352. package/src/detectors/registry/unfilled-placeholder.ts +2 -2
  353. package/src/detectors/registry/vague-density.ts +1 -1
  354. package/src/detectors/request-owner.ts +71 -8
  355. package/src/detectors/requirement-smell.ts +15 -2
  356. package/src/detectors/sentence-length.ts +14 -6
  357. package/src/detectors/sentence-rhythm.ts +8 -4
  358. package/src/detectors/series-label.ts +13 -4
  359. package/src/detectors/signals.ts +94 -26
  360. package/src/detectors/structure-tree.ts +10 -8
  361. package/src/detectors/template-sections.ts +37 -0
  362. package/src/detectors/titled-run.ts +43 -0
  363. package/src/detectors/unbalanced-bracket.ts +3 -0
  364. package/src/detectors/undefined-term.ts +8 -0
  365. package/src/detectors/unfilled-placeholder.ts +10 -0
  366. package/src/detectors/vague-density.ts +68 -0
  367. package/src/facts/fact-scope.ts +53 -4
  368. package/src/facts/table-facts.ts +43 -6
  369. package/src/grade-api.ts +38 -16
  370. package/src/heading-numbering.ts +35 -8
  371. package/src/name-end-letter.ts +9 -0
  372. package/src/name-given-before.ts +40 -0
  373. package/src/name-separator.ts +66 -5
  374. package/src/name-variants.ts +21 -11
  375. package/src/name-word-order.ts +62 -17
  376. package/src/nominal-stem.ts +36 -0
  377. package/src/sentence-load.ts +7 -2
  378. package/src/structure/cited-document-section.ts +42 -0
  379. package/src/structure/html-text-blocks.ts +45 -0
  380. package/src/structure/issues.ts +19 -4
  381. package/src/structure/labelled-date-order.ts +5 -2
  382. package/src/structure/labelled-order.ts +12 -3
  383. package/src/structure/list-numbering.ts +100 -11
  384. package/src/structure/listed-paragraph.ts +45 -0
  385. package/src/structure/numbering.ts +48 -19
  386. package/src/structure/reference-text.ts +25 -3
  387. package/src/structure/section-names.ts +32 -0
  388. package/src/structure/versioned-label.ts +52 -0
  389. package/src/structure/written-headings.ts +137 -0
  390. package/src/structure/written-resolve.ts +57 -0
@@ -0,0 +1,86 @@
1
+ import type { Token } from "../plugin.ts";
2
+
3
+ /** 組織の名前の前に立つ形(語彙表 organisation-form)と、組織や単位の名前を閉じる語(organisation-unit)。 */
4
+ export type OrganisationWords = { readonly forms: readonly string[]; readonly units: readonly string[] };
5
+
6
+ /** 区切りの前に要る字数。1 字なら、普通の語の一部(外部・本部・課題)と見分けられない。 */
7
+ const MIN_BODY_LENGTH = 2;
8
+ const KANJI = /[一-鿿]/u;
9
+
10
+ const surfaceOf = (tokens: readonly Token[]): string => tokens.map((token) => token.surface).join("");
11
+ const isNumber = (token: Token): boolean => token.features?.["NumType"] === "Card";
12
+ const isProperPiece = (token: Token): boolean => token.pos === "PROPN" || token.features?.["NameType"] === "GeoUnit";
13
+
14
+ /** covering の語が、隙間なく続いて並ぶときだけ、その語の並び。改行などで割れていれば読まない。 */
15
+ const contiguous = (tokens: readonly Token[], covering: readonly number[]): Token[] | undefined => {
16
+ const run = covering.flatMap((index) => tokens.slice(index, index + 1));
17
+ const joined = run.every((token, at) => at === 0 || run[at - 1]?.span.end === token.span.start);
18
+ return run.length === covering.length && joined ? run : undefined;
19
+ };
20
+
21
+ /**
22
+ * 連なりのすぐ後ろの語が組織の語そのもの(幼稚園運営企画グループのグループ)なら、それも足した語の並び。
23
+ * 漢字でない組織の語は漢字の連なりの外にあるが、名前はそこで閉じる。
24
+ */
25
+ const withClosingUnit = (tokens: readonly Token[], covering: readonly number[], units: readonly string[]): number[] => {
26
+ const next = (covering.at(-1) ?? -1) + 1;
27
+ const [last, closing] = [tokens[next - 1], tokens[next]];
28
+ const closes =
29
+ covering.length > 0 &&
30
+ last !== undefined &&
31
+ closing !== undefined &&
32
+ last.span.end === closing.span.start &&
33
+ !KANJI.test(closing.surface) &&
34
+ units.includes(closing.surface);
35
+ return closes ? [...covering, next] : [...covering];
36
+ };
37
+
38
+ /** 頭の、語の切れ目で終わる組織の形(国立研究開発法人)の語数。無ければ 0。 */
39
+ const formLength = (run: readonly Token[], forms: ReadonlySet<string>): number =>
40
+ run.reduce((found, _, at) => (forms.has(surfaceOf(run.slice(0, at + 1))) ? at + 1 : found), 0);
41
+
42
+ /** 頭の、固有名詞と地名の単位だけの並び(紀美野町、下神野地区、気象庁)の語数。固有名詞が無ければ 0。 */
43
+ const properLength = (run: readonly Token[]): number => {
44
+ const length = run.findIndex((token) => !isProperPiece(token));
45
+ const stretch = length === -1 ? run.length : length;
46
+ return run.slice(0, stretch).some((token) => token.pos === "PROPN") ? stretch : 0;
47
+ };
48
+
49
+ /** 1 つの区切り(総務課、産業技術総合研究所)か。組織の語で閉じ、その前に 2 字以上の語があり、数を含まない。 */
50
+ const isSegment = (part: readonly Token[], units: readonly string[]): boolean => {
51
+ const text = [...surfaceOf(part)];
52
+ const closing = units.filter((unit) => text.join("").endsWith(unit));
53
+ return !part.some(isNumber) && closing.some((unit) => text.length - [...unit].length >= MIN_BODY_LENGTH);
54
+ };
55
+
56
+ /**
57
+ * run を区切りだけに割ったときの区切りの数の最大。割れなければ 0。
58
+ * 後ろから、各位置から終わりまでを割れる数を一度ずつ決める(長い連なりでも語数の二乗で済む)。
59
+ */
60
+ const segmentCount = (run: readonly Token[], units: readonly string[]): number => {
61
+ const counts = run.reduceRight<number[]>((after, _, start) => {
62
+ const best = run.slice(start).reduce((most, __, offset) => {
63
+ const end = start + offset + 1;
64
+ const rest = after[end - start - 1] ?? 0;
65
+ const closesRun = end === run.length;
66
+ return isSegment(run.slice(start, end), units) && (closesRun || rest > 0) ? Math.max(most, 1 + rest) : most;
67
+ }, 0);
68
+ return [best, ...after];
69
+ }, []);
70
+ return counts[0] ?? 0;
71
+ };
72
+
73
+ /**
74
+ * 漢字の連なりの語(covering: tokens の添字)が 1 つの組織の名前か。名前は書き手が変えられない。
75
+ * 法人格か地名・固有名詞で始まって組織の語で閉じるもの(国立研究開発法人産業技術総合研究所、紀美野町青少年育成町民会議)、
76
+ * または組織の語で閉じる区切りが 2 つ以上続くもの(国土交通省鉄道局総務課)。
77
+ * 組織の語で閉じない連なり(情報処理推進機構認定試験、総務課長)や、頭に名前の無い 1 区切り(個人情報保護法施行規則改正案検討会)は名前ではない。
78
+ */
79
+ export const isOrganisationRun = (tokens: readonly Token[], covering: readonly number[], words: OrganisationWords): boolean => {
80
+ const run = contiguous(tokens, withClosingUnit(tokens, covering, words.units));
81
+ if (run === undefined || run.length === 0) return false;
82
+ const form = formLength(run, new Set(words.forms));
83
+ const head = form > 0 ? form : properLength(run);
84
+ const segments = segmentCount(run.slice(head), words.units);
85
+ return head > 0 ? segments >= 1 : segments >= 2;
86
+ };
@@ -7,8 +7,11 @@ import { nameSpans, touchesAny } from "../team-names.ts";
7
7
  import { furiganaSpans } from "../furigana.ts";
8
8
  import { acronymsIn, isCapitalsNotSpelling } from "../capitals-with-small.ts";
9
9
  import { isPartOfAddress } from "./address-word.ts";
10
+ import { isInCapitalsPassage } from "../capitals-passage.ts";
11
+ import { withoutCoinedNames } from "../coined-hyphen-name.ts";
10
12
  import { wholeWordRuns, type WholeWordRun } from "../whole-word-runs.ts";
11
13
  import { entriesForGenre } from "../lexicon-genres.ts";
14
+ import { asNounWhereNominal } from "../nominal-stem.ts";
12
15
 
13
16
  /**
14
17
  * A word found in the document: where it is, which sentence it is in, its key and spelling, how it is written when that differs, and
@@ -41,6 +44,8 @@ const FUKUSHI = "hiragana-fukushi";
41
44
  const IDIOM = "orthographic-variant-idiom";
42
45
  /** Lexicon: words the tokenizer cuts in one spelling and not the other (お客様, お客 and さま). Each is read whole, keyed by its group. */
43
46
  const WHOLE_WORD = "orthographic-variant-whole-word";
47
+ /** Lexicon: suffixes written onto a word that do not make a compound of it (割当て等, 引受け後): the word keeps its okurigana there. */
48
+ const PHRASE_SUFFIX = "orthographic-variant-phrase-suffix";
44
49
 
45
50
  const patternsOf = (doc: ProseDocument, id: string, genre: string | undefined): string[] =>
46
51
  entriesForGenre(doc.lexicons[id] ?? [], genre).map((entry) => entry.pattern);
@@ -121,7 +126,12 @@ const isCutFromKatakana = ({ token, previous }: InContext): boolean =>
121
126
  const isCopulaReadAsVerb = ({ token, previous }: InContext): boolean =>
122
127
  token.pos === "VERB" && token.surface === "で" && previous !== undefined && previous.pos !== "ADP";
123
128
 
124
- type Skips = { readonly skip: ReadonlySet<string>; readonly idioms: ReadonlySet<string>; readonly wholeWords: ReadonlyMap<string, string> };
129
+ type Skips = {
130
+ readonly skip: ReadonlySet<string>;
131
+ readonly idioms: ReadonlySet<string>;
132
+ readonly wholeWords: ReadonlyMap<string, string>;
133
+ readonly phraseSuffixes: ReadonlySet<string>;
134
+ };
125
135
 
126
136
  const readingKeyOf = (word: InContext, { skip, idioms }: Skips): string | undefined => {
127
137
  const { token } = word;
@@ -156,13 +166,14 @@ const readingWords = (sentence: Sentence, isOpen: IsOpen, skips: Skips): Placed[
156
166
  const whole = runs.flatMap((run) => wholeWordOf(sentence, tokens, run, isOpen, skips.wholeWords));
157
167
  return [
158
168
  ...whole,
159
- ...tokens.flatMap((token, at): Placed[] => {
169
+ ...tokens.flatMap((written, at): Placed[] => {
160
170
  if (inRun.has(at)) return [];
171
+ const next = tokens[at + 1];
172
+ const token = asNounWhereNominal(written, next, skips.phraseSuffixes);
161
173
  const key = readingKeyOf({ token, previous: tokens[at - 1], before: tokens[at - 2] }, skips);
162
174
  const local = token.span.start - sentence.span.start;
163
175
  if (key === undefined || token.lemma === undefined || !isOpen(local, local + token.surface.length)) return [];
164
- const next = tokens[at + 1];
165
- const compoundHead = token.pos === "NOUN" && next?.pos === "NOUN" && isTouching(token, next);
176
+ const compoundHead = token.pos === "NOUN" && next?.pos === "NOUN" && isTouching(token, next) && !skips.phraseSuffixes.has(next.surface);
166
177
  return [{ key, spelling: token.lemma, sentence, offset: token.span.start, compoundHead }];
167
178
  }),
168
179
  ];
@@ -260,7 +271,7 @@ const latinWords = (sentence: Sentence, isOpen: IsOpen, skip: ReadonlySet<string
260
271
  const plain = word.toLowerCase().replaceAll("-", "");
261
272
  const end = match.index + word.length;
262
273
  if (plain.length < MIN_LATIN_KEY || !LOWER.test(word) || isCapitalsNotSpelling(word, acronyms) || skip.has(word.toLowerCase())) return [];
263
- if (isPartOfAddress(sentence.text, match.index, end) || !isOpen(match.index, end)) return [];
274
+ if (isPartOfAddress(sentence.text, match.index, end) || !isOpen(match.index, end) || isInCapitalsPassage(sentence.text, match.index, end)) return [];
264
275
  return [{ key: `latin|${plain}`, spelling: latinSpelling(word), shown: word, sentence, offset: sentence.span.start + match.index }];
265
276
  });
266
277
  };
@@ -305,6 +316,7 @@ export const orthographicVariant: Detector = (doc, options): Finding[] => {
305
316
  skip: new Set([...patternsOf(doc, DISTINCT, options.genre), ...patternsOf(doc, FUKUSHI, options.genre)]),
306
317
  idioms: new Set(patternsOf(doc, IDIOM, options.genre)),
307
318
  wholeWords: new Map((doc.lexicons[WHOLE_WORD] ?? []).map((entry) => [entry.pattern, entry.group ?? entry.pattern])),
319
+ phraseSuffixes: new Set(patternsOf(doc, PHRASE_SUFFIX, options.genre)),
308
320
  };
309
321
  const names = doc.names ?? [];
310
322
  const placed: Place[] = [
@@ -314,7 +326,11 @@ export const orthographicVariant: Detector = (doc, options): Finding[] => {
314
326
  const reading = byKanji(placed.flatMap(({ sentence, isOpen }) => readingWords(sentence, isOpen, skips)));
315
327
  const katakana = placed.flatMap(({ sentence, isOpen }) => katakanaWords(sentence, isOpen));
316
328
  const runs = placed.flatMap(({ sentence, isOpen }) => katakanaRunWords(sentence, isOpen));
317
- const latin = placed.flatMap(({ sentence, isOpen, readsLatin }) => (readsLatin ? latinWords(sentence, isOpen, skips.skip) : []));
329
+ const dictionary = doc.dictionary?.();
330
+ const latin = withoutCoinedNames(
331
+ placed.flatMap(({ sentence, isOpen, readsLatin }) => (readsLatin ? latinWords(sentence, isOpen, skips.skip) : [])),
332
+ (word) => dictionary?.has(word) ?? true,
333
+ );
318
334
  const odd = [reading, katakana, latin].flatMap((words) => oddSpellings(words, options.limit));
319
335
  const oddRuns = oddSpellings(runs, options.limit).filter(({ word }) => !odd.some((other) => coversOffset(word, other.word.offset)));
320
336
  const all = [...reading, ...katakana, ...runs, ...latin];
@@ -0,0 +1,48 @@
1
+ import { escapeRegExp } from "../orthography.ts";
2
+
3
+ /**
4
+ * 行頭の、文字で振った見出しの番号(議事録の Z. の次の AA.、AB. … BZ.)。表計算の列名と同じく A〜Z の次は AA と進む。
5
+ * 略語ではない。行頭で . か ) が続き、その前か後の番号も同じ文書の行頭にあるときだけ番号と読む。1 か所だけの
6
+ * 行頭の大文字(IT. の説明、FAQ) の行)は略語のまま数える。そう読めた番号を区切りの名前の後ろで指す所(See Attachment AA)も
7
+ * 番号。区切りの名前は言語パッケージの語彙表から読む。
8
+ */
9
+
10
+ type Span = { readonly start: number; readonly end: number };
11
+
12
+ const LABEL = /^[ \t]*(?<label>[A-Z]{1,3})[.)](?=[ \t]|$)/gmu;
13
+ const ALPHABET = 26;
14
+ const CODE_BEFORE_A = 64;
15
+
16
+ /** A=1、Z=26、AA=27、AZ=52、BA=53。 */
17
+ const positionOf = (label: string): number => [...label].reduce((total, letter) => total * ALPHABET + (letter.charCodeAt(0) - CODE_BEFORE_A), 0);
18
+
19
+ type Label = { readonly text: string; readonly position: number; readonly span: Span };
20
+
21
+ const labelsOf = (source: string): Label[] =>
22
+ [...source.matchAll(LABEL)].map((match) => {
23
+ const text = match.groups?.["label"] ?? "";
24
+ const start = match.index + match[0].indexOf(text);
25
+ return { text, position: positionOf(text), span: { start, end: start + text.length } };
26
+ });
27
+
28
+ const lineLabels = (source: string): Label[] => {
29
+ const labels = labelsOf(source);
30
+ const positions = new Set(labels.map((label) => label.position));
31
+ return labels.filter(({ position }) => positions.has(position - 1) || positions.has(position + 1));
32
+ };
33
+
34
+ const referenceSpans = (source: string, labels: readonly Label[], divisions: readonly string[]): Span[] => {
35
+ const texts = [...new Set(labels.map((label) => label.text))];
36
+ if (texts.length === 0 || divisions.length === 0) return [];
37
+ const pattern = new RegExp(String.raw`(?<![A-Za-z0-9_])(?:${divisions.map(escapeRegExp).join("|")})\s+(?<label>${texts.join("|")})(?![A-Za-z0-9_&])`, "gu");
38
+ return [...source.matchAll(pattern)].map((match) => {
39
+ const end = match.index + match[0].length;
40
+ return { start: end - (match.groups?.["label"] ?? "").length, end };
41
+ });
42
+ };
43
+
44
+ /** 文書の中の、隣の番号と並んだ文字の番号と、それを区切りの名前の後ろで指す所の範囲。位置は文書の頭からの文字数。 */
45
+ export const outlineLabelSpans = (source: string, divisions: readonly string[]): Span[] => {
46
+ const labels = lineLabels(source);
47
+ return [...labels.map((label) => label.span), ...referenceSpans(source, labels, divisions)];
48
+ };
@@ -0,0 +1,26 @@
1
+ import { escapeRegExp } from "../orthography.ts";
2
+ import type { LengthUnit } from "../plugin.ts";
3
+
4
+ /**
5
+ * Page markers a converted print edition leaves inside a sentence ("( printed page 52044)", "[[Page 52044]]", 「-12-」).
6
+ * They are the page break, not the writer's words. The shapes come from the lexicon: "#" stands for the page number and a
7
+ * space for any run of spaces, none included. A marker touching a digit is part of a number (an address, 1-2-3).
8
+ */
9
+ const NUMBER = "#";
10
+
11
+ const TEMPLATE_PARTS: Readonly<Record<string, string>> = { [NUMBER]: String.raw`\p{Nd}+`, " ": String.raw`\s*` };
12
+
13
+ const templatePattern = (template: string): string => [...template].map((char) => TEMPLATE_PARTS[char] ?? escapeRegExp(char)).join("");
14
+
15
+ /** One pattern for every page marker the lexicon names, or undefined when it names none. */
16
+ export const pageMarkerPattern = (templates: readonly string[]): RegExp | undefined =>
17
+ templates.length === 0 ? undefined : new RegExp(String.raw`(?<!\p{Nd})(?:${templates.map(templatePattern).join("|")})(?!\p{Nd})`, "gu");
18
+
19
+ const sizeOf = (marker: string, unit: LengthUnit): number =>
20
+ unit === "word" ? marker.split(/\s+/u).filter((word) => word.length > 0).length : marker.replace(/\s+/gu, "").length;
21
+
22
+ /** How much of a sentence its page markers take up, in the document's unit: their words, or their characters without spaces. */
23
+ export const pageMarkerSize = (text: string, pattern: RegExp | undefined, unit: LengthUnit): number => {
24
+ if (pattern === undefined) return 0;
25
+ return [...text.matchAll(pattern)].map((match) => match[0]).reduce((size, marker) => size + sizeOf(marker, unit), 0);
26
+ };
@@ -0,0 +1,64 @@
1
+ import type { Lexicon, Sentence, Token } from "../plugin.ts";
2
+
3
+ // 並べた定義: どの文も違う語を、同じ型(〜を指します、〜といいます)で定義する。同じ文末が続くのは型をそろえたからで、
4
+ // 書き手の調子の繰り返しではない。
5
+
6
+ const TRAILING_POS: ReadonlySet<string> = new Set(["PUNCT", "SYM"]);
7
+ const TAIL_POS: ReadonlySet<string> = new Set(["AUX", "PART"]);
8
+
9
+ export type DefinitionWords = { readonly frames: ReadonlySet<string>; readonly topics: ReadonlySet<string> };
10
+
11
+ const patternsOf = (lexicon: Lexicon | undefined, group: string): ReadonlySet<string> =>
12
+ new Set((lexicon ?? []).filter((entry) => entry.group === group).map((entry) => entry.pattern));
13
+
14
+ export const definitionWordsOf = (lexicon: Lexicon | undefined): DefinitionWords => ({
15
+ frames: patternsOf(lexicon, "frame"),
16
+ topics: patternsOf(lexicon, "topic"),
17
+ });
18
+
19
+ const surfacesOf = (tokens: readonly Token[]): string => tokens.map((token) => token.surface).join("");
20
+
21
+ /** 述語(文末の助動詞の前の語)の位置。助動詞で終わらない文には無い。 */
22
+ const predicateAt = (tokens: readonly Token[]): number | undefined => {
23
+ const last = tokens.findLastIndex((token) => !TRAILING_POS.has(token.pos));
24
+ const head = tokens.slice(0, last + 1).findLastIndex((token) => !TAIL_POS.has(token.pos));
25
+ return head > 0 && head < last ? head : undefined;
26
+ };
27
+
28
+ const PARTICLE_POS = "ADP";
29
+ const TERM_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN"]);
30
+
31
+ /** 定義の文が定義する語と、その型(を指す)。 */
32
+ export type Definition = { readonly term: string; readonly frame: string };
33
+
34
+ /** topic の助詞(は)より前。無ければ、述語の前の助詞の直前の名詞(〜を台風といいます の 台風)。 */
35
+ const termOf = (tokens: readonly Token[], head: number, words: DefinitionWords): string => {
36
+ const topic = tokens.slice(0, head - 1).findIndex((token) => token.pos === PARTICLE_POS && words.topics.has(token.surface));
37
+ if (topic > 0) return surfacesOf(tokens.slice(0, topic));
38
+ const before = tokens[head - 2];
39
+ return before !== undefined && TERM_POS.has(before.pos) ? before.surface : "";
40
+ };
41
+
42
+ /** その文が定義する語と型。型の文(を指す、という)でなければ無い。 */
43
+ export const definitionOf = (sentence: Sentence, words: DefinitionWords): Definition | undefined => {
44
+ const tokens = (sentence.tokens ?? []).filter((token) => token.surface.trim() !== "");
45
+ const head = predicateAt(tokens);
46
+ const particle = head === undefined ? undefined : tokens[head - 1];
47
+ const predicate = head === undefined ? undefined : tokens[head];
48
+ if (head === undefined || particle === undefined || predicate === undefined) return undefined;
49
+ const frame = `${particle.surface}${predicate.lemma ?? predicate.surface}`;
50
+ if (!words.frames.has(frame)) return undefined;
51
+ const term = termOf(tokens, head, words);
52
+ return term === "" ? undefined : { term, frame };
53
+ };
54
+
55
+ const isDefinition = (definition: Definition | undefined): definition is Definition => definition !== undefined;
56
+
57
+ /** どの文も同じ型の定義で、定義する語がみな違う。 */
58
+ export const isParallelDefinition = (sentences: readonly Sentence[], words: DefinitionWords): boolean => {
59
+ const definitions = sentences.map((sentence) => definitionOf(sentence, words));
60
+ if (!definitions.every(isDefinition)) return false;
61
+ const terms = new Set(definitions.map((definition) => definition.term));
62
+ const frames = new Set(definitions.map((definition) => definition.frame));
63
+ return terms.size === definitions.length && frames.size === 1;
64
+ };
@@ -17,6 +17,7 @@ const inputOf = (doc: ProseDocument): ReferenceInput | undefined => {
17
17
  asides: patternsOf(doc, "reference-aside"),
18
18
  topicLinks: patternsOf(doc, "reference-topic-link"),
19
19
  vagueTopics: patternsOf(doc, "reference-topic-vague"),
20
+ listJoints: patternsOf(doc, "reference-list-joint"),
20
21
  },
21
22
  };
22
23
  };
@@ -1,4 +1,4 @@
1
1
  import type { Detector } from "../../plugin.ts";
2
- import { unfilledPlaceholder } from "../ai-phrasing.ts";
2
+ import { unfilledPlaceholderOutsideTemplates } from "../unfilled-placeholder.ts";
3
3
 
4
- export const detector: Detector = unfilledPlaceholder;
4
+ export const detector: Detector = unfilledPlaceholderOutsideTemplates;
@@ -1,4 +1,4 @@
1
1
  import type { Detector } from "../../plugin.ts";
2
- import { vagueDensity } from "../lexicon.ts";
2
+ import { vagueDensity } from "../vague-density.ts";
3
3
 
4
4
  export const detector: Detector = vagueDensity;
@@ -1,6 +1,6 @@
1
1
  // A request for an action (「資料をご確認ください」 "Please send the report.") with no deadline and no person or role,
2
2
  // in its own sentence or the next. The words come from the language's lexicons; the code only combines them.
3
- import type { Detector, Finding, Lexicon, ProseDocument, Sentence, Span, StructureNode } from "../plugin.ts";
3
+ import type { Detector, Finding, Lexicon, ProseDocument, Section, Sentence, Span, StructureNode } from "../plugin.ts";
4
4
  import { proseText } from "../measure.ts";
5
5
  import { comparableWords } from "./lexicon-match.ts";
6
6
  import { escapeRegExp } from "../orthography.ts";
@@ -15,6 +15,9 @@ export const REQUEST_LEXICONS = {
15
15
  owner: "request-owner",
16
16
  pointer: "request-pointer",
17
17
  offer: "request-offer",
18
+ timing: "request-timing",
19
+ invitation: "request-invitation",
20
+ contact: "request-contact",
18
21
  } as const;
19
22
 
20
23
  type Words = Readonly<Record<keyof typeof REQUEST_LEXICONS, readonly string[]>>;
@@ -32,6 +35,9 @@ const wordsOf = (doc: ProseDocument): Words => ({
32
35
  owner: patternsOf(doc.lexicons[REQUEST_LEXICONS.owner]),
33
36
  pointer: patternsOf(doc.lexicons[REQUEST_LEXICONS.pointer]),
34
37
  offer: patternsOf(doc.lexicons[REQUEST_LEXICONS.offer]),
38
+ timing: patternsOf(doc.lexicons[REQUEST_LEXICONS.timing]),
39
+ invitation: patternsOf(doc.lexicons[REQUEST_LEXICONS.invitation]),
40
+ contact: patternsOf(doc.lexicons[REQUEST_LEXICONS.contact]),
35
41
  });
36
42
 
37
43
  /** How far apart, in characters, the action word and the request ending may be: the action must be what is requested. */
@@ -57,16 +63,43 @@ export const containsAny = (text: string, patterns: readonly string[]): boolean
57
63
 
58
64
  const gapBetween = (left: Place, right: Place): number => Math.max(left.start - right.end, right.start - left.end, 0);
59
65
 
66
+ /** An invitation verb right at the request ending (「ご相談ください」 "please come"): the reader may come, nothing is to be finished. */
67
+ const INVITATION_REACH = 1;
68
+
69
+ /** A comma or semicolon between an action and the invitation: the action is a request of its own (「申請書を提出し、会場へお越しください」). */
70
+ const CLAUSE_BREAK = /[、,,;;]/u;
71
+
72
+ /** A label and a colon (「電話:」 "Fax:"): a contact line, whose request is how to reach the writer. */
73
+ const LABEL_COLON = /^\s*[::]/u;
74
+
75
+ /** Whether the action is what the invitation is for (「登録をしにお越しください」「入力用にご使用ください」), not a request beside it. */
76
+ const invitedTo = (comparable: string, action: Place, invitation: Place): boolean => {
77
+ const [left, right] = action.start <= invitation.start ? [action, invitation] : [invitation, action];
78
+ return !CLAUSE_BREAK.test(comparable.slice(left.end, right.start));
79
+ };
80
+
81
+ const holdsContactLine = (text: string, words: Words): boolean => {
82
+ const comparable = comparableWords(text);
83
+ return placesOf(comparable, words.contact).some((label) => LABEL_COLON.test(comparable.slice(label.end)));
84
+ };
85
+
60
86
  /**
61
87
  * A request for an action: an action word next to a request ending (「確認してください」「please send」), not a condition
62
88
  * (「ご不明な点があれば…ください」 is an offer), not an open offer to take the reader's questions (「ご質問はメールでお送りください」),
63
- * and not a pointer to where to read more (「詳細は資料をご確認ください」).
89
+ * not a pointer to where to read more (「詳細は資料をご確認ください」), not what an invitation is for (「登録をしにお越しください」),
90
+ * and not the way to reach the writer on a contact line (「ファックス番号:… 送信後、ご連絡ください」).
64
91
  * The action has to be close to the ending, or a noun like 検討会 or 履修登録 earlier in the sentence would make one.
65
92
  */
66
93
  export const isRequest = (text: string, words: Words): boolean => {
94
+ const comparable = comparableWords(text);
67
95
  const endings = placesOf(text, words.ending);
68
- const asked = placesOf(text, words.action).some((action) => endings.some((ending) => gapBetween(action, ending) <= ACTION_REACH));
69
- return asked && ![words.condition, words.offer, words.pointer].some((patterns) => containsAny(text, patterns));
96
+ const invitations = placesOf(text, words.invitation).filter((invitation) => endings.some((ending) => gapBetween(invitation, ending) <= INVITATION_REACH));
97
+ const asked = placesOf(text, words.action).some(
98
+ (action) =>
99
+ endings.some((ending) => gapBetween(action, ending) <= ACTION_REACH) && !invitations.some((invitation) => invitedTo(comparable, action, invitation)),
100
+ );
101
+ if (!asked || holdsContactLine(text, words)) return false;
102
+ return ![words.condition, words.offer, words.pointer].some((patterns) => containsAny(text, patterns));
70
103
  };
71
104
 
72
105
  const NAMED = new Set(["PROPN"]);
@@ -105,13 +138,16 @@ const holdsWord = (sentence: Sentence, patterns: readonly string[]): boolean =>
105
138
  return placesOf(comparable, patterns).some((place) => bounds === undefined || (bounds.starts.has(place.start) && bounds.ends.has(place.end)));
106
139
  };
107
140
 
108
- /** Whether a sentence says when (a date, a clock time, a deadline word) or who (a role; a name in the request itself). */
141
+ /**
142
+ * Whether a sentence says when or who (a role). The request itself may also say when relative to something else
143
+ * (「提出いただく前に」 "before you submit") or name the person; the next sentence's timing or name may be about something else.
144
+ */
109
145
  const saysWhenOrWho = (sentence: Sentence, words: Words, dates: readonly Span[], isRequestItself: boolean): boolean =>
110
146
  datedWithin(dates, sentence.span) ||
111
147
  CLOCK.test(proseText(sentence)) ||
112
148
  holdsWord(sentence, words.deadline) ||
113
149
  holdsWord(sentence, words.owner) ||
114
- (isRequestItself && namesSomeone(sentence));
150
+ (isRequestItself && (holdsWord(sentence, words.timing) || namesSomeone(sentence)));
115
151
 
116
152
  /** A paragraph break or a new list item between two sentences: the second does not continue the first. */
117
153
  const BREAK = /\n[^\S\n]*\n|\n[^\S\n]*(?:[-*+]|\d+[.)])[^\S\n]/u;
@@ -122,14 +158,41 @@ const continues = (source: string, sentence: Sentence, next: Sentence | undefine
122
158
  const dateSpans = (tree: StructureNode | undefined): Span[] =>
123
159
  tree === undefined ? [] : inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [node.span] : []));
124
160
 
125
- /** The request sentences with neither a deadline nor a person in them or in the sentence after, in the same paragraph or item. */
161
+ /** A section's heading, read as a sentence so the deadline words match it the same way. */
162
+ const headingOf = (section: Section): Sentence => ({
163
+ span: section.span,
164
+ text: section.heading,
165
+ ...(section.headingTokens === undefined ? {} : { tokens: section.headingTokens }),
166
+ });
167
+
168
+ /** The headings an offset sits under: its own section's, then each enclosing one's up to the title. */
169
+ const enclosingHeadings = (sections: readonly Section[], offset: number): Sentence[] => {
170
+ const own = sections.findLastIndex((section) => section.span.start <= offset);
171
+ const chain = sections
172
+ .slice(0, own + 1)
173
+ .reduceRight<{ depth: number; found: Section[] }>(
174
+ (walk, section) => (section.depth < walk.depth ? { depth: section.depth, found: [...walk.found, section] } : walk),
175
+ { depth: Number.POSITIVE_INFINITY, found: [] },
176
+ ).found;
177
+ return chain.filter((section) => section.depth > 0 && section.heading.trim() !== "").map(headingOf);
178
+ };
179
+
180
+ /**
181
+ * A notice whose heading names its deadline (「見積提出期限:10月26日」「【随時受付】」): every request under it inherits it.
182
+ * A date alone in a heading is more often the day the notice was issued or the event it announces, so it is not enough.
183
+ */
184
+ const underDeadlineHeading = (sections: readonly Section[], sentence: Sentence, words: Words): boolean =>
185
+ enclosingHeadings(sections, sentence.span.start).some((heading) => holdsWord(heading, words.deadline));
186
+
187
+ /** The request sentences with neither a deadline nor a person in them, in the sentence after (same paragraph or item), or in a heading above. */
126
188
  export const ownerlessRequests = (doc: ProseDocument): Sentence[] => {
127
189
  const words = wordsOf(doc);
128
190
  const dates = dateSpans(doc.structure);
129
191
  return doc.sentences.filter((sentence, index) => {
130
192
  if (!isRequest(proseText(sentence), words) || saysWhenOrWho(sentence, words, dates, true)) return false;
131
193
  const next = doc.sentences[index + 1];
132
- return !continues(doc.source, sentence, next) || !saysWhenOrWho(next, words, dates, false);
194
+ if (continues(doc.source, sentence, next) && saysWhenOrWho(next, words, dates, false)) return false;
195
+ return !underDeadlineHeading(doc.sections, sentence, words);
133
196
  });
134
197
  };
135
198
 
@@ -1,16 +1,20 @@
1
1
  import type { Detector, Finding, Lexicon, LexiconEntry, ProseDocument, Sentence } from "../plugin.ts";
2
2
  import { entryCloses, entryIn, entryRanges } from "./lexicon-match.ts";
3
3
  import { openEndAt, openEndNames, type OpenEndNames } from "./open-end-names.ts";
4
+ import { statesConcrete } from "./vague-deadline.ts";
4
5
 
5
6
  // Requirements smells (Femmer et al. 2017; Berry et al. 2003; ISO/IEC/IEEE 29148): a loophole, an open-ended list and
6
7
  // "and/or", in a sentence that states a requirement. What makes a sentence a requirement, and each smell's words, come
7
8
  // from the language's lexicons; this file knows neither language.
8
9
 
9
10
  const OPEN_END = "requirement-open-end";
11
+ const LOOPHOLE = "requirement-loophole";
12
+ const TIME = "time";
13
+ const CONCRETE_DEADLINE = "concrete-deadline";
10
14
 
11
15
  /** Each smell: the lexicon of its words, and the finding's variant (the rule's messages). */
12
16
  const SMELLS = [
13
- { lexicon: "requirement-loophole", variant: "loophole" },
17
+ { lexicon: LOOPHOLE, variant: "loophole" },
14
18
  { lexicon: OPEN_END, variant: "open-end" },
15
19
  { lexicon: "requirement-either", variant: "either" },
16
20
  ] as const;
@@ -43,9 +47,18 @@ const findingOf = (sentence: Sentence, entry: LexiconEntry, variant: string, off
43
47
  values: { matched: entry.pattern, offset },
44
48
  });
45
49
 
50
+ /** A loophole about when (group: time) that the sentence bounds with a stated limit ("as soon as possible, but no later than two days after"). */
51
+ const boundedInTime = (doc: ProseDocument, sentence: Sentence, entry: LexiconEntry, lexicon: string): boolean =>
52
+ lexicon === LOOPHOLE &&
53
+ entry.group === TIME &&
54
+ statesConcrete(
55
+ { start: sentence.span.start, text: sentence.text },
56
+ (doc.lexicons[CONCRETE_DEADLINE] ?? []).map((limit) => limit.pattern),
57
+ );
58
+
46
59
  /** Where the word stands as a smell. An open-end word inside a name the document gives (業務執行取締役等) is none; without tokens it cannot be told apart. */
47
60
  const smellAt = (doc: ProseDocument, sentence: Sentence, entry: LexiconEntry, lexicon: string, names: () => OpenEndNames): number | undefined => {
48
- if (!entryIn(sentence, entry)) return undefined;
61
+ if (!entryIn(sentence, entry) || boundedInTime(doc, sentence, entry, lexicon)) return undefined;
49
62
  const tokenized = sentence.tokens !== undefined && (entry.tokens?.length ?? 0) > 0;
50
63
  return lexicon === OPEN_END && tokenized ? openEndAt(doc, sentence, entry, names()) : offsetOf(sentence, entry);
51
64
  };
@@ -1,19 +1,26 @@
1
1
  import { lengthOf, proseText } from "../measure.ts";
2
2
  import type { Detector, DetectorOptions, Finding, LengthUnit, Sentence } from "../plugin.ts";
3
+ import { pageMarkerPattern, pageMarkerSize } from "./page-marker.ts";
3
4
 
4
5
  type Measure = { readonly size: number; readonly limit: number; readonly variant?: string };
5
6
 
6
- /** 文書と違う言語の文(和文の中の英文)は、その言語の単位と上限で測る。rule がその言語の段を持たなければ文書のもので。 */
7
- const measureOf = (sentence: Sentence, unit: LengthUnit, options: DetectorOptions): Measure => {
7
+ /**
8
+ * 文書と違う言語の文(和文の中の英文)は、その言語の単位と上限で測る。rule がその言語の段を持たなければ文書のもので。
9
+ * 印刷版のページの印(( printed page 52044))は文の言葉ではないので数えない。
10
+ */
11
+ const measureOf = (sentence: Sentence, unit: LengthUnit, options: DetectorOptions, markers: RegExp | undefined): Measure => {
8
12
  const embedded = sentence.embeddedLanguage;
9
13
  const limit = embedded === undefined ? undefined : options.embeddedLimits?.[embedded.id];
10
- if (embedded === undefined || limit === undefined) return { size: lengthOf(sentence, unit), limit: options.limit };
14
+ if (embedded === undefined || limit === undefined) {
15
+ return { size: lengthOf(sentence, unit) - pageMarkerSize(proseText(sentence), markers, unit), limit: options.limit };
16
+ }
11
17
  return { size: lengthOf(sentence, embedded.lengthUnit), limit, variant: `embedded-${embedded.id}` };
12
18
  };
13
19
 
14
- export const sentenceLength: Detector = (doc, options): Finding[] =>
15
- doc.sentences
16
- .map((sentence) => ({ sentence, measure: measureOf(sentence, doc.lengthUnit, options) }))
20
+ export const sentenceLength: Detector = (doc, options): Finding[] => {
21
+ const markers = pageMarkerPattern((options.lexicon ?? []).map((entry) => entry.pattern));
22
+ return doc.sentences
23
+ .map((sentence) => ({ sentence, measure: measureOf(sentence, doc.lengthUnit, options, markers) }))
17
24
  .filter(({ measure }) => measure.size > measure.limit)
18
25
  .map(({ sentence, measure }) => ({
19
26
  rule: "max-sentence-length",
@@ -24,3 +31,4 @@ export const sentenceLength: Detector = (doc, options): Finding[] =>
24
31
  values: { count: measure.size, limit: measure.limit, offset: sentence.span.start },
25
32
  ...(measure.variant === undefined ? {} : { variant: measure.variant }),
26
33
  }));
34
+ };
@@ -1,5 +1,5 @@
1
1
  import { charLength, proseText } from "../measure.ts";
2
- import type { Detector, Finding } from "../plugin.ts";
2
+ import type { Detector, Finding, ProseDocument, Sentence } from "../plugin.ts";
3
3
 
4
4
  /** 文が少ないと変動係数は当てにならない。ここを下回る文書は見ない。 */
5
5
  const MIN_SENTENCES = 8;
@@ -11,8 +11,12 @@ const coefficientOfVariation = (sizes: readonly number[]): number => {
11
11
  return Math.sqrt(variance) / mean;
12
12
  };
13
13
 
14
+ /** 箇条書きの項目は揃えて書くのが正しい形なので、地の文の文だけを測る。 */
15
+ const proseSentences = (doc: ProseDocument): readonly Sentence[] => doc.paragraphs.flatMap((paragraph) => paragraph.sentences);
16
+
14
17
  export const sentenceRhythm: Detector = (doc, options): Finding[] => {
15
- const sizes = doc.sentences.map((sentence) => charLength(sentence));
18
+ const sentences = proseSentences(doc);
19
+ const sizes = sentences.map((sentence) => charLength(sentence));
16
20
  if (sizes.length < MIN_SENTENCES) return [];
17
21
  const spread = Math.round(coefficientOfVariation(sizes) * 100);
18
22
  if (spread >= options.limit) return [];
@@ -22,8 +26,8 @@ export const sentenceRhythm: Detector = (doc, options): Finding[] => {
22
26
  severity: "warning",
23
27
  line: 0,
24
28
  column: 0,
25
- quote: doc.sentences[0] === undefined ? "" : proseText(doc.sentences[0]),
26
- values: { count: spread, limit: options.limit, offset: doc.sentences[0]?.span.start ?? 0 },
29
+ quote: sentences[0] === undefined ? "" : proseText(sentences[0]),
30
+ values: { count: spread, limit: options.limit, offset: sentences[0]?.span.start ?? 0 },
27
31
  },
28
32
  ];
29
33
  };
@@ -1,13 +1,22 @@
1
1
  /**
2
- * - で区切った文書番号のすぐ前の大文字の語(NIST SP 800-61 の SP、NSF 19-582、IEC 19757-2)。番号の一部で、略語ではない。
2
+ * - で区切った文書番号のすぐ前の大文字の語(NIST SP 800-61 の SP、NSF 19-582、IEC 19757-2、XRIN 0693-XC139、
3
+ * FR Doc. 2026-16371 の FR)。番号の一部で、略語ではない。
3
4
  * 空白で離して書くほかは、- で繋いだ識別子(AC-2)と同じ。番号の直前の 1 語だけで、その前の語(NIST)は略語のまま数える。
4
5
  * 区切りの無い番号(RFC 9110)、. で区切る版(SDK 3.1)、年の範囲(FY 2024-25)の前の語も略語のまま数える。
5
6
  */
6
7
 
7
8
  type Span = { readonly start: number; readonly end: number };
8
9
 
9
- const YEAR_RANGE = String.raw`(?:19|20)\d\d-\d`;
10
- const SERIES_LABEL = new RegExp(String.raw`(?<![A-Za-z0-9_&.-])[A-Z]+ (?!${YEAR_RANGE})(?<number>\d+(?:-\d+)+)[A-Za-z0-9]*(?![A-Za-z0-9_&-])`, "gu");
10
+ /** 年の範囲(2024-25、1999-2000)。後ろが 3 桁や 5 桁の番号(FR Doc. 2026-16371)は年ではない。 */
11
+ const YEAR_RANGE = String.raw`(?:19|20)\d\d-(?:(?:19|20)\d\d|\d\d)(?![A-Za-z0-9_&-])`;
12
+ /** 語と番号の間に置く、点で終わる 2〜3 文字の略し(FR Doc. 2026-16371、EO No. 14028-1)。 */
13
+ const NUMBER_WORD = String.raw`(?:[A-Z][a-z]{1,2}\. )?`;
14
+ /** 2 つめからの区切りは、2 文字までの英字の頭を持てる(RIN の 0693-XC139)。英字の後ろは 3 桁以上の通し番号。 */
15
+ const LATER_GROUP = String.raw`-(?:[A-Z]{1,2}\d{3,}|\d+)`;
16
+ const SERIES_LABEL = new RegExp(
17
+ String.raw`(?<![A-Za-z0-9_&.-])[A-Z]+ ${NUMBER_WORD}(?!${YEAR_RANGE})(?<number>\d+(?:${LATER_GROUP})+)[A-Za-z0-9]*(?![A-Za-z0-9_&-])`,
18
+ "gu",
19
+ );
11
20
 
12
21
  /**
13
22
  * 文書番号は長い通し番号を持つ(800-61、19757-2)。2 桁ずつの番号(BOD 25-01)は範囲・得点・日付(SRE 1-2、NFL 3-1、
@@ -23,7 +32,7 @@ const isRange = (groups: readonly string[]): boolean => {
23
32
 
24
33
  const isDocumentNumber = (number: string): boolean => {
25
34
  const groups = number.split("-");
26
- return groups.some((group) => group.length >= SERIAL_DIGITS) && !isRange(groups);
35
+ return groups.some((group) => group.replace(/\D/gu, "").length >= SERIAL_DIGITS) && !isRange(groups);
27
36
  };
28
37
 
29
38
  /** 文の中の、番号と、その前の大文字の語の範囲。 */