chaffjs 0.25.0 → 0.26.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 (385) hide show
  1. package/README.md +179 -42
  2. package/dist/adapter-import.d.ts +2 -0
  3. package/dist/adapter-import.d.ts.map +1 -0
  4. package/dist/adapter-import.js +11 -0
  5. package/dist/adapter-import.js.map +1 -0
  6. package/dist/adapter-load.d.ts.map +1 -1
  7. package/dist/adapter-load.js +2 -9
  8. package/dist/adapter-load.js.map +1 -1
  9. package/dist/ai-score/human-shares.d.ts +4 -0
  10. package/dist/ai-score/human-shares.d.ts.map +1 -0
  11. package/dist/ai-score/human-shares.js +205 -0
  12. package/dist/ai-score/human-shares.js.map +1 -0
  13. package/dist/ai-score/lint-line.d.ts +4 -0
  14. package/dist/ai-score/lint-line.d.ts.map +1 -0
  15. package/dist/ai-score/lint-line.js +9 -0
  16. package/dist/ai-score/lint-line.js.map +1 -0
  17. package/dist/ai-score/of-document.d.ts +7 -0
  18. package/dist/ai-score/of-document.d.ts.map +1 -0
  19. package/dist/ai-score/of-document.js +40 -0
  20. package/dist/ai-score/of-document.js.map +1 -0
  21. package/dist/ai-score/render.d.ts +24 -0
  22. package/dist/ai-score/render.d.ts.map +1 -0
  23. package/dist/ai-score/render.js +100 -0
  24. package/dist/ai-score/render.js.map +1 -0
  25. package/dist/ai-score/score.d.ts +77 -0
  26. package/dist/ai-score/score.d.ts.map +1 -0
  27. package/dist/ai-score/score.js +52 -0
  28. package/dist/ai-score/score.js.map +1 -0
  29. package/dist/ai-score/signals.d.ts +19 -0
  30. package/dist/ai-score/signals.d.ts.map +1 -0
  31. package/dist/ai-score/signals.js +30 -0
  32. package/dist/ai-score/signals.js.map +1 -0
  33. package/dist/ai-score/text.d.ts +37 -0
  34. package/dist/ai-score/text.d.ts.map +1 -0
  35. package/dist/ai-score/text.js +59 -0
  36. package/dist/ai-score/text.js.map +1 -0
  37. package/dist/ai-score/view.d.ts +5 -0
  38. package/dist/ai-score/view.d.ts.map +1 -0
  39. package/dist/ai-score/view.js +19 -0
  40. package/dist/ai-score/view.js.map +1 -0
  41. package/dist/backends/anthropic.d.ts +0 -1
  42. package/dist/backends/anthropic.d.ts.map +1 -1
  43. package/dist/backends/anthropic.js +0 -1
  44. package/dist/backends/anthropic.js.map +1 -1
  45. package/dist/backends/default-model.d.ts +3 -0
  46. package/dist/backends/default-model.d.ts.map +1 -0
  47. package/dist/backends/default-model.js +5 -0
  48. package/dist/backends/default-model.js.map +1 -0
  49. package/dist/backends/openai.d.ts +0 -1
  50. package/dist/backends/openai.d.ts.map +1 -1
  51. package/dist/backends/openai.js +0 -1
  52. package/dist/backends/openai.js.map +1 -1
  53. package/dist/browser/adapter-import.d.ts +2 -0
  54. package/dist/browser/adapter-import.d.ts.map +1 -0
  55. package/dist/browser/adapter-import.js +17 -0
  56. package/dist/browser/adapter-import.js.map +1 -0
  57. package/dist/browser/bounded-match.d.ts +3 -0
  58. package/dist/browser/bounded-match.d.ts.map +1 -0
  59. package/dist/browser/bounded-match.js +4 -0
  60. package/dist/browser/bounded-match.js.map +1 -0
  61. package/dist/browser/check-text.d.ts +43 -0
  62. package/dist/browser/check-text.d.ts.map +1 -0
  63. package/dist/browser/check-text.js +50 -0
  64. package/dist/browser/check-text.js.map +1 -0
  65. package/dist/browser/detectors.d.ts +4 -0
  66. package/dist/browser/detectors.d.ts.map +1 -0
  67. package/dist/browser/detectors.js +22 -0
  68. package/dist/browser/detectors.js.map +1 -0
  69. package/dist/browser/package-files.d.ts +5 -0
  70. package/dist/browser/package-files.d.ts.map +1 -0
  71. package/dist/browser/package-files.js +8 -0
  72. package/dist/browser/package-files.js.map +1 -0
  73. package/dist/browser/path.d.ts +9 -0
  74. package/dist/browser/path.d.ts.map +1 -0
  75. package/dist/browser/path.js +70 -0
  76. package/dist/browser/path.js.map +1 -0
  77. package/dist/browser/working-dir.d.ts +2 -0
  78. package/dist/browser/working-dir.d.ts.map +1 -0
  79. package/dist/browser/working-dir.js +3 -0
  80. package/dist/browser/working-dir.js.map +1 -0
  81. package/dist/browser-files.d.ts +21 -0
  82. package/dist/browser-files.d.ts.map +1 -0
  83. package/dist/browser-files.js +65 -0
  84. package/dist/browser-files.js.map +1 -0
  85. package/dist/browser.d.ts +9 -0
  86. package/dist/browser.d.ts.map +1 -0
  87. package/dist/browser.js +13 -0
  88. package/dist/browser.js.map +1 -0
  89. package/dist/cli-text.d.ts.map +1 -1
  90. package/dist/cli-text.js +2 -0
  91. package/dist/cli-text.js.map +1 -1
  92. package/dist/cli.d.ts.map +1 -1
  93. package/dist/cli.js +4 -1
  94. package/dist/cli.js.map +1 -1
  95. package/dist/commands/ai-score.d.ts +8 -0
  96. package/dist/commands/ai-score.d.ts.map +1 -0
  97. package/dist/commands/ai-score.js +59 -0
  98. package/dist/commands/ai-score.js.map +1 -0
  99. package/dist/commands/document-commands.d.ts.map +1 -1
  100. package/dist/commands/document-commands.js +2 -0
  101. package/dist/commands/document-commands.js.map +1 -1
  102. package/dist/config/by-path-problems.d.ts +5 -0
  103. package/dist/config/by-path-problems.d.ts.map +1 -0
  104. package/dist/config/by-path-problems.js +11 -0
  105. package/dist/config/by-path-problems.js.map +1 -0
  106. package/dist/config/load.d.ts +9 -2
  107. package/dist/config/load.d.ts.map +1 -1
  108. package/dist/config/load.js +21 -8
  109. package/dist/config/load.js.map +1 -1
  110. package/dist/config/read.d.ts +2 -0
  111. package/dist/config/read.d.ts.map +1 -1
  112. package/dist/config/read.js +4 -1
  113. package/dist/config/read.js.map +1 -1
  114. package/dist/config/warnings.d.ts +1 -1
  115. package/dist/config/warnings.d.ts.map +1 -1
  116. package/dist/config/warnings.js +3 -1
  117. package/dist/config/warnings.js.map +1 -1
  118. package/dist/config/working-dir.d.ts +2 -0
  119. package/dist/config/working-dir.d.ts.map +1 -0
  120. package/dist/config/working-dir.js +3 -0
  121. package/dist/config/working-dir.js.map +1 -0
  122. package/dist/custom/bounded-match.d.ts +1 -12
  123. package/dist/custom/bounded-match.d.ts.map +1 -1
  124. package/dist/custom/bounded-match.js +1 -11
  125. package/dist/custom/bounded-match.js.map +1 -1
  126. package/dist/custom/pattern-timeout.d.ts +13 -0
  127. package/dist/custom/pattern-timeout.d.ts.map +1 -0
  128. package/dist/custom/pattern-timeout.js +13 -0
  129. package/dist/custom/pattern-timeout.js.map +1 -0
  130. package/dist/detect.d.ts.map +1 -1
  131. package/dist/detect.js +4 -5
  132. package/dist/detect.js.map +1 -1
  133. package/dist/detectors/date-year.d.ts +10 -2
  134. package/dist/detectors/date-year.d.ts.map +1 -1
  135. package/dist/detectors/date-year.js +44 -8
  136. package/dist/detectors/date-year.js.map +1 -1
  137. package/dist/detectors/defined-term-form.d.ts +5 -0
  138. package/dist/detectors/defined-term-form.d.ts.map +1 -1
  139. package/dist/detectors/defined-term-form.js +22 -5
  140. package/dist/detectors/defined-term-form.js.map +1 -1
  141. package/dist/detectors/orthographic-variant.d.ts.map +1 -1
  142. package/dist/detectors/orthographic-variant.js +44 -17
  143. package/dist/detectors/orthographic-variant.js.map +1 -1
  144. package/dist/detectors/party-role-name.d.ts +3 -0
  145. package/dist/detectors/party-role-name.d.ts.map +1 -0
  146. package/dist/detectors/party-role-name.js +35 -0
  147. package/dist/detectors/party-role-name.js.map +1 -0
  148. package/dist/detectors/quote-without-source.d.ts +17 -0
  149. package/dist/detectors/quote-without-source.d.ts.map +1 -0
  150. package/dist/detectors/quote-without-source.js +64 -0
  151. package/dist/detectors/quote-without-source.js.map +1 -0
  152. package/dist/detectors/reference-text.js +1 -1
  153. package/dist/detectors/reference-text.js.map +1 -1
  154. package/dist/detectors/registry/party-role-name.d.ts +3 -0
  155. package/dist/detectors/registry/party-role-name.d.ts.map +1 -0
  156. package/dist/detectors/registry/party-role-name.js +3 -0
  157. package/dist/detectors/registry/party-role-name.js.map +1 -0
  158. package/dist/detectors/registry/quote-without-source.d.ts +3 -0
  159. package/dist/detectors/registry/quote-without-source.d.ts.map +1 -0
  160. package/dist/detectors/registry/quote-without-source.js +3 -0
  161. package/dist/detectors/registry/quote-without-source.js.map +1 -0
  162. package/dist/detectors/registry/vague-deadline.d.ts +3 -0
  163. package/dist/detectors/registry/vague-deadline.d.ts.map +1 -0
  164. package/dist/detectors/registry/vague-deadline.js +3 -0
  165. package/dist/detectors/registry/vague-deadline.js.map +1 -0
  166. package/dist/detectors/run-in-label.d.ts +7 -0
  167. package/dist/detectors/run-in-label.d.ts.map +1 -0
  168. package/dist/detectors/run-in-label.js +53 -0
  169. package/dist/detectors/run-in-label.js.map +1 -0
  170. package/dist/detectors/sentence-counts.d.ts +3 -1
  171. package/dist/detectors/sentence-counts.d.ts.map +1 -1
  172. package/dist/detectors/sentence-counts.js +11 -4
  173. package/dist/detectors/sentence-counts.js.map +1 -1
  174. package/dist/detectors/sentence-ending.d.ts +1 -1
  175. package/dist/detectors/sentence-ending.d.ts.map +1 -1
  176. package/dist/detectors/sentence-ending.js +2 -1
  177. package/dist/detectors/sentence-ending.js.map +1 -1
  178. package/dist/detectors/structure-tree.d.ts +4 -1
  179. package/dist/detectors/structure-tree.d.ts.map +1 -1
  180. package/dist/detectors/structure-tree.js +21 -2
  181. package/dist/detectors/structure-tree.js.map +1 -1
  182. package/dist/detectors/unbalanced-bracket.d.ts +1 -1
  183. package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
  184. package/dist/detectors/unbalanced-bracket.js +23 -11
  185. package/dist/detectors/unbalanced-bracket.js.map +1 -1
  186. package/dist/detectors/vague-deadline.d.ts +25 -0
  187. package/dist/detectors/vague-deadline.d.ts.map +1 -0
  188. package/dist/detectors/vague-deadline.js +97 -0
  189. package/dist/detectors/vague-deadline.js.map +1 -0
  190. package/dist/furigana.d.ts +7 -0
  191. package/dist/furigana.d.ts.map +1 -0
  192. package/dist/furigana.js +16 -0
  193. package/dist/furigana.js.map +1 -0
  194. package/dist/genre-load.d.ts.map +1 -1
  195. package/dist/genre-load.js +5 -7
  196. package/dist/genre-load.js.map +1 -1
  197. package/dist/grade/ai-score.d.ts +16 -0
  198. package/dist/grade/ai-score.d.ts.map +1 -0
  199. package/dist/grade/ai-score.js +17 -0
  200. package/dist/grade/ai-score.js.map +1 -0
  201. package/dist/grade/finding.d.ts +6 -0
  202. package/dist/grade/finding.d.ts.map +1 -0
  203. package/dist/grade/finding.js +13 -0
  204. package/dist/grade/finding.js.map +1 -0
  205. package/dist/grade/grade-item.d.ts.map +1 -1
  206. package/dist/grade/grade-item.js +4 -11
  207. package/dist/grade/grade-item.js.map +1 -1
  208. package/dist/grade/render-variants.d.ts.map +1 -1
  209. package/dist/grade/render-variants.js +3 -0
  210. package/dist/grade/render-variants.js.map +1 -1
  211. package/dist/grade/render.d.ts.map +1 -1
  212. package/dist/grade/render.js +1 -0
  213. package/dist/grade/render.js.map +1 -1
  214. package/dist/grade/result.d.ts +3 -0
  215. package/dist/grade/result.d.ts.map +1 -1
  216. package/dist/grade/results-read.d.ts.map +1 -1
  217. package/dist/grade/results-read.js +13 -2
  218. package/dist/grade/results-read.js.map +1 -1
  219. package/dist/grade/summary.d.ts +3 -0
  220. package/dist/grade/summary.d.ts.map +1 -1
  221. package/dist/grade/summary.js +6 -0
  222. package/dist/grade/summary.js.map +1 -1
  223. package/dist/grade/text.d.ts +2 -0
  224. package/dist/grade/text.d.ts.map +1 -1
  225. package/dist/grade/text.js +2 -0
  226. package/dist/grade/text.js.map +1 -1
  227. package/dist/grade/variants-text.d.ts +3 -0
  228. package/dist/grade/variants-text.d.ts.map +1 -1
  229. package/dist/grade/variants-text.js +4 -0
  230. package/dist/grade/variants-text.js.map +1 -1
  231. package/dist/grade/variants.d.ts +3 -0
  232. package/dist/grade/variants.d.ts.map +1 -1
  233. package/dist/grade/variants.js +1 -0
  234. package/dist/grade/variants.js.map +1 -1
  235. package/dist/grade-api.d.ts +1 -0
  236. package/dist/grade-api.d.ts.map +1 -1
  237. package/dist/grade-api.js +2 -1
  238. package/dist/grade-api.js.map +1 -1
  239. package/dist/judge.d.ts +0 -1
  240. package/dist/judge.d.ts.map +1 -1
  241. package/dist/judge.js +0 -1
  242. package/dist/judge.js.map +1 -1
  243. package/dist/known-genres.js +2 -2
  244. package/dist/known-genres.js.map +1 -1
  245. package/dist/package-files.d.ts +6 -0
  246. package/dist/package-files.d.ts.map +1 -0
  247. package/dist/package-files.js +11 -0
  248. package/dist/package-files.js.map +1 -0
  249. package/dist/profile/load.d.ts.map +1 -1
  250. package/dist/profile/load.js +5 -6
  251. package/dist/profile/load.js.map +1 -1
  252. package/dist/rule-load.d.ts.map +1 -1
  253. package/dist/rule-load.js +5 -6
  254. package/dist/rule-load.js.map +1 -1
  255. package/dist/rule-offs.js +3 -3
  256. package/dist/rule-offs.js.map +1 -1
  257. package/dist/run.js +1 -1
  258. package/dist/run.js.map +1 -1
  259. package/dist/structure/date-range.d.ts +4 -0
  260. package/dist/structure/date-range.d.ts.map +1 -1
  261. package/dist/structure/date-range.js +23 -3
  262. package/dist/structure/date-range.js.map +1 -1
  263. package/dist/structure/issues.d.ts +17 -1
  264. package/dist/structure/issues.d.ts.map +1 -1
  265. package/dist/structure/issues.js +54 -5
  266. package/dist/structure/issues.js.map +1 -1
  267. package/dist/structure/party-role.d.ts +30 -0
  268. package/dist/structure/party-role.d.ts.map +1 -0
  269. package/dist/structure/party-role.js +58 -0
  270. package/dist/structure/party-role.js.map +1 -0
  271. package/dist/structure/prose-total.d.ts +28 -0
  272. package/dist/structure/prose-total.d.ts.map +1 -0
  273. package/dist/structure/prose-total.js +99 -0
  274. package/dist/structure/prose-total.js.map +1 -0
  275. package/dist/structure/reference-text.d.ts +5 -0
  276. package/dist/structure/reference-text.d.ts.map +1 -1
  277. package/dist/structure/reference-text.js +17 -4
  278. package/dist/structure/reference-text.js.map +1 -1
  279. package/dist/structure/term-prefix.d.ts +18 -0
  280. package/dist/structure/term-prefix.d.ts.map +1 -0
  281. package/dist/structure/term-prefix.js +40 -0
  282. package/dist/structure/term-prefix.js.map +1 -0
  283. package/dist/structure/total.d.ts +2 -0
  284. package/dist/structure/total.d.ts.map +1 -1
  285. package/dist/structure/total.js +2 -0
  286. package/dist/structure/total.js.map +1 -1
  287. package/dist/structure-shape/baseline-load.d.ts.map +1 -1
  288. package/dist/structure-shape/baseline-load.js +4 -5
  289. package/dist/structure-shape/baseline-load.js.map +1 -1
  290. package/dist/style-load.d.ts.map +1 -1
  291. package/dist/style-load.js +5 -6
  292. package/dist/style-load.js.map +1 -1
  293. package/package.json +24 -3
  294. package/rules/cliche.yaml +28 -8
  295. package/rules/date-without-year.yaml +28 -3
  296. package/rules/defined-term-form.yaml +9 -6
  297. package/rules/demonstrative-opener-run.yaml +24 -7
  298. package/rules/no-mixed-desumasu.yaml +2 -2
  299. package/rules/orthographic-variant.yaml +2 -2
  300. package/rules/party-role-name.yaml +94 -0
  301. package/rules/quote-without-source.yaml +61 -0
  302. package/rules/total-mismatch.yaml +8 -8
  303. package/rules/vague-deadline.yaml +95 -0
  304. package/rules/weasel-word.yaml +25 -7
  305. package/skills/chaff/SKILL.md +12 -2
  306. package/src/adapter-import.ts +3 -0
  307. package/src/adapter-load.ts +2 -1
  308. package/src/ai-score/human-shares.ts +207 -0
  309. package/src/ai-score/lint-line.ts +10 -0
  310. package/src/ai-score/of-document.ts +46 -0
  311. package/src/ai-score/render.ts +124 -0
  312. package/src/ai-score/score.ts +121 -0
  313. package/src/ai-score/signals.ts +39 -0
  314. package/src/ai-score/text.ts +103 -0
  315. package/src/ai-score/view.ts +21 -0
  316. package/src/backends/anthropic.ts +0 -2
  317. package/src/backends/default-model.ts +9 -0
  318. package/src/backends/openai.ts +0 -2
  319. package/src/browser/adapter-import.ts +19 -0
  320. package/src/browser/bounded-match.ts +9 -0
  321. package/src/browser/check-text.ts +106 -0
  322. package/src/browser/detectors.ts +40 -0
  323. package/src/browser/import-meta-glob.d.ts +5 -0
  324. package/src/browser/package-files.ts +13 -0
  325. package/src/browser/path.ts +73 -0
  326. package/src/browser/working-dir.ts +3 -0
  327. package/src/browser-files.ts +87 -0
  328. package/src/browser.ts +19 -0
  329. package/src/cli-text.ts +2 -0
  330. package/src/cli.ts +4 -1
  331. package/src/commands/ai-score.ts +77 -0
  332. package/src/commands/document-commands.ts +2 -0
  333. package/src/config/by-path-problems.ts +17 -0
  334. package/src/config/load.ts +21 -8
  335. package/src/config/read.ts +5 -1
  336. package/src/config/warnings.ts +3 -1
  337. package/src/config/working-dir.ts +3 -0
  338. package/src/custom/bounded-match.ts +1 -16
  339. package/src/custom/pattern-timeout.ts +17 -0
  340. package/src/detect.ts +4 -5
  341. package/src/detectors/date-year.ts +51 -12
  342. package/src/detectors/defined-term-form.ts +28 -6
  343. package/src/detectors/orthographic-variant.ts +59 -18
  344. package/src/detectors/party-role-name.ts +44 -0
  345. package/src/detectors/quote-without-source.ts +90 -0
  346. package/src/detectors/reference-text.ts +1 -1
  347. package/src/detectors/registry/party-role-name.ts +4 -0
  348. package/src/detectors/registry/quote-without-source.ts +4 -0
  349. package/src/detectors/registry/vague-deadline.ts +4 -0
  350. package/src/detectors/run-in-label.ts +60 -0
  351. package/src/detectors/sentence-counts.ts +13 -4
  352. package/src/detectors/sentence-ending.ts +3 -2
  353. package/src/detectors/structure-tree.ts +28 -6
  354. package/src/detectors/unbalanced-bracket.ts +25 -10
  355. package/src/detectors/vague-deadline.ts +132 -0
  356. package/src/furigana.ts +20 -0
  357. package/src/genre-load.ts +5 -7
  358. package/src/grade/ai-score.ts +32 -0
  359. package/src/grade/finding.ts +16 -0
  360. package/src/grade/grade-item.ts +4 -13
  361. package/src/grade/render-variants.ts +5 -0
  362. package/src/grade/render.ts +1 -0
  363. package/src/grade/result.ts +3 -0
  364. package/src/grade/results-read.ts +17 -2
  365. package/src/grade/summary.ts +9 -0
  366. package/src/grade/text.ts +6 -0
  367. package/src/grade/variants-text.ts +7 -0
  368. package/src/grade/variants.ts +4 -0
  369. package/src/grade-api.ts +3 -1
  370. package/src/judge.ts +0 -2
  371. package/src/known-genres.ts +2 -2
  372. package/src/package-files.ts +15 -0
  373. package/src/profile/load.ts +5 -6
  374. package/src/rule-load.ts +5 -6
  375. package/src/rule-offs.ts +3 -3
  376. package/src/run.ts +1 -1
  377. package/src/structure/date-range.ts +28 -3
  378. package/src/structure/issues.ts +61 -6
  379. package/src/structure/party-role.ts +94 -0
  380. package/src/structure/prose-total.ts +135 -0
  381. package/src/structure/reference-text.ts +17 -4
  382. package/src/structure/term-prefix.ts +54 -0
  383. package/src/structure/total.ts +4 -0
  384. package/src/structure-shape/baseline-load.ts +4 -5
  385. package/src/style-load.ts +5 -6
@@ -0,0 +1,21 @@
1
+ import type { LengthUnit, RuleDefinition } from "../plugin.ts";
2
+ import { loadGenres } from "../genre-load.ts";
3
+ import { localized } from "../render/text.ts";
4
+ import { STRUCTURE_TEXT } from "../outline/structure-text.ts";
5
+ import { uiLanguageOf } from "../ui.ts";
6
+ import type { ScoreView } from "./render.ts";
7
+ import { AI_SCORE_TEXT } from "./text.ts";
8
+
9
+ /** The texts and names a score is shown with, in the document's language. */
10
+ export const scoreViewOf = (language: string, unit: LengthUnit, group: string, rules: readonly RuleDefinition[]): ScoreView => {
11
+ const ui = uiLanguageOf(language);
12
+ const name = loadGenres().groups.find((entry) => entry.id === group)?.name;
13
+ return {
14
+ text: AI_SCORE_TEXT[ui],
15
+ structure: STRUCTURE_TEXT[ui],
16
+ language: ui,
17
+ unit,
18
+ groupName: name === undefined ? group : localized(name, ui),
19
+ ruleNames: Object.fromEntries(rules.map((rule) => [rule.id, rule.name])),
20
+ };
21
+ };
@@ -14,8 +14,6 @@ export type AnthropicClient = { readonly messages: { readonly create: (params: A
14
14
  const isTextBlock = (block: unknown): block is { readonly text: string } =>
15
15
  typeof block === "object" && block !== null && "text" in block && typeof block.text === "string";
16
16
 
17
- export const DEFAULT_MODEL = "claude-opus-5";
18
-
19
17
  /**
20
18
  * 認証情報があるか。
21
19
  *
@@ -0,0 +1,9 @@
1
+ import type { BackendName } from "./types.ts";
2
+
3
+ // Apart from the backends, so reading settings does not load the providers' SDKs.
4
+
5
+ const ANTHROPIC_DEFAULT_MODEL = "claude-opus-5";
6
+
7
+ const OPENAI_DEFAULT_MODEL = "gpt-5";
8
+
9
+ export const defaultModel = (backend: BackendName): string => (backend === "openai" ? OPENAI_DEFAULT_MODEL : ANTHROPIC_DEFAULT_MODEL);
@@ -10,8 +10,6 @@ export type OpenAIClient = {
10
10
  readonly chat: { readonly completions: { readonly create: (params: OpenAI.ChatCompletionCreateParamsNonStreaming) => Promise<OpenAIResponse> } };
11
11
  };
12
12
 
13
- export const DEFAULT_MODEL = "gpt-5";
14
-
15
13
  export const hasCredentials = (): boolean => (process.env["OPENAI_API_KEY"] ?? "").length > 0;
16
14
 
17
15
  export const isAuthFailure = (error: unknown): boolean => error instanceof OpenAI.APIError && error.status === 401;
@@ -0,0 +1,19 @@
1
+ import { ensurePackageFiles } from "chaffjs/browser-files";
2
+
3
+ // adapter-import.ts in a bundle: a bundler cannot follow an import by a name known only at run time, so the bundled
4
+ // languages are named here, each loaded only when a document in it is checked, after its files.
5
+
6
+ const ADAPTERS: Readonly<Record<string, () => Promise<unknown>>> = {
7
+ "@chaffjs/lang-ja": () => import("@chaffjs/lang-ja"),
8
+ "@chaffjs/lang-en": () => import("@chaffjs/lang-en"),
9
+ };
10
+
11
+ /** The error Node gives for a package that is not installed, which adapter-load.ts reads as "not installed". */
12
+ const notInstalled = (specifier: string): Error => Object.assign(new Error(`Cannot find package '${specifier}'`), { code: "ERR_MODULE_NOT_FOUND" });
13
+
14
+ export const importPackage = async (specifier: string): Promise<unknown> => {
15
+ const load = Object.hasOwn(ADAPTERS, specifier) ? ADAPTERS[specifier] : undefined;
16
+ if (load === undefined) throw notInstalled(specifier);
17
+ await ensurePackageFiles(specifier);
18
+ return load();
19
+ };
@@ -0,0 +1,9 @@
1
+ import type { TextMatches } from "../custom/pattern-timeout.ts";
2
+
3
+ // custom/bounded-match.ts in a browser, which has no vm to stop a pattern: the same matches, without the time limit.
4
+ // regex-safety.ts still refuses the shapes known to run away before a pattern is used.
5
+
6
+ export const boundedMatches = (source: string, flags: string, texts: readonly string[]): TextMatches =>
7
+ texts.map((text) =>
8
+ [...text.matchAll(new RegExp(source, `g${flags}`))].filter((match) => match[0] !== "").map((match) => ({ index: match.index, text: match[0] })),
9
+ );
@@ -0,0 +1,106 @@
1
+ import { aiScoreOfDocument } from "../ai-score/of-document.ts";
2
+ import { aiScoreHeadline } from "../ai-score/render.ts";
3
+ import { HIGH_SIGNS, MEDIUM_SIGNS } from "../ai-score/score.ts";
4
+ import { scoreViewOf } from "../ai-score/view.ts";
5
+ import { checkSource, type SourceCheck } from "../check-source.ts";
6
+ import { configOf, CONFIG_FILE, EMPTY, type Config } from "../config/load.ts";
7
+ import { withStyle } from "../config/style.ts";
8
+ import { gradeAiScoreOf, type GradeAiScore } from "../grade/ai-score.ts";
9
+ import { findingOf } from "../grade/finding.ts";
10
+ import type { GradeFinding, NotRunEntry } from "../grade/result.ts";
11
+ import type { Finding } from "../plugin.ts";
12
+ import { filledText } from "../render/text.ts";
13
+ import { loadStyles } from "../style-load.ts";
14
+ import { uiLanguageOf, type Texts } from "../ui.ts";
15
+
16
+ // One text checked as `chaff <file>` checks it, with no files around it: imported by browser.ts once chaff's own files
17
+ // are in memory, because the modules below read them as they load.
18
+
19
+ export type BrowserCheckOptions = {
20
+ /** ja or en. Guessed from the text when left out, as the command line guesses. */
21
+ readonly language?: string | undefined;
22
+ /** A genre (business/report…), as --genre. */
23
+ readonly genre?: string | undefined;
24
+ /** chaff.yaml's contents, already parsed. Without it chaff's defaults apply, as with no chaff.yaml. */
25
+ readonly config?: unknown;
26
+ /** The document's name: rules for Markdown run only on a .md name. document.md when left out. */
27
+ readonly path?: string | undefined;
28
+ /** Run the experimental rules too, as --experimental. */
29
+ readonly experimental?: boolean | undefined;
30
+ };
31
+
32
+ /** A finding as `chaff grade` gives it, with the rule's name and its explanation, filled as the command line fills them. */
33
+ export type BrowserFinding = GradeFinding & {
34
+ readonly name: string;
35
+ readonly why: string;
36
+ readonly howToFix: string;
37
+ /** The passage the finding is about. */
38
+ readonly quote: string;
39
+ };
40
+
41
+ /**
42
+ * The AI-likeness quick score as chaff grade gives it, with the lint report's words in the document's language: the
43
+ * level or why it was not scored, the signs, and the note that it is not a verdict on who wrote the text.
44
+ */
45
+ export type BrowserAiScore = GradeAiScore & {
46
+ readonly headline: string;
47
+ /** The signs and the thresholds, when the text was scored. */
48
+ readonly signsLine: string | undefined;
49
+ readonly disclaimer: string;
50
+ };
51
+
52
+ export type BrowserCheck = {
53
+ readonly language: string;
54
+ readonly genre: string;
55
+ readonly findings: readonly BrowserFinding[];
56
+ readonly aiScore: BrowserAiScore;
57
+ /** Each rule that did not run, and why: so no finding is never read as "checked and fine". */
58
+ readonly notRun: readonly NotRunEntry[];
59
+ };
60
+
61
+ export const DEFAULT_PATH = "document.md";
62
+
63
+ /** The settings chaff.yaml can hold that name files or code: a browser has neither, so they are reported, not applied. */
64
+ const NEEDS_FILES = ["plugins", "include", "by_path"] as const;
65
+
66
+ const REASON: Texts<string> = {
67
+ ja: "ファイルを読む設定で、ブラウザでは使えないため",
68
+ en: "it reads files, which a browser does not have",
69
+ };
70
+
71
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
72
+
73
+ /** by_path is dropped: it matches file paths, and a text checked here has none. plugins and include act only on files. */
74
+ const configFrom = (raw: unknown): Config => (raw === undefined ? EMPTY : withStyle({ ...configOf(raw, `/${CONFIG_FILE}`), byPath: [] }, loadStyles()));
75
+
76
+ const unappliedSettings = (raw: unknown, language: string): NotRunEntry[] =>
77
+ isRecord(raw) ? NEEDS_FILES.filter((key) => raw[key] !== undefined).map((key) => ({ rule: key, reason: REASON[uiLanguageOf(language)] })) : [];
78
+
79
+ const browserFindingOf = (finding: Finding, check: SourceCheck): BrowserFinding => {
80
+ const rule = check.rules.find((entry) => entry.id === finding.rule);
81
+ const fill = (field: Parameters<typeof filledText>[0] | undefined): string => (field === undefined ? "" : filledText(field, finding, check.language));
82
+ return { ...findingOf(finding, check), name: fill(rule?.name), why: fill(rule?.why), howToFix: fill(rule?.how_to_fix), quote: finding.quote };
83
+ };
84
+
85
+ const aiScoreFor = (check: SourceCheck): BrowserAiScore => {
86
+ const score = aiScoreOfDocument(check.doc, check.rules, check.genre.genre);
87
+ const view = scoreViewOf(check.language, check.doc.lengthUnit, score.group, check.rules);
88
+ return {
89
+ ...gradeAiScoreOf(score),
90
+ headline: aiScoreHeadline(score, view),
91
+ signsLine: score.level === undefined ? undefined : view.text.signs(score.signs, score.compared, MEDIUM_SIGNS, HIGH_SIGNS),
92
+ disclaimer: view.text.disclaimer,
93
+ };
94
+ };
95
+
96
+ export const checkText = async (text: string, options: BrowserCheckOptions): Promise<BrowserCheck> => {
97
+ const choice = { language: options.language, genre: options.genre, experimental: options.experimental ?? false };
98
+ const check = await checkSource(options.path ?? DEFAULT_PATH, text, configFrom(options.config), choice);
99
+ return {
100
+ language: check.language,
101
+ genre: check.genre.genre,
102
+ findings: check.applied.kept.map((finding) => browserFindingOf(finding, check)),
103
+ aiScore: aiScoreFor(check),
104
+ notRun: [...check.raw.skipped.map((skipped) => ({ rule: skipped.rule, reason: skipped.why })), ...unappliedSettings(options.config, check.language)],
105
+ };
106
+ };
@@ -0,0 +1,40 @@
1
+ import type { CrossDetector, Detector } from "../plugin.ts";
2
+ import { registryEntries } from "../detectors/registry-files.ts";
3
+
4
+ // detectors/index.ts in a bundle: the registry folders cannot be listed at run time, so the bundler lists them
5
+ // (import.meta.glob) when it builds. The same files, named and checked as registry-load.ts names and checks them.
6
+
7
+ type Loaded = Readonly<Record<string, unknown>>;
8
+
9
+ const isDetector = (value: unknown): value is Detector => typeof value === "function";
10
+
11
+ const isCrossDetector = (value: unknown): value is CrossDetector => typeof value === "function";
12
+
13
+ const fileName = (path: string): string => path.slice(path.lastIndexOf("/") + 1);
14
+
15
+ /** From src the files end in .ts, from dist in .js (with .d.ts beside them, which registryEntries leaves out). */
16
+ const registryOf = <T>(loaded: Loaded, isWanted: (value: unknown) => value is T): Readonly<Record<string, T>> => {
17
+ const byName = new Map(Object.entries(loaded).map(([path, detector]) => [fileName(path), detector]));
18
+ const names = [...byName.keys()];
19
+ const extension = names.some((name) => name.endsWith(".js")) ? ".js" : ".ts";
20
+ return Object.fromEntries(
21
+ registryEntries(names, extension).map(({ file, howToFind }) => {
22
+ const detector = byName.get(file);
23
+ if (!isWanted(detector)) throw new Error(`chaff: ${file} must export a function named detector`);
24
+ return [howToFind, detector];
25
+ }),
26
+ );
27
+ };
28
+
29
+ export const DETECTORS: Readonly<Record<string, Detector>> = registryOf(
30
+ import.meta.glob(["../detectors/registry/*.ts", "../detectors/registry/*.js", "!../detectors/registry/*.d.ts"], { eager: true, import: "detector" }),
31
+ isDetector,
32
+ );
33
+
34
+ export const CROSS_DETECTORS: Readonly<Record<string, CrossDetector>> = registryOf(
35
+ import.meta.glob(["../detectors/cross-registry/*.ts", "../detectors/cross-registry/*.js", "!../detectors/cross-registry/*.d.ts"], {
36
+ eager: true,
37
+ import: "detector",
38
+ }),
39
+ isCrossDetector,
40
+ );
@@ -0,0 +1,5 @@
1
+ // The part of a bundler's import.meta.glob (Vite's) that browser/detectors.ts uses: the matching modules' named export,
2
+ // by path, imported when the bundle loads.
3
+ interface ImportMeta {
4
+ glob(patterns: string | readonly string[], options: { readonly eager: true; readonly import: string }): Readonly<Record<string, unknown>>;
5
+ }
@@ -0,0 +1,13 @@
1
+ import { packageFiles } from "chaffjs/browser-files";
2
+
3
+ // package-files.ts in a build without a file system: chaff's files from memory, rooted at /chaffjs.
4
+
5
+ const files = packageFiles("chaffjs");
6
+
7
+ export const PACKAGE_DIR = files.root;
8
+
9
+ export const readText = (path: string): string => files.readText(path);
10
+
11
+ export const readDir = (path: string): string[] => files.readDir(path);
12
+
13
+ export const exists = (path: string): boolean => files.exists(path);
@@ -0,0 +1,73 @@
1
+ // node:path for a build without Node: the POSIX functions chaff imports, over the in-memory files of chaffjs/browser-files.
2
+ // The working folder is "/". test/test_browser_path.ts holds each function to node:path's posix one on generated paths.
3
+
4
+ export const sep = "/";
5
+
6
+ const normalizedParts = (parts: readonly string[], absolute: boolean): string[] =>
7
+ parts.reduce<string[]>((kept, part) => {
8
+ if (part === "" || part === ".") return kept;
9
+ if (part !== "..") return [...kept, part];
10
+ const last = kept.at(-1);
11
+ if (last !== undefined && last !== "..") return kept.slice(0, -1);
12
+ return absolute ? kept : [...kept, ".."];
13
+ }, []);
14
+
15
+ export const normalize = (path: string): string => {
16
+ if (path === "") return ".";
17
+ const absolute = path.startsWith("/");
18
+ const body = normalizedParts(path.split("/"), absolute).join("/");
19
+ const trailing = path.endsWith("/") ? "/" : "";
20
+ if (absolute) return body === "" ? "/" : `/${body}${trailing}`;
21
+ return `${body === "" ? "." : body}${trailing}`;
22
+ };
23
+
24
+ export const join = (...parts: readonly string[]): string => {
25
+ const joined = parts.filter((part) => part !== "").join("/");
26
+ return joined === "" ? "." : normalize(joined);
27
+ };
28
+
29
+ export const isAbsolute = (path: string): boolean => path.startsWith("/");
30
+
31
+ export const resolve = (...parts: readonly string[]): string => {
32
+ const from = parts.reduceRight((acc, part) => {
33
+ if (acc.startsWith("/") || part === "") return acc;
34
+ return acc === "" ? part : `${part}/${acc}`;
35
+ }, "");
36
+ const resolved = normalize(from.startsWith("/") ? from : `/${from}`);
37
+ return resolved.length > 1 && resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
38
+ };
39
+
40
+ const segmentsOf = (path: string): string[] =>
41
+ resolve(path)
42
+ .split("/")
43
+ .filter((part) => part !== "");
44
+
45
+ export const relative = (from: string, to: string): string => {
46
+ const fromParts = segmentsOf(from);
47
+ const toParts = segmentsOf(to);
48
+ const differs = fromParts.findIndex((part, index) => toParts[index] !== part);
49
+ const shared = differs === -1 ? Math.min(fromParts.length, toParts.length) : differs;
50
+ return [...fromParts.slice(shared).map(() => ".."), ...toParts.slice(shared)].join("/");
51
+ };
52
+
53
+ /** The index of the last character that is not a slash, or -1. */
54
+ const lastNonSlash = (path: string): number => {
55
+ let at = path.length - 1;
56
+ while (at >= 0 && path[at] === "/") at -= 1;
57
+ return at;
58
+ };
59
+
60
+ export const dirname = (path: string): string => {
61
+ if (path === "") return ".";
62
+ const last = lastNonSlash(path);
63
+ const end = last <= 0 ? -1 : path.lastIndexOf("/", last);
64
+ if (end < 1) return path.startsWith("/") ? "/" : ".";
65
+ return end === 1 && path.startsWith("/") ? "//" : path.slice(0, end);
66
+ };
67
+
68
+ export const extname = (path: string): string => {
69
+ const end = lastNonSlash(path) + 1;
70
+ const name = path.slice(path.lastIndexOf("/", end - 1) + 1, end);
71
+ const dot = name.lastIndexOf(".");
72
+ return dot <= 0 || name === ".." ? "" : name.slice(dot);
73
+ };
@@ -0,0 +1,3 @@
1
+ // config/working-dir.ts in a build without a process: the root of the in-memory files.
2
+
3
+ export const workingDir = (): string => "/";
@@ -0,0 +1,87 @@
1
+ // The files chaff and its language packages ship (rules, genres, lexicons), held in memory for a build without a file
2
+ // system. Every package reads its own through `packageFiles`, with the same paths the file system would give: one store,
3
+ // imported by name (chaffjs/browser-files) so the browser entry and each package's reads meet in the same module.
4
+
5
+ /** One package's files: path in the package (rules/max-sentence-length.yaml) → text. */
6
+ export type PackageFiles = Readonly<Record<string, string>>;
7
+
8
+ /** Where the files come from, and where kuromoji's dictionary is served (a URL ending in /). */
9
+ export type BrowserSetup = {
10
+ readonly files: (packageName: string) => Promise<PackageFiles>;
11
+ readonly kuromojiDictionaryUrl: string;
12
+ };
13
+
14
+ type Store = { setup: BrowserSetup | undefined; readonly installed: Map<string, PackageFiles> };
15
+
16
+ const store: Store = { setup: undefined, installed: new Map() };
17
+
18
+ /** Each dictionary file is fetched at new URL(file name, kuromojiDictionaryUrl), so that URL must be absolute and end in /. */
19
+ export const setupBrowserFiles = (setup: BrowserSetup): void => {
20
+ if (!URL.canParse(setup.kuromojiDictionaryUrl) || !setup.kuromojiDictionaryUrl.endsWith("/"))
21
+ throw new Error(`chaff: kuromojiDictionaryUrl must be an absolute URL ending in /: ${setup.kuromojiDictionaryUrl}`);
22
+ store.setup = setup;
23
+ };
24
+
25
+ const setupOf = (): BrowserSetup => {
26
+ if (store.setup === undefined) throw new Error("chaff: call setupBrowser() before check()");
27
+ return store.setup;
28
+ };
29
+
30
+ /** Asks for a package's files the first time they are needed, and keeps them. */
31
+ export const ensurePackageFiles = async (packageName: string): Promise<void> => {
32
+ if (store.installed.has(packageName)) return;
33
+ const files = await setupOf().files(packageName);
34
+ store.installed.set(packageName, files);
35
+ };
36
+
37
+ export const kuromojiDictionaryUrl = (): string => setupOf().kuromojiDictionaryUrl;
38
+
39
+ /** A package's files as a read-only file system rooted at /<package name>. */
40
+ export type PackageFileReader = {
41
+ readonly root: string;
42
+ readonly readText: (path: string) => string;
43
+ readonly readDir: (path: string) => string[];
44
+ readonly exists: (path: string) => boolean;
45
+ };
46
+
47
+ const filesOf = (packageName: string): PackageFiles => {
48
+ const files = store.installed.get(packageName);
49
+ if (files === undefined) throw new Error(`chaff: the files of ${packageName} are not loaded`);
50
+ return files;
51
+ };
52
+
53
+ const inPackage = (root: string, path: string): string | undefined => (path.startsWith(`${root}/`) ? path.slice(root.length + 1) : undefined);
54
+
55
+ const childrenOf = (files: PackageFiles, dir: string): string[] =>
56
+ Object.keys(files).flatMap((key) => {
57
+ if (!key.startsWith(`${dir}/`)) return [];
58
+ const name = key.slice(dir.length + 1);
59
+ return name.includes("/") ? [] : [name];
60
+ });
61
+
62
+ const noSuchFile = (path: string): Error => Object.assign(new Error(`ENOENT: no such file or directory, '${path}'`), { code: "ENOENT" });
63
+
64
+ export const packageFiles = (packageName: string): PackageFileReader => {
65
+ const root = `/${packageName}`;
66
+ return {
67
+ root,
68
+ readText: (path) => {
69
+ const key = inPackage(root, path);
70
+ const text = key === undefined ? undefined : filesOf(packageName)[key];
71
+ if (text === undefined) throw noSuchFile(path);
72
+ return text;
73
+ },
74
+ readDir: (path) => {
75
+ const key = inPackage(root, path);
76
+ const names = key === undefined ? [] : childrenOf(filesOf(packageName), key);
77
+ if (names.length === 0) throw noSuchFile(path);
78
+ return names;
79
+ },
80
+ exists: (path) => {
81
+ const key = inPackage(root, path);
82
+ if (key === undefined) return false;
83
+ const files = filesOf(packageName);
84
+ return Object.hasOwn(files, key) || childrenOf(files, key).length > 0;
85
+ },
86
+ };
87
+ };
package/src/browser.ts ADDED
@@ -0,0 +1,19 @@
1
+ // `chaffjs/browser`: chaff in a web page, with no file system and no server. Each package's files (rules, lexicons)
2
+ // come from setupBrowser's `files`, asked for only when needed: chaff's own at the first check, a language's at the
3
+ // first text in it. Nothing the page checks is sent anywhere. Built for a bundler that reads package.json's "browser"
4
+ // field and import.meta.glob (Vite).
5
+ import { ensurePackageFiles, setupBrowserFiles, type BrowserSetup } from "chaffjs/browser-files";
6
+ import type { BrowserCheck, BrowserCheckOptions } from "./browser/check-text.ts";
7
+
8
+ export type { BrowserSetup, PackageFiles } from "chaffjs/browser-files";
9
+ export type { BrowserAiScore, BrowserCheck, BrowserCheckOptions, BrowserFinding } from "./browser/check-text.ts";
10
+ export type { NotRunEntry } from "./grade/result.ts";
11
+
12
+ export const setupBrowser = (setup: BrowserSetup): void => setupBrowserFiles(setup);
13
+
14
+ /** Checks one text as `chaff <file>` does, and returns its findings and the rules that did not run, with why. */
15
+ export const check = async (text: string, options: BrowserCheckOptions = {}): Promise<BrowserCheck> => {
16
+ await ensurePackageFiles("chaffjs");
17
+ const { checkText } = await import("./browser/check-text.ts");
18
+ return checkText(text, options);
19
+ };
package/src/cli-text.ts CHANGED
@@ -42,6 +42,7 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
42
42
  chaff compare <前> <後> 書き換えで事実(数・日付・URL・コード・名前・引用…)が落ちても足されてもいないかを確かめる
43
43
  chaff facts <file> compare が照合する事実を、書き直す前の控えとして一覧にする
44
44
  chaff outline <file> [<後>] 見出しの構成と形(見出しの数・節の平均の長さ・箇条書きの割合・太字)を測る。2 つなら前と後を並べる
45
+ chaff ai-score <file>... [--format json] AI らしさの簡易判定(低・中・高)。生成文に多い目印を、同じジャンルの人の文書と比べる(書いたのが AI かは判定しない)
45
46
  chaff grade <items.jsonl> [--out <results.jsonl>] [--json]
46
47
  AI の出力を JSONL でまとめて採点する(指摘の率・事実・引用・合否。何も送らない)
47
48
  chaff fix-plan <file> [--json] 指摘をルールごとにまとめ、直す方向と確かめのコマンドを、書き直す人や AI 向けの計画にする(何も送らない)
@@ -88,6 +89,7 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
88
89
  chaff compare <before> <after> check that a rewrite dropped no fact and added none (numbers, dates, URLs, code, names, quotations…)
89
90
  chaff facts <file> list the facts compare checks, as an inventory to keep before a rewrite
90
91
  chaff outline <file> [<after>] measure the outline and its shape (headings, average section length, text in lists, bold); two files side by side
92
+ chaff ai-score <file>... [--format json] quick AI-likeness score (low, medium, high): signs common in generated text against human documents of the genre (not a verdict on who wrote it)
91
93
  chaff grade <items.jsonl> [--out <results.jsonl>] [--json]
92
94
  grade a JSONL file of model outputs (finding rates, facts, quotations, pass or fail; sends nothing)
93
95
  chaff fix-plan <file> [--json] a plan for whoever rewrites the file, a person or an AI: findings by rule, how to rewrite each, the checks to run after (sends nothing)
package/src/cli.ts CHANGED
@@ -41,6 +41,7 @@ import { settingWarnings } from "./config/warnings.ts";
41
41
  import { readConfigIn } from "./config/read.ts";
42
42
  import { settingSourcesOf } from "./config/option-problems.ts";
43
43
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
44
+ import { aiScoreLineOf } from "./ai-score/lint-line.ts";
44
45
 
45
46
  import type { Finding, Level, RuleDefinition } from "./plugin.ts";
46
47
  import { CLI_TEXT, type CliText, type GenreSource } from "./cli-text.ts";
@@ -110,7 +111,9 @@ const present = (check: SourceCheck, argv: readonly string[]): Inspected => {
110
111
  const result = { ...raw, findings: split.fresh };
111
112
  const { header, notes } = fileHeader(path, source, language, { genre, from, unread }, { shelved: split.shelved, hushed: applied.suppressed.length });
112
113
  const notRun = notRunAmong(applied.named, raw.skipped);
113
- const text = argv.includes("--compact") ? renderCompact(header, result, rules, language) : renderFriendly(header, result, rules, language, notes);
114
+ const text = argv.includes("--compact")
115
+ ? renderCompact(header, result, rules, language)
116
+ : [renderFriendly(header, result, rules, language, notes), aiScoreLineOf(check)].join("\n");
114
117
  return {
115
118
  text,
116
119
  rules,
@@ -0,0 +1,77 @@
1
+ import { rulesOf } from "../custom/load.ts";
2
+ import { resolveGenre } from "../resolve-genre.ts";
3
+ import { aiScoreOfDocument, genreGroupOf } from "../ai-score/of-document.ts";
4
+ import { aiScoreJson, renderAiScoreCompact, renderAiScoreFriendly } from "../ai-score/render.ts";
5
+ import type { AiScore } from "../ai-score/score.ts";
6
+ import { AI_SCORE_TEXT } from "../ai-score/text.ts";
7
+ import { scoreViewOf } from "../ai-score/view.ts";
8
+ import type { LengthUnit } from "../plugin.ts";
9
+ import { documentFromSource } from "./read-document.ts";
10
+ import { readSource, treeLanguage, type TreeContext } from "./tree.ts";
11
+
12
+ /** Options whose value is the next argument. That value is not a file. */
13
+ const VALUED: ReadonlySet<string> = new Set(["--genre", "--format", "--language"]);
14
+
15
+ const FORMATS: ReadonlySet<string> = new Set(["text", "json"]);
16
+
17
+ export const aiScoreTargets = (argv: readonly string[]): string[] =>
18
+ argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
19
+
20
+ type Scored = { readonly path: string; readonly language: string; readonly genre: string; readonly unit: LengthUnit; readonly score: AiScore };
21
+
22
+ /** One file read as lint reads it (part-of-speech tags on, as the human shares were measured), and scored. */
23
+ const scoreFile = async (path: string, argv: readonly string[], context: TreeContext): Promise<Scored | undefined> => {
24
+ const source = await readSource(path, context);
25
+ if (source === undefined) return undefined;
26
+ const language = treeLanguage(path, source, argv, context);
27
+ const genre = resolveGenre(path, source, context.config, context.flag(argv, "--genre")).genre;
28
+ const { doc } = await documentFromSource(path, source, { language, genre }, context.config, true);
29
+ return { path, language, genre, unit: doc.lengthUnit, score: aiScoreOfDocument(doc, rulesOf(language, context.config), genre) };
30
+ };
31
+
32
+ const scoreAll = async (paths: readonly string[], argv: readonly string[], context: TreeContext): Promise<Scored[] | undefined> =>
33
+ paths.reduce<Promise<Scored[] | undefined>>(async (previous, path) => {
34
+ const earlier = await previous;
35
+ if (earlier === undefined) return undefined;
36
+ const next = await scoreFile(path, argv, context);
37
+ return next === undefined ? undefined : [...earlier, next];
38
+ }, Promise.resolve([]));
39
+
40
+ const friendly = (scored: Scored, context: TreeContext): string[] =>
41
+ renderAiScoreFriendly(
42
+ scored.path,
43
+ scored.score,
44
+ scoreViewOf(scored.language, scored.unit, genreGroupOf(scored.genre), rulesOf(scored.language, context.config)),
45
+ );
46
+
47
+ const render = (all: readonly Scored[], format: string, argv: readonly string[], context: TreeContext): string => {
48
+ if (format === "json")
49
+ return JSON.stringify(
50
+ all.map((scored) => ({ path: scored.path, language: scored.language, genre: scored.genre, ...aiScoreJson(scored.score) })),
51
+ null,
52
+ 2,
53
+ );
54
+ if (argv.includes("--compact")) return all.map((scored) => renderAiScoreCompact(scored.path, scored.score)).join("\n");
55
+ return all.flatMap((scored, index) => [...(index > 0 ? [""] : []), ...friendly(scored, context)]).join("\n");
56
+ };
57
+
58
+ /**
59
+ * The AI-likeness quick score of each file: how many signs of generated text it shows that human documents of its genre
60
+ * rarely do, as low, medium or high. Measures; never judges who wrote it, so it ends with 0 once the files are read.
61
+ */
62
+ export const runAiScore = async (targets: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
63
+ const text = AI_SCORE_TEXT[context.ui ?? "ja"];
64
+ const format = context.flag(argv, "--format") ?? (argv.includes("--json") ? "json" : "text");
65
+ if (!FORMATS.has(format)) {
66
+ console.error(text.unknownFormat(format));
67
+ return 1;
68
+ }
69
+ if (targets.length === 0) {
70
+ console.error(text.usage);
71
+ return 1;
72
+ }
73
+ const all = await scoreAll(targets, argv, context);
74
+ if (all === undefined) return 1;
75
+ console.log(render(all, format, argv, context));
76
+ return 0;
77
+ };
@@ -1,3 +1,4 @@
1
+ import { aiScoreTargets, runAiScore } from "./ai-score.ts";
1
2
  import { citeTargets, runCite } from "./cite.ts";
2
3
  import { compareTargets, runCompare } from "./compare.ts";
3
4
  import { factsTargets, runFacts } from "./facts.ts";
@@ -16,4 +17,5 @@ export const DOCUMENT_COMMANDS: Readonly<Record<string, DocumentCommand>> = {
16
17
  compare: (argv, context) => runCompare(compareTargets(argv), argv, context),
17
18
  facts: (argv, context) => runFacts(factsTargets(argv), argv, context),
18
19
  outline: (argv, context) => runOutline(outlineTargets(argv), argv, context),
20
+ "ai-score": (argv, context) => runAiScore(aiScoreTargets(argv), argv, context),
19
21
  };
@@ -0,0 +1,17 @@
1
+ import type { Config } from "./load.ts";
2
+ import type { Texts, UiLanguage } from "../ui.ts";
3
+
4
+ const TEXT: Texts<{ readonly unreadable: (where: string, value: string) => string }> = {
5
+ ja: {
6
+ unreadable: (where, value) =>
7
+ `${where}: by_path の ${value} は読めないので使いません。by_path は files と、genre・language・profile の組を並べます(- files: ["guides/**/*.md"] と genre: docs/manual)`,
8
+ },
9
+ en: {
10
+ unreadable: (where, value) =>
11
+ `${where}: cannot read ${value} under by_path, so it is not used. by_path is a list of files with genre, language or profile (- files: ["guides/**/*.md"] with genre: docs/manual)`,
12
+ },
13
+ };
14
+
15
+ /** chaff.yaml の by_path のうち、読めなかったもの。 */
16
+ export const byPathProblems = (config: Pick<Config, "unreadableByPath" | "path">, ui: UiLanguage = "ja"): string[] =>
17
+ (config.unreadableByPath ?? []).map((value) => TEXT[ui].unreadable(config.path ?? "chaff.yaml", value));