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
@@ -0,0 +1,75 @@
1
+ import { digitRunAround } from "./number-name.ts";
2
+ import type { Span, Token } from "./plugin.ts";
3
+
4
+ /**
5
+ * 日付・時刻として書かれた数(9月、3時、2026年、10月1日、10時5分)。数を詰めて書くのが決まりで、空け方の好みではない。
6
+ * 長さ(3ヶ月、3時間、3日間)は別の語で書くので、ここには入らない。
7
+ *
8
+ * 単位は語彙表から渡す。chained は日付・時刻の単位を大きい順に(年月日時分秒)。大きい単位の後ろに小さい単位を続けて書けば
9
+ * 一つの日付・時刻になる。「2008年 2年連続」のように同じ単位や大きい単位が続けば、別のものの始まり。
10
+ * positional は数の後ろに置くだけで位置を言う単位(月、時)。year は 4 桁の数の後ろで暦の年を言う単位(年、年度)。
11
+ * 「5日」「5分」「3年」のように、単位だけでは長さと見分けのつかないものは、位置を言う単位と続けて書いたときだけ日付・時刻と読む。
12
+ */
13
+ export type CalendarUnits = { readonly chained: readonly string[]; readonly positional: ReadonlySet<string>; readonly year: ReadonlySet<string> };
14
+
15
+ /** 日付・時刻の数の並び。first は日付の頭の数、inner は前の単位に続く数(2026年9月の 9)。 */
16
+ export type CalendarRun = { readonly run: Span; readonly place: "first" | "inner" };
17
+
18
+ /** 数の並びと、その後ろの単位の終わり(文の中の位置)。rank は chained の中の単位の順(無ければ -1)、anchored は単位だけで日付・時刻と分かるもの。 */
19
+ type Part = { readonly run: Span; readonly unitEnd: number; readonly rank: number; readonly anchored: boolean };
20
+
21
+ const DIGIT = /\d/u;
22
+ const CALENDAR_YEAR = /^\d{4}$/u;
23
+
24
+ /** text の中の数の並び(数字・小数点・ハイフン)を左から。 */
25
+ const digitRuns = (text: string): Span[] => {
26
+ const runs: Span[] = [];
27
+ let at = 0;
28
+ while (at < text.length) {
29
+ const run = DIGIT.test(text[at] ?? "") ? digitRunAround(text, at) : undefined;
30
+ if (run !== undefined) runs.push(run);
31
+ at = Math.max(run?.end ?? 0, at + 1);
32
+ }
33
+ return runs;
34
+ };
35
+
36
+ /** 空白 1 つを越えた位置。 */
37
+ const pastSpace = (text: string, at: number): number => (text[at] === " " ? at + 1 : at);
38
+
39
+ const isUnit = (surface: string, units: CalendarUnits): boolean => units.chained.includes(surface) || units.positional.has(surface) || units.year.has(surface);
40
+
41
+ const isAnchor = (digits: string, surface: string, units: CalendarUnits): boolean =>
42
+ units.positional.has(surface) || (units.year.has(surface) && CALENDAR_YEAR.test(digits));
43
+
44
+ /** 数のすぐ後ろ(空白 1 つまで)の語が日付・時刻の単位なら、その組。語の切れ目が合わなければ組にしない。 */
45
+ const partOf = (text: string, run: Span, tokens: readonly Token[], base: number, units: CalendarUnits): Part | undefined => {
46
+ const unit = tokens.find((token) => token.span.start === base + pastSpace(text, run.end));
47
+ if (unit === undefined || !isUnit(unit.surface, units)) return undefined;
48
+ const rank = units.chained.indexOf(unit.surface);
49
+ return { run, unitEnd: unit.span.end - base, rank, anchored: isAnchor(text.slice(run.start, run.end), unit.surface, units) };
50
+ };
51
+
52
+ /** 前の組の単位のすぐ後ろ(空白 1 つまで)から始まり、単位が前より小さい組か。 */
53
+ const continues = (text: string, previous: Part | undefined, part: Part): boolean =>
54
+ previous !== undefined && previous.rank >= 0 && part.rank > previous.rank && part.run.start === pastSpace(text, previous.unitEnd);
55
+
56
+ /** 前の組に続く組を、同じ日付・時刻にまとめる。 */
57
+ const chainsOf = (text: string, parts: readonly Part[]): Part[][] =>
58
+ parts.reduce<Part[][]>((chains, part) => {
59
+ const chain = chains.at(-1);
60
+ if (chain !== undefined && continues(text, chain.at(-1), part)) chain.push(part);
61
+ else chains.push([part]);
62
+ return chains;
63
+ }, []);
64
+
65
+ /**
66
+ * 文 text の中の、日付・時刻の数の並び。tokens は文書全体の座標で、base は text の先頭の位置。
67
+ * 品詞が無ければ判断せず、何も返さない。
68
+ */
69
+ export const calendarRuns = (text: string, tokens: readonly Token[] | undefined, base: number, units: CalendarUnits): CalendarRun[] => {
70
+ if (tokens === undefined) return [];
71
+ const parts = digitRuns(text).flatMap((run) => partOf(text, run, tokens, base, units) ?? []);
72
+ return chainsOf(text, parts)
73
+ .filter((chain) => chain.some((part) => part.anchored))
74
+ .flatMap((chain) => chain.map((part, index): CalendarRun => ({ run: part.run, place: index === 0 ? "first" : "inner" })));
75
+ };
package/src/cli-args.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  * Options of the commands that take files to check, whose value is the next argument. That value is not a file.
3
3
  * `--why` (relax), `--format` and `--language` (tree) belong to commands that read their own arguments.
4
4
  */
5
- export const VALUE_FLAGS: readonly string[] = ["--sarif", "--genre", "--rule", "--line"];
5
+ const VALUE_FLAGS: ReadonlySet<string> = new Set(["--sarif", "--genre", "--rule", "--line"]);
6
6
 
7
7
  /** The arguments that name what to check: everything that is neither an option nor an option's value. */
8
8
  export const targetsOf = (args: readonly string[]): string[] =>
9
- args.filter((arg, index) => !arg.startsWith("--") && !(index > 0 && VALUE_FLAGS.includes(args[index - 1] ?? "")));
9
+ args.filter((arg, index) => !arg.startsWith("--") && !(index > 0 && VALUE_FLAGS.has(args[index - 1] ?? "")));
package/src/cli-text.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  // What the command line says, in Japanese and English. Per-file text follows the file's language; the rest
2
2
  // follows hostLanguage (chaff.yaml's language, else the terminal's locale).
3
3
  import type { Texts } from "./ui.ts";
4
+ import { counted, formFor } from "./render/plural.ts";
4
5
  import type { GenreSetting } from "./genre-check.ts";
6
+ import type { ProfileSetting } from "./profile/check.ts";
5
7
 
6
8
  /** Where a file's genre came from, as the header names it. */
7
9
  export type GenreSource = "--genre" | "by_path" | "config" | "default" | "front-matter" | "path" | "content";
@@ -12,11 +14,11 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
12
14
  chaff . この場所の Markdown を全部
13
15
  chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
14
16
  判定役は chaff.yaml の ai_backend で選びます
15
- chaff init chaff.yaml を作る
17
+ chaff init chaff.yaml を作る(端末ならジャンルを尋ねる。--genre <ジャンル> でも選べる)
16
18
  chaff --version chaffjs と言語パッケージの版
17
19
  chaff eval <dir> 手元の文書で閾値を測り直す
18
20
  chaff explain <rule> そのルールの意図と根拠を読む
19
- chaff genres ジャンルの一覧
21
+ chaff genres ジャンル(文書の種類)の一覧と、それぞれ何向けか
20
22
  chaff tree <file> [--format sexp|json] 文書を番地の付いた木にする(条・項・定義・参照)
21
23
  chaff cite <原文> <引用.json> 回答の引用(番地と引用文)が原文にあるかを確かめる
22
24
  chaff rules --json いまの設定を JSON で出す(AI に渡す用)
@@ -46,11 +48,11 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
46
48
  chaff . every Markdown file here
47
49
  chaff test <file|dir>... also run the checks that read meaning (needs an API key)
48
50
  the judge is chosen by ai_backend in chaff.yaml
49
- chaff init create chaff.yaml
51
+ chaff init create chaff.yaml (asks for the genre at a terminal; --genre <genre> chooses it)
50
52
  chaff --version the version of chaffjs and its language packages
51
53
  chaff eval <dir> re-measure the limits on your own documents
52
54
  chaff explain <rule> read what a rule is for and why
53
- chaff genres list the genres
55
+ chaff genres list the genres (kinds of document) and what each is for
54
56
  chaff tree <file> [--format sexp|json] the document as a tree of addresses (sections, clauses, definitions, references)
55
57
  chaff cite <source> <quotes.json> check that quoted passages (address and text) are in the source
56
58
  chaff rules --json the current settings as JSON (to give to an AI)
@@ -79,22 +81,25 @@ export type CliText = {
79
81
  readonly languageName: (language: string) => string;
80
82
  readonly genreSource: Readonly<Record<GenreSource, string>>;
81
83
  readonly header: (path: string, genre: string, language: string, from: string, shelved: number, hushed: number) => string;
84
+ readonly suggested: (name: string, genre: string) => string;
85
+ readonly suggestedNote: (name: string, genre: string, used: string) => string;
82
86
  readonly noMarkdown: (targets: string) => string;
83
87
  readonly noMarkdownHere: string;
84
88
  readonly noAdapter: (language: string) => string;
85
89
  readonly unknownGenre: (genre: string, where: string, known: readonly string[]) => string;
86
90
  readonly genreWhere: (where: GenreSetting, files: readonly string[]) => string;
87
91
  readonly unreadFrontMatterGenre: (path: string, genre: string, known: readonly string[]) => string;
92
+ readonly unknownProfile: (profile: string, where: string, known: readonly string[]) => string;
93
+ readonly profileWhere: (where: ProfileSetting, files: readonly string[]) => string;
88
94
  readonly unknownRule: (id: string) => string;
89
95
  readonly unknownRuleWithList: (id: string, list: string) => string;
90
96
  readonly unnamed: string;
91
97
  readonly sarifWritten: (path: string, count: number) => string;
92
98
  readonly unchanged: string;
93
- readonly findingsUnit: string;
99
+ readonly findingsUnit: (count: number) => string;
94
100
  readonly watching: (files: number, findings: number) => string;
95
101
  readonly watchHint: string;
96
102
  readonly baselineDone: (files: number, entries: number, file: string) => readonly string[];
97
- readonly genres: (list: readonly string[]) => string;
98
103
  readonly unit: (ruleId: string, language: string) => string;
99
104
  };
100
105
 
@@ -115,6 +120,9 @@ export const CLI_TEXT: Texts<CliText> = {
115
120
  [`${path} ${genre} · ${language} ジャンルは${from}から`, shelved > 0 ? ` 棚上げ ${shelved} 件` : "", hushed > 0 ? ` stet ${hushed} 件` : ""].join(
116
121
  "",
117
122
  ),
123
+ suggested: (name, genre) => ` ${name}のようです。--genre ${genre} を試せます`,
124
+ suggestedNote: (name, genre, used) =>
125
+ ` ジャンルを決めていないので、${used} として見ました。${name}なら、--genre ${genre} でその種類の書き方に合わせて見ます(npx chaffjs genres で一覧)。`,
118
126
  noMarkdown: (targets) => `Markdown が 1 つも見つかりませんでした: ${targets}`,
119
127
  noMarkdownHere: "Markdown が 1 つも見つかりませんでした。",
120
128
  noAdapter: (language) => `言語 "${language}" のアダプタがありません。`,
@@ -126,12 +134,15 @@ export const CLI_TEXT: Texts<CliText> = {
126
134
  },
127
135
  unreadFrontMatterGenre: (path, genre, known) =>
128
136
  `${path}: front matter の genre "${genre}" はジャンルではないので読みませんでした。使えるのは ${known.join("、")} です。`,
137
+ unknownProfile: (profile, where, known) =>
138
+ `文書の種類 "${profile}" はありません(${where})。使えるのは ${known.join("、")} と、種類を使わない none です。`,
139
+ profileWhere: (where, files) => (where === "config" ? "chaff.yaml の profile" : `chaff.yaml の by_path、files: ${files.join(", ")}`),
129
140
  unknownRule: (id) => `${id} というルールはありません。npx chaff rules --json で一覧が出ます。`,
130
141
  unknownRuleWithList: (id, list) => `${id} というルールはありません。\n一覧:\n${list}`,
131
142
  unnamed: "(名前なし)",
132
143
  sarifWritten: (path, count) => `\n SARIF を書きました: ${path}(${count} 件)`,
133
144
  unchanged: "変わりませんでした",
134
- findingsUnit: " 件",
145
+ findingsUnit: () => " 件",
135
146
  watching: (files, findings) => `\n ${files} ファイルを見ています。いまの指摘は ${findings} 件です。`,
136
147
  watchHint: " 保存するたびに、変わったところだけ出します。止めるには Ctrl-C。\n",
137
148
  baselineDone: (files, entries, file) => [
@@ -144,8 +155,6 @@ export const CLI_TEXT: Texts<CliText> = {
144
155
  ` ${file} を commit してください。`,
145
156
  "",
146
157
  ],
147
- genres: (list) =>
148
- ["", " 使えるジャンル:", ...list.map((genre) => ` ${genre}`), "", " chaff.yaml の genre に書くか、--genre で指定します。", ""].join("\n"),
149
158
  unit: (ruleId, language) => {
150
159
  if (ruleId === "bold-density") return "1000 字あたりの箇所数";
151
160
  if (ruleId !== "max-sentence-length") return "回";
@@ -166,6 +175,9 @@ export const CLI_TEXT: Texts<CliText> = {
166
175
  },
167
176
  header: (path, genre, language, from, shelved, hushed) =>
168
177
  [`${path} ${genre} · ${language} genre from ${from}`, shelved > 0 ? ` ${shelved} shelved` : "", hushed > 0 ? ` ${hushed} stet` : ""].join(""),
178
+ suggested: (name, genre) => ` Looks like: ${name}. Try --genre ${genre}`,
179
+ suggestedNote: (name, genre, used) =>
180
+ ` No genre was set, so this was checked as ${used}. If it is ${name}, --genre ${genre} checks it the way that kind is written (npx chaffjs genres lists them).`,
169
181
  noMarkdown: (targets) => `No Markdown files found: ${targets}`,
170
182
  noMarkdownHere: "No Markdown files found.",
171
183
  noAdapter: (language) => `No language package for "${language}".`,
@@ -176,25 +188,27 @@ export const CLI_TEXT: Texts<CliText> = {
176
188
  },
177
189
  unreadFrontMatterGenre: (path, genre, known) =>
178
190
  `${path}: the front matter's genre "${genre}" is not a genre, so it was not used. The genres are ${known.join(", ")}.`,
191
+ unknownProfile: (profile, where, known) =>
192
+ `There is no document profile "${profile}" (${where}). The profiles are ${known.join(", ")}; none uses no profile.`,
193
+ profileWhere: (where, files) => (where === "config" ? "profile in chaff.yaml" : `by_path in chaff.yaml, files: ${files.join(", ")}`),
179
194
  unknownRule: (id) => `There is no rule named ${id}. npx chaff rules --json lists them.`,
180
195
  unknownRuleWithList: (id, list) => `There is no rule named ${id}.\nRules:\n${list}`,
181
196
  unnamed: "(no name)",
182
- sarifWritten: (path, count) => `\n Wrote SARIF: ${path} (${count} finding${count === 1 ? "" : "s"})`,
197
+ sarifWritten: (path, count) => `\n Wrote SARIF: ${path} (${counted(count, "finding")})`,
183
198
  unchanged: "no change",
184
- findingsUnit: " findings",
185
- watching: (files, findings) => `\n Watching ${files} file${files === 1 ? "" : "s"}. ${findings} finding${findings === 1 ? "" : "s"} now.`,
199
+ findingsUnit: (count) => ` ${formFor(count, "finding", "findings")}`,
200
+ watching: (files, findings) => `\n Watching ${counted(files, "file")}. ${counted(findings, "finding")} now.`,
186
201
  watchHint: " Each save prints only what changed. Ctrl-C to stop.\n",
187
202
  baselineDone: (files, entries, file) => [
188
203
  "",
189
- ` Checked ${files} file${files === 1 ? "" : "s"}.`,
204
+ ` Checked ${counted(files, "file")}.`,
190
205
  "",
191
- ` Recorded ${entries} finding${entries === 1 ? "" : "s"} in ${file}.`,
206
+ ` Recorded ${counted(entries, "finding")} in ${file}.`,
192
207
  " They will not be reported again; only new ones will.",
193
208
  "",
194
209
  ` Commit ${file}.`,
195
210
  "",
196
211
  ],
197
- genres: (list) => ["", " Genres:", ...list.map((genre) => ` ${genre}`), "", " Set one with genre in chaff.yaml, or with --genre.", ""].join("\n"),
198
212
  unit: (ruleId, language) => {
199
213
  if (ruleId === "bold-density") return "places per 1000 characters";
200
214
  if (ruleId !== "max-sentence-length") return "times";
package/src/cli.ts CHANGED
@@ -16,14 +16,17 @@ import { clock, describeChange, snapshotOf, watchPaths, type Snapshot } from "./
16
16
  import { runEval } from "./commands/eval.ts";
17
17
  import { runTest } from "./commands/test.ts";
18
18
  import { GENRES } from "./genre.ts";
19
- import { unknownGenres, writtenGenres } from "./genre-check.ts";
20
19
  import { resolveGenre } from "./resolve-genre.ts";
21
20
  import { runInit } from "./init.ts";
21
+ import { initGenre } from "./commands/init-ask.ts";
22
22
  import { targetsOf } from "./cli-args.ts";
23
23
  import { loadRules } from "./rule-load.ts";
24
24
  import { renderCompact } from "./render/compact.ts";
25
25
  import { renderExplain } from "./render/explain.ts";
26
26
  import { renderFriendly } from "./render/friendly.ts";
27
+ import { renderGenres } from "./render/genres.ts";
28
+ import { loadGenres, presetLevels } from "./genre-load.ts";
29
+ import { fileHeader } from "./file-header.ts";
27
30
  import { rulesJson } from "./render/rules-json.ts";
28
31
  import { renderSarif } from "./render/sarif.ts";
29
32
  import { VERSION, VERSION_LINES } from "./version.ts";
@@ -33,26 +36,20 @@ import { runSkill } from "./commands/skill.ts";
33
36
  import { runFeedback, settingsOf } from "./commands/feedback.ts";
34
37
  import { homedir } from "node:os";
35
38
  import { ruleProblems } from "./config/rule-problems.ts";
39
+ import { nameProblems } from "./config/name-problems.ts";
36
40
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
37
41
  import { neededBy, runRules } from "./run.ts";
38
42
  import type { Level, RuleDefinition } from "./plugin.ts";
39
43
  import { CLI_TEXT, type CliText, type GenreSource } from "./cli-text.ts";
40
- import { hostLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
44
+ import { hostLanguage, sharedLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
41
45
  import { profileFor } from "./profile/for-file.ts";
46
+ import { settingProblems } from "./setting-problems.ts";
42
47
 
43
48
  /** Text for output that is not about one document. */
44
49
  const hostText = (config: Config): CliText => CLI_TEXT[hostLanguage(config.language, process.env)];
45
50
 
46
51
  const readConfig = (): Config => (existsSync(join(process.cwd(), CONFIG_FILE)) ? loadConfig(join(process.cwd(), CONFIG_FILE)) : EMPTY);
47
52
 
48
- const genreProblems = (command: string, argv: readonly string[]): string[] => {
49
- const config = readConfig();
50
- const text = hostText(config);
51
- return unknownGenres(writtenGenres(command, flag(argv, "--genre"), config), GENRES).map((entry) =>
52
- text.unknownGenre(entry.genre, text.genreWhere(entry.where, entry.files), GENRES),
53
- );
54
- };
55
-
56
53
  /** resolveGenre with this run's --genre, for the commands that take it as a dependency. */
57
54
  const genreFrom =
58
55
  (argv: readonly string[]) =>
@@ -90,12 +87,6 @@ type Inspected = {
90
87
  readonly all: readonly string[];
91
88
  };
92
89
 
93
- /** The first line for a file, in the file's language. */
94
- const headerFor = (path: string, genre: string, from: GenreSource, language: string, shelved: number, hushed: number): string => {
95
- const text = CLI_TEXT[uiLanguageOf(language)];
96
- return text.header(path, genre, text.languageName(language), text.genreSource[from], shelved, hushed);
97
- };
98
-
99
90
  const inspect = async (path: string, config: Config, argv: readonly string[]): Promise<Inspected> => {
100
91
  const source = plainSource(await readFile(path, "utf8"));
101
92
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
@@ -105,7 +96,7 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
105
96
  const rules = loadRules(language);
106
97
  const experimental = config.experimental || argv.includes("--experimental");
107
98
  await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
108
- const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
99
+ const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
109
100
  const raw = runRules(doc, rules, config.rules, experimental, genre, config.limits);
110
101
  // 応答は 3 つ。stet で黙らせたものは、ここで落とす。
111
102
  const applied = applySuppressions(
@@ -116,8 +107,8 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
116
107
  const baseline = argv.includes("--show-baseline") ? undefined : readBaseline(join(process.cwd(), BASELINE_FILE));
117
108
  const split = splitByBaseline(path, applied.kept, baseline);
118
109
  const result = { ...raw, findings: split.fresh };
119
- const header = headerFor(path, genre, from, language, split.shelved, applied.suppressed.length);
120
- const text = argv.includes("--compact") ? renderCompact(header, result, rules, language) : renderFriendly(header, result, rules, language);
110
+ const { header, notes } = fileHeader(path, source, language, { genre, from, unread }, { shelved: split.shelved, hushed: applied.suppressed.length });
111
+ const text = argv.includes("--compact") ? renderCompact(header, result, rules, language) : renderFriendly(header, result, rules, language, notes);
121
112
  return {
122
113
  text,
123
114
  rules,
@@ -147,14 +138,14 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[]): voi
147
138
 
148
139
  /** 効いていない設定は、結果の前に一度だけ言う。標準エラーに出すので、JSON や SARIF の出力は汚さない。 */
149
140
  const warnRuleProblems = (config: Config, language: string): void => {
150
- ruleProblems(config, loadRules(language), hostLanguage(config.language, process.env)).forEach((problem) => console.error(`chaff: ${problem}`));
141
+ const ui = hostLanguage(config.language, process.env);
142
+ [...ruleProblems(config, loadRules(language), ui), ...nameProblems(config, ui)].forEach((problem) => console.error(`chaff: ${problem}`));
151
143
  };
152
144
 
153
145
  /** Several files end with one summary: in their language when they share one, else the host's. */
154
146
  const summaryLanguage = (results: readonly Inspected[], config: Config): UiLanguage => {
155
- const languages = new Set(results.map((result) => uiLanguageOf(result.language)));
156
- const [only] = [...languages];
157
- return languages.size === 1 && only !== undefined ? only : hostLanguage(config.language, process.env);
147
+ const languages = results.map((result) => result.language);
148
+ return sharedLanguage(languages, hostLanguage(config.language, process.env));
158
149
  };
159
150
 
160
151
  const lint = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
@@ -216,8 +207,10 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
216
207
  return new Promise(() => undefined);
217
208
  };
218
209
 
219
- const explain = (ruleId: string | undefined): number => {
210
+ /** genreFlag: --genre, which wins over chaff.yaml's genre here as it does in a run. */
211
+ const explain = (ruleId: string | undefined, genreFlag: string | undefined): number => {
220
212
  const config = readConfig();
213
+ const genre = genreFlag ?? config.genre;
221
214
  // The rule's limits differ by language (characters for Japanese, words for English): explain in the one being written.
222
215
  const language = config.language ?? hostLanguage(undefined, process.env);
223
216
  const text = CLI_TEXT[uiLanguageOf(language)];
@@ -228,8 +221,9 @@ const explain = (ruleId: string | undefined): number => {
228
221
  console.error(text.unknownRuleWithList(ruleId ?? text.unnamed, list));
229
222
  return 1;
230
223
  }
231
- const current = config.rules[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
232
- console.log(renderExplain(rule, current, language, text.unit(rule.id, language), config.genre));
224
+ const preset = genre === undefined ? {} : presetLevels(genre);
225
+ const current = config.rules[rule.id] ?? preset[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
226
+ console.log(renderExplain(rule, current, language, text.unit(rule.id, language), genre));
233
227
  return 0;
234
228
  };
235
229
 
@@ -266,16 +260,16 @@ type Handler = (argv: readonly string[]) => number | Promise<number>;
266
260
  /** `--` で始まらない引数。対象のパス。 */
267
261
  const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
268
262
 
269
- const showRules = (): number => {
263
+ const showRules = (genreFlag: string | undefined): number => {
270
264
  const config = readConfig();
271
265
  const language = config.language ?? hostLanguage(undefined, process.env);
272
266
  warnRuleProblems(config, language);
273
- console.log(rulesJson(loadRules(language), config, language, config.genre ?? "blog/tech"));
267
+ console.log(rulesJson(loadRules(language), config, language, genreFlag ?? config.genre ?? "blog/tech"));
274
268
  return 0;
275
269
  };
276
270
 
277
271
  const showGenres = (): number => {
278
- console.log(hostText(readConfig()).genres(GENRES));
272
+ console.log(renderGenres(loadGenres(), hostLanguage(readConfig().language, process.env)));
279
273
  return 0;
280
274
  };
281
275
 
@@ -284,22 +278,28 @@ const treeContext = (): TreeContext => {
284
278
  return { config, flag, ui: hostLanguage(config.language, process.env) };
285
279
  };
286
280
 
281
+ /** What eval and test share: the settings, this run's genre, and the language for what is not about one document. */
282
+ const measureContext = (argv: readonly string[]): { config: Config; resolveGenre: ReturnType<typeof genreFrom>; ui: UiLanguage } => {
283
+ const config = readConfig();
284
+ return { config, resolveGenre: genreFrom(argv), ui: hostLanguage(config.language, process.env) };
285
+ };
286
+
287
287
  /** 分岐を数珠つなぎにせず表にする。足すときに main を太らせない。 */
288
288
  const HANDLERS: Readonly<Record<string, Handler>> = {
289
- init: (argv) => {
290
- runInit(process.cwd(), flag(argv, "--genre") ?? "blog/tech", hostLanguage(readConfig().language, process.env)).forEach((line) => console.log(line));
291
- return 0;
289
+ init: async (argv) => {
290
+ const ui = hostLanguage(readConfig().language, process.env);
291
+ const chosen = await initGenre(flag(argv, "--genre"), ui, process.cwd());
292
+ if ("error" in chosen) console.error(chosen.error);
293
+ else runInit(process.cwd(), chosen.genre, ui).forEach((line) => console.log(line));
294
+ return "error" in chosen ? 1 : 0;
292
295
  },
293
296
  genres: showGenres,
294
- rules: showRules,
295
- explain: (argv) => explain(argv[1]),
296
- eval: (argv) => {
297
- const config = readConfig();
298
- return runEval(positional(argv), argv, { config, resolveGenre: genreFrom(argv), flag, ui: hostLanguage(config.language, process.env) });
299
- },
297
+ rules: (argv) => showRules(flag(argv, "--genre")),
298
+ explain: (argv) => explain(argv[1], flag(argv, "--genre")),
299
+ eval: (argv) => runEval(positional(argv), argv, { ...measureContext(argv), flag }),
300
300
  tree: (argv) => runTree(treeTargets(argv), argv, treeContext()),
301
301
  cite: (argv) => runCite(citeTargets(argv), argv, treeContext()),
302
- test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), inspect }),
302
+ test: (argv) => runTest(positional(argv), argv, { ...measureContext(argv), inspect }),
303
303
  baseline: (argv) => runBaseline(positional(argv), argv),
304
304
  suppressions: (argv) => runSuppressions(positional(argv), argv),
305
305
  relax: (argv) => changeSetting("relaxed", argv[1], flag(argv, "--why")),
@@ -336,8 +336,9 @@ export const main = async (argv: readonly string[]): Promise<number> => {
336
336
  console.log(VERSION_LINES.join("\n"));
337
337
  return 0;
338
338
  }
339
- // 知らないジャンルでは、どの rule も当たらず、動かなかった rule も並ばない。「指摘なし」が素通りに見えるので、何かする前に止める。
340
- const problems = genreProblems(first, argv);
339
+ // 知らないジャンルではどの rule も当たらず、知らない文書の種類では種類の知識が外れる。どちらも素通りに見えるので、何かする前に止める。
340
+ const config = readConfig();
341
+ const problems = settingProblems(first, flag(argv, "--genre"), config, hostText(config));
341
342
  problems.forEach((problem) => console.error(problem));
342
343
  if (problems.length > 0) return 1;
343
344
  const handler = HANDLERS[first];
@@ -2,12 +2,12 @@ import { checkCitations, type Citation, type CitationResult } from "../structure
2
2
  import { readSource, readTree, type TreeContext } from "./tree.ts";
3
3
  import type { Texts, UiLanguage } from "../ui.ts";
4
4
 
5
- const FORMATS = ["text", "json"];
5
+ const FORMATS: ReadonlySet<string> = new Set(["text", "json"]);
6
6
  const QUOTE_WIDTH = 40;
7
- const VALUED = ["--format", "--language"];
7
+ const VALUED: ReadonlySet<string> = new Set(["--format", "--language", "--genre"]);
8
8
 
9
9
  export const citeTargets = (argv: readonly string[]): string[] =>
10
- argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.includes(all[index - 1] ?? ""));
10
+ argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
11
11
 
12
12
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
13
13
 
@@ -88,7 +88,7 @@ const describe = (result: CitationResult, text: CiteText): string => {
88
88
  export const runCite = async (targets: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
89
89
  const [sourcePath, citationsPath] = targets;
90
90
  const format = context.flag(argv, "--format") ?? "text";
91
- if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.includes(format)) {
91
+ if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.has(format)) {
92
92
  console.error(TEXT[context.ui ?? "ja"].usage);
93
93
  return 1;
94
94
  }
@@ -56,7 +56,7 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
56
56
  const adapter = await loadAdapter(language);
57
57
  const { genre } = resolveGenre(path, source, config);
58
58
  await adapter.prepare?.(neededBy(loadRules(language), config.rules, config.experimental, genre, language));
59
- return { doc: buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language)), language, genre };
59
+ return { doc: buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre)), language, genre };
60
60
  }),
61
61
  );
62
62
  const language = docs[0]?.language ?? "ja";
@@ -0,0 +1,53 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { createInterface, type Interface } from "node:readline";
4
+ import { CONFIG_FILE } from "../config/load.ts";
5
+ import { loadGenres } from "../genre-load.ts";
6
+ import { choicePrompt, DEFAULT_GENRE, genreChoices, genreFromAnswer, noAnswer, notAGenre } from "../init-choice.ts";
7
+ import type { UiLanguage } from "../ui.ts";
8
+
9
+ type Terminal = { readonly isTTY?: boolean | undefined };
10
+
11
+ /** Where init asks: the terminal, or streams a test gives it. */
12
+ export type Prompter = { readonly input: NodeJS.ReadableStream & Terminal; readonly output: NodeJS.WritableStream & Terminal };
13
+
14
+ /** Ask only at a terminal, and only when init will write chaff.yaml: a script or CI gets the default without waiting. */
15
+ export const shouldAsk = (stdin: Terminal, stdout: Terminal, configExists: boolean): boolean => stdin.isTTY === true && stdout.isTTY === true && !configExists;
16
+
17
+ /** The first line typed, or undefined when the input ends with none (Ctrl-D): waiting for a question's answer would never end then. */
18
+ const answerOf = (reader: Interface, prompt: string): Promise<string | undefined> => {
19
+ const answer = new Promise<string | undefined>((resolve) => {
20
+ reader.once("line", resolve);
21
+ reader.once("close", () => resolve(undefined));
22
+ });
23
+ reader.setPrompt(prompt);
24
+ reader.prompt();
25
+ return answer;
26
+ };
27
+
28
+ const ask = async (ui: UiLanguage, io: Prompter): Promise<{ readonly genre: string | undefined; readonly answer: string | undefined }> => {
29
+ const data = loadGenres();
30
+ io.output.write(`${genreChoices(data, ui).join("\n")}\n`);
31
+ const reader = createInterface({ input: io.input, output: io.output });
32
+ try {
33
+ const answer = await answerOf(reader, choicePrompt(ui));
34
+ const ids = data.genres.map((genre) => genre.id);
35
+ return { genre: answer === undefined ? undefined : genreFromAnswer(answer, ids), answer };
36
+ } finally {
37
+ reader.close();
38
+ }
39
+ };
40
+
41
+ /** The genre init writes: --genre, else the one picked at the prompt, else the default. An answer that is no genre is an error. */
42
+ export const initGenre = async (
43
+ flagged: string | undefined,
44
+ ui: UiLanguage,
45
+ dir: string,
46
+ io: Prompter = { input: process.stdin, output: process.stdout },
47
+ ): Promise<{ readonly genre: string } | { readonly error: string }> => {
48
+ if (flagged !== undefined) return { genre: flagged };
49
+ if (!shouldAsk(io.input, io.output, existsSync(join(dir, CONFIG_FILE)))) return { genre: DEFAULT_GENRE };
50
+ const picked = await ask(ui, io);
51
+ if (picked.answer === undefined) return { error: noAnswer(ui) };
52
+ return picked.genre === undefined ? { error: notAGenre(picked.answer.trim(), ui) } : { genre: picked.genre };
53
+ };