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.
- package/README.md +7 -1
- package/dist/api.d.ts +14 -0
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js.map +1 -1
- package/dist/cli-args.d.ts +2 -0
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -0
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +2 -0
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +13 -19
- package/dist/cli.js.map +1 -1
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.d.ts.map +1 -1
- package/dist/commands/feedback.js.map +1 -1
- package/dist/commands/fix-plan.d.ts.map +1 -1
- package/dist/commands/fix-plan.js +5 -0
- package/dist/commands/fix-plan.js.map +1 -1
- package/dist/commands/rules.d.ts +9 -0
- package/dist/commands/rules.d.ts.map +1 -0
- package/dist/commands/rules.js +26 -0
- package/dist/commands/rules.js.map +1 -0
- package/dist/config/load.d.ts +6 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +1 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/style.js +1 -1
- package/dist/config/style.js.map +1 -1
- package/dist/config/warnings.d.ts +1 -1
- package/dist/config/warnings.d.ts.map +1 -1
- package/dist/config/warnings.js +3 -1
- package/dist/config/warnings.js.map +1 -1
- package/dist/detectors/ellipsis.d.ts +10 -0
- package/dist/detectors/ellipsis.d.ts.map +1 -0
- package/dist/detectors/ellipsis.js +56 -0
- package/dist/detectors/ellipsis.js.map +1 -0
- package/dist/detectors/registry/ellipsis-consistency.d.ts +3 -0
- package/dist/detectors/registry/ellipsis-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/ellipsis-consistency.js +3 -0
- package/dist/detectors/registry/ellipsis-consistency.js.map +1 -0
- package/dist/extension/load.d.ts +3 -0
- package/dist/extension/load.d.ts.map +1 -1
- package/dist/extension/load.js +1 -0
- package/dist/extension/load.js.map +1 -1
- package/dist/extension/pack-read.d.ts.map +1 -1
- package/dist/extension/pack-read.js +1 -0
- package/dist/extension/pack-read.js.map +1 -1
- package/dist/extension/plugin-load.js.map +1 -1
- package/dist/extension/plugin-parse.d.ts +7 -0
- package/dist/extension/plugin-parse.d.ts.map +1 -1
- package/dist/extension/plugin-parse.js +18 -2
- package/dist/extension/plugin-parse.js.map +1 -1
- package/dist/extension/problem-text.d.ts.map +1 -1
- package/dist/extension/problem-text.js +5 -0
- package/dist/extension/problem-text.js.map +1 -1
- package/dist/fix-plan/plan.d.ts +4 -0
- package/dist/fix-plan/plan.d.ts.map +1 -1
- package/dist/fix-plan/plan.js +1 -0
- package/dist/fix-plan/plan.js.map +1 -1
- package/dist/fix-plan/render.d.ts.map +1 -1
- package/dist/fix-plan/render.js +9 -0
- package/dist/fix-plan/render.js.map +1 -1
- package/dist/fix-plan/text.d.ts +4 -0
- package/dist/fix-plan/text.d.ts.map +1 -1
- package/dist/fix-plan/text.js +8 -2
- package/dist/fix-plan/text.js.map +1 -1
- package/dist/genre-guide/key-rules.d.ts +10 -0
- package/dist/genre-guide/key-rules.d.ts.map +1 -0
- package/dist/genre-guide/key-rules.js +23 -0
- package/dist/genre-guide/key-rules.js.map +1 -0
- package/dist/genre-guide/layer.d.ts +44 -0
- package/dist/genre-guide/layer.d.ts.map +1 -0
- package/dist/genre-guide/layer.js +43 -0
- package/dist/genre-guide/layer.js.map +1 -0
- package/dist/genre-guide/lines.d.ts +15 -0
- package/dist/genre-guide/lines.d.ts.map +1 -0
- package/dist/genre-guide/lines.js +39 -0
- package/dist/genre-guide/lines.js.map +1 -0
- package/dist/genre-guide/of-config.d.ts +13 -0
- package/dist/genre-guide/of-config.d.ts.map +1 -0
- package/dist/genre-guide/of-config.js +18 -0
- package/dist/genre-guide/of-config.js.map +1 -0
- package/dist/genre-guide/problem-text.d.ts +5 -0
- package/dist/genre-guide/problem-text.d.ts.map +1 -0
- package/dist/genre-guide/problem-text.js +29 -0
- package/dist/genre-guide/problem-text.js.map +1 -0
- package/dist/genre-guide/render.d.ts +5 -0
- package/dist/genre-guide/render.d.ts.map +1 -0
- package/dist/genre-guide/render.js +38 -0
- package/dist/genre-guide/render.js.map +1 -0
- package/dist/genre-guide/report.d.ts +32 -0
- package/dist/genre-guide/report.d.ts.map +1 -0
- package/dist/genre-guide/report.js +42 -0
- package/dist/genre-guide/report.js.map +1 -0
- package/dist/genre-guide/resolve.d.ts +16 -0
- package/dist/genre-guide/resolve.d.ts.map +1 -0
- package/dist/genre-guide/resolve.js +31 -0
- package/dist/genre-guide/resolve.js.map +1 -0
- package/dist/genre-parse.d.ts +5 -1
- package/dist/genre-parse.d.ts.map +1 -1
- package/dist/genre-parse.js +8 -1
- package/dist/genre-parse.js.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +14 -0
- package/dist/init.js.map +1 -1
- package/dist/render/rules-json.d.ts +2 -1
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +3 -1
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/sarif.d.ts +3 -1
- package/dist/render/sarif.d.ts.map +1 -1
- package/dist/render/sarif.js +3 -1
- package/dist/render/sarif.js.map +1 -1
- package/dist/style-parse.d.ts +3 -0
- package/dist/style-parse.d.ts.map +1 -1
- package/dist/style-parse.js +18 -2
- package/dist/style-parse.js.map +1 -1
- package/genres.yaml +329 -0
- package/package.json +3 -3
- package/rules/ellipsis-consistency.yaml +73 -0
- package/rules/figurative-density.yaml +84 -0
- package/skills/chaff/SKILL.md +6 -0
- package/src/api.ts +13 -0
- package/src/cli-args.ts +3 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +16 -20
- package/src/commands/feedback.ts +3 -0
- package/src/commands/fix-plan.ts +8 -0
- package/src/commands/rules.ts +34 -0
- package/src/config/load.ts +7 -1
- package/src/config/style.ts +1 -1
- package/src/config/warnings.ts +3 -1
- package/src/detectors/ellipsis.ts +74 -0
- package/src/detectors/registry/ellipsis-consistency.ts +4 -0
- package/src/extension/load.ts +4 -0
- package/src/extension/pack-read.ts +1 -0
- package/src/extension/plugin-load.ts +1 -1
- package/src/extension/plugin-parse.ts +27 -4
- package/src/extension/problem-text.ts +7 -1
- package/src/fix-plan/plan.ts +5 -0
- package/src/fix-plan/render.ts +9 -0
- package/src/fix-plan/text.ts +12 -2
- package/src/genre-guide/key-rules.ts +37 -0
- package/src/genre-guide/layer.ts +77 -0
- package/src/genre-guide/lines.ts +41 -0
- package/src/genre-guide/of-config.ts +26 -0
- package/src/genre-guide/problem-text.ts +34 -0
- package/src/genre-guide/render.ts +50 -0
- package/src/genre-guide/report.ts +63 -0
- package/src/genre-guide/resolve.ts +36 -0
- package/src/genre-parse.ts +13 -3
- package/src/init.ts +14 -0
- package/src/render/rules-json.ts +4 -0
- package/src/render/sarif.ts +4 -1
- 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
|
package/skills/chaff/SKILL.md
CHANGED
|
@@ -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,
|
|
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 {
|
|
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
|
-
|
|
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))])),
|
package/src/commands/feedback.ts
CHANGED
|
@@ -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). */
|
package/src/commands/fix-plan.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/config/load.ts
CHANGED
|
@@ -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
|
|
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
|
};
|
package/src/config/style.ts
CHANGED
|
@@ -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
|
|
package/src/config/warnings.ts
CHANGED
|
@@ -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
|
+
};
|
package/src/extension/load.ts
CHANGED
|
@@ -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
|
|
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 = {
|
|
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
|
};
|