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.
- 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/address-chain.d.ts +9 -0
- package/dist/address-chain.d.ts.map +1 -0
- package/dist/address-chain.js +105 -0
- package/dist/address-chain.js.map +1 -0
- 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 +155 -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 +94 -87
- 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/eval.d.ts.map +1 -1
- package/dist/commands/eval.js +2 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/feedback.d.ts +35 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +157 -0
- package/dist/commands/feedback.js.map +1 -0
- 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/test.d.ts.map +1 -1
- package/dist/commands/test.js +3 -2
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts +11 -1
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +21 -7
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/by-path.d.ts +5 -1
- package/dist/config/by-path.d.ts.map +1 -1
- package/dist/config/by-path.js +1 -4
- package/dist/config/by-path.js.map +1 -1
- package/dist/config/load.d.ts +2 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +3 -1
- package/dist/config/load.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/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +3 -2
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/document.d.ts +2 -2
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +4 -2
- package/dist/document.js.map +1 -1
- package/dist/feedback/draft.d.ts +28 -0
- package/dist/feedback/draft.d.ts.map +1 -0
- package/dist/feedback/draft.js +66 -0
- package/dist/feedback/draft.js.map +1 -0
- package/dist/feedback/excerpt.d.ts +10 -0
- package/dist/feedback/excerpt.d.ts.map +1 -0
- package/dist/feedback/excerpt.js +20 -0
- package/dist/feedback/excerpt.js.map +1 -0
- 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/legal-address.d.ts +2 -0
- package/dist/legal-address.d.ts.map +1 -0
- package/dist/legal-address.js +16 -0
- package/dist/legal-address.js.map +1 -0
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +15 -0
- package/dist/orthography.js.map +1 -1
- package/dist/plugin.d.ts +94 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/for-file.d.ts +7 -0
- package/dist/profile/for-file.d.ts.map +1 -0
- package/dist/profile/for-file.js +6 -0
- package/dist/profile/for-file.js.map +1 -0
- package/dist/profile/load.d.ts +4 -0
- package/dist/profile/load.d.ts.map +1 -0
- package/dist/profile/load.js +25 -0
- package/dist/profile/load.js.map +1 -0
- package/dist/profile/parse.d.ts +19 -0
- package/dist/profile/parse.d.ts.map +1 -0
- package/dist/profile/parse.js +82 -0
- package/dist/profile/parse.js.map +1 -0
- package/dist/profile/select.d.ts +22 -0
- package/dist/profile/select.d.ts.map +1 -0
- package/dist/profile/select.js +21 -0
- package/dist/profile/select.js.map +1 -0
- package/dist/reasons.d.ts +16 -0
- package/dist/reasons.d.ts.map +1 -0
- package/dist/reasons.js +31 -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 +41 -12
- package/dist/run.js.map +1 -1
- package/dist/structure/build.d.ts +2 -1
- package/dist/structure/build.d.ts.map +1 -1
- package/dist/structure/build.js +102 -9
- 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 +35 -10
- 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/structure/of.d.ts +2 -1
- package/dist/structure/of.d.ts.map +1 -1
- package/dist/structure/of.js +7 -1
- package/dist/structure/of.js.map +1 -1
- package/dist/structure/relative-find.d.ts +6 -0
- package/dist/structure/relative-find.d.ts.map +1 -0
- package/dist/structure/relative-find.js +179 -0
- package/dist/structure/relative-find.js.map +1 -0
- package/dist/structure/relative-resolve.d.ts +3 -0
- package/dist/structure/relative-resolve.d.ts.map +1 -0
- package/dist/structure/relative-resolve.js +175 -0
- package/dist/structure/relative-resolve.js.map +1 -0
- package/dist/structure/unread.d.ts +8 -0
- package/dist/structure/unread.d.ts.map +1 -0
- package/dist/structure/unread.js +34 -0
- package/dist/structure/unread.js.map +1 -0
- 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 +5 -3
- package/profiles/statute.yaml +62 -0
- package/skills/chaff/SKILL.md +81 -0
- package/src/adapter-load.ts +41 -10
- package/src/address-chain.ts +113 -0
- package/src/cli-args.ts +9 -0
- package/src/cli-text.ts +186 -0
- package/src/cli.ts +111 -90
- package/src/commands/cite.ts +51 -18
- package/src/commands/eval.ts +2 -1
- package/src/commands/feedback.ts +210 -0
- package/src/commands/skill.ts +74 -0
- package/src/commands/test.ts +3 -2
- package/src/commands/tree.ts +30 -8
- package/src/config/by-path.ts +11 -3
- package/src/config/load.ts +5 -1
- package/src/config/rule-problems.ts +24 -5
- package/src/config/write.ts +33 -9
- package/src/detectors/char-shape.ts +5 -4
- package/src/document.ts +11 -3
- package/src/feedback/draft.ts +102 -0
- package/src/feedback/excerpt.ts +23 -0
- package/src/genre.ts +4 -4
- package/src/init.ts +74 -15
- package/src/orthography.ts +15 -0
- package/src/plugin.ts +88 -0
- package/src/profile/for-file.ts +11 -0
- package/src/profile/load.ts +26 -0
- package/src/profile/parse.ts +106 -0
- package/src/profile/select.ts +35 -0
- package/src/reasons.ts +49 -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 +44 -11
- package/src/structure/build.ts +128 -12
- package/src/structure/cite.ts +3 -1
- package/src/structure/issues.ts +42 -15
- package/src/structure/numbering.ts +12 -7
- package/src/structure/of.ts +15 -3
- package/src/structure/relative-find.ts +224 -0
- package/src/structure/relative-resolve.ts +209 -0
- package/src/structure/unread.ts +39 -0
- package/src/ui.ts +23 -0
- 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
|
-
|
|
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:
|
|
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:
|
|
68
|
-
if (config.genre !== undefined) return { genre: config.genre, from:
|
|
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: "
|
|
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
|
|
66
|
+
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
67
|
+
const rule = findRule(loadRules(language), ruleId);
|
|
78
68
|
if (rule === undefined) {
|
|
79
|
-
console.error(
|
|
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,
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
return
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
206
|
-
console.log(
|
|
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
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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)
|
|
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 ??
|
|
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(
|
|
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,
|
|
300
|
-
cite: (argv) => runCite(citeTargets(argv), argv,
|
|
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(
|
|
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)
|
|
338
|
+
const targets = targetsOf(first === "lint" ? argv.slice(1) : argv);
|
|
318
339
|
return argv.includes("--watch") ? runWatch(targets, argv) : lint(targets, argv);
|
|
319
340
|
};
|
package/src/commands/cite.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
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}
|
|
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(
|
|
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
|
};
|
package/src/commands/eval.ts
CHANGED
|
@@ -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";
|