chaffjs 0.2.0 → 0.4.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 (183) hide show
  1. package/dist/backends/anthropic.d.ts +24 -0
  2. package/dist/backends/anthropic.d.ts.map +1 -0
  3. package/dist/backends/anthropic.js +44 -0
  4. package/dist/backends/anthropic.js.map +1 -0
  5. package/dist/backends/openai.d.ts +29 -0
  6. package/dist/backends/openai.d.ts.map +1 -0
  7. package/dist/backends/openai.js +33 -0
  8. package/dist/backends/openai.js.map +1 -0
  9. package/dist/backends/types.d.ts +32 -0
  10. package/dist/backends/types.d.ts.map +1 -0
  11. package/dist/backends/types.js +18 -0
  12. package/dist/backends/types.js.map +1 -0
  13. package/dist/cli.d.ts.map +1 -1
  14. package/dist/cli.js +9 -5
  15. package/dist/cli.js.map +1 -1
  16. package/dist/commands/eval.d.ts.map +1 -1
  17. package/dist/commands/eval.js +5 -2
  18. package/dist/commands/eval.js.map +1 -1
  19. package/dist/commands/test.d.ts.map +1 -1
  20. package/dist/commands/test.js +62 -16
  21. package/dist/commands/test.js.map +1 -1
  22. package/dist/config/load.d.ts +8 -1
  23. package/dist/config/load.d.ts.map +1 -1
  24. package/dist/config/load.js +16 -3
  25. package/dist/config/load.js.map +1 -1
  26. package/dist/detectors/agentless-passive.d.ts +3 -0
  27. package/dist/detectors/agentless-passive.d.ts.map +1 -0
  28. package/dist/detectors/agentless-passive.js +31 -0
  29. package/dist/detectors/agentless-passive.js.map +1 -0
  30. package/dist/detectors/char-shape.d.ts +12 -0
  31. package/dist/detectors/char-shape.d.ts.map +1 -0
  32. package/dist/detectors/char-shape.js +39 -0
  33. package/dist/detectors/char-shape.js.map +1 -0
  34. package/dist/detectors/en-shape.d.ts +19 -0
  35. package/dist/detectors/en-shape.d.ts.map +1 -0
  36. package/dist/detectors/en-shape.js +176 -0
  37. package/dist/detectors/en-shape.js.map +1 -0
  38. package/dist/detectors/index.d.ts.map +1 -1
  39. package/dist/detectors/index.js +37 -0
  40. package/dist/detectors/index.js.map +1 -1
  41. package/dist/detectors/lexicon.d.ts +15 -0
  42. package/dist/detectors/lexicon.d.ts.map +1 -0
  43. package/dist/detectors/lexicon.js +109 -0
  44. package/dist/detectors/lexicon.js.map +1 -0
  45. package/dist/detectors/repeated-head.d.ts.map +1 -1
  46. package/dist/detectors/repeated-head.js +17 -5
  47. package/dist/detectors/repeated-head.js.map +1 -1
  48. package/dist/detectors/sentence-ending.d.ts +3 -0
  49. package/dist/detectors/sentence-ending.d.ts.map +1 -0
  50. package/dist/detectors/sentence-ending.js +34 -0
  51. package/dist/detectors/sentence-ending.js.map +1 -0
  52. package/dist/detectors/signals.d.ts +6 -0
  53. package/dist/detectors/signals.d.ts.map +1 -0
  54. package/dist/detectors/signals.js +183 -0
  55. package/dist/detectors/signals.js.map +1 -0
  56. package/dist/detectors/structure.d.ts +24 -0
  57. package/dist/detectors/structure.d.ts.map +1 -0
  58. package/dist/detectors/structure.js +111 -0
  59. package/dist/detectors/structure.js.map +1 -0
  60. package/dist/detectors/team.d.ts +17 -0
  61. package/dist/detectors/team.d.ts.map +1 -0
  62. package/dist/detectors/team.js +87 -0
  63. package/dist/detectors/team.js.map +1 -0
  64. package/dist/detectors/token-shape.d.ts +4 -0
  65. package/dist/detectors/token-shape.d.ts.map +1 -0
  66. package/dist/detectors/token-shape.js +62 -0
  67. package/dist/detectors/token-shape.js.map +1 -0
  68. package/dist/document.d.ts +11 -1
  69. package/dist/document.d.ts.map +1 -1
  70. package/dist/document.js +85 -10
  71. package/dist/document.js.map +1 -1
  72. package/dist/env.d.ts +12 -0
  73. package/dist/env.d.ts.map +1 -0
  74. package/dist/env.js +20 -0
  75. package/dist/env.js.map +1 -0
  76. package/dist/init.d.ts.map +1 -1
  77. package/dist/init.js +9 -6
  78. package/dist/init.js.map +1 -1
  79. package/dist/judge.d.ts +14 -16
  80. package/dist/judge.d.ts.map +1 -1
  81. package/dist/judge.js +0 -0
  82. package/dist/judge.js.map +1 -1
  83. package/dist/levels.d.ts +1 -9
  84. package/dist/levels.d.ts.map +1 -1
  85. package/dist/levels.js +15 -3
  86. package/dist/levels.js.map +1 -1
  87. package/dist/look-at.d.ts +18 -0
  88. package/dist/look-at.d.ts.map +1 -0
  89. package/dist/look-at.js +43 -0
  90. package/dist/look-at.js.map +1 -0
  91. package/dist/plugin.d.ts +47 -2
  92. package/dist/plugin.d.ts.map +1 -1
  93. package/dist/render/explain.d.ts +1 -1
  94. package/dist/render/explain.d.ts.map +1 -1
  95. package/dist/render/explain.js +12 -4
  96. package/dist/render/explain.js.map +1 -1
  97. package/dist/render/rules-json.d.ts.map +1 -1
  98. package/dist/render/rules-json.js +13 -5
  99. package/dist/render/rules-json.js.map +1 -1
  100. package/dist/render/semantic.d.ts +2 -1
  101. package/dist/render/semantic.d.ts.map +1 -1
  102. package/dist/render/semantic.js +35 -0
  103. package/dist/render/semantic.js.map +1 -1
  104. package/dist/rule-load.d.ts.map +1 -1
  105. package/dist/rule-load.js +27 -1
  106. package/dist/rule-load.js.map +1 -1
  107. package/dist/run-semantic.d.ts +22 -1
  108. package/dist/run-semantic.d.ts.map +1 -1
  109. package/dist/run-semantic.js +21 -10
  110. package/dist/run-semantic.js.map +1 -1
  111. package/dist/run.d.ts +6 -1
  112. package/dist/run.d.ts.map +1 -1
  113. package/dist/run.js +34 -2
  114. package/dist/run.js.map +1 -1
  115. package/dist/sentence-shape.d.ts +6 -0
  116. package/dist/sentence-shape.d.ts.map +1 -0
  117. package/dist/sentence-shape.js +15 -0
  118. package/dist/sentence-shape.js.map +1 -0
  119. package/package.json +4 -3
  120. package/rules/adverb-overuse.yaml +28 -0
  121. package/rules/agentless-passive.yaml +28 -0
  122. package/rules/ai-tell.yaml +27 -0
  123. package/rules/concrete-evidence-density.yaml +25 -0
  124. package/rules/cushion-phrase-density.yaml +26 -0
  125. package/rules/double-keigo.yaml +27 -0
  126. package/rules/emoji-density.yaml +26 -0
  127. package/rules/excessive-hedging.yaml +26 -0
  128. package/rules/expletive-construction.yaml +27 -0
  129. package/rules/hiragana-fukushi.yaml +27 -0
  130. package/rules/internal-jargon.yaml +27 -0
  131. package/rules/max-kanji-continuous.yaml +26 -0
  132. package/rules/max-paragraph-length.yaml +25 -0
  133. package/rules/max-sentence-length.yaml +22 -0
  134. package/rules/ngram-repetition.yaml +25 -0
  135. package/rules/no-doubled-joshi.yaml +28 -0
  136. package/rules/no-mixed-desumasu.yaml +29 -0
  137. package/rules/no-nakaguro-parallel.yaml +27 -0
  138. package/rules/oxford-comma-consistency.yaml +26 -0
  139. package/rules/paragraph-length-variance.yaml +25 -0
  140. package/rules/preamble-length.yaml +25 -0
  141. package/rules/proper-noun-density.yaml +26 -0
  142. package/rules/repeated-conjunction.yaml +26 -0
  143. package/rules/required-sections.yaml +26 -0
  144. package/rules/rule-of-three.yaml +25 -0
  145. package/rules/sasete-itadaku.yaml +27 -0
  146. package/rules/section-length-uniformity.yaml +25 -0
  147. package/rules/sentence-initial-conjunction-run.yaml +27 -0
  148. package/rules/taigen-dome-in-prose.yaml +27 -0
  149. package/rules/title-case-consistency.yaml +26 -0
  150. package/rules/undefined-acronym.yaml +25 -0
  151. package/rules/unqualified-superlative.yaml +26 -0
  152. package/src/backends/anthropic.ts +61 -0
  153. package/src/backends/openai.ts +46 -0
  154. package/src/backends/types.ts +38 -0
  155. package/src/cli.ts +9 -5
  156. package/src/commands/eval.ts +5 -2
  157. package/src/commands/test.ts +74 -16
  158. package/src/config/load.ts +22 -3
  159. package/src/detectors/agentless-passive.ts +34 -0
  160. package/src/detectors/char-shape.ts +47 -0
  161. package/src/detectors/en-shape.ts +187 -0
  162. package/src/detectors/index.ts +37 -0
  163. package/src/detectors/lexicon.ts +124 -0
  164. package/src/detectors/repeated-head.ts +17 -6
  165. package/src/detectors/sentence-ending.ts +37 -0
  166. package/src/detectors/signals.ts +199 -0
  167. package/src/detectors/structure.ts +119 -0
  168. package/src/detectors/team.ts +90 -0
  169. package/src/detectors/token-shape.ts +81 -0
  170. package/src/document.ts +95 -11
  171. package/src/env.ts +20 -0
  172. package/src/init.ts +9 -6
  173. package/src/judge.ts +0 -0
  174. package/src/levels.ts +16 -4
  175. package/src/look-at.ts +53 -0
  176. package/src/plugin.ts +48 -4
  177. package/src/render/explain.ts +12 -4
  178. package/src/render/rules-json.ts +16 -5
  179. package/src/render/semantic.ts +38 -1
  180. package/src/rule-load.ts +28 -1
  181. package/src/run-semantic.ts +32 -11
  182. package/src/run.ts +36 -3
  183. package/src/sentence-shape.ts +22 -0
@@ -0,0 +1,26 @@
1
+ id: oxford-comma-consistency
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: oxford-comma
5
+ languages: [en]
6
+ severity: info
7
+
8
+ name:
9
+ ja: 並列の読点が揃っていない
10
+ en: Oxford comma use is inconsistent
11
+
12
+ why:
13
+ ja: 最後の and の前に読点を打つかは、スタイルガイドで割れます。どちらが正しいかは決めません。1 つの文書で混ざっていると、読み手は項目の切れ目を毎回読み直すことになります。
14
+ en: Whether to put a comma before the final "and" is a style-guide split. Neither is wrong. Mixed within one document, the reader re-parses every list.
15
+
16
+ message:
17
+ ja: この文だけ並列の読点の打ちかたが他と違います(文書の中で {count} 文)
18
+ en: "This list punctuates differently from the rest ({count} in this document)"
19
+
20
+ how_to_fix:
21
+ ja: 多いほうに揃えてください。
22
+ en: Match the majority.
23
+
24
+ levels: { strict: 3, normal: 2, relaxed: 1 }
25
+
26
+ use_for: [blog, business, technical]
@@ -0,0 +1,25 @@
1
+ id: paragraph-length-variance
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: paragraph-variance
5
+ severity: info
6
+
7
+ name:
8
+ ja: 段落の長さが揃いすぎている
9
+ en: Paragraph lengths are too uniform
10
+
11
+ why:
12
+ ja: 人が書くと、言いたいことの重さで段落の長さは揺れます。揃いすぎは、埋めるために長さを合わせた跡です。
13
+ en: When a person writes, paragraph length follows what matters. Uniform length is the mark of filling a shape.
14
+
15
+ message:
16
+ ja: 段落の長さのばらつきが {variance}% しかありません({limit}% 以上ほしい)
17
+ en: Paragraph length varies by only {variance}% (want at least {limit}%)
18
+
19
+ how_to_fix:
20
+ ja: 短くできる段落を短くしてください。全部を同じ長さにしないこと。
21
+ en: Let the short ones be short.
22
+
23
+ levels: { strict: 40, normal: 30, relaxed: 20 }
24
+
25
+ use_for: [blog]
@@ -0,0 +1,25 @@
1
+ id: preamble-length
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: preamble-length
5
+ severity: info
6
+
7
+ name:
8
+ ja: 本題に入るまでが長い
9
+ en: Too long before the point
10
+
11
+ why:
12
+ ja: 業務文書の読み手は結論を探しに来ています。最初の見出しまでに段落が重なると、探す手間だけが増えます。
13
+ en: A reader of a business document is looking for the decision. Paragraphs before the first heading only delay it.
14
+
15
+ message:
16
+ ja: 最初の見出しまでに {count} 段落あります({limit} 段落まで)
17
+ en: "{count} paragraphs before the first heading (limit {limit})"
18
+
19
+ how_to_fix:
20
+ ja: 結論を先に書くか、前置きを見出しの下へ移してください。
21
+ en: Put the conclusion first, or move the preamble under a heading.
22
+
23
+ levels: { strict: 1, normal: 2, relaxed: 4 }
24
+
25
+ use_for: [business]
@@ -0,0 +1,26 @@
1
+ id: proper-noun-density
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: proper-noun-density
5
+ requires: [pos]
6
+ severity: info
7
+
8
+ name:
9
+ ja: 固有名詞が多い
10
+ en: Too many proper nouns
11
+
12
+ why:
13
+ ja: 製品名・社名・人名が続くと、それを知らない読者は文の骨組みを見失います。知っている人には読めるので、書いた本人には気づきにくいところです。
14
+ en: A run of product, company and person names hides the shape of the sentence from anyone who does not already know them. It reads fine to the person who wrote it.
15
+
16
+ message:
17
+ ja: 1000 語あたり {density} 個の固有名詞があります({limit} 個まで)
18
+ en: "{density} proper nouns per 1000 words (limit {limit})"
19
+
20
+ how_to_fix:
21
+ ja: 2 度目からは「その製品」「そのチーム」のように受けてください。初出で何者かを一言添えると、知らない人も読めます。
22
+ en: After first use, refer back to it. One phrase saying what it is lets a stranger keep reading.
23
+
24
+ levels: { strict: 20, normal: 40, relaxed: 80 }
25
+
26
+ use_for: [blog, business, technical]
@@ -0,0 +1,26 @@
1
+ id: repeated-conjunction
2
+ layer: L2
3
+ status: experimental
4
+ how_to_find: repeated-conjunction
5
+ word_list: paragraph-opener
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 段落が同じ調子で始まり続ける
10
+ en: Paragraphs keep opening the same way
11
+
12
+ why:
13
+ ja: 接続詞で始まる段落は 1 つなら流れを作りますが、続くとどの段落も前の付け足しに見えて、話がどこへ向かっているのか分からなくなります。
14
+ en: One paragraph opening with a conjunction carries the flow. A run of them makes every paragraph an afterthought.
15
+
16
+ message:
17
+ ja: 接続詞で始まる段落が {count} 続いています({limit} まで)
18
+ en: "{count} paragraphs in a row open with a conjunction (limit {limit})"
19
+
20
+ how_to_fix:
21
+ ja: どれか 1 つを残して、他は言いたいことから始めてください。
22
+ en: Keep one. Start the others with what you want to say.
23
+
24
+ levels: { strict: 1, normal: 2, relaxed: 4 }
25
+
26
+ use_for: [business, blog, technical]
@@ -0,0 +1,26 @@
1
+ id: required-sections
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: required-sections
5
+ severity: error
6
+
7
+ name:
8
+ ja: 必要な見出しが無い
9
+ en: A required heading is missing
10
+
11
+ why:
12
+ ja: 「提案書にはリスクの節がある」のような取り決めは、組織ごとに違います。chaff は必須の見出しを持たず、chaff.yaml の required_sections に書かれた取り決めを守れているかだけを見ます。書いていなければ何も言いません。
13
+ en: '"A proposal has a risk section" is a decision each team makes. chaff holds no list of its own; it only checks the one written under `required_sections` in chaff.yaml. With nothing written, this rule says nothing.'
14
+
15
+ message:
16
+ ja: 「{word}」の見出しがありません
17
+ en: 'No heading for "{word}"'
18
+
19
+ how_to_fix:
20
+ ja: その見出しを足してください。要らないと思うなら、chaff.yaml の required_sections から外すのが筋です。
21
+ en: Add the heading. If it is not needed, the honest fix is to remove it from `required_sections`.
22
+
23
+ # 何個足りなければ指摘するか。1 は「1 つでも足りなければ」。
24
+ levels: { strict: 1, normal: 1, relaxed: 2 }
25
+
26
+ use_for: [business, blog, technical]
@@ -0,0 +1,25 @@
1
+ id: rule-of-three
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: rule-of-three
5
+ severity: info
6
+
7
+ name:
8
+ ja: 3 項目の箇条書きばかり
9
+ en: Everything comes in threes
10
+
11
+ why:
12
+ ja: 3 つ並べると収まりがよいので、言いたいことが 2 つでも 4 つでも 3 つにされます。
13
+ en: Three items feel complete, so two become three and four become three.
14
+
15
+ message:
16
+ ja: 箇条書き {total} 個のうち {count} 個が 3 項目です({limit}% まで)
17
+ en: "{count} of {total} lists have exactly three items ({limit}% allowed)"
18
+
19
+ how_to_fix:
20
+ ja: 数を合わせるために足した項目を消してください。2 つで足りるなら 2 つで。
21
+ en: Drop the item you added to reach three.
22
+
23
+ levels: { strict: 50, normal: 70, relaxed: 90 }
24
+
25
+ use_for: [blog]
@@ -0,0 +1,27 @@
1
+ id: sasete-itadaku
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: phrase-match
5
+ word_list: sasete-itadaku
6
+ languages: [ja]
7
+ severity: info
8
+
9
+ name:
10
+ ja: 「させていただく」が多い
11
+ en: Overused humble form
12
+
13
+ why:
14
+ ja: 1 度なら丁寧ですが、重なると誰の判断で何をするのかが消えます。「検討させていただきます」は、検討するのかしないのかを言っていません。
15
+ en: Once it is polite. Repeated, it hides whose decision it is and what will actually happen.
16
+
17
+ message:
18
+ ja: 「{matched}」が文書内に {count} 回あります({limit} 回まで)
19
+ en: '"{matched}" appears {count} times in this document (limit {limit})'
20
+
21
+ how_to_fix:
22
+ ja: 主語を立てて言い切ってください。「検討させていただきます」を「来週までに結論を出します」に。
23
+ en: Name the actor and commit. "We will look into it" becomes "we will decide by Friday".
24
+
25
+ levels: { strict: 1, normal: 3, relaxed: 6 }
26
+
27
+ use_for: [business, blog]
@@ -0,0 +1,25 @@
1
+ id: section-length-uniformity
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: section-uniformity
5
+ severity: info
6
+
7
+ name:
8
+ ja: 節の長さが揃いすぎている
9
+ en: Section lengths are too uniform
10
+
11
+ why:
12
+ ja: 見出しごとに機械的に埋めると、節の長さが揃います。書くことの量は節ごとに違うはずです。
13
+ en: Filling every heading to the same size is what a template does, not what a writer does.
14
+
15
+ message:
16
+ ja: 節の長さのばらつきが {variance}% しかありません({limit}% 以上ほしい)
17
+ en: Section length varies by only {variance}% (want at least {limit}%)
18
+
19
+ how_to_fix:
20
+ ja: 書くことの少ない節は短いままにしてください。
21
+ en: Let the thin sections stay thin.
22
+
23
+ levels: { strict: 45, normal: 35, relaxed: 25 }
24
+
25
+ use_for: [blog]
@@ -0,0 +1,27 @@
1
+ id: sentence-initial-conjunction-run
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: conjunction-run
5
+ word_list: sentence-opener
6
+ languages: [en]
7
+ severity: info
8
+
9
+ name:
10
+ ja: 接続詞で始まる文が続いている
11
+ en: Run of sentences opening with a conjunction
12
+
13
+ why:
14
+ ja: 接続詞で始まる文は 1 つなら効きますが、続くとどの文も前の文の付け足しに見えて、何が主張なのかが分からなくなります。
15
+ en: One sentence opening with "And" or "But" lands. A run of them makes every sentence read as an afterthought.
16
+
17
+ message:
18
+ ja: 接続詞で始まる文が {count} 文続いています({limit} 文まで)
19
+ en: "{count} sentences in a row open with a conjunction (limit {limit})"
20
+
21
+ how_to_fix:
22
+ ja: どれか 1 つを残して、他は前の文に繋ぐか独立させてください。
23
+ en: Keep one. Join the others to the previous sentence, or let them stand alone.
24
+
25
+ levels: { strict: 1, normal: 2, relaxed: 4 }
26
+
27
+ use_for: [blog, business]
@@ -0,0 +1,27 @@
1
+ id: taigen-dome-in-prose
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: noun-ending
5
+ requires: [pos]
6
+ languages: [ja]
7
+ severity: info
8
+
9
+ name:
10
+ ja: 体言止めが続いている
11
+ en: Too many noun-ending sentences
12
+
13
+ why:
14
+ ja: 体言止めは 1 つなら効きますが、続くと文が途中で切れたように読めます。箇条書きでは普通の書きかたなので、本文だけを見ています。
15
+ en: One noun-ending sentence lands. Several in a row read as unfinished. Bullet lists are excluded, where the form is normal.
16
+
17
+ message:
18
+ ja: 本文に体言止めが {count} 文あります({limit} 文まで)
19
+ en: "{count} sentences end on a noun in the body text (limit {limit})"
20
+
21
+ how_to_fix:
22
+ ja: 述語を足してください。「〜が課題。」を「〜が課題です。」に。
23
+ en: Add the predicate.
24
+
25
+ levels: { strict: 2, normal: 4, relaxed: 8 }
26
+
27
+ use_for: [blog, business, technical]
@@ -0,0 +1,26 @@
1
+ id: title-case-consistency
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: title-case-mix
5
+ languages: [en]
6
+ severity: info
7
+
8
+ name:
9
+ ja: 見出しの大文字化が揃っていない
10
+ en: Heading capitalisation is inconsistent
11
+
12
+ why:
13
+ ja: Title Case と sentence case のどちらが正しいかは決めません。1 つの文書で混ざっていると、見出しの階層が変わったように見えます。
14
+ en: Neither Title Case nor sentence case is wrong. Mixed within one document, the headings look like they change level.
15
+
16
+ message:
17
+ ja: この見出しだけ他と大文字化の流儀が違います(文書の中で {count} 個)
18
+ en: "This heading's capitalisation differs from the rest ({count} in this document)"
19
+
20
+ how_to_fix:
21
+ ja: 多いほうに揃えてください。
22
+ en: Match the majority.
23
+
24
+ levels: { strict: 3, normal: 2, relaxed: 1 }
25
+
26
+ use_for: [blog, business, technical]
@@ -0,0 +1,25 @@
1
+ id: undefined-acronym
2
+ layer: L1
3
+ status: experimental
4
+ how_to_find: undefined-acronym
5
+ severity: info
6
+
7
+ name:
8
+ ja: 略語が説明なしで出てくる
9
+ en: Acronym never expanded
10
+
11
+ why:
12
+ ja: 略語は読む人によって指すものが違います。社内で通じても、半年後の自分や別の部署には通じません。
13
+ en: An acronym means different things to different readers. What is obvious inside one team is not obvious outside it.
14
+
15
+ message:
16
+ ja: 「{word}」が説明なしで使われています(説明のない略語 {count} 個)
17
+ en: '"{word}" is used without an expansion ({count} such acronyms)'
18
+
19
+ how_to_fix:
20
+ ja: 初出のところで括弧に入れて展開してください。「CI(継続的インテグレーション)」のように。
21
+ en: Expand it in brackets at first use.
22
+
23
+ levels: { strict: 1, normal: 3, relaxed: 6 }
24
+
25
+ use_for: [business, technical]
@@ -0,0 +1,26 @@
1
+ id: unqualified-superlative
2
+ layer: L2
3
+ status: experimental
4
+ how_to_find: unqualified-superlative
5
+ word_list: superlative
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 比べる相手のない最上級
10
+ en: Superlative with nothing to compare
11
+
12
+ why:
13
+ ja: 「最も速い」だけでは、何と比べて最もなのかがありません。比較対象も条件も無い最上級は、読み手に確かめようがありません。
14
+ en: '"The fastest" without saying than what leaves the reader nothing to check.'
15
+
16
+ message:
17
+ ja: 「{matched}」に比べる相手がありません(そういう言い方が {count} 箇所)
18
+ en: '"{matched}" has nothing to compare against ({count} such claims)'
19
+
20
+ how_to_fix:
21
+ ja: 比較対象か条件を添えてください。「社内の 3 案のうち最も速い」のように。
22
+ en: Name the comparison. "The fastest of the three options we tried."
23
+
24
+ levels: { strict: 1, normal: 2, relaxed: 4 }
25
+
26
+ use_for: [business, blog]
@@ -0,0 +1,61 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import Anthropic from "@anthropic-ai/sdk";
5
+ import { toFailure } from "./types.ts";
6
+ import type { Failure, Judge, Prompt } from "./types.ts";
7
+
8
+ export type AnthropicResponse = { readonly content: readonly { readonly type: string; readonly text?: string | undefined }[] };
9
+
10
+ /** judge が使うのは messages.create だけ。最小の面だけを要求する。テストで差し替えられる。 */
11
+ export type AnthropicClient = { readonly messages: { readonly create: (params: Anthropic.MessageCreateParamsNonStreaming) => Promise<AnthropicResponse> } };
12
+
13
+ const isTextBlock = (block: unknown): block is { readonly text: string } =>
14
+ typeof block === "object" && block !== null && "text" in block && typeof block.text === "string";
15
+
16
+ export const DEFAULT_MODEL = "claude-opus-5";
17
+
18
+ /**
19
+ * 認証情報があるか。
20
+ *
21
+ * env var を見るだけでは足りない。SDK は ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN /
22
+ * `ant auth login` のプロファイル / Workload Identity の順に解決するので、env が
23
+ * 無くても通ることがある。逆に、無いまま呼ぶと SDK は型の無い素の Error を投げる
24
+ * (AnthropicError でも APIError でもない)ので、あとから種類で判別できない。
25
+ * だから先に確かめる。
26
+ */
27
+ const ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_IDENTITY_TOKEN", "ANTHROPIC_IDENTITY_TOKEN_FILE"];
28
+
29
+ export const configDir = (): string => process.env["ANTHROPIC_CONFIG_DIR"] ?? join(homedir(), ".config", "anthropic");
30
+
31
+ export const hasCredentials = (profileDir: string = configDir()): boolean =>
32
+ ENV_KEYS.some((name) => (process.env[name] ?? "").length > 0) || existsSync(profileDir);
33
+
34
+ export const isAuthFailure = (error: unknown): boolean =>
35
+ error instanceof Anthropic.AuthenticationError || (error instanceof Anthropic.APIError && error.status === 401);
36
+
37
+ /** API が返した失敗だけを拾う。ネットワーク断やコードの誤りは、握りつぶさずそのまま投げる。 */
38
+ export const describeFailure = (error: unknown): Failure | undefined => {
39
+ if (!(error instanceof Anthropic.APIError)) return undefined;
40
+ const raw: { status: unknown; message: unknown } = error;
41
+ return toFailure(raw);
42
+ };
43
+
44
+ export const judge =
45
+ (client?: AnthropicClient): Judge =>
46
+ async (prompt: Prompt): Promise<string> => {
47
+ const anthropic: AnthropicClient = client ?? new Anthropic();
48
+ const response = await anthropic.messages.create({
49
+ model: prompt.model,
50
+ max_tokens: 1024,
51
+ system: prompt.system,
52
+ output_config: { format: { type: "json_schema", schema: prompt.schema } },
53
+ messages: [{ role: "user", content: prompt.user }],
54
+ });
55
+ const body = response.content.map((block) => (isTextBlock(block) ? block.text : undefined)).find((text) => text !== undefined);
56
+ if (body === undefined) throw new Error("判定が返りませんでした");
57
+ return body;
58
+ };
59
+
60
+ /** 認証が無いときに人へ見せる案内。 */
61
+ export const SETUP_HINT = "ANTHROPIC_API_KEY を設定するか、ant auth login を実行してください。";
@@ -0,0 +1,46 @@
1
+ import OpenAI from "openai";
2
+ import { toFailure } from "./types.ts";
3
+ import type { Failure, Judge, Prompt } from "./types.ts";
4
+
5
+ export type OpenAIResponse = { readonly choices: readonly { readonly message: { readonly content: string | null } }[] };
6
+
7
+ /** judge が使うのは chat.completions.create だけ。最小の面だけを要求する。 */
8
+ export type OpenAIClient = {
9
+ readonly chat: { readonly completions: { readonly create: (params: OpenAI.ChatCompletionCreateParamsNonStreaming) => Promise<OpenAIResponse> } };
10
+ };
11
+
12
+ export const DEFAULT_MODEL = "gpt-5";
13
+
14
+ export const hasCredentials = (): boolean => (process.env["OPENAI_API_KEY"] ?? "").length > 0;
15
+
16
+ export const isAuthFailure = (error: unknown): boolean => error instanceof OpenAI.APIError && error.status === 401;
17
+
18
+ /** API が返した失敗だけを拾う。ネットワーク断やコードの誤りは、握りつぶさずそのまま投げる。 */
19
+ export const describeFailure = (error: unknown): Failure | undefined => {
20
+ if (!(error instanceof OpenAI.APIError)) return undefined;
21
+ const raw: { status: unknown; message: unknown } = error;
22
+ return toFailure(raw);
23
+ };
24
+
25
+ /**
26
+ * 同じ JSON schema をそのまま使う。包みかたが違うだけで、返させる形は provider で変えない。
27
+ * 変えると、provider を替えたときに判定の意味まで変わる。
28
+ */
29
+ export const judge =
30
+ (client?: OpenAIClient): Judge =>
31
+ async (prompt: Prompt): Promise<string> => {
32
+ const openai: OpenAIClient = client ?? new OpenAI();
33
+ const response = await openai.chat.completions.create({
34
+ model: prompt.model,
35
+ messages: [
36
+ { role: "system", content: prompt.system },
37
+ { role: "user", content: prompt.user },
38
+ ],
39
+ response_format: { type: "json_schema", json_schema: { name: "verdict", schema: prompt.schema, strict: true } },
40
+ });
41
+ const body = response.choices[0]?.message.content;
42
+ if (body === null || body === undefined) throw new Error("判定が返りませんでした");
43
+ return body;
44
+ };
45
+
46
+ export const SETUP_HINT = "OPENAI_API_KEY を設定してください。";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 判定役の面。ここまでが provider ごとに違い、ここから先は共通。
3
+ *
4
+ * 返すのは JSON の文字列で、Verdict への変換と検証は judge.ts が 1 箇所で行う。
5
+ * provider が増えても「返ってきた形の確かめかた」は増やさない。
6
+ */
7
+ export type JsonSchema = Readonly<Record<string, unknown>>;
8
+
9
+ export type Prompt = { readonly system: string; readonly user: string; readonly schema: JsonSchema; readonly model: string };
10
+
11
+ export type Judge = (prompt: Prompt) => Promise<string>;
12
+
13
+ export type BackendName = "anthropic" | "openai";
14
+
15
+ export const BACKENDS: readonly BackendName[] = ["anthropic", "openai"];
16
+
17
+ export const isBackend = (value: unknown): value is BackendName => BACKENDS.some((name) => name === value);
18
+
19
+ /** API が返した失敗。生のスタックトレースを人に見せないための、最小の分類。 */
20
+ export type Failure = { readonly kind: "auth" | "quota" | "api"; readonly status: number | undefined; readonly message: string };
21
+
22
+ const statusOf = (value: unknown): number | undefined => (typeof value === "number" ? value : undefined);
23
+
24
+ const messageOf = (value: unknown): string => (typeof value === "string" && value.length > 0 ? value : "原因は返ってきませんでした");
25
+
26
+ const kindOf = (status: number | undefined): Failure["kind"] => {
27
+ if (status === 401 || status === 403) return "auth";
28
+ return status === 429 ? "quota" : "api";
29
+ };
30
+
31
+ /**
32
+ * SDK の APIError から Failure を作る。分類は provider で変えない。
33
+ * status と message は SDK 上 any なので、ここで 1 度だけ形を確かめる。
34
+ */
35
+ export const toFailure = (error: { readonly status: unknown; readonly message: unknown }): Failure => {
36
+ const status = statusOf(error.status);
37
+ return { kind: kindOf(status), status, message: messageOf(error.message) };
38
+ };
package/src/cli.ts CHANGED
@@ -5,7 +5,7 @@ import { loadAdapter, packageFor } from "./adapter-load.ts";
5
5
  import { CONFIG_FILE, EMPTY, loadConfig, type Config } from "./config/load.ts";
6
6
  import { applyByPath } from "./config/by-path.ts";
7
7
  import { applyLevel } from "./config/write.ts";
8
- import { buildDocument } from "./document.ts";
8
+ import { buildDocument, teamRules } from "./document.ts";
9
9
  import { guessLanguage } from "./detect.ts";
10
10
  import { collectTargets } from "./files.ts";
11
11
  import { BASELINE_FILE, fingerprint, readBaseline, splitByBaseline, writeBaseline } from "./baseline.ts";
@@ -22,7 +22,7 @@ import { renderExplain } from "./render/explain.ts";
22
22
  import { renderFriendly } from "./render/friendly.ts";
23
23
  import { rulesJson } from "./render/rules-json.ts";
24
24
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
25
- import { runRules } from "./run.ts";
25
+ import { neededBy, runRules } from "./run.ts";
26
26
  import type { Level, RuleDefinition } from "./plugin.ts";
27
27
 
28
28
  const USAGE = `chaff — 文章の読みにくいところを見つけます。文章は書き換えません。
@@ -30,6 +30,7 @@ const USAGE = `chaff — 文章の読みにくいところを見つけます。
30
30
  chaff <file|dir|glob>... 検査する(設定も API key も要りません)
31
31
  chaff . この場所の Markdown を全部
32
32
  chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
33
+ 判定役は chaff.yaml の ai_backend で選びます
33
34
  chaff init chaff.yaml を作る
34
35
  chaff eval <dir> 手元の文書で閾値を測り直す
35
36
  chaff explain <rule> そのルールの意図と根拠を読む
@@ -43,6 +44,7 @@ const USAGE = `chaff — 文章の読みにくいところを見つけます。
43
44
  --experimental 試験中の rule も動かす
44
45
  --show-baseline 棚上げした分も含めて全部見る
45
46
  --watch 保存のたびに見直し、変わったところだけ出す
47
+ --dry-run test で、何を AI に送るかだけを見る(API を呼びません)
46
48
 
47
49
  この箇所だけ黙らせる: <!-- stet: rule-id — 理由 -->
48
50
 
@@ -92,9 +94,11 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
92
94
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
93
95
  const adapter = await loadAdapter(language);
94
96
  const { genre, from } = resolveGenre(path, source, config);
95
- const doc = buildDocument(path, source, adapter);
96
97
  const rules = loadRules(language);
97
- const raw = runRules(doc, rules, config.rules, config.experimental || argv.includes("--experimental"), genre);
98
+ const experimental = config.experimental || argv.includes("--experimental");
99
+ await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
100
+ const doc = buildDocument(path, source, adapter, teamRules(config));
101
+ const raw = runRules(doc, rules, config.rules, experimental, genre);
98
102
  // 応答は 3 つ。stet で黙らせたものは、ここで落とす。
99
103
  const applied = applySuppressions(
100
104
  source,
@@ -177,7 +181,7 @@ const explain = (ruleId: string | undefined): number => {
177
181
  return 1;
178
182
  }
179
183
  const current = config.rules[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
180
- console.log(renderExplain(rule, current, language, unitOf(rule.id, language)));
184
+ console.log(renderExplain(rule, current, language, unitOf(rule.id, language), config.genre));
181
185
  return 0;
182
186
  };
183
187
 
@@ -1,12 +1,13 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import { loadAdapter } from "../adapter-load.ts";
3
3
  import { applyByPath } from "../config/by-path.ts";
4
- import { buildDocument } from "../document.ts";
4
+ import { buildDocument, teamRules } from "../document.ts";
5
5
  import { guessLanguage } from "../detect.ts";
6
6
  import { collectTargets } from "../files.ts";
7
7
  import { loadRules } from "../rule-load.ts";
8
8
  import { evaluate } from "../eval.ts";
9
9
  import { renderEval } from "../render/eval.ts";
10
+ import { neededBy } from "../run.ts";
10
11
  import type { Config } from "../config/load.ts";
11
12
 
12
13
  export type Context = {
@@ -32,7 +33,9 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
32
33
  const source = await readFile(path, "utf8");
33
34
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
34
35
  const adapter = await loadAdapter(language);
35
- return { doc: buildDocument(path, source, adapter), language, genre: resolveGenre(path, source, config).genre };
36
+ const { genre } = resolveGenre(path, source, config);
37
+ await adapter.prepare?.(neededBy(loadRules(language), config.rules, config.experimental, genre, language));
38
+ return { doc: buildDocument(path, source, adapter, teamRules(config)), language, genre };
36
39
  }),
37
40
  );
38
41
  const language = docs[0]?.language ?? "ja";