chaffjs 0.6.0 → 0.8.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 (159) hide show
  1. package/README.md +11 -4
  2. package/dist/adapter-load.d.ts +12 -1
  3. package/dist/adapter-load.d.ts.map +1 -1
  4. package/dist/adapter-load.js +37 -13
  5. package/dist/adapter-load.js.map +1 -1
  6. package/dist/cli-args.d.ts +8 -0
  7. package/dist/cli-args.d.ts.map +1 -0
  8. package/dist/cli-args.js +8 -0
  9. package/dist/cli-args.js.map +1 -0
  10. package/dist/cli-text.d.ts +26 -0
  11. package/dist/cli-text.d.ts.map +1 -0
  12. package/dist/cli-text.js +151 -0
  13. package/dist/cli-text.js.map +1 -0
  14. package/dist/cli.d.ts +2 -0
  15. package/dist/cli.d.ts.map +1 -1
  16. package/dist/cli.js +83 -86
  17. package/dist/cli.js.map +1 -1
  18. package/dist/commands/cite.d.ts +2 -1
  19. package/dist/commands/cite.d.ts.map +1 -1
  20. package/dist/commands/cite.js +39 -18
  21. package/dist/commands/cite.js.map +1 -1
  22. package/dist/commands/eval.d.ts.map +1 -1
  23. package/dist/commands/eval.js +1 -1
  24. package/dist/commands/eval.js.map +1 -1
  25. package/dist/commands/skill.d.ts +22 -0
  26. package/dist/commands/skill.d.ts.map +1 -0
  27. package/dist/commands/skill.js +68 -0
  28. package/dist/commands/skill.js.map +1 -0
  29. package/dist/commands/tree.d.ts +11 -1
  30. package/dist/commands/tree.d.ts.map +1 -1
  31. package/dist/commands/tree.js +18 -6
  32. package/dist/commands/tree.js.map +1 -1
  33. package/dist/config/load.d.ts +9 -0
  34. package/dist/config/load.d.ts.map +1 -1
  35. package/dist/config/load.js +27 -1
  36. package/dist/config/load.js.map +1 -1
  37. package/dist/config/rule-problems.d.ts +11 -0
  38. package/dist/config/rule-problems.d.ts.map +1 -0
  39. package/dist/config/rule-problems.js +31 -0
  40. package/dist/config/rule-problems.js.map +1 -0
  41. package/dist/config/write.d.ts.map +1 -1
  42. package/dist/config/write.js +29 -9
  43. package/dist/config/write.js.map +1 -1
  44. package/dist/detectors/index.d.ts.map +1 -1
  45. package/dist/detectors/index.js +3 -0
  46. package/dist/detectors/index.js.map +1 -1
  47. package/dist/detectors/orthography.d.ts +9 -0
  48. package/dist/detectors/orthography.d.ts.map +1 -0
  49. package/dist/detectors/orthography.js +51 -0
  50. package/dist/detectors/orthography.js.map +1 -0
  51. package/dist/document.d.ts +3 -0
  52. package/dist/document.d.ts.map +1 -1
  53. package/dist/document.js +13 -3
  54. package/dist/document.js.map +1 -1
  55. package/dist/eval.d.ts +5 -1
  56. package/dist/eval.d.ts.map +1 -1
  57. package/dist/eval.js +10 -5
  58. package/dist/eval.js.map +1 -1
  59. package/dist/genre.d.ts +1 -1
  60. package/dist/genre.d.ts.map +1 -1
  61. package/dist/genre.js +3 -3
  62. package/dist/genre.js.map +1 -1
  63. package/dist/heading-text.d.ts +14 -0
  64. package/dist/heading-text.d.ts.map +1 -0
  65. package/dist/heading-text.js +28 -0
  66. package/dist/heading-text.js.map +1 -0
  67. package/dist/init.d.ts +2 -1
  68. package/dist/init.d.ts.map +1 -1
  69. package/dist/init.js +62 -13
  70. package/dist/init.js.map +1 -1
  71. package/dist/orthography.d.ts +21 -0
  72. package/dist/orthography.d.ts.map +1 -0
  73. package/dist/orthography.js +110 -0
  74. package/dist/orthography.js.map +1 -0
  75. package/dist/plugin.d.ts +6 -0
  76. package/dist/plugin.d.ts.map +1 -1
  77. package/dist/reasons.d.ts +15 -0
  78. package/dist/reasons.d.ts.map +1 -0
  79. package/dist/reasons.js +29 -0
  80. package/dist/reasons.js.map +1 -0
  81. package/dist/render/explain.d.ts.map +1 -1
  82. package/dist/render/explain.js +36 -12
  83. package/dist/render/explain.js.map +1 -1
  84. package/dist/render/friendly.d.ts.map +1 -1
  85. package/dist/render/friendly.js +32 -9
  86. package/dist/render/friendly.js.map +1 -1
  87. package/dist/render/rules-json.d.ts.map +1 -1
  88. package/dist/render/rules-json.js +57 -27
  89. package/dist/render/rules-json.js.map +1 -1
  90. package/dist/render/summary.d.ts +3 -2
  91. package/dist/render/summary.d.ts.map +1 -1
  92. package/dist/render/summary.js +24 -9
  93. package/dist/render/summary.js.map +1 -1
  94. package/dist/render/suppressions.d.ts +2 -1
  95. package/dist/render/suppressions.d.ts.map +1 -1
  96. package/dist/render/suppressions.js +34 -10
  97. package/dist/render/suppressions.js.map +1 -1
  98. package/dist/run.d.ts +3 -1
  99. package/dist/run.d.ts.map +1 -1
  100. package/dist/run.js +22 -13
  101. package/dist/run.js.map +1 -1
  102. package/dist/structure/build.d.ts.map +1 -1
  103. package/dist/structure/build.js +19 -10
  104. package/dist/structure/build.js.map +1 -1
  105. package/dist/structure/cite.d.ts.map +1 -1
  106. package/dist/structure/cite.js +3 -1
  107. package/dist/structure/cite.js.map +1 -1
  108. package/dist/structure/issues.d.ts +5 -2
  109. package/dist/structure/issues.d.ts.map +1 -1
  110. package/dist/structure/issues.js +27 -9
  111. package/dist/structure/issues.js.map +1 -1
  112. package/dist/structure/numbering.d.ts.map +1 -1
  113. package/dist/structure/numbering.js +11 -6
  114. package/dist/structure/numbering.js.map +1 -1
  115. package/dist/ui.d.ts +14 -0
  116. package/dist/ui.d.ts.map +1 -0
  117. package/dist/ui.js +14 -0
  118. package/dist/ui.js.map +1 -0
  119. package/dist/watch.d.ts +1 -1
  120. package/dist/watch.d.ts.map +1 -1
  121. package/dist/watch.js +2 -2
  122. package/dist/watch.js.map +1 -1
  123. package/package.json +3 -2
  124. package/rules/latin-spacing.yaml +27 -0
  125. package/rules/preferred-term.yaml +27 -0
  126. package/skills/chaff/SKILL.md +61 -0
  127. package/src/adapter-load.ts +41 -10
  128. package/src/cli-args.ts +9 -0
  129. package/src/cli-text.ts +182 -0
  130. package/src/cli.ts +100 -89
  131. package/src/commands/cite.ts +51 -18
  132. package/src/commands/eval.ts +1 -0
  133. package/src/commands/skill.ts +74 -0
  134. package/src/commands/tree.ts +27 -7
  135. package/src/config/load.ts +43 -1
  136. package/src/config/rule-problems.ts +43 -0
  137. package/src/config/write.ts +33 -9
  138. package/src/detectors/index.ts +3 -0
  139. package/src/detectors/orthography.ts +55 -0
  140. package/src/document.ts +23 -4
  141. package/src/eval.ts +16 -5
  142. package/src/genre.ts +4 -4
  143. package/src/heading-text.ts +26 -0
  144. package/src/init.ts +74 -15
  145. package/src/orthography.ts +115 -0
  146. package/src/plugin.ts +6 -0
  147. package/src/reasons.ts +44 -0
  148. package/src/render/explain.ts +46 -12
  149. package/src/render/friendly.ts +42 -10
  150. package/src/render/rules-json.ts +45 -10
  151. package/src/render/summary.ts +34 -9
  152. package/src/render/suppressions.ts +45 -10
  153. package/src/run.ts +35 -12
  154. package/src/structure/build.ts +20 -10
  155. package/src/structure/cite.ts +3 -1
  156. package/src/structure/issues.ts +33 -14
  157. package/src/structure/numbering.ts +12 -7
  158. package/src/ui.ts +23 -0
  159. package/src/watch.ts +2 -2
@@ -0,0 +1,27 @@
1
+ id: latin-spacing
2
+ layer: L3
3
+ status: experimental
4
+ how_to_find: latin-spacing
5
+ languages: [ja]
6
+ severity: warning
7
+
8
+ name:
9
+ ja: 英数字の前後の空白がそろっていない
10
+ en: Spacing around Latin text is inconsistent
11
+
12
+ why:
13
+ ja: 日本語と英字・数字のあいだを空けるか詰めるかは、どちらでも読めます。困るのは一つの文書の中で混ざることで、読み手には違いに意味があるように見えます。chaff はどちらが正しいかを決めず、文書の中で少ないほうを指摘します。英字と、数字の前、数字の後ろは別々に数えます。「を 3回」のように、数字の前は空けて助数詞とは詰める書き方がふつうにあるからです。「3GBの」のように数字に続く単位は、英字ではなく数字の側として数えます。
14
+ en: Whether to put a space between Japanese and Latin letters or digits is a choice either way. What reads badly is mixing both in one document, which suggests the difference means something. chaff does not decide which is right; it points at whichever the document uses less. Letters, the space before a digit and the space after one are counted separately, since spacing before a number but not before its counter is common.
15
+
16
+ message:
17
+ ja: "日本語と{kind}のあいだを{style}(この文書はふつう{usual}。{of} 箇所のうち {count} 箇所が違う)"
18
+ en: "Spacing between Japanese and {kind} differs here ({count} of {of})"
19
+
20
+ how_to_fix:
21
+ ja: 文書の多いほうの書き方に合わせてください。チームで決めているなら、STYLE.md に書いておくと迷いません。
22
+ en: Match the way the rest of the document does it. If the team has decided, write it in the style guide.
23
+
24
+ # 何箇所違えば指摘するか。1 は「1 箇所でも混ざっていれば」。
25
+ levels: { strict: 1, normal: 1, relaxed: 3 }
26
+
27
+ use_for: [business, blog, technical]
@@ -0,0 +1,27 @@
1
+ id: preferred-term
2
+ layer: L2
3
+ status: experimental
4
+ how_to_find: preferred-term
5
+ word_list: preferred-term
6
+ severity: warning
7
+
8
+ name:
9
+ ja: チームの表記と違う
10
+ en: Not the team's spelling
11
+
12
+ why:
13
+ ja: 同じものを「サーバー」とも「サーバ」とも書くと、読み手は別のものかと迷い、検索でも片方を取りこぼします。どちらに寄せるかはチームごとに違うので、chaff.yaml の prefer に「使わない書き方」と「使う書き方」の組を自分で並べてください。並べていなければ何も言いません。
14
+ en: Writing the same thing two ways ("e-mail" and "email") makes a reader wonder whether they differ, and a search misses one of them. Which way wins is the team's choice, so list pairs (the spelling to avoid, then the one to use) under `prefer` in chaff.yaml. With nothing listed, this rule says nothing.
15
+
16
+ message:
17
+ ja: 「{matched}」は「{preferred}」と書きます
18
+ en: 'Write "{preferred}", not "{matched}"'
19
+
20
+ how_to_fix:
21
+ ja: 「{preferred}」に直してください。固有名詞など、そのままにすべき箇所なら stet で理由を添えて黙らせます。
22
+ en: Change it to "{preferred}". Where it must stay (a product's own name, a quotation), silence it with stet and the reason.
23
+
24
+ # チームが並べた書き方なので、1 つでも指摘する。並べた時点で「使わない」と決めている。
25
+ levels: { strict: 1, normal: 1, relaxed: 3 }
26
+
27
+ use_for: [business, blog, technical]
@@ -0,0 +1,61 @@
1
+ ---
2
+ name: chaff
3
+ description: "Check prose with chaff (npx chaffjs): run it on Markdown or text, read each finding, fix the text or record why not, and tune chaff.yaml with rules --json and relax --why. Use when asked to lint, proofread or tighten documents, or to set up a team's writing rules."
4
+ ---
5
+
6
+ # Checking writing with chaff
7
+
8
+ chaff finds what makes writing hard to read — long sentences, padding, mixed styles, broken references in a
9
+ contract — and never rewrites anything. Reading the findings and changing the text is your part. Japanese and
10
+ English documents both work; the language is detected per file.
11
+
12
+ ## Run it
13
+
14
+ ```sh
15
+ npx chaffjs <file|dir|glob>... # human-readable, one block per finding (a folder or glob picks up .md / .markdown / .mdx; name a .txt file directly)
16
+ npx chaffjs . --compact # two lines per finding: line:col severity message, then the rule id
17
+ npx chaffjs <file> --experimental # also the experimental rules
18
+ ```
19
+
20
+ A run exits 1 when a finding is an `error`, and also when it could not check anything (no file to check, an
21
+ unknown `--genre`, no package for the language) — so a clean exit means the files were checked. After the findings comes the list of rules that did not run, and why (the default output only; `--compact`
22
+ gives just their number): read it before saying a document is clean. A run over several files prints only the files that have findings, so to call one file clean,
23
+ run chaff on that file alone and read its list.
24
+
25
+ ## For each finding, choose one of three
26
+
27
+ | | When | How |
28
+ | --- | --- | --- |
29
+ | Fix the text | The finding is right | Edit the sentence. Keep the meaning, the facts and the numbers; change only how it is said. |
30
+ | Silence that spot | Right in general, wrong here (a quotation, a term that must repeat) | `<!-- stet: rule-id — reason -->` before it; `stet-section` / `stet-file` for wider scope. Always give the reason. |
31
+ | Change the rule | Wrong for this team or genre | `npx chaffjs relax <rule-id> --why "reason"` (or `strict` / `off`). The reason and date are written into `chaff.yaml`. |
32
+
33
+ `npx chaffjs explain <rule-id>` shows why a rule exists, its levels and the current one. Read it before
34
+ relaxing a rule.
35
+
36
+ ## Tune chaff.yaml
37
+
38
+ - `npx chaffjs init` writes a commented `chaff.yaml` when there is none (an existing one is left as it is), and
39
+ adds `.chaff-cache/` and `.env*` to `.gitignore`.
40
+ - `npx chaffjs rules --json` prints every rule with its levels, the setting in effect (`now`) and why a rule is
41
+ off. Read it instead of guessing rule names or limits.
42
+ - A level is `strict` / `normal` / `relaxed` / `off`; when even `relaxed` is too tight, a positive number is the
43
+ limit itself (`max-sentence-length: 260`).
44
+ - `prefer:` maps spellings to the team's (`e-mail: email`); `preferred-term: normal` turns the rule on.
45
+ - Unknown rule names and unreadable values are reported on stderr — read stderr after editing `chaff.yaml`.
46
+ - `genre:` sets the kind of document (`npx chaffjs genres` lists them); `--genre` overrides it for one run.
47
+
48
+ ## Structured documents
49
+
50
+ - `npx chaffjs tree <file>` shows a contract or specification as a tree of addresses (`3.2` for 第3条第2項 or
51
+ Section 3.2). `--experimental` adds the structure rules: references to missing provisions, numbering gaps,
52
+ terms defined twice.
53
+ - `npx chaffjs cite <source> <quotes.json>` checks that quotations (`[{ "address", "quote" }]`) are really in
54
+ the source; exit 1 if any is not. Use it to back every quotation you put in an answer or a summary.
55
+
56
+ ## Do not
57
+
58
+ - Do not change what a document says to satisfy a rule. If a finding cannot be fixed without changing the
59
+ meaning, leave it and say so.
60
+ - Do not turn a rule off to make a run pass; relax it with a reason the team would accept.
61
+ - Do not report "no findings" without reading the not-run list.
@@ -59,13 +59,41 @@ const LANGUAGE_ID = /^[a-z]{2,3}$/u;
59
59
  export const packageFor = (language: string): string | undefined =>
60
60
  ADAPTER_PACKAGE[language] ?? (LANGUAGE_ID.test(language) ? `@chaffjs/lang-${language}` : undefined);
61
61
 
62
- const importAdapter = async (specifier: string, language: string): Promise<unknown> => {
62
+ /**
63
+ * 探す順。公式(`@chaffjs/lang-<言語>`)が先で、無ければ第三者の `chaff-lang-<言語>`。
64
+ * `@typescript-eslint/*` と `eslint-plugin-*` の分け方と同じで、README にもそう書いてある。同梱の言語は表のものだけ。
65
+ */
66
+ export const packagesFor = (language: string): readonly string[] => {
67
+ if (ADAPTER_PACKAGE[language] !== undefined) return [ADAPTER_PACKAGE[language]];
68
+ return LANGUAGE_ID.test(language) ? [`@chaffjs/lang-${language}`, `chaff-lang-${language}`] : [];
69
+ };
70
+
71
+ export type Importer = (specifier: string) => Promise<unknown>;
72
+
73
+ /**
74
+ * そのパッケージ自体が入っていないときだけ真。入っているパッケージが自分の依存を見つけられないときも同じ
75
+ * ERR_MODULE_NOT_FOUND になるので、Node の文言が探した名前を挙げているかで分ける。
76
+ */
77
+ export const isAbsent = (err: unknown, specifier: string): boolean =>
78
+ err instanceof Error && "code" in err && err.code === "ERR_MODULE_NOT_FOUND" && err.message.includes(`'${specifier}'`);
79
+
80
+ /** 入っている最初のものを読む。入っていないのは次へ。入っていて壊れているものは、次へ行かずにそのまま投げる。 */
81
+ const importFirst = async (specifiers: readonly string[], language: string, importer: Importer): Promise<{ specifier: string; module: unknown }> => {
82
+ const [first, ...rest] = specifiers;
83
+ if (first === undefined) throw new Error(`no adapter for language "${language}"`);
63
84
  try {
64
- return await import(specifier);
85
+ return { specifier: first, module: await importer(first) };
65
86
  } catch (err) {
66
- const missing = err instanceof Error && "code" in err && err.code === "ERR_MODULE_NOT_FOUND";
67
- if (missing) throw new Error(`言語 ${language} のパッケージ ${specifier} が入っていません(npm i -D ${specifier})`, { cause: err });
68
- throw err;
87
+ if (!isAbsent(err, first)) throw err;
88
+ if (rest.length > 0) return importFirst(rest, language, importer);
89
+ const all = packagesFor(language);
90
+ const install = `npm i -D ${all[0] ?? first}`;
91
+ // Only the requested language is known here; a Japanese message for Japanese, English for any other.
92
+ const message =
93
+ language === "ja"
94
+ ? `言語 ${language} のパッケージが入っていません(${all.join(" か ")}。${install})`
95
+ : `No package for language ${language} is installed (${all.join(" or ")}; ${install})`;
96
+ throw new Error(message, { cause: err });
69
97
  }
70
98
  };
71
99
 
@@ -74,13 +102,16 @@ const importAdapter = async (specifier: string, language: string): Promise<unkno
74
102
  * core だけで起動し、文字種で言語を当ててから、その言語のアダプタを読む。
75
103
  * 全言語を静的に import すると、使わない言語のぶんまで npx の初回取得が膨らむ。
76
104
  */
77
- export const loadAdapter = async (language: string): Promise<LanguageAdapter> => {
78
- const specifier = packageFor(language);
79
- if (specifier === undefined) throw new Error(`no adapter for language "${language}"`);
80
- const module = await importAdapter(specifier, language);
105
+ export const loadAdapter = async (language: string, importer: Importer = (specifier) => import(specifier)): Promise<LanguageAdapter> => {
106
+ const { specifier, module } = await importFirst(packagesFor(language), language, importer);
81
107
  if (typeof module !== "object" || module === null) throw new Error(`${specifier} did not export a module`);
82
108
  const candidate = pickExport(module);
83
109
  const broken = checkAdapter(candidate);
84
- if (broken !== undefined || !isAdapter(candidate)) throw new Error(`${specifier} が LanguageAdapter を export していません: ${broken ?? ""}`);
110
+ if (broken !== undefined || !isAdapter(candidate)) {
111
+ const why = broken ?? "";
112
+ throw new Error(
113
+ language === "ja" ? `${specifier} が LanguageAdapter を export していません: ${why}` : `${specifier} does not export a LanguageAdapter: ${why}`,
114
+ );
115
+ }
85
116
  return candidate;
86
117
  };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Options of the commands that take files to check, whose value is the next argument. That value is not a file.
3
+ * `--why` (relax), `--format` and `--language` (tree) belong to commands that read their own arguments.
4
+ */
5
+ export const VALUE_FLAGS: readonly string[] = ["--sarif", "--genre", "--rule"];
6
+
7
+ /** The arguments that name what to check: everything that is neither an option nor an option's value. */
8
+ export const targetsOf = (args: readonly string[]): string[] =>
9
+ args.filter((arg, index) => !arg.startsWith("--") && !(index > 0 && VALUE_FLAGS.includes(args[index - 1] ?? "")));
@@ -0,0 +1,182 @@
1
+ // What the command line says, in Japanese and English. Per-file text follows the file's language; the rest
2
+ // follows hostLanguage (chaff.yaml's language, else the terminal's locale).
3
+ import type { Texts } from "./ui.ts";
4
+
5
+ /** Where a file's genre came from, as the header names it. */
6
+ export type GenreSource = "--genre" | "by_path" | "config" | "default" | "front-matter" | "path" | "content";
7
+
8
+ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます。文章は書き換えません。
9
+
10
+ chaff <file|dir|glob>... 検査する(設定も API key も要りません)
11
+ chaff . この場所の Markdown を全部
12
+ chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
13
+ 判定役は chaff.yaml の ai_backend で選びます
14
+ chaff init chaff.yaml を作る
15
+ chaff eval <dir> 手元の文書で閾値を測り直す
16
+ chaff explain <rule> そのルールの意図と根拠を読む
17
+ chaff genres ジャンルの一覧
18
+ chaff tree <file> [--format sexp|json] 文書を番地の付いた木にする(条・項・定義・参照)
19
+ chaff cite <原文> <引用.json> 回答の引用(番地と引用文)が原文にあるかを確かめる
20
+ chaff rules --json いまの設定を JSON で出す(AI に渡す用)
21
+ chaff baseline <dir> いまある指摘を棚上げする(既存の repo に入れるとき)
22
+ chaff suppressions <dir> stet で黙らせている指摘を数える
23
+ chaff relax|strict|off <rule> [--why "理由"]
24
+ chaff skill [--global] Claude Code の skill を入れる(.claude/skills/chaff/、--global で ~/.claude/)
25
+
26
+ --compact エンジニア向けの 1 行形式
27
+ --experimental 試験中の rule も動かす
28
+ --genre <ジャンル> この実行だけジャンルを決める(chaff.yaml より優先)
29
+ --show-baseline 棚上げした分も含めて全部見る
30
+ --watch 保存のたびに見直し、変わったところだけ出す
31
+ --dry-run test で、何を AI に送るかだけを見る(API を呼びません)
32
+ --sarif <path> 指摘を SARIF で書き出す(GitHub の PR の行に出すため)
33
+
34
+ この箇所だけ黙らせる: <!-- stet: rule-id — 理由 -->
35
+
36
+ 値は strict / normal / relaxed / off の 4 つから選びます。
37
+ `;
38
+
39
+ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewrites the text.
40
+
41
+ chaff <file|dir|glob>... check (no config or API key needed)
42
+ chaff . every Markdown file here
43
+ chaff test <file|dir>... also run the checks that read meaning (needs an API key)
44
+ the judge is chosen by ai_backend in chaff.yaml
45
+ chaff init create chaff.yaml
46
+ chaff eval <dir> re-measure the limits on your own documents
47
+ chaff explain <rule> read what a rule is for and why
48
+ chaff genres list the genres
49
+ chaff tree <file> [--format sexp|json] the document as a tree of addresses (sections, clauses, definitions, references)
50
+ chaff cite <source> <quotes.json> check that quoted passages (address and text) are in the source
51
+ chaff rules --json the current settings as JSON (to give to an AI)
52
+ chaff baseline <dir> shelve today's findings (when adding chaff to an existing repository)
53
+ chaff suppressions <dir> count the findings silenced with stet
54
+ chaff relax|strict|off <rule> [--why "reason"]
55
+ chaff skill [--global] install the Claude Code skill (.claude/skills/chaff/; --global for ~/.claude/)
56
+
57
+ --compact one line per finding, for engineers
58
+ --experimental run the experimental rules too
59
+ --genre <genre> the genre for this run only (wins over chaff.yaml)
60
+ --show-baseline show the shelved findings too
61
+ --watch re-check on every save and print only what changed
62
+ --dry-run with test, show what would be sent to the AI (no API call)
63
+ --sarif <path> write the findings as SARIF (for comments on a GitHub PR)
64
+
65
+ Silence one spot: <!-- stet: rule-id — reason -->
66
+
67
+ A rule's level is one of strict / normal / relaxed / off.
68
+ `;
69
+
70
+ export type CliText = {
71
+ readonly usage: string;
72
+ readonly languageName: (language: string) => string;
73
+ readonly genreSource: Readonly<Record<GenreSource, string>>;
74
+ readonly header: (path: string, genre: string, language: string, from: string, shelved: number, hushed: number) => string;
75
+ readonly noMarkdown: (targets: string) => string;
76
+ readonly noMarkdownHere: string;
77
+ readonly noAdapter: (language: string) => string;
78
+ readonly unknownGenre: (genre: string) => string;
79
+ readonly unknownRule: (id: string) => string;
80
+ readonly unknownRuleWithList: (id: string, list: string) => string;
81
+ readonly unnamed: string;
82
+ readonly sarifWritten: (path: string, count: number) => string;
83
+ readonly unchanged: string;
84
+ readonly findingsUnit: string;
85
+ readonly watching: (files: number, findings: number) => string;
86
+ readonly watchHint: string;
87
+ readonly baselineDone: (files: number, entries: number, file: string) => readonly string[];
88
+ readonly genres: (list: readonly string[]) => string;
89
+ readonly unit: (ruleId: string, language: string) => string;
90
+ };
91
+
92
+ export const CLI_TEXT: Texts<CliText> = {
93
+ ja: {
94
+ usage: USAGE_JA,
95
+ languageName: (language) => (language === "ja" ? "日本語" : "英語"),
96
+ genreSource: {
97
+ "--genre": "--genre",
98
+ by_path: "chaff.yaml の by_path",
99
+ config: "chaff.yaml",
100
+ default: "既定",
101
+ "front-matter": "front matter",
102
+ path: "パス",
103
+ content: "内容",
104
+ },
105
+ header: (path, genre, language, from, shelved, hushed) =>
106
+ [`${path} ${genre} · ${language} ジャンルは${from}から`, shelved > 0 ? ` 棚上げ ${shelved} 件` : "", hushed > 0 ? ` stet ${hushed} 件` : ""].join(
107
+ "",
108
+ ),
109
+ noMarkdown: (targets) => `Markdown が 1 つも見つかりませんでした: ${targets}`,
110
+ noMarkdownHere: "Markdown が 1 つも見つかりませんでした。",
111
+ noAdapter: (language) => `言語 "${language}" のアダプタがありません。`,
112
+ unknownGenre: (genre) => `ジャンル "${genre}" はありません。npx chaff genres で一覧が出ます。`,
113
+ unknownRule: (id) => `${id} というルールはありません。npx chaff rules --json で一覧が出ます。`,
114
+ unknownRuleWithList: (id, list) => `${id} というルールはありません。\n一覧:\n${list}`,
115
+ unnamed: "(名前なし)",
116
+ sarifWritten: (path, count) => `\n SARIF を書きました: ${path}(${count} 件)`,
117
+ unchanged: "変わりませんでした",
118
+ findingsUnit: " 件",
119
+ watching: (files, findings) => `\n ${files} ファイルを見ています。いまの指摘は ${findings} 件です。`,
120
+ watchHint: " 保存するたびに、変わったところだけ出します。止めるには Ctrl-C。\n",
121
+ baselineDone: (files, entries, file) => [
122
+ "",
123
+ ` ${files} ファイルを走査しました。`,
124
+ "",
125
+ ` ${entries} 件の指摘を ${file} に記録しました。`,
126
+ " 以後、これらは報告されません。新しく増えたものだけが出ます。",
127
+ "",
128
+ ` ${file} を commit してください。`,
129
+ "",
130
+ ],
131
+ genres: (list) =>
132
+ ["", " 使えるジャンル:", ...list.map((genre) => ` ${genre}`), "", " chaff.yaml の genre に書くか、--genre で指定します。", ""].join("\n"),
133
+ unit: (ruleId, language) => {
134
+ if (ruleId === "bold-density") return "1000 字あたりの箇所数";
135
+ if (ruleId !== "max-sentence-length") return "回";
136
+ return language === "ja" ? "文字" : "語";
137
+ },
138
+ },
139
+ en: {
140
+ usage: USAGE_EN,
141
+ languageName: (language) => (language === "ja" ? "Japanese" : "English"),
142
+ genreSource: {
143
+ "--genre": "--genre",
144
+ by_path: "by_path in chaff.yaml",
145
+ config: "chaff.yaml",
146
+ default: "the default",
147
+ "front-matter": "front matter",
148
+ path: "the path",
149
+ content: "the content",
150
+ },
151
+ header: (path, genre, language, from, shelved, hushed) =>
152
+ [`${path} ${genre} · ${language} genre from ${from}`, shelved > 0 ? ` ${shelved} shelved` : "", hushed > 0 ? ` ${hushed} stet` : ""].join(""),
153
+ noMarkdown: (targets) => `No Markdown files found: ${targets}`,
154
+ noMarkdownHere: "No Markdown files found.",
155
+ noAdapter: (language) => `No language package for "${language}".`,
156
+ unknownGenre: (genre) => `There is no genre "${genre}". npx chaff genres lists them.`,
157
+ unknownRule: (id) => `There is no rule named ${id}. npx chaff rules --json lists them.`,
158
+ unknownRuleWithList: (id, list) => `There is no rule named ${id}.\nRules:\n${list}`,
159
+ unnamed: "(no name)",
160
+ sarifWritten: (path, count) => `\n Wrote SARIF: ${path} (${count} finding${count === 1 ? "" : "s"})`,
161
+ unchanged: "no change",
162
+ findingsUnit: " findings",
163
+ watching: (files, findings) => `\n Watching ${files} file${files === 1 ? "" : "s"}. ${findings} finding${findings === 1 ? "" : "s"} now.`,
164
+ watchHint: " Each save prints only what changed. Ctrl-C to stop.\n",
165
+ baselineDone: (files, entries, file) => [
166
+ "",
167
+ ` Checked ${files} file${files === 1 ? "" : "s"}.`,
168
+ "",
169
+ ` Recorded ${entries} finding${entries === 1 ? "" : "s"} in ${file}.`,
170
+ " They will not be reported again; only new ones will.",
171
+ "",
172
+ ` Commit ${file}.`,
173
+ "",
174
+ ],
175
+ genres: (list) => ["", " Genres:", ...list.map((genre) => ` ${genre}`), "", " Set one with genre in chaff.yaml, or with --genre.", ""].join("\n"),
176
+ unit: (ruleId, language) => {
177
+ if (ruleId === "bold-density") return "places per 1000 characters";
178
+ if (ruleId !== "max-sentence-length") return "times";
179
+ return language === "ja" ? "characters" : "words";
180
+ },
181
+ },
182
+ };