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/reasons.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Texts } from "./ui.ts";
|
|
2
|
+
|
|
3
|
+
/** Why a rule did not run, in the document's language. Listed under the findings so "0 findings" is never a silent pass. */
|
|
4
|
+
export type Reasons = {
|
|
5
|
+
readonly otherLanguage: (language: string) => string;
|
|
6
|
+
readonly noCapability: (capability: string) => string;
|
|
7
|
+
readonly noTags: string;
|
|
8
|
+
readonly semantic: string;
|
|
9
|
+
readonly experimental: string;
|
|
10
|
+
readonly turnedOff: string;
|
|
11
|
+
readonly noStructure: (language: string) => string;
|
|
12
|
+
readonly unreadStructure: (clauses: number, units: number) => string;
|
|
13
|
+
readonly noDetector: (name: string) => string;
|
|
14
|
+
readonly noLexicon: (language: string, list: string) => string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const CAPABILITY_NAME: Texts<Readonly<Record<string, string>>> = {
|
|
18
|
+
ja: { pos: "品詞解析", lemma: "原形" },
|
|
19
|
+
en: { pos: "part-of-speech tagging", lemma: "lemmas" },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const REASONS: Texts<Reasons> = {
|
|
23
|
+
ja: {
|
|
24
|
+
otherLanguage: (language) => `${language} 向けの rule ではないため`,
|
|
25
|
+
noCapability: (capability) => `この言語では${CAPABILITY_NAME.ja[capability] ?? capability}が使えないため`,
|
|
26
|
+
noTags: "アダプタが品詞を返さなかったため",
|
|
27
|
+
semantic: "意味を読む検査のため(npx chaff test で動きます)",
|
|
28
|
+
experimental: "まだ試験中のため",
|
|
29
|
+
turnedOff: "設定で止めているため",
|
|
30
|
+
noStructure: (language) => `${language} のパッケージは文書の構造を読めないため`,
|
|
31
|
+
unreadStructure: (clauses, units) =>
|
|
32
|
+
`条項の番号が本文に ${String(clauses)} 個あるのに、番号として読めたのは ${String(units)} 個のため(深い字下げや、行が本文につながった文書)`,
|
|
33
|
+
noDetector: (name) => `検出器 ${name} がないため`,
|
|
34
|
+
noLexicon: (language, list) => `${language} の語彙表 ${list} が無いため`,
|
|
35
|
+
},
|
|
36
|
+
en: {
|
|
37
|
+
otherLanguage: (language) => `not a rule for ${language}`,
|
|
38
|
+
noCapability: (capability) => `${CAPABILITY_NAME.en[capability] ?? capability} is not available for this language`,
|
|
39
|
+
noTags: "the language package returned no parts of speech",
|
|
40
|
+
semantic: "it reads meaning; npx chaff test runs it",
|
|
41
|
+
experimental: "still experimental",
|
|
42
|
+
turnedOff: "turned off in the settings",
|
|
43
|
+
noStructure: (language) => `the ${language} package cannot read a document's structure`,
|
|
44
|
+
unreadStructure: (clauses, units) =>
|
|
45
|
+
`the text has ${String(clauses)} clause numbers but only ${String(units)} were read as numbered lines (deep indents, or lines run into the text)`,
|
|
46
|
+
noDetector: (name) => `no detector named ${name}`,
|
|
47
|
+
noLexicon: (language, list) => `the ${language} package has no word list ${list}`,
|
|
48
|
+
},
|
|
49
|
+
};
|
package/src/render/explain.ts
CHANGED
|
@@ -1,39 +1,73 @@
|
|
|
1
1
|
import { definedLevels, resolve } from "../levels.ts";
|
|
2
2
|
import { localized } from "./text.ts";
|
|
3
|
+
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
3
4
|
import type { Level, RuleDefinition } from "../plugin.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
-
|
|
6
|
+
const TEXT: Texts<{
|
|
7
|
+
readonly off: string;
|
|
8
|
+
readonly defaultGenre: string;
|
|
9
|
+
readonly genreNote: (genre: string, others: string) => string;
|
|
10
|
+
readonly experimental: string;
|
|
11
|
+
readonly howToFix: string;
|
|
12
|
+
readonly values: (unit: string) => string;
|
|
13
|
+
readonly now: (level: string) => string;
|
|
14
|
+
readonly change: (id: string) => string;
|
|
15
|
+
}> = {
|
|
16
|
+
ja: {
|
|
17
|
+
off: "見ない",
|
|
18
|
+
defaultGenre: "既定",
|
|
19
|
+
genreNote: (genre, others) => `この数字は ${genre} のものです。ほかに ${others} で別の数字を持っています。`,
|
|
20
|
+
experimental: "このルールはまだ試験中で、既定では動きません(--experimental で動きます)。",
|
|
21
|
+
howToFix: "直しかた",
|
|
22
|
+
values: (unit) => `設定できる値(単位: ${unit}):`,
|
|
23
|
+
now: (level) => `いまは ${level} です。`,
|
|
24
|
+
change: (id) => `変える: npx chaff relax ${id} --why "理由"`,
|
|
25
|
+
},
|
|
26
|
+
en: {
|
|
27
|
+
off: "not checked",
|
|
28
|
+
defaultGenre: "the default genre",
|
|
29
|
+
genreNote: (genre, others) => `These numbers are for ${genre}. ${others} have numbers of their own.`,
|
|
30
|
+
experimental: "This rule is still experimental and does not run by default (--experimental runs it).",
|
|
31
|
+
howToFix: "How to fix",
|
|
32
|
+
values: (unit) => `Levels (unit: ${unit}):`,
|
|
33
|
+
now: (level) => `Now: ${level}.`,
|
|
34
|
+
change: (id) => `Change it: npx chaff relax ${id} --why "reason"`,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const levelLine = (rule: RuleDefinition, level: Level, current: Level, genre: string | undefined, off: string): string => {
|
|
39
|
+
const mark = level === current ? "→" : " ";
|
|
7
40
|
const name = level.padEnd(9);
|
|
8
|
-
return level === "off" ? ` ${mark} ${name}
|
|
41
|
+
return level === "off" ? ` ${mark} ${name}${off}` : ` ${mark} ${name}${resolve(rule, level, genre).limit}`;
|
|
9
42
|
};
|
|
10
43
|
|
|
11
44
|
/** ジャンルで数字が変わる rule は、それを言わないと「設定したのに効かない」に見える。 */
|
|
12
|
-
const genreNote = (rule: RuleDefinition, genre: string | undefined): string[] => {
|
|
45
|
+
const genreNote = (rule: RuleDefinition, genre: string | undefined, text: (typeof TEXT)["ja"]): string[] => {
|
|
13
46
|
const others = Object.keys(rule.by_genre).filter((key) => key !== genre);
|
|
14
47
|
if (others.length === 0) return [];
|
|
15
|
-
return ["", `
|
|
48
|
+
return ["", ` ${text.genreNote(genre ?? text.defaultGenre, others.join(" / "))}`];
|
|
16
49
|
};
|
|
17
50
|
|
|
18
51
|
/** rule の意図と根拠を読む。指摘に納得できないときの入口。 */
|
|
19
52
|
export const renderExplain = (rule: RuleDefinition, current: Level, language: string, unit: string, genre?: string): string => {
|
|
20
|
-
const
|
|
53
|
+
const text = TEXT[uiLanguageOf(language)];
|
|
54
|
+
const experimental = rule.status === "experimental" ? [` ${text.experimental}`] : [];
|
|
21
55
|
return [
|
|
22
56
|
"",
|
|
23
57
|
` ${localized(rule.name, language)} (${rule.id})`,
|
|
24
58
|
"",
|
|
25
59
|
` ${localized(rule.why, language)}`,
|
|
26
60
|
"",
|
|
27
|
-
`
|
|
61
|
+
` ${text.howToFix}: ${localized(rule.how_to_fix, language)}`,
|
|
28
62
|
"",
|
|
29
|
-
`
|
|
30
|
-
...definedLevels(rule).map((level) => levelLine(rule, level, current, genre)),
|
|
31
|
-
...genreNote(rule, genre),
|
|
63
|
+
` ${text.values(unit)}`,
|
|
64
|
+
...definedLevels(rule).map((level) => levelLine(rule, level, current, genre, text.off)),
|
|
65
|
+
...genreNote(rule, genre, text),
|
|
32
66
|
"",
|
|
33
|
-
`
|
|
67
|
+
` ${text.now(current)}`,
|
|
34
68
|
...experimental,
|
|
35
69
|
"",
|
|
36
|
-
`
|
|
70
|
+
` ${text.change(rule.id)}`,
|
|
37
71
|
"",
|
|
38
72
|
].join("\n");
|
|
39
73
|
};
|
package/src/render/friendly.ts
CHANGED
|
@@ -2,8 +2,41 @@ import type { Finding, RuleDefinition } from "../plugin.ts";
|
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { MARK, localized, messageOf } from "./text.ts";
|
|
4
4
|
import { tally } from "./summary.ts";
|
|
5
|
+
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
5
6
|
|
|
6
7
|
const RULE = 60;
|
|
8
|
+
|
|
9
|
+
const TEXT: Texts<{
|
|
10
|
+
readonly line: (n: number) => string;
|
|
11
|
+
readonly relax: string;
|
|
12
|
+
readonly machine: string;
|
|
13
|
+
readonly deterministic: string;
|
|
14
|
+
readonly untouched: string;
|
|
15
|
+
readonly forced: (n: number, ids: string) => string;
|
|
16
|
+
readonly notRun: (n: number) => string;
|
|
17
|
+
readonly because: (why: string) => string;
|
|
18
|
+
}> = {
|
|
19
|
+
ja: {
|
|
20
|
+
line: (n) => `${n} 行目`,
|
|
21
|
+
relax: "このルールをゆるめる",
|
|
22
|
+
machine: "すべて機械による判定です",
|
|
23
|
+
deterministic: " (同じ文章なら何度実行しても同じ結果になります)",
|
|
24
|
+
untouched: "文章は書き換えていません。直すのは書いた人です。",
|
|
25
|
+
forced: (n, ids) => `試験中の rule を ${n} 件、設定により有効にしています: ${ids}`,
|
|
26
|
+
notRun: (n) => `${n} 件の rule は動いていません:`,
|
|
27
|
+
because: (why) => `(${why})`,
|
|
28
|
+
},
|
|
29
|
+
en: {
|
|
30
|
+
line: (n) => `line ${n}`,
|
|
31
|
+
relax: "Relax this rule",
|
|
32
|
+
machine: "All judged by machine",
|
|
33
|
+
deterministic: " (the same text gives the same result every time)",
|
|
34
|
+
untouched: "The text was not changed. Fixing it is the writer's job.",
|
|
35
|
+
forced: (n, ids) => `${n} experimental rule${n === 1 ? "" : "s"} turned on in the settings: ${ids}`,
|
|
36
|
+
notRun: (n) => `${n} rule${n === 1 ? "" : "s"} did not run:`,
|
|
37
|
+
because: (why) => ` (${why})`,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
7
40
|
const QUOTE_LIMIT = 120;
|
|
8
41
|
|
|
9
42
|
const indent = (text: string, pad: string): string[] => text.split("\n").map((line) => `${pad}${line}`);
|
|
@@ -21,7 +54,8 @@ const quoteOf = (finding: Finding): string[] => {
|
|
|
21
54
|
*/
|
|
22
55
|
const block = (finding: Finding, rule: RuleDefinition, language: string): string[] => [
|
|
23
56
|
"",
|
|
24
|
-
|
|
57
|
+
// "12 行目" and "line 12" take the same width on a terminal, so the rule ends in the same column in both.
|
|
58
|
+
`─── ${TEXT[uiLanguageOf(language)].line(finding.line)} ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
|
|
25
59
|
...quoteOf(finding),
|
|
26
60
|
"",
|
|
27
61
|
` ${MARK[finding.severity] ?? "·"} ${localized(rule.name, language)}`,
|
|
@@ -31,7 +65,7 @@ const block = (finding: Finding, rule: RuleDefinition, language: string): string
|
|
|
31
65
|
"",
|
|
32
66
|
...indent(`→ ${localized(rule.how_to_fix, language)}`, " "),
|
|
33
67
|
"",
|
|
34
|
-
`
|
|
68
|
+
` ${TEXT[uiLanguageOf(language)].relax}: npx chaff relax ${finding.rule}`,
|
|
35
69
|
"",
|
|
36
70
|
];
|
|
37
71
|
|
|
@@ -41,22 +75,20 @@ export const renderFriendly = (header: string, result: RunResult, rules: readonl
|
|
|
41
75
|
const rule = byId.get(finding.rule);
|
|
42
76
|
return rule === undefined ? [] : block(finding, rule, language);
|
|
43
77
|
});
|
|
78
|
+
const text = TEXT[uiLanguageOf(language)];
|
|
44
79
|
const notes = [
|
|
45
80
|
"",
|
|
46
81
|
"─".repeat(RULE),
|
|
47
82
|
"",
|
|
48
|
-
` ${tally(result.findings)}
|
|
49
|
-
|
|
83
|
+
` ${tally(result.findings, uiLanguageOf(language))} ${text.machine}`,
|
|
84
|
+
text.deterministic,
|
|
50
85
|
"",
|
|
51
|
-
|
|
86
|
+
` ${text.untouched}`,
|
|
52
87
|
];
|
|
53
|
-
const forced =
|
|
54
|
-
result.forcedExperimental.length > 0
|
|
55
|
-
? ["", ` 試験中の rule を ${result.forcedExperimental.length} 件、設定により有効にしています: ${result.forcedExperimental.join(", ")}`]
|
|
56
|
-
: [];
|
|
88
|
+
const forced = result.forcedExperimental.length > 0 ? ["", ` ${text.forced(result.forcedExperimental.length, result.forcedExperimental.join(", "))}`] : [];
|
|
57
89
|
const skipped =
|
|
58
90
|
result.skipped.length > 0
|
|
59
|
-
? ["", ` ${result.skipped.length}
|
|
91
|
+
? ["", ` ${text.notRun(result.skipped.length)}`, ...result.skipped.map((entry) => ` ${entry.rule}${text.because(entry.why)}`)]
|
|
60
92
|
: [];
|
|
61
93
|
return ["", header, ...blocks, ...notes, ...forced, ...skipped, ""].join("\n");
|
|
62
94
|
};
|
package/src/render/rules-json.ts
CHANGED
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import { definedLevels, resolve } from "../levels.ts";
|
|
2
2
|
import type { Config } from "../config/load.ts";
|
|
3
3
|
import type { RuleDefinition } from "../plugin.ts";
|
|
4
|
+
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const TEXT: Texts<{
|
|
7
|
+
readonly offBySetting: string;
|
|
8
|
+
readonly offExperimental: string;
|
|
9
|
+
readonly valuesNote: string;
|
|
10
|
+
readonly reason: string;
|
|
11
|
+
readonly byFile: string;
|
|
12
|
+
}> = {
|
|
13
|
+
ja: {
|
|
14
|
+
offBySetting: "設定で止めている",
|
|
15
|
+
offExperimental: "experimental な rule は既定で動かさない",
|
|
16
|
+
valuesNote: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
|
|
17
|
+
reason: "<理由>",
|
|
18
|
+
byFile: "chaff.yaml の rules に <rule-id>: <level> を足す。既定のままのものは書かない。",
|
|
19
|
+
},
|
|
20
|
+
en: {
|
|
21
|
+
offBySetting: "turned off in the settings",
|
|
22
|
+
offExperimental: "experimental rules do not run by default",
|
|
23
|
+
valuesNote: "A number may be written instead of these four words. The words are recommended.",
|
|
24
|
+
reason: "<reason>",
|
|
25
|
+
byFile: "Add <rule-id>: <level> under rules in chaff.yaml. Leave out anything at its default.",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const now = (rule: RuleDefinition, config: Config, genre: string, text: (typeof TEXT)["ja"]): Record<string, unknown> => {
|
|
6
30
|
const explicit = config.rules[rule.id];
|
|
7
|
-
if (explicit === "off") return { level: "off", why_off:
|
|
31
|
+
if (explicit === "off") return { level: "off", why_off: text.offBySetting };
|
|
8
32
|
const limit = rule.layer === "L4" ? undefined : config.limits[rule.id];
|
|
9
33
|
if (limit !== undefined) return { level: "normal", limit, set_as: "number" };
|
|
10
34
|
if (explicit !== undefined) return { level: explicit, limit: resolve(rule, explicit, genre).limit };
|
|
11
35
|
if (rule.status === "experimental" && !config.experimental) {
|
|
12
|
-
return { level: "off", why_off:
|
|
36
|
+
return { level: "off", why_off: text.offExperimental, turn_on_with: `npx chaff lint --experimental` };
|
|
13
37
|
}
|
|
14
38
|
return { level: "normal", limit: resolve(rule, "normal", genre).limit };
|
|
15
39
|
};
|
|
@@ -33,14 +57,15 @@ const levelsOf = (rule: RuleDefinition, genre: string): Record<string, unknown>
|
|
|
33
57
|
};
|
|
34
58
|
|
|
35
59
|
/** AI に設定を書かせるときの入口。推測せずに書けるだけの情報を 1 つに入れる。spec §19.3。 */
|
|
36
|
-
export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string =>
|
|
37
|
-
|
|
60
|
+
export const rulesJson = (rules: readonly RuleDefinition[], config: Config, language: string, genre: string): string => {
|
|
61
|
+
const text = TEXT[uiLanguageOf(language)];
|
|
62
|
+
return JSON.stringify(
|
|
38
63
|
{
|
|
39
64
|
schema_version: 1,
|
|
40
65
|
config_file: "chaff.yaml",
|
|
41
66
|
detected: { genre, language },
|
|
42
67
|
values_you_can_use: ["strict", "normal", "relaxed", "off"],
|
|
43
|
-
values_note:
|
|
68
|
+
values_note: text.valuesNote,
|
|
44
69
|
rules: rules.map((rule) => ({
|
|
45
70
|
id: rule.id,
|
|
46
71
|
layer: rule.layer,
|
|
@@ -52,13 +77,14 @@ export const rulesJson = (rules: readonly RuleDefinition[], config: Config, lang
|
|
|
52
77
|
...levelsOf(rule, genre),
|
|
53
78
|
levels_you_can_set: definedLevels(rule),
|
|
54
79
|
your_setting: yourSetting(rule, config),
|
|
55
|
-
now: now(rule, config, genre),
|
|
80
|
+
now: now(rule, config, genre, text),
|
|
56
81
|
})),
|
|
57
82
|
how_to_change: {
|
|
58
|
-
by_command: [
|
|
59
|
-
by_file:
|
|
83
|
+
by_command: ["relax", "strict", "off"].map((command) => `npx chaff ${command} <rule-id> --why "${text.reason}"`),
|
|
84
|
+
by_file: text.byFile,
|
|
60
85
|
},
|
|
61
86
|
},
|
|
62
87
|
null,
|
|
63
88
|
2,
|
|
64
89
|
);
|
|
90
|
+
};
|
package/src/render/summary.ts
CHANGED
|
@@ -1,22 +1,47 @@
|
|
|
1
1
|
import type { Finding } from "../plugin.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "../ui.ts";
|
|
2
3
|
|
|
3
4
|
export type FileOutcome = { readonly path: string; readonly findings: readonly Finding[]; readonly notRun: number };
|
|
4
5
|
|
|
5
6
|
const count = (findings: readonly Finding[], severity: string): number => findings.filter((finding) => finding.severity === severity).length;
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
type Severity = "error" | "warning" | "info";
|
|
9
|
+
|
|
10
|
+
const TEXT: Texts<{
|
|
11
|
+
readonly counts: Readonly<Record<Severity, (n: number) => string>>;
|
|
12
|
+
readonly join: string;
|
|
13
|
+
readonly none: string;
|
|
14
|
+
readonly files: (seen: number, touched: number) => string;
|
|
15
|
+
}> = {
|
|
16
|
+
ja: {
|
|
17
|
+
counts: { error: (n) => `エラー ${n} 件`, warning: (n) => `注意 ${n} 件`, info: (n) => `参考 ${n} 件` },
|
|
18
|
+
join: "、",
|
|
19
|
+
none: "指摘はありません",
|
|
20
|
+
files: (seen, touched) => `${seen} ファイルを見て、${touched} ファイルに指摘がありました`,
|
|
21
|
+
},
|
|
22
|
+
en: {
|
|
23
|
+
counts: {
|
|
24
|
+
error: (n) => `${n} error${n === 1 ? "" : "s"}`,
|
|
25
|
+
warning: (n) => `${n} warning${n === 1 ? "" : "s"}`,
|
|
26
|
+
info: (n) => `${n} note${n === 1 ? "" : "s"}`,
|
|
27
|
+
},
|
|
28
|
+
join: ", ",
|
|
29
|
+
none: "No findings",
|
|
30
|
+
files: (seen, touched) => `${seen} files checked, ${touched} with findings`,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const SEVERITIES: readonly Severity[] = ["error", "warning", "info"];
|
|
35
|
+
|
|
36
|
+
export const tally = (findings: readonly Finding[], ui: UiLanguage): string => {
|
|
37
|
+
const parts = SEVERITIES.filter((severity) => count(findings, severity) > 0).map((severity) => TEXT[ui].counts[severity](count(findings, severity)));
|
|
38
|
+
return parts.length === 0 ? TEXT[ui].none : parts.join(TEXT[ui].join);
|
|
14
39
|
};
|
|
15
40
|
|
|
16
41
|
/** 複数ファイルを見たときの締め。1 ファイルのときは出さない。 */
|
|
17
|
-
export const renderSummary = (outcomes: readonly FileOutcome[]): string[] => {
|
|
42
|
+
export const renderSummary = (outcomes: readonly FileOutcome[], ui: UiLanguage): string[] => {
|
|
18
43
|
if (outcomes.length < 2) return [];
|
|
19
44
|
const all = outcomes.flatMap((outcome) => outcome.findings);
|
|
20
45
|
const touched = outcomes.filter((outcome) => outcome.findings.length > 0).length;
|
|
21
|
-
return ["", "═".repeat(60), "", ` ${outcomes.length
|
|
46
|
+
return ["", "═".repeat(60), "", ` ${TEXT[ui].files(outcomes.length, touched)}`, ` ${tally(all, ui)}`, ""];
|
|
22
47
|
};
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
import type { Suppressed, Suppression } from "../stet.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "../ui.ts";
|
|
3
|
+
|
|
4
|
+
const TEXT: Texts<{
|
|
5
|
+
readonly nudge: string;
|
|
6
|
+
readonly more: (n: number) => string;
|
|
7
|
+
readonly count: (n: number) => string;
|
|
8
|
+
readonly reasons: (list: string) => string;
|
|
9
|
+
readonly relaxAll: (rule: string) => string;
|
|
10
|
+
readonly none: string;
|
|
11
|
+
readonly total: (n: number) => string;
|
|
12
|
+
readonly reasonless: (n: number) => string;
|
|
13
|
+
}> = {
|
|
14
|
+
ja: {
|
|
15
|
+
nudge: " ← 設定の見直しを検討してください",
|
|
16
|
+
more: (n) => ` ほか ${n} ファイル`,
|
|
17
|
+
count: (n) => `${String(n).padStart(3)} 件`,
|
|
18
|
+
reasons: (list) => `理由: ${list}`,
|
|
19
|
+
relaxAll: (rule) => `ルールごとゆるめる: npx chaff relax ${rule} --why "..."`,
|
|
20
|
+
none: "\n 抑制されている指摘はありません。\n",
|
|
21
|
+
total: (n) => `抑制されている指摘: ${n} 件`,
|
|
22
|
+
reasonless: (n) => `理由が書かれていない抑制: ${n} 件`,
|
|
23
|
+
},
|
|
24
|
+
en: {
|
|
25
|
+
nudge: " <- consider changing the setting instead",
|
|
26
|
+
more: (n) => ` and ${n} more file${n === 1 ? "" : "s"}`,
|
|
27
|
+
count: (n) => `${String(n).padStart(3)}`,
|
|
28
|
+
reasons: (list) => `reasons: ${list}`,
|
|
29
|
+
relaxAll: (rule) => `relax the whole rule: npx chaff relax ${rule} --why "..."`,
|
|
30
|
+
none: "\n No findings are silenced.\n",
|
|
31
|
+
total: (n) => `Silenced findings: ${n}`,
|
|
32
|
+
reasonless: (n) => `Silenced without a reason: ${n}`,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
2
35
|
|
|
3
36
|
export type PerFile = { readonly path: string; readonly suppressed: readonly Suppressed[]; readonly reasonless: readonly Suppression[] };
|
|
4
37
|
|
|
@@ -23,16 +56,17 @@ const groupByRule = (files: readonly PerFile[]): Group[] => {
|
|
|
23
56
|
.sort((left, right) => right.count - left.count);
|
|
24
57
|
};
|
|
25
58
|
|
|
26
|
-
const groupLines = (group: Group): string[] => {
|
|
27
|
-
const
|
|
59
|
+
const groupLines = (group: Group, ui: UiLanguage): string[] => {
|
|
60
|
+
const text = TEXT[ui];
|
|
61
|
+
const nudge = group.count >= NUDGE_AT ? text.nudge : "";
|
|
28
62
|
const shown = group.files.slice(0, 3).join(", ");
|
|
29
|
-
const rest = group.files.length > 3 ?
|
|
63
|
+
const rest = group.files.length > 3 ? text.more(group.files.length - 3) : "";
|
|
30
64
|
return [
|
|
31
65
|
"",
|
|
32
|
-
` ${group.rule.padEnd(26)}${
|
|
66
|
+
` ${group.rule.padEnd(26)}${text.count(group.count)}${nudge}`,
|
|
33
67
|
` ${shown}${rest}`,
|
|
34
68
|
...(group.count >= NUDGE_AT && group.reasons.length > 0
|
|
35
|
-
? [`
|
|
69
|
+
? [` ${text.reasons(group.reasons.slice(0, 2).join(" / "))}`, ` ${text.relaxAll(group.rule)}`]
|
|
36
70
|
: []),
|
|
37
71
|
];
|
|
38
72
|
};
|
|
@@ -42,16 +76,17 @@ const groupLines = (group: Group): string[] => {
|
|
|
42
76
|
* 同じ rule を何度も黙らせているなら、それは (b) ではなく (c) のサイン。
|
|
43
77
|
* workflow spec §7.3。
|
|
44
78
|
*/
|
|
45
|
-
export const renderSuppressions = (files: readonly PerFile[]): string => {
|
|
79
|
+
export const renderSuppressions = (files: readonly PerFile[], ui: UiLanguage = "ja"): string => {
|
|
80
|
+
const text = TEXT[ui];
|
|
46
81
|
const groups = groupByRule(files);
|
|
47
82
|
const total = groups.reduce((sum, group) => sum + group.count, 0);
|
|
48
|
-
if (total === 0) return
|
|
83
|
+
if (total === 0) return text.none;
|
|
49
84
|
const reasonless = files.flatMap((file) => file.reasonless.map(() => file.path));
|
|
50
85
|
return [
|
|
51
86
|
"",
|
|
52
|
-
`
|
|
53
|
-
...groups.flatMap(groupLines),
|
|
54
|
-
...(reasonless.length > 0 ? ["", `
|
|
87
|
+
` ${text.total(total)}`,
|
|
88
|
+
...groups.flatMap((group) => groupLines(group, ui)),
|
|
89
|
+
...(reasonless.length > 0 ? ["", ` ${text.reasonless(reasonless.length)}`, ` ${[...new Set(reasonless)].join(", ")}`] : []),
|
|
55
90
|
"",
|
|
56
91
|
].join("\n");
|
|
57
92
|
};
|
package/src/run.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { DETECTORS } from "./detectors/index.ts";
|
|
|
2
2
|
import { resolve } from "./levels.ts";
|
|
3
3
|
import type { AdapterNeeds, Finding, Level, ProseDocument, RuleDefinition } from "./plugin.ts";
|
|
4
4
|
import { lineStarts, placeOf } from "./position.ts";
|
|
5
|
+
import { REASONS, type Reasons } from "./reasons.ts";
|
|
6
|
+
import { unreadStructure, type Unread } from "./structure/unread.ts";
|
|
7
|
+
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
8
|
+
import { uiLanguageOf } from "./ui.ts";
|
|
5
9
|
|
|
6
10
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
7
11
|
|
|
@@ -22,7 +26,28 @@ const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolea
|
|
|
22
26
|
return "normal";
|
|
23
27
|
};
|
|
24
28
|
|
|
25
|
-
const
|
|
29
|
+
const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.language)];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 構造の rule が動けない理由。木を作れない言語か、番号の行を読めなかった文書。
|
|
33
|
+
* 読めなかった文書で「参照先が無い」が 0 件なのは、確かめた結果ではないので、読めなかったと言う。
|
|
34
|
+
*/
|
|
35
|
+
const treeProblem = (doc: ProseDocument): string | undefined => {
|
|
36
|
+
if (doc.structure === undefined) return reasonsFor(doc).noStructure(doc.language);
|
|
37
|
+
const unread = unreadOf(doc);
|
|
38
|
+
return unread === undefined ? undefined : reasonsFor(doc).unreadStructure(unread.clauses, unread.units);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** 木が読んだのと同じ本文。Markdown はコードを覆ったもの、.txt はそのまま(.txt の字下げはコードではない)。 */
|
|
42
|
+
const textTheTreeRead = (doc: ProseDocument): string => (isMarkdownPath(doc.path) ? (doc.prose ?? doc.source) : doc.source);
|
|
43
|
+
|
|
44
|
+
const unreadByDocument = new WeakMap<ProseDocument, Unread | undefined>();
|
|
45
|
+
|
|
46
|
+
/** 文書ごとに一度だけ数える。構造の rule が三つあっても、本文を三度なめない。 */
|
|
47
|
+
const unreadOf = (doc: ProseDocument): Unread | undefined => {
|
|
48
|
+
if (!unreadByDocument.has(doc)) unreadByDocument.set(doc, doc.structure === undefined ? undefined : unreadStructure(textTheTreeRead(doc), doc.structure));
|
|
49
|
+
return unreadByDocument.get(doc);
|
|
50
|
+
};
|
|
26
51
|
|
|
27
52
|
/** 知らない要求は満たされていないものとして扱う。黙って無視すると、要求なしで動いてしまう。 */
|
|
28
53
|
const has = (capabilities: ProseDocument["capabilities"], need: string): boolean => {
|
|
@@ -42,13 +67,19 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
42
67
|
|
|
43
68
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
44
69
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
45
|
-
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return
|
|
70
|
+
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
46
71
|
const missing = rule.requires.filter((need) => need !== "structure").find((need) => !has(doc.capabilities, need));
|
|
47
|
-
if (missing !== undefined) return
|
|
48
|
-
if (rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc)) return "アダプタが品詞を返さなかったため";
|
|
72
|
+
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
49
73
|
return undefined;
|
|
50
74
|
};
|
|
51
75
|
|
|
76
|
+
/**
|
|
77
|
+
* 品詞は、動かす rule があるときだけ用意する(neededBy)。だから「品詞が無い」は、段階を見た後でしか言えない。
|
|
78
|
+
* 先に聞くと、止めている rule まで「アダプタが品詞を返さなかった」と、違う理由で出る。
|
|
79
|
+
*/
|
|
80
|
+
const untagged = (rule: RuleDefinition, doc: ProseDocument): string | undefined =>
|
|
81
|
+
rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc) ? reasonsFor(doc).noTags : undefined;
|
|
82
|
+
|
|
52
83
|
const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefinition[] =>
|
|
53
84
|
rules.filter((rule) => rule.use_for.some((target) => genre.startsWith(target)));
|
|
54
85
|
|
|
@@ -114,24 +145,26 @@ export const runRules = (
|
|
|
114
145
|
(acc, rule) => {
|
|
115
146
|
// L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
|
|
116
147
|
if (rule.layer === "L4") {
|
|
117
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
148
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).semantic }] };
|
|
118
149
|
}
|
|
119
150
|
const blocked = unmet(rule, doc);
|
|
120
151
|
if (blocked !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: blocked }] };
|
|
121
152
|
const level = levelFor(rule, settings, experimental);
|
|
122
153
|
if (level === "off") {
|
|
123
|
-
const why = rule.status === "experimental" && settings[rule.id] === undefined ?
|
|
154
|
+
const why = rule.status === "experimental" && settings[rule.id] === undefined ? reasonsFor(doc).experimental : reasonsFor(doc).turnedOff;
|
|
124
155
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
125
156
|
}
|
|
157
|
+
const noTags = untagged(rule, doc);
|
|
158
|
+
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
126
159
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
127
160
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
161
|
+
const noTree = rule.requires.includes("structure") ? treeProblem(doc) : undefined;
|
|
162
|
+
if (noTree !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTree }] };
|
|
131
163
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
132
164
|
if (rule.from.length > 0) return acc;
|
|
133
165
|
const detector = DETECTORS[rule.how_to_find];
|
|
134
|
-
if (detector === undefined)
|
|
166
|
+
if (detector === undefined)
|
|
167
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noDetector(rule.how_to_find) }] };
|
|
135
168
|
const options = {
|
|
136
169
|
limit: limitFor(rule, level, genre, limits),
|
|
137
170
|
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
@@ -139,7 +172,7 @@ export const runRules = (
|
|
|
139
172
|
};
|
|
140
173
|
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
141
174
|
if (rule.word_list !== undefined && options.lexicon === undefined) {
|
|
142
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
175
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noLexicon(doc.language, rule.word_list) }] };
|
|
143
176
|
}
|
|
144
177
|
const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
|
|
145
178
|
return { findings: [...acc.findings, ...found], skipped: acc.skipped };
|