chaffjs 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +11 -4
  2. package/dist/adapter-load.d.ts +12 -1
  3. package/dist/adapter-load.d.ts.map +1 -1
  4. package/dist/adapter-load.js +37 -13
  5. package/dist/adapter-load.js.map +1 -1
  6. package/dist/address-chain.d.ts +9 -0
  7. package/dist/address-chain.d.ts.map +1 -0
  8. package/dist/address-chain.js +105 -0
  9. package/dist/address-chain.js.map +1 -0
  10. package/dist/cli-args.d.ts +8 -0
  11. package/dist/cli-args.d.ts.map +1 -0
  12. package/dist/cli-args.js +8 -0
  13. package/dist/cli-args.js.map +1 -0
  14. package/dist/cli-text.d.ts +26 -0
  15. package/dist/cli-text.d.ts.map +1 -0
  16. package/dist/cli-text.js +155 -0
  17. package/dist/cli-text.js.map +1 -0
  18. package/dist/cli.d.ts +2 -0
  19. package/dist/cli.d.ts.map +1 -1
  20. package/dist/cli.js +94 -87
  21. package/dist/cli.js.map +1 -1
  22. package/dist/commands/cite.d.ts +2 -1
  23. package/dist/commands/cite.d.ts.map +1 -1
  24. package/dist/commands/cite.js +39 -18
  25. package/dist/commands/cite.js.map +1 -1
  26. package/dist/commands/eval.d.ts.map +1 -1
  27. package/dist/commands/eval.js +2 -1
  28. package/dist/commands/eval.js.map +1 -1
  29. package/dist/commands/feedback.d.ts +35 -0
  30. package/dist/commands/feedback.d.ts.map +1 -0
  31. package/dist/commands/feedback.js +157 -0
  32. package/dist/commands/feedback.js.map +1 -0
  33. package/dist/commands/skill.d.ts +22 -0
  34. package/dist/commands/skill.d.ts.map +1 -0
  35. package/dist/commands/skill.js +68 -0
  36. package/dist/commands/skill.js.map +1 -0
  37. package/dist/commands/test.d.ts.map +1 -1
  38. package/dist/commands/test.js +3 -2
  39. package/dist/commands/test.js.map +1 -1
  40. package/dist/commands/tree.d.ts +11 -1
  41. package/dist/commands/tree.d.ts.map +1 -1
  42. package/dist/commands/tree.js +21 -7
  43. package/dist/commands/tree.js.map +1 -1
  44. package/dist/config/by-path.d.ts +5 -1
  45. package/dist/config/by-path.d.ts.map +1 -1
  46. package/dist/config/by-path.js +1 -4
  47. package/dist/config/by-path.js.map +1 -1
  48. package/dist/config/load.d.ts +2 -0
  49. package/dist/config/load.d.ts.map +1 -1
  50. package/dist/config/load.js +3 -1
  51. package/dist/config/load.js.map +1 -1
  52. package/dist/config/rule-problems.d.ts +2 -1
  53. package/dist/config/rule-problems.d.ts.map +1 -1
  54. package/dist/config/rule-problems.js +17 -6
  55. package/dist/config/rule-problems.js.map +1 -1
  56. package/dist/config/write.d.ts.map +1 -1
  57. package/dist/config/write.js +29 -9
  58. package/dist/config/write.js.map +1 -1
  59. package/dist/detectors/char-shape.d.ts.map +1 -1
  60. package/dist/detectors/char-shape.js +3 -2
  61. package/dist/detectors/char-shape.js.map +1 -1
  62. package/dist/document.d.ts +2 -2
  63. package/dist/document.d.ts.map +1 -1
  64. package/dist/document.js +4 -2
  65. package/dist/document.js.map +1 -1
  66. package/dist/feedback/draft.d.ts +28 -0
  67. package/dist/feedback/draft.d.ts.map +1 -0
  68. package/dist/feedback/draft.js +66 -0
  69. package/dist/feedback/draft.js.map +1 -0
  70. package/dist/feedback/excerpt.d.ts +10 -0
  71. package/dist/feedback/excerpt.d.ts.map +1 -0
  72. package/dist/feedback/excerpt.js +20 -0
  73. package/dist/feedback/excerpt.js.map +1 -0
  74. package/dist/genre.d.ts +1 -1
  75. package/dist/genre.d.ts.map +1 -1
  76. package/dist/genre.js +3 -3
  77. package/dist/genre.js.map +1 -1
  78. package/dist/init.d.ts +2 -1
  79. package/dist/init.d.ts.map +1 -1
  80. package/dist/init.js +62 -13
  81. package/dist/init.js.map +1 -1
  82. package/dist/legal-address.d.ts +2 -0
  83. package/dist/legal-address.d.ts.map +1 -0
  84. package/dist/legal-address.js +16 -0
  85. package/dist/legal-address.js.map +1 -0
  86. package/dist/orthography.d.ts.map +1 -1
  87. package/dist/orthography.js +15 -0
  88. package/dist/orthography.js.map +1 -1
  89. package/dist/plugin.d.ts +94 -0
  90. package/dist/plugin.d.ts.map +1 -1
  91. package/dist/profile/for-file.d.ts +7 -0
  92. package/dist/profile/for-file.d.ts.map +1 -0
  93. package/dist/profile/for-file.js +6 -0
  94. package/dist/profile/for-file.js.map +1 -0
  95. package/dist/profile/load.d.ts +4 -0
  96. package/dist/profile/load.d.ts.map +1 -0
  97. package/dist/profile/load.js +25 -0
  98. package/dist/profile/load.js.map +1 -0
  99. package/dist/profile/parse.d.ts +19 -0
  100. package/dist/profile/parse.d.ts.map +1 -0
  101. package/dist/profile/parse.js +82 -0
  102. package/dist/profile/parse.js.map +1 -0
  103. package/dist/profile/select.d.ts +22 -0
  104. package/dist/profile/select.d.ts.map +1 -0
  105. package/dist/profile/select.js +21 -0
  106. package/dist/profile/select.js.map +1 -0
  107. package/dist/reasons.d.ts +16 -0
  108. package/dist/reasons.d.ts.map +1 -0
  109. package/dist/reasons.js +31 -0
  110. package/dist/reasons.js.map +1 -0
  111. package/dist/render/explain.d.ts.map +1 -1
  112. package/dist/render/explain.js +36 -12
  113. package/dist/render/explain.js.map +1 -1
  114. package/dist/render/friendly.d.ts.map +1 -1
  115. package/dist/render/friendly.js +32 -9
  116. package/dist/render/friendly.js.map +1 -1
  117. package/dist/render/rules-json.d.ts.map +1 -1
  118. package/dist/render/rules-json.js +47 -27
  119. package/dist/render/rules-json.js.map +1 -1
  120. package/dist/render/summary.d.ts +3 -2
  121. package/dist/render/summary.d.ts.map +1 -1
  122. package/dist/render/summary.js +24 -9
  123. package/dist/render/summary.js.map +1 -1
  124. package/dist/render/suppressions.d.ts +2 -1
  125. package/dist/render/suppressions.d.ts.map +1 -1
  126. package/dist/render/suppressions.js +34 -10
  127. package/dist/render/suppressions.js.map +1 -1
  128. package/dist/run.d.ts.map +1 -1
  129. package/dist/run.js +41 -12
  130. package/dist/run.js.map +1 -1
  131. package/dist/structure/build.d.ts +2 -1
  132. package/dist/structure/build.d.ts.map +1 -1
  133. package/dist/structure/build.js +102 -9
  134. package/dist/structure/build.js.map +1 -1
  135. package/dist/structure/cite.d.ts.map +1 -1
  136. package/dist/structure/cite.js +3 -1
  137. package/dist/structure/cite.js.map +1 -1
  138. package/dist/structure/issues.d.ts +5 -2
  139. package/dist/structure/issues.d.ts.map +1 -1
  140. package/dist/structure/issues.js +35 -10
  141. package/dist/structure/issues.js.map +1 -1
  142. package/dist/structure/numbering.d.ts.map +1 -1
  143. package/dist/structure/numbering.js +11 -6
  144. package/dist/structure/numbering.js.map +1 -1
  145. package/dist/structure/of.d.ts +2 -1
  146. package/dist/structure/of.d.ts.map +1 -1
  147. package/dist/structure/of.js +7 -1
  148. package/dist/structure/of.js.map +1 -1
  149. package/dist/structure/relative-find.d.ts +6 -0
  150. package/dist/structure/relative-find.d.ts.map +1 -0
  151. package/dist/structure/relative-find.js +179 -0
  152. package/dist/structure/relative-find.js.map +1 -0
  153. package/dist/structure/relative-resolve.d.ts +3 -0
  154. package/dist/structure/relative-resolve.d.ts.map +1 -0
  155. package/dist/structure/relative-resolve.js +175 -0
  156. package/dist/structure/relative-resolve.js.map +1 -0
  157. package/dist/structure/unread.d.ts +8 -0
  158. package/dist/structure/unread.d.ts.map +1 -0
  159. package/dist/structure/unread.js +34 -0
  160. package/dist/structure/unread.js.map +1 -0
  161. package/dist/ui.d.ts +14 -0
  162. package/dist/ui.d.ts.map +1 -0
  163. package/dist/ui.js +14 -0
  164. package/dist/ui.js.map +1 -0
  165. package/dist/watch.d.ts +1 -1
  166. package/dist/watch.d.ts.map +1 -1
  167. package/dist/watch.js +2 -2
  168. package/dist/watch.js.map +1 -1
  169. package/package.json +5 -3
  170. package/profiles/statute.yaml +62 -0
  171. package/skills/chaff/SKILL.md +81 -0
  172. package/src/adapter-load.ts +41 -10
  173. package/src/address-chain.ts +113 -0
  174. package/src/cli-args.ts +9 -0
  175. package/src/cli-text.ts +186 -0
  176. package/src/cli.ts +111 -90
  177. package/src/commands/cite.ts +51 -18
  178. package/src/commands/eval.ts +2 -1
  179. package/src/commands/feedback.ts +210 -0
  180. package/src/commands/skill.ts +74 -0
  181. package/src/commands/test.ts +3 -2
  182. package/src/commands/tree.ts +30 -8
  183. package/src/config/by-path.ts +11 -3
  184. package/src/config/load.ts +5 -1
  185. package/src/config/rule-problems.ts +24 -5
  186. package/src/config/write.ts +33 -9
  187. package/src/detectors/char-shape.ts +5 -4
  188. package/src/document.ts +11 -3
  189. package/src/feedback/draft.ts +102 -0
  190. package/src/feedback/excerpt.ts +23 -0
  191. package/src/genre.ts +4 -4
  192. package/src/init.ts +74 -15
  193. package/src/orthography.ts +15 -0
  194. package/src/plugin.ts +88 -0
  195. package/src/profile/for-file.ts +11 -0
  196. package/src/profile/load.ts +26 -0
  197. package/src/profile/parse.ts +106 -0
  198. package/src/profile/select.ts +35 -0
  199. package/src/reasons.ts +49 -0
  200. package/src/render/explain.ts +46 -12
  201. package/src/render/friendly.ts +42 -10
  202. package/src/render/rules-json.ts +35 -9
  203. package/src/render/summary.ts +34 -9
  204. package/src/render/suppressions.ts +45 -10
  205. package/src/run.ts +44 -11
  206. package/src/structure/build.ts +128 -12
  207. package/src/structure/cite.ts +3 -1
  208. package/src/structure/issues.ts +42 -15
  209. package/src/structure/numbering.ts +12 -7
  210. package/src/structure/of.ts +15 -3
  211. package/src/structure/relative-find.ts +224 -0
  212. package/src/structure/relative-resolve.ts +209 -0
  213. package/src/structure/unread.ts +39 -0
  214. package/src/ui.ts +23 -0
  215. package/src/watch.ts +2 -2
package/src/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
+ };
@@ -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 levelLine = (rule: RuleDefinition, level: Level, current: Level, genre: string | undefined): string => {
6
- const mark = level === current ? "\u2192" : " ";
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}見ない` : ` ${mark} ${name}${resolve(rule, level, genre).limit}`;
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 ["", ` この数字は ${genre ?? "既定"} のものです。ほかに ${others.join(" / ")} で別の数字を持っています。`];
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 experimental = rule.status === "experimental" ? [" このルールはまだ試験中で、既定では動きません(--experimental で動きます)。"] : [];
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
- ` 直しかた: ${localized(rule.how_to_fix, language)}`,
61
+ ` ${text.howToFix}: ${localized(rule.how_to_fix, language)}`,
28
62
  "",
29
- ` 設定できる値(単位: ${unit}):`,
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
- ` いまは ${current} です。`,
67
+ ` ${text.now(current)}`,
34
68
  ...experimental,
35
69
  "",
36
- ` 変える: npx chaff relax ${rule.id} --why "理由"`,
70
+ ` ${text.change(rule.id)}`,
37
71
  "",
38
72
  ].join("\n");
39
73
  };
@@ -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
- `─── ${finding.line} 行目 ${"─".repeat(Math.max(0, RULE - String(finding.line).length - 8))}`,
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
- ` このルールをゆるめる: npx chaff relax ${finding.rule}`,
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} 件の rule は動いていません:`, ...result.skipped.map((entry) => ` ${entry.rule}(${entry.why})`)]
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
  };
@@ -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 now = (rule: RuleDefinition, config: Config, genre: string): Record<string, unknown> => {
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: "experimental な rule は既定で動かさない", turn_on_with: `npx chaff lint --experimental` };
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
- JSON.stringify(
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: "この 4 語のかわりに数字を直接書いてもよい。4 語のほうを勧める。",
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: ['npx chaff relax <rule-id> --why "<理由>"', 'npx chaff strict <rule-id> --why "<理由>"', 'npx chaff off <rule-id> --why "<理由>"'],
59
- by_file: "chaff.yaml の rules に <rule-id>: <level> を足す。既定のままのものは書かない。",
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
+ };
@@ -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
- export const tally = (findings: readonly Finding[]): string => {
8
- const parts = [
9
- count(findings, "error") > 0 ? `エラー ${count(findings, "error")} 件` : undefined,
10
- count(findings, "warning") > 0 ? `注意 ${count(findings, "warning")} 件` : undefined,
11
- count(findings, "info") > 0 ? `参考 ${count(findings, "info")} 件` : undefined,
12
- ].filter((part) => part !== undefined);
13
- return parts.length === 0 ? "指摘はありません" : parts.join("、");
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} ファイルを見て、${touched} ファイルに指摘がありました`, ` ${tally(all)}`, ""];
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 nudge = group.count >= NUDGE_AT ? " ← 設定の見直しを検討してください" : "";
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 ? ` ほか ${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)}${String(group.count).padStart(3)} 件${nudge}`,
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
- ? [` 理由: ${group.reasons.slice(0, 2).join(" / ")}`, ` ルールごとゆるめる: npx chaff relax ${group.rule} --why "..."`]
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 "\n 抑制されている指摘はありません。\n";
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
- ` 抑制されている指摘: ${total} 件`,
53
- ...groups.flatMap(groupLines),
54
- ...(reasonless.length > 0 ? ["", ` 理由が書かれていない抑制: ${reasonless.length} 件`, ` ${[...new Set(reasonless)].join(", ")}`] : []),
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 CAPABILITY_NAME: Readonly<Record<string, string>> = { pos: "品詞解析", lemma: "原形" };
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 `${doc.language} 向けの rule ではないため`;
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 `この言語では${CAPABILITY_NAME[missing] ?? missing}が使えないため`;
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: "意味を読む検査のため(npx chaff test で動きます)" }] };
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
- if (rule.requires.includes("structure") && doc.structure === undefined) {
129
- return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `${doc.language} のパッケージは文書の構造を読めないため` }] };
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) return { findings: acc.findings, skipped: [...acc.skipped, { rule: rule.id, why: `検出器 ${rule.how_to_find} がないため` }] };
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: `${doc.language} の語彙表 ${rule.word_list} が無いため` }] };
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 };