chaffjs 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/README.md +27 -2
  2. package/dist/address-chain.js +1 -1
  3. package/dist/address-chain.js.map +1 -1
  4. package/dist/backends/anthropic.d.ts +2 -1
  5. package/dist/backends/anthropic.d.ts.map +1 -1
  6. package/dist/backends/anthropic.js +4 -1
  7. package/dist/backends/anthropic.js.map +1 -1
  8. package/dist/backends/openai.d.ts +2 -1
  9. package/dist/backends/openai.d.ts.map +1 -1
  10. package/dist/backends/openai.js +1 -1
  11. package/dist/backends/openai.js.map +1 -1
  12. package/dist/backends/types.d.ts +2 -1
  13. package/dist/backends/types.d.ts.map +1 -1
  14. package/dist/backends/types.js +1 -1
  15. package/dist/backends/types.js.map +1 -1
  16. package/dist/baseline.js +1 -1
  17. package/dist/baseline.js.map +1 -1
  18. package/dist/calendar-number.d.ts +26 -0
  19. package/dist/calendar-number.d.ts.map +1 -0
  20. package/dist/calendar-number.js +51 -0
  21. package/dist/calendar-number.js.map +1 -0
  22. package/dist/cli-args.d.ts +0 -5
  23. package/dist/cli-args.d.ts.map +1 -1
  24. package/dist/cli-args.js +2 -2
  25. package/dist/cli-args.js.map +1 -1
  26. package/dist/cli-text.d.ts +6 -2
  27. package/dist/cli-text.d.ts.map +1 -1
  28. package/dist/cli-text.js +19 -12
  29. package/dist/cli-text.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +43 -37
  32. package/dist/cli.js.map +1 -1
  33. package/dist/commands/cite.d.ts.map +1 -1
  34. package/dist/commands/cite.js +4 -4
  35. package/dist/commands/cite.js.map +1 -1
  36. package/dist/commands/eval.js +1 -1
  37. package/dist/commands/eval.js.map +1 -1
  38. package/dist/commands/init-ask.d.ts +19 -0
  39. package/dist/commands/init-ask.d.ts.map +1 -0
  40. package/dist/commands/init-ask.js +43 -0
  41. package/dist/commands/init-ask.js.map +1 -0
  42. package/dist/commands/test.d.ts +8 -0
  43. package/dist/commands/test.d.ts.map +1 -1
  44. package/dist/commands/test.js +70 -38
  45. package/dist/commands/test.js.map +1 -1
  46. package/dist/commands/tree.d.ts +1 -8
  47. package/dist/commands/tree.d.ts.map +1 -1
  48. package/dist/commands/tree.js +7 -6
  49. package/dist/commands/tree.js.map +1 -1
  50. package/dist/config/by-path.d.ts.map +1 -1
  51. package/dist/config/by-path.js +10 -9
  52. package/dist/config/by-path.js.map +1 -1
  53. package/dist/config/load.d.ts +4 -0
  54. package/dist/config/load.d.ts.map +1 -1
  55. package/dist/config/load.js +19 -0
  56. package/dist/config/load.js.map +1 -1
  57. package/dist/config/name-problems.d.ts +5 -0
  58. package/dist/config/name-problems.d.ts.map +1 -0
  59. package/dist/config/name-problems.js +11 -0
  60. package/dist/config/name-problems.js.map +1 -0
  61. package/dist/date-stamp.js +1 -1
  62. package/dist/date-stamp.js.map +1 -1
  63. package/dist/detectors/char-shape.d.ts.map +1 -1
  64. package/dist/detectors/char-shape.js +16 -6
  65. package/dist/detectors/char-shape.js.map +1 -1
  66. package/dist/detectors/en-shape.d.ts.map +1 -1
  67. package/dist/detectors/en-shape.js +101 -7
  68. package/dist/detectors/en-shape.js.map +1 -1
  69. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  70. package/dist/detectors/enumerated-runs.js +4 -2
  71. package/dist/detectors/enumerated-runs.js.map +1 -1
  72. package/dist/detectors/gram-place.js +1 -1
  73. package/dist/detectors/gram-place.js.map +1 -1
  74. package/dist/detectors/heading-case.d.ts.map +1 -1
  75. package/dist/detectors/heading-case.js +3 -1
  76. package/dist/detectors/heading-case.js.map +1 -1
  77. package/dist/detectors/index.d.ts.map +1 -1
  78. package/dist/detectors/index.js +4 -3
  79. package/dist/detectors/index.js.map +1 -1
  80. package/dist/detectors/lettered-index.d.ts +9 -0
  81. package/dist/detectors/lettered-index.d.ts.map +1 -0
  82. package/dist/detectors/lettered-index.js +54 -0
  83. package/dist/detectors/lettered-index.js.map +1 -0
  84. package/dist/detectors/lexicon.d.ts +5 -1
  85. package/dist/detectors/lexicon.d.ts.map +1 -1
  86. package/dist/detectors/lexicon.js +46 -3
  87. package/dist/detectors/lexicon.js.map +1 -1
  88. package/dist/detectors/orthography.d.ts.map +1 -1
  89. package/dist/detectors/orthography.js +24 -6
  90. package/dist/detectors/orthography.js.map +1 -1
  91. package/dist/detectors/phrase-match.d.ts +3 -1
  92. package/dist/detectors/phrase-match.d.ts.map +1 -1
  93. package/dist/detectors/phrase-match.js +20 -15
  94. package/dist/detectors/phrase-match.js.map +1 -1
  95. package/dist/detectors/repeated-head.d.ts.map +1 -1
  96. package/dist/detectors/repeated-head.js +5 -4
  97. package/dist/detectors/repeated-head.js.map +1 -1
  98. package/dist/detectors/signals.d.ts.map +1 -1
  99. package/dist/detectors/signals.js +16 -6
  100. package/dist/detectors/signals.js.map +1 -1
  101. package/dist/detectors/stacked-hedge.d.ts +23 -0
  102. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  103. package/dist/detectors/stacked-hedge.js +64 -0
  104. package/dist/detectors/stacked-hedge.js.map +1 -0
  105. package/dist/detectors/superlative-amount.d.ts +9 -0
  106. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  107. package/dist/detectors/superlative-amount.js +20 -0
  108. package/dist/detectors/superlative-amount.js.map +1 -0
  109. package/dist/detectors/superlative-clause.d.ts +18 -0
  110. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  111. package/dist/detectors/superlative-clause.js +61 -0
  112. package/dist/detectors/superlative-clause.js.map +1 -0
  113. package/dist/detectors/superlative-scope.d.ts +6 -0
  114. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  115. package/dist/detectors/superlative-scope.js +3 -3
  116. package/dist/detectors/superlative-scope.js.map +1 -1
  117. package/dist/detectors/team.d.ts +0 -6
  118. package/dist/detectors/team.d.ts.map +1 -1
  119. package/dist/detectors/team.js +17 -1
  120. package/dist/detectors/team.js.map +1 -1
  121. package/dist/detectors/token-shape.d.ts.map +1 -1
  122. package/dist/detectors/token-shape.js +8 -6
  123. package/dist/detectors/token-shape.js.map +1 -1
  124. package/dist/document.d.ts +3 -0
  125. package/dist/document.d.ts.map +1 -1
  126. package/dist/document.js +18 -3
  127. package/dist/document.js.map +1 -1
  128. package/dist/eval.js +1 -1
  129. package/dist/eval.js.map +1 -1
  130. package/dist/feedback/draft.js +1 -1
  131. package/dist/feedback/draft.js.map +1 -1
  132. package/dist/feedback/excerpt.d.ts +0 -1
  133. package/dist/feedback/excerpt.d.ts.map +1 -1
  134. package/dist/feedback/excerpt.js +2 -2
  135. package/dist/feedback/excerpt.js.map +1 -1
  136. package/dist/file-header.d.ts +20 -0
  137. package/dist/file-header.d.ts.map +1 -0
  138. package/dist/file-header.js +21 -0
  139. package/dist/file-header.js.map +1 -0
  140. package/dist/files.js +3 -3
  141. package/dist/files.js.map +1 -1
  142. package/dist/genre-check.js +1 -1
  143. package/dist/genre-check.js.map +1 -1
  144. package/dist/genre-load.d.ts +6 -0
  145. package/dist/genre-load.d.ts.map +1 -0
  146. package/dist/genre-load.js +22 -0
  147. package/dist/genre-load.js.map +1 -0
  148. package/dist/genre-parse.d.ts +40 -0
  149. package/dist/genre-parse.d.ts.map +1 -0
  150. package/dist/genre-parse.js +104 -0
  151. package/dist/genre-parse.js.map +1 -0
  152. package/dist/genre-suggest.d.ts +15 -0
  153. package/dist/genre-suggest.d.ts.map +1 -0
  154. package/dist/genre-suggest.js +26 -0
  155. package/dist/genre-suggest.js.map +1 -0
  156. package/dist/genre.d.ts +1 -1
  157. package/dist/genre.d.ts.map +1 -1
  158. package/dist/genre.js +3 -13
  159. package/dist/genre.js.map +1 -1
  160. package/dist/init-choice.d.ts +12 -0
  161. package/dist/init-choice.d.ts.map +1 -0
  162. package/dist/init-choice.js +41 -0
  163. package/dist/init-choice.js.map +1 -0
  164. package/dist/init.d.ts.map +1 -1
  165. package/dist/init.js +16 -5
  166. package/dist/init.js.map +1 -1
  167. package/dist/judge.d.ts +2 -1
  168. package/dist/judge.d.ts.map +1 -1
  169. package/dist/judge.js +1 -1
  170. package/dist/judge.js.map +1 -1
  171. package/dist/line-paragraphs.d.ts +4 -0
  172. package/dist/line-paragraphs.d.ts.map +1 -0
  173. package/dist/line-paragraphs.js +69 -0
  174. package/dist/line-paragraphs.js.map +1 -0
  175. package/dist/mask.d.ts.map +1 -1
  176. package/dist/mask.js +7 -6
  177. package/dist/mask.js.map +1 -1
  178. package/dist/plugin.d.ts +6 -0
  179. package/dist/plugin.d.ts.map +1 -1
  180. package/dist/profile/check.d.ts +28 -0
  181. package/dist/profile/check.d.ts.map +1 -0
  182. package/dist/profile/check.js +12 -0
  183. package/dist/profile/check.js.map +1 -0
  184. package/dist/profile/for-file.d.ts +2 -2
  185. package/dist/profile/for-file.d.ts.map +1 -1
  186. package/dist/profile/for-file.js +9 -2
  187. package/dist/profile/for-file.js.map +1 -1
  188. package/dist/profile/load.js +1 -1
  189. package/dist/profile/load.js.map +1 -1
  190. package/dist/profile/select.d.ts +4 -2
  191. package/dist/profile/select.d.ts.map +1 -1
  192. package/dist/profile/select.js +8 -3
  193. package/dist/profile/select.js.map +1 -1
  194. package/dist/quoted-span.d.ts +15 -3
  195. package/dist/quoted-span.d.ts.map +1 -1
  196. package/dist/quoted-span.js +16 -11
  197. package/dist/quoted-span.js.map +1 -1
  198. package/dist/reasons.d.ts +1 -0
  199. package/dist/reasons.d.ts.map +1 -1
  200. package/dist/reasons.js +4 -1
  201. package/dist/reasons.js.map +1 -1
  202. package/dist/render/compact.d.ts.map +1 -1
  203. package/dist/render/compact.js +1 -2
  204. package/dist/render/compact.js.map +1 -1
  205. package/dist/render/eval.d.ts.map +1 -1
  206. package/dist/render/eval.js +4 -3
  207. package/dist/render/eval.js.map +1 -1
  208. package/dist/render/friendly.d.ts +2 -1
  209. package/dist/render/friendly.d.ts.map +1 -1
  210. package/dist/render/friendly.js +14 -6
  211. package/dist/render/friendly.js.map +1 -1
  212. package/dist/render/genres.d.ts +5 -0
  213. package/dist/render/genres.d.ts.map +1 -0
  214. package/dist/render/genres.js +28 -0
  215. package/dist/render/genres.js.map +1 -0
  216. package/dist/render/plural.d.ts +3 -0
  217. package/dist/render/plural.d.ts.map +1 -0
  218. package/dist/render/plural.js +5 -0
  219. package/dist/render/plural.js.map +1 -0
  220. package/dist/render/rules-json.d.ts.map +1 -1
  221. package/dist/render/rules-json.js +9 -3
  222. package/dist/render/rules-json.js.map +1 -1
  223. package/dist/render/semantic.d.ts +3 -2
  224. package/dist/render/semantic.d.ts.map +1 -1
  225. package/dist/render/semantic.js +94 -34
  226. package/dist/render/semantic.js.map +1 -1
  227. package/dist/render/summary.d.ts.map +1 -1
  228. package/dist/render/summary.js +4 -3
  229. package/dist/render/summary.js.map +1 -1
  230. package/dist/render/suppressions.d.ts.map +1 -1
  231. package/dist/render/suppressions.js +3 -2
  232. package/dist/render/suppressions.js.map +1 -1
  233. package/dist/render/text.d.ts +2 -0
  234. package/dist/render/text.d.ts.map +1 -1
  235. package/dist/render/text.js +11 -3
  236. package/dist/render/text.js.map +1 -1
  237. package/dist/rule-load.d.ts.map +1 -1
  238. package/dist/rule-load.js +4 -1
  239. package/dist/rule-load.js.map +1 -1
  240. package/dist/run-semantic.js +1 -1
  241. package/dist/run-semantic.js.map +1 -1
  242. package/dist/run.d.ts +2 -0
  243. package/dist/run.d.ts.map +1 -1
  244. package/dist/run.js +25 -15
  245. package/dist/run.js.map +1 -1
  246. package/dist/sentence-shape.d.ts +0 -1
  247. package/dist/sentence-shape.d.ts.map +1 -1
  248. package/dist/sentence-shape.js +3 -5
  249. package/dist/sentence-shape.js.map +1 -1
  250. package/dist/setting-problems.d.ts +5 -0
  251. package/dist/setting-problems.d.ts.map +1 -0
  252. package/dist/setting-problems.js +15 -0
  253. package/dist/setting-problems.js.map +1 -0
  254. package/dist/structure/build.js +5 -5
  255. package/dist/structure/build.js.map +1 -1
  256. package/dist/structure/cite.js +1 -1
  257. package/dist/structure/cite.js.map +1 -1
  258. package/dist/structure/issues.d.ts.map +1 -1
  259. package/dist/structure/issues.js +5 -5
  260. package/dist/structure/issues.js.map +1 -1
  261. package/dist/structure/markdown-path.d.ts.map +1 -1
  262. package/dist/structure/markdown-path.js +2 -2
  263. package/dist/structure/markdown-path.js.map +1 -1
  264. package/dist/structure/numbering.d.ts.map +1 -1
  265. package/dist/structure/numbering.js +1 -1
  266. package/dist/structure/numbering.js.map +1 -1
  267. package/dist/structure/relative-find.js +1 -1
  268. package/dist/structure/relative-find.js.map +1 -1
  269. package/dist/structure/sexp.js +1 -1
  270. package/dist/structure/sexp.js.map +1 -1
  271. package/dist/structure/unnumbered.js +2 -2
  272. package/dist/structure/unnumbered.js.map +1 -1
  273. package/dist/team-names.d.ts +9 -0
  274. package/dist/team-names.d.ts.map +1 -0
  275. package/dist/team-names.js +34 -0
  276. package/dist/team-names.js.map +1 -0
  277. package/dist/ui.d.ts +2 -0
  278. package/dist/ui.d.ts.map +1 -1
  279. package/dist/ui.js +6 -0
  280. package/dist/ui.js.map +1 -1
  281. package/dist/version.js +1 -1
  282. package/dist/version.js.map +1 -1
  283. package/dist/watch.d.ts +2 -2
  284. package/dist/watch.d.ts.map +1 -1
  285. package/dist/watch.js +5 -5
  286. package/dist/watch.js.map +1 -1
  287. package/genres.yaml +363 -0
  288. package/package.json +4 -3
  289. package/rules/adverb-overuse.yaml +2 -2
  290. package/rules/agentless-passive.yaml +1 -1
  291. package/rules/ai-generated-composite.yaml +2 -2
  292. package/rules/ai-tell.yaml +2 -2
  293. package/rules/bold-density.yaml +2 -2
  294. package/rules/closing-cliche.yaml +1 -1
  295. package/rules/concrete-evidence-density.yaml +2 -2
  296. package/rules/contraction-consistency.yaml +1 -1
  297. package/rules/cushion-phrase-density.yaml +1 -1
  298. package/rules/dangling-reference.yaml +1 -1
  299. package/rules/date-order.yaml +1 -1
  300. package/rules/date-weekday-mismatch.yaml +1 -1
  301. package/rules/double-keigo.yaml +1 -1
  302. package/rules/doubled-word.yaml +1 -1
  303. package/rules/duplicate-definition.yaml +1 -1
  304. package/rules/emoji-density.yaml +1 -1
  305. package/rules/empty-conclusion.yaml +2 -1
  306. package/rules/empty-intensifier.yaml +1 -1
  307. package/rules/excessive-hedging.yaml +16 -6
  308. package/rules/expletive-construction.yaml +2 -2
  309. package/rules/heading-echo.yaml +1 -1
  310. package/rules/hiragana-fukushi.yaml +1 -1
  311. package/rules/internal-jargon.yaml +2 -2
  312. package/rules/latin-spacing.yaml +5 -4
  313. package/rules/max-kanji-continuous.yaml +4 -3
  314. package/rules/max-paragraph-length.yaml +9 -2
  315. package/rules/max-sentence-length.yaml +14 -2
  316. package/rules/ngram-repetition.yaml +4 -4
  317. package/rules/no-doubled-joshi.yaml +2 -2
  318. package/rules/no-em-dash.yaml +2 -2
  319. package/rules/no-mixed-desumasu.yaml +1 -1
  320. package/rules/no-nakaguro-parallel.yaml +2 -2
  321. package/rules/numbering-gap.yaml +1 -1
  322. package/rules/oxford-comma-consistency.yaml +2 -1
  323. package/rules/padded-intro.yaml +2 -1
  324. package/rules/paragraph-length-variance.yaml +1 -1
  325. package/rules/preamble-length.yaml +2 -2
  326. package/rules/preferred-term.yaml +1 -1
  327. package/rules/proper-noun-density.yaml +3 -2
  328. package/rules/repeated-conjunction.yaml +2 -2
  329. package/rules/repeated-sentence-head.yaml +2 -2
  330. package/rules/required-sections.yaml +1 -1
  331. package/rules/rule-of-three.yaml +2 -2
  332. package/rules/sasete-itadaku.yaml +3 -3
  333. package/rules/section-length-uniformity.yaml +1 -1
  334. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  335. package/rules/sentence-rhythm.yaml +1 -1
  336. package/rules/taigen-dome-in-prose.yaml +2 -2
  337. package/rules/title-case-consistency.yaml +1 -1
  338. package/rules/total-mismatch.yaml +1 -1
  339. package/rules/undefined-acronym.yaml +4 -4
  340. package/rules/unqualified-superlative.yaml +2 -2
  341. package/skills/chaff/SKILL.md +28 -9
  342. package/src/address-chain.ts +1 -1
  343. package/src/backends/anthropic.ts +5 -1
  344. package/src/backends/openai.ts +2 -1
  345. package/src/backends/types.ts +3 -2
  346. package/src/baseline.ts +1 -1
  347. package/src/calendar-number.ts +75 -0
  348. package/src/cli-args.ts +2 -2
  349. package/src/cli-text.ts +29 -15
  350. package/src/cli.ts +42 -41
  351. package/src/commands/cite.ts +4 -4
  352. package/src/commands/eval.ts +1 -1
  353. package/src/commands/init-ask.ts +53 -0
  354. package/src/commands/test.ts +102 -45
  355. package/src/commands/tree.ts +7 -6
  356. package/src/config/by-path.ts +8 -9
  357. package/src/config/load.ts +24 -0
  358. package/src/config/name-problems.ts +15 -0
  359. package/src/date-stamp.ts +1 -1
  360. package/src/detectors/char-shape.ts +21 -9
  361. package/src/detectors/en-shape.ts +106 -8
  362. package/src/detectors/enumerated-runs.ts +3 -2
  363. package/src/detectors/gram-place.ts +1 -1
  364. package/src/detectors/heading-case.ts +4 -1
  365. package/src/detectors/index.ts +4 -3
  366. package/src/detectors/lettered-index.ts +78 -0
  367. package/src/detectors/lexicon.ts +63 -6
  368. package/src/detectors/orthography.ts +31 -7
  369. package/src/detectors/phrase-match.ts +23 -12
  370. package/src/detectors/repeated-head.ts +3 -4
  371. package/src/detectors/signals.ts +15 -6
  372. package/src/detectors/stacked-hedge.ts +93 -0
  373. package/src/detectors/superlative-amount.ts +24 -0
  374. package/src/detectors/superlative-clause.ts +82 -0
  375. package/src/detectors/superlative-scope.ts +3 -3
  376. package/src/detectors/team.ts +18 -2
  377. package/src/detectors/token-shape.ts +8 -10
  378. package/src/document.ts +29 -3
  379. package/src/eval.ts +1 -1
  380. package/src/feedback/draft.ts +1 -1
  381. package/src/feedback/excerpt.ts +2 -2
  382. package/src/file-header.ts +31 -0
  383. package/src/files.ts +3 -3
  384. package/src/genre-check.ts +1 -1
  385. package/src/genre-load.ts +24 -0
  386. package/src/genre-parse.ts +126 -0
  387. package/src/genre-suggest.ts +37 -0
  388. package/src/genre.ts +4 -13
  389. package/src/init-choice.ts +47 -0
  390. package/src/init.ts +20 -6
  391. package/src/judge.ts +3 -1
  392. package/src/line-paragraphs.ts +83 -0
  393. package/src/mask.ts +5 -6
  394. package/src/plugin.ts +6 -0
  395. package/src/profile/check.ts +34 -0
  396. package/src/profile/for-file.ts +10 -3
  397. package/src/profile/load.ts +1 -1
  398. package/src/profile/select.ts +11 -4
  399. package/src/quoted-span.ts +26 -12
  400. package/src/reasons.ts +5 -1
  401. package/src/render/compact.ts +1 -3
  402. package/src/render/eval.ts +4 -3
  403. package/src/render/friendly.ts +21 -6
  404. package/src/render/genres.ts +31 -0
  405. package/src/render/plural.ts +6 -0
  406. package/src/render/rules-json.ts +10 -3
  407. package/src/render/semantic.ts +117 -34
  408. package/src/render/summary.ts +4 -3
  409. package/src/render/suppressions.ts +3 -2
  410. package/src/render/text.ts +18 -3
  411. package/src/rule-load.ts +6 -1
  412. package/src/run-semantic.ts +1 -1
  413. package/src/run.ts +28 -15
  414. package/src/sentence-shape.ts +3 -7
  415. package/src/setting-problems.ts +24 -0
  416. package/src/structure/build.ts +5 -5
  417. package/src/structure/cite.ts +1 -1
  418. package/src/structure/issues.ts +6 -5
  419. package/src/structure/markdown-path.ts +2 -2
  420. package/src/structure/numbering.ts +1 -1
  421. package/src/structure/relative-find.ts +1 -1
  422. package/src/structure/sexp.ts +1 -1
  423. package/src/structure/unnumbered.ts +2 -2
  424. package/src/team-names.ts +39 -0
  425. package/src/ui.ts +7 -0
  426. package/src/version.ts +1 -1
  427. package/src/watch.ts +5 -5
@@ -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
  * 記号の許容度は言語で大きく違う(ダッシュは英語では普通、日本語の組版では扱いが難しい)。
@@ -95,6 +99,7 @@ const toRule = (raw: unknown, language: string, file: string): RuleDefinition =>
95
99
  why: localizedOf(raw["why"]),
96
100
  how_to_fix: localizedOf(raw["how_to_fix"]),
97
101
  message: localizedOf(raw["message"]),
102
+ messages: messagesOf(raw["messages"]),
98
103
  levels,
99
104
  by_genre: genreTables(raw["by_genre"], language),
100
105
  how_to_find: String(raw["how_to_find"]),
@@ -126,5 +131,5 @@ const parseRule = (dir: string, file: string): unknown => {
126
131
  export const loadRules = (language: string, dir: string = RULES_DIR): RuleDefinition[] =>
127
132
  readdirSync(dir)
128
133
  .filter((file) => file.endsWith(".yaml"))
129
- .sort((left, right) => left.localeCompare(right, "en"))
134
+ .toSorted((left, right) => left.localeCompare(right, "en"))
130
135
  .map((file) => toRule(parseRule(dir, file), language, file));
@@ -129,7 +129,7 @@ export const runSemantic = async (
129
129
  ),
130
130
  );
131
131
  return {
132
- findings: answers.filter((finding) => finding !== undefined).sort((left, right) => left.line - right.line),
132
+ findings: answers.filter((finding) => finding !== undefined).toSorted((left, right) => left.line - right.line),
133
133
  skipped,
134
134
  asked: live.reduce((sum, job) => sum + job.candidates.length, 0),
135
135
  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 => {
@@ -106,7 +118,7 @@ const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefiniti
106
118
  */
107
119
  export const neededBy = (rules: readonly RuleDefinition[], settings: Settings, experimental: boolean, genre: string, language: string): AdapterNeeds => ({
108
120
  pos: forGenre(rules, genre)
109
- .filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental) !== "off")
121
+ .filter((rule) => rule.layer !== "L4" && levelFor(rule, settings, experimental, presetLevels(genre)) !== "off")
110
122
  .filter((rule) => rule.languages === undefined || rule.languages.includes(language))
111
123
  .some((rule) => [...rule.requires, ...rule.uses].some((need) => need === "pos" || need === "lemma")),
112
124
  });
@@ -153,11 +165,12 @@ export const runRules = (
153
165
  genre: string,
154
166
  limits: Limits = {},
155
167
  ): RunResult => {
168
+ const preset = presetLevels(genre);
156
169
  const starts = lineStarts(doc.source);
157
170
  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);
171
+ const experimentalOn = applicable.filter((rule) => rule.status === "experimental" && levelFor(rule, settings, experimental, preset) !== "off");
172
+ const forced = experimentalOn.filter((rule) => settings[rule.id] !== undefined).map((rule) => rule.id);
173
+ const presetOn = experimentalOn.filter((rule) => settings[rule.id] === undefined && preset[rule.id] !== undefined).map((rule) => rule.id);
161
174
  const outcome = applicable.reduce<{ findings: Finding[]; skipped: Skipped[] }>(
162
175
  (acc, rule) => {
163
176
  // L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
@@ -166,11 +179,9 @@ export const runRules = (
166
179
  }
167
180
  const blocked = unmet(rule, doc);
168
181
  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
- }
182
+ const level = levelFor(rule, settings, experimental, preset);
183
+ if (level === "off")
184
+ return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: offReason(rule, settings, preset, genre, reasonsFor(doc)) }] };
174
185
  const noTags = untagged(rule, doc);
175
186
  if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
176
187
  // 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
@@ -197,8 +208,10 @@ export const runRules = (
197
208
  { findings: [], skipped: [] },
198
209
  );
199
210
  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));
211
+ .filter((rule) => rule.from.length > 0 && levelFor(rule, settings, experimental, preset) !== "off")
212
+ .flatMap((rule) =>
213
+ compositeOf(rule, outcome.findings, limitFor(rule, levelFor(rule, settings, experimental, preset), genre, limits), starts, doc.language),
214
+ );
202
215
  const all = [...outcome.findings, ...composites];
203
- return { ...outcome, findings: [...all].sort((left, right) => left.line - right.line), forcedExperimental: forced };
216
+ return { ...outcome, findings: all.toSorted((left, right) => left.line - right.line), forcedExperimental: forced, presetExperimental: presetOn };
204
217
  };
@@ -11,10 +11,6 @@ const ENDS = /[。..!?!?]["))」』\s]*$/u;
11
11
 
12
12
  export const isClosed = (sentence: Sentence): boolean => ENDS.test(sentence.text);
13
13
 
14
- const PREDICATE = new Set(["AUX", "VERB", "ADJ"]);
15
-
16
- export const hasPredicate = (sentence: Sentence): boolean => (sentence.tokens ?? []).some((token) => PREDICATE.has(token.pos));
17
-
18
14
  export const hasParticle = (sentence: Sentence): boolean => (sentence.tokens ?? []).some((token) => token.pos === "ADP" || token.pos === "SCONJ");
19
15
 
20
16
  const SKIP = new Set(["PUNCT", "PART", "SYM"]);
@@ -46,11 +42,11 @@ const CLOSING_QUOTES = ["」", "』", "\u201d"];
46
42
  * 引用で終わる文(画面には「確認します。」。)は、書き手の文末が無いので空。
47
43
  */
48
44
  export const endingTokens = (sentence: Sentence): Token[] => {
49
- const reversed = [...beforeTrailing(sentence)].reverse();
45
+ const reversed = beforeTrailing(sentence).toReversed();
50
46
  const last = reversed.findIndex((token) => !SKIP.has(token.pos));
51
47
  if (last === -1 || reversed.slice(0, last).some((token) => CLOSING_QUOTES.some((quote) => token.surface.includes(quote)))) return [];
52
48
  const head = reversed.findIndex((token, index) => index >= last && token.pos !== "AUX");
53
- return reversed.slice(last, head === -1 ? reversed.length : head + 1).reverse();
49
+ return reversed.slice(last, head === -1 ? reversed.length : head + 1).toReversed();
54
50
  };
55
51
 
56
- export const lastContent = (sentence: Sentence): Token | undefined => [...beforeTrailing(sentence)].reverse().find((token) => !SKIP.has(token.pos));
52
+ export const lastContent = (sentence: Sentence): Token | undefined => beforeTrailing(sentence).findLast((token) => !SKIP.has(token.pos));
@@ -0,0 +1,24 @@
1
+ import type { Config } from "./config/load.ts";
2
+ import type { CliText } from "./cli-text.ts";
3
+ import { GENRES } from "./genre.ts";
4
+ import { unknownGenres, writtenGenres } from "./genre-check.ts";
5
+ import { loadProfiles } from "./profile/load.ts";
6
+ import { unknownProfiles, writtenProfiles } from "./profile/check.ts";
7
+
8
+ const genreProblems = (command: string, genreFlag: string | undefined, config: Config, text: CliText): string[] =>
9
+ unknownGenres(writtenGenres(command, genreFlag, config), GENRES).map((entry) =>
10
+ text.unknownGenre(entry.genre, text.genreWhere(entry.where, entry.files), GENRES),
11
+ );
12
+
13
+ const profileProblems = (command: string, config: Config, text: CliText): string[] => {
14
+ const known = loadProfiles().map((definition) => definition.id);
15
+ return unknownProfiles(writtenProfiles(command, config), known).map((entry) =>
16
+ text.unknownProfile(entry.profile, text.profileWhere(entry.where, entry.files), known),
17
+ );
18
+ };
19
+
20
+ /** Settings that name something chaff does not have. Each would silently check less than was asked, so the run stops on them. */
21
+ export const settingProblems = (command: string, genreFlag: string | undefined, config: Config, text: CliText): string[] => [
22
+ ...genreProblems(command, genreFlag, config, text),
23
+ ...profileProblems(command, config, text),
24
+ ];
@@ -73,9 +73,9 @@ const open = (state: State, frame: Frame): void => {
73
73
  state.stack.push(frame);
74
74
  };
75
75
 
76
- const nearestNumbered = (state: State): Frame | undefined => [...state.stack].reverse().find((frame) => frame.numbered !== undefined);
76
+ const nearestNumbered = (state: State): Frame | undefined => state.stack.findLast((frame) => frame.numbered !== undefined);
77
77
 
78
- const nearestSection = (state: State): Frame | undefined => [...state.stack].reverse().find((frame) => frame.draft.kind === "section");
78
+ const nearestSection = (state: State): Frame | undefined => state.stack.findLast((frame) => frame.draft.kind === "section");
79
79
 
80
80
  /**
81
81
  * 条番号や 4.2 はそれだけで番地になる。項・号・(a) は親の番地に続ける。親が無ければ節の中の番号とする。
@@ -160,12 +160,12 @@ const universalNumber = (patterns: StructurePatterns, text: string, context: Num
160
160
  };
161
161
 
162
162
  /** 行の中の定義・参照・義務・数量を、いま開いている最も内側の節点の子にする。 */
163
- const enclosingArticle = (state: State): Draft | undefined => [...state.stack].reverse().find((frame) => frame.draft.kind === "article")?.draft;
163
+ const enclosingArticle = (state: State): Draft | undefined => state.stack.findLast((frame) => frame.draft.kind === "article")?.draft;
164
164
 
165
165
  /** 条より広い範囲(「In this Part」)なら、その深さの開いているまとまり。開いていなければ(Part の見出しの無い文書)無い。 */
166
166
  const widerScope = (state: State, patterns: StructurePatterns, text: string): Draft | undefined => {
167
167
  const depth = patterns.definitionScopeDepth?.(text);
168
- return depth === undefined ? undefined : [...state.stack].reverse().find((frame) => frame.numbered?.depth === depth)?.draft;
168
+ return depth === undefined ? undefined : state.stack.findLast((frame) => frame.numbered?.depth === depth)?.draft;
169
169
  };
170
170
 
171
171
  /** 範囲を宣言した行を持つまとまり。条の中なら条、条の外(Markdown の見出しの下、Part の直下)なら開いている一番内側のもの。 */
@@ -200,7 +200,7 @@ const addLeaves = (state: State, patterns: StructurePatterns, line: Line, wrappe
200
200
  attrs: mention.attrs,
201
201
  })),
202
202
  )
203
- .sort((left, right) => left.start - right.start);
203
+ .toSorted((left, right) => left.start - right.start);
204
204
  leaves.forEach((leaf) =>
205
205
  parent.children.push({
206
206
  kind: leaf.kind,
@@ -73,7 +73,7 @@ const addressedNodes = (tree: StructureNode): Addressed[] => {
73
73
  const current = pending.pop();
74
74
  if (current === undefined) break;
75
75
  if (current.node.address !== "") found.push(current);
76
- [...current.node.children].reverse().forEach((child) => pending.push({ node: child, depth: current.depth + 1 }));
76
+ current.node.children.toReversed().forEach((child) => pending.push({ node: child, depth: current.depth + 1 }));
77
77
  }
78
78
  return found;
79
79
  };
@@ -7,7 +7,7 @@ import { namesAbsentUnit } from "./unit-word.ts";
7
7
 
8
8
  export type StructureIssue = { readonly offset: number; readonly values: Readonly<Record<string, string | number>> };
9
9
 
10
- const NUMBERED: readonly string[] = ["chapter", "article", "item"];
10
+ const NUMBERED: ReadonlySet<string> = new Set(["chapter", "article", "item"]);
11
11
 
12
12
  /** 木を上から順に平らにする。文書の中での順番と同じになる。再帰にしないのは、深い木でスタックを使い切らないため。 */
13
13
  export const inDocumentOrder = (tree: StructureNode): StructureNode[] => {
@@ -17,7 +17,7 @@ export const inDocumentOrder = (tree: StructureNode): StructureNode[] => {
17
17
  const node = pending.pop();
18
18
  if (node === undefined) break;
19
19
  order.push(node);
20
- pending.push(...[...node.children].reverse());
20
+ pending.push(...node.children.toReversed());
21
21
  }
22
22
  return order;
23
23
  };
@@ -57,6 +57,8 @@ const citesOtherDocument =
57
57
  (node: StructureNode): boolean =>
58
58
  node.attrs["document"] !== undefined || (node.attrs["citedTag"] !== undefined && listed.has(textOf(node, "citedTag")));
59
59
 
60
+ const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
61
+
60
62
  /**
61
63
  * 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
62
64
  * 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
@@ -67,10 +69,9 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
67
69
  if (!nodes.some((node) => node.kind === "article")) return [];
68
70
  const tagged = nodes.some((node) => node.attrs["citedTag"] !== undefined);
69
71
  const citesOther = citesOtherDocument(tagged ? listedTags(source) : new Set());
70
- const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).flatMap(keysOf));
72
+ const addresses = new Set(nodes.filter((node) => NUMBERED.has(node.kind)).flatMap(keysOf));
71
73
  // Section で組んだ法令が "Articles 13 to 21 of the UK GDPR" と別の文書の Article を名指ししていれば、名の無い "Article 6(3)" もそちら。
72
74
  // 名指しがあれば、名の無い "Article 9" が書き間違いか向こうの条かは区別できないので黙る。名指しが無い文書では報告する。
73
- const numbering = (node: StructureNode): unknown => node.attrs["numbering"];
74
75
  const numberings = new Set(nodes.flatMap((node) => (node.kind === "article" && node.numbering !== undefined ? [node.numbering] : [])));
75
76
  const citedElsewhere = new Set(nodes.filter((node) => node.kind === "reference" && citesOther(node)).map(numbering));
76
77
  const otherNumbering = (node: StructureNode): boolean =>
@@ -94,7 +95,7 @@ const definitionsInOrder = (tree: StructureNode): Definition[] => {
94
95
  if (current === undefined) break;
95
96
  if (current.node.kind === "definition") found.push(current);
96
97
  const article = current.node.kind === "article" ? current.node.address : current.article;
97
- [...current.node.children].reverse().forEach((child) => pending.push({ node: child, article }));
98
+ current.node.children.toReversed().forEach((child) => pending.push({ node: child, article }));
98
99
  }
99
100
  return found;
100
101
  };