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.
- 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/bare-comparative.d.ts +26 -0
- package/dist/detectors/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/bare-comparative.js +117 -0
- package/dist/detectors/bare-comparative.js.map +1 -0
- package/dist/detectors/chat-citation.d.ts +6 -0
- package/dist/detectors/chat-citation.d.ts.map +1 -1
- package/dist/detectors/chat-citation.js +26 -1
- package/dist/detectors/chat-citation.js.map +1 -1
- package/dist/detectors/clause-shape.d.ts.map +1 -1
- package/dist/detectors/clause-shape.js +19 -5
- package/dist/detectors/clause-shape.js.map +1 -1
- package/dist/detectors/definition-use.d.ts.map +1 -1
- package/dist/detectors/definition-use.js +4 -2
- package/dist/detectors/definition-use.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/generic-heading.d.ts +8 -0
- package/dist/detectors/generic-heading.d.ts.map +1 -0
- package/dist/detectors/generic-heading.js +26 -0
- package/dist/detectors/generic-heading.js.map +1 -0
- package/dist/detectors/ime-residue.d.ts +13 -0
- package/dist/detectors/ime-residue.d.ts.map +1 -0
- package/dist/detectors/ime-residue.js +27 -0
- package/dist/detectors/ime-residue.js.map +1 -0
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +9 -2
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/lookalike-character.d.ts +18 -0
- package/dist/detectors/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/lookalike-character.js +50 -0
- package/dist/detectors/lookalike-character.js.map +1 -0
- package/dist/detectors/lowercase-name.d.ts +9 -0
- package/dist/detectors/lowercase-name.d.ts.map +1 -0
- package/dist/detectors/lowercase-name.js +40 -0
- package/dist/detectors/lowercase-name.js.map +1 -0
- package/dist/detectors/registry/bare-comparative.d.ts +3 -0
- package/dist/detectors/registry/bare-comparative.d.ts.map +1 -0
- package/dist/detectors/registry/bare-comparative.js +3 -0
- package/dist/detectors/registry/bare-comparative.js.map +1 -0
- package/dist/detectors/registry/calendar-name-case.d.ts +3 -0
- package/dist/detectors/registry/calendar-name-case.d.ts.map +1 -0
- package/dist/detectors/registry/calendar-name-case.js +3 -0
- package/dist/detectors/registry/calendar-name-case.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/detectors/registry/generic-heading.d.ts +3 -0
- package/dist/detectors/registry/generic-heading.d.ts.map +1 -0
- package/dist/detectors/registry/generic-heading.js +3 -0
- package/dist/detectors/registry/generic-heading.js.map +1 -0
- package/dist/detectors/registry/ime-residue.d.ts +3 -0
- package/dist/detectors/registry/ime-residue.d.ts.map +1 -0
- package/dist/detectors/registry/ime-residue.js +3 -0
- package/dist/detectors/registry/ime-residue.js.map +1 -0
- package/dist/detectors/registry/lookalike-character.d.ts +3 -0
- package/dist/detectors/registry/lookalike-character.d.ts.map +1 -0
- package/dist/detectors/registry/lookalike-character.js +3 -0
- package/dist/detectors/registry/lookalike-character.js.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.d.ts.map +1 -0
- package/dist/detectors/registry/unit-spacing-consistency.js +3 -0
- package/dist/detectors/registry/unit-spacing-consistency.js.map +1 -0
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +8 -4
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +32 -7
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/technical-docs.d.ts.map +1 -1
- package/dist/detectors/technical-docs.js +14 -0
- package/dist/detectors/technical-docs.js.map +1 -1
- package/dist/detectors/unit-spacing.d.ts +25 -0
- package/dist/detectors/unit-spacing.d.ts.map +1 -0
- package/dist/detectors/unit-spacing.js +50 -0
- package/dist/detectors/unit-spacing.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/quoted-span.d.ts +7 -0
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +20 -0
- package/dist/quoted-span.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/structure/definition-use.d.ts +14 -4
- package/dist/structure/definition-use.d.ts.map +1 -1
- package/dist/structure/definition-use.js +29 -9
- package/dist/structure/definition-use.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/calendar-name-case.yaml +55 -0
- package/rules/comparative-without-baseline.yaml +71 -0
- package/rules/cross-doc-broken-link.yaml +1 -1
- package/rules/cross-doc-duplicate-definition.yaml +1 -1
- package/rules/cross-doc-fact-conflict.yaml +1 -1
- package/rules/cross-doc-term-variant.yaml +5 -3
- package/rules/ellipsis-consistency.yaml +73 -0
- package/rules/figurative-density.yaml +84 -0
- package/rules/ime-residue.yaml +55 -0
- package/rules/lookalike-character.yaml +82 -0
- package/rules/numbered-generic-heading.yaml +111 -0
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/unit-spacing-consistency.yaml +74 -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/bare-comparative.ts +152 -0
- package/src/detectors/chat-citation.ts +28 -1
- package/src/detectors/clause-shape.ts +20 -6
- package/src/detectors/definition-use.ts +13 -3
- package/src/detectors/ellipsis.ts +74 -0
- package/src/detectors/generic-heading.ts +30 -0
- package/src/detectors/ime-residue.ts +37 -0
- package/src/detectors/lexicon.ts +11 -2
- package/src/detectors/lookalike-character.ts +75 -0
- package/src/detectors/lowercase-name.ts +58 -0
- package/src/detectors/registry/bare-comparative.ts +4 -0
- package/src/detectors/registry/calendar-name-case.ts +4 -0
- package/src/detectors/registry/ellipsis-consistency.ts +4 -0
- package/src/detectors/registry/generic-heading.ts +4 -0
- package/src/detectors/registry/ime-residue.ts +4 -0
- package/src/detectors/registry/lookalike-character.ts +4 -0
- package/src/detectors/registry/unit-spacing-consistency.ts +4 -0
- package/src/detectors/signals.ts +9 -5
- package/src/detectors/structure-tree.ts +55 -30
- package/src/detectors/technical-docs.ts +13 -0
- package/src/detectors/unit-spacing.ts +71 -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/quoted-span.ts +25 -0
- package/src/render/rules-json.ts +4 -0
- package/src/render/sarif.ts +4 -1
- package/src/structure/definition-use.ts +42 -10
- package/src/style-parse.ts +19 -1
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,152 @@
|
|
|
1
|
+
import type { Detector, Finding, Lexicon, Sentence, Span, Token } from "../plugin.ts";
|
|
2
|
+
import { quotedIn } from "../quoted-span.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 比べる相手の無い比較(より良い結果、さらに高速に、The new engine is faster.)。何より良いのか、何より速いのかが書かれていない。
|
|
6
|
+
* 語は語彙表 comparative-baseline が言う。group marker は日本語の比べる副詞(より、さらに)、more は英語の比べる語、comparative は比べた形の語
|
|
7
|
+
* (faster、better)、baseline は相手を言う語(従来、以前、than、previous)、exempt は比較でない決まった言い方(より多くの、より詳しく)、
|
|
8
|
+
* condition は比較を条件や仮定の下に置く語(使うと、if、would)。条件を書いた比較は、その条件の無い場合と比べている。
|
|
9
|
+
* 同じ文か前の二文に相手を言う語があれば、相手は書いてある。
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type ComparativeLists = {
|
|
13
|
+
readonly markers: ReadonlySet<string>;
|
|
14
|
+
readonly more: ReadonlySet<string>;
|
|
15
|
+
readonly comparatives: ReadonlySet<string>;
|
|
16
|
+
readonly baselines: readonly string[];
|
|
17
|
+
readonly exempt: ReadonlySet<string>;
|
|
18
|
+
readonly conditions: ReadonlySet<string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type BareComparative = { readonly first: Token; readonly last: Token };
|
|
22
|
+
|
|
23
|
+
/** 前に立てば、比べた形が述語として読める語の品詞(is faster、runs faster)。 */
|
|
24
|
+
const PREDICATE_HEAD = new Set(["VERB", "AUX"]);
|
|
25
|
+
/** 比べた形の後ろで節が終わる印。 */
|
|
26
|
+
const CLAUSE_END = new Set([".", "!", "?", ",", ";", ":"]);
|
|
27
|
+
/** 述語と比べた形のあいだに挟まってよい副詞の数(is now much faster)。 */
|
|
28
|
+
const MAX_ADVERBS = 2;
|
|
29
|
+
/** 日本語の形容動詞の語幹の後ろに来る語(高速な、高速に、高速です)。 */
|
|
30
|
+
const ADJECTIVAL_TAIL = new Set(["な", "に", "だ", "です", "で"]);
|
|
31
|
+
|
|
32
|
+
const lower = (token: Token | undefined): string => token?.surface.toLowerCase() ?? "";
|
|
33
|
+
|
|
34
|
+
const isThirdPersonVerb = (token: Token): boolean => token.pos === "NOUN" && token.features?.["AlsoVerb"] === "Yes" && token.features["Number"] === "Plur";
|
|
35
|
+
|
|
36
|
+
/** 比べる語の後ろの、程度を言える語(良い、高速な)。形容詞か、形容動詞の語幹。 */
|
|
37
|
+
const isGradable = (tokens: readonly Token[], at: number): boolean => {
|
|
38
|
+
const word = tokens[at];
|
|
39
|
+
if (word?.pos === "ADJ") return true;
|
|
40
|
+
return word?.pos === "NOUN" && word.features?.["Bound"] !== "Yes" && ADJECTIVAL_TAIL.has(tokens[at + 1]?.surface ?? "");
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** 比べる副詞が語の頭に立つか。前が助詞・記号・接続の語か文の頭のときだけ(「学年だより等」の「より」は語の一部)。 */
|
|
44
|
+
const OPENS_WORD = new Set(["ADP", "PUNCT", "SCONJ", "CCONJ", "ADV"]);
|
|
45
|
+
|
|
46
|
+
/** 日本語: 副詞の「より」「さらに」の直後の、程度を言える語。比較の決まった言い方(より多くの)は除く。 */
|
|
47
|
+
const markedAt = (tokens: readonly Token[], at: number, lists: ComparativeLists): BareComparative | undefined => {
|
|
48
|
+
const [marker, word] = [tokens[at], tokens[at + 1]];
|
|
49
|
+
if (marker?.pos !== "ADV" || !lists.markers.has(marker.surface) || word === undefined || lists.exempt.has(word.surface)) return undefined;
|
|
50
|
+
const previous = tokens[at - 1];
|
|
51
|
+
if (previous !== undefined && !OPENS_WORD.has(previous.pos)) return undefined;
|
|
52
|
+
return isGradable(tokens, at + 1) ? { first: marker, last: word } : undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** 比べた形の後ろで節が終わるか。読点の後ろに形容詞が続けば、名詞の前に並べた修飾語(better, more predictable results)。 */
|
|
56
|
+
const closesClause = (tokens: readonly Token[], at: number): boolean => {
|
|
57
|
+
const next = tokens[at + 1];
|
|
58
|
+
if (next === undefined) return true;
|
|
59
|
+
if (!CLAUSE_END.has(next.surface)) return false;
|
|
60
|
+
const after = tokens[at + 2];
|
|
61
|
+
return next.surface !== "," || (after?.pos !== "ADJ" && after?.pos !== "ADV");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** more と形容詞のあいだの副詞(more computationally efficient)を飛ばした、more の位置。無ければ -1。 */
|
|
65
|
+
const moreBefore = (tokens: readonly Token[], at: number, more: ReadonlySet<string>): number => {
|
|
66
|
+
const before = tokens.slice(Math.max(0, at - 1 - MAX_ADVERBS), at).reverse();
|
|
67
|
+
const found = before.findIndex((token) => token.pos !== "ADV" || more.has(lower(token)));
|
|
68
|
+
return found !== -1 && more.has(lower(before[found])) ? at - 1 - found : -1;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** 英語: 述語の後ろで節を閉じる比べた形(is faster.、runs faster,)か、more と形容詞。 */
|
|
72
|
+
const predicateAt = (tokens: readonly Token[], at: number, lists: ComparativeLists): BareComparative | undefined => {
|
|
73
|
+
const word = tokens[at];
|
|
74
|
+
if (word === undefined || !closesClause(tokens, at)) return undefined;
|
|
75
|
+
const moreAt = word.pos === "ADJ" ? moreBefore(tokens, at, lists.more) : -1;
|
|
76
|
+
if (moreAt === -1 && !lists.comparatives.has(lower(word))) return undefined;
|
|
77
|
+
const start = moreAt === -1 ? at : moreAt;
|
|
78
|
+
const first = tokens[start] ?? word;
|
|
79
|
+
const adverbs = tokens.slice(Math.max(0, start - MAX_ADVERBS), start).reverse();
|
|
80
|
+
const skipped = adverbs.findIndex((token) => token.pos !== "ADV");
|
|
81
|
+
const head = tokens[start - 1 - (skipped === -1 ? adverbs.length : skipped)];
|
|
82
|
+
return head !== undefined && (PREDICATE_HEAD.has(head.pos) || isThirdPersonVerb(head)) ? { first, last: word } : undefined;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** 比べた形より前に、条件か仮定を言う語があるか。並べる「と」(A と B)は条件でない。 */
|
|
86
|
+
const isConditional = (tokens: readonly Token[], found: BareComparative, conditions: ReadonlySet<string>): boolean =>
|
|
87
|
+
tokens.some((token) => token.span.start < found.first.span.start && token.pos !== "CCONJ" && conditions.has(lower(token)));
|
|
88
|
+
|
|
89
|
+
export const bareComparativesIn = (tokens: readonly Token[], lists: ComparativeLists): BareComparative[] =>
|
|
90
|
+
tokens
|
|
91
|
+
.flatMap((_, at) => markedAt(tokens, at, lists) ?? predicateAt(tokens, at, lists) ?? [])
|
|
92
|
+
.filter((found) => !isConditional(tokens, found, lists.conditions));
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 語が比べる相手を言う語か。語の形か原形で照らし、一字の語(旧)は語の頭だけ(旧版は相手、復旧は相手でない)。
|
|
96
|
+
*/
|
|
97
|
+
const isBaselineWord = (token: Token, baselines: readonly string[]): boolean => {
|
|
98
|
+
const [surface, lemma] = [token.surface.toLowerCase(), token.lemma?.toLowerCase()];
|
|
99
|
+
return baselines.some((baseline) => surface === baseline || lemma === baseline || ([...baseline].length === 1 && surface.startsWith(baseline)));
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** 相手を言う語が、続く語に分かれて書かれているか(これ・まで、で・は・なく)。 */
|
|
103
|
+
const MAX_BASELINE_TOKENS = 3;
|
|
104
|
+
const isSplitBaseline = (tokens: readonly Token[], at: number, baselines: readonly string[]): boolean => {
|
|
105
|
+
const joined = tokens
|
|
106
|
+
.slice(at, at + MAX_BASELINE_TOKENS)
|
|
107
|
+
.map((token) => token.surface.toLowerCase())
|
|
108
|
+
.reduce<string[]>((runs, surface) => [...runs, `${runs.at(-1) ?? ""}${surface}`], []);
|
|
109
|
+
return joined.slice(1).some((run) => baselines.includes(run));
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** 文の中に、比べる相手を言う語か、相手を受ける助詞の「より」(従来より)があるか。 */
|
|
113
|
+
export const namesBaseline = (sentence: Sentence | undefined, baselines: readonly string[]): boolean =>
|
|
114
|
+
(sentence?.tokens ?? []).some(
|
|
115
|
+
(token, at, tokens) => (token.surface === "より" && token.pos === "ADP") || isBaselineWord(token, baselines) || isSplitBaseline(tokens, at, baselines),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/** 比べる相手を探す、前の文の数。段落の中で相手を言ってから、一つ文を挟んで比べることがある。 */
|
|
119
|
+
const BASELINE_REACH = 2;
|
|
120
|
+
|
|
121
|
+
const groupOf = (lexicon: Lexicon, group: string): string[] => lexicon.filter((entry) => entry.group === group).map((entry) => entry.pattern.toLowerCase());
|
|
122
|
+
|
|
123
|
+
export const listsOf = (lexicon: Lexicon): ComparativeLists => ({
|
|
124
|
+
markers: new Set(groupOf(lexicon, "marker")),
|
|
125
|
+
more: new Set(groupOf(lexicon, "more")),
|
|
126
|
+
comparatives: new Set(groupOf(lexicon, "comparative")),
|
|
127
|
+
baselines: groupOf(lexicon, "baseline"),
|
|
128
|
+
exempt: new Set(groupOf(lexicon, "exempt")),
|
|
129
|
+
conditions: new Set(groupOf(lexicon, "condition")),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
/** 鉤括弧の中の比較(「給与所得の確定申告がさらに簡単に!」ページ)は題や語を挙げたもので、書き手の比較ではない。 */
|
|
133
|
+
const insideSpan = (found: BareComparative, span: Span): boolean => found.first.span.start >= span.start && found.last.span.end <= span.end;
|
|
134
|
+
|
|
135
|
+
export const bareComparative: Detector = (doc): Finding[] => {
|
|
136
|
+
const lists = listsOf(doc.lexicons["comparative-baseline"] ?? []);
|
|
137
|
+
return doc.sentences.flatMap((sentence, at) => {
|
|
138
|
+
const nearby = doc.sentences.slice(Math.max(0, at - BASELINE_REACH), at + 1);
|
|
139
|
+
if (sentence.embeddedLanguage !== undefined || nearby.some((near) => namesBaseline(near, lists.baselines))) return [];
|
|
140
|
+
const quoted = quotedIn(sentence);
|
|
141
|
+
return bareComparativesIn(sentence.tokens ?? [], lists)
|
|
142
|
+
.filter((found) => !quoted.some((span) => insideSpan(found, span)))
|
|
143
|
+
.map((found) => ({
|
|
144
|
+
rule: "",
|
|
145
|
+
severity: "info",
|
|
146
|
+
line: 0,
|
|
147
|
+
column: 0,
|
|
148
|
+
quote: sentence.text.trim(),
|
|
149
|
+
values: { matched: doc.source.slice(found.first.span.start, found.last.span.end), offset: found.first.span.start },
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Detector, Finding, Lexicon, ProseDocument, Span } from "../plugin.ts";
|
|
2
2
|
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
3
4
|
import { findingAt, markupOf } from "./markup-finding.ts";
|
|
4
5
|
|
|
5
6
|
/** One marker a chat interface leaves when its answer is pasted: where it is and what was written. */
|
|
@@ -42,9 +43,35 @@ const reachesReader = (doc: ProseDocument, span: Span): boolean => {
|
|
|
42
43
|
return showsInProse(doc.prose, span) || within([...markup.texts, ...destinations], span);
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
/** Quotation marks, parentheses and sentence punctuation at the ends of a word, which are not part of it. */
|
|
47
|
+
const SURROUNDING = new Set(['"', "'", "“", "”", "‘", "’", "(", ")", ".", ",", ";", ":", "!", "?"]);
|
|
48
|
+
const SPACE = /\s/u;
|
|
49
|
+
|
|
50
|
+
/** What is left of a string after the surrounding marks at both of its ends. */
|
|
51
|
+
const withoutSurrounding = (text: string): string => {
|
|
52
|
+
const chars = [...text];
|
|
53
|
+
const first = chars.findIndex((char) => !SURROUNDING.has(char));
|
|
54
|
+
return first === -1 ? "" : chars.slice(first, chars.findLastIndex((char) => !SURROUNDING.has(char)) + 1).join("");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Whether the marker stands as a word of its own (links ending in "?utm_source=chatgpt.com", marks such as oaicite.), or
|
|
59
|
+
* alone in quotation marks, as Japanese writes it with no spaces (「?utm_source=chatgpt.com」の付いたリンク): prose
|
|
60
|
+
* about the marks names them. What a chat answer leaves is always joined to a URL or a citation (oaicite:0).
|
|
61
|
+
*/
|
|
62
|
+
export const standsAlone = (source: string, span: Span): boolean => {
|
|
63
|
+
if (isQuotedAlone(source, span)) return true;
|
|
64
|
+
const lineStart = source.lastIndexOf("\n", span.start - 1) + 1;
|
|
65
|
+
const lineEnd = source.indexOf("\n", span.end);
|
|
66
|
+
const before = source.slice(lineStart, span.start).split(SPACE).at(-1) ?? "";
|
|
67
|
+
const after = source.slice(span.end, lineEnd === -1 ? source.length : lineEnd).split(SPACE)[0] ?? "";
|
|
68
|
+
const marker = source.slice(span.start, span.end);
|
|
69
|
+
return withoutSurrounding(`${before}${marker}${after}`) === marker;
|
|
70
|
+
};
|
|
71
|
+
|
|
45
72
|
/** Each marker is a leftover in its own right, so each is reported, from the first. */
|
|
46
73
|
export const chatCitationResidue: Detector = (doc, options): Finding[] => {
|
|
47
|
-
const markers = markersIn(doc.source, options.lexicon ?? []).filter((marker) => reachesReader(doc, marker.span));
|
|
74
|
+
const markers = markersIn(doc.source, options.lexicon ?? []).filter((marker) => reachesReader(doc, marker.span) && !standsAlone(doc.source, marker.span));
|
|
48
75
|
if (markers.length === 0 || markers.length < options.limit) return [];
|
|
49
76
|
return markers.map((marker) => findingAt(doc, marker.span, { matched: marker.matched, count: markers.length, limit: options.limit }));
|
|
50
77
|
};
|
|
@@ -159,16 +159,30 @@ const NOMINAL_POS: ReadonlySet<string> = new Set(["NOUN", "PROPN"]);
|
|
|
159
159
|
const headAfter = (tokens: readonly Token[], at: number): number =>
|
|
160
160
|
tokens.findIndex((token, index) => index > at && !isSpace(token) && !SKIPPED_POS.has(token.pos) && token.features?.["Bound"] !== "Yes");
|
|
161
161
|
|
|
162
|
+
/** The last noun of the compound that starts at head (新規受付, 受付開始): the compound is named by its last word. */
|
|
163
|
+
const compoundEnd = (tokens: readonly Token[], head: number): number => {
|
|
164
|
+
const after = tokens.findIndex((token, index) => index > head && !isSpace(token) && !NOMINAL_POS.has(token.pos));
|
|
165
|
+
const end = after === -1 ? tokens.length : after;
|
|
166
|
+
return tokens.slice(head, end).reduce((last, token, offset) => (NOMINAL_POS.has(token.pos) ? head + offset : last), head);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const SENTENCE_STOP = "。";
|
|
170
|
+
|
|
162
171
|
/**
|
|
163
|
-
* The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself:
|
|
164
|
-
*
|
|
172
|
+
* The head names a start or a sending (開始, 参る, 連絡). A noun head must be the action itself: a compound whose last
|
|
173
|
+
* noun is listed (新規受付を開始, 受付開始します), followed by a light verb, the end of the sentence, or an object marker
|
|
174
|
+
* whose verb is listed too; not a noun being compared (昨年より販売が増えた, 昨年より新規受付を増やした) or a compound
|
|
175
|
+
* named by another noun (昨年より受付件数を増やした).
|
|
165
176
|
*/
|
|
166
177
|
const isOriginHead = (tokens: readonly Token[], head: number, heads: ReadonlySet<string>, objectMarkers: ReadonlySet<string>): boolean => {
|
|
167
178
|
const token = tokens[head];
|
|
168
|
-
if (token === undefined
|
|
169
|
-
if (!NOMINAL_POS.has(token.pos)) return
|
|
170
|
-
const
|
|
171
|
-
|
|
179
|
+
if (token === undefined) return false;
|
|
180
|
+
if (!NOMINAL_POS.has(token.pos)) return isListed(heads, token);
|
|
181
|
+
const last = compoundEnd(tokens, head);
|
|
182
|
+
if (!isListed(heads, tokens[last])) return false;
|
|
183
|
+
const next = nextWord(tokens, last);
|
|
184
|
+
if (next === undefined || next.surface === SENTENCE_STOP || next.features?.["VerbType"] === "Light") return true;
|
|
185
|
+
return objectMarkers.has(next.surface) && isListed(heads, nextWord(tokens, tokens.indexOf(next)));
|
|
172
186
|
};
|
|
173
187
|
|
|
174
188
|
type ParticleWords = { readonly particles: ReadonlySet<string>; readonly heads: ReadonlySet<string>; readonly objectMarkers: ReadonlySet<string> };
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
2
|
-
import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText } from "../structure/definition-use.ts";
|
|
2
|
+
import { definedTerms, unusedDefinitions, usesBeforeDefinition, type BodyText, type Mentioned } from "../structure/definition-use.ts";
|
|
3
3
|
import { expansionConflicts, expansionsIn } from "../acronym-expansions.ts";
|
|
4
4
|
import { linesOf, lineNumberAt } from "../structure/lines.ts";
|
|
5
|
+
import { isQuotedAlone } from "../quoted-span.ts";
|
|
5
6
|
import { quoteAt } from "./structure-tree.ts";
|
|
6
7
|
|
|
7
8
|
const bodyOf = (doc: ProseDocument): BodyText[] => doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text }));
|
|
8
9
|
|
|
10
|
+
const mentionedIn =
|
|
11
|
+
(doc: ProseDocument): Mentioned =>
|
|
12
|
+
(start, end) =>
|
|
13
|
+
isQuotedAlone(doc.source, { start, end });
|
|
14
|
+
|
|
9
15
|
const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<string, string | number>>): Finding => ({
|
|
10
16
|
rule: "",
|
|
11
17
|
severity: "warning",
|
|
@@ -19,13 +25,17 @@ const findingAt = (doc: ProseDocument, offset: number, values: Readonly<Record<s
|
|
|
19
25
|
export const unusedDefinition: Detector = (doc): Finding[] =>
|
|
20
26
|
doc.structure === undefined
|
|
21
27
|
? []
|
|
22
|
-
: unusedDefinitions(definedTerms(doc.structure), bodyOf(doc)).map((defined) =>
|
|
28
|
+
: unusedDefinitions(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((defined) =>
|
|
29
|
+
findingAt(doc, defined.span.start, { term: defined.term }),
|
|
30
|
+
);
|
|
23
31
|
|
|
24
32
|
/** 文の途中で括弧に入れて定義した語を、定義より前で使っている所。 */
|
|
25
33
|
export const useBeforeDefinition: Detector = (doc): Finding[] =>
|
|
26
34
|
doc.structure === undefined
|
|
27
35
|
? []
|
|
28
|
-
: usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc)).map((use) =>
|
|
36
|
+
: usesBeforeDefinition(definedTerms(doc.structure), bodyOf(doc), mentionedIn(doc)).map((use) =>
|
|
37
|
+
findingAt(doc, use.offset, { term: use.term.term, defined: use.term.line }),
|
|
38
|
+
);
|
|
29
39
|
|
|
30
40
|
/** 同じ略語を、文書の中で二通りに展開している所。 */
|
|
31
41
|
export const acronymExpansionConflict: Detector = (doc): Finding[] => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { formMinority } from "./form-minority.ts";
|
|
4
|
+
import { quoteAround } from "./quote-around.ts";
|
|
5
|
+
|
|
6
|
+
// One document writing an ellipsis two ways ("..." and "…", "……" and "・・・"). Neither is called right; the less common is
|
|
7
|
+
// reported. The forms come from each language's lexicon "ellipsis".
|
|
8
|
+
|
|
9
|
+
/** One ellipsis as written. form is the lexicon's pattern it matched. */
|
|
10
|
+
export type WrittenEllipsis = { readonly offset: number; readonly form: string };
|
|
11
|
+
|
|
12
|
+
/** Characters an ellipsis is made of. A longer run ("....", "…・") is not read, since its form is unclear. */
|
|
13
|
+
const ELLIPSIS_PART = "[..…・‥]";
|
|
14
|
+
|
|
15
|
+
/** A digit or a slash beside the dots: a range (1...10), a path (docs/.../index.md) or a page number after a leader (第1章・・・3). */
|
|
16
|
+
const NOT_PROSE_BESIDE = /[\p{N}/]/u;
|
|
17
|
+
/** The rest of a line after a leader in a table of contents ("Intro ... 12"). */
|
|
18
|
+
const PAGE_NUMBER_LINE = /^\s*\p{N}+\s*(?:\n|$)/u;
|
|
19
|
+
/** A word holding a scheme or a slash around the dots is an address (https://example.com/a...b). */
|
|
20
|
+
const ADDRESS_WORD = /:\/\/|\//u;
|
|
21
|
+
|
|
22
|
+
const SPACE = /\s/u;
|
|
23
|
+
|
|
24
|
+
/** The run of non-space characters holding the dots. */
|
|
25
|
+
const wordAround = (text: string, start: number, end: number): string => {
|
|
26
|
+
const from =
|
|
27
|
+
text
|
|
28
|
+
.slice(0, start)
|
|
29
|
+
.split("")
|
|
30
|
+
.findLastIndex((char) => SPACE.test(char)) + 1;
|
|
31
|
+
const after = text
|
|
32
|
+
.slice(end)
|
|
33
|
+
.split("")
|
|
34
|
+
.findIndex((char) => SPACE.test(char));
|
|
35
|
+
return text.slice(from, after === -1 ? text.length : end + after);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/** Dots that are not an ellipsis in running text: a range, a path or address, a table of contents leader. */
|
|
39
|
+
const isNotProse = (text: string, start: number, end: number): boolean =>
|
|
40
|
+
NOT_PROSE_BESIDE.test(text.charAt(start - 1)) ||
|
|
41
|
+
NOT_PROSE_BESIDE.test(text.charAt(end)) ||
|
|
42
|
+
PAGE_NUMBER_LINE.test(text.slice(end)) ||
|
|
43
|
+
ADDRESS_WORD.test(wordAround(text, start, end));
|
|
44
|
+
|
|
45
|
+
/** The ellipses in a text, matched longest form first, standing alone (not part of a longer run of dots). */
|
|
46
|
+
export const ellipsesIn = (text: string, forms: readonly string[]): WrittenEllipsis[] => {
|
|
47
|
+
if (forms.length === 0) return [];
|
|
48
|
+
const alternatives = forms
|
|
49
|
+
.toSorted((left, right) => right.length - left.length)
|
|
50
|
+
.map(escapeRegExp)
|
|
51
|
+
.join("|");
|
|
52
|
+
const pattern = new RegExp(`(?<!${ELLIPSIS_PART})(?:${alternatives})(?!${ELLIPSIS_PART})`, "gu");
|
|
53
|
+
return [...text.matchAll(pattern)].flatMap((match) =>
|
|
54
|
+
isNotProse(text, match.index, match.index + match[0].length) ? [] : [{ offset: match.index, form: match[0] }],
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const ellipsisConsistency: Detector = (doc, options): Finding[] => {
|
|
59
|
+
const text = doc.prose ?? doc.source;
|
|
60
|
+
const ellipses = ellipsesIn(
|
|
61
|
+
text,
|
|
62
|
+
(doc.lexicons["ellipsis"] ?? []).map((entry) => entry.pattern),
|
|
63
|
+
);
|
|
64
|
+
const minority = formMinority(ellipses, options.limit);
|
|
65
|
+
if (minority === undefined) return [];
|
|
66
|
+
return minority.odd.map((ellipsis) => ({
|
|
67
|
+
rule: "",
|
|
68
|
+
severity: "info",
|
|
69
|
+
line: 0,
|
|
70
|
+
column: 0,
|
|
71
|
+
quote: quoteAround(text, ellipsis.offset, ellipsis.offset + ellipsis.form.length),
|
|
72
|
+
values: { written: ellipsis.form, usual: minority.usual.form, count: minority.odd.length, of: ellipses.length, offset: ellipsis.offset },
|
|
73
|
+
}));
|
|
74
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Detector, Finding, Lexicon, MarkupHeading, ProseDocument } from "../plugin.ts";
|
|
2
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
3
|
+
import { findingAt, writtenHeadings } from "./markup-finding.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 札と番号だけの見出し(メリット1、ポイント2、Benefit 3)。目次を見ても、節に何が書いてあるかが分からない。
|
|
7
|
+
* 札は語彙表 generic-heading-label が言う。題を足した見出し(メリット1:設定が要らない)は読まない。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** 番号の書き方。数字、丸数字、漢数字。前に「その」や「#」、後ろに区切りの記号が付いてもよい。 */
|
|
11
|
+
const NUMBER = String.raw`(?:その)?\s*#?\s*[((]?\s*(?:[0-90-9]+|[①-⑳]|[一二三四五六七八九十]+)\s*(?:つ目)?\s*[::..。))]?`;
|
|
12
|
+
|
|
13
|
+
/** 札のどれかと番号だけで書いた見出しの形。札が無ければ undefined。 */
|
|
14
|
+
export const bareLabelPattern = (labels: Lexicon): RegExp | undefined => {
|
|
15
|
+
const words = labels.map((entry) => escapeRegExp(entry.pattern)).filter((word) => word !== "");
|
|
16
|
+
return words.length === 0 ? undefined : new RegExp(`^(?:${words.join("|")})\\s*${NUMBER}$`, "iu");
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** 札と番号だけの見出し。 */
|
|
20
|
+
export const bareLabelHeadings = (doc: ProseDocument): MarkupHeading[] => {
|
|
21
|
+
const pattern = bareLabelPattern(doc.lexicons["generic-heading-label"] ?? []);
|
|
22
|
+
return pattern === undefined ? [] : writtenHeadings(doc).filter((heading) => pattern.test(heading.text.trim()));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 数で見る。一つなら書き手の選び方、並べば型をなぞった見出し。 */
|
|
26
|
+
export const genericHeading: Detector = (doc, options): Finding[] => {
|
|
27
|
+
const bare = bareLabelHeadings(doc);
|
|
28
|
+
if (bare.length === 0 || bare.length < options.limit) return [];
|
|
29
|
+
return bare.map((heading) => findingAt(doc, heading, { heading: heading.text.trim(), count: bare.length, limit: options.limit }));
|
|
30
|
+
};
|