chaffjs 0.15.0 → 0.16.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 +27 -2
- package/dist/address-chain.js +1 -1
- package/dist/address-chain.js.map +1 -1
- package/dist/backends/anthropic.d.ts +2 -1
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +4 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/openai.d.ts +2 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/openai.js +1 -1
- package/dist/backends/openai.js.map +1 -1
- package/dist/backends/types.d.ts +2 -1
- package/dist/backends/types.d.ts.map +1 -1
- package/dist/backends/types.js +1 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/baseline.js +1 -1
- package/dist/baseline.js.map +1 -1
- package/dist/calendar-number.d.ts +26 -0
- package/dist/calendar-number.d.ts.map +1 -0
- package/dist/calendar-number.js +51 -0
- package/dist/calendar-number.js.map +1 -0
- package/dist/cli-args.d.ts +0 -5
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -2
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-text.d.ts +6 -2
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +19 -12
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -37
- package/dist/cli.js.map +1 -1
- package/dist/commands/cite.d.ts.map +1 -1
- package/dist/commands/cite.js +4 -4
- package/dist/commands/cite.js.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init-ask.d.ts +19 -0
- package/dist/commands/init-ask.d.ts.map +1 -0
- package/dist/commands/init-ask.js +43 -0
- package/dist/commands/init-ask.js.map +1 -0
- package/dist/commands/test.d.ts +8 -0
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +70 -38
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts +1 -8
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +7 -6
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/by-path.d.ts.map +1 -1
- package/dist/config/by-path.js +10 -9
- package/dist/config/by-path.js.map +1 -1
- package/dist/config/load.d.ts +4 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +19 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/name-problems.d.ts +5 -0
- package/dist/config/name-problems.d.ts.map +1 -0
- package/dist/config/name-problems.js +11 -0
- package/dist/config/name-problems.js.map +1 -0
- package/dist/date-stamp.js +1 -1
- package/dist/date-stamp.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +16 -6
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +101 -7
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/enumerated-runs.d.ts.map +1 -1
- package/dist/detectors/enumerated-runs.js +4 -2
- package/dist/detectors/enumerated-runs.js.map +1 -1
- package/dist/detectors/gram-place.js +1 -1
- package/dist/detectors/gram-place.js.map +1 -1
- package/dist/detectors/heading-case.d.ts.map +1 -1
- package/dist/detectors/heading-case.js +3 -1
- package/dist/detectors/heading-case.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -3
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lettered-index.d.ts +9 -0
- package/dist/detectors/lettered-index.d.ts.map +1 -0
- package/dist/detectors/lettered-index.js +54 -0
- package/dist/detectors/lettered-index.js.map +1 -0
- package/dist/detectors/lexicon.d.ts +5 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +46 -3
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +24 -6
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/phrase-match.d.ts +3 -1
- package/dist/detectors/phrase-match.d.ts.map +1 -1
- package/dist/detectors/phrase-match.js +20 -15
- package/dist/detectors/phrase-match.js.map +1 -1
- package/dist/detectors/repeated-head.d.ts.map +1 -1
- package/dist/detectors/repeated-head.js +5 -4
- package/dist/detectors/repeated-head.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +16 -6
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/stacked-hedge.d.ts +23 -0
- package/dist/detectors/stacked-hedge.d.ts.map +1 -0
- package/dist/detectors/stacked-hedge.js +64 -0
- package/dist/detectors/stacked-hedge.js.map +1 -0
- package/dist/detectors/superlative-amount.d.ts +9 -0
- package/dist/detectors/superlative-amount.d.ts.map +1 -0
- package/dist/detectors/superlative-amount.js +20 -0
- package/dist/detectors/superlative-amount.js.map +1 -0
- package/dist/detectors/superlative-clause.d.ts +18 -0
- package/dist/detectors/superlative-clause.d.ts.map +1 -0
- package/dist/detectors/superlative-clause.js +61 -0
- package/dist/detectors/superlative-clause.js.map +1 -0
- package/dist/detectors/superlative-scope.d.ts +6 -0
- package/dist/detectors/superlative-scope.d.ts.map +1 -1
- package/dist/detectors/superlative-scope.js +3 -3
- package/dist/detectors/superlative-scope.js.map +1 -1
- package/dist/detectors/team.d.ts +0 -6
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +17 -1
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/token-shape.d.ts.map +1 -1
- package/dist/detectors/token-shape.js +8 -6
- package/dist/detectors/token-shape.js.map +1 -1
- package/dist/document.d.ts +3 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +18 -3
- package/dist/document.js.map +1 -1
- package/dist/eval.js +1 -1
- package/dist/eval.js.map +1 -1
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/draft.js.map +1 -1
- package/dist/feedback/excerpt.d.ts +0 -1
- package/dist/feedback/excerpt.d.ts.map +1 -1
- package/dist/feedback/excerpt.js +2 -2
- package/dist/feedback/excerpt.js.map +1 -1
- package/dist/file-header.d.ts +20 -0
- package/dist/file-header.d.ts.map +1 -0
- package/dist/file-header.js +21 -0
- package/dist/file-header.js.map +1 -0
- package/dist/files.js +3 -3
- package/dist/files.js.map +1 -1
- package/dist/genre-check.js +1 -1
- package/dist/genre-check.js.map +1 -1
- package/dist/genre-load.d.ts +6 -0
- package/dist/genre-load.d.ts.map +1 -0
- package/dist/genre-load.js +22 -0
- package/dist/genre-load.js.map +1 -0
- package/dist/genre-parse.d.ts +40 -0
- package/dist/genre-parse.d.ts.map +1 -0
- package/dist/genre-parse.js +104 -0
- package/dist/genre-parse.js.map +1 -0
- package/dist/genre-suggest.d.ts +15 -0
- package/dist/genre-suggest.d.ts.map +1 -0
- package/dist/genre-suggest.js +26 -0
- package/dist/genre-suggest.js.map +1 -0
- package/dist/genre.d.ts +1 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +3 -13
- package/dist/genre.js.map +1 -1
- package/dist/init-choice.d.ts +12 -0
- package/dist/init-choice.d.ts.map +1 -0
- package/dist/init-choice.js +41 -0
- package/dist/init-choice.js.map +1 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +16 -5
- package/dist/init.js.map +1 -1
- package/dist/judge.d.ts +2 -1
- package/dist/judge.d.ts.map +1 -1
- package/dist/judge.js +1 -1
- package/dist/judge.js.map +1 -1
- package/dist/line-paragraphs.d.ts +4 -0
- package/dist/line-paragraphs.d.ts.map +1 -0
- package/dist/line-paragraphs.js +69 -0
- package/dist/line-paragraphs.js.map +1 -0
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +7 -6
- package/dist/mask.js.map +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/check.d.ts +28 -0
- package/dist/profile/check.d.ts.map +1 -0
- package/dist/profile/check.js +12 -0
- package/dist/profile/check.js.map +1 -0
- package/dist/profile/for-file.d.ts +2 -2
- package/dist/profile/for-file.d.ts.map +1 -1
- package/dist/profile/for-file.js +9 -2
- package/dist/profile/for-file.js.map +1 -1
- package/dist/profile/load.js +1 -1
- package/dist/profile/load.js.map +1 -1
- package/dist/profile/select.d.ts +4 -2
- package/dist/profile/select.d.ts.map +1 -1
- package/dist/profile/select.js +8 -3
- package/dist/profile/select.js.map +1 -1
- package/dist/quoted-span.d.ts +15 -3
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +16 -11
- package/dist/quoted-span.js.map +1 -1
- package/dist/reasons.d.ts +1 -0
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +4 -1
- package/dist/reasons.js.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +1 -2
- package/dist/render/compact.js.map +1 -1
- package/dist/render/eval.d.ts.map +1 -1
- package/dist/render/eval.js +4 -3
- package/dist/render/eval.js.map +1 -1
- package/dist/render/friendly.d.ts +2 -1
- package/dist/render/friendly.d.ts.map +1 -1
- package/dist/render/friendly.js +14 -6
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/genres.d.ts +5 -0
- package/dist/render/genres.d.ts.map +1 -0
- package/dist/render/genres.js +28 -0
- package/dist/render/genres.js.map +1 -0
- package/dist/render/plural.d.ts +3 -0
- package/dist/render/plural.d.ts.map +1 -0
- package/dist/render/plural.js +5 -0
- package/dist/render/plural.js.map +1 -0
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +9 -3
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/semantic.d.ts +3 -2
- package/dist/render/semantic.d.ts.map +1 -1
- package/dist/render/semantic.js +94 -34
- package/dist/render/semantic.js.map +1 -1
- package/dist/render/summary.d.ts.map +1 -1
- package/dist/render/summary.js +4 -3
- package/dist/render/summary.js.map +1 -1
- package/dist/render/suppressions.d.ts.map +1 -1
- package/dist/render/suppressions.js +3 -2
- package/dist/render/suppressions.js.map +1 -1
- package/dist/render/text.d.ts +2 -0
- package/dist/render/text.d.ts.map +1 -1
- package/dist/render/text.js +11 -3
- package/dist/render/text.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +4 -1
- package/dist/rule-load.js.map +1 -1
- package/dist/run-semantic.js +1 -1
- package/dist/run-semantic.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +25 -15
- package/dist/run.js.map +1 -1
- package/dist/sentence-shape.d.ts +0 -1
- package/dist/sentence-shape.d.ts.map +1 -1
- package/dist/sentence-shape.js +3 -5
- package/dist/sentence-shape.js.map +1 -1
- package/dist/setting-problems.d.ts +5 -0
- package/dist/setting-problems.d.ts.map +1 -0
- package/dist/setting-problems.js +15 -0
- package/dist/setting-problems.js.map +1 -0
- package/dist/structure/build.js +5 -5
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cite.js +1 -1
- package/dist/structure/cite.js.map +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +5 -5
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/markdown-path.d.ts.map +1 -1
- package/dist/structure/markdown-path.js +2 -2
- package/dist/structure/markdown-path.js.map +1 -1
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +1 -1
- package/dist/structure/numbering.js.map +1 -1
- package/dist/structure/relative-find.js +1 -1
- package/dist/structure/relative-find.js.map +1 -1
- package/dist/structure/sexp.js +1 -1
- package/dist/structure/sexp.js.map +1 -1
- package/dist/structure/unnumbered.js +2 -2
- package/dist/structure/unnumbered.js.map +1 -1
- package/dist/team-names.d.ts +9 -0
- package/dist/team-names.d.ts.map +1 -0
- package/dist/team-names.js +34 -0
- package/dist/team-names.js.map +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +6 -0
- package/dist/ui.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/watch.d.ts +2 -2
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +5 -5
- package/dist/watch.js.map +1 -1
- package/genres.yaml +363 -0
- package/package.json +4 -3
- package/rules/adverb-overuse.yaml +2 -2
- package/rules/agentless-passive.yaml +1 -1
- package/rules/ai-generated-composite.yaml +2 -2
- package/rules/ai-tell.yaml +2 -2
- package/rules/bold-density.yaml +2 -2
- package/rules/closing-cliche.yaml +1 -1
- package/rules/concrete-evidence-density.yaml +2 -2
- package/rules/contraction-consistency.yaml +1 -1
- package/rules/cushion-phrase-density.yaml +1 -1
- package/rules/dangling-reference.yaml +1 -1
- package/rules/date-order.yaml +1 -1
- package/rules/date-weekday-mismatch.yaml +1 -1
- package/rules/double-keigo.yaml +1 -1
- package/rules/doubled-word.yaml +1 -1
- package/rules/duplicate-definition.yaml +1 -1
- package/rules/emoji-density.yaml +1 -1
- package/rules/empty-conclusion.yaml +2 -1
- package/rules/empty-intensifier.yaml +1 -1
- package/rules/excessive-hedging.yaml +16 -6
- package/rules/expletive-construction.yaml +2 -2
- package/rules/heading-echo.yaml +1 -1
- package/rules/hiragana-fukushi.yaml +1 -1
- package/rules/internal-jargon.yaml +2 -2
- package/rules/latin-spacing.yaml +5 -4
- package/rules/max-kanji-continuous.yaml +4 -3
- package/rules/max-paragraph-length.yaml +9 -2
- package/rules/max-sentence-length.yaml +14 -2
- package/rules/ngram-repetition.yaml +4 -4
- package/rules/no-doubled-joshi.yaml +2 -2
- package/rules/no-em-dash.yaml +2 -2
- package/rules/no-mixed-desumasu.yaml +1 -1
- package/rules/no-nakaguro-parallel.yaml +2 -2
- package/rules/numbering-gap.yaml +1 -1
- package/rules/oxford-comma-consistency.yaml +2 -1
- package/rules/padded-intro.yaml +2 -1
- package/rules/paragraph-length-variance.yaml +1 -1
- package/rules/preamble-length.yaml +2 -2
- package/rules/preferred-term.yaml +1 -1
- package/rules/proper-noun-density.yaml +3 -2
- package/rules/repeated-conjunction.yaml +2 -2
- package/rules/repeated-sentence-head.yaml +2 -2
- package/rules/required-sections.yaml +1 -1
- package/rules/rule-of-three.yaml +2 -2
- package/rules/sasete-itadaku.yaml +3 -3
- package/rules/section-length-uniformity.yaml +1 -1
- package/rules/sentence-initial-conjunction-run.yaml +2 -2
- package/rules/sentence-rhythm.yaml +1 -1
- package/rules/taigen-dome-in-prose.yaml +2 -2
- package/rules/title-case-consistency.yaml +1 -1
- package/rules/total-mismatch.yaml +1 -1
- package/rules/undefined-acronym.yaml +4 -4
- package/rules/unqualified-superlative.yaml +2 -2
- package/skills/chaff/SKILL.md +28 -9
- package/src/address-chain.ts +1 -1
- package/src/backends/anthropic.ts +5 -1
- package/src/backends/openai.ts +2 -1
- package/src/backends/types.ts +3 -2
- package/src/baseline.ts +1 -1
- package/src/calendar-number.ts +75 -0
- package/src/cli-args.ts +2 -2
- package/src/cli-text.ts +29 -15
- package/src/cli.ts +42 -41
- package/src/commands/cite.ts +4 -4
- package/src/commands/eval.ts +1 -1
- package/src/commands/init-ask.ts +53 -0
- package/src/commands/test.ts +102 -45
- package/src/commands/tree.ts +7 -6
- package/src/config/by-path.ts +8 -9
- package/src/config/load.ts +24 -0
- package/src/config/name-problems.ts +15 -0
- package/src/date-stamp.ts +1 -1
- package/src/detectors/char-shape.ts +21 -9
- package/src/detectors/en-shape.ts +106 -8
- package/src/detectors/enumerated-runs.ts +3 -2
- package/src/detectors/gram-place.ts +1 -1
- package/src/detectors/heading-case.ts +4 -1
- package/src/detectors/index.ts +4 -3
- package/src/detectors/lettered-index.ts +78 -0
- package/src/detectors/lexicon.ts +63 -6
- package/src/detectors/orthography.ts +31 -7
- package/src/detectors/phrase-match.ts +23 -12
- package/src/detectors/repeated-head.ts +3 -4
- package/src/detectors/signals.ts +15 -6
- package/src/detectors/stacked-hedge.ts +93 -0
- package/src/detectors/superlative-amount.ts +24 -0
- package/src/detectors/superlative-clause.ts +82 -0
- package/src/detectors/superlative-scope.ts +3 -3
- package/src/detectors/team.ts +18 -2
- package/src/detectors/token-shape.ts +8 -10
- package/src/document.ts +29 -3
- package/src/eval.ts +1 -1
- package/src/feedback/draft.ts +1 -1
- package/src/feedback/excerpt.ts +2 -2
- package/src/file-header.ts +31 -0
- package/src/files.ts +3 -3
- package/src/genre-check.ts +1 -1
- package/src/genre-load.ts +24 -0
- package/src/genre-parse.ts +126 -0
- package/src/genre-suggest.ts +37 -0
- package/src/genre.ts +4 -13
- package/src/init-choice.ts +47 -0
- package/src/init.ts +20 -6
- package/src/judge.ts +3 -1
- package/src/line-paragraphs.ts +83 -0
- package/src/mask.ts +5 -6
- package/src/plugin.ts +6 -0
- package/src/profile/check.ts +34 -0
- package/src/profile/for-file.ts +10 -3
- package/src/profile/load.ts +1 -1
- package/src/profile/select.ts +11 -4
- package/src/quoted-span.ts +26 -12
- package/src/reasons.ts +5 -1
- package/src/render/compact.ts +1 -3
- package/src/render/eval.ts +4 -3
- package/src/render/friendly.ts +21 -6
- package/src/render/genres.ts +31 -0
- package/src/render/plural.ts +6 -0
- package/src/render/rules-json.ts +10 -3
- package/src/render/semantic.ts +117 -34
- package/src/render/summary.ts +4 -3
- package/src/render/suppressions.ts +3 -2
- package/src/render/text.ts +18 -3
- package/src/rule-load.ts +6 -1
- package/src/run-semantic.ts +1 -1
- package/src/run.ts +28 -15
- package/src/sentence-shape.ts +3 -7
- package/src/setting-problems.ts +24 -0
- package/src/structure/build.ts +5 -5
- package/src/structure/cite.ts +1 -1
- package/src/structure/issues.ts +6 -5
- package/src/structure/markdown-path.ts +2 -2
- package/src/structure/numbering.ts +1 -1
- package/src/structure/relative-find.ts +1 -1
- package/src/structure/sexp.ts +1 -1
- package/src/structure/unnumbered.ts +2 -2
- package/src/team-names.ts +39 -0
- package/src/ui.ts +7 -0
- package/src/version.ts +1 -1
- package/src/watch.ts +5 -5
package/src/init.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import type { Texts, UiLanguage } from "./ui.ts";
|
|
4
|
+
import { loadGenres } from "./genre-load.ts";
|
|
4
5
|
|
|
5
6
|
const TEXT: Texts<{
|
|
6
|
-
readonly config: (genre: string) => string;
|
|
7
|
+
readonly config: (genre: string, summary: string) => string;
|
|
7
8
|
readonly created: string;
|
|
8
9
|
readonly appended: (lines: readonly string[]) => string;
|
|
9
10
|
readonly configNote: string;
|
|
@@ -15,7 +16,7 @@ const TEXT: Texts<{
|
|
|
15
16
|
readonly nextCheck: string;
|
|
16
17
|
}> = {
|
|
17
18
|
ja: {
|
|
18
|
-
config: (genre) => `# chaff.yaml — このチームの文章規範
|
|
19
|
+
config: (genre, summary) => `# chaff.yaml — このチームの文章規範
|
|
19
20
|
#
|
|
20
21
|
# ここに書くのは「既定から変えたもの」だけです。書かなければ既定で動きます。
|
|
21
22
|
# このファイルを消しても chaff は動きます。
|
|
@@ -33,9 +34,13 @@ const TEXT: Texts<{
|
|
|
33
34
|
# npx chaff explain bold-density そのルールの意図を読む
|
|
34
35
|
# npx chaff rules --json AI に設定を書かせるときに渡す
|
|
35
36
|
|
|
36
|
-
#
|
|
37
|
+
# この場所に置く文書の種類(${summary})。ほかの種類: npx chaffjs genres
|
|
37
38
|
genre: ${genre}
|
|
38
39
|
|
|
40
|
+
# チームが書く固有名詞(組織名・製品名)。1 つの名前として読み、漢字の連なりに数えない。
|
|
41
|
+
# names:
|
|
42
|
+
# - 個人情報保護委員会
|
|
43
|
+
|
|
39
44
|
# 既定から変えたものだけを書く。
|
|
40
45
|
rules:
|
|
41
46
|
`,
|
|
@@ -50,7 +55,7 @@ rules:
|
|
|
50
55
|
nextCheck: " npx chaff . この場所の Markdown を全部見る",
|
|
51
56
|
},
|
|
52
57
|
en: {
|
|
53
|
-
config: (genre) => `# chaff.yaml — this team's writing rules
|
|
58
|
+
config: (genre, summary) => `# chaff.yaml — this team's writing rules
|
|
54
59
|
#
|
|
55
60
|
# Write only what differs from the defaults; anything left out uses the default.
|
|
56
61
|
# chaff still runs if this file is deleted.
|
|
@@ -68,9 +73,13 @@ rules:
|
|
|
68
73
|
# npx chaff explain bold-density read what the rule is for
|
|
69
74
|
# npx chaff rules --json give this to an AI that writes the settings
|
|
70
75
|
|
|
71
|
-
# The kind of document kept here.
|
|
76
|
+
# The kind of document kept here (${summary}). The others: npx chaffjs genres
|
|
72
77
|
genre: ${genre}
|
|
73
78
|
|
|
79
|
+
# The names your team writes (organisations, products). Each is read as one name, not as words to count.
|
|
80
|
+
# names:
|
|
81
|
+
# - Bank of England
|
|
82
|
+
|
|
74
83
|
# Only what differs from the defaults.
|
|
75
84
|
rules:
|
|
76
85
|
`,
|
|
@@ -108,9 +117,14 @@ const ensureGitignore = (dir: string, text: (typeof TEXT)["ja"]): Written | unde
|
|
|
108
117
|
return { path, note: text.appended(missing) };
|
|
109
118
|
};
|
|
110
119
|
|
|
120
|
+
/** What the genre is for, as chaff genres says it; the genre itself when chaff does not know it. */
|
|
121
|
+
const summaryOf = (genre: string, ui: UiLanguage): string => loadGenres().genres.find((entry) => entry.id === genre)?.summary[ui] ?? genre;
|
|
122
|
+
|
|
111
123
|
export const runInit = (dir: string, genre: string, ui: UiLanguage = "ja"): string[] => {
|
|
112
124
|
const text = TEXT[ui];
|
|
113
|
-
const made = [writeIfAbsent(join(dir, "chaff.yaml"), text.config(genre), text.configNote), ensureGitignore(dir, text)].filter(
|
|
125
|
+
const made = [writeIfAbsent(join(dir, "chaff.yaml"), text.config(genre, summaryOf(genre, ui)), text.configNote), ensureGitignore(dir, text)].filter(
|
|
126
|
+
(entry) => entry !== undefined,
|
|
127
|
+
);
|
|
114
128
|
if (made.length === 0) return [text.exists];
|
|
115
129
|
return [
|
|
116
130
|
"",
|
package/src/judge.ts
CHANGED
|
@@ -6,6 +6,7 @@ import * as openai from "./backends/openai.ts";
|
|
|
6
6
|
import type { AnthropicClient } from "./backends/anthropic.ts";
|
|
7
7
|
import type { OpenAIClient } from "./backends/openai.ts";
|
|
8
8
|
import type { BackendName, Failure, Judge, JsonSchema } from "./backends/types.ts";
|
|
9
|
+
import type { UiLanguage } from "./ui.ts";
|
|
9
10
|
|
|
10
11
|
export const CACHE_DIR = ".chaff-cache";
|
|
11
12
|
|
|
@@ -87,7 +88,8 @@ export const isAuthFailure = (backend: BackendName, error: unknown): boolean =>
|
|
|
87
88
|
export const describeFailure = (backend: BackendName, error: unknown): Failure | undefined =>
|
|
88
89
|
backend === "openai" ? openai.describeFailure(error) : anthropic.describeFailure(error);
|
|
89
90
|
|
|
90
|
-
export const credentialHint = (backend: BackendName): string =>
|
|
91
|
+
export const credentialHint = (backend: BackendName, language: UiLanguage): string =>
|
|
92
|
+
(backend === "openai" ? openai.SETUP_HINT : anthropic.SETUP_HINT)[language];
|
|
91
93
|
|
|
92
94
|
/**
|
|
93
95
|
* 同じ (model, rule, rubric, candidate) なら 2 度目は問い合わせない。
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 1 行に 1 段落を書き、段落の間に空行を置かない文書(青空文庫、議事録、`<br>` で改行した HTML)。
|
|
5
|
+
* Markdown では全体が 1 つの段落になるが、書いた人にとっては行が段落。
|
|
6
|
+
*
|
|
7
|
+
* 割るのは文の終わる改行だけ。文の境目は解析器が改行をつないだ上で決めたもの(#248)なので、折り返した文は割れない。
|
|
8
|
+
* 形で見分けるのは 2 つ:
|
|
9
|
+
* - 固定幅の折り返しは、行の多くが文の途中で終わる。文で終わる行(段落の最後の行を含む)が半分に届かなければ割らない
|
|
10
|
+
* (議事録は発言者の行と発言の行が交互に来るので、半分ちょうどは割る)。
|
|
11
|
+
* - 1 行 1 文の書き方(行を意味の切れ目で改める。英語の Markdown に多い)は、段落のつもりではない。
|
|
12
|
+
* 文の頭で始まり文の終わりで終わる行が 2 文以上を持つことが、段落を 1 行に書いた印。それがたまにしか無ければ割らない。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** 2 文以上を持つ行がこれより少なければ、1 行 1 文の書き方にたまに混じる 2 文の行と見分けられない。 */
|
|
16
|
+
const MIN_MULTI_SENTENCE = 3;
|
|
17
|
+
/** 2 文以上を持つ行が、割った段落の数のこの割合に届かなければ割らない。議事録は 1 文の発言が多い。 */
|
|
18
|
+
const MULTI_SENTENCE_SHARE = 1 / 5;
|
|
19
|
+
|
|
20
|
+
const NEWLINE = /\r?\n/gu;
|
|
21
|
+
|
|
22
|
+
type Line = { readonly span: Span; readonly newline: number };
|
|
23
|
+
|
|
24
|
+
/** 段落の中の行と、その行を終える改行の位置(最後の行は段落の終わり)。 */
|
|
25
|
+
const linesOf = (source: string, paragraph: Span): Line[] => {
|
|
26
|
+
const text = source.slice(paragraph.start, paragraph.end);
|
|
27
|
+
const breaks = [...text.matchAll(NEWLINE)].map((match) => ({ at: paragraph.start + match.index, width: match[0].length }));
|
|
28
|
+
const starts = [paragraph.start, ...breaks.map((found) => found.at + found.width)];
|
|
29
|
+
return starts.map((start, index) => {
|
|
30
|
+
const end = breaks[index]?.at ?? paragraph.end;
|
|
31
|
+
return { span: { start, end }, newline: end };
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** 文の前後の空白を除いた範囲。改行が文の中にあるかを、空白でなく文字で決める。 */
|
|
36
|
+
const coreOf = (source: string, sentence: Span): Span => {
|
|
37
|
+
const text = source.slice(sentence.start, sentence.end);
|
|
38
|
+
return { start: sentence.start + text.length - text.trimStart().length, end: sentence.end - (text.length - text.trimEnd().length) };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** cores(並び順、重ならない)の中で、start が offset 以上の最初の添字。何万行の段落でも行ごとに全部をなめない。 */
|
|
42
|
+
const firstStartingAt = (cores: readonly Span[], offset: number): number => {
|
|
43
|
+
const search = (low: number, high: number): number => {
|
|
44
|
+
if (low >= high) return low;
|
|
45
|
+
const middle = (low + high) >> 1;
|
|
46
|
+
return (cores[middle]?.start ?? Number.POSITIVE_INFINITY) >= offset ? search(low, middle) : search(middle + 1, high);
|
|
47
|
+
};
|
|
48
|
+
return search(0, cores.length);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** 改行をまたぐ文が無い。またぐなら、改行の直前に始まった文がまだ続いている。 */
|
|
52
|
+
const endsSentence = (line: Line, cores: readonly Span[]): boolean => (cores[firstStartingAt(cores, line.newline) - 1]?.end ?? 0) <= line.newline;
|
|
53
|
+
|
|
54
|
+
/** 行の並びを、文の終わる改行で区切った段落。 */
|
|
55
|
+
const groupAt = (lines: readonly Line[], closing: readonly boolean[]): Span[] => {
|
|
56
|
+
const firsts = lines.flatMap((_, index) => (index === 0 || closing[index - 1] === true ? [index] : []));
|
|
57
|
+
return firsts.map((first, index) => ({
|
|
58
|
+
start: lines[first]?.span.start ?? 0,
|
|
59
|
+
end: lines[(firsts[index + 1] ?? lines.length) - 1]?.span.end ?? 0,
|
|
60
|
+
}));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const sentencesIn = (group: Span, cores: readonly Span[]): number => firstStartingAt(cores, group.end) - firstStartingAt(cores, group.start);
|
|
64
|
+
|
|
65
|
+
/** 文の頭で始まり文の終わりで終わる行のうち、2 文以上を持つもの。固定幅の折り返しでは、行の両端が偶然そろうことはまれ。 */
|
|
66
|
+
const wholeMultiSentenceLines = (lines: readonly Line[], closing: readonly boolean[], cores: readonly Span[]): number =>
|
|
67
|
+
lines.filter((line, index) => (index === 0 || closing[index - 1] === true) && closing[index] === true && sentencesIn(line.span, cores) >= 2).length;
|
|
68
|
+
|
|
69
|
+
/** 行ごとの段落として読む形か。 */
|
|
70
|
+
const isLineShaped = (lines: readonly Line[], closing: readonly boolean[], groups: readonly Span[], cores: readonly Span[]): boolean => {
|
|
71
|
+
const closed = closing.filter(Boolean).length;
|
|
72
|
+
const multi = wholeMultiSentenceLines(lines, closing, cores);
|
|
73
|
+
return closed * 2 >= closing.length && multi >= MIN_MULTI_SENTENCE && multi >= groups.length * MULTI_SENTENCE_SHARE;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** paragraph を、書いた人が段落のつもりで改めた行ごとに割る。そう読めなければ paragraph 1 つ。sentences は段落の中の文。 */
|
|
77
|
+
export const lineParagraphs = (source: string, paragraph: Span, sentences: readonly Span[]): Span[] => {
|
|
78
|
+
const lines = linesOf(source, paragraph);
|
|
79
|
+
const cores = sentences.map((sentence) => coreOf(source, sentence)).filter((core) => core.end > core.start);
|
|
80
|
+
const closing = lines.map((line) => endsSentence(line, cores));
|
|
81
|
+
const groups = groupAt(lines, closing);
|
|
82
|
+
return isLineShaped(lines, closing, groups, cores) ? groups : [paragraph];
|
|
83
|
+
};
|
package/src/mask.ts
CHANGED
|
@@ -17,14 +17,13 @@ import type { Span } from "./plugin.ts";
|
|
|
17
17
|
const blankOut = (text: string): string => text.replace(/[^\n]/gu, (char) => " ".repeat(char.length));
|
|
18
18
|
|
|
19
19
|
const merge = (spans: readonly Span[]): Span[] =>
|
|
20
|
-
|
|
21
|
-
.
|
|
20
|
+
spans
|
|
21
|
+
.toSorted((left, right) => left.start - right.start)
|
|
22
22
|
.reduce<Span[]>((acc, span) => {
|
|
23
23
|
const last = acc.at(-1);
|
|
24
|
-
if (last !== undefined && span.start <= last.end) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return [...acc, span];
|
|
24
|
+
if (last !== undefined && span.start <= last.end) acc[acc.length - 1] = { start: last.start, end: Math.max(last.end, span.end) };
|
|
25
|
+
else acc.push(span);
|
|
26
|
+
return acc;
|
|
28
27
|
}, []);
|
|
29
28
|
|
|
30
29
|
export const maskSpans = (source: string, spans: readonly Span[]): string => {
|
package/src/plugin.ts
CHANGED
|
@@ -314,6 +314,8 @@ export type ProseDocument = {
|
|
|
314
314
|
readonly lexicons: Readonly<Record<string, Lexicon>>;
|
|
315
315
|
/** この種類の文書に無いと困る見出し。チームが chaff.yaml で決める。 */
|
|
316
316
|
readonly requiredSections: readonly string[];
|
|
317
|
+
/** チームが chaff.yaml の names に並べた固有名詞。1 つの名前として読み、書き手の落ち度に数えない。 */
|
|
318
|
+
readonly names?: readonly string[];
|
|
317
319
|
/** 番地の付いた木(§27)。adapter が structure を持たない言語では無い。 */
|
|
318
320
|
readonly structure: StructureNode | undefined;
|
|
319
321
|
/** 文書の種類(法令など)。選ばれなければ無い。 */
|
|
@@ -333,6 +335,8 @@ export type Finding = {
|
|
|
333
335
|
readonly quote: string;
|
|
334
336
|
/** message のプレースホルダに入れる値。 */
|
|
335
337
|
readonly values: Readonly<Record<string, string | number>>;
|
|
338
|
+
/** 1 つの rule が 2 通りの見つけ方を持つとき、どちらで見つけたか。rule の messages から文を選ぶ。無ければ message。 */
|
|
339
|
+
readonly variant?: string;
|
|
336
340
|
};
|
|
337
341
|
|
|
338
342
|
/** rule 定義が threshold を渡す。数値は 4 語から解決済み。 */
|
|
@@ -360,6 +364,8 @@ export type RuleDefinition = {
|
|
|
360
364
|
readonly why: Localized;
|
|
361
365
|
readonly how_to_fix: Localized;
|
|
362
366
|
readonly message: Localized;
|
|
367
|
+
/** 見つけ方ごとの message(Finding の variant で引く)。ほとんどの rule は持たない。 */
|
|
368
|
+
readonly messages: Readonly<Record<string, Localized>>;
|
|
363
369
|
/** 4 語と数値の対応。2 つ以上。未定義の段は normal に落ちる。spec §18.1。
|
|
364
370
|
* 言語別の閾値を持つ rule(max-sentence-length)は、読み込み時に言語で平坦化済み。 */
|
|
365
371
|
readonly levels: LevelTable;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NO_PROFILE } from "./select.ts";
|
|
2
|
+
|
|
3
|
+
/** Where a profile was set: chaff.yaml's profile, or one of its by_path entries. */
|
|
4
|
+
export type ProfileSetting = "config" | "by_path";
|
|
5
|
+
|
|
6
|
+
type UnknownProfile = {
|
|
7
|
+
readonly where: ProfileSetting;
|
|
8
|
+
readonly profile: string;
|
|
9
|
+
/** The by_path entry's files, so the message can say which entry; empty for chaff.yaml's profile. */
|
|
10
|
+
readonly files: readonly string[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PathProfile = { readonly files: readonly string[]; readonly profile?: string | undefined };
|
|
14
|
+
|
|
15
|
+
type WrittenProfiles = { readonly config: string | undefined; readonly byPath: readonly PathProfile[] };
|
|
16
|
+
|
|
17
|
+
/** Commands that never read a document, so never choose a profile. A wrong one in chaff.yaml must not stop them. */
|
|
18
|
+
const PROFILE_FREE: ReadonlySet<string> = new Set(["init", "genres", "skill", "relax", "strict", "off", "rules", "explain"]);
|
|
19
|
+
|
|
20
|
+
/** The profiles a command would read from chaff.yaml: none for a command that reads no document. */
|
|
21
|
+
export const writtenProfiles = (command: string, config: { readonly profile: string | undefined; readonly byPath: readonly PathProfile[] }): WrittenProfiles =>
|
|
22
|
+
PROFILE_FREE.has(command) ? { config: undefined, byPath: [] } : { config: config.profile, byPath: config.byPath };
|
|
23
|
+
|
|
24
|
+
const setting = (where: ProfileSetting, profile: string | undefined, files: readonly string[] = []): UnknownProfile[] =>
|
|
25
|
+
profile === undefined ? [] : [{ where, profile, files }];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Every profile a setting names that is neither a bundled one nor none. Such a name chooses nothing and also stops the choice from the content,
|
|
29
|
+
* so it silently acts as none. All of them, not only the one a file would use, because a wrong setting is wrong for every file.
|
|
30
|
+
*/
|
|
31
|
+
export const unknownProfiles = (written: WrittenProfiles, known: readonly string[]): UnknownProfile[] =>
|
|
32
|
+
[...setting("config", written.config), ...written.byPath.flatMap((entry) => setting("by_path", entry.profile, entry.files))].filter(
|
|
33
|
+
(entry) => entry.profile !== NO_PROFILE && !known.includes(entry.profile),
|
|
34
|
+
);
|
package/src/profile/for-file.ts
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import { applyByPath } from "../config/by-path.ts";
|
|
2
2
|
import type { Config } from "../config/load.ts";
|
|
3
3
|
import type { DocumentProfile } from "../plugin.ts";
|
|
4
|
+
import { presetProfile } from "../genre-load.ts";
|
|
4
5
|
import { loadProfiles } from "./load.ts";
|
|
5
6
|
import { chooseProfile } from "./select.ts";
|
|
6
7
|
|
|
7
8
|
type ProfileSettings = Pick<Config, "profile" | "byPath" | "baseDir">;
|
|
8
9
|
|
|
9
|
-
/** 1 ファイルの文書の種類。by_path、chaff.yaml
|
|
10
|
-
export const profileFor = (config: ProfileSettings, path: string, source: string, language: string): DocumentProfile | undefined =>
|
|
11
|
-
chooseProfile(loadProfiles(), {
|
|
10
|
+
/** 1 ファイルの文書の種類。by_path、chaff.yaml、ジャンル、内容の順に選ぶ。 */
|
|
11
|
+
export const profileFor = (config: ProfileSettings, path: string, source: string, language: string, genre?: string): DocumentProfile | undefined =>
|
|
12
|
+
chooseProfile(loadProfiles(), {
|
|
13
|
+
byPath: applyByPath(config.byPath, config.baseDir, path).profile,
|
|
14
|
+
config: config.profile,
|
|
15
|
+
genre: genre === undefined ? undefined : presetProfile(genre),
|
|
16
|
+
source,
|
|
17
|
+
language,
|
|
18
|
+
})?.profile;
|
package/src/profile/load.ts
CHANGED
|
@@ -12,7 +12,7 @@ const loaded: { value: readonly ProfileDefinition[] | undefined } = { value: und
|
|
|
12
12
|
export const loadProfiles = (): readonly ProfileDefinition[] => {
|
|
13
13
|
loaded.value ??= readdirSync(PROFILES_DIR)
|
|
14
14
|
.filter((file) => file.endsWith(".yaml"))
|
|
15
|
-
.
|
|
15
|
+
.toSorted((left, right) => left.localeCompare(right))
|
|
16
16
|
.flatMap((file) => {
|
|
17
17
|
const path = join(PROFILES_DIR, file);
|
|
18
18
|
try {
|
package/src/profile/select.ts
CHANGED
|
@@ -4,13 +4,15 @@ import { detected, type ProfileDefinition } from "./parse.ts";
|
|
|
4
4
|
/** 設定で書いた「使わない」。内容から選ぶのも止める。 */
|
|
5
5
|
export const NO_PROFILE = "none";
|
|
6
6
|
|
|
7
|
-
type ProfileSource = "by-path" | "config" | "content";
|
|
7
|
+
type ProfileSource = "by-path" | "config" | "genre" | "content";
|
|
8
8
|
|
|
9
9
|
export type ChosenProfile = { readonly profile: DocumentProfile; readonly from: ProfileSource };
|
|
10
10
|
|
|
11
11
|
export type ProfileRequest = {
|
|
12
12
|
readonly byPath: string | undefined;
|
|
13
13
|
readonly config: string | undefined;
|
|
14
|
+
/** The profile the file's genre reads with (genres.yaml), when it names one. */
|
|
15
|
+
readonly genre?: string | undefined;
|
|
14
16
|
readonly source: string;
|
|
15
17
|
readonly language: string;
|
|
16
18
|
};
|
|
@@ -18,16 +20,21 @@ export type ProfileRequest = {
|
|
|
18
20
|
const named = (definitions: readonly ProfileDefinition[], id: string, language: string): DocumentProfile | undefined =>
|
|
19
21
|
definitions.find((definition) => definition.id === id)?.languages[language];
|
|
20
22
|
|
|
23
|
+
const writtenIn = (request: ProfileRequest): ProfileSource => {
|
|
24
|
+
if (request.byPath !== undefined) return "by-path";
|
|
25
|
+
return request.config === undefined ? "genre" : "config";
|
|
26
|
+
};
|
|
27
|
+
|
|
21
28
|
/**
|
|
22
|
-
* 強いものから: by_path、chaff.yaml の profile、内容。どれかが none と書けばそこで止め、内容からも選ばない。
|
|
29
|
+
* 強いものから: by_path、chaff.yaml の profile、ジャンルの profile、内容。どれかが none と書けばそこで止め、内容からも選ばない。
|
|
23
30
|
* 名前で選んだ種類にその言語の中身が無ければ選ばない。黙って別の種類にしない。
|
|
24
31
|
*/
|
|
25
32
|
export const chooseProfile = (definitions: readonly ProfileDefinition[], request: ProfileRequest): ChosenProfile | undefined => {
|
|
26
|
-
const written = request.byPath ?? request.config;
|
|
33
|
+
const written = request.byPath ?? request.config ?? request.genre;
|
|
27
34
|
if (written === NO_PROFILE) return undefined;
|
|
28
35
|
if (written !== undefined) {
|
|
29
36
|
const profile = named(definitions, written, request.language);
|
|
30
|
-
return profile === undefined ? undefined : { profile, from: request
|
|
37
|
+
return profile === undefined ? undefined : { profile, from: writtenIn(request) };
|
|
31
38
|
}
|
|
32
39
|
const found = definitions.find((definition) => detected(definition, request.source, request.language));
|
|
33
40
|
const profile = found?.languages[request.language];
|
package/src/quoted-span.ts
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
|
-
import type { Span } from "./plugin.ts";
|
|
1
|
+
import type { Sentence, Span } from "./plugin.ts";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
|
|
3
|
+
/** 開きの字と、それを閉じる字。 */
|
|
4
|
+
type Marks = Readonly<Record<string, string>>;
|
|
5
|
+
|
|
6
|
+
/** 鉤括弧。日本語の引用と題名。 */
|
|
7
|
+
export const KAGI_MARKS: Marks = { "「": "」", "『": "』" };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 引用符も含めた、人の言葉を引く印。英語の "…" は開きと閉じが同じ字なので、開いている間に来たら閉じと読む。
|
|
11
|
+
* 一重の ’ は "don’t" の省略の印と見分けられないので入れない。
|
|
12
|
+
*/
|
|
13
|
+
export const QUOTATION_MARKS: Marks = { ...KAGI_MARKS, "“": "”", '"': '"' };
|
|
5
14
|
|
|
6
15
|
type Open = { readonly closer: string; readonly at: number };
|
|
7
16
|
type Scan = { readonly open: readonly Open[]; readonly spans: readonly Span[] };
|
|
8
17
|
|
|
9
|
-
const step =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
};
|
|
18
|
+
const step =
|
|
19
|
+
(marks: Marks) =>
|
|
20
|
+
(scan: Scan, char: string, at: number): Scan => {
|
|
21
|
+
const last = scan.open.at(-1);
|
|
22
|
+
if (last !== undefined && last.closer === char) return { open: scan.open.slice(0, -1), spans: [...scan.spans, { start: last.at + 1, end: at }] };
|
|
23
|
+
const closer = marks[char];
|
|
24
|
+
return closer === undefined ? scan : { open: [...scan.open, { closer, at }], spans: scan.spans };
|
|
25
|
+
};
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
|
-
*
|
|
28
|
+
* 引いたものの中身の位置(既定は鉤括弧「」『』)。括弧そのものは含まない。入れ子はそれぞれを返す。
|
|
19
29
|
* 閉じない括弧と、開きと組にならない閉じ括弧は中身を作らない。位置は text の先頭を 0 とする。
|
|
20
30
|
* 括弧は BMP の字なので、UTF-16 の 1 単位ずつ見れば位置がそのまま使える。
|
|
21
31
|
*/
|
|
22
|
-
export const quotedSpans = (text: string): Span[] => [...text.split("").reduce<Scan>(
|
|
32
|
+
export const quotedSpans = (text: string, marks: Marks = KAGI_MARKS): Span[] => [...text.split("").reduce<Scan>(step(marks), { open: [], spans: [] }).spans];
|
|
33
|
+
|
|
34
|
+
/** 文の中の、引いたものを括弧ごと、文書全体の座標で。 */
|
|
35
|
+
export const quotedIn = (sentence: Sentence, marks: Marks = KAGI_MARKS): Span[] =>
|
|
36
|
+
quotedSpans(sentence.text, marks).map((span) => ({ start: sentence.span.start + span.start - 1, end: sentence.span.start + span.end + 1 }));
|
|
23
37
|
|
|
24
38
|
/** inner が、spans のどれかにまるごと入っているか。 */
|
|
25
39
|
export const isWithinAny = (spans: readonly Span[], inner: Span): boolean => spans.some((span) => inner.start >= span.start && inner.end <= span.end);
|
package/src/reasons.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Texts } from "./ui.ts";
|
|
2
|
+
import { formFor } from "./render/plural.ts";
|
|
2
3
|
|
|
3
4
|
/** Why a rule did not run, in the document's language. Listed under the findings so "0 findings" is never a silent pass. */
|
|
4
5
|
export type Reasons = {
|
|
@@ -8,6 +9,7 @@ export type Reasons = {
|
|
|
8
9
|
readonly semantic: string;
|
|
9
10
|
readonly experimental: string;
|
|
10
11
|
readonly turnedOff: string;
|
|
12
|
+
readonly presetOff: (genre: string) => string;
|
|
11
13
|
readonly noStructure: (language: string) => string;
|
|
12
14
|
readonly unreadStructure: (clauses: number, units: number) => string;
|
|
13
15
|
readonly noDetector: (name: string) => string;
|
|
@@ -27,6 +29,7 @@ export const REASONS: Texts<Reasons> = {
|
|
|
27
29
|
semantic: "意味を読む検査のため(npx chaff test で動きます)",
|
|
28
30
|
experimental: "まだ試験中のため",
|
|
29
31
|
turnedOff: "設定で止めているため",
|
|
32
|
+
presetOff: (genre) => `ジャンル ${genre} では見ないため`,
|
|
30
33
|
noStructure: (language) => `${language} のパッケージは文書の構造を読めないため`,
|
|
31
34
|
unreadStructure: (clauses, units) =>
|
|
32
35
|
`条項の番号が本文に ${String(clauses)} 個あるのに、番号として読めたのは ${String(units)} 個のため(深い字下げや、行が本文につながった文書)`,
|
|
@@ -40,9 +43,10 @@ export const REASONS: Texts<Reasons> = {
|
|
|
40
43
|
semantic: "it reads meaning; npx chaff test runs it",
|
|
41
44
|
experimental: "still experimental",
|
|
42
45
|
turnedOff: "turned off in the settings",
|
|
46
|
+
presetOff: (genre) => `the ${genre} genre does not check it`,
|
|
43
47
|
noStructure: (language) => `the ${language} package cannot read a document's structure`,
|
|
44
48
|
unreadStructure: (clauses, units) =>
|
|
45
|
-
`the text has ${String(clauses)} clause numbers but only ${String(units)} were read as numbered lines (deep indents, or lines run into the text)`,
|
|
49
|
+
`the text has ${String(clauses)} clause numbers but only ${String(units)} ${formFor(units, "was read as a numbered line", "were read as numbered lines")} (deep indents, or lines run into the text)`,
|
|
46
50
|
noDetector: (name) => `no detector named ${name}`,
|
|
47
51
|
noLexicon: (language, list) => `the ${language} package has no word list ${list}`,
|
|
48
52
|
},
|
package/src/render/compact.ts
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { RuleDefinition } from "../plugin.ts";
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { messageOf } from "./text.ts";
|
|
4
|
+
import { counted } from "./plural.ts";
|
|
4
5
|
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
5
6
|
|
|
6
|
-
/** 「1 finding」「2 findings」。 */
|
|
7
|
-
const counted = (count: number, noun: string): string => `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
|
|
8
|
-
|
|
9
7
|
/**
|
|
10
8
|
* 最後の集計の行は、画面のほかの部分と同じく文書の言語で出す。行ごとの重さ(warning / error)は、grep や別の道具が読むので英語のまま。
|
|
11
9
|
*/
|
package/src/render/eval.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MIN_CORPUS, TARGET_HIT_RATE, type Point, type RuleReport } from "../eval.ts";
|
|
2
2
|
import type { Texts, UiLanguage } from "../ui.ts";
|
|
3
|
+
import { counted, formFor } from "./plural.ts";
|
|
3
4
|
|
|
4
5
|
const pct = (part: number, whole: number): string => `${((part / Math.max(1, whole)) * 100).toFixed(1)}%`;
|
|
5
6
|
|
|
@@ -51,19 +52,19 @@ const TEXT: Texts<{
|
|
|
51
52
|
recommended: " ← recommended",
|
|
52
53
|
both: " ← current / recommended",
|
|
53
54
|
point: (point, share) =>
|
|
54
|
-
`${String(point.documents).padStart(3)} ${point.documents
|
|
55
|
+
`${String(point.documents).padStart(3)} ${formFor(point.documents, "doc ", "docs")} (${share.padStart(6)}) ${String(point.findings).padStart(3)} ${formFor(point.findings, "finding ", "findings")} ${point.per10k.toFixed(1)} per 10,000 characters`,
|
|
55
56
|
fits: ` The current limit meets the target (under ${TARGET}%).`,
|
|
56
57
|
nothingFits: " No limit meets the target. The rule itself may not suit this corpus.",
|
|
57
58
|
over: (share) => ` At the current limit ${share} of the documents are hit, over the target (under ${TARGET}%).`,
|
|
58
59
|
recommend: (limit) => ` Recommended: ${limit}`,
|
|
59
60
|
toConfig: (rule, limit) => `${EN_CONFIG_LEAD}rules:\n${" ".repeat(EN_CONFIG_LEAD.length + YAML_INDENT)}${rule}: ${limit}`,
|
|
60
|
-
measured: (paths, rules) => ` Measured ${rules
|
|
61
|
+
measured: (paths, rules) => ` Measured ${counted(rules, "rule")} on ${counted(paths, "file")} as a corpus.`,
|
|
61
62
|
premise: [
|
|
62
63
|
" The documents here are taken as good documents that people wrote and published.",
|
|
63
64
|
` A rule that fires often on them may have a limit that does not fit real writing (target: under ${TARGET}%).`,
|
|
64
65
|
],
|
|
65
66
|
smallCorpus: (total) => [
|
|
66
|
-
` Note: the corpus has only ${total
|
|
67
|
+
` Note: the corpus has only ${counted(total, "document")}. Showing ${TARGET}% takes at least ${MIN_CORPUS}.`,
|
|
67
68
|
" For now each share is either 0 or all, so read the density on the right (findings per 10,000 characters).",
|
|
68
69
|
],
|
|
69
70
|
untouched: " No limit was changed. A different corpus gives a different answer.",
|
package/src/render/friendly.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Finding, RuleDefinition } from "../plugin.ts";
|
|
|
2
2
|
import type { RunResult } from "../run.ts";
|
|
3
3
|
import { MARK, localized, messageOf } from "./text.ts";
|
|
4
4
|
import { tally } from "./summary.ts";
|
|
5
|
+
import { counted } from "./plural.ts";
|
|
5
6
|
import { uiLanguageOf, type Texts } from "../ui.ts";
|
|
6
7
|
|
|
7
8
|
const RULE = 60;
|
|
@@ -13,6 +14,7 @@ const TEXT: Texts<{
|
|
|
13
14
|
readonly deterministic: string;
|
|
14
15
|
readonly untouched: string;
|
|
15
16
|
readonly forced: (n: number, ids: string) => string;
|
|
17
|
+
readonly presetOn: (n: number, ids: string) => string;
|
|
16
18
|
readonly notRun: (n: number) => string;
|
|
17
19
|
readonly because: (why: string) => string;
|
|
18
20
|
}> = {
|
|
@@ -23,6 +25,7 @@ const TEXT: Texts<{
|
|
|
23
25
|
deterministic: " (同じ文章なら何度実行しても同じ結果になります)",
|
|
24
26
|
untouched: "文章は書き換えていません。直すのは書いた人です。",
|
|
25
27
|
forced: (n, ids) => `試験中の rule を ${n} 件、設定により有効にしています: ${ids}`,
|
|
28
|
+
presetOn: (n, ids) => `試験中の rule を ${n} 件、ジャンルの既定で有効にしています: ${ids}`,
|
|
26
29
|
notRun: (n) => `${n} 件の rule は動いていません:`,
|
|
27
30
|
because: (why) => `(${why})`,
|
|
28
31
|
},
|
|
@@ -32,8 +35,9 @@ const TEXT: Texts<{
|
|
|
32
35
|
machine: "All judged by machine",
|
|
33
36
|
deterministic: " (the same text gives the same result every time)",
|
|
34
37
|
untouched: "The text was not changed. Fixing it is the writer's job.",
|
|
35
|
-
forced: (n, ids) => `${n
|
|
36
|
-
|
|
38
|
+
forced: (n, ids) => `${counted(n, "experimental rule")} turned on in the settings: ${ids}`,
|
|
39
|
+
presetOn: (n, ids) => `${counted(n, "experimental rule")} turned on by the genre: ${ids}`,
|
|
40
|
+
notRun: (n) => `${counted(n, "rule")} did not run:`,
|
|
37
41
|
because: (why) => ` (${why})`,
|
|
38
42
|
},
|
|
39
43
|
};
|
|
@@ -69,14 +73,21 @@ const block = (finding: Finding, rule: RuleDefinition, language: string): string
|
|
|
69
73
|
"",
|
|
70
74
|
];
|
|
71
75
|
|
|
72
|
-
|
|
76
|
+
/** notes: lines after the not-run list (the genre chaff suggests when none is set). */
|
|
77
|
+
export const renderFriendly = (
|
|
78
|
+
header: string,
|
|
79
|
+
result: RunResult,
|
|
80
|
+
rules: readonly RuleDefinition[],
|
|
81
|
+
language: string,
|
|
82
|
+
notes: readonly string[] = [],
|
|
83
|
+
): string => {
|
|
73
84
|
const byId = new Map(rules.map((rule) => [rule.id, rule]));
|
|
74
85
|
const blocks = result.findings.flatMap((finding) => {
|
|
75
86
|
const rule = byId.get(finding.rule);
|
|
76
87
|
return rule === undefined ? [] : block(finding, rule, language);
|
|
77
88
|
});
|
|
78
89
|
const text = TEXT[uiLanguageOf(language)];
|
|
79
|
-
const
|
|
90
|
+
const closing = [
|
|
80
91
|
"",
|
|
81
92
|
"─".repeat(RULE),
|
|
82
93
|
"",
|
|
@@ -85,10 +96,14 @@ export const renderFriendly = (header: string, result: RunResult, rules: readonl
|
|
|
85
96
|
"",
|
|
86
97
|
` ${text.untouched}`,
|
|
87
98
|
];
|
|
88
|
-
const forced =
|
|
99
|
+
const forced = [
|
|
100
|
+
...(result.forcedExperimental.length > 0 ? ["", ` ${text.forced(result.forcedExperimental.length, result.forcedExperimental.join(", "))}`] : []),
|
|
101
|
+
...(result.presetExperimental.length > 0 ? ["", ` ${text.presetOn(result.presetExperimental.length, result.presetExperimental.join(", "))}`] : []),
|
|
102
|
+
];
|
|
89
103
|
const skipped =
|
|
90
104
|
result.skipped.length > 0
|
|
91
105
|
? ["", ` ${text.notRun(result.skipped.length)}`, ...result.skipped.map((entry) => ` ${entry.rule}${text.because(entry.why)}`)]
|
|
92
106
|
: [];
|
|
93
|
-
|
|
107
|
+
const after = notes.flatMap((note) => ["", note]);
|
|
108
|
+
return ["", header, ...blocks, ...closing, ...forced, ...skipped, ...after, ""].join("\n");
|
|
94
109
|
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { GenreData } from "../genre-parse.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "../ui.ts";
|
|
3
|
+
|
|
4
|
+
const TEXT: Texts<{ readonly heading: string; readonly usage: readonly string[] }> = {
|
|
5
|
+
ja: {
|
|
6
|
+
heading: " ジャンル(文書の種類)を選ぶと、その種類の書き方に合わせて見ます:",
|
|
7
|
+
usage: [
|
|
8
|
+
" この実行だけ: npx chaffjs --genre legal/contract 契約書.md",
|
|
9
|
+
" この場所に決める: npx chaffjs init --genre legal/contract (chaff.yaml の genre に書きます)",
|
|
10
|
+
],
|
|
11
|
+
},
|
|
12
|
+
en: {
|
|
13
|
+
heading: " Pick the genre (the kind of document) and chaff checks it the way that kind is written:",
|
|
14
|
+
usage: [
|
|
15
|
+
" For one run: npx chaffjs --genre legal/contract contract.md",
|
|
16
|
+
" For this folder: npx chaffjs init --genre legal/contract (writes genre in chaff.yaml)",
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** `chaff genres`: every genre under its group, with what it is for, in the output language. */
|
|
22
|
+
export const renderGenres = (data: GenreData, ui: UiLanguage): string => {
|
|
23
|
+
const width = Math.max(...data.genres.map((genre) => genre.id.length)) + 2;
|
|
24
|
+
const groups = data.groups.flatMap((group) => {
|
|
25
|
+
const members = data.genres.filter((genre) => genre.id.startsWith(`${group.id}/`));
|
|
26
|
+
return members.length === 0
|
|
27
|
+
? []
|
|
28
|
+
: ["", ` ${group.name[ui] ?? group.id}`, ...members.map((genre) => ` ${genre.id.padEnd(width)}${genre.summary[ui] ?? ""}`)];
|
|
29
|
+
});
|
|
30
|
+
return ["", TEXT[ui].heading, ...groups, "", ...TEXT[ui].usage, ""].join("\n");
|
|
31
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** English agreement: only exactly one takes the singular ("0 findings", "1 finding", "1.5 findings"). */
|
|
2
|
+
const isOne = (count: number | string): boolean => String(count) === "1";
|
|
3
|
+
|
|
4
|
+
export const formFor = (count: number | string, singular: string, plural: string): string => (isOne(count) ? singular : plural);
|
|
5
|
+
|
|
6
|
+
export const counted = (count: number, singular: string, plural = `${singular}s`): string => `${String(count)} ${formFor(count, singular, plural)}`;
|