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/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,8 @@ 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 { uiLanguageOf } from "./ui.ts";
|
|
5
7
|
|
|
6
8
|
export type Skipped = { readonly rule: string; readonly why: string };
|
|
7
9
|
|
|
@@ -22,7 +24,7 @@ const levelFor = (rule: RuleDefinition, settings: Settings, experimental: boolea
|
|
|
22
24
|
return "normal";
|
|
23
25
|
};
|
|
24
26
|
|
|
25
|
-
const
|
|
27
|
+
const reasonsFor = (doc: ProseDocument): Reasons => REASONS[uiLanguageOf(doc.language)];
|
|
26
28
|
|
|
27
29
|
/** 知らない要求は満たされていないものとして扱う。黙って無視すると、要求なしで動いてしまう。 */
|
|
28
30
|
const has = (capabilities: ProseDocument["capabilities"], need: string): boolean => {
|
|
@@ -42,13 +44,19 @@ const hasTokens = (doc: ProseDocument): boolean => doc.sentences.length === 0 ||
|
|
|
42
44
|
|
|
43
45
|
/** 要求を満たさない rule は動かせない。満たさないまま動かすと「指摘 0 件」が保証に見える。 */
|
|
44
46
|
const unmet = (rule: RuleDefinition, doc: ProseDocument): string | undefined => {
|
|
45
|
-
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return
|
|
47
|
+
if (rule.languages !== undefined && !rule.languages.includes(doc.language)) return reasonsFor(doc).otherLanguage(doc.language);
|
|
46
48
|
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 "アダプタが品詞を返さなかったため";
|
|
49
|
+
if (missing !== undefined) return reasonsFor(doc).noCapability(missing);
|
|
49
50
|
return undefined;
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* 品詞は、動かす rule があるときだけ用意する(neededBy)。だから「品詞が無い」は、段階を見た後でしか言えない。
|
|
55
|
+
* 先に聞くと、止めている rule まで「アダプタが品詞を返さなかった」と、違う理由で出る。
|
|
56
|
+
*/
|
|
57
|
+
const untagged = (rule: RuleDefinition, doc: ProseDocument): string | undefined =>
|
|
58
|
+
rule.requires.some((need) => need === "pos" || need === "lemma") && !hasTokens(doc) ? reasonsFor(doc).noTags : undefined;
|
|
59
|
+
|
|
52
60
|
const forGenre = (rules: readonly RuleDefinition[], genre: string): RuleDefinition[] =>
|
|
53
61
|
rules.filter((rule) => rule.use_for.some((target) => genre.startsWith(target)));
|
|
54
62
|
|
|
@@ -114,24 +122,27 @@ export const runRules = (
|
|
|
114
122
|
(acc, rule) => {
|
|
115
123
|
// L4 は意味を読む検査。chaff test が扱う。ここで「検出器が無い」と言わせない。
|
|
116
124
|
if (rule.layer === "L4") {
|
|
117
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
125
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).semantic }] };
|
|
118
126
|
}
|
|
119
127
|
const blocked = unmet(rule, doc);
|
|
120
128
|
if (blocked !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: blocked }] };
|
|
121
129
|
const level = levelFor(rule, settings, experimental);
|
|
122
130
|
if (level === "off") {
|
|
123
|
-
const why = rule.status === "experimental" && settings[rule.id] === undefined ?
|
|
131
|
+
const why = rule.status === "experimental" && settings[rule.id] === undefined ? reasonsFor(doc).experimental : reasonsFor(doc).turnedOff;
|
|
124
132
|
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why }] };
|
|
125
133
|
}
|
|
134
|
+
const noTags = untagged(rule, doc);
|
|
135
|
+
if (noTags !== undefined) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: noTags }] };
|
|
126
136
|
// 木は capability ではなく、adapter が structure を持つかで決まる。持たない言語で動かすと「参照先が無い」が 0 件に見える。
|
|
127
137
|
// 段階を見た後で聞く。doc.structure は触れたときに木を作るので、止めている rule のために作らない。
|
|
128
138
|
if (rule.requires.includes("structure") && doc.structure === undefined) {
|
|
129
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
139
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noStructure(doc.language) }] };
|
|
130
140
|
}
|
|
131
141
|
// 複合シグナルは二段目で扱う。一段目では「検出器が無い」と言わせない。
|
|
132
142
|
if (rule.from.length > 0) return acc;
|
|
133
143
|
const detector = DETECTORS[rule.how_to_find];
|
|
134
|
-
if (detector === undefined)
|
|
144
|
+
if (detector === undefined)
|
|
145
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noDetector(rule.how_to_find) }] };
|
|
135
146
|
const options = {
|
|
136
147
|
limit: limitFor(rule, level, genre, limits),
|
|
137
148
|
lexicon: rule.word_list === undefined ? undefined : doc.lexicons[rule.word_list],
|
|
@@ -139,7 +150,7 @@ export const runRules = (
|
|
|
139
150
|
};
|
|
140
151
|
// 語彙表を要求する rule で、その言語に語彙表が無ければ動かせない。黙って通さない。
|
|
141
152
|
if (rule.word_list !== undefined && options.lexicon === undefined) {
|
|
142
|
-
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why:
|
|
153
|
+
return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: reasonsFor(doc).noLexicon(doc.language, rule.word_list) }] };
|
|
143
154
|
}
|
|
144
155
|
const found = detector(doc, options).map((finding) => place(starts, { ...finding, rule: rule.id, severity: rule.severity }));
|
|
145
156
|
return { findings: [...acc.findings, ...found], skipped: acc.skipped };
|
package/src/structure/build.ts
CHANGED
|
@@ -15,6 +15,8 @@ type Draft = {
|
|
|
15
15
|
readonly attrs: Readonly<Record<string, string | number>>;
|
|
16
16
|
readonly children: Draft[];
|
|
17
17
|
readonly ordinal?: number | undefined;
|
|
18
|
+
readonly level?: number | undefined;
|
|
19
|
+
readonly ordinalTo?: number | undefined;
|
|
18
20
|
};
|
|
19
21
|
|
|
20
22
|
/** 開いている節点。rank の大きいものほど内側。 */
|
|
@@ -88,7 +90,12 @@ const openNumbered = (state: State, line: Line, numbered: NumberedLine, headingD
|
|
|
88
90
|
const rank = headingDepth ?? NUMBERED_RANK + numbered.depth;
|
|
89
91
|
// 番地は、自分と同じか内側を閉じてから決める。閉じる前だと、同じ深さの前の条を親と取り違える。
|
|
90
92
|
while (state.stack.length > 1 && top(state).rank >= rank) state.stack.pop();
|
|
91
|
-
const draft = {
|
|
93
|
+
const draft = {
|
|
94
|
+
...draftOf(numbered.kind, addressOf(state, numbered), line, attrs),
|
|
95
|
+
ordinal: numbered.ordinal,
|
|
96
|
+
level: numbered.depth,
|
|
97
|
+
ordinalTo: numbered.ordinalTo,
|
|
98
|
+
};
|
|
92
99
|
open(state, { draft, rank, numbered });
|
|
93
100
|
};
|
|
94
101
|
|
|
@@ -136,6 +143,8 @@ const freeze = (draft: Draft): StructureNode => ({
|
|
|
136
143
|
attrs: draft.attrs,
|
|
137
144
|
children: draft.children.map(freeze),
|
|
138
145
|
...(draft.ordinal === undefined ? {} : { ordinal: draft.ordinal }),
|
|
146
|
+
...(draft.level === undefined ? {} : { level: draft.level }),
|
|
147
|
+
...(draft.ordinalTo === undefined ? {} : { ordinalTo: draft.ordinalTo }),
|
|
139
148
|
});
|
|
140
149
|
|
|
141
150
|
/** Markdown から取った手がかり。見出しと、中を読まない範囲(コード)。.txt はどちらも空。 */
|
|
@@ -156,7 +165,6 @@ const headingsByLine = (lines: readonly Line[], headings: readonly Heading[]): M
|
|
|
156
165
|
}),
|
|
157
166
|
);
|
|
158
167
|
|
|
159
|
-
/** 見出しの行から「#」の印を外した文字列。コードを覆った後の行から取るので、見出しの中のコードも読まない。 */
|
|
160
168
|
/**
|
|
161
169
|
* 見出しの行から見出しの言葉を取る。コードを覆った後の行から取るので、見出しの中のコードも読まない。
|
|
162
170
|
* ATX(行頭が #)なら印と閉じの # を外す。setext(下線で書く見出し)の末尾の # は言葉なので残す。
|
package/src/structure/cite.ts
CHANGED
|
@@ -100,7 +100,9 @@ export const checkCitations = (source: string, tree: StructureNode, citations: r
|
|
|
100
100
|
const whole = normalize(source);
|
|
101
101
|
const starts = lineStarts(source);
|
|
102
102
|
return citations.map((citation): CitationResult => {
|
|
103
|
-
|
|
103
|
+
// 第 1 項に番号の無い書き方では、「12.1」は第12条そのもの。参照の fallback と同じに読む。
|
|
104
|
+
const firstParagraph = citation.address.endsWith(".1") ? byAddress.get(citation.address.slice(0, -2)) : undefined;
|
|
105
|
+
const candidates = byAddress.get(citation.address) ?? firstParagraph ?? [];
|
|
104
106
|
const first = candidates[0];
|
|
105
107
|
if (first === undefined) return { citation, status: "missing-address" };
|
|
106
108
|
const key = normalize(citation.quote).text;
|
package/src/structure/issues.ts
CHANGED
|
@@ -34,30 +34,49 @@ const resolves = (node: StructureNode, addresses: ReadonlySet<string>): boolean
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* 参照の番地が木に無い。他の文書の名前が付いた参照(民法第709条、Section 9 of the Master Agreement)は引かない。
|
|
37
|
-
*
|
|
37
|
+
* 条を 1 つも持たない文書も見ない。他の文書を指しているだけかもしれない。
|
|
38
38
|
*/
|
|
39
39
|
export const danglingReferences = (tree: StructureNode): StructureIssue[] => {
|
|
40
40
|
const nodes = inDocumentOrder(tree);
|
|
41
|
+
// 参照は条を指す。条を一つも持たない文書(契約書に付ける承諾書のひな形など)の「契約書第6条」は、別の文書の条。
|
|
42
|
+
if (!nodes.some((node) => node.kind === "article")) return [];
|
|
41
43
|
const addresses = new Set(nodes.filter((node) => NUMBERED.includes(node.kind)).map((node) => node.address));
|
|
42
|
-
if (addresses.size === 0) return [];
|
|
43
44
|
return nodes
|
|
44
45
|
.filter((node) => node.kind === "reference" && node.attrs["document"] === undefined)
|
|
45
46
|
.filter((node) => !resolves(node, addresses))
|
|
46
47
|
.map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
|
|
47
48
|
};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
+
type Definition = { readonly node: StructureNode; readonly article: string };
|
|
51
|
+
|
|
52
|
+
/** 定義を文書の順に、それが置かれた条の番地と一緒に並べる。範囲を限った定義は、その条の中でだけ比べるため。 */
|
|
53
|
+
const definitionsInOrder = (tree: StructureNode): Definition[] => {
|
|
54
|
+
const found: Definition[] = [];
|
|
55
|
+
const pending: Definition[] = [{ node: tree, article: "" }];
|
|
56
|
+
while (pending.length > 0) {
|
|
57
|
+
const current = pending.pop();
|
|
58
|
+
if (current === undefined) break;
|
|
59
|
+
if (current.node.kind === "definition") found.push(current);
|
|
60
|
+
const article = current.node.kind === "article" ? current.node.address : current.article;
|
|
61
|
+
[...current.node.children].reverse().forEach((child) => pending.push({ node: child, article }));
|
|
62
|
+
}
|
|
63
|
+
return found;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 同じ語の二度目以降の定義。どちらが正しいかは決めず、両方の場所を示す。
|
|
68
|
+
* 範囲を限った定義(この条において「X」とは)は、同じ条の中でだけ比べる。別の条で定義し直すのは正しい書き方。
|
|
69
|
+
*/
|
|
50
70
|
export const duplicateDefinitions = (tree: StructureNode): StructureIssue[] => {
|
|
51
71
|
const first = new Map<string, StructureNode>();
|
|
52
|
-
return
|
|
53
|
-
|
|
54
|
-
.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
});
|
|
72
|
+
return definitionsInOrder(tree).flatMap(({ node, article }) => {
|
|
73
|
+
const term = textOf(node, "term");
|
|
74
|
+
const key = node.attrs["scope"] === "local" ? `${article}\u0000${term}` : term;
|
|
75
|
+
const earlier = first.get(key);
|
|
76
|
+
if (earlier === undefined) {
|
|
77
|
+
first.set(key, node);
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
return [{ offset: node.span.start, values: { term, first: earlier.line } }];
|
|
81
|
+
});
|
|
63
82
|
};
|
|
@@ -3,17 +3,21 @@ import { inDocumentOrder, type StructureIssue } from "./issues.ts";
|
|
|
3
3
|
|
|
4
4
|
const labelOf = (node: StructureNode): string => String(node.attrs["label"] ?? node.address);
|
|
5
5
|
|
|
6
|
-
type Ordered = { readonly node: StructureNode; readonly ordinal: number };
|
|
6
|
+
type Ordered = { readonly node: StructureNode; readonly ordinal: number; readonly last: number };
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* 同じ親の子のうち、番号の付いたものを種類と深さごとに分ける。条と項は別の並び、項と号も別の並び。
|
|
10
|
+
* 法令の第 1 項は番号が無いので、その号(一、二)と第 2 項(2)は同じ条の直下に並ぶ。深さで分けないと「二の次が2」に見える。
|
|
11
|
+
*/
|
|
9
12
|
const sequencesOf = (parent: StructureNode): Ordered[][] => {
|
|
10
13
|
const groups = new Map<string, Ordered[]>();
|
|
11
14
|
parent.children.forEach((node) => {
|
|
12
15
|
if (node.ordinal === undefined) return;
|
|
16
|
+
const key = `${node.kind}/${String(node.level ?? "")}`;
|
|
13
17
|
// 配列を作り直さずに足す。条が何千もある法令で二乗にしない。
|
|
14
|
-
const group = groups.get(
|
|
15
|
-
group.push({ node, ordinal: node.ordinal });
|
|
16
|
-
groups.set(
|
|
18
|
+
const group = groups.get(key) ?? [];
|
|
19
|
+
group.push({ node, ordinal: node.ordinal, last: node.ordinalTo ?? node.ordinal });
|
|
20
|
+
groups.set(key, group);
|
|
17
21
|
});
|
|
18
22
|
return [...groups.values()];
|
|
19
23
|
};
|
|
@@ -23,11 +27,12 @@ const breaksIn = (sequence: readonly Ordered[]): StructureIssue[] =>
|
|
|
23
27
|
sequence.flatMap((current, index) => {
|
|
24
28
|
const previous = sequence[index - 1];
|
|
25
29
|
// 1 に戻ったら新しい並び。一つの条に (a)(b) の箇条書きが二つある、附則が第1条から振り直す、のどちらも誤りではない。
|
|
26
|
-
|
|
30
|
+
// 範囲をまとめた行(第四十三条から第五十五条まで)の次は、範囲の最後から数える。
|
|
31
|
+
if (previous === undefined || current.ordinal === previous.last + 1 || current.ordinal === 1) return [];
|
|
27
32
|
return [
|
|
28
33
|
{
|
|
29
34
|
offset: current.node.span.start,
|
|
30
|
-
values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.
|
|
35
|
+
values: { previous: labelOf(previous.node), label: labelOf(current.node), expected: previous.last + 1, found: current.ordinal },
|
|
31
36
|
},
|
|
32
37
|
];
|
|
33
38
|
});
|