chaffjs 0.7.0 → 0.9.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 (215) 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/address-chain.d.ts +9 -0
  7. package/dist/address-chain.d.ts.map +1 -0
  8. package/dist/address-chain.js +105 -0
  9. package/dist/address-chain.js.map +1 -0
  10. package/dist/cli-args.d.ts +8 -0
  11. package/dist/cli-args.d.ts.map +1 -0
  12. package/dist/cli-args.js +8 -0
  13. package/dist/cli-args.js.map +1 -0
  14. package/dist/cli-text.d.ts +26 -0
  15. package/dist/cli-text.d.ts.map +1 -0
  16. package/dist/cli-text.js +155 -0
  17. package/dist/cli-text.js.map +1 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +94 -87
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/cite.d.ts +2 -1
  23. package/dist/commands/cite.d.ts.map +1 -1
  24. package/dist/commands/cite.js +39 -18
  25. package/dist/commands/cite.js.map +1 -1
  26. package/dist/commands/eval.d.ts.map +1 -1
  27. package/dist/commands/eval.js +2 -1
  28. package/dist/commands/eval.js.map +1 -1
  29. package/dist/commands/feedback.d.ts +35 -0
  30. package/dist/commands/feedback.d.ts.map +1 -0
  31. package/dist/commands/feedback.js +157 -0
  32. package/dist/commands/feedback.js.map +1 -0
  33. package/dist/commands/skill.d.ts +22 -0
  34. package/dist/commands/skill.d.ts.map +1 -0
  35. package/dist/commands/skill.js +68 -0
  36. package/dist/commands/skill.js.map +1 -0
  37. package/dist/commands/test.d.ts.map +1 -1
  38. package/dist/commands/test.js +3 -2
  39. package/dist/commands/test.js.map +1 -1
  40. package/dist/commands/tree.d.ts +11 -1
  41. package/dist/commands/tree.d.ts.map +1 -1
  42. package/dist/commands/tree.js +21 -7
  43. package/dist/commands/tree.js.map +1 -1
  44. package/dist/config/by-path.d.ts +5 -1
  45. package/dist/config/by-path.d.ts.map +1 -1
  46. package/dist/config/by-path.js +1 -4
  47. package/dist/config/by-path.js.map +1 -1
  48. package/dist/config/load.d.ts +2 -0
  49. package/dist/config/load.d.ts.map +1 -1
  50. package/dist/config/load.js +3 -1
  51. package/dist/config/load.js.map +1 -1
  52. package/dist/config/rule-problems.d.ts +2 -1
  53. package/dist/config/rule-problems.d.ts.map +1 -1
  54. package/dist/config/rule-problems.js +17 -6
  55. package/dist/config/rule-problems.js.map +1 -1
  56. package/dist/config/write.d.ts.map +1 -1
  57. package/dist/config/write.js +29 -9
  58. package/dist/config/write.js.map +1 -1
  59. package/dist/detectors/char-shape.d.ts.map +1 -1
  60. package/dist/detectors/char-shape.js +3 -2
  61. package/dist/detectors/char-shape.js.map +1 -1
  62. package/dist/document.d.ts +2 -2
  63. package/dist/document.d.ts.map +1 -1
  64. package/dist/document.js +4 -2
  65. package/dist/document.js.map +1 -1
  66. package/dist/feedback/draft.d.ts +28 -0
  67. package/dist/feedback/draft.d.ts.map +1 -0
  68. package/dist/feedback/draft.js +66 -0
  69. package/dist/feedback/draft.js.map +1 -0
  70. package/dist/feedback/excerpt.d.ts +10 -0
  71. package/dist/feedback/excerpt.d.ts.map +1 -0
  72. package/dist/feedback/excerpt.js +20 -0
  73. package/dist/feedback/excerpt.js.map +1 -0
  74. package/dist/genre.d.ts +1 -1
  75. package/dist/genre.d.ts.map +1 -1
  76. package/dist/genre.js +3 -3
  77. package/dist/genre.js.map +1 -1
  78. package/dist/init.d.ts +2 -1
  79. package/dist/init.d.ts.map +1 -1
  80. package/dist/init.js +62 -13
  81. package/dist/init.js.map +1 -1
  82. package/dist/legal-address.d.ts +2 -0
  83. package/dist/legal-address.d.ts.map +1 -0
  84. package/dist/legal-address.js +16 -0
  85. package/dist/legal-address.js.map +1 -0
  86. package/dist/orthography.d.ts.map +1 -1
  87. package/dist/orthography.js +15 -0
  88. package/dist/orthography.js.map +1 -1
  89. package/dist/plugin.d.ts +94 -0
  90. package/dist/plugin.d.ts.map +1 -1
  91. package/dist/profile/for-file.d.ts +7 -0
  92. package/dist/profile/for-file.d.ts.map +1 -0
  93. package/dist/profile/for-file.js +6 -0
  94. package/dist/profile/for-file.js.map +1 -0
  95. package/dist/profile/load.d.ts +4 -0
  96. package/dist/profile/load.d.ts.map +1 -0
  97. package/dist/profile/load.js +25 -0
  98. package/dist/profile/load.js.map +1 -0
  99. package/dist/profile/parse.d.ts +19 -0
  100. package/dist/profile/parse.d.ts.map +1 -0
  101. package/dist/profile/parse.js +82 -0
  102. package/dist/profile/parse.js.map +1 -0
  103. package/dist/profile/select.d.ts +22 -0
  104. package/dist/profile/select.d.ts.map +1 -0
  105. package/dist/profile/select.js +21 -0
  106. package/dist/profile/select.js.map +1 -0
  107. package/dist/reasons.d.ts +16 -0
  108. package/dist/reasons.d.ts.map +1 -0
  109. package/dist/reasons.js +31 -0
  110. package/dist/reasons.js.map +1 -0
  111. package/dist/render/explain.d.ts.map +1 -1
  112. package/dist/render/explain.js +36 -12
  113. package/dist/render/explain.js.map +1 -1
  114. package/dist/render/friendly.d.ts.map +1 -1
  115. package/dist/render/friendly.js +32 -9
  116. package/dist/render/friendly.js.map +1 -1
  117. package/dist/render/rules-json.d.ts.map +1 -1
  118. package/dist/render/rules-json.js +47 -27
  119. package/dist/render/rules-json.js.map +1 -1
  120. package/dist/render/summary.d.ts +3 -2
  121. package/dist/render/summary.d.ts.map +1 -1
  122. package/dist/render/summary.js +24 -9
  123. package/dist/render/summary.js.map +1 -1
  124. package/dist/render/suppressions.d.ts +2 -1
  125. package/dist/render/suppressions.d.ts.map +1 -1
  126. package/dist/render/suppressions.js +34 -10
  127. package/dist/render/suppressions.js.map +1 -1
  128. package/dist/run.d.ts.map +1 -1
  129. package/dist/run.js +41 -12
  130. package/dist/run.js.map +1 -1
  131. package/dist/structure/build.d.ts +2 -1
  132. package/dist/structure/build.d.ts.map +1 -1
  133. package/dist/structure/build.js +102 -9
  134. package/dist/structure/build.js.map +1 -1
  135. package/dist/structure/cite.d.ts.map +1 -1
  136. package/dist/structure/cite.js +3 -1
  137. package/dist/structure/cite.js.map +1 -1
  138. package/dist/structure/issues.d.ts +5 -2
  139. package/dist/structure/issues.d.ts.map +1 -1
  140. package/dist/structure/issues.js +35 -10
  141. package/dist/structure/issues.js.map +1 -1
  142. package/dist/structure/numbering.d.ts.map +1 -1
  143. package/dist/structure/numbering.js +11 -6
  144. package/dist/structure/numbering.js.map +1 -1
  145. package/dist/structure/of.d.ts +2 -1
  146. package/dist/structure/of.d.ts.map +1 -1
  147. package/dist/structure/of.js +7 -1
  148. package/dist/structure/of.js.map +1 -1
  149. package/dist/structure/relative-find.d.ts +6 -0
  150. package/dist/structure/relative-find.d.ts.map +1 -0
  151. package/dist/structure/relative-find.js +179 -0
  152. package/dist/structure/relative-find.js.map +1 -0
  153. package/dist/structure/relative-resolve.d.ts +3 -0
  154. package/dist/structure/relative-resolve.d.ts.map +1 -0
  155. package/dist/structure/relative-resolve.js +175 -0
  156. package/dist/structure/relative-resolve.js.map +1 -0
  157. package/dist/structure/unread.d.ts +8 -0
  158. package/dist/structure/unread.d.ts.map +1 -0
  159. package/dist/structure/unread.js +34 -0
  160. package/dist/structure/unread.js.map +1 -0
  161. package/dist/ui.d.ts +14 -0
  162. package/dist/ui.d.ts.map +1 -0
  163. package/dist/ui.js +14 -0
  164. package/dist/ui.js.map +1 -0
  165. package/dist/watch.d.ts +1 -1
  166. package/dist/watch.d.ts.map +1 -1
  167. package/dist/watch.js +2 -2
  168. package/dist/watch.js.map +1 -1
  169. package/package.json +5 -3
  170. package/profiles/statute.yaml +62 -0
  171. package/skills/chaff/SKILL.md +81 -0
  172. package/src/adapter-load.ts +41 -10
  173. package/src/address-chain.ts +113 -0
  174. package/src/cli-args.ts +9 -0
  175. package/src/cli-text.ts +186 -0
  176. package/src/cli.ts +111 -90
  177. package/src/commands/cite.ts +51 -18
  178. package/src/commands/eval.ts +2 -1
  179. package/src/commands/feedback.ts +210 -0
  180. package/src/commands/skill.ts +74 -0
  181. package/src/commands/test.ts +3 -2
  182. package/src/commands/tree.ts +30 -8
  183. package/src/config/by-path.ts +11 -3
  184. package/src/config/load.ts +5 -1
  185. package/src/config/rule-problems.ts +24 -5
  186. package/src/config/write.ts +33 -9
  187. package/src/detectors/char-shape.ts +5 -4
  188. package/src/document.ts +11 -3
  189. package/src/feedback/draft.ts +102 -0
  190. package/src/feedback/excerpt.ts +23 -0
  191. package/src/genre.ts +4 -4
  192. package/src/init.ts +74 -15
  193. package/src/orthography.ts +15 -0
  194. package/src/plugin.ts +88 -0
  195. package/src/profile/for-file.ts +11 -0
  196. package/src/profile/load.ts +26 -0
  197. package/src/profile/parse.ts +106 -0
  198. package/src/profile/select.ts +35 -0
  199. package/src/reasons.ts +49 -0
  200. package/src/render/explain.ts +46 -12
  201. package/src/render/friendly.ts +42 -10
  202. package/src/render/rules-json.ts +35 -9
  203. package/src/render/summary.ts +34 -9
  204. package/src/render/suppressions.ts +45 -10
  205. package/src/run.ts +44 -11
  206. package/src/structure/build.ts +128 -12
  207. package/src/structure/cite.ts +3 -1
  208. package/src/structure/issues.ts +42 -15
  209. package/src/structure/numbering.ts +12 -7
  210. package/src/structure/of.ts +15 -3
  211. package/src/structure/relative-find.ts +224 -0
  212. package/src/structure/relative-resolve.ts +209 -0
  213. package/src/structure/unread.ts +39 -0
  214. package/src/ui.ts +23 -0
  215. package/src/watch.ts +2 -2
package/src/cli.ts CHANGED
@@ -16,6 +16,7 @@ import { runEval } from "./commands/eval.ts";
16
16
  import { runTest } from "./commands/test.ts";
17
17
  import { frontMatterGenre, guessGenre } from "./genre.ts";
18
18
  import { GENRES, runInit } from "./init.ts";
19
+ import { targetsOf } from "./cli-args.ts";
19
20
  import { loadRules } from "./rule-load.ts";
20
21
  import { renderCompact } from "./render/compact.ts";
21
22
  import { renderExplain } from "./render/explain.ts";
@@ -23,63 +24,52 @@ import { renderFriendly } from "./render/friendly.ts";
23
24
  import { rulesJson } from "./render/rules-json.ts";
24
25
  import { renderSarif } from "./render/sarif.ts";
25
26
  import { VERSION } from "./version.ts";
26
- import { runTree, treeTargets } from "./commands/tree.ts";
27
+ import { runTree, treeTargets, type TreeContext } from "./commands/tree.ts";
27
28
  import { citeTargets, runCite } from "./commands/cite.ts";
29
+ import { runSkill } from "./commands/skill.ts";
30
+ import { runFeedback, settingsOf } from "./commands/feedback.ts";
31
+ import { homedir } from "node:os";
28
32
  import { ruleProblems } from "./config/rule-problems.ts";
29
33
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
30
34
  import { neededBy, runRules } from "./run.ts";
31
35
  import type { Level, RuleDefinition } from "./plugin.ts";
36
+ import { CLI_TEXT, type CliText, type GenreSource } from "./cli-text.ts";
37
+ import { hostLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
38
+ import { profileFor } from "./profile/for-file.ts";
32
39
 
33
- const USAGE = `chaff — 文章の読みにくいところを見つけます。文章は書き換えません。
34
-
35
- chaff <file|dir|glob>... 検査する(設定も API key も要りません)
36
- chaff . この場所の Markdown を全部
37
- chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
38
- 判定役は chaff.yaml の ai_backend で選びます
39
- chaff init chaff.yaml を作る
40
- chaff eval <dir> 手元の文書で閾値を測り直す
41
- chaff explain <rule> そのルールの意図と根拠を読む
42
- chaff genres ジャンルの一覧
43
- chaff tree <file> [--format sexp|json] 文書を番地の付いた木にする(条・項・定義・参照)
44
- chaff cite <原文> <引用.json> 回答の引用(番地と引用文)が原文にあるかを確かめる
45
- chaff rules --json いまの設定を JSON で出す(AI に渡す用)
46
- chaff baseline <dir> いまある指摘を棚上げする(既存の repo に入れるとき)
47
- chaff suppressions <dir> stet で黙らせている指摘を数える
48
- chaff relax|strict|off <rule> [--why "理由"]
49
-
50
- --compact エンジニア向けの 1 行形式
51
- --experimental 試験中の rule も動かす
52
- --show-baseline 棚上げした分も含めて全部見る
53
- --watch 保存のたびに見直し、変わったところだけ出す
54
- --dry-run test で、何を AI に送るかだけを見る(API を呼びません)
55
- --sarif <path> 指摘を SARIF で書き出す(GitHub の PR の行に出すため)
56
-
57
- この箇所だけ黙らせる: <!-- stet: rule-id — 理由 -->
58
-
59
- 値は strict / normal / relaxed / off の 4 つから選びます。
60
- `;
40
+ /** Text for output that is not about one document. */
41
+ const hostText = (config: Config): CliText => CLI_TEXT[hostLanguage(config.language, process.env)];
61
42
 
62
43
  const readConfig = (): Config => (existsSync(join(process.cwd(), CONFIG_FILE)) ? loadConfig(join(process.cwd(), CONFIG_FILE)) : EMPTY);
63
44
 
64
- const resolveGenre = (path: string, source: string, config: Config): { genre: string; from: string } => {
65
- // パスごとの上書きが最優先。「全体はこう、ここだけは違う」を書けるようにする。
45
+ const resolveGenre = (path: string, source: string, config: Config, cliGenre?: string): { genre: string; from: GenreSource } => {
46
+ // コマンドで指定したものが最優先。その実行だけの指定だから、設定より強い。
47
+ if (cliGenre !== undefined) return { genre: cliGenre, from: "--genre" };
48
+ // パスごとの上書きが次。「全体はこう、ここだけは違う」を書けるようにする。
66
49
  const override = applyByPath(config.byPath, config.baseDir, path);
67
- if (override.genre !== undefined) return { genre: override.genre, from: `${CONFIG_FILE} の by_path` };
68
- if (config.genre !== undefined) return { genre: config.genre, from: CONFIG_FILE };
50
+ if (override.genre !== undefined) return { genre: override.genre, from: "by_path" };
51
+ if (config.genre !== undefined) return { genre: config.genre, from: "config" };
69
52
  const guess = guessGenre(path, source, frontMatterGenre(source));
70
- return guess === undefined ? { genre: "blog/tech", from: "既定" } : { genre: guess.genre, from: guess.from };
53
+ return guess === undefined ? { genre: "blog/tech", from: "default" } : { genre: guess.genre, from: guess.from };
71
54
  };
72
55
 
56
+ /** resolveGenre with this run's --genre, for the commands that take it as a dependency. */
57
+ const genreFrom =
58
+ (argv: readonly string[]) =>
59
+ (path: string, source: string, config: Config): { genre: string; from: GenreSource } =>
60
+ resolveGenre(path, source, config, flag(argv, "--genre"));
61
+
73
62
  const findRule = (rules: readonly RuleDefinition[], id: string | undefined): RuleDefinition | undefined => rules.find((rule) => rule.id === id);
74
63
 
75
64
  const changeSetting = (command: Level, ruleId: string | undefined, why: string | undefined): number => {
76
65
  const config = readConfig();
77
- const rule = findRule(loadRules(config.language ?? "ja"), ruleId);
66
+ const language = config.language ?? hostLanguage(undefined, process.env);
67
+ const rule = findRule(loadRules(language), ruleId);
78
68
  if (rule === undefined) {
79
- console.error(`${ruleId ?? "(名前なし)"} というルールはありません。npx chaff rules --json で一覧が出ます。`);
69
+ console.error(hostText(config).unknownRule(ruleId ?? hostText(config).unnamed));
80
70
  return 1;
81
71
  }
82
- const outcome = applyLevel(join(process.cwd(), CONFIG_FILE), rule, command, why, config.language ?? "ja", process.env["USER"] ?? "unknown");
72
+ const outcome = applyLevel(join(process.cwd(), CONFIG_FILE), rule, command, why, language, process.env["USER"] ?? "unknown");
83
73
  console.log(outcome.message);
84
74
  return outcome.ok ? 0 : 1;
85
75
  };
@@ -94,26 +84,27 @@ type Inspected = {
94
84
  readonly rules: readonly RuleDefinition[];
95
85
  /** ファイルごとの言語。by_path で 1 つの repo に 2 言語が混ざる。 */
96
86
  readonly language: string;
87
+ readonly genre: string;
97
88
  readonly outcome: FileOutcome;
98
89
  readonly perFile: PerFile;
99
90
  readonly all: readonly string[];
100
91
  };
101
92
 
102
- const headerFor = (path: string, genre: string, from: string, language: string, shelved: number, hushed: number): string => {
103
- const shelf = shelved > 0 ? ` 棚上げ ${shelved} 件` : "";
104
- const stet = hushed > 0 ? ` stet ${hushed} 件` : "";
105
- return `${path} ${genre} \u00b7 ${language === "ja" ? "日本語" : "英語"} ジャンルは${from}から${shelf}${stet}`;
93
+ /** The first line for a file, in the file's language. */
94
+ const headerFor = (path: string, genre: string, from: GenreSource, language: string, shelved: number, hushed: number): string => {
95
+ const text = CLI_TEXT[uiLanguageOf(language)];
96
+ return text.header(path, genre, text.languageName(language), text.genreSource[from], shelved, hushed);
106
97
  };
107
98
 
108
99
  const inspect = async (path: string, config: Config, argv: readonly string[]): Promise<Inspected> => {
109
100
  const source = await readFile(path, "utf8");
110
101
  const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
111
102
  const adapter = await loadAdapter(language);
112
- const { genre, from } = resolveGenre(path, source, config);
103
+ const { genre, from } = resolveGenre(path, source, config, flag(argv, "--genre"));
113
104
  const rules = loadRules(language);
114
105
  const experimental = config.experimental || argv.includes("--experimental");
115
106
  await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
116
- const doc = buildDocument(path, source, adapter, teamRules(config));
107
+ const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
117
108
  const raw = runRules(doc, rules, config.rules, experimental, genre, config.limits);
118
109
  // 応答は 3 つ。stet で黙らせたものは、ここで落とす。
119
110
  const applied = applySuppressions(
@@ -130,6 +121,7 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
130
121
  text,
131
122
  rules,
132
123
  language,
124
+ genre,
133
125
  outcome: { path, findings: split.fresh, notRun: raw.skipped.length },
134
126
  perFile: { path, suppressed: applied.suppressed, reasonless: applied.unusedReasonless },
135
127
  all: applied.kept.map((finding) => fingerprint(path, finding)),
@@ -149,32 +141,42 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[]): voi
149
141
  );
150
142
  mkdirSync(dirname(path), { recursive: true });
151
143
  writeFileSync(path, renderSarif(located, VERSION), "utf8");
152
- console.log(`\n SARIF を書きました: ${path}(${located.length} 件)`);
144
+ console.log(hostText(readConfig()).sarifWritten(path, located.length));
153
145
  };
154
146
 
155
147
  /** 効いていない設定は、結果の前に一度だけ言う。標準エラーに出すので、JSON や SARIF の出力は汚さない。 */
156
148
  const warnRuleProblems = (config: Config, language: string): void => {
157
- ruleProblems(config, loadRules(language)).forEach((problem) => console.error(`chaff: ${problem}`));
149
+ ruleProblems(config, loadRules(language), hostLanguage(config.language, process.env)).forEach((problem) => console.error(`chaff: ${problem}`));
150
+ };
151
+
152
+ /** Several files end with one summary: in their language when they share one, else the host's. */
153
+ const summaryLanguage = (results: readonly Inspected[], config: Config): UiLanguage => {
154
+ const languages = new Set(results.map((result) => uiLanguageOf(result.language)));
155
+ const [only] = [...languages];
156
+ return languages.size === 1 && only !== undefined ? only : hostLanguage(config.language, process.env);
158
157
  };
159
158
 
160
159
  const lint = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
161
160
  const paths = collectTargets(targets);
161
+ const config = readConfig();
162
162
  if (paths.length === 0) {
163
163
  // 0 件を成功にすると「CI は通っているが何も検証していない」状態が続く。§14。
164
- console.error(`Markdown が 1 つも見つかりませんでした: ${targets.join(", ")}`);
164
+ console.error(hostText(config).noMarkdown(targets.join(", ")));
165
165
  return 1;
166
166
  }
167
- const config = readConfig();
168
167
  const language = config.language ?? "ja";
169
168
  if (packageFor(language) === undefined) {
170
- console.error(`言語 "${language}" のアダプタがありません。`);
169
+ console.error(hostText(config).noAdapter(language));
171
170
  return 1;
172
171
  }
173
172
  warnRuleProblems(config, language);
174
173
  const results = await Promise.all(paths.map((path) => inspect(path, config, argv)));
175
174
  writeSarif(results, argv);
176
175
  results.filter((result) => result.outcome.findings.length > 0 || paths.length === 1).forEach((result) => console.log(result.text));
177
- renderSummary(results.map((result) => result.outcome)).forEach((line) => console.log(line));
176
+ renderSummary(
177
+ results.map((result) => result.outcome),
178
+ summaryLanguage(results, config),
179
+ ).forEach((line) => console.log(line));
178
180
  return results.some((result) => result.outcome.findings.some((finding) => finding.severity === "error")) ? 1 : 0;
179
181
  };
180
182
 
@@ -184,11 +186,12 @@ const lint = async (targets: readonly string[], argv: readonly string[]): Promis
184
186
  */
185
187
  const runWatch = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
186
188
  const paths = collectTargets(targets);
189
+ const config = readConfig();
190
+ const text = hostText(config);
187
191
  if (paths.length === 0) {
188
- console.error(`Markdown が 1 つも見つかりませんでした: ${targets.join(", ")}`);
192
+ console.error(text.noMarkdown(targets.join(", ")));
189
193
  return 1;
190
194
  }
191
- const config = readConfig();
192
195
  warnRuleProblems(config, config.language ?? "ja");
193
196
  const seen = new Map<string, Snapshot>();
194
197
  const once = async (path: string, first: boolean): Promise<void> => {
@@ -197,13 +200,13 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
197
200
  const before = seen.get(path) ?? {};
198
201
  seen.set(path, after);
199
202
  if (first) return;
200
- const change = describeChange(before, after);
201
- console.log(`${clock()} ${path} ${change ?? "変わりませんでした"}`);
203
+ const change = describeChange(before, after, text.findingsUnit);
204
+ console.log(`${clock()} ${path} ${change ?? text.unchanged}`);
202
205
  };
203
206
  await Promise.all(paths.map((path) => once(path, true)));
204
207
  const totals = [...seen.values()].reduce((sum, snapshot) => sum + Object.values(snapshot).reduce((inner, count) => inner + count, 0), 0);
205
- console.log(`\n ${paths.length} ファイルを見ています。いまの指摘は ${totals} 件です。`);
206
- console.log(" 保存するたびに、変わったところだけ出します。止めるには Ctrl-C。\n");
208
+ console.log(text.watching(paths.length, totals));
209
+ console.log(text.watchHint);
207
210
  const stop = watchPaths(paths, (path) => void once(path, false));
208
211
  process.on("SIGINT", () => {
209
212
  stop();
@@ -214,49 +217,33 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
214
217
 
215
218
  const explain = (ruleId: string | undefined): number => {
216
219
  const config = readConfig();
217
- const language = config.language ?? "ja";
220
+ // The rule's limits differ by language (characters for Japanese, words for English): explain in the one being written.
221
+ const language = config.language ?? hostLanguage(undefined, process.env);
222
+ const text = CLI_TEXT[uiLanguageOf(language)];
218
223
  const rules = loadRules(language);
219
224
  const rule = findRule(rules, ruleId);
220
225
  if (rule === undefined) {
221
226
  const list = rules.map((entry) => ` ${entry.id}`).join("\n");
222
- console.error(`${ruleId ?? "(名前なし)"} というルールはありません。\n一覧:\n${list}`);
227
+ console.error(text.unknownRuleWithList(ruleId ?? text.unnamed, list));
223
228
  return 1;
224
229
  }
225
230
  const current = config.rules[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
226
- console.log(renderExplain(rule, current, language, unitOf(rule.id, language), config.genre));
231
+ console.log(renderExplain(rule, current, language, text.unit(rule.id, language), config.genre));
227
232
  return 0;
228
233
  };
229
234
 
230
- /** 閾値の単位。explain で「4 とは何の 4 か」を示すため。 */
231
- const unitOf = (ruleId: string, language: string): string => {
232
- if (ruleId === "bold-density") return "1000 字あたりの箇所数";
233
- if (ruleId !== "max-sentence-length") return "回";
234
- return language === "ja" ? "文字" : "語";
235
- };
236
-
237
235
  const runBaseline = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
238
236
  const paths = collectTargets(targets.length > 0 ? targets : ["."]);
237
+ const config = readConfig();
239
238
  if (paths.length === 0) {
240
- console.error("Markdown が 1 つも見つかりませんでした。");
239
+ console.error(hostText(config).noMarkdownHere);
241
240
  return 1;
242
241
  }
243
- const config = readConfig();
244
242
  const results = await Promise.all(paths.map((path) => inspect(path, config, [...argv, "--show-baseline"])));
245
243
  const entries = results.flatMap((result) => result.all);
246
244
  const file = join(process.cwd(), BASELINE_FILE);
247
245
  writeBaseline(file, entries);
248
- console.log(
249
- [
250
- "",
251
- ` ${paths.length} ファイルを走査しました。`,
252
- "",
253
- ` ${entries.length} 件の指摘を ${BASELINE_FILE} に記録しました。`,
254
- " 以後、これらは報告されません。新しく増えたものだけが出ます。",
255
- "",
256
- ` ${BASELINE_FILE} を commit してください。`,
257
- "",
258
- ].join("\n"),
259
- );
246
+ console.log(hostText(config).baselineDone(paths.length, entries.length, BASELINE_FILE).join("\n"));
260
247
  return 0;
261
248
  };
262
249
 
@@ -264,56 +251,90 @@ const runSuppressions = async (targets: readonly string[], argv: readonly string
264
251
  const paths = collectTargets(targets.length > 0 ? targets : ["."]);
265
252
  const config = readConfig();
266
253
  const results = await Promise.all(paths.map((path) => inspect(path, config, [...argv, "--show-baseline"])));
267
- console.log(renderSuppressions(results.map((result) => result.perFile)));
254
+ console.log(
255
+ renderSuppressions(
256
+ results.map((result) => result.perFile),
257
+ hostLanguage(config.language, process.env),
258
+ ),
259
+ );
268
260
  return 0;
269
261
  };
270
262
 
271
263
  type Handler = (argv: readonly string[]) => number | Promise<number>;
272
264
 
273
265
  /** `--` で始まらない引数。対象のパス。 */
274
- const positional = (argv: readonly string[]): string[] => argv.slice(1).filter((arg) => !arg.startsWith("--"));
266
+ const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
275
267
 
276
268
  const showRules = (): number => {
277
269
  const config = readConfig();
278
- const language = config.language ?? "ja";
270
+ const language = config.language ?? hostLanguage(undefined, process.env);
279
271
  warnRuleProblems(config, language);
280
272
  console.log(rulesJson(loadRules(language), config, language, config.genre ?? "blog/tech"));
281
273
  return 0;
282
274
  };
283
275
 
284
276
  const showGenres = (): number => {
285
- console.log(["", " 使えるジャンル:", ...GENRES.map((genre) => ` ${genre}`), "", " chaff.yaml の genre に書くか、--genre で指定します。", ""].join("\n"));
277
+ console.log(hostText(readConfig()).genres(GENRES));
286
278
  return 0;
287
279
  };
288
280
 
281
+ const treeContext = (): TreeContext => {
282
+ const config = readConfig();
283
+ return { config, flag, ui: hostLanguage(config.language, process.env) };
284
+ };
285
+
289
286
  /** 分岐を数珠つなぎにせず表にする。足すときに main を太らせない。 */
290
287
  const HANDLERS: Readonly<Record<string, Handler>> = {
291
288
  init: (argv) => {
292
- runInit(process.cwd(), flag(argv, "--genre") ?? "blog/tech").forEach((line) => console.log(line));
289
+ runInit(process.cwd(), flag(argv, "--genre") ?? "blog/tech", hostLanguage(readConfig().language, process.env)).forEach((line) => console.log(line));
293
290
  return 0;
294
291
  },
295
292
  genres: showGenres,
296
293
  rules: showRules,
297
294
  explain: (argv) => explain(argv[1]),
298
- eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre, flag }),
299
- tree: (argv) => runTree(treeTargets(argv), argv, { config: readConfig(), flag }),
300
- cite: (argv) => runCite(citeTargets(argv), argv, { config: readConfig(), flag }),
301
- test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre, inspect }),
295
+ eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), flag }),
296
+ tree: (argv) => runTree(treeTargets(argv), argv, treeContext()),
297
+ cite: (argv) => runCite(citeTargets(argv), argv, treeContext()),
298
+ test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), inspect }),
302
299
  baseline: (argv) => runBaseline(positional(argv), argv),
303
300
  suppressions: (argv) => runSuppressions(positional(argv), argv),
304
301
  relax: (argv) => changeSetting("relaxed", argv[1], flag(argv, "--why")),
305
302
  strict: (argv) => changeSetting("strict", argv[1], flag(argv, "--why")),
306
303
  off: (argv) => changeSetting("off", argv[1], flag(argv, "--why")),
304
+ feedback: (argv) => {
305
+ const config = readConfig();
306
+ return runFeedback(positional(argv), argv, {
307
+ cwd: process.cwd(),
308
+ ui: hostLanguage(config.language, process.env),
309
+ version: VERSION,
310
+ runtime: `Node ${process.version} · ${process.platform} ${process.arch}`,
311
+ flag,
312
+ settingsOf: (ruleIds) => settingsOf(config, ruleIds),
313
+ check: async (path) => {
314
+ const inspected = await inspect(path, config, [...argv, "--show-baseline"]);
315
+ return { findings: inspected.outcome.findings, rules: inspected.rules, language: inspected.language, genre: inspected.genre };
316
+ },
317
+ });
318
+ },
319
+ skill: (argv) => runSkill(argv, { cwd: process.cwd(), home: homedir(), ui: hostLanguage(readConfig().language, process.env) }),
307
320
  };
308
321
 
322
+ /** Every subcommand. Anything else on the command line is a file to check. */
323
+ export const COMMANDS: readonly string[] = Object.keys(HANDLERS);
324
+
309
325
  export const main = async (argv: readonly string[]): Promise<number> => {
310
326
  const first = argv[0];
311
327
  if (first === undefined || first === "--help" || first === "-h") {
312
- console.log(USAGE);
328
+ console.log(hostText(readConfig()).usage);
313
329
  return first === undefined ? 1 : 0;
314
330
  }
331
+ const cliGenre = flag(argv, "--genre");
332
+ if (cliGenre !== undefined && !GENRES.includes(cliGenre)) {
333
+ console.error(hostText(readConfig()).unknownGenre(cliGenre));
334
+ return 1;
335
+ }
315
336
  const handler = HANDLERS[first];
316
337
  if (handler !== undefined) return handler(argv);
317
- const targets = (first === "lint" ? argv.slice(1) : argv).filter((arg) => !arg.startsWith("--"));
338
+ const targets = targetsOf(first === "lint" ? argv.slice(1) : argv);
318
339
  return argv.includes("--watch") ? runWatch(targets, argv) : lint(targets, argv);
319
340
  };
@@ -1,7 +1,7 @@
1
1
  import { checkCitations, type Citation, type CitationResult } from "../structure/cite.ts";
2
2
  import { readSource, readTree, type TreeContext } from "./tree.ts";
3
+ import type { Texts, UiLanguage } from "../ui.ts";
3
4
 
4
- const USAGE = "使い方: chaff cite <原文> <引用.json> [--format text|json] [--language ja|en|…]";
5
5
  const FORMATS = ["text", "json"];
6
6
  const QUOTE_WIDTH = 40;
7
7
  const VALUED = ["--format", "--language"];
@@ -15,37 +15,70 @@ const isCitation = (value: unknown): value is Citation => isRecord(value) && typ
15
15
 
16
16
  export type ParsedCitations = { readonly citations: readonly Citation[] } | { readonly error: string };
17
17
 
18
- const parseJson = (text: string): { readonly value: unknown } | { readonly error: string } => {
18
+ const parseJson = (text: string, ui: UiLanguage): { readonly value: unknown } | { readonly error: string } => {
19
19
  try {
20
20
  return { value: JSON.parse(text) };
21
21
  } catch (err) {
22
- return { error: `JSON として読めません: ${err instanceof Error ? err.message : String(err)}` };
22
+ return { error: TEXT[ui].notJson(err instanceof Error ? err.message : String(err)) };
23
23
  }
24
24
  };
25
25
 
26
26
  /** 引用の一覧。[{ address, quote }] の配列だけを受け取る。形が違えば理由を返す。 */
27
- export const parseCitations = (text: string): ParsedCitations => {
28
- const parsed = parseJson(text);
27
+ export const parseCitations = (text: string, ui: UiLanguage = "ja"): ParsedCitations => {
28
+ const parsed = parseJson(text, ui);
29
29
  if ("error" in parsed) return parsed;
30
- if (!Array.isArray(parsed.value)) return { error: '引用は配列で渡してください: [{ "address": "3.2", "quote": "…" }]' };
30
+ if (!Array.isArray(parsed.value)) return { error: TEXT[ui].notArray };
31
31
  const entries: readonly unknown[] = parsed.value;
32
32
  const bad = entries.findIndex((entry) => !isCitation(entry));
33
- if (bad !== -1) return { error: `${String(bad + 1)} 件目に address(文字列)と quote(文字列)がありません` };
33
+ if (bad !== -1) return { error: TEXT[ui].badEntry(bad + 1) };
34
34
  return { citations: entries.filter(isCitation).map((entry) => ({ address: entry.address, quote: entry.quote })) };
35
35
  };
36
36
 
37
- const MESSAGES: Readonly<Record<CitationResult["status"], (result: CitationResult) => string>> = {
38
- ok: () => "一致",
39
- "missing-address": (result) => `番地 ${result.citation.address} は原文にありません`,
40
- "quote-elsewhere": (result) => `引用文は ${result.citation.address} ではなく ${result.foundAt ?? "番地の外"}(${String(result.line ?? "?")} 行目)にあります`,
41
- "quote-not-found": () => "引用文が原文のどこにもありません",
37
+ type CiteText = {
38
+ readonly usage: string;
39
+ readonly quoted: (text: string) => string;
40
+ readonly notJson: (why: string) => string;
41
+ readonly notArray: string;
42
+ readonly badEntry: (n: number) => string;
43
+ readonly results: Readonly<Record<CitationResult["status"], (result: CitationResult) => string>>;
42
44
  };
43
45
 
44
- const describe = (result: CitationResult): string => {
46
+ const TEXT: Texts<CiteText> = {
47
+ ja: {
48
+ quoted: (text) => `「${text}」`,
49
+ usage: "使い方: chaff cite <原文> <引用.json> [--format text|json] [--language ja|en|…]",
50
+ notJson: (why) => `JSON として読めません: ${why}`,
51
+ notArray: '引用は配列で渡してください: [{ "address": "3.2", "quote": "…" }]',
52
+ badEntry: (n) => `${String(n)} 件目に address(文字列)と quote(文字列)がありません`,
53
+ results: {
54
+ ok: () => "一致",
55
+ "missing-address": (result) => `番地 ${result.citation.address} は原文にありません`,
56
+ "quote-elsewhere": (result) =>
57
+ `引用文は ${result.citation.address} ではなく ${result.foundAt ?? "番地の外"}(${String(result.line ?? "?")} 行目)にあります`,
58
+ "quote-not-found": () => "引用文が原文のどこにもありません",
59
+ },
60
+ },
61
+ en: {
62
+ quoted: (text) => ` "${text}"`,
63
+ usage: "usage: chaff cite <source> <quotes.json> [--format text|json] [--language ja|en|…]",
64
+ notJson: (why) => `Not valid JSON: ${why}`,
65
+ notArray: 'Give the quotations as an array: [{ "address": "3.2", "quote": "…" }]',
66
+ badEntry: (n) => `Entry ${String(n)} needs address (a string) and quote (a string)`,
67
+ results: {
68
+ ok: () => "matches",
69
+ "missing-address": (result) => `address ${result.citation.address} is not in the source`,
70
+ "quote-elsewhere": (result) =>
71
+ `the quotation is not at ${result.citation.address} but at ${result.foundAt ?? "no address"} (line ${String(result.line ?? "?")})`,
72
+ "quote-not-found": () => "the quotation is nowhere in the source",
73
+ },
74
+ },
75
+ };
76
+
77
+ const describe = (result: CitationResult, text: CiteText): string => {
45
78
  const mark = result.status === "ok" ? "✓" : "✗";
46
79
  const oneLine = result.citation.quote.replace(/\s+/gu, " ").trim();
47
80
  const quote = oneLine.length > QUOTE_WIDTH ? `${oneLine.slice(0, QUOTE_WIDTH)}…` : oneLine;
48
- return `${mark} ${result.citation.address}「${quote}」: ${MESSAGES[result.status](result)}`;
81
+ return `${mark} ${result.citation.address}${text.quoted(quote)}: ${text.results[result.status](result)}`;
49
82
  };
50
83
 
51
84
  /**
@@ -56,12 +89,12 @@ export const runCite = async (targets: readonly string[], argv: readonly string[
56
89
  const [sourcePath, citationsPath] = targets;
57
90
  const format = context.flag(argv, "--format") ?? "text";
58
91
  if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.includes(format)) {
59
- console.error(USAGE);
92
+ console.error(TEXT[context.ui ?? "ja"].usage);
60
93
  return 1;
61
94
  }
62
- const citationsText = await readSource(citationsPath);
95
+ const citationsText = await readSource(citationsPath, context);
63
96
  if (citationsText === undefined) return 1;
64
- const parsed = parseCitations(citationsText);
97
+ const parsed = parseCitations(citationsText, context.ui ?? "ja");
65
98
  if ("error" in parsed) {
66
99
  console.error(`${citationsPath}: ${parsed.error}`);
67
100
  return 1;
@@ -69,6 +102,6 @@ export const runCite = async (targets: readonly string[], argv: readonly string[
69
102
  const read = await readTree(sourcePath, argv, context);
70
103
  if (read === undefined) return 1;
71
104
  const results = checkCitations(read.source, read.tree, parsed.citations);
72
- console.log(format === "json" ? JSON.stringify(results, null, 2) : results.map(describe).join("\n"));
105
+ console.log(format === "json" ? JSON.stringify(results, null, 2) : results.map((result) => describe(result, TEXT[context.ui ?? "ja"])).join("\n"));
73
106
  return results.every((result) => result.status === "ok") ? 0 : 1;
74
107
  };
@@ -9,6 +9,7 @@ import { evaluate } from "../eval.ts";
9
9
  import { renderEval } from "../render/eval.ts";
10
10
  import { neededBy } from "../run.ts";
11
11
  import type { Config } from "../config/load.ts";
12
+ import { profileFor } from "../profile/for-file.ts";
12
13
 
13
14
  export type Context = {
14
15
  readonly config: Config;
@@ -35,7 +36,7 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
35
36
  const adapter = await loadAdapter(language);
36
37
  const { genre } = resolveGenre(path, source, config);
37
38
  await adapter.prepare?.(neededBy(loadRules(language), config.rules, config.experimental, genre, language));
38
- return { doc: buildDocument(path, source, adapter, teamRules(config)), language, genre };
39
+ return { doc: buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language)), language, genre };
39
40
  }),
40
41
  );
41
42
  const language = docs[0]?.language ?? "ja";