chaffjs 0.15.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (528) hide show
  1. package/README.md +27 -2
  2. package/dist/address-chain.js +1 -1
  3. package/dist/address-chain.js.map +1 -1
  4. package/dist/backends/anthropic.d.ts +2 -1
  5. package/dist/backends/anthropic.d.ts.map +1 -1
  6. package/dist/backends/anthropic.js +4 -1
  7. package/dist/backends/anthropic.js.map +1 -1
  8. package/dist/backends/openai.d.ts +2 -1
  9. package/dist/backends/openai.d.ts.map +1 -1
  10. package/dist/backends/openai.js +1 -1
  11. package/dist/backends/openai.js.map +1 -1
  12. package/dist/backends/types.d.ts +2 -1
  13. package/dist/backends/types.d.ts.map +1 -1
  14. package/dist/backends/types.js +1 -1
  15. package/dist/backends/types.js.map +1 -1
  16. package/dist/baseline.js +1 -1
  17. package/dist/baseline.js.map +1 -1
  18. package/dist/calendar-number.d.ts +31 -0
  19. package/dist/calendar-number.d.ts.map +1 -0
  20. package/dist/calendar-number.js +58 -0
  21. package/dist/calendar-number.js.map +1 -0
  22. package/dist/cli-args.d.ts +0 -5
  23. package/dist/cli-args.d.ts.map +1 -1
  24. package/dist/cli-args.js +2 -2
  25. package/dist/cli-args.js.map +1 -1
  26. package/dist/cli-text.d.ts +6 -2
  27. package/dist/cli-text.d.ts.map +1 -1
  28. package/dist/cli-text.js +19 -12
  29. package/dist/cli-text.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +43 -37
  32. package/dist/cli.js.map +1 -1
  33. package/dist/closing-numbers.d.ts +20 -0
  34. package/dist/closing-numbers.d.ts.map +1 -0
  35. package/dist/closing-numbers.js +56 -0
  36. package/dist/closing-numbers.js.map +1 -0
  37. package/dist/commands/cite.d.ts.map +1 -1
  38. package/dist/commands/cite.js +4 -4
  39. package/dist/commands/cite.js.map +1 -1
  40. package/dist/commands/eval.js +1 -1
  41. package/dist/commands/eval.js.map +1 -1
  42. package/dist/commands/init-ask.d.ts +19 -0
  43. package/dist/commands/init-ask.d.ts.map +1 -0
  44. package/dist/commands/init-ask.js +43 -0
  45. package/dist/commands/init-ask.js.map +1 -0
  46. package/dist/commands/test.d.ts +8 -0
  47. package/dist/commands/test.d.ts.map +1 -1
  48. package/dist/commands/test.js +78 -44
  49. package/dist/commands/test.js.map +1 -1
  50. package/dist/commands/tree.d.ts +1 -8
  51. package/dist/commands/tree.d.ts.map +1 -1
  52. package/dist/commands/tree.js +7 -6
  53. package/dist/commands/tree.js.map +1 -1
  54. package/dist/config/by-path.d.ts.map +1 -1
  55. package/dist/config/by-path.js +10 -9
  56. package/dist/config/by-path.js.map +1 -1
  57. package/dist/config/load.d.ts +4 -0
  58. package/dist/config/load.d.ts.map +1 -1
  59. package/dist/config/load.js +19 -0
  60. package/dist/config/load.js.map +1 -1
  61. package/dist/config/name-problems.d.ts +5 -0
  62. package/dist/config/name-problems.d.ts.map +1 -0
  63. package/dist/config/name-problems.js +11 -0
  64. package/dist/config/name-problems.js.map +1 -0
  65. package/dist/date-stamp.js +1 -1
  66. package/dist/date-stamp.js.map +1 -1
  67. package/dist/detectors/acronym-context.d.ts +2 -0
  68. package/dist/detectors/acronym-context.d.ts.map +1 -1
  69. package/dist/detectors/acronym-context.js +13 -0
  70. package/dist/detectors/acronym-context.js.map +1 -1
  71. package/dist/detectors/acronym-expansion.d.ts +2 -0
  72. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  73. package/dist/detectors/acronym-expansion.js +104 -1
  74. package/dist/detectors/acronym-expansion.js.map +1 -1
  75. package/dist/detectors/agreement-slip.d.ts +32 -0
  76. package/dist/detectors/agreement-slip.d.ts.map +1 -0
  77. package/dist/detectors/agreement-slip.js +137 -0
  78. package/dist/detectors/agreement-slip.js.map +1 -0
  79. package/dist/detectors/char-shape.d.ts.map +1 -1
  80. package/dist/detectors/char-shape.js +16 -6
  81. package/dist/detectors/char-shape.js.map +1 -1
  82. package/dist/detectors/content-morphemes.d.ts +4 -0
  83. package/dist/detectors/content-morphemes.d.ts.map +1 -0
  84. package/dist/detectors/content-morphemes.js +12 -0
  85. package/dist/detectors/content-morphemes.js.map +1 -0
  86. package/dist/detectors/en-shape.d.ts +0 -1
  87. package/dist/detectors/en-shape.d.ts.map +1 -1
  88. package/dist/detectors/en-shape.js +2 -143
  89. package/dist/detectors/en-shape.js.map +1 -1
  90. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  91. package/dist/detectors/enumerated-runs.js +4 -2
  92. package/dist/detectors/enumerated-runs.js.map +1 -1
  93. package/dist/detectors/gram-place.js +1 -1
  94. package/dist/detectors/gram-place.js.map +1 -1
  95. package/dist/detectors/heading-case.d.ts.map +1 -1
  96. package/dist/detectors/heading-case.js +3 -1
  97. package/dist/detectors/heading-case.js.map +1 -1
  98. package/dist/detectors/heading-echo.d.ts.map +1 -1
  99. package/dist/detectors/heading-echo.js +12 -18
  100. package/dist/detectors/heading-echo.js.map +1 -1
  101. package/dist/detectors/heading-overlap.d.ts +13 -0
  102. package/dist/detectors/heading-overlap.d.ts.map +1 -0
  103. package/dist/detectors/heading-overlap.js +52 -0
  104. package/dist/detectors/heading-overlap.js.map +1 -0
  105. package/dist/detectors/index.d.ts.map +1 -1
  106. package/dist/detectors/index.js +10 -4
  107. package/dist/detectors/index.js.map +1 -1
  108. package/dist/detectors/letter-joined-name.d.ts +13 -0
  109. package/dist/detectors/letter-joined-name.d.ts.map +1 -0
  110. package/dist/detectors/letter-joined-name.js +11 -0
  111. package/dist/detectors/letter-joined-name.js.map +1 -0
  112. package/dist/detectors/lettered-index.d.ts +9 -0
  113. package/dist/detectors/lettered-index.d.ts.map +1 -0
  114. package/dist/detectors/lettered-index.js +54 -0
  115. package/dist/detectors/lettered-index.js.map +1 -0
  116. package/dist/detectors/lexicon.d.ts +5 -1
  117. package/dist/detectors/lexicon.d.ts.map +1 -1
  118. package/dist/detectors/lexicon.js +51 -5
  119. package/dist/detectors/lexicon.js.map +1 -1
  120. package/dist/detectors/orthography.d.ts.map +1 -1
  121. package/dist/detectors/orthography.js +24 -6
  122. package/dist/detectors/orthography.js.map +1 -1
  123. package/dist/detectors/oxford-comma.d.ts +3 -0
  124. package/dist/detectors/oxford-comma.d.ts.map +1 -0
  125. package/dist/detectors/oxford-comma.js +340 -0
  126. package/dist/detectors/oxford-comma.js.map +1 -0
  127. package/dist/detectors/phrase-match.d.ts +3 -1
  128. package/dist/detectors/phrase-match.d.ts.map +1 -1
  129. package/dist/detectors/phrase-match.js +20 -15
  130. package/dist/detectors/phrase-match.js.map +1 -1
  131. package/dist/detectors/repeated-head.d.ts.map +1 -1
  132. package/dist/detectors/repeated-head.js +5 -4
  133. package/dist/detectors/repeated-head.js.map +1 -1
  134. package/dist/detectors/signals.d.ts.map +1 -1
  135. package/dist/detectors/signals.js +45 -16
  136. package/dist/detectors/signals.js.map +1 -1
  137. package/dist/detectors/stacked-hedge.d.ts +16 -0
  138. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  139. package/dist/detectors/stacked-hedge.js +67 -0
  140. package/dist/detectors/stacked-hedge.js.map +1 -0
  141. package/dist/detectors/stray-space.d.ts +7 -0
  142. package/dist/detectors/stray-space.d.ts.map +1 -0
  143. package/dist/detectors/stray-space.js +34 -0
  144. package/dist/detectors/stray-space.js.map +1 -0
  145. package/dist/detectors/structure.d.ts +3 -1
  146. package/dist/detectors/structure.d.ts.map +1 -1
  147. package/dist/detectors/structure.js +4 -2
  148. package/dist/detectors/structure.js.map +1 -1
  149. package/dist/detectors/superlative-amount.d.ts +9 -0
  150. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  151. package/dist/detectors/superlative-amount.js +20 -0
  152. package/dist/detectors/superlative-amount.js.map +1 -0
  153. package/dist/detectors/superlative-clause.d.ts +18 -0
  154. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  155. package/dist/detectors/superlative-clause.js +61 -0
  156. package/dist/detectors/superlative-clause.js.map +1 -0
  157. package/dist/detectors/superlative-scope.d.ts +6 -0
  158. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  159. package/dist/detectors/superlative-scope.js +3 -3
  160. package/dist/detectors/superlative-scope.js.map +1 -1
  161. package/dist/detectors/team.d.ts +0 -6
  162. package/dist/detectors/team.d.ts.map +1 -1
  163. package/dist/detectors/team.js +17 -1
  164. package/dist/detectors/team.js.map +1 -1
  165. package/dist/detectors/token-shape.d.ts.map +1 -1
  166. package/dist/detectors/token-shape.js +8 -6
  167. package/dist/detectors/token-shape.js.map +1 -1
  168. package/dist/document.d.ts +3 -0
  169. package/dist/document.d.ts.map +1 -1
  170. package/dist/document.js +33 -6
  171. package/dist/document.js.map +1 -1
  172. package/dist/eval.js +1 -1
  173. package/dist/eval.js.map +1 -1
  174. package/dist/feedback/draft.js +1 -1
  175. package/dist/feedback/draft.js.map +1 -1
  176. package/dist/feedback/excerpt.d.ts +0 -1
  177. package/dist/feedback/excerpt.d.ts.map +1 -1
  178. package/dist/feedback/excerpt.js +2 -2
  179. package/dist/feedback/excerpt.js.map +1 -1
  180. package/dist/file-header.d.ts +20 -0
  181. package/dist/file-header.d.ts.map +1 -0
  182. package/dist/file-header.js +21 -0
  183. package/dist/file-header.js.map +1 -0
  184. package/dist/files.js +3 -3
  185. package/dist/files.js.map +1 -1
  186. package/dist/genre-check.js +1 -1
  187. package/dist/genre-check.js.map +1 -1
  188. package/dist/genre-load.d.ts +6 -0
  189. package/dist/genre-load.d.ts.map +1 -0
  190. package/dist/genre-load.js +22 -0
  191. package/dist/genre-load.js.map +1 -0
  192. package/dist/genre-parse.d.ts +44 -0
  193. package/dist/genre-parse.d.ts.map +1 -0
  194. package/dist/genre-parse.js +117 -0
  195. package/dist/genre-parse.js.map +1 -0
  196. package/dist/genre-suggest.d.ts +15 -0
  197. package/dist/genre-suggest.d.ts.map +1 -0
  198. package/dist/genre-suggest.js +40 -0
  199. package/dist/genre-suggest.js.map +1 -0
  200. package/dist/genre.d.ts +1 -1
  201. package/dist/genre.d.ts.map +1 -1
  202. package/dist/genre.js +3 -13
  203. package/dist/genre.js.map +1 -1
  204. package/dist/init-choice.d.ts +12 -0
  205. package/dist/init-choice.d.ts.map +1 -0
  206. package/dist/init-choice.js +41 -0
  207. package/dist/init-choice.js.map +1 -0
  208. package/dist/init.d.ts.map +1 -1
  209. package/dist/init.js +16 -5
  210. package/dist/init.js.map +1 -1
  211. package/dist/judge.d.ts +2 -1
  212. package/dist/judge.d.ts.map +1 -1
  213. package/dist/judge.js +1 -1
  214. package/dist/judge.js.map +1 -1
  215. package/dist/line-paragraphs.d.ts +4 -0
  216. package/dist/line-paragraphs.d.ts.map +1 -0
  217. package/dist/line-paragraphs.js +71 -0
  218. package/dist/line-paragraphs.js.map +1 -0
  219. package/dist/mask.d.ts.map +1 -1
  220. package/dist/mask.js +2 -10
  221. package/dist/mask.js.map +1 -1
  222. package/dist/number-name.js +1 -1
  223. package/dist/number-name.js.map +1 -1
  224. package/dist/opening-line.d.ts +3 -0
  225. package/dist/opening-line.d.ts.map +1 -0
  226. package/dist/opening-line.js +14 -0
  227. package/dist/opening-line.js.map +1 -0
  228. package/dist/orthography.d.ts +5 -1
  229. package/dist/orthography.d.ts.map +1 -1
  230. package/dist/orthography.js +7 -1
  231. package/dist/orthography.js.map +1 -1
  232. package/dist/paragraph-length.d.ts +7 -0
  233. package/dist/paragraph-length.d.ts.map +1 -0
  234. package/dist/paragraph-length.js +10 -0
  235. package/dist/paragraph-length.js.map +1 -0
  236. package/dist/plugin.d.ts +12 -0
  237. package/dist/plugin.d.ts.map +1 -1
  238. package/dist/profile/check.d.ts +28 -0
  239. package/dist/profile/check.d.ts.map +1 -0
  240. package/dist/profile/check.js +12 -0
  241. package/dist/profile/check.js.map +1 -0
  242. package/dist/profile/for-file.d.ts +2 -2
  243. package/dist/profile/for-file.d.ts.map +1 -1
  244. package/dist/profile/for-file.js +9 -2
  245. package/dist/profile/for-file.js.map +1 -1
  246. package/dist/profile/load.js +1 -1
  247. package/dist/profile/load.js.map +1 -1
  248. package/dist/profile/select.d.ts +4 -2
  249. package/dist/profile/select.d.ts.map +1 -1
  250. package/dist/profile/select.js +8 -3
  251. package/dist/profile/select.js.map +1 -1
  252. package/dist/quoted-span.d.ts +13 -3
  253. package/dist/quoted-span.d.ts.map +1 -1
  254. package/dist/quoted-span.js +16 -11
  255. package/dist/quoted-span.js.map +1 -1
  256. package/dist/reasons.d.ts +1 -0
  257. package/dist/reasons.d.ts.map +1 -1
  258. package/dist/reasons.js +4 -1
  259. package/dist/reasons.js.map +1 -1
  260. package/dist/render/compact.d.ts.map +1 -1
  261. package/dist/render/compact.js +1 -2
  262. package/dist/render/compact.js.map +1 -1
  263. package/dist/render/eval.d.ts.map +1 -1
  264. package/dist/render/eval.js +4 -3
  265. package/dist/render/eval.js.map +1 -1
  266. package/dist/render/friendly.d.ts +2 -1
  267. package/dist/render/friendly.d.ts.map +1 -1
  268. package/dist/render/friendly.js +14 -6
  269. package/dist/render/friendly.js.map +1 -1
  270. package/dist/render/genres.d.ts +5 -0
  271. package/dist/render/genres.d.ts.map +1 -0
  272. package/dist/render/genres.js +28 -0
  273. package/dist/render/genres.js.map +1 -0
  274. package/dist/render/plural.d.ts +3 -0
  275. package/dist/render/plural.d.ts.map +1 -0
  276. package/dist/render/plural.js +5 -0
  277. package/dist/render/plural.js.map +1 -0
  278. package/dist/render/rules-json.d.ts.map +1 -1
  279. package/dist/render/rules-json.js +9 -3
  280. package/dist/render/rules-json.js.map +1 -1
  281. package/dist/render/semantic.d.ts +3 -2
  282. package/dist/render/semantic.d.ts.map +1 -1
  283. package/dist/render/semantic.js +94 -34
  284. package/dist/render/semantic.js.map +1 -1
  285. package/dist/render/summary.d.ts.map +1 -1
  286. package/dist/render/summary.js +4 -3
  287. package/dist/render/summary.js.map +1 -1
  288. package/dist/render/suppressions.d.ts.map +1 -1
  289. package/dist/render/suppressions.js +3 -2
  290. package/dist/render/suppressions.js.map +1 -1
  291. package/dist/render/text.d.ts +2 -0
  292. package/dist/render/text.d.ts.map +1 -1
  293. package/dist/render/text.js +11 -3
  294. package/dist/render/text.js.map +1 -1
  295. package/dist/rule-load.d.ts.map +1 -1
  296. package/dist/rule-load.js +12 -1
  297. package/dist/rule-load.js.map +1 -1
  298. package/dist/run-semantic.d.ts +3 -1
  299. package/dist/run-semantic.d.ts.map +1 -1
  300. package/dist/run-semantic.js +9 -4
  301. package/dist/run-semantic.js.map +1 -1
  302. package/dist/run.d.ts +4 -0
  303. package/dist/run.d.ts.map +1 -1
  304. package/dist/run.js +29 -16
  305. package/dist/run.js.map +1 -1
  306. package/dist/semantic.d.ts +1 -1
  307. package/dist/semantic.d.ts.map +1 -1
  308. package/dist/semantic.js +10 -3
  309. package/dist/semantic.js.map +1 -1
  310. package/dist/sentence-shape.d.ts +0 -1
  311. package/dist/sentence-shape.d.ts.map +1 -1
  312. package/dist/sentence-shape.js +3 -5
  313. package/dist/sentence-shape.js.map +1 -1
  314. package/dist/setting-problems.d.ts +5 -0
  315. package/dist/setting-problems.d.ts.map +1 -0
  316. package/dist/setting-problems.js +15 -0
  317. package/dist/setting-problems.js.map +1 -0
  318. package/dist/span-merge.d.ts +4 -0
  319. package/dist/span-merge.d.ts.map +1 -0
  320. package/dist/span-merge.js +12 -0
  321. package/dist/span-merge.js.map +1 -0
  322. package/dist/speaker-labels.d.ts +4 -0
  323. package/dist/speaker-labels.d.ts.map +1 -0
  324. package/dist/speaker-labels.js +106 -0
  325. package/dist/speaker-labels.js.map +1 -0
  326. package/dist/stray-space.d.ts +17 -0
  327. package/dist/stray-space.d.ts.map +1 -0
  328. package/dist/stray-space.js +66 -0
  329. package/dist/stray-space.js.map +1 -0
  330. package/dist/structure/build.js +5 -5
  331. package/dist/structure/build.js.map +1 -1
  332. package/dist/structure/cite.js +1 -1
  333. package/dist/structure/cite.js.map +1 -1
  334. package/dist/structure/issues.d.ts.map +1 -1
  335. package/dist/structure/issues.js +5 -5
  336. package/dist/structure/issues.js.map +1 -1
  337. package/dist/structure/markdown-path.d.ts.map +1 -1
  338. package/dist/structure/markdown-path.js +2 -2
  339. package/dist/structure/markdown-path.js.map +1 -1
  340. package/dist/structure/numbering.d.ts.map +1 -1
  341. package/dist/structure/numbering.js +1 -1
  342. package/dist/structure/numbering.js.map +1 -1
  343. package/dist/structure/relative-find.js +1 -1
  344. package/dist/structure/relative-find.js.map +1 -1
  345. package/dist/structure/relative-resolve.d.ts.map +1 -1
  346. package/dist/structure/relative-resolve.js +26 -20
  347. package/dist/structure/relative-resolve.js.map +1 -1
  348. package/dist/structure/sexp.js +1 -1
  349. package/dist/structure/sexp.js.map +1 -1
  350. package/dist/structure/unnumbered.js +2 -2
  351. package/dist/structure/unnumbered.js.map +1 -1
  352. package/dist/team-names.d.ts +9 -0
  353. package/dist/team-names.d.ts.map +1 -0
  354. package/dist/team-names.js +31 -0
  355. package/dist/team-names.js.map +1 -0
  356. package/dist/ui.d.ts +2 -0
  357. package/dist/ui.d.ts.map +1 -1
  358. package/dist/ui.js +6 -0
  359. package/dist/ui.js.map +1 -1
  360. package/dist/version.js +1 -1
  361. package/dist/version.js.map +1 -1
  362. package/dist/watch.d.ts +2 -2
  363. package/dist/watch.d.ts.map +1 -1
  364. package/dist/watch.js +5 -5
  365. package/dist/watch.js.map +1 -1
  366. package/genres.yaml +395 -0
  367. package/package.json +4 -3
  368. package/rules/adverb-overuse.yaml +2 -2
  369. package/rules/agentless-passive.yaml +1 -1
  370. package/rules/agreement-slip.yaml +36 -0
  371. package/rules/ai-generated-composite.yaml +2 -2
  372. package/rules/ai-tell.yaml +2 -2
  373. package/rules/bold-density.yaml +2 -2
  374. package/rules/closing-cliche.yaml +1 -1
  375. package/rules/concrete-evidence-density.yaml +2 -2
  376. package/rules/contraction-consistency.yaml +1 -1
  377. package/rules/cushion-phrase-density.yaml +1 -1
  378. package/rules/dangling-reference.yaml +1 -1
  379. package/rules/date-order.yaml +1 -1
  380. package/rules/date-weekday-mismatch.yaml +1 -1
  381. package/rules/double-keigo.yaml +1 -1
  382. package/rules/doubled-word.yaml +1 -1
  383. package/rules/duplicate-definition.yaml +1 -1
  384. package/rules/emoji-density.yaml +1 -1
  385. package/rules/empty-conclusion.yaml +3 -1
  386. package/rules/empty-intensifier.yaml +1 -1
  387. package/rules/excessive-hedging.yaml +16 -6
  388. package/rules/expletive-construction.yaml +2 -2
  389. package/rules/heading-echo.yaml +2 -1
  390. package/rules/hiragana-fukushi.yaml +1 -1
  391. package/rules/internal-jargon.yaml +2 -2
  392. package/rules/latin-spacing.yaml +5 -4
  393. package/rules/max-kanji-continuous.yaml +4 -3
  394. package/rules/max-paragraph-length.yaml +18 -4
  395. package/rules/max-sentence-length.yaml +18 -2
  396. package/rules/ngram-repetition.yaml +4 -4
  397. package/rules/no-doubled-joshi.yaml +2 -2
  398. package/rules/no-em-dash.yaml +2 -2
  399. package/rules/no-mixed-desumasu.yaml +1 -1
  400. package/rules/no-nakaguro-parallel.yaml +2 -2
  401. package/rules/numbering-gap.yaml +1 -1
  402. package/rules/oxford-comma-consistency.yaml +2 -1
  403. package/rules/padded-intro.yaml +2 -1
  404. package/rules/paragraph-length-variance.yaml +1 -1
  405. package/rules/preamble-length.yaml +2 -2
  406. package/rules/preferred-term.yaml +1 -1
  407. package/rules/proper-noun-density.yaml +3 -2
  408. package/rules/repeated-conjunction.yaml +2 -2
  409. package/rules/repeated-sentence-head.yaml +2 -2
  410. package/rules/required-sections.yaml +1 -1
  411. package/rules/rule-of-three.yaml +2 -2
  412. package/rules/sasete-itadaku.yaml +3 -3
  413. package/rules/section-length-uniformity.yaml +1 -1
  414. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  415. package/rules/sentence-rhythm.yaml +1 -1
  416. package/rules/stray-space.yaml +38 -0
  417. package/rules/taigen-dome-in-prose.yaml +2 -2
  418. package/rules/title-case-consistency.yaml +1 -1
  419. package/rules/total-mismatch.yaml +1 -1
  420. package/rules/undefined-acronym.yaml +5 -4
  421. package/rules/unqualified-superlative.yaml +2 -2
  422. package/skills/chaff/SKILL.md +28 -9
  423. package/src/address-chain.ts +1 -1
  424. package/src/backends/anthropic.ts +5 -1
  425. package/src/backends/openai.ts +2 -1
  426. package/src/backends/types.ts +3 -2
  427. package/src/baseline.ts +1 -1
  428. package/src/calendar-number.ts +84 -0
  429. package/src/cli-args.ts +2 -2
  430. package/src/cli-text.ts +29 -15
  431. package/src/cli.ts +42 -41
  432. package/src/closing-numbers.ts +72 -0
  433. package/src/commands/cite.ts +4 -4
  434. package/src/commands/eval.ts +1 -1
  435. package/src/commands/init-ask.ts +53 -0
  436. package/src/commands/test.ts +118 -56
  437. package/src/commands/tree.ts +7 -6
  438. package/src/config/by-path.ts +8 -9
  439. package/src/config/load.ts +24 -0
  440. package/src/config/name-problems.ts +15 -0
  441. package/src/date-stamp.ts +1 -1
  442. package/src/detectors/acronym-context.ts +16 -0
  443. package/src/detectors/acronym-expansion.ts +132 -1
  444. package/src/detectors/agreement-slip.ts +191 -0
  445. package/src/detectors/char-shape.ts +21 -9
  446. package/src/detectors/content-morphemes.ts +19 -0
  447. package/src/detectors/en-shape.ts +2 -173
  448. package/src/detectors/enumerated-runs.ts +3 -2
  449. package/src/detectors/gram-place.ts +1 -1
  450. package/src/detectors/heading-case.ts +4 -1
  451. package/src/detectors/heading-echo.ts +12 -20
  452. package/src/detectors/heading-overlap.ts +65 -0
  453. package/src/detectors/index.ts +10 -4
  454. package/src/detectors/letter-joined-name.ts +15 -0
  455. package/src/detectors/lettered-index.ts +78 -0
  456. package/src/detectors/lexicon.ts +69 -8
  457. package/src/detectors/orthography.ts +31 -7
  458. package/src/detectors/oxford-comma.ts +402 -0
  459. package/src/detectors/phrase-match.ts +23 -12
  460. package/src/detectors/repeated-head.ts +3 -4
  461. package/src/detectors/signals.ts +47 -15
  462. package/src/detectors/stacked-hedge.ts +96 -0
  463. package/src/detectors/stray-space.ts +40 -0
  464. package/src/detectors/structure.ts +10 -2
  465. package/src/detectors/superlative-amount.ts +24 -0
  466. package/src/detectors/superlative-clause.ts +82 -0
  467. package/src/detectors/superlative-scope.ts +3 -3
  468. package/src/detectors/team.ts +18 -2
  469. package/src/detectors/token-shape.ts +8 -10
  470. package/src/document.ts +54 -7
  471. package/src/eval.ts +1 -1
  472. package/src/feedback/draft.ts +1 -1
  473. package/src/feedback/excerpt.ts +2 -2
  474. package/src/file-header.ts +31 -0
  475. package/src/files.ts +3 -3
  476. package/src/genre-check.ts +1 -1
  477. package/src/genre-load.ts +24 -0
  478. package/src/genre-parse.ts +151 -0
  479. package/src/genre-suggest.ts +50 -0
  480. package/src/genre.ts +4 -13
  481. package/src/init-choice.ts +47 -0
  482. package/src/init.ts +20 -6
  483. package/src/judge.ts +3 -1
  484. package/src/line-paragraphs.ts +86 -0
  485. package/src/mask.ts +2 -12
  486. package/src/number-name.ts +1 -1
  487. package/src/opening-line.ts +17 -0
  488. package/src/orthography.ts +10 -2
  489. package/src/paragraph-length.ts +9 -0
  490. package/src/plugin.ts +12 -0
  491. package/src/profile/check.ts +34 -0
  492. package/src/profile/for-file.ts +10 -3
  493. package/src/profile/load.ts +1 -1
  494. package/src/profile/select.ts +11 -4
  495. package/src/quoted-span.ts +26 -12
  496. package/src/reasons.ts +5 -1
  497. package/src/render/compact.ts +1 -3
  498. package/src/render/eval.ts +4 -3
  499. package/src/render/friendly.ts +21 -6
  500. package/src/render/genres.ts +31 -0
  501. package/src/render/plural.ts +6 -0
  502. package/src/render/rules-json.ts +10 -3
  503. package/src/render/semantic.ts +117 -34
  504. package/src/render/summary.ts +4 -3
  505. package/src/render/suppressions.ts +3 -2
  506. package/src/render/text.ts +18 -3
  507. package/src/rule-load.ts +14 -1
  508. package/src/run-semantic.ts +29 -21
  509. package/src/run.ts +33 -16
  510. package/src/semantic.ts +11 -3
  511. package/src/sentence-shape.ts +3 -7
  512. package/src/setting-problems.ts +24 -0
  513. package/src/span-merge.ts +13 -0
  514. package/src/speaker-labels.ts +129 -0
  515. package/src/stray-space.ts +81 -0
  516. package/src/structure/build.ts +5 -5
  517. package/src/structure/cite.ts +1 -1
  518. package/src/structure/issues.ts +6 -5
  519. package/src/structure/markdown-path.ts +2 -2
  520. package/src/structure/numbering.ts +1 -1
  521. package/src/structure/relative-find.ts +1 -1
  522. package/src/structure/relative-resolve.ts +34 -19
  523. package/src/structure/sexp.ts +1 -1
  524. package/src/structure/unnumbered.ts +2 -2
  525. package/src/team-names.ts +39 -0
  526. package/src/ui.ts +7 -0
  527. package/src/version.ts +1 -1
  528. package/src/watch.ts +5 -5
@@ -0,0 +1,86 @@
1
+ import type { Span } from "./plugin.ts";
2
+
3
+ /**
4
+ * 1 行に 1 段落を書き、段落の間に空行を置かない文書(青空文庫、議事録、`<br>` で改行した HTML)。
5
+ * Markdown では全体が 1 つの段落になるが、書いた人にとっては行が段落。
6
+ *
7
+ * 割るのは文の終わる改行だけ。文の境目は解析器が改行をつないだ上で決めたもの(#248)なので、折り返した文は割れない。
8
+ * 形で見分けるのは 2 つ:
9
+ * - 固定幅の折り返しは、行の多くが文の途中で終わる。文で終わる行(段落の最後の行を含む)が半分に届かなければ割らない
10
+ * (議事録は発言者の行と発言の行が交互に来るので、半分ちょうどは割る)。
11
+ * - 1 行 1 文の書き方(行を意味の切れ目で改める。英語の Markdown に多い)は、段落のつもりではない。
12
+ * 文の頭で始まり文の終わりで終わる行が 2 文以上を持つことが、段落を 1 行に書いた印。それがたまにしか無ければ割らない。
13
+ */
14
+
15
+ /** 2 文以上を持つ行がこれより少なければ、1 行 1 文の書き方にたまに混じる 2 文の行と見分けられない。 */
16
+ const MIN_MULTI_SENTENCE = 3;
17
+ /** 2 文以上を持つ行が、割った段落の数のこの割合に届かなければ割らない。議事録は 1 文の発言が多い。 */
18
+ const MULTI_SENTENCE_SHARE = 1 / 5;
19
+
20
+ const NEWLINE = /\r?\n/gu;
21
+
22
+ type Line = { readonly span: Span; readonly newline: number };
23
+
24
+ /** 段落の中の行と、その行を終える改行の位置(最後の行は段落の終わり)。 */
25
+ const linesOf = (source: string, paragraph: Span): Line[] => {
26
+ const text = source.slice(paragraph.start, paragraph.end);
27
+ const breaks = [...text.matchAll(NEWLINE)].map((match) => ({ at: paragraph.start + match.index, width: match[0].length }));
28
+ const starts = [paragraph.start, ...breaks.map((found) => found.at + found.width)];
29
+ return starts.map((start, index) => {
30
+ const end = breaks[index]?.at ?? paragraph.end;
31
+ return { span: { start, end }, newline: end };
32
+ });
33
+ };
34
+
35
+ /** 文の前後の空白を除いた範囲。改行が文の中にあるかを、空白でなく文字で決める。 */
36
+ const coreOf = (source: string, sentence: Span): Span => {
37
+ const text = source.slice(sentence.start, sentence.end);
38
+ return { start: sentence.start + text.length - text.trimStart().length, end: sentence.end - (text.length - text.trimEnd().length) };
39
+ };
40
+
41
+ /** cores(並び順、重ならない)の中で、start が offset 以上の最初の添字。何万行の段落でも行ごとに全部をなめない。 */
42
+ const firstStartingAt = (cores: readonly Span[], offset: number): number => {
43
+ const search = (low: number, high: number): number => {
44
+ if (low >= high) return low;
45
+ const middle = (low + high) >> 1;
46
+ return (cores[middle]?.start ?? Number.POSITIVE_INFINITY) >= offset ? search(low, middle) : search(middle + 1, high);
47
+ };
48
+ return search(0, cores.length);
49
+ };
50
+
51
+ /** 改行をまたぐ文が無い。またぐなら、改行の直前に始まった文がまだ続いている。 */
52
+ const endsSentence = (line: Line, cores: readonly Span[]): boolean => (cores[firstStartingAt(cores, line.newline) - 1]?.end ?? 0) <= line.newline;
53
+
54
+ /** 覆われて空白だけになった行(話し手の名前の行)は段落を終えない。終えると、文の無い段落ができる。 */
55
+ const hasText = (source: string, line: Line): boolean => source.slice(line.span.start, line.span.end).trim().length > 0;
56
+
57
+ /** 行の並びを、文の終わる改行で区切った段落。 */
58
+ const groupAt = (lines: readonly Line[], closing: readonly boolean[]): Span[] => {
59
+ const firsts = lines.flatMap((_, index) => (index === 0 || closing[index - 1] === true ? [index] : []));
60
+ return firsts.map((first, index) => ({
61
+ start: lines[first]?.span.start ?? 0,
62
+ end: lines[(firsts[index + 1] ?? lines.length) - 1]?.span.end ?? 0,
63
+ }));
64
+ };
65
+
66
+ const sentencesIn = (group: Span, cores: readonly Span[]): number => firstStartingAt(cores, group.end) - firstStartingAt(cores, group.start);
67
+
68
+ /** 文の頭で始まり文の終わりで終わる行のうち、2 文以上を持つもの。固定幅の折り返しでは、行の両端が偶然そろうことはまれ。 */
69
+ const wholeMultiSentenceLines = (lines: readonly Line[], closing: readonly boolean[], cores: readonly Span[]): number =>
70
+ lines.filter((line, index) => (index === 0 || closing[index - 1] === true) && closing[index] === true && sentencesIn(line.span, cores) >= 2).length;
71
+
72
+ /** 行ごとの段落として読む形か。 */
73
+ const isLineShaped = (lines: readonly Line[], closing: readonly boolean[], groups: readonly Span[], cores: readonly Span[]): boolean => {
74
+ const closed = closing.filter(Boolean).length;
75
+ const multi = wholeMultiSentenceLines(lines, closing, cores);
76
+ return closed * 2 >= closing.length && multi >= MIN_MULTI_SENTENCE && multi >= groups.length * MULTI_SENTENCE_SHARE;
77
+ };
78
+
79
+ /** paragraph を、書いた人が段落のつもりで改めた行ごとに割る。そう読めなければ paragraph 1 つ。sentences は段落の中の文。 */
80
+ export const lineParagraphs = (source: string, paragraph: Span, sentences: readonly Span[]): Span[] => {
81
+ const lines = linesOf(source, paragraph);
82
+ const cores = sentences.map((sentence) => coreOf(source, sentence)).filter((core) => core.end > core.start);
83
+ const closing = lines.map((line) => hasText(source, line) && endsSentence(line, cores));
84
+ const groups = groupAt(lines, closing);
85
+ return isLineShaped(lines, closing, groups, cores) ? groups : [paragraph];
86
+ };
package/src/mask.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Span } from "./plugin.ts";
2
+ import { mergeSpans } from "./span-merge.ts";
2
3
 
3
4
  /**
4
5
  * 非 prose(コードブロック・インラインコード・表・見出し・URL)を、
@@ -16,19 +17,8 @@ import type { Span } from "./plugin.ts";
16
17
  */
17
18
  const blankOut = (text: string): string => text.replace(/[^\n]/gu, (char) => " ".repeat(char.length));
18
19
 
19
- const merge = (spans: readonly Span[]): Span[] =>
20
- [...spans]
21
- .sort((left, right) => left.start - right.start)
22
- .reduce<Span[]>((acc, span) => {
23
- const last = acc.at(-1);
24
- if (last !== undefined && span.start <= last.end) {
25
- return [...acc.slice(0, -1), { start: last.start, end: Math.max(last.end, span.end) }];
26
- }
27
- return [...acc, span];
28
- }, []);
29
-
30
20
  export const maskSpans = (source: string, spans: readonly Span[]): string => {
31
- const merged = merge(spans);
21
+ const merged = mergeSpans(spans, true);
32
22
  const { parts, cursor } = merged.reduce<{ parts: string[]; cursor: number }>(
33
23
  (acc, span) => ({
34
24
  parts: [...acc.parts, source.slice(acc.cursor, span.start), blankOut(source.slice(span.start, span.end))],
@@ -88,7 +88,7 @@ export const placeChainBefore = (tokens: readonly Token[], end: number): Token[]
88
88
  * ハイフンの無い数(千代田区23 番)も読まない。topUnits は都道府県の単位(語彙表 prefecture-unit)。
89
89
  */
90
90
  const isAddressNumber = (text: string, run: Span, tokens: readonly Token[], base: number, topUnits: ReadonlySet<string>): boolean =>
91
- [...text.slice(run.start, run.end)].some(isHyphen) &&
91
+ Array.from(text.slice(run.start, run.end)).some(isHyphen) &&
92
92
  placeChainBefore(tokens, base + run.start).some((token) => isGeoUnit(token) && !topUnits.has(token.surface));
93
93
 
94
94
  /** ハイフンでつないだ識別子か、番号の立つ位置の番号か、文頭の節番号か。 */
@@ -0,0 +1,17 @@
1
+ import { plainSource } from "./plain-source.ts";
2
+
3
+ const FRONT_MATTER_FENCE = "---";
4
+ /** An ATX heading; four spaces of indent make it a code line instead. */
5
+ const HEADING = /^ {0,3}#{1,6}(?:\s|$)/u;
6
+
7
+ /** The lines after a leading front matter block. */
8
+ const bodyLines = (lines: readonly string[]): readonly string[] => {
9
+ const close = lines[0] === FRONT_MATTER_FENCE ? lines.indexOf(FRONT_MATTER_FENCE, 1) : -1;
10
+ return close === -1 ? lines : lines.slice(close + 1);
11
+ };
12
+
13
+ /** The line a document's body opens with (a letter's salutation), past front matter, blank lines and Markdown headings. */
14
+ export const openingLine = (source: string): string | undefined =>
15
+ bodyLines(plainSource(source).split("\n"))
16
+ .find((line) => line.trim() !== "" && !HEADING.test(line))
17
+ ?.trim();
@@ -2,6 +2,14 @@
2
2
 
3
3
  export const escapeRegExp = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
4
4
 
5
+ /**
6
+ * 字に挟まれた ’ と、どこにあっても ʼ を ' にする(don’t → don't)。語彙表は ' で書くので、’ で書いた本文も同じ語に当てるため。
7
+ * 英語アダプタの解析器が同じ決めかたをする(chaff とアダプタは実行時の値を共有しないので、ここにも置く)。一字を一字に置き換えるので位置は変わらない。
8
+ */
9
+ const APOSTROPHE = /(?<=[\p{L}\p{N}])’(?=\p{L})|ʼ/gu;
10
+
11
+ export const straightApostrophes = (text: string): string => text.replace(APOSTROPHE, "'");
12
+
5
13
  /** text の中で word が現れる [始まり, 終わり) を、左から順に。大文字小文字を区別しないときは i で探す(位置は元の文字列のまま)。 */
6
14
  const spansOf = (text: string, word: string, ignoreCase: boolean): [number, number][] => {
7
15
  const pattern = new RegExp(escapeRegExp(word), ignoreCase ? "giu" : "gu");
@@ -55,7 +63,7 @@ const kindOf = (char: string | undefined, digitSide: "before-digit" | "after-dig
55
63
  return DIGIT.test(char) ? digitSide : undefined;
56
64
  };
57
65
 
58
- const isJapanese = (char: string | undefined): boolean => char !== undefined && JAPANESE.test(char);
66
+ export const isJapanese = (char: string | undefined): boolean => char !== undefined && JAPANESE.test(char);
59
67
 
60
68
  /** japanese が日本語の字のとき、other の英字・数字の種類。 */
61
69
  const latinBeside = (japanese: string | undefined, other: string | undefined, digitSide: "before-digit" | "after-digit"): SpacingKind | undefined =>
@@ -189,7 +197,7 @@ export const latinBoundaries = (text: string, written: string = text): Boundary[
189
197
  * 混ざっているとき、そろえるべき少数派。種類(英字・数字の前・数字の後ろ)ごとに数える。
190
198
  * 同数なら、文書が先に使った書き方をその文書の書き方とみなし、後から出た書き方を少数派にする。
191
199
  */
192
- export const minorityStyle = (boundaries: readonly Boundary[]): boolean | undefined => {
200
+ export const minorityStyle = (boundaries: readonly { readonly spaced: boolean }[]): boolean | undefined => {
193
201
  const spaced = boundaries.filter((boundary) => boundary.spaced).length;
194
202
  const touching = boundaries.length - spaced;
195
203
  if (spaced === 0 || touching === 0) return undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 段落が長すぎるか。文の数が上限を超え、しかも長さが「上限の数だけ普通の長さの文を並べた長さ」を超えるときだけ。
3
+ * 短い文をいくつ並べても、読み手が息をつけない壁にはならない。長さは adapter の単位(日本語は文字、英語は語)。
4
+ * fullSentence が無ければ、文の数だけで決める。
5
+ */
6
+ export const isTooLongParagraph = (sentenceSizes: readonly number[], limit: number, fullSentence: number | undefined): boolean => {
7
+ const size = sentenceSizes.reduce((sum, sentence) => sum + sentence, 0);
8
+ return sentenceSizes.length > limit && (fullSentence === undefined || size > limit * fullSentence);
9
+ };
package/src/plugin.ts CHANGED
@@ -204,6 +204,8 @@ export type Section = {
204
204
  /** 見出しの深さ。見出しより前の導入部は 0。 */
205
205
  readonly depth: number;
206
206
  readonly heading: string;
207
+ /** 見出しを文と同じ解析器で語に分けたもの。文が tokens を持つときだけ入る。 */
208
+ readonly headingTokens?: readonly Token[];
207
209
  readonly span: Span;
208
210
  readonly sentences: readonly Sentence[];
209
211
  /** この節に含まれる強調(Markdown の ** **)の数。 */
@@ -314,6 +316,8 @@ export type ProseDocument = {
314
316
  readonly lexicons: Readonly<Record<string, Lexicon>>;
315
317
  /** この種類の文書に無いと困る見出し。チームが chaff.yaml で決める。 */
316
318
  readonly requiredSections: readonly string[];
319
+ /** チームが chaff.yaml の names に並べた固有名詞。1 つの名前として読み、書き手の落ち度に数えない。 */
320
+ readonly names?: readonly string[];
317
321
  /** 番地の付いた木(§27)。adapter が structure を持たない言語では無い。 */
318
322
  readonly structure: StructureNode | undefined;
319
323
  /** 文書の種類(法令など)。選ばれなければ無い。 */
@@ -333,6 +337,8 @@ export type Finding = {
333
337
  readonly quote: string;
334
338
  /** message のプレースホルダに入れる値。 */
335
339
  readonly values: Readonly<Record<string, string | number>>;
340
+ /** 1 つの rule が 2 通りの見つけ方を持つとき、どちらで見つけたか。rule の messages から文を選ぶ。無ければ message。 */
341
+ readonly variant?: string;
336
342
  };
337
343
 
338
344
  /** rule 定義が threshold を渡す。数値は 4 語から解決済み。 */
@@ -342,6 +348,8 @@ export type DetectorOptions = {
342
348
  readonly lexicon?: Lexicon | undefined;
343
349
  /** 見る範囲。opening は冒頭 2 段落、closing は最後の節、whole は全体。 */
344
350
  readonly where?: string | undefined;
351
+ /** 普通の長さの文 1 つの長さ(adapter の単位)。段落の長さを文の数と一緒に見る rule だけが持つ。 */
352
+ readonly fullSentence?: number | undefined;
345
353
  };
346
354
 
347
355
  export type Detector = (doc: ProseDocument, options: DetectorOptions) => Finding[];
@@ -360,6 +368,8 @@ export type RuleDefinition = {
360
368
  readonly why: Localized;
361
369
  readonly how_to_fix: Localized;
362
370
  readonly message: Localized;
371
+ /** 見つけ方ごとの message(Finding の variant で引く)。ほとんどの rule は持たない。 */
372
+ readonly messages: Readonly<Record<string, Localized>>;
363
373
  /** 4 語と数値の対応。2 つ以上。未定義の段は normal に落ちる。spec §18.1。
364
374
  * 言語別の閾値を持つ rule(max-sentence-length)は、読み込み時に言語で平坦化済み。 */
365
375
  readonly levels: LevelTable;
@@ -372,6 +382,8 @@ export type RuleDefinition = {
372
382
  /** L4 のみ。LLM に渡す決まり。言語別。 */
373
383
  readonly what_to_check: Localized | undefined;
374
384
  readonly where: string | undefined;
385
+ /** 普通の長さの文 1 つの長さ。言語で単位が違うので、levels と同じく読み込み時に言語で平坦化済み。 */
386
+ readonly full_sentence: number | undefined;
375
387
  /** adapter に要る capability。"pos" / "lemma"。満たさなければ動かさない。spec §16。 */
376
388
  readonly requires: readonly string[];
377
389
  /** 使えるなら用意してほしい capability("pos")。requires と違い、満たせなくても動かす(品詞が無ければ文字だけで見る)。 */
@@ -0,0 +1,34 @@
1
+ import { NO_PROFILE } from "./select.ts";
2
+
3
+ /** Where a profile was set: chaff.yaml's profile, or one of its by_path entries. */
4
+ export type ProfileSetting = "config" | "by_path";
5
+
6
+ type UnknownProfile = {
7
+ readonly where: ProfileSetting;
8
+ readonly profile: string;
9
+ /** The by_path entry's files, so the message can say which entry; empty for chaff.yaml's profile. */
10
+ readonly files: readonly string[];
11
+ };
12
+
13
+ type PathProfile = { readonly files: readonly string[]; readonly profile?: string | undefined };
14
+
15
+ type WrittenProfiles = { readonly config: string | undefined; readonly byPath: readonly PathProfile[] };
16
+
17
+ /** Commands that never read a document, so never choose a profile. A wrong one in chaff.yaml must not stop them. */
18
+ const PROFILE_FREE: ReadonlySet<string> = new Set(["init", "genres", "skill", "relax", "strict", "off", "rules", "explain"]);
19
+
20
+ /** The profiles a command would read from chaff.yaml: none for a command that reads no document. */
21
+ export const writtenProfiles = (command: string, config: { readonly profile: string | undefined; readonly byPath: readonly PathProfile[] }): WrittenProfiles =>
22
+ PROFILE_FREE.has(command) ? { config: undefined, byPath: [] } : { config: config.profile, byPath: config.byPath };
23
+
24
+ const setting = (where: ProfileSetting, profile: string | undefined, files: readonly string[] = []): UnknownProfile[] =>
25
+ profile === undefined ? [] : [{ where, profile, files }];
26
+
27
+ /**
28
+ * Every profile a setting names that is neither a bundled one nor none. Such a name chooses nothing and also stops the choice from the content,
29
+ * so it silently acts as none. All of them, not only the one a file would use, because a wrong setting is wrong for every file.
30
+ */
31
+ export const unknownProfiles = (written: WrittenProfiles, known: readonly string[]): UnknownProfile[] =>
32
+ [...setting("config", written.config), ...written.byPath.flatMap((entry) => setting("by_path", entry.profile, entry.files))].filter(
33
+ (entry) => entry.profile !== NO_PROFILE && !known.includes(entry.profile),
34
+ );
@@ -1,11 +1,18 @@
1
1
  import { applyByPath } from "../config/by-path.ts";
2
2
  import type { Config } from "../config/load.ts";
3
3
  import type { DocumentProfile } from "../plugin.ts";
4
+ import { presetProfile } from "../genre-load.ts";
4
5
  import { loadProfiles } from "./load.ts";
5
6
  import { chooseProfile } from "./select.ts";
6
7
 
7
8
  type ProfileSettings = Pick<Config, "profile" | "byPath" | "baseDir">;
8
9
 
9
- /** 1 ファイルの文書の種類。by_path、chaff.yaml、内容の順に選ぶ。 */
10
- export const profileFor = (config: ProfileSettings, path: string, source: string, language: string): DocumentProfile | undefined =>
11
- chooseProfile(loadProfiles(), { byPath: applyByPath(config.byPath, config.baseDir, path).profile, config: config.profile, source, language })?.profile;
10
+ /** 1 ファイルの文書の種類。by_path、chaff.yaml、ジャンル、内容の順に選ぶ。 */
11
+ export const profileFor = (config: ProfileSettings, path: string, source: string, language: string, genre?: string): DocumentProfile | undefined =>
12
+ chooseProfile(loadProfiles(), {
13
+ byPath: applyByPath(config.byPath, config.baseDir, path).profile,
14
+ config: config.profile,
15
+ genre: genre === undefined ? undefined : presetProfile(genre),
16
+ source,
17
+ language,
18
+ })?.profile;
@@ -12,7 +12,7 @@ const loaded: { value: readonly ProfileDefinition[] | undefined } = { value: und
12
12
  export const loadProfiles = (): readonly ProfileDefinition[] => {
13
13
  loaded.value ??= readdirSync(PROFILES_DIR)
14
14
  .filter((file) => file.endsWith(".yaml"))
15
- .sort((left, right) => left.localeCompare(right))
15
+ .toSorted((left, right) => left.localeCompare(right))
16
16
  .flatMap((file) => {
17
17
  const path = join(PROFILES_DIR, file);
18
18
  try {
@@ -4,13 +4,15 @@ import { detected, type ProfileDefinition } from "./parse.ts";
4
4
  /** 設定で書いた「使わない」。内容から選ぶのも止める。 */
5
5
  export const NO_PROFILE = "none";
6
6
 
7
- type ProfileSource = "by-path" | "config" | "content";
7
+ type ProfileSource = "by-path" | "config" | "genre" | "content";
8
8
 
9
9
  export type ChosenProfile = { readonly profile: DocumentProfile; readonly from: ProfileSource };
10
10
 
11
11
  export type ProfileRequest = {
12
12
  readonly byPath: string | undefined;
13
13
  readonly config: string | undefined;
14
+ /** The profile the file's genre reads with (genres.yaml), when it names one. */
15
+ readonly genre?: string | undefined;
14
16
  readonly source: string;
15
17
  readonly language: string;
16
18
  };
@@ -18,16 +20,21 @@ export type ProfileRequest = {
18
20
  const named = (definitions: readonly ProfileDefinition[], id: string, language: string): DocumentProfile | undefined =>
19
21
  definitions.find((definition) => definition.id === id)?.languages[language];
20
22
 
23
+ const writtenIn = (request: ProfileRequest): ProfileSource => {
24
+ if (request.byPath !== undefined) return "by-path";
25
+ return request.config === undefined ? "genre" : "config";
26
+ };
27
+
21
28
  /**
22
- * 強いものから: by_path、chaff.yaml の profile、内容。どれかが none と書けばそこで止め、内容からも選ばない。
29
+ * 強いものから: by_path、chaff.yaml の profile、ジャンルの profile、内容。どれかが none と書けばそこで止め、内容からも選ばない。
23
30
  * 名前で選んだ種類にその言語の中身が無ければ選ばない。黙って別の種類にしない。
24
31
  */
25
32
  export const chooseProfile = (definitions: readonly ProfileDefinition[], request: ProfileRequest): ChosenProfile | undefined => {
26
- const written = request.byPath ?? request.config;
33
+ const written = request.byPath ?? request.config ?? request.genre;
27
34
  if (written === NO_PROFILE) return undefined;
28
35
  if (written !== undefined) {
29
36
  const profile = named(definitions, written, request.language);
30
- return profile === undefined ? undefined : { profile, from: request.byPath === undefined ? "config" : "by-path" };
37
+ return profile === undefined ? undefined : { profile, from: writtenIn(request) };
31
38
  }
32
39
  const found = definitions.find((definition) => detected(definition, request.source, request.language));
33
40
  const profile = found?.languages[request.language];
@@ -1,25 +1,39 @@
1
- import type { Span } from "./plugin.ts";
1
+ import type { Sentence, Span } from "./plugin.ts";
2
2
 
3
- /** 鉤括弧の開きと、それを閉じる字。 */
4
- const CLOSER: Readonly<Record<string, string>> = { "「": "」", "『": "』" };
3
+ /** 開きの字と、それを閉じる字。 */
4
+ type Marks = Readonly<Record<string, string>>;
5
+
6
+ /** 鉤括弧。日本語の引用と題名。 */
7
+ const KAGI_MARKS: Marks = { "「": "」", "『": "』" };
8
+
9
+ /**
10
+ * 引用符も含めた、人の言葉を引く印。英語の "…" は開きと閉じが同じ字なので、開いている間に来たら閉じと読む。
11
+ * 一重の ’ は "don’t" の省略の印と見分けられないので入れない。
12
+ */
13
+ export const QUOTATION_MARKS: Marks = { ...KAGI_MARKS, "“": "”", '"': '"' };
5
14
 
6
15
  type Open = { readonly closer: string; readonly at: number };
7
16
  type Scan = { readonly open: readonly Open[]; readonly spans: readonly Span[] };
8
17
 
9
- const step = (scan: Scan, char: string, at: number): Scan => {
10
- const closer = CLOSER[char];
11
- if (closer !== undefined) return { open: [...scan.open, { closer, at }], spans: scan.spans };
12
- const last = scan.open.at(-1);
13
- if (last === undefined || last.closer !== char) return scan;
14
- return { open: scan.open.slice(0, -1), spans: [...scan.spans, { start: last.at + 1, end: at }] };
15
- };
18
+ const step =
19
+ (marks: Marks) =>
20
+ (scan: Scan, char: string, at: number): Scan => {
21
+ const last = scan.open.at(-1);
22
+ if (last !== undefined && last.closer === char) return { open: scan.open.slice(0, -1), spans: [...scan.spans, { start: last.at + 1, end: at }] };
23
+ const closer = marks[char];
24
+ return closer === undefined ? scan : { open: [...scan.open, { closer, at }], spans: scan.spans };
25
+ };
16
26
 
17
27
  /**
18
- * 鉤括弧(「」『』)の中身の位置。括弧そのものは含まない。入れ子はそれぞれを返す。
28
+ * 引いたものの中身の位置(既定は鉤括弧「」『』)。括弧そのものは含まない。入れ子はそれぞれを返す。
19
29
  * 閉じない括弧と、開きと組にならない閉じ括弧は中身を作らない。位置は text の先頭を 0 とする。
20
30
  * 括弧は BMP の字なので、UTF-16 の 1 単位ずつ見れば位置がそのまま使える。
21
31
  */
22
- export const quotedSpans = (text: string): Span[] => [...text.split("").reduce<Scan>((scan, char, at) => step(scan, char, at), { open: [], spans: [] }).spans];
32
+ export const quotedSpans = (text: string, marks: Marks = KAGI_MARKS): Span[] => [...text.split("").reduce<Scan>(step(marks), { open: [], spans: [] }).spans];
33
+
34
+ /** 文の中の、引いたものを括弧ごと、文書全体の座標で。 */
35
+ export const quotedIn = (sentence: Sentence, marks: Marks = KAGI_MARKS): Span[] =>
36
+ quotedSpans(sentence.text, marks).map((span) => ({ start: sentence.span.start + span.start - 1, end: sentence.span.start + span.end + 1 }));
23
37
 
24
38
  /** inner が、spans のどれかにまるごと入っているか。 */
25
39
  export const isWithinAny = (spans: readonly Span[], inner: Span): boolean => spans.some((span) => inner.start >= span.start && inner.end <= span.end);
package/src/reasons.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Texts } from "./ui.ts";
2
+ import { formFor } from "./render/plural.ts";
2
3
 
3
4
  /** Why a rule did not run, in the document's language. Listed under the findings so "0 findings" is never a silent pass. */
4
5
  export type Reasons = {
@@ -8,6 +9,7 @@ export type Reasons = {
8
9
  readonly semantic: string;
9
10
  readonly experimental: string;
10
11
  readonly turnedOff: string;
12
+ readonly presetOff: (genre: string) => string;
11
13
  readonly noStructure: (language: string) => string;
12
14
  readonly unreadStructure: (clauses: number, units: number) => string;
13
15
  readonly noDetector: (name: string) => string;
@@ -27,6 +29,7 @@ export const REASONS: Texts<Reasons> = {
27
29
  semantic: "意味を読む検査のため(npx chaff test で動きます)",
28
30
  experimental: "まだ試験中のため",
29
31
  turnedOff: "設定で止めているため",
32
+ presetOff: (genre) => `ジャンル ${genre} では見ないため`,
30
33
  noStructure: (language) => `${language} のパッケージは文書の構造を読めないため`,
31
34
  unreadStructure: (clauses, units) =>
32
35
  `条項の番号が本文に ${String(clauses)} 個あるのに、番号として読めたのは ${String(units)} 個のため(深い字下げや、行が本文につながった文書)`,
@@ -40,9 +43,10 @@ export const REASONS: Texts<Reasons> = {
40
43
  semantic: "it reads meaning; npx chaff test runs it",
41
44
  experimental: "still experimental",
42
45
  turnedOff: "turned off in the settings",
46
+ presetOff: (genre) => `the ${genre} genre does not check it`,
43
47
  noStructure: (language) => `the ${language} package cannot read a document's structure`,
44
48
  unreadStructure: (clauses, units) =>
45
- `the text has ${String(clauses)} clause numbers but only ${String(units)} were read as numbered lines (deep indents, or lines run into the text)`,
49
+ `the text has ${String(clauses)} clause numbers but only ${String(units)} ${formFor(units, "was read as a numbered line", "were read as numbered lines")} (deep indents, or lines run into the text)`,
46
50
  noDetector: (name) => `no detector named ${name}`,
47
51
  noLexicon: (language, list) => `the ${language} package has no word list ${list}`,
48
52
  },
@@ -1,11 +1,9 @@
1
1
  import type { RuleDefinition } from "../plugin.ts";
2
2
  import type { RunResult } from "../run.ts";
3
3
  import { messageOf } from "./text.ts";
4
+ import { counted } from "./plural.ts";
4
5
  import { uiLanguageOf, type Texts } from "../ui.ts";
5
6
 
6
- /** 「1 finding」「2 findings」。 */
7
- const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
8
-
9
7
  /**
10
8
  * 最後の集計の行は、画面のほかの部分と同じく文書の言語で出す。行ごとの重さ(warning / error)は、grep や別の道具が読むので英語のまま。
11
9
  */
@@ -1,5 +1,6 @@
1
1
  import { MIN_CORPUS, TARGET_HIT_RATE, type Point, type RuleReport } from "../eval.ts";
2
2
  import type { Texts, UiLanguage } from "../ui.ts";
3
+ import { counted, formFor } from "./plural.ts";
3
4
 
4
5
  const pct = (part: number, whole: number): string => `${((part / Math.max(1, whole)) * 100).toFixed(1)}%`;
5
6
 
@@ -51,19 +52,19 @@ const TEXT: Texts<{
51
52
  recommended: " ← recommended",
52
53
  both: " ← current / recommended",
53
54
  point: (point, share) =>
54
- `${String(point.documents).padStart(3)} ${point.documents === 1 ? "doc " : "docs"} (${share.padStart(6)}) ${String(point.findings).padStart(3)} ${point.findings === 1 ? "finding " : "findings"} ${point.per10k.toFixed(1)} per 10,000 characters`,
55
+ `${String(point.documents).padStart(3)} ${formFor(point.documents, "doc ", "docs")} (${share.padStart(6)}) ${String(point.findings).padStart(3)} ${formFor(point.findings, "finding ", "findings")} ${point.per10k.toFixed(1)} per 10,000 characters`,
55
56
  fits: ` The current limit meets the target (under ${TARGET}%).`,
56
57
  nothingFits: " No limit meets the target. The rule itself may not suit this corpus.",
57
58
  over: (share) => ` At the current limit ${share} of the documents are hit, over the target (under ${TARGET}%).`,
58
59
  recommend: (limit) => ` Recommended: ${limit}`,
59
60
  toConfig: (rule, limit) => `${EN_CONFIG_LEAD}rules:\n${" ".repeat(EN_CONFIG_LEAD.length + YAML_INDENT)}${rule}: ${limit}`,
60
- measured: (paths, rules) => ` Measured ${rules} rule${rules === 1 ? "" : "s"} on ${paths} file${paths === 1 ? "" : "s"} as a corpus.`,
61
+ measured: (paths, rules) => ` Measured ${counted(rules, "rule")} on ${counted(paths, "file")} as a corpus.`,
61
62
  premise: [
62
63
  " The documents here are taken as good documents that people wrote and published.",
63
64
  ` A rule that fires often on them may have a limit that does not fit real writing (target: under ${TARGET}%).`,
64
65
  ],
65
66
  smallCorpus: (total) => [
66
- ` Note: the corpus has only ${total} document${total === 1 ? "" : "s"}. Showing ${TARGET}% takes at least ${MIN_CORPUS}.`,
67
+ ` Note: the corpus has only ${counted(total, "document")}. Showing ${TARGET}% takes at least ${MIN_CORPUS}.`,
67
68
  " For now each share is either 0 or all, so read the density on the right (findings per 10,000 characters).",
68
69
  ],
69
70
  untouched: " No limit was changed. A different corpus gives a different answer.",
@@ -2,6 +2,7 @@ import type { Finding, RuleDefinition } from "../plugin.ts";
2
2
  import type { RunResult } from "../run.ts";
3
3
  import { MARK, localized, messageOf } from "./text.ts";
4
4
  import { tally } from "./summary.ts";
5
+ import { counted } from "./plural.ts";
5
6
  import { uiLanguageOf, type Texts } from "../ui.ts";
6
7
 
7
8
  const RULE = 60;
@@ -13,6 +14,7 @@ const TEXT: Texts<{
13
14
  readonly deterministic: string;
14
15
  readonly untouched: string;
15
16
  readonly forced: (n: number, ids: string) => string;
17
+ readonly presetOn: (n: number, ids: string) => string;
16
18
  readonly notRun: (n: number) => string;
17
19
  readonly because: (why: string) => string;
18
20
  }> = {
@@ -23,6 +25,7 @@ const TEXT: Texts<{
23
25
  deterministic: " (同じ文章なら何度実行しても同じ結果になります)",
24
26
  untouched: "文章は書き換えていません。直すのは書いた人です。",
25
27
  forced: (n, ids) => `試験中の rule を ${n} 件、設定により有効にしています: ${ids}`,
28
+ presetOn: (n, ids) => `試験中の rule を ${n} 件、ジャンルの既定で有効にしています: ${ids}`,
26
29
  notRun: (n) => `${n} 件の rule は動いていません:`,
27
30
  because: (why) => `(${why})`,
28
31
  },
@@ -32,8 +35,9 @@ const TEXT: Texts<{
32
35
  machine: "All judged by machine",
33
36
  deterministic: " (the same text gives the same result every time)",
34
37
  untouched: "The text was not changed. Fixing it is the writer's job.",
35
- forced: (n, ids) => `${n} experimental rule${n === 1 ? "" : "s"} turned on in the settings: ${ids}`,
36
- notRun: (n) => `${n} rule${n === 1 ? "" : "s"} did not run:`,
38
+ forced: (n, ids) => `${counted(n, "experimental rule")} turned on in the settings: ${ids}`,
39
+ presetOn: (n, ids) => `${counted(n, "experimental rule")} turned on by the genre: ${ids}`,
40
+ notRun: (n) => `${counted(n, "rule")} did not run:`,
37
41
  because: (why) => ` (${why})`,
38
42
  },
39
43
  };
@@ -69,14 +73,21 @@ const block = (finding: Finding, rule: RuleDefinition, language: string): string
69
73
  "",
70
74
  ];
71
75
 
72
- export const renderFriendly = (header: string, result: RunResult, rules: readonly RuleDefinition[], language: string): string => {
76
+ /** notes: lines after the not-run list (the genre chaff suggests when none is set). */
77
+ export const renderFriendly = (
78
+ header: string,
79
+ result: RunResult,
80
+ rules: readonly RuleDefinition[],
81
+ language: string,
82
+ notes: readonly string[] = [],
83
+ ): string => {
73
84
  const byId = new Map(rules.map((rule) => [rule.id, rule]));
74
85
  const blocks = result.findings.flatMap((finding) => {
75
86
  const rule = byId.get(finding.rule);
76
87
  return rule === undefined ? [] : block(finding, rule, language);
77
88
  });
78
89
  const text = TEXT[uiLanguageOf(language)];
79
- const notes = [
90
+ const closing = [
80
91
  "",
81
92
  "─".repeat(RULE),
82
93
  "",
@@ -85,10 +96,14 @@ export const renderFriendly = (header: string, result: RunResult, rules: readonl
85
96
  "",
86
97
  ` ${text.untouched}`,
87
98
  ];
88
- const forced = result.forcedExperimental.length > 0 ? ["", ` ${text.forced(result.forcedExperimental.length, result.forcedExperimental.join(", "))}`] : [];
99
+ const forced = [
100
+ ...(result.forcedExperimental.length > 0 ? ["", ` ${text.forced(result.forcedExperimental.length, result.forcedExperimental.join(", "))}`] : []),
101
+ ...(result.presetExperimental.length > 0 ? ["", ` ${text.presetOn(result.presetExperimental.length, result.presetExperimental.join(", "))}`] : []),
102
+ ];
89
103
  const skipped =
90
104
  result.skipped.length > 0
91
105
  ? ["", ` ${text.notRun(result.skipped.length)}`, ...result.skipped.map((entry) => ` ${entry.rule}${text.because(entry.why)}`)]
92
106
  : [];
93
- return ["", header, ...blocks, ...notes, ...forced, ...skipped, ""].join("\n");
107
+ const after = notes.flatMap((note) => ["", note]);
108
+ return ["", header, ...blocks, ...closing, ...forced, ...skipped, ...after, ""].join("\n");
94
109
  };
@@ -0,0 +1,31 @@
1
+ import type { GenreData } from "../genre-parse.ts";
2
+ import type { Texts, UiLanguage } from "../ui.ts";
3
+
4
+ const TEXT: Texts<{ readonly heading: string; readonly usage: readonly string[] }> = {
5
+ ja: {
6
+ heading: " ジャンル(文書の種類)を選ぶと、その種類の書き方に合わせて見ます:",
7
+ usage: [
8
+ " この実行だけ: npx chaffjs --genre legal/contract 契約書.md",
9
+ " この場所に決める: npx chaffjs init --genre legal/contract (chaff.yaml の genre に書きます)",
10
+ ],
11
+ },
12
+ en: {
13
+ heading: " Pick the genre (the kind of document) and chaff checks it the way that kind is written:",
14
+ usage: [
15
+ " For one run: npx chaffjs --genre legal/contract contract.md",
16
+ " For this folder: npx chaffjs init --genre legal/contract (writes genre in chaff.yaml)",
17
+ ],
18
+ },
19
+ };
20
+
21
+ /** `chaff genres`: every genre under its group, with what it is for, in the output language. */
22
+ export const renderGenres = (data: GenreData, ui: UiLanguage): string => {
23
+ const width = Math.max(...data.genres.map((genre) => genre.id.length)) + 2;
24
+ const groups = data.groups.flatMap((group) => {
25
+ const members = data.genres.filter((genre) => genre.id.startsWith(`${group.id}/`));
26
+ return members.length === 0
27
+ ? []
28
+ : ["", ` ${group.name[ui] ?? group.id}`, ...members.map((genre) => ` ${genre.id.padEnd(width)}${genre.summary[ui] ?? ""}`)];
29
+ });
30
+ return ["", TEXT[ui].heading, ...groups, "", ...TEXT[ui].usage, ""].join("\n");
31
+ };
@@ -0,0 +1,6 @@
1
+ /** English agreement: only exactly one takes the singular ("0 findings", "1 finding", "1.5 findings"). */
2
+ const isOne = (count: number | string): boolean => String(count) === "1";
3
+
4
+ export const formFor = (count: number | string, singular: string, plural: string): string => (isOne(count) ? singular : plural);
5
+
6
+ export const counted = (count: number, singular: string, plural = `${singular}s`): string => `${String(count)} ${formFor(count, singular, plural)}`;