chaffjs 0.14.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 (602) hide show
  1. package/README.md +27 -2
  2. package/dist/address-chain.d.ts +4 -1
  3. package/dist/address-chain.d.ts.map +1 -1
  4. package/dist/address-chain.js +6 -3
  5. package/dist/address-chain.js.map +1 -1
  6. package/dist/backends/anthropic.d.ts +2 -1
  7. package/dist/backends/anthropic.d.ts.map +1 -1
  8. package/dist/backends/anthropic.js +4 -1
  9. package/dist/backends/anthropic.js.map +1 -1
  10. package/dist/backends/openai.d.ts +2 -1
  11. package/dist/backends/openai.d.ts.map +1 -1
  12. package/dist/backends/openai.js +1 -1
  13. package/dist/backends/openai.js.map +1 -1
  14. package/dist/backends/types.d.ts +2 -1
  15. package/dist/backends/types.d.ts.map +1 -1
  16. package/dist/backends/types.js +1 -1
  17. package/dist/backends/types.js.map +1 -1
  18. package/dist/baseline.js +1 -1
  19. package/dist/baseline.js.map +1 -1
  20. package/dist/calendar-number.d.ts +26 -0
  21. package/dist/calendar-number.d.ts.map +1 -0
  22. package/dist/calendar-number.js +51 -0
  23. package/dist/calendar-number.js.map +1 -0
  24. package/dist/cli-args.d.ts +0 -5
  25. package/dist/cli-args.d.ts.map +1 -1
  26. package/dist/cli-args.js +2 -2
  27. package/dist/cli-args.js.map +1 -1
  28. package/dist/cli-text.d.ts +10 -3
  29. package/dist/cli-text.d.ts.map +1 -1
  30. package/dist/cli-text.js +33 -14
  31. package/dist/cli-text.js.map +1 -1
  32. package/dist/cli.d.ts.map +1 -1
  33. package/dist/cli.js +53 -47
  34. package/dist/cli.js.map +1 -1
  35. package/dist/commands/cite.d.ts.map +1 -1
  36. package/dist/commands/cite.js +4 -4
  37. package/dist/commands/cite.js.map +1 -1
  38. package/dist/commands/eval.d.ts +4 -0
  39. package/dist/commands/eval.d.ts.map +1 -1
  40. package/dist/commands/eval.js +21 -7
  41. package/dist/commands/eval.js.map +1 -1
  42. package/dist/commands/feedback.d.ts.map +1 -1
  43. package/dist/commands/feedback.js +2 -1
  44. package/dist/commands/feedback.js.map +1 -1
  45. package/dist/commands/init-ask.d.ts +19 -0
  46. package/dist/commands/init-ask.d.ts.map +1 -0
  47. package/dist/commands/init-ask.js +43 -0
  48. package/dist/commands/init-ask.js.map +1 -0
  49. package/dist/commands/test.d.ts +8 -0
  50. package/dist/commands/test.d.ts.map +1 -1
  51. package/dist/commands/test.js +73 -40
  52. package/dist/commands/test.js.map +1 -1
  53. package/dist/commands/tree.d.ts +1 -8
  54. package/dist/commands/tree.d.ts.map +1 -1
  55. package/dist/commands/tree.js +9 -7
  56. package/dist/commands/tree.js.map +1 -1
  57. package/dist/config/by-path.d.ts.map +1 -1
  58. package/dist/config/by-path.js +10 -9
  59. package/dist/config/by-path.js.map +1 -1
  60. package/dist/config/load.d.ts +4 -0
  61. package/dist/config/load.d.ts.map +1 -1
  62. package/dist/config/load.js +19 -0
  63. package/dist/config/load.js.map +1 -1
  64. package/dist/config/name-problems.d.ts +5 -0
  65. package/dist/config/name-problems.d.ts.map +1 -0
  66. package/dist/config/name-problems.js +11 -0
  67. package/dist/config/name-problems.js.map +1 -0
  68. package/dist/date-stamp.js +1 -1
  69. package/dist/date-stamp.js.map +1 -1
  70. package/dist/detectors/acronym-compound.d.ts +8 -0
  71. package/dist/detectors/acronym-compound.d.ts.map +1 -0
  72. package/dist/detectors/acronym-compound.js +8 -0
  73. package/dist/detectors/acronym-compound.js.map +1 -0
  74. package/dist/detectors/acronym-context.d.ts +4 -0
  75. package/dist/detectors/acronym-context.d.ts.map +1 -1
  76. package/dist/detectors/acronym-context.js +17 -1
  77. package/dist/detectors/acronym-context.js.map +1 -1
  78. package/dist/detectors/acronym-expansion.d.ts +10 -0
  79. package/dist/detectors/acronym-expansion.d.ts.map +1 -1
  80. package/dist/detectors/acronym-expansion.js +63 -5
  81. package/dist/detectors/acronym-expansion.js.map +1 -1
  82. package/dist/detectors/char-shape.d.ts.map +1 -1
  83. package/dist/detectors/char-shape.js +47 -15
  84. package/dist/detectors/char-shape.js.map +1 -1
  85. package/dist/detectors/citation-key.d.ts +13 -0
  86. package/dist/detectors/citation-key.d.ts.map +1 -0
  87. package/dist/detectors/citation-key.js +9 -0
  88. package/dist/detectors/citation-key.js.map +1 -0
  89. package/dist/detectors/cited-name.d.ts +11 -0
  90. package/dist/detectors/cited-name.d.ts.map +1 -0
  91. package/dist/detectors/cited-name.js +15 -0
  92. package/dist/detectors/cited-name.js.map +1 -0
  93. package/dist/detectors/concrete-evidence.d.ts +7 -0
  94. package/dist/detectors/concrete-evidence.d.ts.map +1 -0
  95. package/dist/detectors/concrete-evidence.js +21 -0
  96. package/dist/detectors/concrete-evidence.js.map +1 -0
  97. package/dist/detectors/counter-edge.d.ts +8 -0
  98. package/dist/detectors/counter-edge.d.ts.map +1 -0
  99. package/dist/detectors/counter-edge.js +27 -0
  100. package/dist/detectors/counter-edge.js.map +1 -0
  101. package/dist/detectors/dotted-name.d.ts +13 -0
  102. package/dist/detectors/dotted-name.d.ts.map +1 -0
  103. package/dist/detectors/dotted-name.js +10 -0
  104. package/dist/detectors/dotted-name.js.map +1 -0
  105. package/dist/detectors/doubled-word.d.ts +6 -1
  106. package/dist/detectors/doubled-word.d.ts.map +1 -1
  107. package/dist/detectors/doubled-word.js +22 -2
  108. package/dist/detectors/doubled-word.js.map +1 -1
  109. package/dist/detectors/en-shape.d.ts +1 -1
  110. package/dist/detectors/en-shape.d.ts.map +1 -1
  111. package/dist/detectors/en-shape.js +112 -29
  112. package/dist/detectors/en-shape.js.map +1 -1
  113. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  114. package/dist/detectors/enumerated-runs.js +4 -2
  115. package/dist/detectors/enumerated-runs.js.map +1 -1
  116. package/dist/detectors/gram-place.d.ts +0 -4
  117. package/dist/detectors/gram-place.d.ts.map +1 -1
  118. package/dist/detectors/gram-place.js +7 -2
  119. package/dist/detectors/gram-place.js.map +1 -1
  120. package/dist/detectors/gram-predicate.d.ts +7 -0
  121. package/dist/detectors/gram-predicate.d.ts.map +1 -0
  122. package/dist/detectors/gram-predicate.js +44 -0
  123. package/dist/detectors/gram-predicate.js.map +1 -0
  124. package/dist/detectors/heading-case.d.ts +21 -0
  125. package/dist/detectors/heading-case.d.ts.map +1 -0
  126. package/dist/detectors/heading-case.js +40 -0
  127. package/dist/detectors/heading-case.js.map +1 -0
  128. package/dist/detectors/heading-echo.d.ts.map +1 -1
  129. package/dist/detectors/heading-echo.js +4 -1
  130. package/dist/detectors/heading-echo.js.map +1 -1
  131. package/dist/detectors/index.d.ts.map +1 -1
  132. package/dist/detectors/index.js +4 -3
  133. package/dist/detectors/index.js.map +1 -1
  134. package/dist/detectors/lettered-index.d.ts +9 -0
  135. package/dist/detectors/lettered-index.d.ts.map +1 -0
  136. package/dist/detectors/lettered-index.js +54 -0
  137. package/dist/detectors/lettered-index.js.map +1 -0
  138. package/dist/detectors/lexicon.d.ts +5 -1
  139. package/dist/detectors/lexicon.d.ts.map +1 -1
  140. package/dist/detectors/lexicon.js +51 -5
  141. package/dist/detectors/lexicon.js.map +1 -1
  142. package/dist/detectors/name-numeral.d.ts +16 -0
  143. package/dist/detectors/name-numeral.d.ts.map +1 -0
  144. package/dist/detectors/name-numeral.js +30 -0
  145. package/dist/detectors/name-numeral.js.map +1 -0
  146. package/dist/detectors/name-part.d.ts +8 -0
  147. package/dist/detectors/name-part.d.ts.map +1 -0
  148. package/dist/detectors/name-part.js +20 -0
  149. package/dist/detectors/name-part.js.map +1 -0
  150. package/dist/detectors/name-run.d.ts +8 -0
  151. package/dist/detectors/name-run.d.ts.map +1 -0
  152. package/dist/detectors/name-run.js +13 -0
  153. package/dist/detectors/name-run.js.map +1 -0
  154. package/dist/detectors/orthography.d.ts.map +1 -1
  155. package/dist/detectors/orthography.js +35 -7
  156. package/dist/detectors/orthography.js.map +1 -1
  157. package/dist/detectors/phrase-match.d.ts +3 -1
  158. package/dist/detectors/phrase-match.d.ts.map +1 -1
  159. package/dist/detectors/phrase-match.js +20 -15
  160. package/dist/detectors/phrase-match.js.map +1 -1
  161. package/dist/detectors/placeholder.d.ts +14 -0
  162. package/dist/detectors/placeholder.d.ts.map +1 -0
  163. package/dist/detectors/placeholder.js +24 -0
  164. package/dist/detectors/placeholder.js.map +1 -0
  165. package/dist/detectors/quoted-variant.d.ts +7 -0
  166. package/dist/detectors/quoted-variant.d.ts.map +1 -0
  167. package/dist/detectors/quoted-variant.js +18 -0
  168. package/dist/detectors/quoted-variant.js.map +1 -0
  169. package/dist/detectors/repeated-head.d.ts.map +1 -1
  170. package/dist/detectors/repeated-head.js +5 -4
  171. package/dist/detectors/repeated-head.js.map +1 -1
  172. package/dist/detectors/series-label.d.ts +13 -0
  173. package/dist/detectors/series-label.d.ts.map +1 -0
  174. package/dist/detectors/series-label.js +26 -0
  175. package/dist/detectors/series-label.js.map +1 -0
  176. package/dist/detectors/signals.d.ts.map +1 -1
  177. package/dist/detectors/signals.js +44 -29
  178. package/dist/detectors/signals.js.map +1 -1
  179. package/dist/detectors/stacked-hedge.d.ts +23 -0
  180. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  181. package/dist/detectors/stacked-hedge.js +64 -0
  182. package/dist/detectors/stacked-hedge.js.map +1 -0
  183. package/dist/detectors/structure-tree.js +1 -1
  184. package/dist/detectors/structure-tree.js.map +1 -1
  185. package/dist/detectors/superlative-amount.d.ts +9 -0
  186. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  187. package/dist/detectors/superlative-amount.js +20 -0
  188. package/dist/detectors/superlative-amount.js.map +1 -0
  189. package/dist/detectors/superlative-clause.d.ts +18 -0
  190. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  191. package/dist/detectors/superlative-clause.js +61 -0
  192. package/dist/detectors/superlative-clause.js.map +1 -0
  193. package/dist/detectors/superlative-name.d.ts +9 -0
  194. package/dist/detectors/superlative-name.d.ts.map +1 -0
  195. package/dist/detectors/superlative-name.js +24 -0
  196. package/dist/detectors/superlative-name.js.map +1 -0
  197. package/dist/detectors/superlative-scope.d.ts +6 -0
  198. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  199. package/dist/detectors/superlative-scope.js +3 -3
  200. package/dist/detectors/superlative-scope.js.map +1 -1
  201. package/dist/detectors/team.d.ts +0 -6
  202. package/dist/detectors/team.d.ts.map +1 -1
  203. package/dist/detectors/team.js +17 -1
  204. package/dist/detectors/team.js.map +1 -1
  205. package/dist/detectors/token-shape.d.ts.map +1 -1
  206. package/dist/detectors/token-shape.js +15 -5
  207. package/dist/detectors/token-shape.js.map +1 -1
  208. package/dist/document.d.ts +3 -0
  209. package/dist/document.d.ts.map +1 -1
  210. package/dist/document.js +28 -7
  211. package/dist/document.js.map +1 -1
  212. package/dist/eval.js +1 -1
  213. package/dist/eval.js.map +1 -1
  214. package/dist/feedback/draft.js +1 -1
  215. package/dist/feedback/draft.js.map +1 -1
  216. package/dist/feedback/excerpt.d.ts +0 -1
  217. package/dist/feedback/excerpt.d.ts.map +1 -1
  218. package/dist/feedback/excerpt.js +2 -2
  219. package/dist/feedback/excerpt.js.map +1 -1
  220. package/dist/file-header.d.ts +20 -0
  221. package/dist/file-header.d.ts.map +1 -0
  222. package/dist/file-header.js +21 -0
  223. package/dist/file-header.js.map +1 -0
  224. package/dist/files.js +3 -3
  225. package/dist/files.js.map +1 -1
  226. package/dist/genre-check.d.ts +34 -0
  227. package/dist/genre-check.d.ts.map +1 -0
  228. package/dist/genre-check.js +21 -0
  229. package/dist/genre-check.js.map +1 -0
  230. package/dist/genre-load.d.ts +6 -0
  231. package/dist/genre-load.d.ts.map +1 -0
  232. package/dist/genre-load.js +22 -0
  233. package/dist/genre-load.js.map +1 -0
  234. package/dist/genre-parse.d.ts +40 -0
  235. package/dist/genre-parse.d.ts.map +1 -0
  236. package/dist/genre-parse.js +104 -0
  237. package/dist/genre-parse.js.map +1 -0
  238. package/dist/genre-suggest.d.ts +15 -0
  239. package/dist/genre-suggest.d.ts.map +1 -0
  240. package/dist/genre-suggest.js +26 -0
  241. package/dist/genre-suggest.js.map +1 -0
  242. package/dist/genre.d.ts +8 -1
  243. package/dist/genre.d.ts.map +1 -1
  244. package/dist/genre.js +19 -5
  245. package/dist/genre.js.map +1 -1
  246. package/dist/init-choice.d.ts +12 -0
  247. package/dist/init-choice.d.ts.map +1 -0
  248. package/dist/init-choice.js +41 -0
  249. package/dist/init-choice.js.map +1 -0
  250. package/dist/init.d.ts +0 -1
  251. package/dist/init.d.ts.map +1 -1
  252. package/dist/init.js +16 -17
  253. package/dist/init.js.map +1 -1
  254. package/dist/joined-view.d.ts +25 -0
  255. package/dist/joined-view.d.ts.map +1 -0
  256. package/dist/joined-view.js +76 -0
  257. package/dist/joined-view.js.map +1 -0
  258. package/dist/judge.d.ts +2 -1
  259. package/dist/judge.d.ts.map +1 -1
  260. package/dist/judge.js +1 -1
  261. package/dist/judge.js.map +1 -1
  262. package/dist/line-continues.d.ts +6 -0
  263. package/dist/line-continues.d.ts.map +1 -0
  264. package/dist/line-continues.js +33 -0
  265. package/dist/line-continues.js.map +1 -0
  266. package/dist/line-paragraphs.d.ts +4 -0
  267. package/dist/line-paragraphs.d.ts.map +1 -0
  268. package/dist/line-paragraphs.js +69 -0
  269. package/dist/line-paragraphs.js.map +1 -0
  270. package/dist/mask.d.ts.map +1 -1
  271. package/dist/mask.js +7 -6
  272. package/dist/mask.js.map +1 -1
  273. package/dist/measure.d.ts +1 -0
  274. package/dist/measure.d.ts.map +1 -1
  275. package/dist/measure.js +5 -1
  276. package/dist/measure.js.map +1 -1
  277. package/dist/number-name.d.ts +6 -3
  278. package/dist/number-name.d.ts.map +1 -1
  279. package/dist/number-name.js +31 -14
  280. package/dist/number-name.js.map +1 -1
  281. package/dist/orthography.d.ts +4 -0
  282. package/dist/orthography.d.ts.map +1 -1
  283. package/dist/orthography.js +11 -9
  284. package/dist/orthography.js.map +1 -1
  285. package/dist/plain-source.d.ts +6 -0
  286. package/dist/plain-source.d.ts.map +1 -0
  287. package/dist/plain-source.js +6 -0
  288. package/dist/plain-source.js.map +1 -0
  289. package/dist/plugin.d.ts +22 -1
  290. package/dist/plugin.d.ts.map +1 -1
  291. package/dist/profile/check.d.ts +28 -0
  292. package/dist/profile/check.d.ts.map +1 -0
  293. package/dist/profile/check.js +12 -0
  294. package/dist/profile/check.js.map +1 -0
  295. package/dist/profile/for-file.d.ts +2 -2
  296. package/dist/profile/for-file.d.ts.map +1 -1
  297. package/dist/profile/for-file.js +9 -2
  298. package/dist/profile/for-file.js.map +1 -1
  299. package/dist/profile/load.js +1 -1
  300. package/dist/profile/load.js.map +1 -1
  301. package/dist/profile/select.d.ts +4 -2
  302. package/dist/profile/select.d.ts.map +1 -1
  303. package/dist/profile/select.js +8 -3
  304. package/dist/profile/select.js.map +1 -1
  305. package/dist/quoted-span.d.ts +22 -0
  306. package/dist/quoted-span.d.ts.map +1 -0
  307. package/dist/quoted-span.js +25 -0
  308. package/dist/quoted-span.js.map +1 -0
  309. package/dist/reasons.d.ts +1 -0
  310. package/dist/reasons.d.ts.map +1 -1
  311. package/dist/reasons.js +4 -1
  312. package/dist/reasons.js.map +1 -1
  313. package/dist/render/compact.d.ts.map +1 -1
  314. package/dist/render/compact.js +1 -2
  315. package/dist/render/compact.js.map +1 -1
  316. package/dist/render/eval.d.ts +2 -1
  317. package/dist/render/eval.d.ts.map +1 -1
  318. package/dist/render/eval.js +85 -43
  319. package/dist/render/eval.js.map +1 -1
  320. package/dist/render/friendly.d.ts +2 -1
  321. package/dist/render/friendly.d.ts.map +1 -1
  322. package/dist/render/friendly.js +14 -6
  323. package/dist/render/friendly.js.map +1 -1
  324. package/dist/render/genres.d.ts +5 -0
  325. package/dist/render/genres.d.ts.map +1 -0
  326. package/dist/render/genres.js +28 -0
  327. package/dist/render/genres.js.map +1 -0
  328. package/dist/render/plural.d.ts +3 -0
  329. package/dist/render/plural.d.ts.map +1 -0
  330. package/dist/render/plural.js +5 -0
  331. package/dist/render/plural.js.map +1 -0
  332. package/dist/render/rules-json.d.ts.map +1 -1
  333. package/dist/render/rules-json.js +9 -3
  334. package/dist/render/rules-json.js.map +1 -1
  335. package/dist/render/semantic.d.ts +3 -2
  336. package/dist/render/semantic.d.ts.map +1 -1
  337. package/dist/render/semantic.js +94 -34
  338. package/dist/render/semantic.js.map +1 -1
  339. package/dist/render/summary.d.ts.map +1 -1
  340. package/dist/render/summary.js +4 -3
  341. package/dist/render/summary.js.map +1 -1
  342. package/dist/render/suppressions.d.ts.map +1 -1
  343. package/dist/render/suppressions.js +3 -2
  344. package/dist/render/suppressions.js.map +1 -1
  345. package/dist/render/text.d.ts +2 -0
  346. package/dist/render/text.d.ts.map +1 -1
  347. package/dist/render/text.js +11 -3
  348. package/dist/render/text.js.map +1 -1
  349. package/dist/resolve-genre.d.ts +11 -0
  350. package/dist/resolve-genre.d.ts.map +1 -0
  351. package/dist/resolve-genre.js +18 -0
  352. package/dist/resolve-genre.js.map +1 -0
  353. package/dist/rule-load.d.ts.map +1 -1
  354. package/dist/rule-load.js +4 -1
  355. package/dist/rule-load.js.map +1 -1
  356. package/dist/run-semantic.js +1 -1
  357. package/dist/run-semantic.js.map +1 -1
  358. package/dist/run.d.ts +2 -0
  359. package/dist/run.d.ts.map +1 -1
  360. package/dist/run.js +25 -15
  361. package/dist/run.js.map +1 -1
  362. package/dist/sentence-shape.d.ts +0 -1
  363. package/dist/sentence-shape.d.ts.map +1 -1
  364. package/dist/sentence-shape.js +3 -5
  365. package/dist/sentence-shape.js.map +1 -1
  366. package/dist/setting-problems.d.ts +5 -0
  367. package/dist/setting-problems.d.ts.map +1 -0
  368. package/dist/setting-problems.js +15 -0
  369. package/dist/setting-problems.js.map +1 -0
  370. package/dist/soft-break.d.ts +19 -0
  371. package/dist/soft-break.d.ts.map +1 -0
  372. package/dist/soft-break.js +78 -0
  373. package/dist/soft-break.js.map +1 -0
  374. package/dist/structure/build.d.ts.map +1 -1
  375. package/dist/structure/build.js +24 -14
  376. package/dist/structure/build.js.map +1 -1
  377. package/dist/structure/cite.js +1 -1
  378. package/dist/structure/cite.js.map +1 -1
  379. package/dist/structure/date-order.d.ts +3 -0
  380. package/dist/structure/date-order.d.ts.map +1 -1
  381. package/dist/structure/date-order.js +9 -1
  382. package/dist/structure/date-order.js.map +1 -1
  383. package/dist/structure/issues.d.ts +1 -1
  384. package/dist/structure/issues.d.ts.map +1 -1
  385. package/dist/structure/issues.js +19 -8
  386. package/dist/structure/issues.js.map +1 -1
  387. package/dist/structure/listed-tags.d.ts +3 -0
  388. package/dist/structure/listed-tags.d.ts.map +1 -0
  389. package/dist/structure/listed-tags.js +6 -0
  390. package/dist/structure/listed-tags.js.map +1 -0
  391. package/dist/structure/markdown-path.d.ts.map +1 -1
  392. package/dist/structure/markdown-path.js +2 -2
  393. package/dist/structure/markdown-path.js.map +1 -1
  394. package/dist/structure/number-in-sentence.d.ts +15 -0
  395. package/dist/structure/number-in-sentence.d.ts.map +1 -0
  396. package/dist/structure/number-in-sentence.js +13 -0
  397. package/dist/structure/number-in-sentence.js.map +1 -0
  398. package/dist/structure/numbering.d.ts.map +1 -1
  399. package/dist/structure/numbering.js +1 -1
  400. package/dist/structure/numbering.js.map +1 -1
  401. package/dist/structure/relative-find.js +1 -1
  402. package/dist/structure/relative-find.js.map +1 -1
  403. package/dist/structure/sexp.js +1 -1
  404. package/dist/structure/sexp.js.map +1 -1
  405. package/dist/structure/unit-word.d.ts +8 -0
  406. package/dist/structure/unit-word.d.ts.map +1 -0
  407. package/dist/structure/unit-word.js +10 -0
  408. package/dist/structure/unit-word.js.map +1 -0
  409. package/dist/structure/unnumbered.js +2 -2
  410. package/dist/structure/unnumbered.js.map +1 -1
  411. package/dist/structure/wrapped-tail.d.ts +18 -0
  412. package/dist/structure/wrapped-tail.d.ts.map +1 -0
  413. package/dist/structure/wrapped-tail.js +11 -0
  414. package/dist/structure/wrapped-tail.js.map +1 -0
  415. package/dist/team-names.d.ts +9 -0
  416. package/dist/team-names.d.ts.map +1 -0
  417. package/dist/team-names.js +34 -0
  418. package/dist/team-names.js.map +1 -0
  419. package/dist/ui.d.ts +2 -0
  420. package/dist/ui.d.ts.map +1 -1
  421. package/dist/ui.js +6 -0
  422. package/dist/ui.js.map +1 -1
  423. package/dist/version.js +1 -1
  424. package/dist/version.js.map +1 -1
  425. package/dist/watch.d.ts +2 -2
  426. package/dist/watch.d.ts.map +1 -1
  427. package/dist/watch.js +5 -5
  428. package/dist/watch.js.map +1 -1
  429. package/genres.yaml +363 -0
  430. package/package.json +4 -3
  431. package/rules/adverb-overuse.yaml +2 -2
  432. package/rules/agentless-passive.yaml +1 -1
  433. package/rules/ai-generated-composite.yaml +2 -2
  434. package/rules/ai-tell.yaml +2 -2
  435. package/rules/bold-density.yaml +2 -2
  436. package/rules/closing-cliche.yaml +1 -1
  437. package/rules/concrete-evidence-density.yaml +2 -2
  438. package/rules/contraction-consistency.yaml +1 -1
  439. package/rules/cushion-phrase-density.yaml +1 -1
  440. package/rules/dangling-reference.yaml +3 -3
  441. package/rules/date-order.yaml +3 -3
  442. package/rules/date-weekday-mismatch.yaml +1 -1
  443. package/rules/double-keigo.yaml +1 -1
  444. package/rules/doubled-word.yaml +1 -1
  445. package/rules/duplicate-definition.yaml +1 -1
  446. package/rules/emoji-density.yaml +1 -1
  447. package/rules/empty-conclusion.yaml +2 -1
  448. package/rules/empty-intensifier.yaml +1 -1
  449. package/rules/excessive-hedging.yaml +16 -6
  450. package/rules/expletive-construction.yaml +2 -2
  451. package/rules/heading-echo.yaml +1 -1
  452. package/rules/hiragana-fukushi.yaml +1 -1
  453. package/rules/internal-jargon.yaml +2 -2
  454. package/rules/latin-spacing.yaml +6 -3
  455. package/rules/max-kanji-continuous.yaml +5 -4
  456. package/rules/max-paragraph-length.yaml +9 -2
  457. package/rules/max-sentence-length.yaml +14 -2
  458. package/rules/ngram-repetition.yaml +4 -4
  459. package/rules/no-doubled-joshi.yaml +4 -4
  460. package/rules/no-em-dash.yaml +2 -2
  461. package/rules/no-mixed-desumasu.yaml +1 -1
  462. package/rules/no-nakaguro-parallel.yaml +2 -2
  463. package/rules/numbering-gap.yaml +1 -1
  464. package/rules/oxford-comma-consistency.yaml +2 -1
  465. package/rules/padded-intro.yaml +2 -1
  466. package/rules/paragraph-length-variance.yaml +1 -1
  467. package/rules/preamble-length.yaml +2 -2
  468. package/rules/preferred-term.yaml +1 -1
  469. package/rules/proper-noun-density.yaml +3 -2
  470. package/rules/repeated-conjunction.yaml +2 -2
  471. package/rules/repeated-sentence-head.yaml +2 -2
  472. package/rules/required-sections.yaml +1 -1
  473. package/rules/rule-of-three.yaml +2 -2
  474. package/rules/sasete-itadaku.yaml +3 -3
  475. package/rules/section-length-uniformity.yaml +1 -1
  476. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  477. package/rules/sentence-rhythm.yaml +1 -1
  478. package/rules/taigen-dome-in-prose.yaml +2 -2
  479. package/rules/title-case-consistency.yaml +3 -3
  480. package/rules/total-mismatch.yaml +1 -1
  481. package/rules/undefined-acronym.yaml +18 -5
  482. package/rules/unqualified-superlative.yaml +3 -3
  483. package/skills/chaff/SKILL.md +28 -9
  484. package/src/address-chain.ts +6 -3
  485. package/src/backends/anthropic.ts +5 -1
  486. package/src/backends/openai.ts +2 -1
  487. package/src/backends/types.ts +3 -2
  488. package/src/baseline.ts +1 -1
  489. package/src/calendar-number.ts +75 -0
  490. package/src/cli-args.ts +2 -2
  491. package/src/cli-text.ts +48 -18
  492. package/src/cli.ts +51 -47
  493. package/src/commands/cite.ts +4 -4
  494. package/src/commands/eval.ts +27 -6
  495. package/src/commands/feedback.ts +2 -1
  496. package/src/commands/init-ask.ts +53 -0
  497. package/src/commands/test.ts +105 -47
  498. package/src/commands/tree.ts +9 -7
  499. package/src/config/by-path.ts +8 -9
  500. package/src/config/load.ts +24 -0
  501. package/src/config/name-problems.ts +15 -0
  502. package/src/date-stamp.ts +1 -1
  503. package/src/detectors/acronym-compound.ts +9 -0
  504. package/src/detectors/acronym-context.ts +22 -1
  505. package/src/detectors/acronym-expansion.ts +72 -5
  506. package/src/detectors/char-shape.ts +56 -16
  507. package/src/detectors/citation-key.ts +13 -0
  508. package/src/detectors/cited-name.ts +23 -0
  509. package/src/detectors/concrete-evidence.ts +31 -0
  510. package/src/detectors/counter-edge.ts +29 -0
  511. package/src/detectors/dotted-name.ts +14 -0
  512. package/src/detectors/doubled-word.ts +25 -2
  513. package/src/detectors/en-shape.ts +123 -38
  514. package/src/detectors/enumerated-runs.ts +3 -2
  515. package/src/detectors/gram-place.ts +9 -2
  516. package/src/detectors/gram-predicate.ts +55 -0
  517. package/src/detectors/heading-case.ts +44 -0
  518. package/src/detectors/heading-echo.ts +5 -1
  519. package/src/detectors/index.ts +4 -3
  520. package/src/detectors/lettered-index.ts +78 -0
  521. package/src/detectors/lexicon.ts +68 -8
  522. package/src/detectors/name-numeral.ts +37 -0
  523. package/src/detectors/name-part.ts +24 -0
  524. package/src/detectors/name-run.ts +16 -0
  525. package/src/detectors/orthography.ts +43 -10
  526. package/src/detectors/phrase-match.ts +23 -12
  527. package/src/detectors/placeholder.ts +38 -0
  528. package/src/detectors/quoted-variant.ts +20 -0
  529. package/src/detectors/repeated-head.ts +3 -4
  530. package/src/detectors/series-label.ts +33 -0
  531. package/src/detectors/signals.ts +49 -35
  532. package/src/detectors/stacked-hedge.ts +93 -0
  533. package/src/detectors/structure-tree.ts +2 -2
  534. package/src/detectors/superlative-amount.ts +24 -0
  535. package/src/detectors/superlative-clause.ts +82 -0
  536. package/src/detectors/superlative-name.ts +30 -0
  537. package/src/detectors/superlative-scope.ts +3 -3
  538. package/src/detectors/team.ts +18 -2
  539. package/src/detectors/token-shape.ts +13 -6
  540. package/src/document.ts +41 -8
  541. package/src/eval.ts +1 -1
  542. package/src/feedback/draft.ts +1 -1
  543. package/src/feedback/excerpt.ts +2 -2
  544. package/src/file-header.ts +31 -0
  545. package/src/files.ts +3 -3
  546. package/src/genre-check.ts +46 -0
  547. package/src/genre-load.ts +24 -0
  548. package/src/genre-parse.ts +126 -0
  549. package/src/genre-suggest.ts +37 -0
  550. package/src/genre.ts +25 -5
  551. package/src/init-choice.ts +47 -0
  552. package/src/init.ts +20 -19
  553. package/src/joined-view.ts +96 -0
  554. package/src/judge.ts +3 -1
  555. package/src/line-continues.ts +43 -0
  556. package/src/line-paragraphs.ts +83 -0
  557. package/src/mask.ts +5 -6
  558. package/src/measure.ts +9 -1
  559. package/src/number-name.ts +35 -13
  560. package/src/orthography.ts +15 -9
  561. package/src/plain-source.ts +5 -0
  562. package/src/plugin.ts +27 -2
  563. package/src/profile/check.ts +34 -0
  564. package/src/profile/for-file.ts +10 -3
  565. package/src/profile/load.ts +1 -1
  566. package/src/profile/select.ts +11 -4
  567. package/src/quoted-span.ts +39 -0
  568. package/src/reasons.ts +5 -1
  569. package/src/render/compact.ts +1 -3
  570. package/src/render/eval.ts +102 -38
  571. package/src/render/friendly.ts +21 -6
  572. package/src/render/genres.ts +31 -0
  573. package/src/render/plural.ts +6 -0
  574. package/src/render/rules-json.ts +10 -3
  575. package/src/render/semantic.ts +117 -34
  576. package/src/render/summary.ts +4 -3
  577. package/src/render/suppressions.ts +3 -2
  578. package/src/render/text.ts +18 -3
  579. package/src/resolve-genre.ts +20 -0
  580. package/src/rule-load.ts +6 -1
  581. package/src/run-semantic.ts +1 -1
  582. package/src/run.ts +28 -15
  583. package/src/sentence-shape.ts +3 -7
  584. package/src/setting-problems.ts +24 -0
  585. package/src/soft-break.ts +94 -0
  586. package/src/structure/build.ts +24 -15
  587. package/src/structure/cite.ts +1 -1
  588. package/src/structure/date-order.ts +16 -1
  589. package/src/structure/issues.ts +24 -8
  590. package/src/structure/listed-tags.ts +7 -0
  591. package/src/structure/markdown-path.ts +2 -2
  592. package/src/structure/number-in-sentence.ts +20 -0
  593. package/src/structure/numbering.ts +1 -1
  594. package/src/structure/relative-find.ts +1 -1
  595. package/src/structure/sexp.ts +1 -1
  596. package/src/structure/unit-word.ts +11 -0
  597. package/src/structure/unnumbered.ts +2 -2
  598. package/src/structure/wrapped-tail.ts +22 -0
  599. package/src/team-names.ts +39 -0
  600. package/src/ui.ts +7 -0
  601. package/src/version.ts +1 -1
  602. package/src/watch.ts +5 -5
@@ -0,0 +1,126 @@
1
+ import { isLevel } from "./levels.ts";
2
+ import type { Level, Localized } from "./plugin.ts";
3
+
4
+ /** A preset's rules: the level each named rule runs at, under whatever chaff.yaml's rules say. */
5
+ export type PresetLevels = Readonly<Record<string, Level>>;
6
+
7
+ /** A group of genres (legal, literature): its name, and the levels every genre in it shares. */
8
+ export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels };
9
+
10
+ /** One genre of genres.yaml: what it is for, the levels it sets on top of its group's, and the profile it reads with. */
11
+ export type GenreDefinition = {
12
+ readonly id: string;
13
+ readonly name: Localized;
14
+ readonly summary: Localized;
15
+ readonly rules: PresetLevels;
16
+ readonly profile: string | undefined;
17
+ readonly suggest: GenreSuggest;
18
+ };
19
+
20
+ /** A line shape that marks the genre, and how many lines must have it. */
21
+ type LineCue = { readonly line: RegExp; readonly minLines: number };
22
+
23
+ /** What makes a document look like this genre when no genre is set: its path, or enough lines of the genre's own shape, per language. */
24
+ export type GenreSuggest = { readonly paths: readonly RegExp[]; readonly lines: Readonly<Record<string, LineCue>> };
25
+
26
+ export type GenreData = { readonly groups: readonly GenreGroup[]; readonly genres: readonly GenreDefinition[] };
27
+
28
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
29
+
30
+ const isLocalized = (value: unknown): value is Localized =>
31
+ isRecord(value) && typeof value["ja"] === "string" && typeof value["en"] === "string" && value["ja"] !== "" && value["en"] !== "";
32
+
33
+ const levelsOf = (value: unknown, where: string): PresetLevels => {
34
+ if (value === undefined) return {};
35
+ if (!isRecord(value)) throw new Error(`${where}: rules must be a map of rule ids to levels`);
36
+ const wrong = Object.entries(value).find(([, level]) => !isLevel(level));
37
+ if (wrong !== undefined) throw new Error(`${where}: ${wrong[0]} has the level ${String(wrong[1])}; use strict, normal, relaxed or off`);
38
+ return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, Level] => isLevel(entry[1])));
39
+ };
40
+
41
+ const localizedOf = (value: unknown, field: string, where: string): Localized => {
42
+ if (!isLocalized(value)) throw new Error(`${where}: ${field} needs a ja and an en text`);
43
+ return value;
44
+ };
45
+
46
+ const idOf = (value: unknown, index: number): string => {
47
+ const id = isRecord(value) ? value["id"] : undefined;
48
+ if (typeof id !== "string" || id === "") throw new Error(`entry ${String(index + 1)} has no id`);
49
+ return id;
50
+ };
51
+
52
+ const groupOf = (value: unknown, index: number): GenreGroup => {
53
+ const id = idOf(value, index);
54
+ const raw = isRecord(value) ? value : {};
55
+ return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id) };
56
+ };
57
+
58
+ const patternOf = (value: unknown, flags: string, where: string): RegExp => {
59
+ if (typeof value !== "string" || value === "") throw new Error(`${where}: a pattern must be a non-empty string`);
60
+ try {
61
+ return new RegExp(value, flags);
62
+ } catch (error) {
63
+ throw new Error(`${where}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
64
+ }
65
+ };
66
+
67
+ const lineCueOf = (value: unknown, where: string): LineCue => {
68
+ if (!isRecord(value)) throw new Error(`${where}: needs line and min_lines`);
69
+ const minLines = value["min_lines"];
70
+ if (typeof minLines !== "number" || !Number.isInteger(minLines) || minLines < 1) throw new Error(`${where}: min_lines must be a whole number of 1 or more`);
71
+ return { line: patternOf(value["line"], "u", where), minLines };
72
+ };
73
+
74
+ const suggestOf = (value: unknown, where: string): GenreSuggest => {
75
+ if (value === undefined) return { paths: [], lines: {} };
76
+ if (!isRecord(value)) throw new Error(`${where}: suggest must be a map`);
77
+ const paths = value["paths"] ?? [];
78
+ if (!Array.isArray(paths)) throw new Error(`${where}: suggest.paths must be a list`);
79
+ const lines = Object.entries(value).filter(([key]) => key !== "paths");
80
+ return {
81
+ paths: paths.map((path) => patternOf(path, "iu", `${where} suggest.paths`)),
82
+ lines: Object.fromEntries(lines.map(([language, cue]) => [language, lineCueOf(cue, `${where} suggest.${language}`)])),
83
+ };
84
+ };
85
+
86
+ const genreOf = (value: unknown, index: number): GenreDefinition => {
87
+ const id = idOf(value, index);
88
+ const raw = isRecord(value) ? value : {};
89
+ const profile = raw["profile"];
90
+ if (profile !== undefined && (typeof profile !== "string" || profile === "")) throw new Error(`${id}: profile must be a profile's id`);
91
+ return {
92
+ id,
93
+ name: localizedOf(raw["name"], "name", id),
94
+ summary: localizedOf(raw["summary"], "summary", id),
95
+ rules: levelsOf(raw["rules"], id),
96
+ profile,
97
+ suggest: suggestOf(raw["suggest"], id),
98
+ };
99
+ };
100
+
101
+ const listOf = (raw: Record<string, unknown>, key: string): unknown[] => {
102
+ const value = raw[key];
103
+ if (!Array.isArray(value)) throw new Error(`genres.yaml needs a list under ${key}`);
104
+ return value;
105
+ };
106
+
107
+ /** genres.yaml as read by the yaml package. Throws on anything it cannot read: the file ships with chaff, so a wrong entry is chaff's bug. */
108
+ export const parseGenres = (raw: unknown): GenreData => {
109
+ if (!isRecord(raw)) throw new Error("genres.yaml must be a map with groups and genres");
110
+ const groups = listOf(raw, "groups").map(groupOf);
111
+ const genres = listOf(raw, "genres").map(genreOf);
112
+ const orphan = genres.find((genre) => !groups.some((group) => genre.id.startsWith(`${group.id}/`)));
113
+ if (orphan !== undefined) throw new Error(`${orphan.id}: no group matches the part before its /`);
114
+ return { groups, genres };
115
+ };
116
+
117
+ /** The levels a genre's preset sets: its group's, then its own on top. Empty for a genre chaff does not know. */
118
+ export const presetLevelsOf = (data: GenreData, genre: string): PresetLevels => {
119
+ const own = data.genres.find((entry) => entry.id === genre);
120
+ if (own === undefined) return {};
121
+ const group = data.groups.find((entry) => genre.startsWith(`${entry.id}/`));
122
+ return { ...group?.rules, ...own.rules };
123
+ };
124
+
125
+ /** The profile a genre reads its documents with, when it names one. */
126
+ export const presetProfileOf = (data: GenreData, genre: string): string | undefined => data.genres.find((entry) => entry.id === genre)?.profile;
@@ -0,0 +1,37 @@
1
+ import type { GenreData, GenreDefinition } from "./genre-parse.ts";
2
+ import { detected, type ProfileDefinition } from "./profile/parse.ts";
3
+
4
+ /** The file a genre is suggested for: where it is, what it says, and its language. */
5
+ export type SuggestInput = { readonly path: string; readonly source: string; readonly language: string };
6
+
7
+ /** A path cue names the kind outright, so it outweighs any count of lines. */
8
+ const BY_PATH = Number.POSITIVE_INFINITY;
9
+
10
+ /** How many times over the genre's lines reach their minimum: 1 is just enough, 0 is none of that shape. */
11
+ const linesScore = (genre: GenreDefinition, input: SuggestInput): number => {
12
+ const cue = genre.suggest.lines[input.language];
13
+ if (cue === undefined) return 0;
14
+ return input.source.split("\n").filter((line) => cue.line.test(line)).length / cue.minLines;
15
+ };
16
+
17
+ /** A genre that reads with a profile has the profile's shape (a statute's article lines) as just-enough evidence. */
18
+ const profileScore = (genre: GenreDefinition, input: SuggestInput, profiles: readonly ProfileDefinition[]): number => {
19
+ const profile = profiles.find((definition) => definition.id === genre.profile);
20
+ return profile !== undefined && detected(profile, input.source, input.language) ? 1 : 0;
21
+ };
22
+
23
+ const scoreOf = (genre: GenreDefinition, input: SuggestInput, profiles: readonly ProfileDefinition[]): number =>
24
+ genre.suggest.paths.some((pattern) => pattern.test(input.path)) ? BY_PATH : Math.max(linesScore(genre, input), profileScore(genre, input, profiles));
25
+
26
+ type Scored = { readonly genre: GenreDefinition; readonly score: number };
27
+
28
+ /**
29
+ * The genre a file with no genre set looks like, for the screen to suggest. Never used to check the file: switching silently
30
+ * would change what an existing run reports. The genre whose cues the file meets most strongly wins (a transcript that
31
+ * discusses a bill has more speaker lines than lines about "this Act"); a tie goes to the genre listed first in genres.yaml.
32
+ */
33
+ export const suggestGenre = (input: SuggestInput, data: GenreData, profiles: readonly ProfileDefinition[]): GenreDefinition | undefined =>
34
+ data.genres
35
+ .map((genre): Scored => ({ genre, score: scoreOf(genre, input, profiles) }))
36
+ .filter((entry) => entry.score >= 1)
37
+ .reduce<Scored | undefined>((best, entry) => (best === undefined || entry.score > best.score ? entry : best), undefined)?.genre;
package/src/genre.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import { loadGenres } from "./genre-load.ts";
2
+
3
+ /** chaff が知っているジャンル。genres.yaml の並び。rule の use_for はこの頭の部分で当てる。 */
4
+ export const GENRES: readonly string[] = loadGenres().genres.map((genre) => genre.id);
5
+
1
6
  export type GenreGuess = { readonly genre: string; readonly from: "front-matter" | "path" | "content" };
2
7
 
3
8
  /**
@@ -35,9 +40,24 @@ export const guessGenre = (path: string, source: string, front: string | undefin
35
40
  return undefined;
36
41
  };
37
42
 
38
- /** front matter の genre / type を拾う。失敗しても落とさない。 */
39
- export const frontMatterGenre = (source: string): string | undefined => {
40
- const match = /^---\n([\s\S]*?)\n---/u.exec(source);
41
- const line = match?.[1] === undefined ? undefined : /^(?:genre|type):\s*(\S+)\s*$/mu.exec(match[1]);
42
- return line?.[1];
43
+ /** YAML の 1 行の値。行末のコメントと、値を囲む引用符を外す。 */
44
+ const scalarOf = (raw: string): string => {
45
+ const comment = raw.search(/\s#/u);
46
+ const value = (comment === -1 ? raw : raw.slice(0, comment)).trim();
47
+ return /^(["'])(.*)\1$/u.exec(value)?.[2] ?? value;
48
+ };
49
+
50
+ const valuesOf = (block: string, key: string): string[] => [...block.matchAll(new RegExp(`^${key}:(.*)$`, "gmu"))].map((line) => scalarOf(line[1] ?? ""));
51
+
52
+ /** front matter の 1 つの欄に書かれた値。front matter が無ければ空。 */
53
+ export const frontMatterValues = (source: string, key: string): string[] => {
54
+ const block = /^---\n([\s\S]*?)\n---/u.exec(source)?.[1];
55
+ return block === undefined ? [] : valuesOf(block, key);
43
56
  };
57
+
58
+ /**
59
+ * front matter の genre / type を拾う。失敗しても落とさない。genre が type より先。
60
+ * 知っているジャンルだけを返す。Zenn の type(tech / idea)のような別の意味の値をジャンルにすると、どの rule も当てはまらず「指摘なし」になる。
61
+ */
62
+ export const frontMatterGenre = (source: string): string | undefined =>
63
+ [...frontMatterValues(source, "genre"), ...frontMatterValues(source, "type")].find((value) => GENRES.includes(value));
@@ -0,0 +1,47 @@
1
+ import type { GenreData } from "./genre-parse.ts";
2
+ import type { Texts, UiLanguage } from "./ui.ts";
3
+
4
+ /** The genre init writes when nothing is chosen: the one a check uses when none is set. */
5
+ export const DEFAULT_GENRE = "blog/tech";
6
+
7
+ const TEXT: Texts<{ readonly heading: string; readonly prompt: string; readonly notAGenre: (answer: string) => string; readonly noAnswer: string }> = {
8
+ ja: {
9
+ heading: "どの種類の文書をここに置きますか? 選んだ種類の書き方に合わせて見ます。",
10
+ prompt: `番号か名前を入れてください(Enter で ${DEFAULT_GENRE}): `,
11
+ notAGenre: (answer) => `「${answer}」は一覧にありません。何も作っていません。npx chaffjs init --genre <ジャンル> でも選べます。`,
12
+ noAnswer: "答えが無いまま入力が終わったので、何も作っていません。",
13
+ },
14
+ en: {
15
+ heading: "What kind of document goes here? chaff checks it the way that kind is written.",
16
+ prompt: `Type a number or a name (Enter for ${DEFAULT_GENRE}): `,
17
+ notAGenre: (answer) => `"${answer}" is not in the list. Nothing was created. npx chaffjs init --genre <genre> chooses one too.`,
18
+ noAnswer: "The input ended without an answer, so nothing was created.",
19
+ },
20
+ };
21
+
22
+ /** The numbered list init shows before it asks: every genre with what it is for, in genres.yaml's order. */
23
+ export const genreChoices = (data: GenreData, ui: UiLanguage): string[] => {
24
+ const width = Math.max(...data.genres.map((genre) => genre.id.length)) + 2;
25
+ const numberWidth = String(data.genres.length).length;
26
+ return [
27
+ "",
28
+ TEXT[ui].heading,
29
+ "",
30
+ ...data.genres.map((genre, index) => ` ${String(index + 1).padStart(numberWidth)} ${genre.id.padEnd(width)}${genre.summary[ui] ?? ""}`),
31
+ "",
32
+ ];
33
+ };
34
+
35
+ export const choicePrompt = (ui: UiLanguage): string => TEXT[ui].prompt;
36
+
37
+ export const notAGenre = (answer: string, ui: UiLanguage): string => TEXT[ui].notAGenre(answer);
38
+
39
+ export const noAnswer = (ui: UiLanguage): string => TEXT[ui].noAnswer;
40
+
41
+ /** A number from the list, a genre's name, or nothing (the default). undefined for anything else: init then writes nothing. */
42
+ export const genreFromAnswer = (answer: string, ids: readonly string[]): string | undefined => {
43
+ const typed = answer.trim();
44
+ if (typed === "") return DEFAULT_GENRE;
45
+ if (ids.includes(typed)) return typed;
46
+ return /^\d+$/u.test(typed) ? ids[Number(typed) - 1] : undefined;
47
+ };
package/src/init.ts CHANGED
@@ -1,9 +1,10 @@
1
1
  import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import type { Texts, UiLanguage } from "./ui.ts";
4
+ import { loadGenres } from "./genre-load.ts";
4
5
 
5
6
  const TEXT: Texts<{
6
- readonly config: (genre: string) => string;
7
+ readonly config: (genre: string, summary: string) => string;
7
8
  readonly created: string;
8
9
  readonly appended: (lines: readonly string[]) => string;
9
10
  readonly configNote: string;
@@ -15,7 +16,7 @@ const TEXT: Texts<{
15
16
  readonly nextCheck: string;
16
17
  }> = {
17
18
  ja: {
18
- config: (genre) => `# chaff.yaml — このチームの文章規範
19
+ config: (genre, summary) => `# chaff.yaml — このチームの文章規範
19
20
  #
20
21
  # ここに書くのは「既定から変えたもの」だけです。書かなければ既定で動きます。
21
22
  # このファイルを消しても chaff は動きます。
@@ -33,9 +34,13 @@ const TEXT: Texts<{
33
34
  # npx chaff explain bold-density そのルールの意図を読む
34
35
  # npx chaff rules --json AI に設定を書かせるときに渡す
35
36
 
36
- # この場所に置く文書の種類。
37
+ # この場所に置く文書の種類(${summary})。ほかの種類: npx chaffjs genres
37
38
  genre: ${genre}
38
39
 
40
+ # チームが書く固有名詞(組織名・製品名)。1 つの名前として読み、漢字の連なりに数えない。
41
+ # names:
42
+ # - 個人情報保護委員会
43
+
39
44
  # 既定から変えたものだけを書く。
40
45
  rules:
41
46
  `,
@@ -50,7 +55,7 @@ rules:
50
55
  nextCheck: " npx chaff . この場所の Markdown を全部見る",
51
56
  },
52
57
  en: {
53
- config: (genre) => `# chaff.yaml — this team's writing rules
58
+ config: (genre, summary) => `# chaff.yaml — this team's writing rules
54
59
  #
55
60
  # Write only what differs from the defaults; anything left out uses the default.
56
61
  # chaff still runs if this file is deleted.
@@ -68,9 +73,13 @@ rules:
68
73
  # npx chaff explain bold-density read what the rule is for
69
74
  # npx chaff rules --json give this to an AI that writes the settings
70
75
 
71
- # The kind of document kept here.
76
+ # The kind of document kept here (${summary}). The others: npx chaffjs genres
72
77
  genre: ${genre}
73
78
 
79
+ # The names your team writes (organisations, products). Each is read as one name, not as words to count.
80
+ # names:
81
+ # - Bank of England
82
+
74
83
  # Only what differs from the defaults.
75
84
  rules:
76
85
  `,
@@ -108,9 +117,14 @@ const ensureGitignore = (dir: string, text: (typeof TEXT)["ja"]): Written | unde
108
117
  return { path, note: text.appended(missing) };
109
118
  };
110
119
 
120
+ /** What the genre is for, as chaff genres says it; the genre itself when chaff does not know it. */
121
+ const summaryOf = (genre: string, ui: UiLanguage): string => loadGenres().genres.find((entry) => entry.id === genre)?.summary[ui] ?? genre;
122
+
111
123
  export const runInit = (dir: string, genre: string, ui: UiLanguage = "ja"): string[] => {
112
124
  const text = TEXT[ui];
113
- const made = [writeIfAbsent(join(dir, "chaff.yaml"), text.config(genre), text.configNote), ensureGitignore(dir, text)].filter((entry) => entry !== undefined);
125
+ const made = [writeIfAbsent(join(dir, "chaff.yaml"), text.config(genre, summaryOf(genre, ui)), text.configNote), ensureGitignore(dir, text)].filter(
126
+ (entry) => entry !== undefined,
127
+ );
114
128
  if (made.length === 0) return [text.exists];
115
129
  return [
116
130
  "",
@@ -125,16 +139,3 @@ export const runInit = (dir: string, genre: string, ui: UiLanguage = "ja"): stri
125
139
  "",
126
140
  ];
127
141
  };
128
-
129
- export const GENRES: readonly string[] = [
130
- "technical/spec",
131
- "technical/readme",
132
- "blog/tech",
133
- "blog/essay",
134
- "blog/owned-media",
135
- "business/proposal",
136
- "business/report",
137
- "business/email",
138
- "business/press-release",
139
- "business/meeting-notes",
140
- ];
@@ -0,0 +1,96 @@
1
+ import { spansWithin, withoutSpans } from "./soft-break.ts";
2
+ import { continuedBreaks, wrapBreaks } from "./line-continues.ts";
3
+ import type { Segmentation, Sentence, Span, Token } from "./plugin.ts";
4
+
5
+ /**
6
+ * 消える改行を取り除いた文字列と、その上の範囲を元の文字列の範囲に戻す写像。
7
+ *
8
+ * 解析はつないだ文字列で行い、指摘の位置は元の文字列で出す。行・列も、引用して見せる範囲も、元の文書のまま。
9
+ */
10
+ export type JoinedView = { readonly text: string; readonly toSource: (span: Span) => Span };
11
+
12
+ type Removed = { readonly at: number; readonly removedBefore: number };
13
+
14
+ /** 取り除いた各範囲が、つないだ文字列のどこにあったか。removedBefore はそこまでに取り除いた長さ(その範囲を含む)。 */
15
+ const removedPlaces = (breaks: readonly Span[]): Removed[] => {
16
+ const places: Removed[] = [];
17
+ breaks.reduce((before, span) => {
18
+ const removedBefore = before + span.end - span.start;
19
+ places.push({ at: span.start - before, removedBefore });
20
+ return removedBefore;
21
+ }, 0);
22
+ return places;
23
+ };
24
+
25
+ /** at が offset 以下(inclusive)または未満の、最後の範囲までに取り除いた長さ。places は at の昇順。 */
26
+ const removedUpTo = (places: readonly Removed[], offset: number, inclusive: boolean): number => {
27
+ const search = (low: number, high: number, found: number): number => {
28
+ if (low > high) return found;
29
+ const middle = (low + high) >> 1;
30
+ const place = places[middle];
31
+ if (place === undefined) return found;
32
+ const reached = inclusive ? place.at <= offset : place.at < offset;
33
+ return reached ? search(middle + 1, high, place.removedBefore) : search(low, middle - 1, found);
34
+ };
35
+ return search(0, places.length - 1, 0);
36
+ };
37
+
38
+ /**
39
+ * 始まりは、そこで取り除いた改行の後ろ(次の文字)へ。終わりは、そこで取り除いた改行の手前へ。
40
+ * 改行をまたぐ語(「関\nする」)は、改行ごと覆う範囲になる。
41
+ */
42
+ export const joinedView = (text: string, breaks: readonly Span[]): JoinedView => {
43
+ const places = removedPlaces(breaks);
44
+ const toSource = (span: Span): Span => {
45
+ const start = span.start + removedUpTo(places, span.start, true);
46
+ const end = span.end + removedUpTo(places, span.end, false);
47
+ return { start, end: Math.max(start, end) };
48
+ };
49
+ return { text: withoutSpans(text, breaks), toSource };
50
+ };
51
+
52
+ const tokenInSource =
53
+ (view: JoinedView) =>
54
+ (token: Token): Token => ({ ...token, span: view.toSource(token.span) });
55
+
56
+ const sentenceInSource =
57
+ (text: string, view: JoinedView) =>
58
+ (sentence: Sentence): Sentence => {
59
+ const span = view.toSource(sentence.span);
60
+ return {
61
+ span,
62
+ text: text.slice(span.start, span.end),
63
+ ...(sentence.tokens === undefined ? {} : { tokens: sentence.tokens.map(tokenInSource(view)) }),
64
+ };
65
+ };
66
+
67
+ type Segment = (text: string) => Segmentation;
68
+
69
+ /** breaks を除いて segment し、文と語の範囲を text の上に戻す。文の text は元の文字列の切り出しのまま。 */
70
+ const segmentWithout = (text: string, breaks: readonly Span[], segment: Segment): Sentence[] => {
71
+ const view = joinedView(text, breaks);
72
+ return segment(view.text).sentences.map(sentenceInSource(text, view));
73
+ };
74
+
75
+ const withWrapBreaks = (sentence: Sentence, wrapped: readonly Span[]): Sentence => {
76
+ const inside = spansWithin(wrapped, sentence.span);
77
+ return inside.length === 0 ? sentence : { ...sentence, wrapBreaks: inside };
78
+ };
79
+
80
+ const tokensOf = (sentences: readonly Sentence[]): Token[] => sentences.flatMap((sentence) => sentence.tokens ?? []);
81
+
82
+ /**
83
+ * 消える改行を、行が続いていると言えるものだけ除いて segment する。
84
+ * 一度すべて除いて解析し、その語で決めて、残す改行があれば読み直す。
85
+ * 語が無ければ(品詞を読まない adapter)決められないので、前と同じく改行ごと渡す。
86
+ * 除く改行が無ければ segment の結果をそのまま返す。つながない段落の文は、前と同じものになる。
87
+ */
88
+ export const segmentJoined = (text: string, breaks: readonly Span[], segment: Segment): readonly Sentence[] => {
89
+ if (breaks.length === 0) return segment(text).sentences;
90
+ const joined = segmentWithout(text, breaks, segment);
91
+ const kept = continuedBreaks(breaks, tokensOf(joined));
92
+ if (kept.length === 0) return segment(text).sentences;
93
+ const sentences = kept.length === breaks.length ? joined : segmentWithout(text, kept, segment);
94
+ const wrapped = wrapBreaks(kept, tokensOf(sentences));
95
+ return sentences.map((sentence) => withWrapBreaks(sentence, wrapped));
96
+ };
package/src/judge.ts CHANGED
@@ -6,6 +6,7 @@ import * as openai from "./backends/openai.ts";
6
6
  import type { AnthropicClient } from "./backends/anthropic.ts";
7
7
  import type { OpenAIClient } from "./backends/openai.ts";
8
8
  import type { BackendName, Failure, Judge, JsonSchema } from "./backends/types.ts";
9
+ import type { UiLanguage } from "./ui.ts";
9
10
 
10
11
  export const CACHE_DIR = ".chaff-cache";
11
12
 
@@ -87,7 +88,8 @@ export const isAuthFailure = (backend: BackendName, error: unknown): boolean =>
87
88
  export const describeFailure = (backend: BackendName, error: unknown): Failure | undefined =>
88
89
  backend === "openai" ? openai.describeFailure(error) : anthropic.describeFailure(error);
89
90
 
90
- export const credentialHint = (backend: BackendName): string => (backend === "openai" ? openai.SETUP_HINT : anthropic.SETUP_HINT);
91
+ export const credentialHint = (backend: BackendName, language: UiLanguage): string =>
92
+ (backend === "openai" ? openai.SETUP_HINT : anthropic.SETUP_HINT)[language];
91
93
 
92
94
  /**
93
95
  * 同じ (model, rule, rubric, candidate) なら 2 度目は問い合わせない。
@@ -0,0 +1,43 @@
1
+ import { firstEndingAfter } from "./soft-break.ts";
2
+ import type { Span, Token } from "./plugin.ts";
3
+
4
+ /**
5
+ * 消える改行のうち、どれを消して読むか。決めるのは解析器が読んだ語(全部の改行を消して読んだもの)。
6
+ *
7
+ * Markdown の段落は改行を消して表示されるが、書いた人は改行で区切ったつもりのことがある。
8
+ * 番号だけの見出しの行(「2.1 注文の登録\n登録は」)、宛先や署名を 1 行に 1 つ書いた並び。
9
+ * そこを消して読むと「登録登録」が語の重なりになる。行が続いていると語で言えるときだけ消す。
10
+ */
11
+
12
+ const inside = (token: Token | undefined, part: Span): boolean => token !== undefined && token.span.start < part.start && token.span.end > part.end;
13
+
14
+ type Around = { readonly across: Token | undefined; readonly before: Token | undefined };
15
+
16
+ /** 各改行の位置で、改行をまたぐ語(あれば)と、改行の直前で終わる語。tokens は並び順(重ならない)。 */
17
+ const aroundEach = (breaks: readonly Span[], tokens: readonly Token[]): Around[] => {
18
+ const spans = tokens.map((token) => token.span);
19
+ return breaks.map((part) => {
20
+ const at = firstEndingAfter(spans, part.start);
21
+ return { across: tokens[at], before: tokens[at - 1] };
22
+ });
23
+ };
24
+
25
+ /** 解析器が語の途中に置いた改行。語をまたぐなら、行を折り返しただけ。 */
26
+ export const wrapBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
27
+ const around = aroundEach(breaks, tokens);
28
+ return breaks.filter((part, index) => inside(around[index]?.across, part));
29
+ };
30
+
31
+ /** 句を閉じない語。助詞・接続助詞・接続詞で終わる行は、次の行へ続く。 */
32
+ const CONTINUING_POS = new Set(["ADP", "SCONJ", "CCONJ"]);
33
+ /** 読点で終わる行も続く。句点・括弧・名詞で終わる行は、見出しや 1 行 1 項目の終わりと見分けられない。 */
34
+ const COMMAS = new Set(["、", ","]);
35
+
36
+ const continuesPast = (token: Token | undefined, part: Span): boolean =>
37
+ token !== undefined && token.span.end === part.start && (CONTINUING_POS.has(token.pos) || COMMAS.has(token.surface));
38
+
39
+ /** 行が続いている改行: 語をまたぐものと、句を閉じない語の直後にあるもの。 */
40
+ export const continuedBreaks = (breaks: readonly Span[], tokens: readonly Token[]): Span[] => {
41
+ const around = aroundEach(breaks, tokens);
42
+ return breaks.filter((part, index) => inside(around[index]?.across, part) || continuesPast(around[index]?.before, part));
43
+ };
@@ -0,0 +1,83 @@
1
+ import type { Span } from "./plugin.ts";
2
+
3
+ /**
4
+ * 1 行に 1 段落を書き、段落の間に空行を置かない文書(青空文庫、議事録、`<br>` で改行した HTML)。
5
+ * Markdown では全体が 1 つの段落になるが、書いた人にとっては行が段落。
6
+ *
7
+ * 割るのは文の終わる改行だけ。文の境目は解析器が改行をつないだ上で決めたもの(#248)なので、折り返した文は割れない。
8
+ * 形で見分けるのは 2 つ:
9
+ * - 固定幅の折り返しは、行の多くが文の途中で終わる。文で終わる行(段落の最後の行を含む)が半分に届かなければ割らない
10
+ * (議事録は発言者の行と発言の行が交互に来るので、半分ちょうどは割る)。
11
+ * - 1 行 1 文の書き方(行を意味の切れ目で改める。英語の Markdown に多い)は、段落のつもりではない。
12
+ * 文の頭で始まり文の終わりで終わる行が 2 文以上を持つことが、段落を 1 行に書いた印。それがたまにしか無ければ割らない。
13
+ */
14
+
15
+ /** 2 文以上を持つ行がこれより少なければ、1 行 1 文の書き方にたまに混じる 2 文の行と見分けられない。 */
16
+ const MIN_MULTI_SENTENCE = 3;
17
+ /** 2 文以上を持つ行が、割った段落の数のこの割合に届かなければ割らない。議事録は 1 文の発言が多い。 */
18
+ const MULTI_SENTENCE_SHARE = 1 / 5;
19
+
20
+ const NEWLINE = /\r?\n/gu;
21
+
22
+ type Line = { readonly span: Span; readonly newline: number };
23
+
24
+ /** 段落の中の行と、その行を終える改行の位置(最後の行は段落の終わり)。 */
25
+ const linesOf = (source: string, paragraph: Span): Line[] => {
26
+ const text = source.slice(paragraph.start, paragraph.end);
27
+ const breaks = [...text.matchAll(NEWLINE)].map((match) => ({ at: paragraph.start + match.index, width: match[0].length }));
28
+ const starts = [paragraph.start, ...breaks.map((found) => found.at + found.width)];
29
+ return starts.map((start, index) => {
30
+ const end = breaks[index]?.at ?? paragraph.end;
31
+ return { span: { start, end }, newline: end };
32
+ });
33
+ };
34
+
35
+ /** 文の前後の空白を除いた範囲。改行が文の中にあるかを、空白でなく文字で決める。 */
36
+ const coreOf = (source: string, sentence: Span): Span => {
37
+ const text = source.slice(sentence.start, sentence.end);
38
+ return { start: sentence.start + text.length - text.trimStart().length, end: sentence.end - (text.length - text.trimEnd().length) };
39
+ };
40
+
41
+ /** cores(並び順、重ならない)の中で、start が offset 以上の最初の添字。何万行の段落でも行ごとに全部をなめない。 */
42
+ const firstStartingAt = (cores: readonly Span[], offset: number): number => {
43
+ const search = (low: number, high: number): number => {
44
+ if (low >= high) return low;
45
+ const middle = (low + high) >> 1;
46
+ return (cores[middle]?.start ?? Number.POSITIVE_INFINITY) >= offset ? search(low, middle) : search(middle + 1, high);
47
+ };
48
+ return search(0, cores.length);
49
+ };
50
+
51
+ /** 改行をまたぐ文が無い。またぐなら、改行の直前に始まった文がまだ続いている。 */
52
+ const endsSentence = (line: Line, cores: readonly Span[]): boolean => (cores[firstStartingAt(cores, line.newline) - 1]?.end ?? 0) <= line.newline;
53
+
54
+ /** 行の並びを、文の終わる改行で区切った段落。 */
55
+ const groupAt = (lines: readonly Line[], closing: readonly boolean[]): Span[] => {
56
+ const firsts = lines.flatMap((_, index) => (index === 0 || closing[index - 1] === true ? [index] : []));
57
+ return firsts.map((first, index) => ({
58
+ start: lines[first]?.span.start ?? 0,
59
+ end: lines[(firsts[index + 1] ?? lines.length) - 1]?.span.end ?? 0,
60
+ }));
61
+ };
62
+
63
+ const sentencesIn = (group: Span, cores: readonly Span[]): number => firstStartingAt(cores, group.end) - firstStartingAt(cores, group.start);
64
+
65
+ /** 文の頭で始まり文の終わりで終わる行のうち、2 文以上を持つもの。固定幅の折り返しでは、行の両端が偶然そろうことはまれ。 */
66
+ const wholeMultiSentenceLines = (lines: readonly Line[], closing: readonly boolean[], cores: readonly Span[]): number =>
67
+ lines.filter((line, index) => (index === 0 || closing[index - 1] === true) && closing[index] === true && sentencesIn(line.span, cores) >= 2).length;
68
+
69
+ /** 行ごとの段落として読む形か。 */
70
+ const isLineShaped = (lines: readonly Line[], closing: readonly boolean[], groups: readonly Span[], cores: readonly Span[]): boolean => {
71
+ const closed = closing.filter(Boolean).length;
72
+ const multi = wholeMultiSentenceLines(lines, closing, cores);
73
+ return closed * 2 >= closing.length && multi >= MIN_MULTI_SENTENCE && multi >= groups.length * MULTI_SENTENCE_SHARE;
74
+ };
75
+
76
+ /** paragraph を、書いた人が段落のつもりで改めた行ごとに割る。そう読めなければ paragraph 1 つ。sentences は段落の中の文。 */
77
+ export const lineParagraphs = (source: string, paragraph: Span, sentences: readonly Span[]): Span[] => {
78
+ const lines = linesOf(source, paragraph);
79
+ const cores = sentences.map((sentence) => coreOf(source, sentence)).filter((core) => core.end > core.start);
80
+ const closing = lines.map((line) => endsSentence(line, cores));
81
+ const groups = groupAt(lines, closing);
82
+ return isLineShaped(lines, closing, groups, cores) ? groups : [paragraph];
83
+ };
package/src/mask.ts CHANGED
@@ -17,14 +17,13 @@ import type { Span } from "./plugin.ts";
17
17
  const blankOut = (text: string): string => text.replace(/[^\n]/gu, (char) => " ".repeat(char.length));
18
18
 
19
19
  const merge = (spans: readonly Span[]): Span[] =>
20
- [...spans]
21
- .sort((left, right) => left.start - right.start)
20
+ spans
21
+ .toSorted((left, right) => left.start - right.start)
22
22
  .reduce<Span[]>((acc, span) => {
23
23
  const last = acc.at(-1);
24
- if (last !== undefined && span.start <= last.end) {
25
- return [...acc.slice(0, -1), { start: last.start, end: Math.max(last.end, span.end) }];
26
- }
27
- return [...acc, span];
24
+ if (last !== undefined && span.start <= last.end) acc[acc.length - 1] = { start: last.start, end: Math.max(last.end, span.end) };
25
+ else acc.push(span);
26
+ return acc;
28
27
  }, []);
29
28
 
30
29
  export const maskSpans = (source: string, spans: readonly Span[]): string => {
package/src/measure.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { withoutSpans } from "./soft-break.ts";
1
2
  import type { Sentence } from "./plugin.ts";
2
3
 
3
4
  /**
@@ -9,7 +10,14 @@ import type { Sentence } from "./plugin.ts";
9
10
  *
10
11
  * 日本語は文字数、英語は語数。単位は adapter が宣言する。
11
12
  */
12
- export const proseText = (sentence: Sentence): string => sentence.text.replace(/\s+/gu, " ").trim();
13
+ /** 語の途中で折り返した改行(wrapBreaks)は空白にせず除く。「関\nする」を「関 する」と読まない。 */
14
+ export const proseText = (sentence: Sentence): string =>
15
+ withoutSpans(
16
+ sentence.text,
17
+ (sentence.wrapBreaks ?? []).map((span) => ({ start: span.start - sentence.span.start, end: span.end - sentence.span.start })),
18
+ )
19
+ .replace(/\s+/gu, " ")
20
+ .trim();
13
21
 
14
22
  export const charLength = (sentence: Sentence): number => sentence.text.replace(/\s+/gu, "").length;
15
23