chaffjs 0.15.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (427) hide show
  1. package/README.md +27 -2
  2. package/dist/address-chain.js +1 -1
  3. package/dist/address-chain.js.map +1 -1
  4. package/dist/backends/anthropic.d.ts +2 -1
  5. package/dist/backends/anthropic.d.ts.map +1 -1
  6. package/dist/backends/anthropic.js +4 -1
  7. package/dist/backends/anthropic.js.map +1 -1
  8. package/dist/backends/openai.d.ts +2 -1
  9. package/dist/backends/openai.d.ts.map +1 -1
  10. package/dist/backends/openai.js +1 -1
  11. package/dist/backends/openai.js.map +1 -1
  12. package/dist/backends/types.d.ts +2 -1
  13. package/dist/backends/types.d.ts.map +1 -1
  14. package/dist/backends/types.js +1 -1
  15. package/dist/backends/types.js.map +1 -1
  16. package/dist/baseline.js +1 -1
  17. package/dist/baseline.js.map +1 -1
  18. package/dist/calendar-number.d.ts +26 -0
  19. package/dist/calendar-number.d.ts.map +1 -0
  20. package/dist/calendar-number.js +51 -0
  21. package/dist/calendar-number.js.map +1 -0
  22. package/dist/cli-args.d.ts +0 -5
  23. package/dist/cli-args.d.ts.map +1 -1
  24. package/dist/cli-args.js +2 -2
  25. package/dist/cli-args.js.map +1 -1
  26. package/dist/cli-text.d.ts +6 -2
  27. package/dist/cli-text.d.ts.map +1 -1
  28. package/dist/cli-text.js +19 -12
  29. package/dist/cli-text.js.map +1 -1
  30. package/dist/cli.d.ts.map +1 -1
  31. package/dist/cli.js +43 -37
  32. package/dist/cli.js.map +1 -1
  33. package/dist/commands/cite.d.ts.map +1 -1
  34. package/dist/commands/cite.js +4 -4
  35. package/dist/commands/cite.js.map +1 -1
  36. package/dist/commands/eval.js +1 -1
  37. package/dist/commands/eval.js.map +1 -1
  38. package/dist/commands/init-ask.d.ts +19 -0
  39. package/dist/commands/init-ask.d.ts.map +1 -0
  40. package/dist/commands/init-ask.js +43 -0
  41. package/dist/commands/init-ask.js.map +1 -0
  42. package/dist/commands/test.d.ts +8 -0
  43. package/dist/commands/test.d.ts.map +1 -1
  44. package/dist/commands/test.js +70 -38
  45. package/dist/commands/test.js.map +1 -1
  46. package/dist/commands/tree.d.ts +1 -8
  47. package/dist/commands/tree.d.ts.map +1 -1
  48. package/dist/commands/tree.js +7 -6
  49. package/dist/commands/tree.js.map +1 -1
  50. package/dist/config/by-path.d.ts.map +1 -1
  51. package/dist/config/by-path.js +10 -9
  52. package/dist/config/by-path.js.map +1 -1
  53. package/dist/config/load.d.ts +4 -0
  54. package/dist/config/load.d.ts.map +1 -1
  55. package/dist/config/load.js +19 -0
  56. package/dist/config/load.js.map +1 -1
  57. package/dist/config/name-problems.d.ts +5 -0
  58. package/dist/config/name-problems.d.ts.map +1 -0
  59. package/dist/config/name-problems.js +11 -0
  60. package/dist/config/name-problems.js.map +1 -0
  61. package/dist/date-stamp.js +1 -1
  62. package/dist/date-stamp.js.map +1 -1
  63. package/dist/detectors/char-shape.d.ts.map +1 -1
  64. package/dist/detectors/char-shape.js +16 -6
  65. package/dist/detectors/char-shape.js.map +1 -1
  66. package/dist/detectors/en-shape.d.ts.map +1 -1
  67. package/dist/detectors/en-shape.js +101 -7
  68. package/dist/detectors/en-shape.js.map +1 -1
  69. package/dist/detectors/enumerated-runs.d.ts.map +1 -1
  70. package/dist/detectors/enumerated-runs.js +4 -2
  71. package/dist/detectors/enumerated-runs.js.map +1 -1
  72. package/dist/detectors/gram-place.js +1 -1
  73. package/dist/detectors/gram-place.js.map +1 -1
  74. package/dist/detectors/heading-case.d.ts.map +1 -1
  75. package/dist/detectors/heading-case.js +3 -1
  76. package/dist/detectors/heading-case.js.map +1 -1
  77. package/dist/detectors/index.d.ts.map +1 -1
  78. package/dist/detectors/index.js +4 -3
  79. package/dist/detectors/index.js.map +1 -1
  80. package/dist/detectors/lettered-index.d.ts +9 -0
  81. package/dist/detectors/lettered-index.d.ts.map +1 -0
  82. package/dist/detectors/lettered-index.js +54 -0
  83. package/dist/detectors/lettered-index.js.map +1 -0
  84. package/dist/detectors/lexicon.d.ts +5 -1
  85. package/dist/detectors/lexicon.d.ts.map +1 -1
  86. package/dist/detectors/lexicon.js +46 -3
  87. package/dist/detectors/lexicon.js.map +1 -1
  88. package/dist/detectors/orthography.d.ts.map +1 -1
  89. package/dist/detectors/orthography.js +24 -6
  90. package/dist/detectors/orthography.js.map +1 -1
  91. package/dist/detectors/phrase-match.d.ts +3 -1
  92. package/dist/detectors/phrase-match.d.ts.map +1 -1
  93. package/dist/detectors/phrase-match.js +20 -15
  94. package/dist/detectors/phrase-match.js.map +1 -1
  95. package/dist/detectors/repeated-head.d.ts.map +1 -1
  96. package/dist/detectors/repeated-head.js +5 -4
  97. package/dist/detectors/repeated-head.js.map +1 -1
  98. package/dist/detectors/signals.d.ts.map +1 -1
  99. package/dist/detectors/signals.js +16 -6
  100. package/dist/detectors/signals.js.map +1 -1
  101. package/dist/detectors/stacked-hedge.d.ts +23 -0
  102. package/dist/detectors/stacked-hedge.d.ts.map +1 -0
  103. package/dist/detectors/stacked-hedge.js +64 -0
  104. package/dist/detectors/stacked-hedge.js.map +1 -0
  105. package/dist/detectors/superlative-amount.d.ts +9 -0
  106. package/dist/detectors/superlative-amount.d.ts.map +1 -0
  107. package/dist/detectors/superlative-amount.js +20 -0
  108. package/dist/detectors/superlative-amount.js.map +1 -0
  109. package/dist/detectors/superlative-clause.d.ts +18 -0
  110. package/dist/detectors/superlative-clause.d.ts.map +1 -0
  111. package/dist/detectors/superlative-clause.js +61 -0
  112. package/dist/detectors/superlative-clause.js.map +1 -0
  113. package/dist/detectors/superlative-scope.d.ts +6 -0
  114. package/dist/detectors/superlative-scope.d.ts.map +1 -1
  115. package/dist/detectors/superlative-scope.js +3 -3
  116. package/dist/detectors/superlative-scope.js.map +1 -1
  117. package/dist/detectors/team.d.ts +0 -6
  118. package/dist/detectors/team.d.ts.map +1 -1
  119. package/dist/detectors/team.js +17 -1
  120. package/dist/detectors/team.js.map +1 -1
  121. package/dist/detectors/token-shape.d.ts.map +1 -1
  122. package/dist/detectors/token-shape.js +8 -6
  123. package/dist/detectors/token-shape.js.map +1 -1
  124. package/dist/document.d.ts +3 -0
  125. package/dist/document.d.ts.map +1 -1
  126. package/dist/document.js +18 -3
  127. package/dist/document.js.map +1 -1
  128. package/dist/eval.js +1 -1
  129. package/dist/eval.js.map +1 -1
  130. package/dist/feedback/draft.js +1 -1
  131. package/dist/feedback/draft.js.map +1 -1
  132. package/dist/feedback/excerpt.d.ts +0 -1
  133. package/dist/feedback/excerpt.d.ts.map +1 -1
  134. package/dist/feedback/excerpt.js +2 -2
  135. package/dist/feedback/excerpt.js.map +1 -1
  136. package/dist/file-header.d.ts +20 -0
  137. package/dist/file-header.d.ts.map +1 -0
  138. package/dist/file-header.js +21 -0
  139. package/dist/file-header.js.map +1 -0
  140. package/dist/files.js +3 -3
  141. package/dist/files.js.map +1 -1
  142. package/dist/genre-check.js +1 -1
  143. package/dist/genre-check.js.map +1 -1
  144. package/dist/genre-load.d.ts +6 -0
  145. package/dist/genre-load.d.ts.map +1 -0
  146. package/dist/genre-load.js +22 -0
  147. package/dist/genre-load.js.map +1 -0
  148. package/dist/genre-parse.d.ts +40 -0
  149. package/dist/genre-parse.d.ts.map +1 -0
  150. package/dist/genre-parse.js +104 -0
  151. package/dist/genre-parse.js.map +1 -0
  152. package/dist/genre-suggest.d.ts +15 -0
  153. package/dist/genre-suggest.d.ts.map +1 -0
  154. package/dist/genre-suggest.js +26 -0
  155. package/dist/genre-suggest.js.map +1 -0
  156. package/dist/genre.d.ts +1 -1
  157. package/dist/genre.d.ts.map +1 -1
  158. package/dist/genre.js +3 -13
  159. package/dist/genre.js.map +1 -1
  160. package/dist/init-choice.d.ts +12 -0
  161. package/dist/init-choice.d.ts.map +1 -0
  162. package/dist/init-choice.js +41 -0
  163. package/dist/init-choice.js.map +1 -0
  164. package/dist/init.d.ts.map +1 -1
  165. package/dist/init.js +16 -5
  166. package/dist/init.js.map +1 -1
  167. package/dist/judge.d.ts +2 -1
  168. package/dist/judge.d.ts.map +1 -1
  169. package/dist/judge.js +1 -1
  170. package/dist/judge.js.map +1 -1
  171. package/dist/line-paragraphs.d.ts +4 -0
  172. package/dist/line-paragraphs.d.ts.map +1 -0
  173. package/dist/line-paragraphs.js +69 -0
  174. package/dist/line-paragraphs.js.map +1 -0
  175. package/dist/mask.d.ts.map +1 -1
  176. package/dist/mask.js +7 -6
  177. package/dist/mask.js.map +1 -1
  178. package/dist/plugin.d.ts +6 -0
  179. package/dist/plugin.d.ts.map +1 -1
  180. package/dist/profile/check.d.ts +28 -0
  181. package/dist/profile/check.d.ts.map +1 -0
  182. package/dist/profile/check.js +12 -0
  183. package/dist/profile/check.js.map +1 -0
  184. package/dist/profile/for-file.d.ts +2 -2
  185. package/dist/profile/for-file.d.ts.map +1 -1
  186. package/dist/profile/for-file.js +9 -2
  187. package/dist/profile/for-file.js.map +1 -1
  188. package/dist/profile/load.js +1 -1
  189. package/dist/profile/load.js.map +1 -1
  190. package/dist/profile/select.d.ts +4 -2
  191. package/dist/profile/select.d.ts.map +1 -1
  192. package/dist/profile/select.js +8 -3
  193. package/dist/profile/select.js.map +1 -1
  194. package/dist/quoted-span.d.ts +15 -3
  195. package/dist/quoted-span.d.ts.map +1 -1
  196. package/dist/quoted-span.js +16 -11
  197. package/dist/quoted-span.js.map +1 -1
  198. package/dist/reasons.d.ts +1 -0
  199. package/dist/reasons.d.ts.map +1 -1
  200. package/dist/reasons.js +4 -1
  201. package/dist/reasons.js.map +1 -1
  202. package/dist/render/compact.d.ts.map +1 -1
  203. package/dist/render/compact.js +1 -2
  204. package/dist/render/compact.js.map +1 -1
  205. package/dist/render/eval.d.ts.map +1 -1
  206. package/dist/render/eval.js +4 -3
  207. package/dist/render/eval.js.map +1 -1
  208. package/dist/render/friendly.d.ts +2 -1
  209. package/dist/render/friendly.d.ts.map +1 -1
  210. package/dist/render/friendly.js +14 -6
  211. package/dist/render/friendly.js.map +1 -1
  212. package/dist/render/genres.d.ts +5 -0
  213. package/dist/render/genres.d.ts.map +1 -0
  214. package/dist/render/genres.js +28 -0
  215. package/dist/render/genres.js.map +1 -0
  216. package/dist/render/plural.d.ts +3 -0
  217. package/dist/render/plural.d.ts.map +1 -0
  218. package/dist/render/plural.js +5 -0
  219. package/dist/render/plural.js.map +1 -0
  220. package/dist/render/rules-json.d.ts.map +1 -1
  221. package/dist/render/rules-json.js +9 -3
  222. package/dist/render/rules-json.js.map +1 -1
  223. package/dist/render/semantic.d.ts +3 -2
  224. package/dist/render/semantic.d.ts.map +1 -1
  225. package/dist/render/semantic.js +94 -34
  226. package/dist/render/semantic.js.map +1 -1
  227. package/dist/render/summary.d.ts.map +1 -1
  228. package/dist/render/summary.js +4 -3
  229. package/dist/render/summary.js.map +1 -1
  230. package/dist/render/suppressions.d.ts.map +1 -1
  231. package/dist/render/suppressions.js +3 -2
  232. package/dist/render/suppressions.js.map +1 -1
  233. package/dist/render/text.d.ts +2 -0
  234. package/dist/render/text.d.ts.map +1 -1
  235. package/dist/render/text.js +11 -3
  236. package/dist/render/text.js.map +1 -1
  237. package/dist/rule-load.d.ts.map +1 -1
  238. package/dist/rule-load.js +4 -1
  239. package/dist/rule-load.js.map +1 -1
  240. package/dist/run-semantic.js +1 -1
  241. package/dist/run-semantic.js.map +1 -1
  242. package/dist/run.d.ts +2 -0
  243. package/dist/run.d.ts.map +1 -1
  244. package/dist/run.js +25 -15
  245. package/dist/run.js.map +1 -1
  246. package/dist/sentence-shape.d.ts +0 -1
  247. package/dist/sentence-shape.d.ts.map +1 -1
  248. package/dist/sentence-shape.js +3 -5
  249. package/dist/sentence-shape.js.map +1 -1
  250. package/dist/setting-problems.d.ts +5 -0
  251. package/dist/setting-problems.d.ts.map +1 -0
  252. package/dist/setting-problems.js +15 -0
  253. package/dist/setting-problems.js.map +1 -0
  254. package/dist/structure/build.js +5 -5
  255. package/dist/structure/build.js.map +1 -1
  256. package/dist/structure/cite.js +1 -1
  257. package/dist/structure/cite.js.map +1 -1
  258. package/dist/structure/issues.d.ts.map +1 -1
  259. package/dist/structure/issues.js +5 -5
  260. package/dist/structure/issues.js.map +1 -1
  261. package/dist/structure/markdown-path.d.ts.map +1 -1
  262. package/dist/structure/markdown-path.js +2 -2
  263. package/dist/structure/markdown-path.js.map +1 -1
  264. package/dist/structure/numbering.d.ts.map +1 -1
  265. package/dist/structure/numbering.js +1 -1
  266. package/dist/structure/numbering.js.map +1 -1
  267. package/dist/structure/relative-find.js +1 -1
  268. package/dist/structure/relative-find.js.map +1 -1
  269. package/dist/structure/sexp.js +1 -1
  270. package/dist/structure/sexp.js.map +1 -1
  271. package/dist/structure/unnumbered.js +2 -2
  272. package/dist/structure/unnumbered.js.map +1 -1
  273. package/dist/team-names.d.ts +9 -0
  274. package/dist/team-names.d.ts.map +1 -0
  275. package/dist/team-names.js +34 -0
  276. package/dist/team-names.js.map +1 -0
  277. package/dist/ui.d.ts +2 -0
  278. package/dist/ui.d.ts.map +1 -1
  279. package/dist/ui.js +6 -0
  280. package/dist/ui.js.map +1 -1
  281. package/dist/version.js +1 -1
  282. package/dist/version.js.map +1 -1
  283. package/dist/watch.d.ts +2 -2
  284. package/dist/watch.d.ts.map +1 -1
  285. package/dist/watch.js +5 -5
  286. package/dist/watch.js.map +1 -1
  287. package/genres.yaml +363 -0
  288. package/package.json +4 -3
  289. package/rules/adverb-overuse.yaml +2 -2
  290. package/rules/agentless-passive.yaml +1 -1
  291. package/rules/ai-generated-composite.yaml +2 -2
  292. package/rules/ai-tell.yaml +2 -2
  293. package/rules/bold-density.yaml +2 -2
  294. package/rules/closing-cliche.yaml +1 -1
  295. package/rules/concrete-evidence-density.yaml +2 -2
  296. package/rules/contraction-consistency.yaml +1 -1
  297. package/rules/cushion-phrase-density.yaml +1 -1
  298. package/rules/dangling-reference.yaml +1 -1
  299. package/rules/date-order.yaml +1 -1
  300. package/rules/date-weekday-mismatch.yaml +1 -1
  301. package/rules/double-keigo.yaml +1 -1
  302. package/rules/doubled-word.yaml +1 -1
  303. package/rules/duplicate-definition.yaml +1 -1
  304. package/rules/emoji-density.yaml +1 -1
  305. package/rules/empty-conclusion.yaml +2 -1
  306. package/rules/empty-intensifier.yaml +1 -1
  307. package/rules/excessive-hedging.yaml +16 -6
  308. package/rules/expletive-construction.yaml +2 -2
  309. package/rules/heading-echo.yaml +1 -1
  310. package/rules/hiragana-fukushi.yaml +1 -1
  311. package/rules/internal-jargon.yaml +2 -2
  312. package/rules/latin-spacing.yaml +5 -4
  313. package/rules/max-kanji-continuous.yaml +4 -3
  314. package/rules/max-paragraph-length.yaml +9 -2
  315. package/rules/max-sentence-length.yaml +14 -2
  316. package/rules/ngram-repetition.yaml +4 -4
  317. package/rules/no-doubled-joshi.yaml +2 -2
  318. package/rules/no-em-dash.yaml +2 -2
  319. package/rules/no-mixed-desumasu.yaml +1 -1
  320. package/rules/no-nakaguro-parallel.yaml +2 -2
  321. package/rules/numbering-gap.yaml +1 -1
  322. package/rules/oxford-comma-consistency.yaml +2 -1
  323. package/rules/padded-intro.yaml +2 -1
  324. package/rules/paragraph-length-variance.yaml +1 -1
  325. package/rules/preamble-length.yaml +2 -2
  326. package/rules/preferred-term.yaml +1 -1
  327. package/rules/proper-noun-density.yaml +3 -2
  328. package/rules/repeated-conjunction.yaml +2 -2
  329. package/rules/repeated-sentence-head.yaml +2 -2
  330. package/rules/required-sections.yaml +1 -1
  331. package/rules/rule-of-three.yaml +2 -2
  332. package/rules/sasete-itadaku.yaml +3 -3
  333. package/rules/section-length-uniformity.yaml +1 -1
  334. package/rules/sentence-initial-conjunction-run.yaml +2 -2
  335. package/rules/sentence-rhythm.yaml +1 -1
  336. package/rules/taigen-dome-in-prose.yaml +2 -2
  337. package/rules/title-case-consistency.yaml +1 -1
  338. package/rules/total-mismatch.yaml +1 -1
  339. package/rules/undefined-acronym.yaml +4 -4
  340. package/rules/unqualified-superlative.yaml +2 -2
  341. package/skills/chaff/SKILL.md +28 -9
  342. package/src/address-chain.ts +1 -1
  343. package/src/backends/anthropic.ts +5 -1
  344. package/src/backends/openai.ts +2 -1
  345. package/src/backends/types.ts +3 -2
  346. package/src/baseline.ts +1 -1
  347. package/src/calendar-number.ts +75 -0
  348. package/src/cli-args.ts +2 -2
  349. package/src/cli-text.ts +29 -15
  350. package/src/cli.ts +42 -41
  351. package/src/commands/cite.ts +4 -4
  352. package/src/commands/eval.ts +1 -1
  353. package/src/commands/init-ask.ts +53 -0
  354. package/src/commands/test.ts +102 -45
  355. package/src/commands/tree.ts +7 -6
  356. package/src/config/by-path.ts +8 -9
  357. package/src/config/load.ts +24 -0
  358. package/src/config/name-problems.ts +15 -0
  359. package/src/date-stamp.ts +1 -1
  360. package/src/detectors/char-shape.ts +21 -9
  361. package/src/detectors/en-shape.ts +106 -8
  362. package/src/detectors/enumerated-runs.ts +3 -2
  363. package/src/detectors/gram-place.ts +1 -1
  364. package/src/detectors/heading-case.ts +4 -1
  365. package/src/detectors/index.ts +4 -3
  366. package/src/detectors/lettered-index.ts +78 -0
  367. package/src/detectors/lexicon.ts +63 -6
  368. package/src/detectors/orthography.ts +31 -7
  369. package/src/detectors/phrase-match.ts +23 -12
  370. package/src/detectors/repeated-head.ts +3 -4
  371. package/src/detectors/signals.ts +15 -6
  372. package/src/detectors/stacked-hedge.ts +93 -0
  373. package/src/detectors/superlative-amount.ts +24 -0
  374. package/src/detectors/superlative-clause.ts +82 -0
  375. package/src/detectors/superlative-scope.ts +3 -3
  376. package/src/detectors/team.ts +18 -2
  377. package/src/detectors/token-shape.ts +8 -10
  378. package/src/document.ts +29 -3
  379. package/src/eval.ts +1 -1
  380. package/src/feedback/draft.ts +1 -1
  381. package/src/feedback/excerpt.ts +2 -2
  382. package/src/file-header.ts +31 -0
  383. package/src/files.ts +3 -3
  384. package/src/genre-check.ts +1 -1
  385. package/src/genre-load.ts +24 -0
  386. package/src/genre-parse.ts +126 -0
  387. package/src/genre-suggest.ts +37 -0
  388. package/src/genre.ts +4 -13
  389. package/src/init-choice.ts +47 -0
  390. package/src/init.ts +20 -6
  391. package/src/judge.ts +3 -1
  392. package/src/line-paragraphs.ts +83 -0
  393. package/src/mask.ts +5 -6
  394. package/src/plugin.ts +6 -0
  395. package/src/profile/check.ts +34 -0
  396. package/src/profile/for-file.ts +10 -3
  397. package/src/profile/load.ts +1 -1
  398. package/src/profile/select.ts +11 -4
  399. package/src/quoted-span.ts +26 -12
  400. package/src/reasons.ts +5 -1
  401. package/src/render/compact.ts +1 -3
  402. package/src/render/eval.ts +4 -3
  403. package/src/render/friendly.ts +21 -6
  404. package/src/render/genres.ts +31 -0
  405. package/src/render/plural.ts +6 -0
  406. package/src/render/rules-json.ts +10 -3
  407. package/src/render/semantic.ts +117 -34
  408. package/src/render/summary.ts +4 -3
  409. package/src/render/suppressions.ts +3 -2
  410. package/src/render/text.ts +18 -3
  411. package/src/rule-load.ts +6 -1
  412. package/src/run-semantic.ts +1 -1
  413. package/src/run.ts +28 -15
  414. package/src/sentence-shape.ts +3 -7
  415. package/src/setting-problems.ts +24 -0
  416. package/src/structure/build.ts +5 -5
  417. package/src/structure/cite.ts +1 -1
  418. package/src/structure/issues.ts +6 -5
  419. package/src/structure/markdown-path.ts +2 -2
  420. package/src/structure/numbering.ts +1 -1
  421. package/src/structure/relative-find.ts +1 -1
  422. package/src/structure/sexp.ts +1 -1
  423. package/src/structure/unnumbered.ts +2 -2
  424. package/src/team-names.ts +39 -0
  425. package/src/ui.ts +7 -0
  426. package/src/version.ts +1 -1
  427. package/src/watch.ts +5 -5
@@ -7,20 +7,80 @@ import { buildDocument, teamRules } from "../document.ts";
7
7
  import { guessLanguage } from "../detect.ts";
8
8
  import { collectTargets } from "../files.ts";
9
9
  import { loadRules } from "../rule-load.ts";
10
+ import { counted } from "../render/plural.ts";
10
11
  import { CHECKS_FILE, loadChecks, type UserCheck } from "../checks.ts";
11
- import { CACHE_DIR, credentialHint, describeFailure, hasCredentials } from "../judge.ts";
12
+ import { CACHE_DIR, credentialHint, describeFailure, hasCredentials, type JudgeOptions } from "../judge.ts";
12
13
  import { ENV_FILE, loadEnvFile } from "../env.ts";
13
14
  import { planSemantic, runSemantic } from "../run-semantic.ts";
14
- import { MACHINE_BANNER, renderPlan, renderSemantic } from "../render/semantic.ts";
15
+ import { machineBanner, renderPlan, renderSemantic } from "../render/semantic.ts";
15
16
  import type { Config } from "../config/load.ts";
16
17
  import type { Finding, RuleDefinition } from "../plugin.ts";
17
- import type { Failure } from "../backends/types.ts";
18
+ import type { BackendName, Failure } from "../backends/types.ts";
18
19
  import { profileFor } from "../profile/for-file.ts";
20
+ import { CLI_TEXT } from "../cli-text.ts";
21
+ import { sharedLanguage, type Texts, type UiLanguage } from "../ui.ts";
22
+
23
+ const TEXT: Texts<{
24
+ readonly total: (total: number) => string;
25
+ readonly sentTo: (backend: string, model: string, found: boolean) => string;
26
+ readonly envRead: (path: string) => string;
27
+ readonly envWhere: (found: string | undefined) => string;
28
+ readonly notRun: (why: string, what: string, where: string) => readonly string[];
29
+ readonly noCredentials: (backend: BackendName) => string;
30
+ readonly refused: (backend: BackendName, status: string) => string;
31
+ readonly checkKey: string;
32
+ readonly quota: (backend: BackendName, status: string) => string;
33
+ readonly returned: (backend: BackendName, status: string) => string;
34
+ readonly noCause: string;
35
+ readonly tally: (machine: number, ai: number) => string;
36
+ readonly untouched: string;
37
+ }> = {
38
+ ja: {
39
+ total: (total) => ` 合計 ${total} 箇所を送ります。--dry-run なので API は呼んでいません。`,
40
+ sentTo: (backend, model, found) => ` 送り先: ${backend} / ${model}(認証${found ? "あり" : "なし"})`,
41
+ envRead: (path) => ` ${path} を読みました。`,
42
+ envWhere: (found) => (found === undefined ? `${ENV_FILE} に書いても読みます(いまは ${ENV_FILE} がありません)` : `${found} は読みました`),
43
+ notRun: (why, what, where) => [` 意味を読む検査は動かしていません。${why}`, ` ${what}`, ` ${where}。`, " 機械による判定はすべて動いています。"],
44
+ noCredentials: (backend) => `${backend} の認証情報がありません。`,
45
+ refused: (backend, status) => `${backend} が鍵を受け付けませんでした(${status})。`,
46
+ checkKey: "鍵が正しいか、その provider のものかを確かめてください。",
47
+ quota: (backend, status) => `${backend} の残高か上限に達しました(${status})。`,
48
+ returned: (backend, status) => `${backend} が ${status} を返しました。`,
49
+ noCause: "原因は返ってきませんでした",
50
+ tally: (machine, ai) => ` 機械 ${machine} 件 / AI ${ai} 件`,
51
+ untouched: " 文章は書き換えていません。直すのは書いた人です。",
52
+ },
53
+ en: {
54
+ total: (total) => ` ${counted(total, "passage")} in all would be sent. With --dry-run the API was not called.`,
55
+ sentTo: (backend, model, found) => ` Sent to: ${backend} / ${model} (${found ? "credentials found" : "no credentials"})`,
56
+ envRead: (path) => ` Read ${path}.`,
57
+ envWhere: (found) => (found === undefined ? `A key written in ${ENV_FILE} is read too (there is no ${ENV_FILE} now)` : `${found} was read`),
58
+ notRun: (why, what, where) => [` The checks that read meaning did not run. ${why}`, ` ${what}`, ` ${where}.`, " Every machine check ran."],
59
+ noCredentials: (backend) => `There are no credentials for ${backend}.`,
60
+ refused: (backend, status) => `${backend} did not accept the key (${status}).`,
61
+ checkKey: "Check that the key is right and that it is for that provider.",
62
+ quota: (backend, status) => `${backend} has run out of balance or hit a limit (${status}).`,
63
+ returned: (backend, status) => `${backend} returned ${status}.`,
64
+ noCause: "No reason was given.",
65
+ tally: (machine, ai) => ` ${machine} by machine / ${ai} by AI`,
66
+ untouched: " The text was not changed. Fixing it is the writer's job.",
67
+ },
68
+ };
69
+
70
+ const DIVIDER = "─".repeat(60);
19
71
 
20
72
  export type TestContext = {
21
73
  readonly config: Config;
22
74
  readonly resolveGenre: (path: string, source: string, config: Config) => { genre: string; from: string };
23
- readonly inspect: (path: string, config: Config, argv: readonly string[]) => Promise<{ text: string; outcome: { findings: readonly Finding[] } }>;
75
+ readonly inspect: (
76
+ path: string,
77
+ config: Config,
78
+ argv: readonly string[],
79
+ ) => Promise<{ text: string; language: string; outcome: { findings: readonly Finding[] } }>;
80
+ /** The language of what is not about the documents: chaff.yaml's language, else the locale. */
81
+ readonly ui: UiLanguage;
82
+ /** The judges to call instead of the providers' own clients. Tests pass stand-ins; the command line never does. */
83
+ readonly clients?: Pick<JudgeOptions, "anthropicClient" | "openaiClient">;
24
84
  };
25
85
 
26
86
  type Judged = { path: string; outcome: Awaited<ReturnType<typeof runSemantic>>; rules: RuleDefinition[]; language: string };
@@ -37,8 +97,8 @@ const judgeAll = async (
37
97
  const source = plainSource(await readFile(path, "utf8"));
38
98
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
39
99
  const adapter = await loadAdapter(language);
40
- const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
41
100
  const { genre } = resolveGenre(path, source, config);
101
+ const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
42
102
  const rules = loadRules(language);
43
103
  return { path, outcome: await runSemantic(doc, rules, checks, config.rules, genre, options), rules, language };
44
104
  }),
@@ -50,43 +110,51 @@ const dryRun = async (
50
110
  config: Config,
51
111
  checks: readonly UserCheck[],
52
112
  resolveGenre: TestContext["resolveGenre"],
53
- envFile: string | undefined,
113
+ closing: { readonly envFile: string | undefined; readonly language: UiLanguage },
54
114
  ): Promise<void> => {
55
115
  const plans = await Promise.all(
56
116
  paths.map(async (path) => {
57
117
  const source = plainSource(await readFile(path, "utf8"));
58
118
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
59
119
  const adapter = await loadAdapter(language);
60
- const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
61
120
  const { genre } = resolveGenre(path, source, config);
62
- return { path, jobs: planSemantic(doc, loadRules(language), checks, config.rules, genre), sentences: doc.sentences.length };
121
+ const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
122
+ return { path, jobs: planSemantic(doc, loadRules(language), checks, config.rules, genre), sentences: doc.sentences.length, language };
63
123
  }),
64
124
  );
65
- plans.forEach(({ path, jobs, sentences }) => console.log(renderPlan(path, jobs, sentences).join("\n")));
125
+ plans.forEach(({ path, jobs, sentences, language }) => console.log(renderPlan(path, jobs, sentences, language).join("\n")));
66
126
  const total = plans.reduce((sum, plan) => sum + plan.jobs.reduce((count, job) => count + job.candidates.length, 0), 0);
67
- const from = envFile === undefined ? "" : ` ${envFile} を読みました。`;
68
- const where = `${config.aiBackend} / ${config.aiModel}(認証${hasCredentials(config.aiBackend) ? "あり" : "なし"})`;
69
- console.log(
70
- [
71
- "",
72
- "─".repeat(60),
73
- "",
74
- ` 合計 ${total} 箇所を送ります。--dry-run なので API は呼んでいません。`,
75
- ` 送り先: ${where}`,
76
- ...(from === "" ? [] : [from]),
77
- "",
78
- ].join("\n"),
79
- );
127
+ const text = TEXT[closing.language];
128
+ const from = closing.envFile === undefined ? [] : [text.envRead(closing.envFile)];
129
+ console.log(["", DIVIDER, "", text.total(total), text.sentTo(config.aiBackend, config.aiModel, hasCredentials(config.aiBackend)), ...from, ""].join("\n"));
130
+ };
131
+
132
+ /**
133
+ * API が返した失敗を人の言葉にする。生のスタックトレースを人に見せない。
134
+ * 鍵はあるのに弾かれたときに「ありません」と言うと、設定済みの鍵をもう一度設定しに行かせる。
135
+ * 残高切れも同じで、「鍵を確かめてください」と言われても鍵は正しい。
136
+ */
137
+ const whyFailed = (failure: Failure, backend: BackendName, language: UiLanguage): { why: string; what: string } => {
138
+ const text = TEXT[language];
139
+ const status = String(failure.status ?? "?");
140
+ if (failure.kind === "auth") return { why: text.refused(backend, status), what: text.checkKey };
141
+ const what = failure.message ?? text.noCause;
142
+ return failure.kind === "quota" ? { why: text.quota(backend, status), what } : { why: text.returned(backend, status), what };
80
143
  };
81
144
 
145
+ /** Why the checks that read meaning did not run, and where a key would be read from. */
146
+ const notRunNotice = (why: string, what: string, envFile: string | undefined, language: UiLanguage): string =>
147
+ ["", ...TEXT[language].notRun(why, what, TEXT[language].envWhere(envFile)), ""].join("\n");
148
+
82
149
  /**
83
150
  * 機械と AI を見出しで分けて出す。読む人が「これは揺れる判定か」を知らないと、
84
151
  * AI の誤検知に振り回される。workflow spec §7、samples/README の軸 2。
152
+ * 見出しと指摘は文書の言語、締めは文書がそろっていればその言語、混ざっていれば ui の言語。
85
153
  */
86
154
  export const runTest = async (targets: readonly string[], argv: readonly string[], context: TestContext): Promise<number> => {
87
155
  const paths = collectTargets(targets.length > 0 ? targets : ["."]);
88
156
  if (paths.length === 0) {
89
- console.error(`Markdown が 1 つも見つかりませんでした: ${targets.join(", ")}`);
157
+ console.error(CLI_TEXT[context.ui].noMarkdown(targets.join(", ")));
90
158
  return 1;
91
159
  }
92
160
  const { config, resolveGenre, inspect } = context;
@@ -95,31 +163,20 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
95
163
  const checks = loadChecks(join(process.cwd(), CHECKS_FILE));
96
164
  const results = await Promise.all(paths.map((path) => inspect(path, config, argv)));
97
165
  results.forEach((result) => {
98
- console.log(result.text.replace(/\n/u, `\n${MACHINE_BANNER.join("\n")}`));
166
+ console.log(result.text.replace(/\n/u, `\n${machineBanner(result.language).join("\n")}`));
99
167
  });
168
+ const closing = sharedLanguage(
169
+ results.map((result) => result.language),
170
+ context.ui,
171
+ );
100
172
  const machineOnly = results.some((result) => result.outcome.findings.some((finding) => finding.severity === "error")) ? 1 : 0;
101
- const where = envFile === undefined ? `${ENV_FILE} に書いても読みます(いまは ${ENV_FILE} がありません)` : `${envFile} は読みました`;
102
- const notRun = (why: string, what: string): string =>
103
- ["", ` 意味を読む検査は動かしていません。${why}`, ` ${what}`, ` ${where}。`, " 機械による判定はすべて動いています。", ""].join("\n");
104
- const noCredentials = notRun(`${config.aiBackend} の認証情報がありません。`, credentialHint(config.aiBackend));
105
- /**
106
- * API が返した失敗を人の言葉にする。生のスタックトレースを人に見せない。
107
- * 鍵はあるのに弾かれたときに「ありません」と言うと、設定済みの鍵をもう一度設定しに行かせる。
108
- * 残高切れも同じで、「鍵を確かめてください」と言われても鍵は正しい。
109
- */
110
- const failed = (failure: Failure): string => {
111
- if (failure.kind === "auth")
112
- return notRun(`${config.aiBackend} が鍵を受け付けませんでした(${failure.status ?? "?"})。`, "鍵が正しいか、その provider のものかを確かめてください。");
113
- if (failure.kind === "quota") return notRun(`${config.aiBackend} の残高か上限に達しました(${failure.status ?? "?"})。`, failure.message);
114
- return notRun(`${config.aiBackend} が ${failure.status ?? "?"} を返しました。`, failure.message);
115
- };
116
173
  if (argv.includes("--dry-run")) {
117
- await dryRun(paths, config, checks, resolveGenre, envFile);
174
+ await dryRun(paths, config, checks, resolveGenre, { envFile, language: closing });
118
175
  return machineOnly;
119
176
  }
120
177
  // 呼んでから落ちるのを待たない。認証が無いときの SDK の例外は型で判別できない。
121
178
  if (!hasCredentials(config.aiBackend)) {
122
- console.log(noCredentials);
179
+ console.log(notRunNotice(TEXT[closing].noCredentials(config.aiBackend), credentialHint(config.aiBackend, closing), envFile, closing));
123
180
  return machineOnly;
124
181
  }
125
182
  const options = {
@@ -127,12 +184,14 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
127
184
  backend: config.aiBackend,
128
185
  cacheDir: join(process.cwd(), CACHE_DIR),
129
186
  confidenceThreshold: config.confidenceThreshold,
187
+ ...context.clients,
130
188
  };
131
189
  const semantic = await judgeAll(paths, config, checks, options, resolveGenre).catch((error: unknown) => {
132
190
  // API が返した失敗だけを受け止める。ネットワーク断やコードの誤りは握りつぶさない。
133
191
  const failure = describeFailure(config.aiBackend, error);
134
192
  if (failure === undefined) throw error;
135
- console.log(failed(failure));
193
+ const { why, what } = whyFailed(failure, config.aiBackend, closing);
194
+ console.log(notRunNotice(why, what, envFile, closing));
136
195
  return undefined;
137
196
  });
138
197
  if (semantic === undefined) return machineOnly;
@@ -142,8 +201,6 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
142
201
  });
143
202
  const machine = results.flatMap((result) => result.outcome.findings);
144
203
  const ai = semantic.flatMap((entry) => entry.outcome.findings);
145
- console.log(
146
- ["", "─".repeat(60), "", ` 機械 ${machine.length} 件 / AI ${ai.length} 件`, " 文章は書き換えていません。直すのは書いた人です。", ""].join("\n"),
147
- );
204
+ console.log(["", DIVIDER, "", TEXT[closing].tally(machine.length, ai.length), TEXT[closing].untouched, ""].join("\n"));
148
205
  return [...machine, ...ai].some((finding) => finding.severity === "error") ? 1 : 0;
149
206
  };
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
3
3
  import { loadAdapter } from "../adapter-load.ts";
4
4
  import { applyByPath } from "../config/by-path.ts";
5
5
  import { profileFor } from "../profile/for-file.ts";
6
+ import { resolveGenre } from "../resolve-genre.ts";
6
7
  import { guessLanguage } from "../detect.ts";
7
8
  import { isMarkdownPath } from "../structure/markdown-path.ts";
8
9
  import { buildStructure } from "../structure/of.ts";
@@ -35,15 +36,15 @@ const TEXT: Texts<{
35
36
  },
36
37
  };
37
38
 
38
- export const treeText = (context: TreeContext): (typeof TEXT)["ja"] => TEXT[context.ui ?? "ja"];
39
+ const treeText = (context: TreeContext): (typeof TEXT)["ja"] => TEXT[context.ui ?? "ja"];
39
40
 
40
- const FORMATS = ["sexp", "json"];
41
+ const FORMATS: ReadonlySet<string> = new Set(["sexp", "json"]);
41
42
 
42
43
  /** 値を取るフラグ。その次の引数は値で、対象のファイルではない。 */
43
- const VALUED = ["--format", "--language"];
44
+ const VALUED: ReadonlySet<string> = new Set(["--format", "--language", "--genre"]);
44
45
 
45
46
  export const treeTargets = (argv: readonly string[]): string[] =>
46
- argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.includes(all[index - 1] ?? ""));
47
+ argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
47
48
 
48
49
  export const readSource = async (path: string, context: TreeContext): Promise<string | undefined> => {
49
50
  try {
@@ -81,7 +82,7 @@ export const readTree = async (path: string, argv: readonly string[], context: T
81
82
  }
82
83
  // 日本語は形態素で数量と日付を読む。解析器が無ければ単位の表で読むので、木は作れる。
83
84
  await adapter.prepare?.({ pos: true });
84
- const profile = profileFor(context.config, path, source, language);
85
+ const profile = profileFor(context.config, path, source, language, resolveGenre(path, source, context.config, context.flag(argv, "--genre")).genre);
85
86
  return { source, tree: buildStructure({ path, source, language, markdown: isMarkdownPath(path), profile }, adapter.structure) };
86
87
  };
87
88
 
@@ -108,7 +109,7 @@ export const inOrder = async (paths: readonly string[], each: (path: string) =>
108
109
  */
109
110
  export const runTree = async (paths: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
110
111
  const format = context.flag(argv, "--format") ?? "sexp";
111
- if (paths.length === 0 || !FORMATS.includes(format)) {
112
+ if (paths.length === 0 || !FORMATS.has(format)) {
112
113
  console.error(treeText(context).usage);
113
114
  return 1;
114
115
  }
@@ -44,12 +44,11 @@ export const matches = (glob: string, base: string, path: string): boolean => to
44
44
  type PathSettings = { genre?: string; language?: string; profile?: string };
45
45
 
46
46
  export const applyByPath = (rules: readonly PathRule[], base: string, path: string): PathSettings =>
47
- rules.reduce<PathSettings>((acc, rule) => {
48
- if (!rule.files.some((glob) => matches(glob, base, path))) return acc;
49
- return {
50
- ...acc,
51
- ...(rule.genre === undefined ? {} : { genre: rule.genre }),
52
- ...(rule.language === undefined ? {} : { language: rule.language }),
53
- ...(rule.profile === undefined ? {} : { profile: rule.profile }),
54
- };
55
- }, {});
47
+ rules
48
+ .filter((rule) => rule.files.some((glob) => matches(glob, base, path)))
49
+ .reduce<PathSettings>((settings, rule) => {
50
+ if (rule.genre !== undefined) settings.genre = rule.genre;
51
+ if (rule.language !== undefined) settings.language = rule.language;
52
+ if (rule.profile !== undefined) settings.profile = rule.profile;
53
+ return settings;
54
+ }, {});
@@ -30,6 +30,10 @@ export type Config = {
30
30
  readonly prefer: Readonly<Record<string, string>>;
31
31
  /** この種類の文書に無いと困る見出し。チームが自分で決める。 */
32
32
  readonly requiredSections: readonly string[];
33
+ /** チームの固有名詞(組織名・製品名)。1 つの名前として読み、漢字の連なりや繰り返しに数えない。 */
34
+ readonly names: readonly string[];
35
+ /** names に書かれていたが名前として読めなかった値。黙って捨てると、並べたつもりの名前が効いていないことに気づけない。 */
36
+ readonly unreadableNames: readonly string[];
33
37
  /** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
34
38
  readonly byPath: readonly PathRule[];
35
39
  readonly baseDir: string;
@@ -54,6 +58,8 @@ export const EMPTY: Config = {
54
58
  jargon: [],
55
59
  prefer: {},
56
60
  requiredSections: [],
61
+ names: [],
62
+ unreadableNames: [],
57
63
  byPath: [],
58
64
  baseDir: process.cwd(),
59
65
  };
@@ -114,6 +120,21 @@ const preferOf = (raw: unknown): Record<string, string> =>
114
120
  )
115
121
  : {};
116
122
 
123
+ const isNameEntry = (value: unknown): value is string | number => typeof value === "string" || typeof value === "number";
124
+
125
+ const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
126
+
127
+ /** 名前は文字列か数(2025 のような名前)の並び。並びでない値と、並びの中の名前でない項目は読めないものとして返す。 */
128
+ const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable: string[] } => {
129
+ if (raw === undefined || raw === null) return { names: [], unreadable: [] };
130
+ if (!Array.isArray(raw)) return { names: [], unreadable: [printed(raw)] };
131
+ const entries: unknown[] = raw;
132
+ return {
133
+ names: wordsOf(entries.filter(isNameEntry)),
134
+ unreadable: entries.filter((entry) => !isNameEntry(entry)).map(printed),
135
+ };
136
+ };
137
+
117
138
  const byPathOf = (raw: unknown): PathRule[] => (Array.isArray(raw) ? raw.map(toPathRule).filter((rule) => rule !== undefined) : []);
118
139
 
119
140
  /** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
@@ -122,6 +143,7 @@ export const loadConfig = (path: string): Config => {
122
143
  if (!isRecord(raw)) return { ...EMPTY, path };
123
144
  const declared: unknown = raw["ai_backend"];
124
145
  const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
146
+ const names = namesOf(raw["names"]);
125
147
  return {
126
148
  genre: str(raw["genre"]),
127
149
  profile: str(raw["profile"]),
@@ -137,6 +159,8 @@ export const loadConfig = (path: string): Config => {
137
159
  jargon: wordsOf(raw["jargon"]),
138
160
  prefer: preferOf(raw["prefer"]),
139
161
  requiredSections: wordsOf(raw["required_sections"]),
162
+ names: names.names,
163
+ unreadableNames: names.unreadable,
140
164
  byPath: byPathOf(raw["by_path"]),
141
165
  baseDir: dirname(path),
142
166
  };
@@ -0,0 +1,15 @@
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) => `${where}: names の ${value} は名前として読めません。names には名前を 1 行に 1 つずつ並べてください(- 個人情報保護委員会)`,
7
+ },
8
+ en: {
9
+ unreadable: (where, value) => `${where}: cannot read ${value} under names as a name. Write names as a list, one name per line (- Bank of England)`,
10
+ },
11
+ };
12
+
13
+ /** chaff.yaml の names のうち、名前として読めなかったもの。読めないまま黙ると、書いた人は名前を並べたつもりで何も変わっていない。 */
14
+ export const nameProblems = (config: Pick<Config, "unreadableNames" | "path">, ui: UiLanguage = "ja"): string[] =>
15
+ config.unreadableNames.map((value) => TEXT[ui].unreadable(config.path ?? "chaff.yaml", value));
package/src/date-stamp.ts CHANGED
@@ -21,7 +21,7 @@ const LABEL_EDGE = /[\s::]/u;
21
21
  const datesOf = (candidate: StampCandidate): Span[] => candidate.dates.filter((date) => date.end > date.start);
22
22
 
23
23
  const withoutDates = (candidate: StampCandidate): string => {
24
- const ordered = datesOf(candidate).sort((left, right) => left.start - right.start);
24
+ const ordered = datesOf(candidate).toSorted((left, right) => left.start - right.start);
25
25
  const gaps = ordered.reduce<{ readonly parts: readonly string[]; readonly from: number }>(
26
26
  (acc, date) => ({ parts: [...acc.parts, candidate.text.slice(acc.from, date.start)], from: Math.max(acc.from, date.end) }),
27
27
  { parts: [], from: 0 },
@@ -6,6 +6,9 @@ import { isAddressRun } from "./place-run.ts";
6
6
  import { isOneName } from "./name-run.ts";
7
7
  import { proseText } from "../measure.ts";
8
8
  import { parallelDotCount } from "./middle-dot.ts";
9
+ import { maskSpans } from "../mask.ts";
10
+ import { quotedSpans } from "../quoted-span.ts";
11
+ import { nameSpans } from "../team-names.ts";
9
12
 
10
13
  /**
11
14
  * 文字の並びを見る検出。漢字の連なりが住所か名前かだけは、形態素解析の固有名詞と数で決める。
@@ -13,8 +16,8 @@ import { parallelDotCount } from "./middle-dot.ts";
13
16
  */
14
17
  const KANJI_RUN = /[一-鿿]+/gu;
15
18
 
16
- /** 漢字の連なりと、その文書全体の座標での範囲。 */
17
- type KanjiRun = { readonly text: string; readonly span: Span | undefined };
19
+ /** 漢字の連なりと、その文書全体の座標での範囲。quoted は鉤括弧の中身がまるごとこの連なりであること。 */
20
+ type KanjiRun = { readonly text: string; readonly span: Span | undefined; readonly quoted: boolean };
18
21
 
19
22
  const SPACE = /\s/gu;
20
23
 
@@ -22,14 +25,22 @@ const SPACE = /\s/gu;
22
25
  * 同じ連なりが文に二度出ても、それぞれの位置で読む。proseText は空白を詰めたり除いたりするだけなので、
23
26
  * 空白でない k 文字目は元の文でも空白でない k 文字目。番地の覆いは長さを保つので、proseText の位置がそのまま使える。
24
27
  */
25
- const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined): KanjiRun[] => {
28
+ /** 番地と、チームが並べた名前を空白で覆う。名前は読み手が 1 語と知っているので、前後は別の連なりになる。 */
29
+ const maskedProse = (prose: string, profile: DocumentProfile | undefined, names: readonly string[]): string =>
30
+ maskSpans(maskAddresses(prose, profile), nameSpans(prose, names));
31
+
32
+ /** 鉤括弧の中身が、ちょうどこの範囲(文の先頭からの位置)か。括弧でくくった漢字だけの語は、書き手が 1 つの名前として示したもの。 */
33
+ const fillsQuote = (sentence: Sentence, start: number, end: number): boolean =>
34
+ quotedSpans(sentence.text).some((span) => span.start === start && span.end === end);
35
+
36
+ const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined, names: readonly string[]): KanjiRun[] => {
26
37
  const prose = proseText(sentence);
27
38
  const printed = compacted(sentence.text, "char").offsets;
28
- return [...maskAddresses(prose, profile).matchAll(KANJI_RUN)].map((match) => {
39
+ return [...maskedProse(prose, profile, names).matchAll(KANJI_RUN)].map((match) => {
29
40
  const before = prose.slice(0, match.index).replace(SPACE, "").length;
30
41
  const [start, last] = [printed[before], printed[before + match[0].length - 1]];
31
- const span = start === undefined || last === undefined ? undefined : { start: sentence.span.start + start, end: sentence.span.start + last + 1 };
32
- return { text: match[0], span };
42
+ if (start === undefined || last === undefined) return { text: match[0], span: undefined, quoted: false };
43
+ return { text: match[0], span: { start: sentence.span.start + start, end: sentence.span.start + last + 1 }, quoted: fillsQuote(sentence, start, last + 1) };
33
44
  });
34
45
  };
35
46
 
@@ -44,14 +55,15 @@ const isPlaceName = (tokens: readonly Token[], span: Span, topUnits: ReadonlySet
44
55
  * 住所か名前かは、助数詞を外した残りで決める(1日日本銀行の「日本銀行」)。品詞が無ければ判定しない。
45
56
  */
46
57
  const measuredRun = (tokens: readonly Token[] | undefined, run: KanjiRun, topUnits: ReadonlySet<string>): string => {
58
+ if (run.quoted) return "";
47
59
  if (tokens === undefined || run.span === undefined) return run.text;
48
60
  const counter = leadingCounter(tokens, run.span.start, run.text);
49
61
  const rest = counter === undefined ? run.span : { start: counter.span.end, end: run.span.end };
50
62
  return isPlaceName(tokens, rest, topUnits) ? "" : run.text.slice(counter?.surface.length ?? 0);
51
63
  };
52
64
 
53
- const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string>): string =>
54
- runsOf(sentence, profile)
65
+ const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string>, names: readonly string[]): string =>
66
+ runsOf(sentence, profile, names)
55
67
  .map((run) => measuredRun(sentence.tokens, run, topUnits))
56
68
  .reduce((longest, run) => (run.length > longest.length ? run : longest), "");
57
69
 
@@ -62,7 +74,7 @@ const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined,
62
74
  export const kanjiRun: Detector = (doc, options): Finding[] => {
63
75
  const topUnits = new Set((doc.lexicons["prefecture-unit"] ?? []).map((entry) => entry.pattern));
64
76
  return doc.sentences
65
- .map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits) }))
77
+ .map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits, doc.names ?? []) }))
66
78
  .filter(({ run }) => run.length > options.limit)
67
79
  .map(({ sentence, run }) => ({
68
80
  rule: "max-kanji-continuous",
@@ -79,11 +79,11 @@ const firstWord = (sentence: Sentence): string =>
79
79
  * 何が主張なのかが分からなくなる。
80
80
  */
81
81
  export const conjunctionRun: Detector = (doc, options): Finding[] => {
82
- const heads = (options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase());
82
+ const heads = new Set((options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase()));
83
83
  const runs = doc.sentences.filter(isClosed).reduce<Sentence[][]>(
84
84
  (acc, sentence) => {
85
85
  const last = acc.at(-1) ?? [];
86
- if (!heads.includes(firstWord(sentence).toLowerCase())) return [...acc.slice(0, -1), last, []];
86
+ if (!heads.has(firstWord(sentence).toLowerCase())) return [...acc.slice(0, -1), last, []];
87
87
  return [...acc.slice(0, -1), [...last, sentence]];
88
88
  },
89
89
  [[]],
@@ -153,7 +153,8 @@ const depthsOf = (tokens: readonly Token[]): number[] =>
153
153
  { depths: [], open: 0 },
154
154
  ).depths;
155
155
 
156
- type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[] };
156
+ /** participleWords は、解析器が分詞と読まないが読点のあとで分詞の句を始める語(meaning)。 */
157
+ type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[]; readonly participleWords: ReadonlySet<string> };
157
158
 
158
159
  /** 冠詞や引用符を飛ばした、項目の頭の品詞。the parser と an exporter と samples を同じ形と見る。 */
159
160
  const NOMINAL = new Set(["NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
@@ -191,6 +192,21 @@ const itemsBefore = (clause: Clause, at: number): Token[][] => {
191
192
  .filter((item) => item.length > 0);
192
193
  };
193
194
 
195
+ /**
196
+ * and / or で始まる項目で、前の並びは閉じている(offering, giving, or receiving, directly or indirectly)。
197
+ * 次の並びはその項目から始まる。項目が動詞で始まれば、その目的語から(Develop, maintain, and track courses, materials and events)。
198
+ */
199
+ const afterClosedList = (items: readonly Token[][]): Token[][] => {
200
+ const closed = items.findLastIndex((item) => LIST_CONJUNCTION.has(openingOf(item)?.surface.toLowerCase() ?? ""));
201
+ const closing = items[closed];
202
+ if (closing === undefined) return [...items];
203
+ const body = closing.slice(closing.findIndex((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase())) + 1);
204
+ const head = body.findIndex(isContent);
205
+ const rest = VERBAL.has(body[head]?.pos ?? "") ? body.slice(head + 1) : body;
206
+ // 項目が残らなければ、後ろは新しい句の頭。導入の句(directly or indirectly,)を外す。
207
+ return rest.some(isContent) ? [rest, ...items.slice(closed + 1)] : [...withoutLead(items.slice(closed + 1))];
208
+ };
209
+
194
210
  /** and / or の後ろの項目。次の読点か節の切れ目まで。 */
195
211
  const itemAfter = (tokens: readonly Token[], at: number): Token[] => {
196
212
  const end = tokens.findIndex((token, index) => index > at && (isComma(token) || CLAUSE_BREAK.has(token.surface)));
@@ -232,7 +248,9 @@ const joinAdjectives = (items: readonly Token[][]): Token[][] =>
232
248
  items.reduce<Token[][]>((joined, item) => {
233
249
  const previous = joined.at(-1);
234
250
  const stacked = previous !== undefined && lastContent(previous)?.pos === "ADJ" && lastContent(item)?.pos !== "ADJ";
235
- return stacked ? [...joined.slice(0, -1), [...previous, ...item]] : [...joined, item];
251
+ if (stacked) joined[joined.length - 1] = [...previous, ...item];
252
+ else joined.push(item);
253
+ return joined;
236
254
  }, []);
237
255
 
238
256
  /**
@@ -255,14 +273,93 @@ const hasVerb = (item: readonly Token[]): boolean => item.some((token) => VERBAL
255
273
 
256
274
  const predicatesAgree = (items: readonly Token[][], shape: string): boolean => !VERBAL.has(shape) || items.every(hasVerb);
257
275
 
276
+ /** -ing 形(VerbForm=Ger)と過去分詞(Part)。filling the gaps と sentenced to prison は並べても並列にならない。 */
277
+ const VERB_FORMS = new Set(["Ger", "Part"]);
278
+
279
+ const verbFormOf = (token: Token): string | undefined => {
280
+ const form = token.features?.["VerbForm"];
281
+ return form !== undefined && VERB_FORMS.has(form) ? form : undefined;
282
+ };
283
+
284
+ /** 項目の頭の分詞とその形。副詞は飛ばす(originally written)。引用符の中の語(‘modelling’)は語の例なので分詞と読まない。 */
285
+ const participleOpening = (item: readonly Token[], words: ReadonlySet<string>): { readonly at: number; readonly form: string } | undefined => {
286
+ const at = item.findIndex((token) => token.pos !== "PUNCT" && token.pos !== "ADV");
287
+ const opening = item[at];
288
+ if (opening === undefined) return undefined;
289
+ const form = words.has(opening.surface.toLowerCase()) ? "Ger" : verbFormOf(opening);
290
+ return form === undefined ? undefined : { at, form };
291
+ };
292
+
293
+ const hasParticiple = (item: readonly Token[], form: string): boolean => item.some((token) => verbFormOf(token) === form);
294
+
295
+ /**
296
+ * 最初より後ろの項目が分詞で始まるとき。最初の項目にも同じ形の分詞があれば分詞の並び
297
+ * (are hiring engineers, buying laptops and renting desks / Tested, reviewed and approved)。後ろの項目が動詞で始まれば述語の並びで、
298
+ * 分詞は解析器の読み違い(the jury, cross-examined the witnesses, presented witnesses, and made)。そうでなければ分詞の句
299
+ * (from Latin, meaning ship or boat / the judge, sitting without a jury, and sentenced)で、並びがあるならその句の中から始まる。
300
+ * 分詞より前の項目と分詞そのものは項目ではない(programs, including the office, the desk and the team)。句の中の項目の形が揃わなければ
301
+ * 句は挿入で、並びは句の後ろだけ(relief, including an injunction, in any court ... and without / including before the date, to a
302
+ * Recipient ... “confidential”, “proprietary”, or the like)。
303
+ */
304
+ /** 解析器は過去形と過去分詞(fixed)を見分けられない。動詞のある項目のあとの「過去分詞」の並びは、述語の並びと読む。 */
305
+ const pastPredicates = (first: readonly Token[], form: string, after: readonly Token[], words: ReadonlySet<string>): boolean =>
306
+ form === "Part" && participleOpening(after, words)?.form === "Part" && hasVerb(first);
307
+
308
+ const fromParticiplePhrase = (items: readonly Token[][], after: readonly Token[], words: ReadonlySet<string>): readonly Token[][] => {
309
+ const at = items.findIndex((item, index) => index > 0 && participleOpening(item, words) !== undefined);
310
+ const phrase = items[at];
311
+ const first = items[0];
312
+ const opening = phrase === undefined ? undefined : participleOpening(phrase, words);
313
+ if (first === undefined || phrase === undefined || opening === undefined) return items;
314
+ const rest = items.slice(at + 1);
315
+ const predicates = rest.some((item) => VERBAL.has(shapeOf(item) ?? "")) || pastPredicates(first, opening.form, after, words);
316
+ if (hasParticiple(first, opening.form) || predicates) return items;
317
+ const inPhrase = withoutLead([phrase.slice(opening.at + 1), ...rest].filter((item) => item.some(isContent)));
318
+ return new Set(inPhrase.map(shapeOf)).size > 1 ? rest : inPhrase;
319
+ };
320
+
321
+ const APPOSITIVE_ANCHOR = new Set(["NOUN", "PROPN"]);
322
+
323
+ /**
324
+ * 名詞のすぐ後ろの、読点で挟まれた X and Y は同格(two HPV types, HPV16 and HPV18, that account / Governments, both state and
325
+ * federal, spend)。項目が 2 つに見え、and の前に読点が無く、and の後ろの項目が読点で閉じ、後ろの 2 つに動詞が無いときに同格と読む。
326
+ * 2 つを結ぶ同格に Oxford comma は打たないので、and の前に読点があれば 3 つの並び(the subcommittee, full committee, and
327
+ * chamber levels, as well as)。読点の無い本当の並び(apples, pears and plums, then ...)はこの形と見分けられず、判定から外れる。
328
+ */
329
+ const isAppositive = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean => {
330
+ const [anchor, head] = items;
331
+ if (items.length !== 2 || anchor === undefined || head === undefined || commaBefore(clause.tokens, at)) return false;
332
+ const end = at + 1 + after.length;
333
+ // 閉じた読点のあとに and / or が続くなら、並びのあとに節をつないでいる(apples, pears and plums, and went home)。
334
+ const joinsClause = LIST_CONJUNCTION.has(clause.tokens[end + 1]?.surface.toLowerCase() ?? "");
335
+ const closed = clause.tokens[end]?.surface === "," && clause.depths[end] === clause.depths[at] && !joinsClause;
336
+ return closed && APPOSITIVE_ANCHOR.has(lastContent(anchor)?.pos ?? "") && !hasVerb(head) && !hasVerb(after);
337
+ };
338
+
339
+ /** 副詞を飛ばした頭の形。explain and justify と ultimately ensure は同じ動詞の項目。 */
340
+ const leadShape = (item: readonly Token[]): string | undefined => shapeOf(item.filter((token) => token.pos !== "ADV"));
341
+
342
+ /**
343
+ * and の後ろの項目に読点と、同じ形の項目を連れた and / or が続くなら、この and は項目の中にある(searches and seizures, and
344
+ * the Eighth's ban)。頭の形か、節かどうかが違えば別の節をつなぐ and で、この and までが並び(apples, pears and plums, and went home / and figs fell)。
345
+ */
346
+ const listContinues = (clause: Clause, at: number, after: readonly Token[]): boolean => {
347
+ const next = at + 1 + after.length;
348
+ if (clause.tokens[next]?.surface !== "," || !LIST_CONJUNCTION.has(clause.tokens[next + 1]?.surface.toLowerCase() ?? "")) return false;
349
+ const following = itemAfter(clause.tokens, next + 1);
350
+ const shape = leadShape(following);
351
+ return shape !== undefined && shape === leadShape(after) && isClause(following) === isClause(after);
352
+ };
353
+
258
354
  /**
259
355
  * 読点で区切った項目が and / or の前に 2 つ以上あり、最後の項目と and の後ろが同じ形のときだけ並列。
260
356
  * 導入の読点(After the review, the team fixed the bug and shipped it.)や、節をつなぐ読点
261
357
  * (We tested it, and the team shipped it.)は、Oxford comma を打つかどうかの選択を見せない。
262
358
  */
263
359
  const listAt = (clause: Clause, at: number): boolean | undefined => {
264
- const items = withoutLead(joinAdjectives(itemsBefore(clause, at)));
265
360
  const after = itemAfter(clause.tokens, at);
361
+ const items = fromParticiplePhrase(afterClosedList(withoutLead(joinAdjectives(itemsBefore(clause, at)))), after, clause.participleWords);
362
+ if (listContinues(clause, at, after) || isAppositive(clause, at, items, after)) return undefined;
266
363
  const last = items.at(-1);
267
364
  const shape = last === undefined ? undefined : shapeOf(last);
268
365
  if (items.length < 2 || shape === undefined || shape !== shapeOf(after)) return undefined;
@@ -276,8 +373,8 @@ const listAt = (clause: Clause, at: number): boolean | undefined => {
276
373
  * どちらが正しいかは決めない。スタイルガイドで割れる論点に立場を取ると rule ごと無視される。
277
374
  * 見るのは 1 つの文書で揃っているかだけ。spec §12.3。
278
375
  */
279
- const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
280
- const clause = { tokens, depths: depthsOf(tokens) };
376
+ const oxfordIn = (tokens: readonly Token[], participleWords: ReadonlySet<string>): boolean | undefined => {
377
+ const clause = { tokens, depths: depthsOf(tokens), participleWords };
281
378
  return tokens.reduce<boolean | undefined>(
282
379
  (found, token, at) => found ?? (at > 0 && LIST_CONJUNCTION.has(token.surface.toLowerCase()) ? listAt(clause, at) : undefined),
283
380
  undefined,
@@ -285,8 +382,9 @@ const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
285
382
  };
286
383
 
287
384
  export const oxfordComma: Detector = (doc, options): Finding[] => {
385
+ const participleWords = new Set((doc.lexicons["participle-word"] ?? []).map((entry) => entry.pattern.toLowerCase()));
288
386
  const judged = doc.sentences.flatMap((sentence) => {
289
- const oxford = oxfordIn(sentence.tokens ?? []);
387
+ const oxford = oxfordIn(sentence.tokens ?? [], participleWords);
290
388
  return oxford === undefined ? [] : [{ sentence, oxford }];
291
389
  });
292
390
  const withComma = judged.filter((entry) => entry.oxford).length;