chaffjs 0.7.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.
- package/README.md +11 -4
- package/dist/adapter-load.d.ts +12 -1
- package/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +37 -13
- package/dist/adapter-load.js.map +1 -1
- package/dist/cli-args.d.ts +8 -0
- package/dist/cli-args.d.ts.map +1 -0
- package/dist/cli-args.js +8 -0
- package/dist/cli-args.js.map +1 -0
- package/dist/cli-text.d.ts +26 -0
- package/dist/cli-text.d.ts.map +1 -0
- package/dist/cli-text.js +151 -0
- package/dist/cli-text.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +75 -86
- package/dist/cli.js.map +1 -1
- package/dist/commands/cite.d.ts +2 -1
- package/dist/commands/cite.d.ts.map +1 -1
- package/dist/commands/cite.js +39 -18
- package/dist/commands/cite.js.map +1 -1
- package/dist/commands/skill.d.ts +22 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +68 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/tree.d.ts +11 -1
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +18 -6
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/rule-problems.d.ts +2 -1
- package/dist/config/rule-problems.d.ts.map +1 -1
- package/dist/config/rule-problems.js +17 -6
- package/dist/config/rule-problems.js.map +1 -1
- package/dist/config/write.d.ts.map +1 -1
- package/dist/config/write.js +29 -9
- package/dist/config/write.js.map +1 -1
- package/dist/genre.d.ts +1 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +3 -3
- package/dist/genre.js.map +1 -1
- package/dist/init.d.ts +2 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +62 -13
- package/dist/init.js.map +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/reasons.d.ts +15 -0
- package/dist/reasons.d.ts.map +1 -0
- package/dist/reasons.js +29 -0
- package/dist/reasons.js.map +1 -0
- package/dist/render/explain.d.ts.map +1 -1
- package/dist/render/explain.js +36 -12
- package/dist/render/explain.js.map +1 -1
- package/dist/render/friendly.d.ts.map +1 -1
- package/dist/render/friendly.js +32 -9
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +47 -27
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/summary.d.ts +3 -2
- package/dist/render/summary.d.ts.map +1 -1
- package/dist/render/summary.js +24 -9
- package/dist/render/summary.js.map +1 -1
- package/dist/render/suppressions.d.ts +2 -1
- package/dist/render/suppressions.d.ts.map +1 -1
- package/dist/render/suppressions.js +34 -10
- package/dist/render/suppressions.js.map +1 -1
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +18 -10
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +8 -2
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cite.d.ts.map +1 -1
- package/dist/structure/cite.js +3 -1
- package/dist/structure/cite.js.map +1 -1
- package/dist/structure/issues.d.ts +5 -2
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +27 -9
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +11 -6
- package/dist/structure/numbering.js.map +1 -1
- package/dist/ui.d.ts +14 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +14 -0
- package/dist/ui.js.map +1 -0
- package/dist/watch.d.ts +1 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +2 -2
- package/dist/watch.js.map +1 -1
- package/package.json +3 -2
- package/skills/chaff/SKILL.md +61 -0
- package/src/adapter-load.ts +41 -10
- package/src/cli-args.ts +9 -0
- package/src/cli-text.ts +182 -0
- package/src/cli.ts +91 -89
- package/src/commands/cite.ts +51 -18
- package/src/commands/skill.ts +74 -0
- package/src/commands/tree.ts +27 -7
- package/src/config/rule-problems.ts +24 -5
- package/src/config/write.ts +33 -9
- package/src/genre.ts +4 -4
- package/src/init.ts +74 -15
- package/src/plugin.ts +6 -0
- package/src/reasons.ts +44 -0
- package/src/render/explain.ts +46 -12
- package/src/render/friendly.ts +42 -10
- package/src/render/rules-json.ts +35 -9
- package/src/render/summary.ts +34 -9
- package/src/render/suppressions.ts +45 -10
- package/src/run.ts +20 -9
- package/src/structure/build.ts +10 -2
- package/src/structure/cite.ts +3 -1
- package/src/structure/issues.ts +33 -14
- package/src/structure/numbering.ts +12 -7
- package/src/ui.ts +23 -0
- package/src/watch.ts +2 -2
package/src/adapter-load.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
85
|
+
return { specifier: first, module: await importer(first) };
|
|
65
86
|
} catch (err) {
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
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 =
|
|
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))
|
|
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
|
};
|
package/src/cli-args.ts
ADDED
|
@@ -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] ?? "")));
|
package/src/cli-text.ts
ADDED
|
@@ -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
|
+
};
|
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,50 @@ 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 { homedir } from "node:os";
|
|
28
31
|
import { ruleProblems } from "./config/rule-problems.ts";
|
|
29
32
|
import { renderSummary, type FileOutcome } from "./render/summary.ts";
|
|
30
33
|
import { neededBy, runRules } from "./run.ts";
|
|
31
34
|
import type { Level, RuleDefinition } from "./plugin.ts";
|
|
35
|
+
import { CLI_TEXT, type CliText, type GenreSource } from "./cli-text.ts";
|
|
36
|
+
import { hostLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
|
|
32
37
|
|
|
33
|
-
|
|
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
|
-
`;
|
|
38
|
+
/** Text for output that is not about one document. */
|
|
39
|
+
const hostText = (config: Config): CliText => CLI_TEXT[hostLanguage(config.language, process.env)];
|
|
61
40
|
|
|
62
41
|
const readConfig = (): Config => (existsSync(join(process.cwd(), CONFIG_FILE)) ? loadConfig(join(process.cwd(), CONFIG_FILE)) : EMPTY);
|
|
63
42
|
|
|
64
|
-
const resolveGenre = (path: string, source: string, config: Config): { genre: string; from:
|
|
65
|
-
//
|
|
43
|
+
const resolveGenre = (path: string, source: string, config: Config, cliGenre?: string): { genre: string; from: GenreSource } => {
|
|
44
|
+
// コマンドで指定したものが最優先。その実行だけの指定だから、設定より強い。
|
|
45
|
+
if (cliGenre !== undefined) return { genre: cliGenre, from: "--genre" };
|
|
46
|
+
// パスごとの上書きが次。「全体はこう、ここだけは違う」を書けるようにする。
|
|
66
47
|
const override = applyByPath(config.byPath, config.baseDir, path);
|
|
67
|
-
if (override.genre !== undefined) return { genre: override.genre, from:
|
|
68
|
-
if (config.genre !== undefined) return { genre: config.genre, from:
|
|
48
|
+
if (override.genre !== undefined) return { genre: override.genre, from: "by_path" };
|
|
49
|
+
if (config.genre !== undefined) return { genre: config.genre, from: "config" };
|
|
69
50
|
const guess = guessGenre(path, source, frontMatterGenre(source));
|
|
70
|
-
return guess === undefined ? { genre: "blog/tech", from: "
|
|
51
|
+
return guess === undefined ? { genre: "blog/tech", from: "default" } : { genre: guess.genre, from: guess.from };
|
|
71
52
|
};
|
|
72
53
|
|
|
54
|
+
/** resolveGenre with this run's --genre, for the commands that take it as a dependency. */
|
|
55
|
+
const genreFrom =
|
|
56
|
+
(argv: readonly string[]) =>
|
|
57
|
+
(path: string, source: string, config: Config): { genre: string; from: GenreSource } =>
|
|
58
|
+
resolveGenre(path, source, config, flag(argv, "--genre"));
|
|
59
|
+
|
|
73
60
|
const findRule = (rules: readonly RuleDefinition[], id: string | undefined): RuleDefinition | undefined => rules.find((rule) => rule.id === id);
|
|
74
61
|
|
|
75
62
|
const changeSetting = (command: Level, ruleId: string | undefined, why: string | undefined): number => {
|
|
76
63
|
const config = readConfig();
|
|
77
|
-
const
|
|
64
|
+
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
65
|
+
const rule = findRule(loadRules(language), ruleId);
|
|
78
66
|
if (rule === undefined) {
|
|
79
|
-
console.error(
|
|
67
|
+
console.error(hostText(config).unknownRule(ruleId ?? hostText(config).unnamed));
|
|
80
68
|
return 1;
|
|
81
69
|
}
|
|
82
|
-
const outcome = applyLevel(join(process.cwd(), CONFIG_FILE), rule, command, why,
|
|
70
|
+
const outcome = applyLevel(join(process.cwd(), CONFIG_FILE), rule, command, why, language, process.env["USER"] ?? "unknown");
|
|
83
71
|
console.log(outcome.message);
|
|
84
72
|
return outcome.ok ? 0 : 1;
|
|
85
73
|
};
|
|
@@ -99,17 +87,17 @@ type Inspected = {
|
|
|
99
87
|
readonly all: readonly string[];
|
|
100
88
|
};
|
|
101
89
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
return
|
|
90
|
+
/** The first line for a file, in the file's language. */
|
|
91
|
+
const headerFor = (path: string, genre: string, from: GenreSource, language: string, shelved: number, hushed: number): string => {
|
|
92
|
+
const text = CLI_TEXT[uiLanguageOf(language)];
|
|
93
|
+
return text.header(path, genre, text.languageName(language), text.genreSource[from], shelved, hushed);
|
|
106
94
|
};
|
|
107
95
|
|
|
108
96
|
const inspect = async (path: string, config: Config, argv: readonly string[]): Promise<Inspected> => {
|
|
109
97
|
const source = await readFile(path, "utf8");
|
|
110
98
|
const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
|
|
111
99
|
const adapter = await loadAdapter(language);
|
|
112
|
-
const { genre, from } = resolveGenre(path, source, config);
|
|
100
|
+
const { genre, from } = resolveGenre(path, source, config, flag(argv, "--genre"));
|
|
113
101
|
const rules = loadRules(language);
|
|
114
102
|
const experimental = config.experimental || argv.includes("--experimental");
|
|
115
103
|
await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
|
|
@@ -149,32 +137,42 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[]): voi
|
|
|
149
137
|
);
|
|
150
138
|
mkdirSync(dirname(path), { recursive: true });
|
|
151
139
|
writeFileSync(path, renderSarif(located, VERSION), "utf8");
|
|
152
|
-
console.log(
|
|
140
|
+
console.log(hostText(readConfig()).sarifWritten(path, located.length));
|
|
153
141
|
};
|
|
154
142
|
|
|
155
143
|
/** 効いていない設定は、結果の前に一度だけ言う。標準エラーに出すので、JSON や SARIF の出力は汚さない。 */
|
|
156
144
|
const warnRuleProblems = (config: Config, language: string): void => {
|
|
157
|
-
ruleProblems(config, loadRules(language)).forEach((problem) => console.error(`chaff: ${problem}`));
|
|
145
|
+
ruleProblems(config, loadRules(language), hostLanguage(config.language, process.env)).forEach((problem) => console.error(`chaff: ${problem}`));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** Several files end with one summary: in their language when they share one, else the host's. */
|
|
149
|
+
const summaryLanguage = (results: readonly Inspected[], config: Config): UiLanguage => {
|
|
150
|
+
const languages = new Set(results.map((result) => uiLanguageOf(result.language)));
|
|
151
|
+
const [only] = [...languages];
|
|
152
|
+
return languages.size === 1 && only !== undefined ? only : hostLanguage(config.language, process.env);
|
|
158
153
|
};
|
|
159
154
|
|
|
160
155
|
const lint = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
|
|
161
156
|
const paths = collectTargets(targets);
|
|
157
|
+
const config = readConfig();
|
|
162
158
|
if (paths.length === 0) {
|
|
163
159
|
// 0 件を成功にすると「CI は通っているが何も検証していない」状態が続く。§14。
|
|
164
|
-
console.error(
|
|
160
|
+
console.error(hostText(config).noMarkdown(targets.join(", ")));
|
|
165
161
|
return 1;
|
|
166
162
|
}
|
|
167
|
-
const config = readConfig();
|
|
168
163
|
const language = config.language ?? "ja";
|
|
169
164
|
if (packageFor(language) === undefined) {
|
|
170
|
-
console.error(
|
|
165
|
+
console.error(hostText(config).noAdapter(language));
|
|
171
166
|
return 1;
|
|
172
167
|
}
|
|
173
168
|
warnRuleProblems(config, language);
|
|
174
169
|
const results = await Promise.all(paths.map((path) => inspect(path, config, argv)));
|
|
175
170
|
writeSarif(results, argv);
|
|
176
171
|
results.filter((result) => result.outcome.findings.length > 0 || paths.length === 1).forEach((result) => console.log(result.text));
|
|
177
|
-
renderSummary(
|
|
172
|
+
renderSummary(
|
|
173
|
+
results.map((result) => result.outcome),
|
|
174
|
+
summaryLanguage(results, config),
|
|
175
|
+
).forEach((line) => console.log(line));
|
|
178
176
|
return results.some((result) => result.outcome.findings.some((finding) => finding.severity === "error")) ? 1 : 0;
|
|
179
177
|
};
|
|
180
178
|
|
|
@@ -184,11 +182,12 @@ const lint = async (targets: readonly string[], argv: readonly string[]): Promis
|
|
|
184
182
|
*/
|
|
185
183
|
const runWatch = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
|
|
186
184
|
const paths = collectTargets(targets);
|
|
185
|
+
const config = readConfig();
|
|
186
|
+
const text = hostText(config);
|
|
187
187
|
if (paths.length === 0) {
|
|
188
|
-
console.error(
|
|
188
|
+
console.error(text.noMarkdown(targets.join(", ")));
|
|
189
189
|
return 1;
|
|
190
190
|
}
|
|
191
|
-
const config = readConfig();
|
|
192
191
|
warnRuleProblems(config, config.language ?? "ja");
|
|
193
192
|
const seen = new Map<string, Snapshot>();
|
|
194
193
|
const once = async (path: string, first: boolean): Promise<void> => {
|
|
@@ -197,13 +196,13 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
|
|
|
197
196
|
const before = seen.get(path) ?? {};
|
|
198
197
|
seen.set(path, after);
|
|
199
198
|
if (first) return;
|
|
200
|
-
const change = describeChange(before, after);
|
|
201
|
-
console.log(`${clock()} ${path} ${change ??
|
|
199
|
+
const change = describeChange(before, after, text.findingsUnit);
|
|
200
|
+
console.log(`${clock()} ${path} ${change ?? text.unchanged}`);
|
|
202
201
|
};
|
|
203
202
|
await Promise.all(paths.map((path) => once(path, true)));
|
|
204
203
|
const totals = [...seen.values()].reduce((sum, snapshot) => sum + Object.values(snapshot).reduce((inner, count) => inner + count, 0), 0);
|
|
205
|
-
console.log(
|
|
206
|
-
console.log(
|
|
204
|
+
console.log(text.watching(paths.length, totals));
|
|
205
|
+
console.log(text.watchHint);
|
|
207
206
|
const stop = watchPaths(paths, (path) => void once(path, false));
|
|
208
207
|
process.on("SIGINT", () => {
|
|
209
208
|
stop();
|
|
@@ -214,49 +213,33 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
|
|
|
214
213
|
|
|
215
214
|
const explain = (ruleId: string | undefined): number => {
|
|
216
215
|
const config = readConfig();
|
|
217
|
-
|
|
216
|
+
// The rule's limits differ by language (characters for Japanese, words for English): explain in the one being written.
|
|
217
|
+
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
218
|
+
const text = CLI_TEXT[uiLanguageOf(language)];
|
|
218
219
|
const rules = loadRules(language);
|
|
219
220
|
const rule = findRule(rules, ruleId);
|
|
220
221
|
if (rule === undefined) {
|
|
221
222
|
const list = rules.map((entry) => ` ${entry.id}`).join("\n");
|
|
222
|
-
console.error(
|
|
223
|
+
console.error(text.unknownRuleWithList(ruleId ?? text.unnamed, list));
|
|
223
224
|
return 1;
|
|
224
225
|
}
|
|
225
226
|
const current = config.rules[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
|
|
226
|
-
console.log(renderExplain(rule, current, language,
|
|
227
|
+
console.log(renderExplain(rule, current, language, text.unit(rule.id, language), config.genre));
|
|
227
228
|
return 0;
|
|
228
229
|
};
|
|
229
230
|
|
|
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
231
|
const runBaseline = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
|
|
238
232
|
const paths = collectTargets(targets.length > 0 ? targets : ["."]);
|
|
233
|
+
const config = readConfig();
|
|
239
234
|
if (paths.length === 0) {
|
|
240
|
-
console.error(
|
|
235
|
+
console.error(hostText(config).noMarkdownHere);
|
|
241
236
|
return 1;
|
|
242
237
|
}
|
|
243
|
-
const config = readConfig();
|
|
244
238
|
const results = await Promise.all(paths.map((path) => inspect(path, config, [...argv, "--show-baseline"])));
|
|
245
239
|
const entries = results.flatMap((result) => result.all);
|
|
246
240
|
const file = join(process.cwd(), BASELINE_FILE);
|
|
247
241
|
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
|
-
);
|
|
242
|
+
console.log(hostText(config).baselineDone(paths.length, entries.length, BASELINE_FILE).join("\n"));
|
|
260
243
|
return 0;
|
|
261
244
|
};
|
|
262
245
|
|
|
@@ -264,56 +247,75 @@ const runSuppressions = async (targets: readonly string[], argv: readonly string
|
|
|
264
247
|
const paths = collectTargets(targets.length > 0 ? targets : ["."]);
|
|
265
248
|
const config = readConfig();
|
|
266
249
|
const results = await Promise.all(paths.map((path) => inspect(path, config, [...argv, "--show-baseline"])));
|
|
267
|
-
console.log(
|
|
250
|
+
console.log(
|
|
251
|
+
renderSuppressions(
|
|
252
|
+
results.map((result) => result.perFile),
|
|
253
|
+
hostLanguage(config.language, process.env),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
268
256
|
return 0;
|
|
269
257
|
};
|
|
270
258
|
|
|
271
259
|
type Handler = (argv: readonly string[]) => number | Promise<number>;
|
|
272
260
|
|
|
273
261
|
/** `--` で始まらない引数。対象のパス。 */
|
|
274
|
-
const positional = (argv: readonly string[]): string[] => argv.slice(1)
|
|
262
|
+
const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
|
|
275
263
|
|
|
276
264
|
const showRules = (): number => {
|
|
277
265
|
const config = readConfig();
|
|
278
|
-
const language = config.language ??
|
|
266
|
+
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
279
267
|
warnRuleProblems(config, language);
|
|
280
268
|
console.log(rulesJson(loadRules(language), config, language, config.genre ?? "blog/tech"));
|
|
281
269
|
return 0;
|
|
282
270
|
};
|
|
283
271
|
|
|
284
272
|
const showGenres = (): number => {
|
|
285
|
-
console.log(
|
|
273
|
+
console.log(hostText(readConfig()).genres(GENRES));
|
|
286
274
|
return 0;
|
|
287
275
|
};
|
|
288
276
|
|
|
277
|
+
const treeContext = (): TreeContext => {
|
|
278
|
+
const config = readConfig();
|
|
279
|
+
return { config, flag, ui: hostLanguage(config.language, process.env) };
|
|
280
|
+
};
|
|
281
|
+
|
|
289
282
|
/** 分岐を数珠つなぎにせず表にする。足すときに main を太らせない。 */
|
|
290
283
|
const HANDLERS: Readonly<Record<string, Handler>> = {
|
|
291
284
|
init: (argv) => {
|
|
292
|
-
runInit(process.cwd(), flag(argv, "--genre") ?? "blog/tech").forEach((line) => console.log(line));
|
|
285
|
+
runInit(process.cwd(), flag(argv, "--genre") ?? "blog/tech", hostLanguage(readConfig().language, process.env)).forEach((line) => console.log(line));
|
|
293
286
|
return 0;
|
|
294
287
|
},
|
|
295
288
|
genres: showGenres,
|
|
296
289
|
rules: showRules,
|
|
297
290
|
explain: (argv) => explain(argv[1]),
|
|
298
|
-
eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre, flag }),
|
|
299
|
-
tree: (argv) => runTree(treeTargets(argv), argv,
|
|
300
|
-
cite: (argv) => runCite(citeTargets(argv), argv,
|
|
301
|
-
test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre, inspect }),
|
|
291
|
+
eval: (argv) => runEval(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), flag }),
|
|
292
|
+
tree: (argv) => runTree(treeTargets(argv), argv, treeContext()),
|
|
293
|
+
cite: (argv) => runCite(citeTargets(argv), argv, treeContext()),
|
|
294
|
+
test: (argv) => runTest(positional(argv), argv, { config: readConfig(), resolveGenre: genreFrom(argv), inspect }),
|
|
302
295
|
baseline: (argv) => runBaseline(positional(argv), argv),
|
|
303
296
|
suppressions: (argv) => runSuppressions(positional(argv), argv),
|
|
304
297
|
relax: (argv) => changeSetting("relaxed", argv[1], flag(argv, "--why")),
|
|
305
298
|
strict: (argv) => changeSetting("strict", argv[1], flag(argv, "--why")),
|
|
306
299
|
off: (argv) => changeSetting("off", argv[1], flag(argv, "--why")),
|
|
300
|
+
skill: (argv) => runSkill(argv, { cwd: process.cwd(), home: homedir(), ui: hostLanguage(readConfig().language, process.env) }),
|
|
307
301
|
};
|
|
308
302
|
|
|
303
|
+
/** Every subcommand. Anything else on the command line is a file to check. */
|
|
304
|
+
export const COMMANDS: readonly string[] = Object.keys(HANDLERS);
|
|
305
|
+
|
|
309
306
|
export const main = async (argv: readonly string[]): Promise<number> => {
|
|
310
307
|
const first = argv[0];
|
|
311
308
|
if (first === undefined || first === "--help" || first === "-h") {
|
|
312
|
-
console.log(
|
|
309
|
+
console.log(hostText(readConfig()).usage);
|
|
313
310
|
return first === undefined ? 1 : 0;
|
|
314
311
|
}
|
|
312
|
+
const cliGenre = flag(argv, "--genre");
|
|
313
|
+
if (cliGenre !== undefined && !GENRES.includes(cliGenre)) {
|
|
314
|
+
console.error(hostText(readConfig()).unknownGenre(cliGenre));
|
|
315
|
+
return 1;
|
|
316
|
+
}
|
|
315
317
|
const handler = HANDLERS[first];
|
|
316
318
|
if (handler !== undefined) return handler(argv);
|
|
317
|
-
const targets = (first === "lint" ? argv.slice(1) : argv)
|
|
319
|
+
const targets = targetsOf(first === "lint" ? argv.slice(1) : argv);
|
|
318
320
|
return argv.includes("--watch") ? runWatch(targets, argv) : lint(targets, argv);
|
|
319
321
|
};
|