chaffjs 0.22.0 → 0.24.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 (267) 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/bare-comparative.d.ts +26 -0
  36. package/dist/detectors/bare-comparative.d.ts.map +1 -0
  37. package/dist/detectors/bare-comparative.js +117 -0
  38. package/dist/detectors/bare-comparative.js.map +1 -0
  39. package/dist/detectors/chat-citation.d.ts +6 -0
  40. package/dist/detectors/chat-citation.d.ts.map +1 -1
  41. package/dist/detectors/chat-citation.js +26 -1
  42. package/dist/detectors/chat-citation.js.map +1 -1
  43. package/dist/detectors/clause-shape.d.ts.map +1 -1
  44. package/dist/detectors/clause-shape.js +19 -5
  45. package/dist/detectors/clause-shape.js.map +1 -1
  46. package/dist/detectors/definition-use.d.ts.map +1 -1
  47. package/dist/detectors/definition-use.js +4 -2
  48. package/dist/detectors/definition-use.js.map +1 -1
  49. package/dist/detectors/ellipsis.d.ts +10 -0
  50. package/dist/detectors/ellipsis.d.ts.map +1 -0
  51. package/dist/detectors/ellipsis.js +56 -0
  52. package/dist/detectors/ellipsis.js.map +1 -0
  53. package/dist/detectors/generic-heading.d.ts +8 -0
  54. package/dist/detectors/generic-heading.d.ts.map +1 -0
  55. package/dist/detectors/generic-heading.js +26 -0
  56. package/dist/detectors/generic-heading.js.map +1 -0
  57. package/dist/detectors/ime-residue.d.ts +13 -0
  58. package/dist/detectors/ime-residue.d.ts.map +1 -0
  59. package/dist/detectors/ime-residue.js +27 -0
  60. package/dist/detectors/ime-residue.js.map +1 -0
  61. package/dist/detectors/lexicon.d.ts.map +1 -1
  62. package/dist/detectors/lexicon.js +9 -2
  63. package/dist/detectors/lexicon.js.map +1 -1
  64. package/dist/detectors/lookalike-character.d.ts +18 -0
  65. package/dist/detectors/lookalike-character.d.ts.map +1 -0
  66. package/dist/detectors/lookalike-character.js +50 -0
  67. package/dist/detectors/lookalike-character.js.map +1 -0
  68. package/dist/detectors/lowercase-name.d.ts +9 -0
  69. package/dist/detectors/lowercase-name.d.ts.map +1 -0
  70. package/dist/detectors/lowercase-name.js +40 -0
  71. package/dist/detectors/lowercase-name.js.map +1 -0
  72. package/dist/detectors/registry/bare-comparative.d.ts +3 -0
  73. package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
  74. package/dist/detectors/registry/bare-comparative.js +3 -0
  75. package/dist/detectors/registry/bare-comparative.js.map +1 -0
  76. package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
  77. package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
  78. package/dist/detectors/registry/calendar-name-case.js +3 -0
  79. package/dist/detectors/registry/calendar-name-case.js.map +1 -0
  80. package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
  81. package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
  82. package/dist/detectors/registry/ellipsis-consistency.js +3 -0
  83. package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
  84. package/dist/detectors/registry/generic-heading.d.ts +3 -0
  85. package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
  86. package/dist/detectors/registry/generic-heading.js +3 -0
  87. package/dist/detectors/registry/generic-heading.js.map +1 -0
  88. package/dist/detectors/registry/ime-residue.d.ts +3 -0
  89. package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
  90. package/dist/detectors/registry/ime-residue.js +3 -0
  91. package/dist/detectors/registry/ime-residue.js.map +1 -0
  92. package/dist/detectors/registry/lookalike-character.d.ts +3 -0
  93. package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
  94. package/dist/detectors/registry/lookalike-character.js +3 -0
  95. package/dist/detectors/registry/lookalike-character.js.map +1 -0
  96. package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
  97. package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
  98. package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
  99. package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
  100. package/dist/detectors/signals.d.ts.map +1 -1
  101. package/dist/detectors/signals.js +8 -4
  102. package/dist/detectors/signals.js.map +1 -1
  103. package/dist/detectors/structure-tree.d.ts.map +1 -1
  104. package/dist/detectors/structure-tree.js +32 -7
  105. package/dist/detectors/structure-tree.js.map +1 -1
  106. package/dist/detectors/technical-docs.d.ts.map +1 -1
  107. package/dist/detectors/technical-docs.js +14 -0
  108. package/dist/detectors/technical-docs.js.map +1 -1
  109. package/dist/detectors/unit-spacing.d.ts +25 -0
  110. package/dist/detectors/unit-spacing.d.ts.map +1 -0
  111. package/dist/detectors/unit-spacing.js +50 -0
  112. package/dist/detectors/unit-spacing.js.map +1 -0
  113. package/dist/extension/load.d.ts +3 -0
  114. package/dist/extension/load.d.ts.map +1 -1
  115. package/dist/extension/load.js +1 -0
  116. package/dist/extension/load.js.map +1 -1
  117. package/dist/extension/pack-read.d.ts.map +1 -1
  118. package/dist/extension/pack-read.js +1 -0
  119. package/dist/extension/pack-read.js.map +1 -1
  120. package/dist/extension/plugin-load.js.map +1 -1
  121. package/dist/extension/plugin-parse.d.ts +7 -0
  122. package/dist/extension/plugin-parse.d.ts.map +1 -1
  123. package/dist/extension/plugin-parse.js +18 -2
  124. package/dist/extension/plugin-parse.js.map +1 -1
  125. package/dist/extension/problem-text.d.ts.map +1 -1
  126. package/dist/extension/problem-text.js +5 -0
  127. package/dist/extension/problem-text.js.map +1 -1
  128. package/dist/fix-plan/plan.d.ts +4 -0
  129. package/dist/fix-plan/plan.d.ts.map +1 -1
  130. package/dist/fix-plan/plan.js +1 -0
  131. package/dist/fix-plan/plan.js.map +1 -1
  132. package/dist/fix-plan/render.d.ts.map +1 -1
  133. package/dist/fix-plan/render.js +9 -0
  134. package/dist/fix-plan/render.js.map +1 -1
  135. package/dist/fix-plan/text.d.ts +4 -0
  136. package/dist/fix-plan/text.d.ts.map +1 -1
  137. package/dist/fix-plan/text.js +8 -2
  138. package/dist/fix-plan/text.js.map +1 -1
  139. package/dist/genre-guide/key-rules.d.ts +10 -0
  140. package/dist/genre-guide/key-rules.d.ts.map +1 -0
  141. package/dist/genre-guide/key-rules.js +23 -0
  142. package/dist/genre-guide/key-rules.js.map +1 -0
  143. package/dist/genre-guide/layer.d.ts +44 -0
  144. package/dist/genre-guide/layer.d.ts.map +1 -0
  145. package/dist/genre-guide/layer.js +43 -0
  146. package/dist/genre-guide/layer.js.map +1 -0
  147. package/dist/genre-guide/lines.d.ts +15 -0
  148. package/dist/genre-guide/lines.d.ts.map +1 -0
  149. package/dist/genre-guide/lines.js +39 -0
  150. package/dist/genre-guide/lines.js.map +1 -0
  151. package/dist/genre-guide/of-config.d.ts +13 -0
  152. package/dist/genre-guide/of-config.d.ts.map +1 -0
  153. package/dist/genre-guide/of-config.js +18 -0
  154. package/dist/genre-guide/of-config.js.map +1 -0
  155. package/dist/genre-guide/problem-text.d.ts +5 -0
  156. package/dist/genre-guide/problem-text.d.ts.map +1 -0
  157. package/dist/genre-guide/problem-text.js +29 -0
  158. package/dist/genre-guide/problem-text.js.map +1 -0
  159. package/dist/genre-guide/render.d.ts +5 -0
  160. package/dist/genre-guide/render.d.ts.map +1 -0
  161. package/dist/genre-guide/render.js +38 -0
  162. package/dist/genre-guide/render.js.map +1 -0
  163. package/dist/genre-guide/report.d.ts +32 -0
  164. package/dist/genre-guide/report.d.ts.map +1 -0
  165. package/dist/genre-guide/report.js +42 -0
  166. package/dist/genre-guide/report.js.map +1 -0
  167. package/dist/genre-guide/resolve.d.ts +16 -0
  168. package/dist/genre-guide/resolve.d.ts.map +1 -0
  169. package/dist/genre-guide/resolve.js +31 -0
  170. package/dist/genre-guide/resolve.js.map +1 -0
  171. package/dist/genre-parse.d.ts +5 -1
  172. package/dist/genre-parse.d.ts.map +1 -1
  173. package/dist/genre-parse.js +8 -1
  174. package/dist/genre-parse.js.map +1 -1
  175. package/dist/init.d.ts.map +1 -1
  176. package/dist/init.js +14 -0
  177. package/dist/init.js.map +1 -1
  178. package/dist/quoted-span.d.ts +7 -0
  179. package/dist/quoted-span.d.ts.map +1 -1
  180. package/dist/quoted-span.js +20 -0
  181. package/dist/quoted-span.js.map +1 -1
  182. package/dist/render/rules-json.d.ts +2 -1
  183. package/dist/render/rules-json.d.ts.map +1 -1
  184. package/dist/render/rules-json.js +3 -1
  185. package/dist/render/rules-json.js.map +1 -1
  186. package/dist/render/sarif.d.ts +3 -1
  187. package/dist/render/sarif.d.ts.map +1 -1
  188. package/dist/render/sarif.js +3 -1
  189. package/dist/render/sarif.js.map +1 -1
  190. package/dist/structure/definition-use.d.ts +14 -4
  191. package/dist/structure/definition-use.d.ts.map +1 -1
  192. package/dist/structure/definition-use.js +29 -9
  193. package/dist/structure/definition-use.js.map +1 -1
  194. package/dist/style-parse.d.ts +3 -0
  195. package/dist/style-parse.d.ts.map +1 -1
  196. package/dist/style-parse.js +18 -2
  197. package/dist/style-parse.js.map +1 -1
  198. package/genres.yaml +329 -0
  199. package/package.json +3 -3
  200. package/rules/calendar-name-case.yaml +55 -0
  201. package/rules/comparative-without-baseline.yaml +71 -0
  202. package/rules/cross-doc-broken-link.yaml +1 -1
  203. package/rules/cross-doc-duplicate-definition.yaml +1 -1
  204. package/rules/cross-doc-fact-conflict.yaml +1 -1
  205. package/rules/cross-doc-term-variant.yaml +5 -3
  206. package/rules/ellipsis-consistency.yaml +73 -0
  207. package/rules/figurative-density.yaml +84 -0
  208. package/rules/ime-residue.yaml +55 -0
  209. package/rules/lookalike-character.yaml +82 -0
  210. package/rules/numbered-generic-heading.yaml +111 -0
  211. package/rules/undefined-acronym.yaml +1 -0
  212. package/rules/unit-spacing-consistency.yaml +74 -0
  213. package/skills/chaff/SKILL.md +6 -0
  214. package/src/api.ts +13 -0
  215. package/src/cli-args.ts +3 -0
  216. package/src/cli-text.ts +2 -0
  217. package/src/cli.ts +16 -20
  218. package/src/commands/feedback.ts +3 -0
  219. package/src/commands/fix-plan.ts +8 -0
  220. package/src/commands/rules.ts +34 -0
  221. package/src/config/load.ts +7 -1
  222. package/src/config/style.ts +1 -1
  223. package/src/config/warnings.ts +3 -1
  224. package/src/detectors/bare-comparative.ts +152 -0
  225. package/src/detectors/chat-citation.ts +28 -1
  226. package/src/detectors/clause-shape.ts +20 -6
  227. package/src/detectors/definition-use.ts +13 -3
  228. package/src/detectors/ellipsis.ts +74 -0
  229. package/src/detectors/generic-heading.ts +30 -0
  230. package/src/detectors/ime-residue.ts +37 -0
  231. package/src/detectors/lexicon.ts +11 -2
  232. package/src/detectors/lookalike-character.ts +75 -0
  233. package/src/detectors/lowercase-name.ts +58 -0
  234. package/src/detectors/registry/bare-comparative.ts +4 -0
  235. package/src/detectors/registry/calendar-name-case.ts +4 -0
  236. package/src/detectors/registry/ellipsis-consistency.ts +4 -0
  237. package/src/detectors/registry/generic-heading.ts +4 -0
  238. package/src/detectors/registry/ime-residue.ts +4 -0
  239. package/src/detectors/registry/lookalike-character.ts +4 -0
  240. package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
  241. package/src/detectors/signals.ts +9 -5
  242. package/src/detectors/structure-tree.ts +55 -30
  243. package/src/detectors/technical-docs.ts +13 -0
  244. package/src/detectors/unit-spacing.ts +71 -0
  245. package/src/extension/load.ts +4 -0
  246. package/src/extension/pack-read.ts +1 -0
  247. package/src/extension/plugin-load.ts +1 -1
  248. package/src/extension/plugin-parse.ts +27 -4
  249. package/src/extension/problem-text.ts +7 -1
  250. package/src/fix-plan/plan.ts +5 -0
  251. package/src/fix-plan/render.ts +9 -0
  252. package/src/fix-plan/text.ts +12 -2
  253. package/src/genre-guide/key-rules.ts +37 -0
  254. package/src/genre-guide/layer.ts +77 -0
  255. package/src/genre-guide/lines.ts +41 -0
  256. package/src/genre-guide/of-config.ts +26 -0
  257. package/src/genre-guide/problem-text.ts +34 -0
  258. package/src/genre-guide/render.ts +50 -0
  259. package/src/genre-guide/report.ts +63 -0
  260. package/src/genre-guide/resolve.ts +36 -0
  261. package/src/genre-parse.ts +13 -3
  262. package/src/init.ts +14 -0
  263. package/src/quoted-span.ts +25 -0
  264. package/src/render/rules-json.ts +4 -0
  265. package/src/render/sarif.ts +4 -1
  266. package/src/structure/definition-use.ts +42 -10
  267. package/src/style-parse.ts +19 -1
@@ -4,6 +4,7 @@ import type { Texts, UiLanguage } from "../ui.ts";
4
4
  import { customProblemSentence } from "../custom/problems.ts";
5
5
  import type { LoadProblem } from "./load.ts";
6
6
  import type { PluginProblem } from "./plugin-parse.ts";
7
+ import { guideProblemText } from "../genre-guide/problem-text.ts";
7
8
 
8
9
  // What in the code chaff.yaml names could not be loaded, as sentences. Each names the rule or plugin and the file or
9
10
  // package, as chaff.yaml writes it.
@@ -11,7 +12,9 @@ import type { PluginProblem } from "./plugin-parse.ts";
11
12
  type ModuleText = Readonly<Record<LoadProblem["kind"], (rule: string, file: string, detail: string) => string>>;
12
13
 
13
14
  type PluginText = {
14
- readonly problems: Readonly<Record<Exclude<PluginProblem["kind"], "rule">, (plugin: string, detail: string) => string>>;
15
+ readonly problems: Readonly<Record<Exclude<PluginProblem["kind"], "rule" | "guide">, (plugin: string, detail: string) => string>>;
16
+ /** Where a plugin's guide is, for a problem chaff.yaml's guide could have too. */
17
+ readonly guideScope: (plugin: string) => string;
15
18
  /** Where a plugin's rule is, for a problem a rule in custom_rules could have too. */
16
19
  readonly ruleScope: (plugin: string) => string;
17
20
  };
@@ -42,6 +45,7 @@ const MODULE_TEXT: Texts<ModuleText> = {
42
45
  const PLUGIN_TEXT: Texts<PluginText> = {
43
46
  ja: {
44
47
  ruleScope: (plugin) => `plugins の ${plugin} の rules`,
48
+ guideScope: (plugin) => `plugins の ${plugin}`,
45
49
  problems: {
46
50
  "not-a-list": (_plugin, detail) => `plugins はプラグインの並び(- chaff-plugin-foo や - ./local-plugin)で書いてください(${detail})`,
47
51
  "bad-specifier": (plugin) =>
@@ -65,6 +69,7 @@ const PLUGIN_TEXT: Texts<PluginText> = {
65
69
  },
66
70
  en: {
67
71
  ruleScope: (plugin) => `plugins ${plugin} rules`,
72
+ guideScope: (plugin) => `plugins ${plugin}`,
68
73
  problems: {
69
74
  "not-a-list": (_plugin, detail) => `write plugins as a list (- chaff-plugin-foo or - ./local-plugin), not ${detail}`,
70
75
  "bad-specifier": (plugin) =>
@@ -90,6 +95,7 @@ const PLUGIN_TEXT: Texts<PluginText> = {
90
95
  const pluginSentence = (problem: PluginProblem, ui: UiLanguage): string => {
91
96
  const text = PLUGIN_TEXT[ui];
92
97
  if (problem.kind === "rule") return customProblemSentence(problem.problem, ui, text.ruleScope(problem.plugin));
98
+ if (problem.kind === "guide") return `${text.guideScope(problem.plugin)}: ${guideProblemText(problem.problem, ui)}`;
93
99
  return text.problems[problem.kind](problem.plugin, problem.detail);
94
100
  };
95
101
 
@@ -8,6 +8,7 @@ import { COMPOSITE_RULE, recommendMode, type ModeChoice } from "./mode.ts";
8
8
  import type { StructureScore } from "../structure-shape/score.ts";
9
9
  import { structureTargetsOf, type StructureTarget } from "./structure-targets.ts";
10
10
  import { DEFAULT_DEPTH, depthIncludes, type RewriteDepth } from "../rewrite-depth.ts";
11
+ import type { ReportedGuide } from "../genre-guide/report.ts";
11
12
 
12
13
  /** One flagged spot, where it is and what chaff said about it. */
13
14
  export type PlanSpot = { readonly line: number; readonly column: number; readonly quote: string; readonly message: string };
@@ -46,6 +47,8 @@ export type FixPlan = {
46
47
  readonly rewrittenPath: string;
47
48
  readonly language: string;
48
49
  readonly genre: string;
50
+ /** What a good document of the genre does, to rewrite against; null when the genre was not set or the guide is off. */
51
+ readonly guide: ReportedGuide | null;
49
52
  readonly experimental: boolean;
50
53
  readonly mode: ModeChoice;
51
54
  /** None when chaff recommends the depth itself. */
@@ -80,6 +83,7 @@ export type PlanInput = {
80
83
  /** The ai-tell lexicon of the document's language, whose entries may carry their own hint. */
81
84
  readonly phrases: Lexicon;
82
85
  readonly chosenDepth?: ChosenDepth | undefined;
86
+ readonly guide?: ReportedGuide | undefined;
83
87
  };
84
88
 
85
89
  const BOLD_DENSITY_RULE = "bold-density";
@@ -220,6 +224,7 @@ export const buildFixPlan = (input: PlanInput): FixPlan => {
220
224
  rewrittenPath: rewrittenPathOf(input.path),
221
225
  language: input.language,
222
226
  genre: input.genre,
227
+ guide: input.guide ?? null,
223
228
  experimental: input.experimental,
224
229
  mode,
225
230
  chosenDepth: input.chosenDepth,
@@ -129,6 +129,14 @@ const checkLines = (plan: FixPlan, text: FixPlanText): string[] => [
129
129
  text.checksNote,
130
130
  ];
131
131
 
132
+ /** The genre's guide, first: the instruction the rewrite is held to. Nothing when the genre was not set or the guide is off. */
133
+ const guideLines = (plan: FixPlan, text: FixPlanText): string[] => {
134
+ const { guide } = plan;
135
+ if (guide === null) return [];
136
+ const rules = guide.rules.length === 0 ? [] : ["", text.guideRules(guide.rules.map((rule) => `\`${rule}\``).join(", "))];
137
+ return [`## ${text.guideHeading(guide.name, guide.genre)}`, "", text.guideLead, "", ...bullets(guide.lines), ...rules, ""];
138
+ };
139
+
132
140
  const textOf = (plan: FixPlan): FixPlanText => FIX_PLAN_TEXT[uiLanguageOf(plan.language)];
133
141
 
134
142
  /** The plan as Markdown, in the document's language: what an agent reads before rewriting. */
@@ -141,6 +149,7 @@ export const renderFixPlanMarkdown = (plan: FixPlan): string => {
141
149
  "",
142
150
  text.intro,
143
151
  "",
152
+ ...guideLines(plan, text),
144
153
  `## ${text.constraintsHeading}`,
145
154
  "",
146
155
  ...text.constraints.map((constraint, index) => `${String(index + 1)}. ${constraint}`),
@@ -7,6 +7,10 @@ export type FixPlanText = {
7
7
  readonly title: (path: string) => string;
8
8
  readonly about: (language: string, genre: string) => string;
9
9
  readonly intro: string;
10
+ /** The genre's guide, as the first section: what the rewrite is held to. */
11
+ readonly guideHeading: (name: string, genre: string) => string;
12
+ readonly guideLead: string;
13
+ readonly guideRules: (ids: string) => string;
10
14
  readonly constraintsHeading: string;
11
15
  readonly constraints: readonly string[];
12
16
  readonly modeHeading: string;
@@ -43,12 +47,15 @@ export type FixPlanText = {
43
47
 
44
48
  export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
45
49
  ja: {
46
- usage: "使い方: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <ジャンル>] [--json]",
50
+ usage: "使い方: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <ジャンル>] [--no-guide] [--json]",
47
51
  notFound: (path) => `${path} がありません。`,
48
52
  title: (path) => `直す計画: ${path}`,
49
53
  about: (language, genre) => `言語 ${language}、ジャンル ${genre}`,
50
54
  intro:
51
55
  "chaff が機械で見つけた箇所と、それぞれの直す方向です。chaff は書き直しません。書き直すのは、この計画を読む人か AI です。直したら、最後の確かめのコマンドを実行してください。",
56
+ guideHeading: (name, genre) => `指針: ${name}(${genre})`,
57
+ guideLead: "このジャンルのよい文書が満たすことです。書き直しはこれに沿わせ、書き終えたら一つずつ確かめてください。",
58
+ guideRules: (ids) => `このジャンルで特に効く rule: ${ids}`,
52
59
  constraintsHeading: "守ること",
53
60
  constraints: [
54
61
  "事実、数、日付、条件、名前を変えない。",
@@ -109,12 +116,15 @@ export const FIX_PLAN_TEXT: Texts<FixPlanText> = {
109
116
  "1 つめは書き直した文書の指摘、2 つめは事実が落ちていないか・足されていないか、3 つめは構成の変化です。数は chaff の出力から取り、形容詞で言わないでください。",
110
117
  },
111
118
  en: {
112
- usage: "usage: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <genre>] [--json]",
119
+ usage: "usage: chaff fix-plan <file> [--depth light|structure|register] [--experimental] [--genre <genre>] [--no-guide] [--json]",
113
120
  notFound: (path) => `${path} does not exist.`,
114
121
  title: (path) => `Fix plan: ${path}`,
115
122
  about: (language, genre) => `language ${language}, genre ${genre}`,
116
123
  intro:
117
124
  "What chaff found by machine, and how to rewrite each kind of spot. chaff does not rewrite; whoever reads this plan does, a person or an AI. When done, run the checks at the end.",
125
+ guideHeading: (name, genre) => `Guide: ${name} (${genre})`,
126
+ guideLead: "What a good document of this genre does. Rewrite toward it, and check the result against each line when done.",
127
+ guideRules: (ids) => `Rules that matter most for this genre: ${ids}`,
118
128
  constraintsHeading: "Constraints",
119
129
  constraints: [
120
130
  "Keep every fact, number, date, condition and name.",
@@ -0,0 +1,37 @@
1
+ import { presetLevelsOf, type GenreData } from "../genre-parse.ts";
2
+ import type { Level, RuleDefinition } from "../plugin.ts";
3
+ import { standingIn } from "../rule-genres.ts";
4
+
5
+ /** A rule that runs in at most this share of the genres speaks for the few it runs in. */
6
+ const RARE_SHARE = 0.25;
7
+
8
+ type RuleFacts = Pick<RuleDefinition, "id" | "status" | "use_for" | "languages">;
9
+
10
+ const runsIn = (rule: RuleFacts, data: GenreData, genre: string): boolean => standingIn(rule, genre, presetLevelsOf(data, genre)).kind === "on";
11
+
12
+ const isRare = (rule: RuleFacts, data: GenreData): boolean =>
13
+ data.genres.filter((entry) => runsIn(rule, data, entry.id)).length <= data.genres.length * RARE_SHARE;
14
+
15
+ const readsLanguage = (rule: RuleFacts, language: string): boolean => rule.languages === undefined || rule.languages.includes(language);
16
+
17
+ /**
18
+ * The rules that matter most for a genre, by id: those its preset turns on, and those that run in it but in few other
19
+ * genres. A rule the team's settings turn off is left out. Pure: the genres and the settings come in.
20
+ */
21
+ export const keyRulesOf = (
22
+ rules: readonly RuleFacts[],
23
+ data: GenreData,
24
+ genre: string,
25
+ language: string,
26
+ settings: Readonly<Record<string, Level>>,
27
+ ): string[] => {
28
+ const preset = presetLevelsOf(data, genre);
29
+ const matters = (rule: RuleFacts): boolean => {
30
+ const standing = standingIn(rule, genre, preset);
31
+ return standing.kind === "on" && (standing.byPreset || isRare(rule, data));
32
+ };
33
+ return rules
34
+ .filter((rule) => readsLanguage(rule, language) && settings[rule.id] !== "off" && matters(rule))
35
+ .map((rule) => rule.id)
36
+ .toSorted((left, right) => left.localeCompare(right, "en"));
37
+ };
@@ -0,0 +1,77 @@
1
+ import { guideLinesOf, type GuideLines } from "./lines.ts";
2
+
3
+ // A team's changes to the genre guides, as chaff.yaml, a style or a rule pack writes them under guide:
4
+ //
5
+ // guide:
6
+ // blog/tech: # a genre, or a group (legal) for every genre in it
7
+ // add: { ja: [...], en: [...] }
8
+ // legal/contract:
9
+ // replace: { ja: [...], en: [...] }
10
+ // business/email: off
11
+ //
12
+ // replace sets the lines for the languages it gives (the others keep theirs); add puts lines after them; off leaves the
13
+ // genre with no guide. A text or a list alone, without ja: or en:, is the same in every language. guide: off turns every
14
+ // genre's guide off; a stronger layer may still add lines back.
15
+
16
+ export type GuideEdit = { readonly kind: "off" } | { readonly kind: "edit"; readonly replace?: GuideLines; readonly add?: GuideLines };
17
+
18
+ /**
19
+ * One place's changes, by genre or group, and the place as a reader is told it (chaff.yaml, style: house, plugins: foo).
20
+ * allOff: guide: off, which empties every genre's guide before this layer's own entries.
21
+ */
22
+ export type GuideLayer = { readonly from: string; readonly allOff: boolean; readonly edits: Readonly<Record<string, GuideEdit>> };
23
+
24
+ /** What could not be read: the genre or group it was written under (none when guide: itself is wrong), and what was there. */
25
+ export type GuideProblem =
26
+ | { readonly kind: "not-a-map"; readonly written: string }
27
+ | { readonly kind: "unknown-genre"; readonly genre: string }
28
+ | { readonly kind: "bad-entry"; readonly genre: string; readonly written: string }
29
+ | { readonly kind: "bad-lines"; readonly genre: string; readonly field: "replace" | "add" };
30
+
31
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
32
+
33
+ const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
34
+
35
+ /** Whether guide: as written turns every guide off (guide: off, or false). */
36
+ export const turnsGuidesOff = (raw: unknown): boolean => raw === "off" || raw === false;
37
+
38
+ const EDIT_FIELDS: ReadonlySet<string> = new Set(["replace", "add"]);
39
+
40
+ type Read = { readonly edit: GuideEdit } | { readonly problem: GuideProblem };
41
+
42
+ type LinesRead = { readonly lines: GuideLines | undefined } | { readonly problem: GuideProblem };
43
+
44
+ const linesIn = (raw: Record<string, unknown>, field: "replace" | "add", genre: string): LinesRead => {
45
+ if (raw[field] === undefined) return { lines: undefined };
46
+ const lines = guideLinesOf(raw[field]);
47
+ return lines === undefined ? { problem: { kind: "bad-lines", genre, field } } : { lines };
48
+ };
49
+
50
+ const editOf = (genre: string, value: unknown): Read => {
51
+ if (turnsGuidesOff(value)) return { edit: { kind: "off" } };
52
+ const fields = isRecord(value) ? Object.keys(value) : [];
53
+ if (!isRecord(value) || fields.length === 0 || fields.some((field) => !EDIT_FIELDS.has(field))) {
54
+ return { problem: { kind: "bad-entry", genre, written: printed(value) } };
55
+ }
56
+ const replace = linesIn(value, "replace", genre);
57
+ if ("problem" in replace) return replace;
58
+ const add = linesIn(value, "add", genre);
59
+ if ("problem" in add) return add;
60
+ return { edit: { kind: "edit", ...(replace.lines === undefined ? {} : { replace: replace.lines }), ...(add.lines === undefined ? {} : { add: add.lines }) } };
61
+ };
62
+
63
+ /**
64
+ * guide: as written, read into a layer. known: the genres and groups an entry may name. An entry that cannot be read is
65
+ * left out and reported; the rest still apply. Nothing written is an empty layer.
66
+ */
67
+ export const guideLayerOf = (raw: unknown, from: string, known: readonly string[]): { layer: GuideLayer; problems: GuideProblem[] } => {
68
+ if (raw === undefined || raw === null || turnsGuidesOff(raw)) return { layer: { from, allOff: turnsGuidesOff(raw), edits: {} }, problems: [] };
69
+ if (!isRecord(raw)) return { layer: { from, allOff: false, edits: {} }, problems: [{ kind: "not-a-map", written: printed(raw) }] };
70
+ const read = Object.entries(raw).map(([genre, value]): readonly [string, Read] =>
71
+ known.includes(genre) ? [genre, editOf(genre, value)] : [genre, { problem: { kind: "unknown-genre", genre } }],
72
+ );
73
+ return {
74
+ layer: { from, allOff: false, edits: Object.fromEntries(read.flatMap(([genre, entry]) => ("edit" in entry ? [[genre, entry.edit]] : []))) },
75
+ problems: read.flatMap(([, entry]) => ("problem" in entry ? [entry.problem] : [])),
76
+ };
77
+ };
@@ -0,0 +1,41 @@
1
+ // What a good document of a genre looks like, as lines an AI or a person checks a draft against: genres.yaml's guide:,
2
+ // and the replace / add a team writes over it. Pure: the YAML comes in already parsed.
3
+
4
+ /** The guide's lines by language: { ja: [...], en: [...] }. */
5
+ export type GuideLines = Readonly<Record<string, readonly string[]>>;
6
+
7
+ /** The languages a guide is written in: chaff's. A text written without ja: or en: is given to each. */
8
+ export const GUIDE_LANGUAGES: readonly string[] = ["ja", "en"];
9
+
10
+ const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
11
+
12
+ const isLine = (value: unknown): value is string => typeof value === "string" && value.trim() !== "";
13
+
14
+ /** One line, or a list of them. undefined when anything in it is not a line of text. */
15
+ export const linesOf = (value: unknown): string[] | undefined => {
16
+ if (isLine(value)) return [value.trim()];
17
+ if (!Array.isArray(value) || value.length === 0) return undefined;
18
+ const entries: unknown[] = value;
19
+ return entries.every(isLine) ? entries.map((entry) => entry.trim()) : undefined;
20
+ };
21
+
22
+ /**
23
+ * Lines by language as a team writes them: { ja: ..., en: ... }, each one line or a list; or one line or a list alone,
24
+ * which reads the same in every language. undefined when it cannot be read, a language chaff has no guide in included:
25
+ * lines in it would never be shown.
26
+ */
27
+ export const guideLinesOf = (value: unknown): GuideLines | undefined => {
28
+ const everywhere = linesOf(value);
29
+ if (everywhere !== undefined) return Object.fromEntries(GUIDE_LANGUAGES.map((language) => [language, everywhere]));
30
+ if (!isRecord(value) || Object.keys(value).length === 0 || Object.keys(value).some((key) => !GUIDE_LANGUAGES.includes(key))) return undefined;
31
+ const read = Object.entries(value).map(([language, lines]) => [language, linesOf(lines)] as const);
32
+ return read.every((entry): entry is readonly [string, string[]] => entry[1] !== undefined) ? Object.fromEntries(read) : undefined;
33
+ };
34
+
35
+ /** A bundled guide: a list of lines in ja and in en. Throws, since the file ships with chaff and a wrong entry is chaff's bug. */
36
+ export const bundledGuideOf = (value: unknown, where: string): GuideLines | undefined => {
37
+ if (value === undefined) return undefined;
38
+ const lines = isRecord(value) ? guideLinesOf(value) : undefined;
39
+ if (lines?.["ja"] === undefined || lines["en"] === undefined) throw new Error(`${where}: guide needs a list of lines in ja and in en`);
40
+ return lines;
41
+ };
@@ -0,0 +1,26 @@
1
+ import { CONFIG_FILE, type Config } from "../config/load.ts";
2
+ import { loadGenres } from "../genre-load.ts";
3
+ import { knownGenres } from "../known-genres.ts";
4
+ import type { UiLanguage } from "../ui.ts";
5
+ import { guideLayerOf, type GuideLayer } from "./layer.ts";
6
+ import { guideProblemText } from "./problem-text.ts";
7
+ import { resolveGuide, type ResolvedGuide } from "./resolve.ts";
8
+
9
+ type GuideSettings = Pick<Config, "guide" | "applied" | "extensions">;
10
+
11
+ const configLayer = (config: Pick<Config, "guide">): ReturnType<typeof guideLayerOf> => guideLayerOf(config.guide, CONFIG_FILE, knownGenres());
12
+
13
+ /** Every place that changes the bundled guides, weakest first: the rule packs, then the chosen style, then chaff.yaml. */
14
+ export const guideLayersOf = (config: GuideSettings): GuideLayer[] => [
15
+ ...(config.extensions?.guides ?? []),
16
+ ...(config.applied?.guide === undefined ? [] : [config.applied.guide]),
17
+ configLayer(config).layer,
18
+ ];
19
+
20
+ /** The guide a document of this genre is checked against, in its language; undefined when it is turned off or the genre has none. */
21
+ export const genreGuideFor = (config: GuideSettings, genre: string, language: UiLanguage): ResolvedGuide | undefined =>
22
+ resolveGuide(loadGenres(), genre, language, guideLayersOf(config));
23
+
24
+ /** What chaff.yaml's guide: says that cannot be read. The rest of it still applies, so these are warnings, not a stop. */
25
+ export const configGuideProblems = (config: Pick<Config, "guide" | "path">, ui: UiLanguage): string[] =>
26
+ configLayer(config).problems.map((problem) => `${config.path ?? CONFIG_FILE}: ${guideProblemText(problem, ui)}`);
@@ -0,0 +1,34 @@
1
+ import type { Texts, UiLanguage } from "../ui.ts";
2
+ import type { GuideProblem } from "./layer.ts";
3
+
4
+ type ProblemText = Readonly<{ [K in GuideProblem["kind"]]: (problem: Extract<GuideProblem, { kind: K }>) => string }>;
5
+
6
+ const TEXT: Texts<ProblemText> = {
7
+ ja: {
8
+ "not-a-map": (problem) => `guide の ${problem.written} は読めません。ジャンルか群の名前の下に replace・add・off を書くか、guide: off と書いてください`,
9
+ "unknown-genre": (problem) => `guide の ${problem.genre} というジャンルや群はありません(npx chaffjs genres で一覧が出ます)`,
10
+ "bad-entry": (problem) => `guide の ${problem.genre}: ${problem.written} は読めません。replace か add に行を書くか、off と書いてください`,
11
+ "bad-lines": (problem) => `guide の ${problem.genre}: ${problem.field} は 1 行か行の並びで、言語ごとなら ja: と en: の下に書いてください`,
12
+ },
13
+ en: {
14
+ "not-a-map": (problem) => `guide ${problem.written} cannot be read; write replace, add or off under a genre or group, or guide: off`,
15
+ "unknown-genre": (problem) => `guide names ${problem.genre}, which is no genre or group (npx chaffjs genres lists them)`,
16
+ "bad-entry": (problem) => `guide ${problem.genre}: ${problem.written} cannot be read; write lines under replace or add, or off`,
17
+ "bad-lines": (problem) => `guide ${problem.genre}: write ${problem.field} as a line or a list of lines, under ja: and en: for each language`,
18
+ },
19
+ };
20
+
21
+ /** One guide problem as a sentence, without the place it was written (the caller names chaff.yaml, a style or a plugin). */
22
+ export const guideProblemText = (problem: GuideProblem, ui: UiLanguage): string => {
23
+ const text = TEXT[ui];
24
+ switch (problem.kind) {
25
+ case "not-a-map":
26
+ return text["not-a-map"](problem);
27
+ case "unknown-genre":
28
+ return text["unknown-genre"](problem);
29
+ case "bad-entry":
30
+ return text["bad-entry"](problem);
31
+ case "bad-lines":
32
+ return text["bad-lines"](problem);
33
+ }
34
+ };
@@ -0,0 +1,50 @@
1
+ import type { Texts, UiLanguage } from "../ui.ts";
2
+ import type { ReportedGuide } from "./report.ts";
3
+ import { BUNDLED_GUIDES } from "./resolve.ts";
4
+
5
+ const TEXT: Texts<{
6
+ readonly heading: (name: string, genre: string) => string;
7
+ readonly lead: string;
8
+ readonly rules: (ids: string) => string;
9
+ readonly from: (places: string) => string;
10
+ readonly change: string;
11
+ }> = {
12
+ ja: {
13
+ heading: (name, genre) => `指針: ${name}(${genre})`,
14
+ lead: "下の指摘より先に、原稿がこれを満たしているかを確かめてください。",
15
+ rules: (ids) => `このジャンルで特に効く rule: ${ids}`,
16
+ from: (places) => `書いたところ: ${places}`,
17
+ change: "chaff.yaml の guide: で書き換えられます。出さないときは --no-guide。",
18
+ },
19
+ en: {
20
+ heading: (name, genre) => `Guide: ${name} (${genre})`,
21
+ lead: "Before the findings below, check the draft against these.",
22
+ rules: (ids) => `Rules that matter most for this genre: ${ids}`,
23
+ from: (places) => `Written in: ${places}`,
24
+ change: "Change it with guide: in chaff.yaml; leave it out with --no-guide.",
25
+ },
26
+ };
27
+
28
+ const RULE = 60;
29
+
30
+ /** Who wrote the guide, said only when a team changed it: the bundled text alone needs no source line. */
31
+ const fromLines = (guide: ReportedGuide, ui: UiLanguage): string[] =>
32
+ guide.from.length === 1 && guide.from[0] === BUNDLED_GUIDES ? [] : [` ${TEXT[ui].from(guide.from.join(" → "))}`];
33
+
34
+ /** The guide as the text report opens with it, in the document's language: read first, as the instruction to write against. */
35
+ export const renderGuideBlock = (guide: ReportedGuide, ui: UiLanguage): string[] => {
36
+ const text = TEXT[ui];
37
+ return [
38
+ "",
39
+ text.heading(guide.name, guide.genre),
40
+ "",
41
+ ` ${text.lead}`,
42
+ "",
43
+ ...guide.lines.map((line) => ` - ${line}`),
44
+ ...(guide.rules.length === 0 ? [] : ["", ` ${text.rules(guide.rules.join(", "))}`]),
45
+ ...fromLines(guide, ui),
46
+ ` ${text.change}`,
47
+ "",
48
+ "═".repeat(RULE),
49
+ ];
50
+ };
@@ -0,0 +1,63 @@
1
+ import { showsGuide } from "../cli-args.ts";
2
+ import type { GenreSource } from "../cli-text.ts";
3
+ import type { Config } from "../config/load.ts";
4
+ import { loadGenres } from "../genre-load.ts";
5
+ import type { RuleDefinition } from "../plugin.ts";
6
+ import { localized } from "../render/text.ts";
7
+ import { uiLanguageOf, type UiLanguage } from "../ui.ts";
8
+ import { keyRulesOf } from "./key-rules.ts";
9
+ import { genreGuideFor } from "./of-config.ts";
10
+
11
+ /** A genre's guide as chaff reports it: the genre and its name, the lines, the rules that matter most, and who wrote it. */
12
+ export type ReportedGuide = {
13
+ readonly genre: string;
14
+ readonly name: string;
15
+ readonly lines: readonly string[];
16
+ readonly rules: readonly string[];
17
+ readonly from: readonly string[];
18
+ };
19
+
20
+ /**
21
+ * Where a genre counts as set, so its guide is shown. A defaulted genre is not one, nor one guessed from the path or the
22
+ * text: a guide for the wrong kind of document misleads more than none.
23
+ */
24
+ const SET_BY: ReadonlySet<GenreSource> = new Set<GenreSource>(["--genre", "by_path", "config", "front-matter"]);
25
+
26
+ export const genreWasSet = (from: GenreSource): boolean => SET_BY.has(from);
27
+
28
+ type GuideSettings = Pick<Config, "guide" | "applied" | "extensions" | "rules">;
29
+
30
+ /** The guide for a genre in a document's language, with the rules that matter most; undefined when it is off or there is none. */
31
+ export const reportedGuide = (config: GuideSettings, genre: string, language: string, rules: readonly RuleDefinition[]): ReportedGuide | undefined => {
32
+ const ui = uiLanguageOf(language);
33
+ const guide = genreGuideFor(config, genre, ui);
34
+ if (guide === undefined) return undefined;
35
+ const data = loadGenres();
36
+ const name = data.genres.find((entry) => entry.id === genre)?.name;
37
+ return {
38
+ genre,
39
+ name: name === undefined ? genre : localized(name, ui),
40
+ lines: guide.lines,
41
+ rules: keyRulesOf(rules, data, genre, language, config.rules),
42
+ from: guide.from,
43
+ };
44
+ };
45
+
46
+ /** One checked document, as far as its guide goes. */
47
+ type GuidedDocument = { readonly genre: string; readonly genreFrom: GenreSource; readonly language: string; readonly rules: readonly RuleDefinition[] };
48
+
49
+ /** A guide with the language it is written in, for output that holds several documents. */
50
+ export type RunGuide = { readonly language: UiLanguage; readonly guide: ReportedGuide };
51
+
52
+ /** A run's guides: one per genre and language among the documents whose genre was set, first seen first. None with --no-guide. */
53
+ export const guidesOfRun = (documents: readonly GuidedDocument[], config: GuideSettings, args: readonly string[]): RunGuide[] => {
54
+ if (!showsGuide(args)) return [];
55
+ const keyed = documents
56
+ .filter((document) => genreWasSet(document.genreFrom))
57
+ .map((document) => ({ key: `${document.genre}\n${uiLanguageOf(document.language)}`, document }));
58
+ const firsts = keyed.filter((entry, index) => keyed.findIndex((other) => other.key === entry.key) === index);
59
+ return firsts.flatMap(({ document }) => {
60
+ const guide = reportedGuide(config, document.genre, document.language, document.rules);
61
+ return guide === undefined ? [] : [{ language: uiLanguageOf(document.language), guide }];
62
+ });
63
+ };
@@ -0,0 +1,36 @@
1
+ import type { GenreData } from "../genre-parse.ts";
2
+ import type { GuideEdit, GuideLayer } from "./layer.ts";
3
+
4
+ /** Where the bundled guides come from, as a resolved guide names it. */
5
+ export const BUNDLED_GUIDES = "genres.yaml";
6
+
7
+ /** A genre's guide in one language, and every place that wrote it, weakest first. */
8
+ export type ResolvedGuide = { readonly genre: string; readonly lines: readonly string[]; readonly from: readonly string[] };
9
+
10
+ type Current = { readonly lines: readonly string[]; readonly from: readonly string[] };
11
+
12
+ const applied = (current: Current, edit: GuideEdit | undefined, language: string, from: string): Current => {
13
+ if (edit === undefined) return current;
14
+ if (edit.kind === "off") return { lines: [], from: [...current.from, from] };
15
+ const replaced = edit.replace?.[language];
16
+ const added = edit.add?.[language] ?? [];
17
+ if (replaced === undefined && added.length === 0) return current;
18
+ return { lines: [...(replaced ?? current.lines), ...added], from: [...current.from, from] };
19
+ };
20
+
21
+ /**
22
+ * A genre's guide in a language: genres.yaml's for the genre, else its group's, then each layer in order (weakest first):
23
+ * its guide: off, then its group's entry, then its genre's. undefined for a genre chaff does not know, or when no lines are left.
24
+ */
25
+ export const resolveGuide = (data: GenreData, genre: string, language: string, layers: readonly GuideLayer[]): ResolvedGuide | undefined => {
26
+ const own = data.genres.find((entry) => entry.id === genre);
27
+ if (own === undefined) return undefined;
28
+ const group = data.groups.find((entry) => genre.startsWith(`${entry.id}/`));
29
+ const bundled = own.guide ?? group?.guide;
30
+ const start: Current = { lines: bundled?.[language] ?? [], from: bundled === undefined ? [] : [BUNDLED_GUIDES] };
31
+ const end = layers.reduce((current, layer) => {
32
+ const cleared = applied(current, layer.allOff ? { kind: "off" } : undefined, language, layer.from);
33
+ return applied(applied(cleared, group === undefined ? undefined : layer.edits[group.id], language, layer.from), layer.edits[genre], language, layer.from);
34
+ }, start);
35
+ return end.lines.length === 0 ? undefined : { genre, lines: end.lines, from: end.from };
36
+ };
@@ -1,12 +1,13 @@
1
1
  import { isLevel } from "./levels.ts";
2
2
  import type { Level, Localized } from "./plugin.ts";
3
3
  import type { RuleOff } from "./rule-offs.ts";
4
+ import { bundledGuideOf, type GuideLines } from "./genre-guide/lines.ts";
4
5
 
5
6
  /** A preset's rules: the level each named rule runs at, under whatever chaff.yaml's rules say. */
6
7
  export type PresetLevels = Readonly<Record<string, Level>>;
7
8
 
8
- /** A group of genres (legal, literature): its name, and the levels every genre in it shares. */
9
- export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels };
9
+ /** A group of genres (legal, literature): its name, the levels every genre in it shares, and the guide a genre without its own reads. */
10
+ export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels; readonly guide?: GuideLines | undefined };
10
11
 
11
12
  /** One genre of genres.yaml: what it is for, the levels it sets on top of its group's, and the profile it reads with. */
12
13
  export type GenreDefinition = {
@@ -16,6 +17,8 @@ export type GenreDefinition = {
16
17
  readonly rules: PresetLevels;
17
18
  readonly profile: string | undefined;
18
19
  readonly suggest: GenreSuggest;
20
+ /** What a good document of this genre does, as lines to check a draft against (genre-guide/). */
21
+ readonly guide?: GuideLines | undefined;
19
22
  };
20
23
 
21
24
  /** A line shape that marks the genre, and how many lines must have it. */
@@ -59,10 +62,16 @@ const idOf = (value: unknown, index: number): string => {
59
62
  return id;
60
63
  };
61
64
 
65
+ /** guide: as a field only when written, so an entry without one reads as it did before guides. */
66
+ const guideField = (raw: Record<string, unknown>, id: string): { guide?: GuideLines } => {
67
+ const guide = bundledGuideOf(raw["guide"], id);
68
+ return guide === undefined ? {} : { guide };
69
+ };
70
+
62
71
  const groupOf = (value: unknown, index: number): GenreGroup => {
63
72
  const id = idOf(value, index);
64
73
  const raw = isRecord(value) ? value : {};
65
- return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id) };
74
+ return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id), ...guideField(raw, id) };
66
75
  };
67
76
 
68
77
  const patternOf = (value: unknown, flags: string, where: string): RegExp => {
@@ -121,6 +130,7 @@ const genreOf = (value: unknown, index: number): GenreDefinition => {
121
130
  rules: levelsOf(raw["rules"], id),
122
131
  profile,
123
132
  suggest: suggestOf(raw["suggest"], id),
133
+ ...guideField(raw, id),
124
134
  };
125
135
  };
126
136
 
package/src/init.ts CHANGED
@@ -41,6 +41,13 @@ genre: ${genre}
41
41
  # names:
42
42
  # - 個人情報保護委員会
43
43
 
44
+ # このジャンルの指針(よい文書が満たすこと)。検査の結果より先に出て、AI が書き直すときの指示になる。
45
+ # 既定は chaff に入っている文。replace で置き換え、add で足し、off で出さない。
46
+ # guide:
47
+ # ${genre}:
48
+ # add:
49
+ # - 金額は税込みか税抜きかを書いているか
50
+
44
51
  # 既定から変えたものだけを書く。
45
52
  rules:
46
53
  `,
@@ -80,6 +87,13 @@ genre: ${genre}
80
87
  # names:
81
88
  # - Bank of England
82
89
 
90
+ # This genre's guide: what a good document of this kind does. It comes before the findings, and an AI
91
+ # rewriting the text works to it. chaff ships one; replace it, add lines, or turn it off.
92
+ # guide:
93
+ # ${genre}:
94
+ # add:
95
+ # - Does each amount say whether tax is included?
96
+
83
97
  # Only what differs from the defaults.
84
98
  rules:
85
99
  `,
@@ -41,5 +41,30 @@ export const quotedSpans = (text: string, marks: Marks = KAGI_MARKS): Span[] =>
41
41
  export const quotedIn = (sentence: Sentence, marks: Marks = KAGI_MARKS): Span[] =>
42
42
  quotedSpans(sentence.text, marks).map((span) => ({ start: sentence.span.start + span.start - 1, end: sentence.span.start + span.end + 1 }));
43
43
 
44
+ const MARKS = /[\s\p{P}]+/u;
45
+ const LETTERS = /^\p{L}+$/u;
46
+
47
+ /** 記号と空白のあいだに残る語が、字だけの語で、多くとも allowed 個か。 */
48
+ const holdsAtMost = (text: string, allowed: number): boolean => {
49
+ const words = text.split(MARKS).filter((word) => word !== "");
50
+ return words.length <= allowed && words.every((word) => LETTERS.test(word));
51
+ };
52
+
53
+ /**
54
+ * span だけを引いた引用符("delves"、"?utm_source=chatgpt.com"、"October 26, 2026 – October 19, 2026")の中か。
55
+ * 引用符が語だけを囲めば、その語は使ったのではなく例として挙げたもの。周りの記号と空白は語の一部に数えない。
56
+ * wordsBesides は、span のほかに引用符の中にあってよい語の数(日付なら曜日の一語)。
57
+ * 話した言葉の引用("We ship on Monday, October 26," she said.)は語のほかの字を持つので当たらない。引用は一行の中だけを見る。
58
+ */
59
+ export const isQuotedAlone = (text: string, span: Span, wordsBesides = 0): boolean => {
60
+ const lineStart = text.lastIndexOf("\n", span.start - 1) + 1;
61
+ const newline = text.indexOf("\n", span.end);
62
+ const line = text.slice(lineStart, newline === -1 ? text.length : newline);
63
+ const [start, end] = [span.start - lineStart, span.end - lineStart];
64
+ return quotedSpans(line, QUOTATION_MARKS).some(
65
+ (quoted) => quoted.start <= start && end <= quoted.end && holdsAtMost(`${line.slice(quoted.start, start)} ${line.slice(end, quoted.end)}`, wordsBesides),
66
+ );
67
+ };
68
+
44
69
  /** inner が、spans のどれかにまるごと入っているか。 */
45
70
  export const isWithinAny = (spans: readonly Span[], inner: Span): boolean => spans.some((span) => inner.start >= span.start && inner.end <= span.end);