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
@@ -2,9 +2,12 @@ import { definedLevels, resolve } from "../levels.ts";
2
2
  import type { Config } from "../config/load.ts";
3
3
  import type { RuleDefinition } from "../plugin.ts";
4
4
  import { uiLanguageOf, type Texts } from "../ui.ts";
5
+ import { presetLevels } from "../genre-load.ts";
6
+ import type { PresetLevels } from "../genre-parse.ts";
5
7
 
6
8
  const TEXT: Texts<{
7
9
  readonly offBySetting: string;
10
+ readonly offByGenre: (genre: string) => string;
8
11
  readonly offExperimental: string;
9
12
  readonly valuesNote: string;
10
13
  readonly reason: string;
@@ -12,6 +15,7 @@ const TEXT: Texts<{
12
15
  }> = {
13
16
  ja: {
14
17
  offBySetting: "設定で止めている",
18
+ offByGenre: (genre) => `ジャンル ${genre} では見ない`,
15
19
  offExperimental: "experimental な rule は既定で動かさない",
16
20
  valuesNote: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
17
21
  reason: "<理由>",
@@ -19,6 +23,7 @@ const TEXT: Texts<{
19
23
  },
20
24
  en: {
21
25
  offBySetting: "turned off in the settings",
26
+ offByGenre: (genre) => `the ${genre} genre does not check it`,
22
27
  offExperimental: "experimental rules do not run by default",
23
28
  valuesNote: "A number may be written instead of these four words. The words are recommended.",
24
29
  reason: "<reason>",
@@ -26,8 +31,9 @@ const TEXT: Texts<{
26
31
  },
27
32
  };
28
33
 
29
- const now = (rule: RuleDefinition, config: Config, genre: string, text: (typeof TEXT)["ja"]): Record<string, unknown> => {
30
- const explicit = config.rules[rule.id];
34
+ const now = (rule: RuleDefinition, config: Config, genre: string, text: (typeof TEXT)["ja"], preset: PresetLevels): Record<string, unknown> => {
35
+ const explicit = config.rules[rule.id] ?? preset[rule.id];
36
+ if (explicit === "off" && config.rules[rule.id] === undefined) return { level: "off", why_off: text.offByGenre(genre) };
31
37
  if (explicit === "off") return { level: "off", why_off: text.offBySetting };
32
38
  const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
33
39
  if (limit !== undefined) return { level: "normal", limit, set_as: "number" };
@@ -59,6 +65,7 @@ const levelsOf = (rule: RuleDefinition, genre: string): Record<string, unknown>
59
65
  /** AI に設定を書かせるときの入口。推測せずに書けるだけの情報を 1 つに入れる。spec §19.3。 */
60
66
  export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string => {
61
67
  const text = TEXT[uiLanguageOf(language)];
68
+ const preset = presetLevels(genre);
62
69
  return JSON.stringify(
63
70
  {
64
71
  schema_version: 1,
@@ -77,7 +84,7 @@ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, lang
77
84
  ...levelsOf(rule, genre),
78
85
  levels_you_can_set: definedLevels(rule),
79
86
  your_setting: yourSetting(rule, config),
80
- now: now(rule, config, genre, text),
87
+ now: now(rule, config, genre, text, preset),
81
88
  })),
82
89
  how_to_change: {
83
90
  by_command: ["relax", "strict", "off"].map((command) => `npx chaff ${command} <rule-id> --why "${text.reason}"`),
@@ -1,35 +1,113 @@
1
1
  import type { Finding, RuleDefinition } from "../plugin.ts";
2
2
  import type { UserCheck } from "../checks.ts";
3
+ import type { Narrowing } from "../look-at.ts";
3
4
  import type { Job as Plan, SemanticResult } from "../run-semantic.ts";
5
+ import { uiLanguageOf, type Texts, type UiLanguage } from "../ui.ts";
4
6
  import { MARK, localized } from "./text.ts";
7
+ import { counted, formFor } from "./plural.ts";
5
8
 
6
9
  const RULE = 60;
7
10
 
11
+ const TEXT: Texts<{
12
+ readonly machineBanner: readonly string[];
13
+ readonly aiTitle: string;
14
+ readonly aiNote: readonly string[];
15
+ readonly read: (asked: number, seen: number) => string;
16
+ readonly wholeRead: (name: string) => readonly string[];
17
+ readonly word: (word: string) => string;
18
+ readonly number: string;
19
+ readonly narrowedRead: (name: string, what: string, kept: number, total: number) => string;
20
+ readonly line: (line: number) => string;
21
+ readonly confidence: (value: string) => string;
22
+ readonly disagree: readonly string[];
23
+ readonly silence: (rule: string) => string;
24
+ readonly relax: (rule: string) => string;
25
+ readonly notRunChecks: (count: number) => string;
26
+ readonly byMachine: string;
27
+ readonly whole: string;
28
+ readonly containing: (parts: string) => string;
29
+ readonly planLine: (count: number, name: string, how: string) => string;
30
+ readonly firstSample: string;
31
+ readonly planHead: (path: string, sentences: number, asked: number) => string;
32
+ }> = {
33
+ ja: {
34
+ machineBanner: ["", `═══ 機械による判定 ${"═".repeat(RULE - 18)}`, " 同じ文章なら何度実行しても同じ結果になります", ""],
35
+ aiTitle: `═══ AI による判定 ${"═".repeat(RULE - 17)}`,
36
+ aiNote: [" 文章の意味を読んでいます。実行するたび結果が変わることが", " あります。おかしいと思ったら、そのまま無視して構いません。"],
37
+ read: (asked, seen) => ` ${seen} 文のうち ${asked} 箇所を読みました(残りは機械が対象外と判断)`,
38
+ wholeRead: (name) => [` ${name}: 見るところを絞れず、全文を読みました`, ` look_at に「」で語を書くと、その語を含む文だけになります`],
39
+ word: (word) => `「${word}」`,
40
+ number: "数字",
41
+ narrowedRead: (name, what, kept, total) => ` ${name}: ${what} を含む ${kept} 文だけを読みました(全 ${total} 文)`,
42
+ line: (line) => `${line} 行目`,
43
+ confidence: (value) => `確からしさ ${value}`,
44
+ disagree: [" この指摘が違うと思ったら:"],
45
+ silence: (rule) => ` この箇所だけ黙らせる <!-- stet: ${rule} — 理由 -->`,
46
+ relax: (rule) => ` ルールごとゆるめる npx chaff relax ${rule}`,
47
+ notRunChecks: (count) => ` ${count} 件の検査は、見るところが無いため動いていません。`,
48
+ byMachine: "機械で絞り込み済み",
49
+ whole: "絞り込めず全文",
50
+ containing: (parts) => `${parts} を含む文`,
51
+ planLine: (count, name, how) => ` ${String(count).padStart(3)} 箇所 ${name} (${how})`,
52
+ firstSample: " 最初の 1 件に送る文章:",
53
+ planHead: (path, sentences, asked) => `${path} 全 ${sentences} 文のうち ${asked} 箇所を送ります(API は呼んでいません)`,
54
+ },
55
+ en: {
56
+ machineBanner: ["", `═══ Judged by machine ${"═".repeat(RULE - 21)}`, " The same text gives the same result every time", ""],
57
+ aiTitle: `═══ Judged by AI ${"═".repeat(RULE - 16)}`,
58
+ aiNote: [" These read the meaning of the text. The result can change", " from run to run. If one looks wrong, feel free to ignore it."],
59
+ read: (asked, seen) => ` Read ${counted(asked, "passage")} out of ${counted(seen, "sentence")} (the machine ruled out the rest)`,
60
+ wholeRead: (name) => [
61
+ ` ${name}: could not narrow what to read, so read the whole text`,
62
+ ` Put words in quotes in look_at ("like this") to read only the sentences that contain them`,
63
+ ],
64
+ word: (word) => `"${word}"`,
65
+ number: "a number",
66
+ narrowedRead: (name, what, kept, total) => ` ${name}: read only the ${counted(kept, "sentence")} containing ${what} (of ${total})`,
67
+ line: (line) => `line ${line}`,
68
+ confidence: (value) => `confidence ${value}`,
69
+ disagree: [" If you think this finding is wrong:"],
70
+ silence: (rule) => ` Silence this spot only <!-- stet: ${rule} — reason -->`,
71
+ relax: (rule) => ` Relax the whole rule npx chaff relax ${rule}`,
72
+ notRunChecks: (count) => ` ${counted(count, "check")} did not run: there was nothing for ${formFor(count, "it", "them")} to look at.`,
73
+ byMachine: "narrowed by machine",
74
+ whole: "not narrowed: the whole text",
75
+ containing: (parts) => `sentences containing ${parts}`,
76
+ planLine: (count, name, how) => ` ${String(count).padStart(3)} to send ${name} (${how})`,
77
+ firstSample: " The text sent for the first one:",
78
+ planHead: (path, sentences, asked) => `${path} sends ${counted(asked, "passage")} out of ${counted(sentences, "sentence")} (the API was not called)`,
79
+ },
80
+ };
81
+
8
82
  const indent = (text: string, pad: string): string[] => text.split("\n").map((line) => `${pad}${line}`);
9
83
 
10
- export const MACHINE_BANNER = ["", `═══ 機械による判定 ${"═".repeat(RULE - 18)}`, " 同じ文章なら何度実行しても同じ結果になります", ""];
84
+ /** The banner over a file's machine findings, in the file's language. */
85
+ export const machineBanner = (language: string): readonly string[] => TEXT[uiLanguageOf(language)].machineBanner;
86
+
87
+ /** The words a narrowing looks for, as the screen names them. */
88
+ const lookedFor = (narrowing: Narrowing, language: UiLanguage): string[] => {
89
+ const text = TEXT[language];
90
+ return [...narrowing.words.map((word) => text.word(word)), ...(narrowing.needsNumber ? [text.number] : [])];
91
+ };
11
92
 
12
93
  /**
13
94
  * 自分の書いた検査が、どこまで絞り込めたかを見せる。
14
95
  * 絞り込めないまま全文を読んでいることに気づけないと、遅さの原因が分からない。
15
96
  */
16
- const narrowingLines = (result: SemanticResult): string[] =>
97
+ const narrowingLines = (result: SemanticResult, language: UiLanguage): string[] =>
17
98
  result.narrowed.flatMap(({ name, narrowing }) => {
18
- if (narrowing.words.length === 0 && !narrowing.needsNumber) {
19
- return [` ${name}: 見るところを絞れず、全文を読みました`, ` look_at に「」で語を書くと、その語を含む文だけになります`];
20
- }
21
- const what = [...narrowing.words.map((word) => `「${word}」`), ...(narrowing.needsNumber ? ["数字"] : [])].join(" ");
22
- return [` ${name}: ${what} を含む ${narrowing.kept} 文だけを読みました(全 ${narrowing.total} 文)`];
99
+ if (narrowing.words.length === 0 && !narrowing.needsNumber) return [...TEXT[language].wholeRead(name)];
100
+ const what = lookedFor(narrowing, language).join(language === "ja" ? " " : ", ");
101
+ return [TEXT[language].narrowedRead(name, what, narrowing.kept, narrowing.total)];
23
102
  });
24
103
 
25
- const aiBanner = (result: SemanticResult): string[] => [
104
+ const aiBanner = (result: SemanticResult, language: UiLanguage): string[] => [
26
105
  "",
27
- `═══ AI による判定 ${"═".repeat(RULE - 17)}`,
28
- " 文章の意味を読んでいます。実行するたび結果が変わることが",
29
- " あります。おかしいと思ったら、そのまま無視して構いません。",
106
+ TEXT[language].aiTitle,
107
+ ...TEXT[language].aiNote,
30
108
  "",
31
- ` ${result.sentencesSeen} 文のうち ${result.asked} 箇所を読みました(残りは機械が対象外と判断)`,
32
- ...narrowingLines(result),
109
+ TEXT[language].read(result.asked, result.sentencesSeen),
110
+ ...narrowingLines(result, language),
33
111
  "",
34
112
  ];
35
113
 
@@ -46,32 +124,37 @@ const nameOf = (rule: string, rules: readonly RuleDefinition[], checks: readonly
46
124
  * AI の指摘にだけ確からしさを出し、納得できないときの逃げ道を 2 つ添える。
47
125
  * 揺れる判定だと読む人が知らないと、誤検知に振り回される。
48
126
  */
49
- const block = (finding: Finding, named: Named): string[] => {
127
+ const block = (finding: Finding, named: Named, language: UiLanguage): string[] => {
128
+ const text = TEXT[language];
50
129
  const confidence = finding.values["confidence"];
51
130
  const reason = String(finding.values["reason"] ?? "");
52
- const sure = confidence === undefined ? "" : ` 確からしさ ${String(confidence)}`;
131
+ const sure = confidence === undefined ? "" : ` ${text.confidence(String(confidence))}`;
53
132
  const fix = named.howToFix.length > 0 ? ["", ...indent(`→ ${named.howToFix}`, " ")] : [];
54
133
  return [
55
134
  "",
56
- `─── ${finding.line} 行目 ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
135
+ // "12 行目" and "line 12" take the same width on a terminal, so the rule ends in the same column in both.
136
+ `─── ${text.line(finding.line)} ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
57
137
  "",
58
138
  ` ${MARK[finding.severity] ?? "·"} ${named.name}${sure}`,
59
139
  "",
60
140
  ...indent(reason, " "),
61
141
  ...fix,
62
142
  "",
63
- " この指摘が違うと思ったら:",
64
- ` この箇所だけ黙らせる <!-- stet: ${finding.rule} — 理由 -->`,
65
- ` ルールごとゆるめる npx chaff relax ${finding.rule}`,
143
+ ...text.disagree,
144
+ text.silence(finding.rule),
145
+ text.relax(finding.rule),
66
146
  "",
67
147
  ];
68
148
  };
69
149
 
70
- export const renderSemantic = (result: SemanticResult, rules: readonly RuleDefinition[], checks: readonly UserCheck[], language: string): string[] => [
71
- ...aiBanner(result),
72
- ...result.findings.flatMap((finding) => block(finding, nameOf(finding.rule, rules, checks, language))),
73
- ...(result.skipped.length > 0 ? [` ${result.skipped.length} 件の検査は、見るところが無いため動いていません。`, ""] : []),
74
- ];
150
+ export const renderSemantic = (result: SemanticResult, rules: readonly RuleDefinition[], checks: readonly UserCheck[], language: string): string[] => {
151
+ const ui = uiLanguageOf(language);
152
+ return [
153
+ ...aiBanner(result, ui),
154
+ ...result.findings.flatMap((finding) => block(finding, nameOf(finding.rule, rules, checks, language), ui)),
155
+ ...(result.skipped.length > 0 ? [TEXT[ui].notRunChecks(result.skipped.length), ""] : []),
156
+ ];
157
+ };
75
158
 
76
159
  /**
77
160
  * 何が送られるのかを、API を呼ばずに見せる。`chaff test --dry-run`。
@@ -79,19 +162,19 @@ export const renderSemantic = (result: SemanticResult, rules: readonly RuleDefin
79
162
  * 鍵が無い環境でも二段構えの 1 段目まで確かめられる。全文が送られようとしていることに
80
163
  * 請求書で気づくのでは遅い。spec §14。
81
164
  */
82
- const howNarrowed = (job: Plan): string => {
165
+ const howNarrowed = (job: Plan, language: UiLanguage): string => {
83
166
  const narrowing = job.narrowing;
84
- if (narrowing === undefined) return "機械で絞り込み済み";
85
- const quoted = narrowing.words.map((word) => `「${word}」`);
86
- const parts = narrowing.needsNumber ? [...quoted, "数字"] : quoted;
87
- if (parts.length === 0) return "絞り込めず全文";
88
- return `${parts.join(" ")} を含む文`;
167
+ if (narrowing === undefined) return TEXT[language].byMachine;
168
+ const parts = lookedFor(narrowing, language);
169
+ if (parts.length === 0) return TEXT[language].whole;
170
+ return TEXT[language].containing(parts.join(language === "ja" ? " " : ", "));
89
171
  };
90
172
 
91
- export const renderPlan = (path: string, jobs: readonly Plan[], sentences: number): string[] => {
173
+ export const renderPlan = (path: string, jobs: readonly Plan[], sentences: number, language: string): string[] => {
174
+ const text = TEXT[uiLanguageOf(language)];
92
175
  const asked = jobs.reduce((sum, job) => sum + job.candidates.length, 0);
93
- const lines = jobs.map((job) => ` ${String(job.candidates.length).padStart(3)} 箇所 ${job.name} (${howNarrowed(job)})`);
176
+ const lines = jobs.map((job) => text.planLine(job.candidates.length, job.name, howNarrowed(job, uiLanguageOf(language))));
94
177
  const first = jobs.find((job) => job.candidates.length > 0);
95
- const sample = first === undefined ? [] : ["", " 最初の 1 件に送る文章:", "", ...indent(String(first.candidates[0]?.text ?? "").slice(0, 300), " ")];
96
- return ["", `${path} 全 ${sentences} 文のうち ${asked} 箇所を送ります(API は呼んでいません)`, "", ...lines, ...sample, ""];
178
+ const sample = first === undefined ? [] : ["", text.firstSample, "", ...indent(String(first.candidates[0]?.text ?? "").slice(0, 300), " ")];
179
+ return ["", text.planHead(path, sentences, asked), "", ...lines, ...sample, ""];
97
180
  };
@@ -1,5 +1,6 @@
1
1
  import type { Finding } from "../plugin.ts";
2
2
  import type { Texts, UiLanguage } from "../ui.ts";
3
+ import { counted } from "./plural.ts";
3
4
 
4
5
  export type FileOutcome = { readonly path: string; readonly findings: readonly Finding[]; readonly notRun: number };
5
6
 
@@ -21,9 +22,9 @@ const TEXT: Texts<{
21
22
  },
22
23
  en: {
23
24
  counts: {
24
- error: (n) => `${n} error${n === 1 ? "" : "s"}`,
25
- warning: (n) => `${n} warning${n === 1 ? "" : "s"}`,
26
- info: (n) => `${n} note${n === 1 ? "" : "s"}`,
25
+ error: (n) => counted(n, "error"),
26
+ warning: (n) => counted(n, "warning"),
27
+ info: (n) => counted(n, "note"),
27
28
  },
28
29
  join: ", ",
29
30
  none: "No findings",
@@ -1,5 +1,6 @@
1
1
  import type { Suppressed, Suppression } from "../stet.ts";
2
2
  import type { Texts, UiLanguage } from "../ui.ts";
3
+ import { counted } from "./plural.ts";
3
4
 
4
5
  const TEXT: Texts<{
5
6
  readonly nudge: string;
@@ -23,7 +24,7 @@ const TEXT: Texts<{
23
24
  },
24
25
  en: {
25
26
  nudge: " <- consider changing the setting instead",
26
- more: (n) => ` and ${n} more file${n === 1 ? "" : "s"}`,
27
+ more: (n) => ` and ${counted(n, "more file")}`,
27
28
  count: (n) => `${String(n).padStart(3)}`,
28
29
  reasons: (list) => `reasons: ${list}`,
29
30
  relaxAll: (rule) => `relax the whole rule: npx chaff relax ${rule} --why "..."`,
@@ -53,7 +54,7 @@ const groupByRule = (files: readonly PerFile[]): Group[] => {
53
54
  reasons: [...new Set(mine.flatMap((row) => (row.reason === undefined ? [] : [row.reason])))],
54
55
  };
55
56
  })
56
- .sort((left, right) => right.count - left.count);
57
+ .toSorted((left, right) => right.count - left.count);
57
58
  };
58
59
 
59
60
  const groupLines = (group: Group, ui: UiLanguage): string[] => {
@@ -1,13 +1,28 @@
1
1
  import type { Finding, Localized, RuleDefinition } from "../plugin.ts";
2
+ import { formFor } from "./plural.ts";
2
3
 
3
4
  export const localized = (field: Localized, language: string): string => field[language] ?? field["en"] ?? Object.values(field)[0] ?? "";
4
5
 
6
+ /** `{count}` is the value; `{count|word|words}` is the form that agrees with it (English only; Japanese has no plural). */
7
+ const PLACEHOLDER = /\{(\w+)(?:\|([^|{}]*)\|([^|{}]*))?\}/gu;
8
+
5
9
  const fill = (template: string, values: Readonly<Record<string, string | number>>): string =>
6
- template.replace(/\{(\w+)\}/gu, (whole, key: string) => {
10
+ template.replace(PLACEHOLDER, (whole, key: string, singular: string | undefined, plural: string | undefined) => {
7
11
  const value = values[key];
8
- return value === undefined ? whole : String(value);
12
+ if (value === undefined) return whole;
13
+ return singular === undefined || plural === undefined ? String(value) : formFor(value, singular, plural);
9
14
  });
10
15
 
11
- export const messageOf = (rule: RuleDefinition, finding: Finding, language: string): string => fill(localized(rule.message, language), finding.values);
16
+ /** A template as the rule reference shows it: each agreeing form as its plural ("{count} such words"). */
17
+ export const templateForReading = (template: string): string =>
18
+ template.replace(PLACEHOLDER, (whole, _key: string, singular: string | undefined, plural: string | undefined) =>
19
+ singular === undefined || plural === undefined ? whole : plural,
20
+ );
21
+
22
+ const templateOf = (rule: RuleDefinition, finding: Finding): Localized =>
23
+ (finding.variant === undefined ? undefined : rule.messages[finding.variant]) ?? rule.message;
24
+
25
+ export const messageOf = (rule: RuleDefinition, finding: Finding, language: string): string =>
26
+ fill(localized(templateOf(rule, finding), language), finding.values);
12
27
 
13
28
  export const MARK: Readonly<Record<string, string>> = { error: "✖", warning: "⚠", info: "·" };
package/src/rule-load.ts CHANGED
@@ -68,6 +68,10 @@ const missingFields = (raw: Record<string, unknown>, levels: LevelTable | undefi
68
68
 
69
69
  const localizedOf = (value: unknown): Record<string, string> => (isLocalized(value) ? value : {});
70
70
 
71
+ /** 見つけ方ごとの message。読めないものは捨てる。 */
72
+ const messagesOf = (value: unknown): Record<string, Record<string, string>> =>
73
+ isRecord(value) ? Object.fromEntries(Object.entries(value).flatMap(([variant, text]) => (isLocalized(text) ? [[variant, text]] : []))) : {};
74
+
71
75
  /**
72
76
  * severity も言語別に書ける。levels と同じ畳みかた。
73
77
  * 記号の許容度は言語で大きく違う(ダッシュは英語では普通、日本語の組版では扱いが難しい)。
@@ -79,6 +83,13 @@ const severityOf = (raw: unknown, language: string): Severity => {
79
83
  return isSeverity(picked) ? picked : "warning";
80
84
  };
81
85
 
86
+ /** 言語で単位が違う数(日本語は文字、英語は語)。その言語のぶんだけを取る。無い言語では undefined。 */
87
+ const numberFor = (raw: unknown, language: string): number | undefined => {
88
+ if (typeof raw === "number") return raw;
89
+ const picked: unknown = isRecord(raw) ? (raw[language] ?? raw["default"]) : undefined;
90
+ return typeof picked === "number" ? picked : undefined;
91
+ };
92
+
82
93
  const stringList = (value: unknown): string[] | undefined => (Array.isArray(value) ? value.map((entry) => String(entry)) : undefined);
83
94
 
84
95
  const toRule = (raw: unknown, language: string, file: string): RuleDefinition => {
@@ -95,6 +106,7 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
95
106
  why: localizedOf(raw["why"]),
96
107
  how_to_fix: localizedOf(raw["how_to_fix"]),
97
108
  message: localizedOf(raw["message"]),
109
+ messages: messagesOf(raw["messages"]),
98
110
  levels,
99
111
  by_genre: genreTables(raw["by_genre"], language),
100
112
  how_to_find: String(raw["how_to_find"]),
@@ -102,6 +114,7 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
102
114
  extra_word_lists: stringList(raw["extra_word_lists"]) ?? [],
103
115
  what_to_check: isLocalized(raw["what_to_check"]) ? raw["what_to_check"] : undefined,
104
116
  where: typeof raw["where"] === "string" ? raw["where"] : undefined,
117
+ full_sentence: numberFor(raw["full_sentence"], language),
105
118
  requires: stringList(raw["requires"]) ?? [],
106
119
  uses: stringList(raw["uses"]) ?? [],
107
120
  from: stringList(raw["from"]) ?? [],
@@ -126,5 +139,5 @@ const parseRule = (dir: string, file: string): unknown => {
126
139
  export const loadRules = (language: string, dir: string = RULES_DIR): RuleDefinition[] =>
127
140
  readdirSync(dir)
128
141
  .filter((file) => file.endsWith(".yaml"))
129
- .sort((left, right) => left.localeCompare(right, "en"))
142
+ .toSorted((left, right) => left.localeCompare(right, "en"))
130
143
  .map((file) => toRule(parseRule(dir, file), language, file));
@@ -3,8 +3,9 @@ import { ask, type JudgeOptions } from "./judge.ts";
3
3
  import { FILTERS, type Candidate } from "./semantic.ts";
4
4
  import { narrow, type Narrowing } from "./look-at.ts";
5
5
  import { localized } from "./render/text.ts";
6
- import type { Finding, Level, ProseDocument, RuleDefinition, Severity } from "./plugin.ts";
6
+ import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition, Severity } from "./plugin.ts";
7
7
  import { lineStarts, placeOf } from "./position.ts";
8
+ import { wantsTags } from "./run.ts";
8
9
 
9
10
  export type SemanticResult = {
10
11
  readonly findings: readonly Finding[];
@@ -39,26 +40,33 @@ const severityFromLevel = (rule: RuleDefinition, level: Level): Severity => {
39
40
  return SEVERITY_BY_VALUE[Math.min(3, Math.max(1, Math.round(value)))] ?? "warning";
40
41
  };
41
42
 
43
+ /** この文書で動かす、意味を読む rule。 */
44
+ const semanticRules = (rules: readonly RuleDefinition[], settings: Readonly<Record<string, Level>>, genre: string): RuleDefinition[] =>
45
+ rules.filter((rule) => rule.layer === "L4" && rule.use_for.some((target) => genre.startsWith(target)) && settings[rule.id] !== "off");
46
+
47
+ /** 意味を読む rule の絞り込みが品詞を使うか。chaff test はこれで解析器を読み込む(neededBy は L4 を数えない)。 */
48
+ export const semanticNeeds = (rules: readonly RuleDefinition[], settings: Readonly<Record<string, Level>>, genre: string): AdapterNeeds => ({
49
+ pos: semanticRules(rules, settings, genre).some(wantsTags),
50
+ });
51
+
42
52
  const builtInJobs = (doc: ProseDocument, rules: readonly RuleDefinition[], settings: Readonly<Record<string, Level>>, genre: string): Job[] =>
43
- rules
44
- .filter((rule) => rule.layer === "L4" && rule.use_for.some((target) => genre.startsWith(target)) && settings[rule.id] !== "off")
45
- .flatMap((rule) => {
46
- const rubric = rule.what_to_check === undefined ? undefined : localized(rule.what_to_check, doc.language);
47
- if (rubric === undefined || rubric.length === 0) return [];
48
- // 絞り込みが無い rule は動かさない。黙って落とさず、下の skipped に出す。
49
- const filter = filterFor(rule.how_to_find);
50
- if (filter === undefined) return [];
51
- return [
52
- {
53
- rule: rule.id,
54
- name: localized(rule.name, doc.language),
55
- rubric,
56
- howToFix: localized(rule.how_to_fix, doc.language),
57
- severity: severityFromLevel(rule, settings[rule.id] ?? "normal"),
58
- candidates: filter(doc),
59
- },
60
- ];
61
- });
53
+ semanticRules(rules, settings, genre).flatMap((rule) => {
54
+ const rubric = rule.what_to_check === undefined ? undefined : localized(rule.what_to_check, doc.language);
55
+ if (rubric === undefined || rubric.length === 0) return [];
56
+ // 絞り込みが無い rule は動かさない。黙って落とさず、下の skipped に出す。
57
+ const filter = filterFor(rule.how_to_find);
58
+ if (filter === undefined) return [];
59
+ return [
60
+ {
61
+ rule: rule.id,
62
+ name: localized(rule.name, doc.language),
63
+ rubric,
64
+ howToFix: localized(rule.how_to_fix, doc.language),
65
+ severity: severityFromLevel(rule, settings[rule.id] ?? "normal"),
66
+ candidates: filter(doc),
67
+ },
68
+ ];
69
+ });
62
70
 
63
71
  const userJobs = (doc: ProseDocument, checks: readonly UserCheck[], genre: string): Job[] =>
64
72
  checks
@@ -129,7 +137,7 @@ export const runSemantic = async (
129
137
  ),
130
138
  );
131
139
  return {
132
- findings: answers.filter((finding) => finding !== undefined).sort((left, right) => left.line - right.line),
140
+ findings: answers.filter((finding) => finding !== undefined).toSorted((left, right) => left.line - right.line),
133
141
  skipped,
134
142
  asked: live.reduce((sum, job) => sum + job.candidates.length, 0),
135
143
  sentencesSeen: doc.sentences.length,
package/src/run.ts CHANGED
@@ -10,6 +10,7 @@ import { isMarkdownPath } from "./structure/markdown-path.ts";
10
10
  import { maskSpans } from "./mask.ts";
11
11
  import { textOutline } from "./page-furniture.ts";
12
12
  import { uiLanguageOf } from "./ui.ts";
13
+ import { presetLevels } from "./genre-load.ts";
13
14
 
14
15
  export type Skipped = { readonly rule: string; readonly why: string };
15
16
 
@@ -18,18 +19,29 @@ export type RunResult = {
18
19
  readonly skipped: readonly Skipped[];
19
20
  /** 設定で明示的に有効にした experimental な rule。実行ごとに一度報告する。spec §18.4。 */
20
21
  readonly forcedExperimental: readonly string[];
22
+ /** The experimental rules the genre's preset turns on. Reported apart from forcedExperimental: chaff.yaml did not name them. */
23
+ readonly presetExperimental: readonly string[];
21
24
  };
22
25
 
23
26
  export type Settings = Readonly<Record<string, Level>>;
24
27
 
25
- const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolean): Level => {
28
+ const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolean, preset: Settings): Level => {
26
29
  const explicit = settings[rule.id];
27
30
  // 明示設定は status の既定に勝つ。名指しで有効にしたものを黙って無効にしない。
28
31
  if (explicit !== undefined) return explicit;
32
+ // ジャンルの既定は chaff.yaml より弱く、status の既定より強い。--experimental でも、ジャンルが止めたものは止めたまま。
33
+ const fromPreset = preset[rule.id];
34
+ if (fromPreset !== undefined) return fromPreset;
29
35
  if (rule.status === "experimental" && !experimental) return "off";
30
36
  return "normal";
31
37
  };
32
38
 
39
+ /** Why a rule at off did not run: chaff.yaml turned it off, the genre's preset did, or it is experimental. */
40
+ const offReason = (rule: RuleDefinition, settings: Settings, preset: Settings, genre: string, reasons: Reasons): string => {
41
+ if (settings[rule.id] !== undefined) return reasons.turnedOff;
42
+ return preset[rule.id] === undefined ? reasons.experimental : reasons.presetOff(genre);
43
+ };
44
+
33
45
  const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.language)];
34
46
 
35
47
  /**
@@ -41,11 +53,11 @@ const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.lan
41
53
  * 読むので、番号を読めなかった文書でも動く。どちらも capability ではなく、adapter が木を作れるかで決まる。
42
54
  */
43
55
  const TREE_NEEDS: ReadonlySet<string> = new Set(["structure", "dates", "quantities"]);
44
- const VALUE_NEEDS: readonly string[] = ["dates", "quantities"];
56
+ const VALUE_NEEDS: ReadonlySet<string> = new Set(["dates", "quantities"]);
45
57
 
46
58
  const treeNeed = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
47
59
  if (rule.requires.includes("structure")) return treeProblem(doc);
48
- return rule.requires.some((need) => VALUE_NEEDS.includes(need)) && doc.structure === undefined ? reasonsFor(doc).noStructure(doc.language) : undefined;
60
+ return rule.requires.some((need) => VALUE_NEEDS.has(need)) && doc.structure === undefined ? reasonsFor(doc).noStructure(doc.language) : undefined;
49
61
  };
50
62
 
51
63
  const treeProblem = (doc: ProseDocument): string | undefined => {
@@ -100,15 +112,18 @@ const untagged = (rule: RuleDefinition, doc: ProseDocument): string | undefined
100
112
  const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefinition[] =>
101
113
  rules.filter((rule) => rule.use_for.some((target) => genre.startsWith(target)));
102
114
 
115
+ /** rule が品詞か見出し語を要求するか、使えるなら使うか。 */
116
+ export const wantsTags = (rule: RuleDefinition): boolean => [...rule.requires, ...rule.uses].some((need) => need === "pos" || need === "lemma");
117
+
103
118
  /**
104
119
  * 解析器の初期化に払う代金を決める。動く rule が 1 本も要求しないなら読み込まない。
105
120
  * capabilities は「払えばできる」の宣言なので、ここでは見ない。
106
121
  */
107
122
  export const neededBy = (rules: readonly RuleDefinition[], settings: Settings, experimental: boolean, genre: string, language: string): AdapterNeeds => ({
108
123
  pos: forGenre(rules, genre)
109
- .filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental) !== "off")
124
+ .filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental, presetLevels(genre)) !== "off")
110
125
  .filter((rule) => rule.languages === undefined || rule.languages.includes(language))
111
- .some((rule) => [...rule.requires, ...rule.uses].some((need) => need === "pos" || need === "lemma")),
126
+ .some(wantsTags),
112
127
  });
113
128
 
114
129
  const place = (starts: readonly number[], finding: Finding): Finding => {
@@ -153,11 +168,12 @@ export const runRules = (
153
168
  genre: string,
154
169
  limits: Limits = {},
155
170
  ): RunResult => {
171
+ const preset = presetLevels(genre);
156
172
  const starts = lineStarts(doc.source);
157
173
  const applicable = forGenre(rules, genre);
158
- const forced = applicable
159
- .filter((rule) => rule.status === "experimental" && settings[rule.id] !== undefined && settings[rule.id] !== "off")
160
- .map((rule) => rule.id);
174
+ const experimentalOn = applicable.filter((rule) => rule.status === "experimental" && levelFor(rule, settings, experimental, preset) !== "off");
175
+ const forced = experimentalOn.filter((rule) => settings[rule.id] !== undefined).map((rule) => rule.id);
176
+ const presetOn = experimentalOn.filter((rule) => settings[rule.id] === undefined && preset[rule.id] !== undefined).map((rule) => rule.id);
161
177
  const outcome = applicable.reduce<{ findings: Finding[]; skipped: Skipped[] }>(
162
178
  (acc, rule) => {
163
179
  // L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
@@ -166,11 +182,9 @@ export const runRules = (
166
182
  }
167
183
  const blocked = unmet(rule, doc);
168
184
  if (blocked !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: blocked }] };
169
- const level = levelFor(rule, settings, experimental);
170
- if (level === "off") {
171
- const why = rule.status === "experimental" && settings[rule.id] === undefined ? reasonsFor(doc).experimental : reasonsFor(doc).turnedOff;
172
- return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
173
- }
185
+ const level = levelFor(rule, settings, experimental, preset);
186
+ if (level === "off")
187
+ return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: offReason(rule, settings, preset, genre, reasonsFor(doc)) }] };
174
188
  const noTags = untagged(rule, doc);
175
189
  if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
176
190
  // 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
@@ -190,6 +204,7 @@ export const runRules = (
190
204
  limit: limitFor(rule, level, genre, limits),
191
205
  lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
192
206
  where: rule.where,
207
+ fullSentence: rule.full_sentence,
193
208
  };
194
209
  const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
195
210
  return { findings: [...acc.findings, ...found], skipped: acc.skipped };
@@ -197,8 +212,10 @@ export const runRules = (
197
212
  { findings: [], skipped: [] },
198
213
  );
199
214
  const composites = applicable
200
- .filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental) !== "off")
201
- .flatMap((rule) => compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental), genre, limits), starts, doc.language));
215
+ .filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental, preset) !== "off")
216
+ .flatMap((rule) =>
217
+ compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental, preset), genre, limits), starts, doc.language),
218
+ );
202
219
  const all = [...outcome.findings, ...composites];
203
- return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
220
+ return { ...outcome, findings: all.toSorted((left, right) => left.line - right.line), forcedExperimental: forced, presetExperimental: presetOn };
204
221
  };
package/src/semantic.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { calendarUnitsOf } from "./calendar-number.ts";
2
+ import { closingAndEarlier, newNumbers } from "./closing-numbers.ts";
1
3
  import type { ProseDocument, Section } from "./plugin.ts";
2
4
 
3
5
  /**
@@ -27,17 +29,23 @@ export const riskDisclosure: Filter = (doc) => {
27
29
  return [{ text: body.slice(0, 4000), offset: doc.sentences[0]?.span.start ?? 0 }];
28
30
  };
29
31
 
30
- const EVIDENCE = /\d|`|https?:\/\//u;
32
+ const EVIDENCE = /`|https?:\/\//u;
33
+
34
+ /** 結びに、前に書いていない数があるか。本文の数を繰り返すだけ(「以上のように、9月は…」)なら具体物ではない。 */
35
+ const hasNewNumber = (doc: ProseDocument, last: Section): boolean => {
36
+ const { closing, earlier } = closingAndEarlier(doc, last);
37
+ return newNumbers(closing, earlier, calendarUnitsOf(doc)).length > 0;
38
+ };
31
39
 
32
40
  /**
33
41
  * 結びが本文の要約でしかないか。
34
- * 最後の節だけを見て、そこに具体物が無いときにだけ問い合わせる。
42
+ * 最後の節だけを見て、そこに具体物(前に書いていない数、コード、リンク)が無いときにだけ問い合わせる。
35
43
  */
36
44
  export const emptyConclusion: Filter = (doc) => {
37
45
  const last = doc.sections.at(-1);
38
46
  if (last === undefined || last.sentences.length === 0) return [];
39
47
  const text = sectionText(doc, last);
40
- if (EVIDENCE.test(text)) return [];
48
+ if (EVIDENCE.test(text) || hasNewNumber(doc, last)) return [];
41
49
  return [{ text: text.slice(0, 2000), offset: last.sentences[0]?.span.start ?? last.span.start }];
42
50
  };
43
51