chaffjs 0.22.0 → 0.23.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 (157) hide show
  1. package/README.md +7 -1
  2. package/dist/api.d.ts +14 -0
  3. package/dist/api.d.ts.map +1 -1
  4. package/dist/api.js.map +1 -1
  5. package/dist/cli-args.d.ts +2 -0
  6. package/dist/cli-args.d.ts.map +1 -1
  7. package/dist/cli-args.js +2 -0
  8. package/dist/cli-args.js.map +1 -1
  9. package/dist/cli-text.d.ts.map +1 -1
  10. package/dist/cli-text.js +2 -0
  11. package/dist/cli-text.js.map +1 -1
  12. package/dist/cli.d.ts.map +1 -1
  13. package/dist/cli.js +13 -19
  14. package/dist/cli.js.map +1 -1
  15. package/dist/commands/feedback.d.ts +3 -0
  16. package/dist/commands/feedback.d.ts.map +1 -1
  17. package/dist/commands/feedback.js.map +1 -1
  18. package/dist/commands/fix-plan.d.ts.map +1 -1
  19. package/dist/commands/fix-plan.js +5 -0
  20. package/dist/commands/fix-plan.js.map +1 -1
  21. package/dist/commands/rules.d.ts +9 -0
  22. package/dist/commands/rules.d.ts.map +1 -0
  23. package/dist/commands/rules.js +26 -0
  24. package/dist/commands/rules.js.map +1 -0
  25. package/dist/config/load.d.ts +6 -1
  26. package/dist/config/load.d.ts.map +1 -1
  27. package/dist/config/load.js +1 -0
  28. package/dist/config/load.js.map +1 -1
  29. package/dist/config/style.js +1 -1
  30. package/dist/config/style.js.map +1 -1
  31. package/dist/config/warnings.d.ts +1 -1
  32. package/dist/config/warnings.d.ts.map +1 -1
  33. package/dist/config/warnings.js +3 -1
  34. package/dist/config/warnings.js.map +1 -1
  35. package/dist/detectors/ellipsis.d.ts +10 -0
  36. package/dist/detectors/ellipsis.d.ts.map +1 -0
  37. package/dist/detectors/ellipsis.js +56 -0
  38. package/dist/detectors/ellipsis.js.map +1 -0
  39. package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
  40. package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
  41. package/dist/detectors/registry/ellipsis-consistency.js +3 -0
  42. package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
  43. package/dist/extension/load.d.ts +3 -0
  44. package/dist/extension/load.d.ts.map +1 -1
  45. package/dist/extension/load.js +1 -0
  46. package/dist/extension/load.js.map +1 -1
  47. package/dist/extension/pack-read.d.ts.map +1 -1
  48. package/dist/extension/pack-read.js +1 -0
  49. package/dist/extension/pack-read.js.map +1 -1
  50. package/dist/extension/plugin-load.js.map +1 -1
  51. package/dist/extension/plugin-parse.d.ts +7 -0
  52. package/dist/extension/plugin-parse.d.ts.map +1 -1
  53. package/dist/extension/plugin-parse.js +18 -2
  54. package/dist/extension/plugin-parse.js.map +1 -1
  55. package/dist/extension/problem-text.d.ts.map +1 -1
  56. package/dist/extension/problem-text.js +5 -0
  57. package/dist/extension/problem-text.js.map +1 -1
  58. package/dist/fix-plan/plan.d.ts +4 -0
  59. package/dist/fix-plan/plan.d.ts.map +1 -1
  60. package/dist/fix-plan/plan.js +1 -0
  61. package/dist/fix-plan/plan.js.map +1 -1
  62. package/dist/fix-plan/render.d.ts.map +1 -1
  63. package/dist/fix-plan/render.js +9 -0
  64. package/dist/fix-plan/render.js.map +1 -1
  65. package/dist/fix-plan/text.d.ts +4 -0
  66. package/dist/fix-plan/text.d.ts.map +1 -1
  67. package/dist/fix-plan/text.js +8 -2
  68. package/dist/fix-plan/text.js.map +1 -1
  69. package/dist/genre-guide/key-rules.d.ts +10 -0
  70. package/dist/genre-guide/key-rules.d.ts.map +1 -0
  71. package/dist/genre-guide/key-rules.js +23 -0
  72. package/dist/genre-guide/key-rules.js.map +1 -0
  73. package/dist/genre-guide/layer.d.ts +44 -0
  74. package/dist/genre-guide/layer.d.ts.map +1 -0
  75. package/dist/genre-guide/layer.js +43 -0
  76. package/dist/genre-guide/layer.js.map +1 -0
  77. package/dist/genre-guide/lines.d.ts +15 -0
  78. package/dist/genre-guide/lines.d.ts.map +1 -0
  79. package/dist/genre-guide/lines.js +39 -0
  80. package/dist/genre-guide/lines.js.map +1 -0
  81. package/dist/genre-guide/of-config.d.ts +13 -0
  82. package/dist/genre-guide/of-config.d.ts.map +1 -0
  83. package/dist/genre-guide/of-config.js +18 -0
  84. package/dist/genre-guide/of-config.js.map +1 -0
  85. package/dist/genre-guide/problem-text.d.ts +5 -0
  86. package/dist/genre-guide/problem-text.d.ts.map +1 -0
  87. package/dist/genre-guide/problem-text.js +29 -0
  88. package/dist/genre-guide/problem-text.js.map +1 -0
  89. package/dist/genre-guide/render.d.ts +5 -0
  90. package/dist/genre-guide/render.d.ts.map +1 -0
  91. package/dist/genre-guide/render.js +38 -0
  92. package/dist/genre-guide/render.js.map +1 -0
  93. package/dist/genre-guide/report.d.ts +32 -0
  94. package/dist/genre-guide/report.d.ts.map +1 -0
  95. package/dist/genre-guide/report.js +42 -0
  96. package/dist/genre-guide/report.js.map +1 -0
  97. package/dist/genre-guide/resolve.d.ts +16 -0
  98. package/dist/genre-guide/resolve.d.ts.map +1 -0
  99. package/dist/genre-guide/resolve.js +31 -0
  100. package/dist/genre-guide/resolve.js.map +1 -0
  101. package/dist/genre-parse.d.ts +5 -1
  102. package/dist/genre-parse.d.ts.map +1 -1
  103. package/dist/genre-parse.js +8 -1
  104. package/dist/genre-parse.js.map +1 -1
  105. package/dist/init.d.ts.map +1 -1
  106. package/dist/init.js +14 -0
  107. package/dist/init.js.map +1 -1
  108. package/dist/render/rules-json.d.ts +2 -1
  109. package/dist/render/rules-json.d.ts.map +1 -1
  110. package/dist/render/rules-json.js +3 -1
  111. package/dist/render/rules-json.js.map +1 -1
  112. package/dist/render/sarif.d.ts +3 -1
  113. package/dist/render/sarif.d.ts.map +1 -1
  114. package/dist/render/sarif.js +3 -1
  115. package/dist/render/sarif.js.map +1 -1
  116. package/dist/style-parse.d.ts +3 -0
  117. package/dist/style-parse.d.ts.map +1 -1
  118. package/dist/style-parse.js +18 -2
  119. package/dist/style-parse.js.map +1 -1
  120. package/genres.yaml +329 -0
  121. package/package.json +3 -3
  122. package/rules/ellipsis-consistency.yaml +73 -0
  123. package/rules/figurative-density.yaml +84 -0
  124. package/skills/chaff/SKILL.md +6 -0
  125. package/src/api.ts +13 -0
  126. package/src/cli-args.ts +3 -0
  127. package/src/cli-text.ts +2 -0
  128. package/src/cli.ts +16 -20
  129. package/src/commands/feedback.ts +3 -0
  130. package/src/commands/fix-plan.ts +8 -0
  131. package/src/commands/rules.ts +34 -0
  132. package/src/config/load.ts +7 -1
  133. package/src/config/style.ts +1 -1
  134. package/src/config/warnings.ts +3 -1
  135. package/src/detectors/ellipsis.ts +74 -0
  136. package/src/detectors/registry/ellipsis-consistency.ts +4 -0
  137. package/src/extension/load.ts +4 -0
  138. package/src/extension/pack-read.ts +1 -0
  139. package/src/extension/plugin-load.ts +1 -1
  140. package/src/extension/plugin-parse.ts +27 -4
  141. package/src/extension/problem-text.ts +7 -1
  142. package/src/fix-plan/plan.ts +5 -0
  143. package/src/fix-plan/render.ts +9 -0
  144. package/src/fix-plan/text.ts +12 -2
  145. package/src/genre-guide/key-rules.ts +37 -0
  146. package/src/genre-guide/layer.ts +77 -0
  147. package/src/genre-guide/lines.ts +41 -0
  148. package/src/genre-guide/of-config.ts +26 -0
  149. package/src/genre-guide/problem-text.ts +34 -0
  150. package/src/genre-guide/render.ts +50 -0
  151. package/src/genre-guide/report.ts +63 -0
  152. package/src/genre-guide/resolve.ts +36 -0
  153. package/src/genre-parse.ts +13 -3
  154. package/src/init.ts +14 -0
  155. package/src/render/rules-json.ts +4 -0
  156. package/src/render/sarif.ts +4 -1
  157. package/src/style-parse.ts +19 -1
@@ -0,0 +1,84 @@
1
+ id: figurative-density
2
+ layer: L2
3
+ status: stable
4
+ how_to_find: phrase-density
5
+ word_list: figurative-phrase
6
+ uses: [pos]
7
+ severity: info
8
+
9
+ name:
10
+ ja: 飾りのたとえが多い
11
+ en: Too many decorative figures
12
+
13
+ why:
14
+ ja: 「まるで魔法のように動く」「呪文のようなコマンド」「おまじないとして書く」のようなたとえは、一つなら文に色を付けますが、重なると何がどう動くのかが書かれないまま文書が進みます。読み手は仕組みを知りたいのに、たとえしか受け取れません。一つ一つは誤りではないので、文書の長さあたりの数で見ます。たとえの一覧は語彙表 figurative-phrase にあり、語ごとに代わりに書くことの例が付いています。
15
+ en: 'A figure such as "works like magic", "a one-line incantation" or "our secret sauce" adds colour once. Piled up, the document goes on without saying what actually happens, and a reader who wants the mechanism gets only the figure. None of them is wrong alone, so the rule counts them per length of the document. The figures are in the lexicon figurative-phrase, each with what to write instead.'
16
+
17
+ message:
18
+ ja: 「{matched}」など、飾りのたとえが 1000 字あたり {density} 個あります
19
+ en: '"{matched}" and other decorative figures: {density} per 1000 words'
20
+
21
+ how_to_fix:
22
+ ja: たとえの代わりに、何がどう動くのか、何ができるのかを書いてください(まるで魔法のように設定が済む → 設定ファイルを読んで自動で済ませる)。
23
+ en: Replace the figure with what happens ("the config sets itself up like magic" becomes "the tool reads the config file and fills in the defaults").
24
+
25
+ # 「まるで魔法のように」は一つのたとえに二語が当たるので、既定は 1000 字あたり 2 個まで。
26
+ levels: { strict: 1, normal: 2, relaxed: 3 }
27
+
28
+ use_for: [business, technical, blog, legal, docs, academic, literature, speech]
29
+ off_for:
30
+ literature: "Figures are the writer's material in literature."
31
+ speech: "A speaker's figures are part of the talk."
32
+
33
+ group: ai-tells
34
+ summary:
35
+ ja: 「まるで魔法のように」「呪文のような」「おまじない」のような、仕組みの代わりのたとえが多い所
36
+ en: Decorative figures such as "like magic", "incantation" and "secret sauce" piled up in place of the mechanism
37
+
38
+ example:
39
+ ja:
40
+ pad: true
41
+ before: |
42
+ このツールを入れると、まるで魔法のように設定が終わります。最初の一行は呪文のようなものなので、おまじないとして書いておいてください。
43
+ after: |
44
+ このツールを入れると、設定ファイルを読んで既定の値を自動で埋めます。最初の一行は文字コードを指定する行なので、消さずに残してください。
45
+ en:
46
+ pad: true
47
+ before: |
48
+ The installer works like magic. Our secret sauce is a one-line incantation that magically fixes every path.
49
+ after: |
50
+ The installer reads your config file and fills in the defaults. The fix is one command that rewrites every path to an absolute one.
51
+
52
+ not_flagged:
53
+ ja: 一つ一つは誤りではないので、短い文書は測りません。例を挙げる「〜のような」(A や B のような製品)は数えません。文学と講演のジャンルでは止めています。
54
+ en: None of these is wrong alone, so short documents are not measured. A plain comparison or example ("tools such as A and B") is not counted. The literature and speech genres turn it off.
55
+ level_meaning:
56
+ ja: 1000 字あたり飾りのたとえ {limit} 個まで
57
+ en: up to {limit} decorative figures per 1000 words
58
+
59
+ rewrite:
60
+ depth: register
61
+ ja:
62
+ direction: たとえを、何がどう動くのか、何ができるのかの説明に替えてください。語彙表の語ごとの書き換えの例に従い、文書に書かれていない仕組みは作りません。
63
+ pairs:
64
+ - before: このツールを入れると、まるで魔法のように設定が終わります。
65
+ after: このツールを入れると、設定ファイルを読んで既定の値を自動で埋めます。
66
+ - before: 最初の一行はおまじないとして書いておいてください。
67
+ after: 最初の一行は文字コードを指定する行なので、消さずに残してください。
68
+ keep:
69
+ - 書いてある事実と手順
70
+ avoid:
71
+ - 文書に書かれていない仕組みを作って書く
72
+ - たとえを別のたとえに替える
73
+ en:
74
+ direction: Replace each figure with what actually happens or what the thing does, following the lexicon's hint for the phrase. Do not invent a mechanism the document does not give.
75
+ pairs:
76
+ - before: The installer works like magic.
77
+ after: The installer reads your config file and fills in the defaults.
78
+ - before: Our secret sauce is a one-line incantation that fixes every path.
79
+ after: The fix is one command that rewrites every path to an absolute one.
80
+ keep:
81
+ - the facts and steps the document gives
82
+ avoid:
83
+ - inventing a mechanism the document does not describe
84
+ - swapping one figure for another
@@ -23,6 +23,10 @@ read. A contract checked as a blog post gets wrong findings, so choose the genre
23
23
  `npx chaffjs init --genre legal/contract` writes it into `chaff.yaml` (always pass `--genre`: without it,
24
24
  `init` asks at a terminal).
25
25
  4. The header's first line shows the genre used and where it came from. Check it before trusting the findings.
26
+ 5. With the genre set, the report opens with the genre's guide: what a good document of that kind does, and the
27
+ rules that matter most for it. Read it first, before any finding, and hold the draft (or your rewrite) to each
28
+ line; most of it is what no rule can check. The team may have changed it with `guide:` in `chaff.yaml`. A
29
+ guessed or default genre shows no guide.
26
30
 
27
31
  A rule the genre turns off is listed under "did not run" with the genre as the reason. That is the preset's
28
32
  choice, not a gap; turn it back on (`rules:` in `chaff.yaml`) only when the person asks.
@@ -112,6 +116,8 @@ generated. chaff never rewrites; the rewriting is yours.
112
116
  **Start with the plan.** Run `npx chaffjs fix-plan <file>` (with the genre's `--genre`; `--json` to
113
117
  read it as data). It prints, in the document's language:
114
118
 
119
+ - the genre's guide first, when the genre is set: what a good document of the kind does, the lines your rewrite
120
+ is held to;
115
121
  - the constraints: keep facts, numbers, conditions and names; add no fact; ask the writer instead of inventing a
116
122
  specific; two passes at most;
117
123
  - the recommended mode and why, and the document-level signals with the outline's numbers;
package/src/api.ts CHANGED
@@ -138,6 +138,15 @@ export const defineRule = (spec: RuleSpec): Rule => ({ ...spec, apiVersion: API_
138
138
  /** A word list in each language it has words for: { ja: [...], en: [...] }. A string is an entry with only a pattern. */
139
139
  export type LexiconSpec = Readonly<Record<string, readonly (string | LexiconEntry)[]>>;
140
140
 
141
+ /** Lines of a genre guide: one line or a list, the same in every language, or by language ({ ja: [...], en: [...] }). */
142
+ export type GuideText = string | readonly string[] | Readonly<Record<string, string | readonly string[]>>;
143
+
144
+ /**
145
+ * Changes to the genre guides chaff prints before the findings, by genre or group (blog/tech, legal): replace sets the
146
+ * lines, add puts lines after them, off leaves the genre with none.
147
+ */
148
+ export type GuideSpec = Readonly<Record<string, "off" | { readonly replace?: GuideText; readonly add?: GuideText }>>;
149
+
141
150
  /** A house style a plugin ships: rule levels and options, and the guideline it follows. */
142
151
  export type StyleSpec = {
143
152
  readonly id: string;
@@ -146,6 +155,8 @@ export type StyleSpec = {
146
155
  readonly source: { readonly title: Text; readonly url: string };
147
156
  readonly rules?: Readonly<Record<string, "strict" | "normal" | "relaxed" | "off">>;
148
157
  readonly options?: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
158
+ /** Its changes to the genre guides, while the style is chosen. */
159
+ readonly guide?: GuideSpec;
149
160
  };
150
161
 
151
162
  /**
@@ -158,6 +169,8 @@ export type PluginSpec = {
158
169
  readonly rules?: readonly (Rule | RuleSpec | Readonly<Record<string, unknown>>)[];
159
170
  readonly lexicons?: Readonly<Record<string, LexiconSpec>>;
160
171
  readonly styles?: readonly StyleSpec[];
172
+ /** Its changes to the genre guides, whenever the plugin is loaded. */
173
+ readonly guide?: GuideSpec;
161
174
  };
162
175
 
163
176
  export type Plugin = PluginSpec & { readonly apiVersion: number };
package/src/cli-args.ts CHANGED
@@ -14,6 +14,9 @@ export const withExperimental = <T extends { readonly experimental: boolean }>(c
14
14
  experimental: config.experimental || args.includes("--experimental"),
15
15
  });
16
16
 
17
+ /** Whether this run shows the genre's guide: everywhere it would, unless --no-guide leaves it out. */
18
+ export const showsGuide = (args: readonly string[]): boolean => !args.includes("--no-guide");
19
+
17
20
  /** The settings with each --include <glob> added to chaff.yaml's include: the command line widens a directory walk as chaff.yaml can. */
18
21
  export const withIncludes = <T extends { readonly include?: readonly string[] | undefined }>(config: T, args: readonly string[]): T => {
19
22
  const given = args.flatMap((arg, index) => (index > 0 && args[index - 1] === "--include" && !arg.startsWith("--") ? [arg] : []));
package/src/cli-text.ts CHANGED
@@ -56,6 +56,7 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
56
56
  誤った指摘・見逃しの報告の下書きを作る(何も送らない。--with-config で chaff.yaml 全体も載せる)
57
57
 
58
58
  --compact エンジニア向けの 1 行形式
59
+ --no-guide ジャンルを決めたときに最初に出す、そのジャンルの指針を出さない
59
60
  --experimental 試験中の rule も動かす
60
61
  --genre <ジャンル> この実行だけジャンルを決める(chaff.yaml より優先)
61
62
  --show-baseline 棚上げした分も含めて全部見る
@@ -101,6 +102,7 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
101
102
  draft a report of a wrong or missed finding (sends nothing; --with-config adds all of chaff.yaml)
102
103
 
103
104
  --compact one line per finding, for engineers
105
+ --no-guide leave out the genre's guide, which comes first when the genre is set
104
106
  --experimental run the experimental rules too
105
107
  --genre <genre> the genre for this run only (wins over chaff.yaml)
106
108
  --show-baseline show the shelved findings too
package/src/cli.ts CHANGED
@@ -15,7 +15,10 @@ import { GENRES } from "./genre.ts";
15
15
  import { resolveGenre } from "./resolve-genre.ts";
16
16
  import { runInitCommand } from "./commands/init-command.ts";
17
17
  import { runPluginTest } from "./commands/plugin-test.ts";
18
- import { targetsOf, withExperimental, withIncludes } from "./cli-args.ts";
18
+ import { targetsOf, withIncludes } from "./cli-args.ts";
19
+ import { guidesOfRun, type RunGuide } from "./genre-guide/report.ts";
20
+ import { showRules } from "./commands/rules.ts";
21
+ import { renderGuideBlock } from "./genre-guide/render.ts";
19
22
  import { rulesOf } from "./custom/load.ts";
20
23
  import { renderCompact } from "./render/compact.ts";
21
24
  import { renderExplain } from "./render/explain.ts";
@@ -23,8 +26,6 @@ import { renderFriendly } from "./render/friendly.ts";
23
26
  import { renderGenres } from "./render/genres.ts";
24
27
  import { loadGenres, presetLevels } from "./genre-load.ts";
25
28
  import { fileHeader } from "./file-header.ts";
26
- import { rulesJson } from "./render/rules-json.ts";
27
- import { rulesTable } from "./render/rules-table.ts";
28
29
  import { renderSarif } from "./render/sarif.ts";
29
30
  import { VERSION, VERSION_LINES } from "./version.ts";
30
31
  import type { TreeContext } from "./commands/tree.ts";
@@ -38,7 +39,7 @@ import { runConditions, runFeedback, settingsOf, type Checked } from "./commands
38
39
  import { homedir } from "node:os";
39
40
  import { settingWarnings } from "./config/warnings.ts";
40
41
  import { readConfigIn } from "./config/read.ts";
41
- import { optionLayersOf, settingSourcesOf } from "./config/option-problems.ts";
42
+ import { settingSourcesOf } from "./config/option-problems.ts";
42
43
  import { renderSummary, type FileOutcome } from "./render/summary.ts";
43
44
 
44
45
  import type { Finding, Level, RuleDefinition } from "./plugin.ts";
@@ -117,7 +118,7 @@ const present = (check: SourceCheck, argv: readonly string[]): Inspected => {
117
118
  genre,
118
119
  outcome: { path, findings: split.fresh, notRun: raw.skipped.length },
119
120
  perFile: { path, suppressed: applied.suppressed, reasonless: applied.unusedReasonless, notRun },
120
- checked: { findings: split.fresh, rules, language, genre, skipped: raw.skipped },
121
+ checked: { findings: split.fresh, rules, language, genre, genreFrom: from, skipped: raw.skipped },
121
122
  kept: applied.kept,
122
123
  };
123
124
  };
@@ -132,7 +133,7 @@ const inspectRun = async (paths: readonly string[], config: Config, argv: readon
132
133
  * 指摘を PR の変更行に出すための出口。--sarif <path> を書いたときだけ作る。
133
134
  * 端末の出力は変えない。CI で上げるためのファイルが増えるだけ。
134
135
  */
135
- const writeSarif = (results: readonly Inspected[], argv: readonly string[], config: Config): void => {
136
+ const writeSarif = (results: readonly Inspected[], argv: readonly string[], config: Config, guides: readonly RunGuide[]): void => {
136
137
  const path = flag(argv, "--sarif");
137
138
  if (path === undefined) return;
138
139
  // 文言はファイルの言語で描く。by_path で 1 つの repo に 2 言語が混ざるため。
@@ -141,7 +142,7 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[], conf
141
142
  );
142
143
  const notRun = results.flatMap((result) => result.checked.skipped.map((entry) => ({ path: result.outcome.path, rule: entry.rule, why: entry.why })));
143
144
  mkdirSync(dirname(path), { recursive: true });
144
- writeFileSync(path, renderSarif(located, VERSION, notRun), "utf8");
145
+ writeFileSync(path, renderSarif(located, VERSION, notRun, guides), "utf8");
145
146
  console.log(hostText(config).sarifWritten(path, located.length));
146
147
  };
147
148
 
@@ -169,7 +170,13 @@ const lint = async (targets: readonly string[], argv: readonly string[], config:
169
170
  }
170
171
  warnRuleProblems(config, language);
171
172
  const results = await inspectRun(paths, config, argv);
172
- writeSarif(results, argv, config);
173
+ const guides = guidesOfRun(
174
+ results.map((result) => result.checked),
175
+ config,
176
+ argv,
177
+ );
178
+ writeSarif(results, argv, config, guides);
179
+ if (!argv.includes("--compact")) guides.forEach((entry) => console.log(renderGuideBlock(entry.guide, entry.language).join("\n")));
173
180
  results.filter((result) => result.outcome.findings.length > 0 || paths.length === 1).forEach((result) => console.log(result.text));
174
181
  renderSummary(
175
182
  results.map((result) => result.outcome),
@@ -255,17 +262,6 @@ type Handler = (argv: readonly string[], config: Config) => number | Promise<num
255
262
  /** `--` で始まらない引数。対象のパス。 */
256
263
  const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
257
264
 
258
- /** `rules --json` for an AI to read; `rules` alone, a table for a person. */
259
- const showRules = (argv: readonly string[], written: Config): number => {
260
- const config = withExperimental(written, argv);
261
- const language = config.language ?? hostLanguage(undefined, process.env);
262
- warnRuleProblems(config, language);
263
- const genre = flag(argv, "--genre") ?? config.genre ?? "blog/tech";
264
- const rules = rulesOf(language, config);
265
- console.log(argv.includes("--json") ? rulesJson(rules, config, language, genre, optionLayersOf(config)) : rulesTable(rules, config, language, genre));
266
- return 0;
267
- };
268
-
269
265
  const showGenres = (config: Config): number => {
270
266
  console.log(renderGenres(loadGenres(), hostLanguage(config.language, process.env)));
271
267
  return 0;
@@ -284,7 +280,7 @@ const measureContext = (argv: readonly string[], config: Config): { config: Conf
284
280
  const HANDLERS: Readonly<Record<string, Handler>> = {
285
281
  init: (argv, config) => runInitCommand((name) => flag(argv, name), argv.includes("--plugin"), hostLanguage(config.language, process.env), process.cwd()),
286
282
  genres: (_argv, config) => showGenres(config),
287
- rules: showRules,
283
+ rules: (argv, config) => showRules(argv, config, { flag, warn: warnRuleProblems }),
288
284
  explain: (argv, config) => explain(config, argv[1], flag(argv, "--genre")),
289
285
  eval: (argv, config) => runEval(positional(argv), argv, { ...measureContext(argv, config), flag }),
290
286
  ...Object.fromEntries(Object.entries(DOCUMENT_COMMANDS).map(([name, run]): [string, Handler] => [name, (argv, config) => run(argv, treeContext(config))])),
@@ -9,6 +9,7 @@ import { excerptsAround } from "../feedback/excerpt.ts";
9
9
  import { feedbackDraft, type FeedbackDraft, type FeedbackKind, type ReportedFinding } from "../feedback/draft.ts";
10
10
  import { notRunAmong } from "../not-run.ts";
11
11
  import type { Skipped } from "../run.ts";
12
+ import type { GenreSource } from "../cli-text.ts";
12
13
 
13
14
  export const FEEDBACK_FILE = ".chaff-feedback.md";
14
15
  const NEW_ISSUE = "https://github.com/isamu/lab/issues/new";
@@ -20,6 +21,8 @@ export type Checked = {
20
21
  readonly rules: readonly RuleDefinition[];
21
22
  readonly language: string;
22
23
  readonly genre: string;
24
+ /** Where the genre came from: set (--genre, chaff.yaml, by_path, front matter), guessed or defaulted. */
25
+ readonly genreFrom: GenreSource;
23
26
  /** The rules this check did not run, and why. */
24
27
  readonly skipped: readonly Skipped[];
25
28
  /** How the check was run, as the reader of a report would repeat it (runConditions). */
@@ -8,6 +8,9 @@ import { chosenDepthOf } from "../fix-plan/chosen-depth.ts";
8
8
  import type { Checked } from "./feedback.ts";
9
9
  import { readDocument } from "./read-document.ts";
10
10
  import type { TreeContext } from "./tree.ts";
11
+ import { genreWasSet, reportedGuide, type ReportedGuide } from "../genre-guide/report.ts";
12
+ import { showsGuide } from "../cli-args.ts";
13
+ import type { Config } from "../config/load.ts";
11
14
 
12
15
  /** Options whose value is the next argument. That value is not a file. */
13
16
  const VALUED: ReadonlySet<string> = new Set(["--language", "--genre", "--depth"]);
@@ -24,6 +27,10 @@ export type FixPlanContext = TreeContext & {
24
27
  readonly check: (path: string) => Promise<Omit<Checked, "conditions">>;
25
28
  };
26
29
 
30
+ /** The genre's guide, the plan's first section; none when the genre was not set or the guide is off. */
31
+ const guideOf = (checked: Omit<Checked, "conditions">, config: Config, argv: readonly string[]): ReportedGuide | undefined =>
32
+ genreWasSet(checked.genreFrom) && showsGuide(argv) ? reportedGuide(config, checked.genre, checked.language, checked.rules) : undefined;
33
+
27
34
  /**
28
35
  * An instruction document for whoever rewrites the file: the findings grouped by rule, each rule's rewrite direction,
29
36
  * the document-level signals, the structure targets from human articles, a recommended mode and the checks to run after. Deterministic; sends nothing anywhere.
@@ -61,6 +68,7 @@ export const runFixPlan = async (targets: readonly string[], argv: readonly stri
61
68
  structure: structureOf(prose.doc),
62
69
  phrases: prose.doc.lexicons["ai-tell"] ?? [],
63
70
  chosenDepth: depth.chosen,
71
+ guide: guideOf(checked, context.config, argv),
64
72
  });
65
73
  console.log(argv.includes("--json") ? renderFixPlanJson(plan) : renderFixPlanMarkdown(plan));
66
74
  return 0;
@@ -0,0 +1,34 @@
1
+ import { showsGuide, withExperimental } from "../cli-args.ts";
2
+ import type { Config } from "../config/load.ts";
3
+ import { optionLayersOf } from "../config/option-problems.ts";
4
+ import { rulesOf } from "../custom/load.ts";
5
+ import { reportedGuide } from "../genre-guide/report.ts";
6
+ import { rulesJson } from "../render/rules-json.ts";
7
+ import { rulesTable } from "../render/rules-table.ts";
8
+ import { hostLanguage } from "../ui.ts";
9
+
10
+ /** The genre the rules are shown for when none is set. */
11
+ const SHOWN_GENRE = "blog/tech";
12
+
13
+ export type RulesContext = {
14
+ readonly flag: (argv: readonly string[], name: string) => string | undefined;
15
+ /** Says once, on stderr, what in the settings has no effect. */
16
+ readonly warn: (config: Config, language: string) => void;
17
+ };
18
+
19
+ /** `rules --json` for an AI to read, with the guide of a genre that was set; `rules` alone, a table for a person. */
20
+ export const showRules = (argv: readonly string[], written: Config, context: RulesContext): number => {
21
+ const config = withExperimental(written, argv);
22
+ const language = config.language ?? hostLanguage(undefined, process.env);
23
+ context.warn(config, language);
24
+ const setGenre = context.flag(argv, "--genre") ?? config.genre;
25
+ const genre = setGenre ?? SHOWN_GENRE;
26
+ const rules = rulesOf(language, config);
27
+ if (!argv.includes("--json")) {
28
+ console.log(rulesTable(rules, config, language, genre));
29
+ return 0;
30
+ }
31
+ const guide = setGenre !== undefined && showsGuide(argv) ? reportedGuide(config, genre, language, rules) : undefined;
32
+ console.log(rulesJson(rules, config, language, genre, optionLayersOf(config), guide));
33
+ return 0;
34
+ };
@@ -7,6 +7,7 @@ import type { Level } from "../plugin.ts";
7
7
  import type { PathRule } from "./by-path.ts";
8
8
  import type { Extensions } from "../extension/load.ts";
9
9
  import type { StyleLimits } from "../style-parse.ts";
10
+ import type { GuideLayer } from "../genre-guide/layer.ts";
10
11
 
11
12
  export const CONFIG_FILE = "chaff.yaml";
12
13
 
@@ -46,7 +47,7 @@ export type Config = {
46
47
  readonly unreadableOptions?: string | undefined;
47
48
  /** The house style chaff.yaml names (styles/*.yaml), as written. A value that is not a name is kept printed, to be reported. */
48
49
  readonly style?: string | undefined;
49
- /** The style once applied (config/style.ts): its id, the rules whose level it decided, and its options. */
50
+ /** The style once applied (config/style.ts): its id, the rules whose level it decided, its options and its guide. */
50
51
  readonly applied?: AppliedStyle | undefined;
51
52
  /** custom_rules as written: the team's own rules. custom/parse.ts reads and checks them. */
52
53
  readonly customRules?: unknown;
@@ -54,6 +55,8 @@ export type Config = {
54
55
  readonly grade?: unknown;
55
56
  /** fix_plan as written: the depth chaff fix-plan rewrites to (fix-plan/chosen-depth.ts reads and checks it). */
56
57
  readonly fixPlan?: unknown;
58
+ /** guide as written: the team's changes to the genre guides, or off (genre-guide/of-config.ts reads it). */
59
+ readonly guide?: unknown;
57
60
  /** plugins as written: package names and paths. extension/plugin-load.ts reads and checks them. */
58
61
  readonly plugins?: unknown;
59
62
  /** The code chaff.yaml names, once loaded (extension/load.ts). Reading chaff.yaml does not load it. */
@@ -66,6 +69,8 @@ export type AppliedStyle = {
66
69
  readonly options: Readonly<Record<string, unknown>>;
67
70
  /** The style's numbers, by rule and language, for the rules whose level it decided. */
68
71
  readonly limits: StyleLimits;
72
+ /** The style's changes to the genre guides, when it has any. */
73
+ readonly guide?: GuideLayer | undefined;
69
74
  };
70
75
 
71
76
  /** 判定の質が誤検知に直結するので、既定は最上位のモデル。cost は絞り込みで削る。spec §14。 */
@@ -200,6 +205,7 @@ export const loadConfig = (path: string): Config => {
200
205
  customRules: raw["custom_rules"],
201
206
  grade: raw["grade"],
202
207
  fixPlan: raw["fix_plan"],
208
+ guide: raw["guide"],
203
209
  plugins: raw["plugins"],
204
210
  };
205
211
  };
@@ -22,7 +22,7 @@ export const withStyle = (config: Config, styles: readonly StyleDefinition[]): C
22
22
  return {
23
23
  ...config,
24
24
  rules: { ...style.rules, ...config.rules },
25
- applied: { style: style.id, levelsFrom: fromStyle, options: style.options, limits },
25
+ applied: { style: style.id, levelsFrom: fromStyle, options: style.options, limits, guide: style.guide },
26
26
  };
27
27
  };
28
28
 
@@ -4,10 +4,12 @@ import type { UiLanguage } from "../ui.ts";
4
4
  import { ruleProblems } from "./rule-problems.ts";
5
5
  import { nameProblems } from "./name-problems.ts";
6
6
  import { configOptionProblems } from "./option-problems.ts";
7
+ import { configGuideProblems } from "../genre-guide/of-config.ts";
7
8
 
8
- /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply. Said before the results. */
9
+ /** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply, guides that cannot be read. Said before the results. */
9
10
  export const settingWarnings = (config: Config, rules: readonly RuleDefinition[], ui: UiLanguage): string[] => [
10
11
  ...ruleProblems(config, rules, ui),
11
12
  ...nameProblems(config, ui),
12
13
  ...configOptionProblems(config, rules, ui),
14
+ ...configGuideProblems(config, ui),
13
15
  ];
@@ -0,0 +1,74 @@
1
+ import type { Detector, Finding } from "../plugin.ts";
2
+ import { escapeRegExp } from "../orthography.ts";
3
+ import { formMinority } from "./form-minority.ts";
4
+ import { quoteAround } from "./quote-around.ts";
5
+
6
+ // One document writing an ellipsis two ways ("..." and "…", "……" and "・・・"). Neither is called right; the less common is
7
+ // reported. The forms come from each language's lexicon "ellipsis".
8
+
9
+ /** One ellipsis as written. form is the lexicon's pattern it matched. */
10
+ export type WrittenEllipsis = { readonly offset: number; readonly form: string };
11
+
12
+ /** Characters an ellipsis is made of. A longer run ("....", "…・") is not read, since its form is unclear. */
13
+ const ELLIPSIS_PART = "[..…・‥]";
14
+
15
+ /** A digit or a slash beside the dots: a range (1...10), a path (docs/.../index.md) or a page number after a leader (第1章・・・3). */
16
+ const NOT_PROSE_BESIDE = /[\p{N}/]/u;
17
+ /** The rest of a line after a leader in a table of contents ("Intro ... 12"). */
18
+ const PAGE_NUMBER_LINE = /^\s*\p{N}+\s*(?:\n|$)/u;
19
+ /** A word holding a scheme or a slash around the dots is an address (https://example.com/a...b). */
20
+ const ADDRESS_WORD = /:\/\/|\//u;
21
+
22
+ const SPACE = /\s/u;
23
+
24
+ /** The run of non-space characters holding the dots. */
25
+ const wordAround = (text: string, start: number, end: number): string => {
26
+ const from =
27
+ text
28
+ .slice(0, start)
29
+ .split("")
30
+ .findLastIndex((char) => SPACE.test(char)) + 1;
31
+ const after = text
32
+ .slice(end)
33
+ .split("")
34
+ .findIndex((char) => SPACE.test(char));
35
+ return text.slice(from, after === -1 ? text.length : end + after);
36
+ };
37
+
38
+ /** Dots that are not an ellipsis in running text: a range, a path or address, a table of contents leader. */
39
+ const isNotProse = (text: string, start: number, end: number): boolean =>
40
+ NOT_PROSE_BESIDE.test(text.charAt(start - 1)) ||
41
+ NOT_PROSE_BESIDE.test(text.charAt(end)) ||
42
+ PAGE_NUMBER_LINE.test(text.slice(end)) ||
43
+ ADDRESS_WORD.test(wordAround(text, start, end));
44
+
45
+ /** The ellipses in a text, matched longest form first, standing alone (not part of a longer run of dots). */
46
+ export const ellipsesIn = (text: string, forms: readonly string[]): WrittenEllipsis[] => {
47
+ if (forms.length === 0) return [];
48
+ const alternatives = forms
49
+ .toSorted((left, right) => right.length - left.length)
50
+ .map(escapeRegExp)
51
+ .join("|");
52
+ const pattern = new RegExp(`(?<!${ELLIPSIS_PART})(?:${alternatives})(?!${ELLIPSIS_PART})`, "gu");
53
+ return [...text.matchAll(pattern)].flatMap((match) =>
54
+ isNotProse(text, match.index, match.index + match[0].length) ? [] : [{ offset: match.index, form: match[0] }],
55
+ );
56
+ };
57
+
58
+ export const ellipsisConsistency: Detector = (doc, options): Finding[] => {
59
+ const text = doc.prose ?? doc.source;
60
+ const ellipses = ellipsesIn(
61
+ text,
62
+ (doc.lexicons["ellipsis"] ?? []).map((entry) => entry.pattern),
63
+ );
64
+ const minority = formMinority(ellipses, options.limit);
65
+ if (minority === undefined) return [];
66
+ return minority.odd.map((ellipsis) => ({
67
+ rule: "",
68
+ severity: "info",
69
+ line: 0,
70
+ column: 0,
71
+ quote: quoteAround(text, ellipsis.offset, ellipsis.offset + ellipsis.form.length),
72
+ values: { written: ellipsis.form, usual: minority.usual.form, count: minority.odd.length, of: ellipses.length, offset: ellipsis.offset },
73
+ }));
74
+ };
@@ -0,0 +1,4 @@
1
+ import type { Detector } from "../../plugin.ts";
2
+ import { ellipsisConsistency } from "../ellipsis.ts";
3
+
4
+ export const detector: Detector = ellipsisConsistency;
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import type { Config } from "../config/load.ts";
3
3
  import type { Detector, Lexicon, RuleDefinition } from "../plugin.ts";
4
4
  import type { StyleDefinition } from "../style-parse.ts";
5
+ import type { GuideLayer } from "../genre-guide/layer.ts";
5
6
  import { customRulesOf } from "../custom/load.ts";
6
7
  import { withStyle } from "../config/style.ts";
7
8
  import { moduleDetector } from "./module-detector.ts";
@@ -33,6 +34,8 @@ export type Extensions = {
33
34
  readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
34
35
  /** The plugins' house styles. */
35
36
  readonly styles: readonly StyleDefinition[];
37
+ /** The plugins' changes to the genre guides, in the order chaff.yaml lists the plugins. */
38
+ readonly guides?: readonly GuideLayer[];
36
39
  readonly problems: readonly LoadProblem[];
37
40
  readonly pluginProblems: readonly PluginProblem[];
38
41
  };
@@ -85,6 +88,7 @@ const loadExtensions = async (config: Config): Promise<Extensions> => {
85
88
  rules: plugins.flatMap((plugin) => plugin.rules),
86
89
  lexicons: lexiconsByLanguage(plugins),
87
90
  styles: plugins.flatMap((plugin) => plugin.styles),
91
+ guides: plugins.flatMap((plugin) => (plugin.guide === undefined ? [] : [plugin.guide])),
88
92
  problems: modules.flatMap((entry) => ("problem" in entry.loaded ? [entry.loaded.problem] : [])),
89
93
  pluginProblems,
90
94
  };
@@ -123,6 +123,7 @@ export const readPack = (dir: string, manifest: Readonly<Record<string, unknown>
123
123
  rules: rules.value,
124
124
  lexicons: lexicons.value,
125
125
  styles: styles.value,
126
+ ...(manifest["guide"] === undefined ? {} : { guide: manifest["guide"] }),
126
127
  },
127
128
  };
128
129
  };
@@ -19,7 +19,7 @@ export type LoadedPlugins = { readonly plugins: readonly ParsedPlugin[]; readonl
19
19
  /** pack: the folder of a rule pack (YAML only), which is read rather than imported. */
20
20
  type Located = { readonly origin: PluginOrigin; readonly pack?: string } | { readonly problem: PluginProblem };
21
21
 
22
- const problemOf = (kind: Exclude<PluginProblem["kind"], "rule">, plugin: string, detail = ""): { problem: PluginProblem } => ({
22
+ const problemOf = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, plugin: string, detail = ""): { problem: PluginProblem } => ({
23
23
  problem: { kind, plugin, detail },
24
24
  });
25
25
 
@@ -7,6 +7,7 @@ import type { UntrustedDetector } from "./module-detector.ts";
7
7
  import { describeValue } from "./returned-findings.ts";
8
8
  import { isPluginName } from "./plugin-name.ts";
9
9
  import { knownGenres } from "../known-genres.ts";
10
+ import { guideLayerOf, type GuideLayer, type GuideProblem } from "../genre-guide/layer.ts";
10
11
 
11
12
  // What a plugin package exports (api.ts PluginSpec), read into what chaff runs: its rules with ids under the plugin's
12
13
  // name, their detectors, its word lists and its house styles. A rule is read exactly like a rule in custom_rules, with
@@ -32,7 +33,8 @@ type PluginProblemKind =
32
33
  /** plugin: as chaff.yaml writes it. detail: what was there instead, the name expected, the rule, list or style at fault. */
33
34
  export type PluginProblem =
34
35
  | { readonly kind: PluginProblemKind; readonly plugin: string; readonly detail: string }
35
- | { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem };
36
+ | { readonly kind: "rule"; readonly plugin: string; readonly problem: CustomProblem }
37
+ | { readonly kind: "guide"; readonly plugin: string; readonly problem: GuideProblem };
36
38
 
37
39
  /** Where a plugin came from: as chaff.yaml writes it, the file imported, and the name its package's name gives (none for a path). */
38
40
  export type PluginOrigin = { readonly written: string; readonly file: string; readonly expectedName: string | undefined };
@@ -47,11 +49,13 @@ export type ParsedPlugin = {
47
49
  /** Word lists by language, then by name with the plugin's prefix. */
48
50
  readonly lexicons: Readonly<Record<string, Readonly<Record<string, Lexicon>>>>;
49
51
  readonly styles: readonly StyleDefinition[];
52
+ /** Its changes to the genre guides, applied whenever the plugin is loaded: under a style's and chaff.yaml's. */
53
+ readonly guide?: GuideLayer | undefined;
50
54
  };
51
55
 
52
56
  export type PluginRead = { readonly plugin: ParsedPlugin | undefined; readonly problems: readonly PluginProblem[] };
53
57
 
54
- type Problem = (kind: Exclude<PluginProblem["kind"], "rule">, detail: string) => PluginProblem;
58
+ type Problem = (kind: Exclude<PluginProblem["kind"], "rule" | "guide">, detail: string) => PluginProblem;
55
59
 
56
60
  const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
57
61
 
@@ -215,6 +219,16 @@ const stylesOf = (raw: unknown, name: string, problem: Problem): Styles => {
215
219
  };
216
220
  };
217
221
 
222
+ // ───────── guide ─────────
223
+
224
+ type Guide = { readonly guide: GuideLayer | undefined; readonly problems: readonly PluginProblem[] };
225
+
226
+ const guideOf = (raw: unknown, name: string, written: string): Guide => {
227
+ if (raw === undefined) return { guide: undefined, problems: [] };
228
+ const read = guideLayerOf(raw, `plugins: ${name}`, knownGenres());
229
+ return { guide: read.layer, problems: read.problems.map((problem) => ({ kind: "guide", plugin: written, problem })) };
230
+ };
231
+
218
232
  // ───────── the plugin ─────────
219
233
 
220
234
  /** A plugin's default export, read. Every problem is reported; a plugin with any is left out whole, and the run is stopped. */
@@ -229,8 +243,17 @@ export const parsePlugin = (exported: unknown, origin: PluginOrigin, useFor: rea
229
243
  const lexicons = lexiconsOf(exported["lexicons"], name, problem);
230
244
  const rules = rulesOf(exported["rules"], { name, origin, useFor, lists: lexicons.lists });
231
245
  const styles = stylesOf(exported["styles"], name, problem);
232
- const problems = [...rules.problems, ...lexicons.problems, ...styles.problems];
246
+ const guide = guideOf(exported["guide"], name, origin.written);
247
+ const problems = [...rules.problems, ...lexicons.problems, ...styles.problems, ...guide.problems];
233
248
  if (problems.length > 0) return { plugin: undefined, problems };
234
- const plugin = { name, written: origin.written, rules: rules.rules, detectors: rules.detectors, lexicons: lexicons.lexicons, styles: styles.styles };
249
+ const plugin = {
250
+ name,
251
+ written: origin.written,
252
+ rules: rules.rules,
253
+ detectors: rules.detectors,
254
+ lexicons: lexicons.lexicons,
255
+ styles: styles.styles,
256
+ ...(guide.guide === undefined ? {} : { guide: guide.guide }),
257
+ };
235
258
  return { plugin, problems: [] };
236
259
  };