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
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { digitRunAround } from "./number-name.ts";
|
|
2
|
+
import type { Span, Token } from "./plugin.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 日付・時刻として書かれた数(9月、3時、2026年、10月1日、10時5分)。数を詰めて書くのが決まりで、空け方の好みではない。
|
|
6
|
+
* 長さ(3ヶ月、3時間、3日間)は別の語で書くので、ここには入らない。
|
|
7
|
+
*
|
|
8
|
+
* 単位は語彙表から渡す。chained は日付・時刻の単位を大きい順に(年月日時分秒)。大きい単位の後ろに小さい単位を続けて書けば
|
|
9
|
+
* 一つの日付・時刻になる。「2008年 2年連続」のように同じ単位や大きい単位が続けば、別のものの始まり。
|
|
10
|
+
* positional は数の後ろに置くだけで位置を言う単位(月、時)。year は 4 桁の数の後ろで暦の年を言う単位(年、年度)。
|
|
11
|
+
* 「5日」「5分」「3年」のように、単位だけでは長さと見分けのつかないものは、位置を言う単位と続けて書いたときだけ日付・時刻と読む。
|
|
12
|
+
*/
|
|
13
|
+
export type CalendarUnits = { readonly chained: readonly string[]; readonly positional: ReadonlySet<string>; readonly year: ReadonlySet<string> };
|
|
14
|
+
|
|
15
|
+
/** 日付・時刻の数の並び。first は日付の頭の数、inner は前の単位に続く数(2026年9月の 9)。 */
|
|
16
|
+
export type CalendarRun = { readonly run: Span; readonly place: "first" | "inner" };
|
|
17
|
+
|
|
18
|
+
/** 数の並びと、その後ろの単位の終わり(文の中の位置)。rank は chained の中の単位の順(無ければ -1)、anchored は単位だけで日付・時刻と分かるもの。 */
|
|
19
|
+
type Part = { readonly run: Span; readonly unitEnd: number; readonly rank: number; readonly anchored: boolean };
|
|
20
|
+
|
|
21
|
+
const DIGIT = /\d/u;
|
|
22
|
+
const CALENDAR_YEAR = /^\d{4}$/u;
|
|
23
|
+
|
|
24
|
+
/** text の中の数の並び(数字・小数点・ハイフン)を左から。 */
|
|
25
|
+
const digitRuns = (text: string): Span[] => {
|
|
26
|
+
const runs: Span[] = [];
|
|
27
|
+
let at = 0;
|
|
28
|
+
while (at < text.length) {
|
|
29
|
+
const run = DIGIT.test(text[at] ?? "") ? digitRunAround(text, at) : undefined;
|
|
30
|
+
if (run !== undefined) runs.push(run);
|
|
31
|
+
at = Math.max(run?.end ?? 0, at + 1);
|
|
32
|
+
}
|
|
33
|
+
return runs;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** 空白 1 つを越えた位置。 */
|
|
37
|
+
const pastSpace = (text: string, at: number): number => (text[at] === " " ? at + 1 : at);
|
|
38
|
+
|
|
39
|
+
const isUnit = (surface: string, units: CalendarUnits): boolean => units.chained.includes(surface) || units.positional.has(surface) || units.year.has(surface);
|
|
40
|
+
|
|
41
|
+
const isAnchor = (digits: string, surface: string, units: CalendarUnits): boolean =>
|
|
42
|
+
units.positional.has(surface) || (units.year.has(surface) && CALENDAR_YEAR.test(digits));
|
|
43
|
+
|
|
44
|
+
/** 数のすぐ後ろ(空白 1 つまで)の語が日付・時刻の単位なら、その組。語の切れ目が合わなければ組にしない。 */
|
|
45
|
+
const partOf = (text: string, run: Span, tokens: readonly Token[], base: number, units: CalendarUnits): Part | undefined => {
|
|
46
|
+
const unit = tokens.find((token) => token.span.start === base + pastSpace(text, run.end));
|
|
47
|
+
if (unit === undefined || !isUnit(unit.surface, units)) return undefined;
|
|
48
|
+
const rank = units.chained.indexOf(unit.surface);
|
|
49
|
+
return { run, unitEnd: unit.span.end - base, rank, anchored: isAnchor(text.slice(run.start, run.end), unit.surface, units) };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 前の組の単位のすぐ後ろ(空白 1 つまで)から始まり、単位が前より小さい組か。 */
|
|
53
|
+
const continues = (text: string, previous: Part | undefined, part: Part): boolean =>
|
|
54
|
+
previous !== undefined && previous.rank >= 0 && part.rank > previous.rank && part.run.start === pastSpace(text, previous.unitEnd);
|
|
55
|
+
|
|
56
|
+
/** 前の組に続く組を、同じ日付・時刻にまとめる。 */
|
|
57
|
+
const chainsOf = (text: string, parts: readonly Part[]): Part[][] =>
|
|
58
|
+
parts.reduce<Part[][]>((chains, part) => {
|
|
59
|
+
const chain = chains.at(-1);
|
|
60
|
+
if (chain !== undefined && continues(text, chain.at(-1), part)) chain.push(part);
|
|
61
|
+
else chains.push([part]);
|
|
62
|
+
return chains;
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 文 text の中の、日付・時刻の数の並び。tokens は文書全体の座標で、base は text の先頭の位置。
|
|
67
|
+
* 品詞が無ければ判断せず、何も返さない。
|
|
68
|
+
*/
|
|
69
|
+
export const calendarRuns = (text: string, tokens: readonly Token[] | undefined, base: number, units: CalendarUnits): CalendarRun[] => {
|
|
70
|
+
if (tokens === undefined) return [];
|
|
71
|
+
const parts = digitRuns(text).flatMap((run) => partOf(text, run, tokens, base, units) ?? []);
|
|
72
|
+
return chainsOf(text, parts)
|
|
73
|
+
.filter((chain) => chain.some((part) => part.anchored))
|
|
74
|
+
.flatMap((chain) => chain.map((part, index): CalendarRun => ({ run: part.run, place: index === 0 ? "first" : "inner" })));
|
|
75
|
+
};
|
package/src/cli-args.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Options of the commands that take files to check, whose value is the next argument. That value is not a file.
|
|
3
3
|
* `--why` (relax), `--format` and `--language` (tree) belong to commands that read their own arguments.
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
const VALUE_FLAGS: ReadonlySet<string> = new Set(["--sarif", "--genre", "--rule", "--line"]);
|
|
6
6
|
|
|
7
7
|
/** The arguments that name what to check: everything that is neither an option nor an option's value. */
|
|
8
8
|
export const targetsOf = (args: readonly string[]): string[] =>
|
|
9
|
-
args.filter((arg, index) => !arg.startsWith("--") && !(index > 0 && VALUE_FLAGS.
|
|
9
|
+
args.filter((arg, index) => !arg.startsWith("--") && !(index > 0 && VALUE_FLAGS.has(args[index - 1] ?? "")));
|
package/src/cli-text.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// What the command line says, in Japanese and English. Per-file text follows the file's language; the rest
|
|
2
2
|
// follows hostLanguage (chaff.yaml's language, else the terminal's locale).
|
|
3
3
|
import type { Texts } from "./ui.ts";
|
|
4
|
+
import { counted, formFor } from "./render/plural.ts";
|
|
4
5
|
import type { GenreSetting } from "./genre-check.ts";
|
|
6
|
+
import type { ProfileSetting } from "./profile/check.ts";
|
|
5
7
|
|
|
6
8
|
/** Where a file's genre came from, as the header names it. */
|
|
7
9
|
export type GenreSource = "--genre" | "by_path" | "config" | "default" | "front-matter" | "path" | "content";
|
|
@@ -12,11 +14,11 @@ const USAGE_JA = `chaff — 文章の読みにくいところを見つけます
|
|
|
12
14
|
chaff . この場所の Markdown を全部
|
|
13
15
|
chaff test <file|dir>... 意味を読む検査も動かす(API key が要ります)
|
|
14
16
|
判定役は chaff.yaml の ai_backend で選びます
|
|
15
|
-
chaff init chaff.yaml
|
|
17
|
+
chaff init chaff.yaml を作る(端末ならジャンルを尋ねる。--genre <ジャンル> でも選べる)
|
|
16
18
|
chaff --version chaffjs と言語パッケージの版
|
|
17
19
|
chaff eval <dir> 手元の文書で閾値を測り直す
|
|
18
20
|
chaff explain <rule> そのルールの意図と根拠を読む
|
|
19
|
-
chaff genres
|
|
21
|
+
chaff genres ジャンル(文書の種類)の一覧と、それぞれ何向けか
|
|
20
22
|
chaff tree <file> [--format sexp|json] 文書を番地の付いた木にする(条・項・定義・参照)
|
|
21
23
|
chaff cite <原文> <引用.json> 回答の引用(番地と引用文)が原文にあるかを確かめる
|
|
22
24
|
chaff rules --json いまの設定を JSON で出す(AI に渡す用)
|
|
@@ -46,11 +48,11 @@ const USAGE_EN = `chaff — finds what makes writing hard to read. It never rewr
|
|
|
46
48
|
chaff . every Markdown file here
|
|
47
49
|
chaff test <file|dir>... also run the checks that read meaning (needs an API key)
|
|
48
50
|
the judge is chosen by ai_backend in chaff.yaml
|
|
49
|
-
chaff init create chaff.yaml
|
|
51
|
+
chaff init create chaff.yaml (asks for the genre at a terminal; --genre <genre> chooses it)
|
|
50
52
|
chaff --version the version of chaffjs and its language packages
|
|
51
53
|
chaff eval <dir> re-measure the limits on your own documents
|
|
52
54
|
chaff explain <rule> read what a rule is for and why
|
|
53
|
-
chaff genres list the genres
|
|
55
|
+
chaff genres list the genres (kinds of document) and what each is for
|
|
54
56
|
chaff tree <file> [--format sexp|json] the document as a tree of addresses (sections, clauses, definitions, references)
|
|
55
57
|
chaff cite <source> <quotes.json> check that quoted passages (address and text) are in the source
|
|
56
58
|
chaff rules --json the current settings as JSON (to give to an AI)
|
|
@@ -79,22 +81,25 @@ export type CliText = {
|
|
|
79
81
|
readonly languageName: (language: string) => string;
|
|
80
82
|
readonly genreSource: Readonly<Record<GenreSource, string>>;
|
|
81
83
|
readonly header: (path: string, genre: string, language: string, from: string, shelved: number, hushed: number) => string;
|
|
84
|
+
readonly suggested: (name: string, genre: string) => string;
|
|
85
|
+
readonly suggestedNote: (name: string, genre: string, used: string) => string;
|
|
82
86
|
readonly noMarkdown: (targets: string) => string;
|
|
83
87
|
readonly noMarkdownHere: string;
|
|
84
88
|
readonly noAdapter: (language: string) => string;
|
|
85
89
|
readonly unknownGenre: (genre: string, where: string, known: readonly string[]) => string;
|
|
86
90
|
readonly genreWhere: (where: GenreSetting, files: readonly string[]) => string;
|
|
87
91
|
readonly unreadFrontMatterGenre: (path: string, genre: string, known: readonly string[]) => string;
|
|
92
|
+
readonly unknownProfile: (profile: string, where: string, known: readonly string[]) => string;
|
|
93
|
+
readonly profileWhere: (where: ProfileSetting, files: readonly string[]) => string;
|
|
88
94
|
readonly unknownRule: (id: string) => string;
|
|
89
95
|
readonly unknownRuleWithList: (id: string, list: string) => string;
|
|
90
96
|
readonly unnamed: string;
|
|
91
97
|
readonly sarifWritten: (path: string, count: number) => string;
|
|
92
98
|
readonly unchanged: string;
|
|
93
|
-
readonly findingsUnit: string;
|
|
99
|
+
readonly findingsUnit: (count: number) => string;
|
|
94
100
|
readonly watching: (files: number, findings: number) => string;
|
|
95
101
|
readonly watchHint: string;
|
|
96
102
|
readonly baselineDone: (files: number, entries: number, file: string) => readonly string[];
|
|
97
|
-
readonly genres: (list: readonly string[]) => string;
|
|
98
103
|
readonly unit: (ruleId: string, language: string) => string;
|
|
99
104
|
};
|
|
100
105
|
|
|
@@ -115,6 +120,9 @@ export const CLI_TEXT: Texts<CliText> = {
|
|
|
115
120
|
[`${path} ${genre} · ${language} ジャンルは${from}から`, shelved > 0 ? ` 棚上げ ${shelved} 件` : "", hushed > 0 ? ` stet ${hushed} 件` : ""].join(
|
|
116
121
|
"",
|
|
117
122
|
),
|
|
123
|
+
suggested: (name, genre) => ` ${name}のようです。--genre ${genre} を試せます`,
|
|
124
|
+
suggestedNote: (name, genre, used) =>
|
|
125
|
+
` ジャンルを決めていないので、${used} として見ました。${name}なら、--genre ${genre} でその種類の書き方に合わせて見ます(npx chaffjs genres で一覧)。`,
|
|
118
126
|
noMarkdown: (targets) => `Markdown が 1 つも見つかりませんでした: ${targets}`,
|
|
119
127
|
noMarkdownHere: "Markdown が 1 つも見つかりませんでした。",
|
|
120
128
|
noAdapter: (language) => `言語 "${language}" のアダプタがありません。`,
|
|
@@ -126,12 +134,15 @@ export const CLI_TEXT: Texts<CliText> = {
|
|
|
126
134
|
},
|
|
127
135
|
unreadFrontMatterGenre: (path, genre, known) =>
|
|
128
136
|
`${path}: front matter の genre "${genre}" はジャンルではないので読みませんでした。使えるのは ${known.join("、")} です。`,
|
|
137
|
+
unknownProfile: (profile, where, known) =>
|
|
138
|
+
`文書の種類 "${profile}" はありません(${where})。使えるのは ${known.join("、")} と、種類を使わない none です。`,
|
|
139
|
+
profileWhere: (where, files) => (where === "config" ? "chaff.yaml の profile" : `chaff.yaml の by_path、files: ${files.join(", ")}`),
|
|
129
140
|
unknownRule: (id) => `${id} というルールはありません。npx chaff rules --json で一覧が出ます。`,
|
|
130
141
|
unknownRuleWithList: (id, list) => `${id} というルールはありません。\n一覧:\n${list}`,
|
|
131
142
|
unnamed: "(名前なし)",
|
|
132
143
|
sarifWritten: (path, count) => `\n SARIF を書きました: ${path}(${count} 件)`,
|
|
133
144
|
unchanged: "変わりませんでした",
|
|
134
|
-
findingsUnit: " 件",
|
|
145
|
+
findingsUnit: () => " 件",
|
|
135
146
|
watching: (files, findings) => `\n ${files} ファイルを見ています。いまの指摘は ${findings} 件です。`,
|
|
136
147
|
watchHint: " 保存するたびに、変わったところだけ出します。止めるには Ctrl-C。\n",
|
|
137
148
|
baselineDone: (files, entries, file) => [
|
|
@@ -144,8 +155,6 @@ export const CLI_TEXT: Texts<CliText> = {
|
|
|
144
155
|
` ${file} を commit してください。`,
|
|
145
156
|
"",
|
|
146
157
|
],
|
|
147
|
-
genres: (list) =>
|
|
148
|
-
["", " 使えるジャンル:", ...list.map((genre) => ` ${genre}`), "", " chaff.yaml の genre に書くか、--genre で指定します。", ""].join("\n"),
|
|
149
158
|
unit: (ruleId, language) => {
|
|
150
159
|
if (ruleId === "bold-density") return "1000 字あたりの箇所数";
|
|
151
160
|
if (ruleId !== "max-sentence-length") return "回";
|
|
@@ -166,6 +175,9 @@ export const CLI_TEXT: Texts<CliText> = {
|
|
|
166
175
|
},
|
|
167
176
|
header: (path, genre, language, from, shelved, hushed) =>
|
|
168
177
|
[`${path} ${genre} · ${language} genre from ${from}`, shelved > 0 ? ` ${shelved} shelved` : "", hushed > 0 ? ` ${hushed} stet` : ""].join(""),
|
|
178
|
+
suggested: (name, genre) => ` Looks like: ${name}. Try --genre ${genre}`,
|
|
179
|
+
suggestedNote: (name, genre, used) =>
|
|
180
|
+
` No genre was set, so this was checked as ${used}. If it is ${name}, --genre ${genre} checks it the way that kind is written (npx chaffjs genres lists them).`,
|
|
169
181
|
noMarkdown: (targets) => `No Markdown files found: ${targets}`,
|
|
170
182
|
noMarkdownHere: "No Markdown files found.",
|
|
171
183
|
noAdapter: (language) => `No language package for "${language}".`,
|
|
@@ -176,25 +188,27 @@ export const CLI_TEXT: Texts<CliText> = {
|
|
|
176
188
|
},
|
|
177
189
|
unreadFrontMatterGenre: (path, genre, known) =>
|
|
178
190
|
`${path}: the front matter's genre "${genre}" is not a genre, so it was not used. The genres are ${known.join(", ")}.`,
|
|
191
|
+
unknownProfile: (profile, where, known) =>
|
|
192
|
+
`There is no document profile "${profile}" (${where}). The profiles are ${known.join(", ")}; none uses no profile.`,
|
|
193
|
+
profileWhere: (where, files) => (where === "config" ? "profile in chaff.yaml" : `by_path in chaff.yaml, files: ${files.join(", ")}`),
|
|
179
194
|
unknownRule: (id) => `There is no rule named ${id}. npx chaff rules --json lists them.`,
|
|
180
195
|
unknownRuleWithList: (id, list) => `There is no rule named ${id}.\nRules:\n${list}`,
|
|
181
196
|
unnamed: "(no name)",
|
|
182
|
-
sarifWritten: (path, count) => `\n Wrote SARIF: ${path} (${count
|
|
197
|
+
sarifWritten: (path, count) => `\n Wrote SARIF: ${path} (${counted(count, "finding")})`,
|
|
183
198
|
unchanged: "no change",
|
|
184
|
-
findingsUnit: " findings"
|
|
185
|
-
watching: (files, findings) => `\n Watching ${files
|
|
199
|
+
findingsUnit: (count) => ` ${formFor(count, "finding", "findings")}`,
|
|
200
|
+
watching: (files, findings) => `\n Watching ${counted(files, "file")}. ${counted(findings, "finding")} now.`,
|
|
186
201
|
watchHint: " Each save prints only what changed. Ctrl-C to stop.\n",
|
|
187
202
|
baselineDone: (files, entries, file) => [
|
|
188
203
|
"",
|
|
189
|
-
` Checked ${files
|
|
204
|
+
` Checked ${counted(files, "file")}.`,
|
|
190
205
|
"",
|
|
191
|
-
` Recorded ${entries
|
|
206
|
+
` Recorded ${counted(entries, "finding")} in ${file}.`,
|
|
192
207
|
" They will not be reported again; only new ones will.",
|
|
193
208
|
"",
|
|
194
209
|
` Commit ${file}.`,
|
|
195
210
|
"",
|
|
196
211
|
],
|
|
197
|
-
genres: (list) => ["", " Genres:", ...list.map((genre) => ` ${genre}`), "", " Set one with genre in chaff.yaml, or with --genre.", ""].join("\n"),
|
|
198
212
|
unit: (ruleId, language) => {
|
|
199
213
|
if (ruleId === "bold-density") return "places per 1000 characters";
|
|
200
214
|
if (ruleId !== "max-sentence-length") return "times";
|
package/src/cli.ts
CHANGED
|
@@ -16,14 +16,17 @@ import { clock, describeChange, snapshotOf, watchPaths, type Snapshot } from "./
|
|
|
16
16
|
import { runEval } from "./commands/eval.ts";
|
|
17
17
|
import { runTest } from "./commands/test.ts";
|
|
18
18
|
import { GENRES } from "./genre.ts";
|
|
19
|
-
import { unknownGenres, writtenGenres } from "./genre-check.ts";
|
|
20
19
|
import { resolveGenre } from "./resolve-genre.ts";
|
|
21
20
|
import { runInit } from "./init.ts";
|
|
21
|
+
import { initGenre } from "./commands/init-ask.ts";
|
|
22
22
|
import { targetsOf } from "./cli-args.ts";
|
|
23
23
|
import { loadRules } from "./rule-load.ts";
|
|
24
24
|
import { renderCompact } from "./render/compact.ts";
|
|
25
25
|
import { renderExplain } from "./render/explain.ts";
|
|
26
26
|
import { renderFriendly } from "./render/friendly.ts";
|
|
27
|
+
import { renderGenres } from "./render/genres.ts";
|
|
28
|
+
import { loadGenres, presetLevels } from "./genre-load.ts";
|
|
29
|
+
import { fileHeader } from "./file-header.ts";
|
|
27
30
|
import { rulesJson } from "./render/rules-json.ts";
|
|
28
31
|
import { renderSarif } from "./render/sarif.ts";
|
|
29
32
|
import { VERSION, VERSION_LINES } from "./version.ts";
|
|
@@ -33,26 +36,20 @@ import { runSkill } from "./commands/skill.ts";
|
|
|
33
36
|
import { runFeedback, settingsOf } from "./commands/feedback.ts";
|
|
34
37
|
import { homedir } from "node:os";
|
|
35
38
|
import { ruleProblems } from "./config/rule-problems.ts";
|
|
39
|
+
import { nameProblems } from "./config/name-problems.ts";
|
|
36
40
|
import { renderSummary, type FileOutcome } from "./render/summary.ts";
|
|
37
41
|
import { neededBy, runRules } from "./run.ts";
|
|
38
42
|
import type { Level, RuleDefinition } from "./plugin.ts";
|
|
39
43
|
import { CLI_TEXT, type CliText, type GenreSource } from "./cli-text.ts";
|
|
40
|
-
import { hostLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
|
|
44
|
+
import { hostLanguage, sharedLanguage, uiLanguageOf, type UiLanguage } from "./ui.ts";
|
|
41
45
|
import { profileFor } from "./profile/for-file.ts";
|
|
46
|
+
import { settingProblems } from "./setting-problems.ts";
|
|
42
47
|
|
|
43
48
|
/** Text for output that is not about one document. */
|
|
44
49
|
const hostText = (config: Config): CliText => CLI_TEXT[hostLanguage(config.language, process.env)];
|
|
45
50
|
|
|
46
51
|
const readConfig = (): Config => (existsSync(join(process.cwd(), CONFIG_FILE)) ? loadConfig(join(process.cwd(), CONFIG_FILE)) : EMPTY);
|
|
47
52
|
|
|
48
|
-
const genreProblems = (command: string, argv: readonly string[]): string[] => {
|
|
49
|
-
const config = readConfig();
|
|
50
|
-
const text = hostText(config);
|
|
51
|
-
return unknownGenres(writtenGenres(command, flag(argv, "--genre"), config), GENRES).map((entry) =>
|
|
52
|
-
text.unknownGenre(entry.genre, text.genreWhere(entry.where, entry.files), GENRES),
|
|
53
|
-
);
|
|
54
|
-
};
|
|
55
|
-
|
|
56
53
|
/** resolveGenre with this run's --genre, for the commands that take it as a dependency. */
|
|
57
54
|
const genreFrom =
|
|
58
55
|
(argv: readonly string[]) =>
|
|
@@ -90,12 +87,6 @@ type Inspected = {
|
|
|
90
87
|
readonly all: readonly string[];
|
|
91
88
|
};
|
|
92
89
|
|
|
93
|
-
/** The first line for a file, in the file's language. */
|
|
94
|
-
const headerFor = (path: string, genre: string, from: GenreSource, language: string, shelved: number, hushed: number): string => {
|
|
95
|
-
const text = CLI_TEXT[uiLanguageOf(language)];
|
|
96
|
-
return text.header(path, genre, text.languageName(language), text.genreSource[from], shelved, hushed);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
90
|
const inspect = async (path: string, config: Config, argv: readonly string[]): Promise<Inspected> => {
|
|
100
91
|
const source = plainSource(await readFile(path, "utf8"));
|
|
101
92
|
const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
|
|
@@ -105,7 +96,7 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
|
|
|
105
96
|
const rules = loadRules(language);
|
|
106
97
|
const experimental = config.experimental || argv.includes("--experimental");
|
|
107
98
|
await adapter.prepare?.(neededBy(rules, config.rules, experimental, genre, language));
|
|
108
|
-
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
|
|
99
|
+
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
|
|
109
100
|
const raw = runRules(doc, rules, config.rules, experimental, genre, config.limits);
|
|
110
101
|
// 応答は 3 つ。stet で黙らせたものは、ここで落とす。
|
|
111
102
|
const applied = applySuppressions(
|
|
@@ -116,8 +107,8 @@ const inspect = async (path: string, config: Config, argv: readonly string[]): P
|
|
|
116
107
|
const baseline = argv.includes("--show-baseline") ? undefined : readBaseline(join(process.cwd(), BASELINE_FILE));
|
|
117
108
|
const split = splitByBaseline(path, applied.kept, baseline);
|
|
118
109
|
const result = { ...raw, findings: split.fresh };
|
|
119
|
-
const header =
|
|
120
|
-
const text = argv.includes("--compact") ? renderCompact(header, result, rules, language) : renderFriendly(header, result, rules, language);
|
|
110
|
+
const { header, notes } = fileHeader(path, source, language, { genre, from, unread }, { shelved: split.shelved, hushed: applied.suppressed.length });
|
|
111
|
+
const text = argv.includes("--compact") ? renderCompact(header, result, rules, language) : renderFriendly(header, result, rules, language, notes);
|
|
121
112
|
return {
|
|
122
113
|
text,
|
|
123
114
|
rules,
|
|
@@ -147,14 +138,14 @@ const writeSarif = (results: readonly Inspected[], argv: readonly string[]): voi
|
|
|
147
138
|
|
|
148
139
|
/** 効いていない設定は、結果の前に一度だけ言う。標準エラーに出すので、JSON や SARIF の出力は汚さない。 */
|
|
149
140
|
const warnRuleProblems = (config: Config, language: string): void => {
|
|
150
|
-
|
|
141
|
+
const ui = hostLanguage(config.language, process.env);
|
|
142
|
+
[...ruleProblems(config, loadRules(language), ui), ...nameProblems(config, ui)].forEach((problem) => console.error(`chaff: ${problem}`));
|
|
151
143
|
};
|
|
152
144
|
|
|
153
145
|
/** Several files end with one summary: in their language when they share one, else the host's. */
|
|
154
146
|
const summaryLanguage = (results: readonly Inspected[], config: Config): UiLanguage => {
|
|
155
|
-
const languages =
|
|
156
|
-
|
|
157
|
-
return languages.size === 1 && only !== undefined ? only : hostLanguage(config.language, process.env);
|
|
147
|
+
const languages = results.map((result) => result.language);
|
|
148
|
+
return sharedLanguage(languages, hostLanguage(config.language, process.env));
|
|
158
149
|
};
|
|
159
150
|
|
|
160
151
|
const lint = async (targets: readonly string[], argv: readonly string[]): Promise<number> => {
|
|
@@ -216,8 +207,10 @@ const runWatch = async (targets: readonly string[], argv: readonly string[]): Pr
|
|
|
216
207
|
return new Promise(() => undefined);
|
|
217
208
|
};
|
|
218
209
|
|
|
219
|
-
|
|
210
|
+
/** genreFlag: --genre, which wins over chaff.yaml's genre here as it does in a run. */
|
|
211
|
+
const explain = (ruleId: string | undefined, genreFlag: string | undefined): number => {
|
|
220
212
|
const config = readConfig();
|
|
213
|
+
const genre = genreFlag ?? config.genre;
|
|
221
214
|
// The rule's limits differ by language (characters for Japanese, words for English): explain in the one being written.
|
|
222
215
|
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
223
216
|
const text = CLI_TEXT[uiLanguageOf(language)];
|
|
@@ -228,8 +221,9 @@ const explain = (ruleId: string | undefined): number => {
|
|
|
228
221
|
console.error(text.unknownRuleWithList(ruleId ?? text.unnamed, list));
|
|
229
222
|
return 1;
|
|
230
223
|
}
|
|
231
|
-
const
|
|
232
|
-
|
|
224
|
+
const preset = genre === undefined ? {} : presetLevels(genre);
|
|
225
|
+
const current = config.rules[rule.id] ?? preset[rule.id] ?? (rule.status === "experimental" && !config.experimental ? "off" : "normal");
|
|
226
|
+
console.log(renderExplain(rule, current, language, text.unit(rule.id, language), genre));
|
|
233
227
|
return 0;
|
|
234
228
|
};
|
|
235
229
|
|
|
@@ -266,16 +260,16 @@ type Handler = (argv: readonly string[]) => number | Promise<number>;
|
|
|
266
260
|
/** `--` で始まらない引数。対象のパス。 */
|
|
267
261
|
const positional = (argv: readonly string[]): string[] => targetsOf(argv.slice(1));
|
|
268
262
|
|
|
269
|
-
const showRules = (): number => {
|
|
263
|
+
const showRules = (genreFlag: string | undefined): number => {
|
|
270
264
|
const config = readConfig();
|
|
271
265
|
const language = config.language ?? hostLanguage(undefined, process.env);
|
|
272
266
|
warnRuleProblems(config, language);
|
|
273
|
-
console.log(rulesJson(loadRules(language), config, language, config.genre ?? "blog/tech"));
|
|
267
|
+
console.log(rulesJson(loadRules(language), config, language, genreFlag ?? config.genre ?? "blog/tech"));
|
|
274
268
|
return 0;
|
|
275
269
|
};
|
|
276
270
|
|
|
277
271
|
const showGenres = (): number => {
|
|
278
|
-
console.log(
|
|
272
|
+
console.log(renderGenres(loadGenres(), hostLanguage(readConfig().language, process.env)));
|
|
279
273
|
return 0;
|
|
280
274
|
};
|
|
281
275
|
|
|
@@ -284,22 +278,28 @@ const treeContext = (): TreeContext => {
|
|
|
284
278
|
return { config, flag, ui: hostLanguage(config.language, process.env) };
|
|
285
279
|
};
|
|
286
280
|
|
|
281
|
+
/** What eval and test share: the settings, this run's genre, and the language for what is not about one document. */
|
|
282
|
+
const measureContext = (argv: readonly string[]): { config: Config; resolveGenre: ReturnType<typeof genreFrom>; ui: UiLanguage } => {
|
|
283
|
+
const config = readConfig();
|
|
284
|
+
return { config, resolveGenre: genreFrom(argv), ui: hostLanguage(config.language, process.env) };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
287
|
/** 分岐を数珠つなぎにせず表にする。足すときに main を太らせない。 */
|
|
288
288
|
const HANDLERS: Readonly<Record<string, Handler>> = {
|
|
289
|
-
init: (argv) => {
|
|
290
|
-
|
|
291
|
-
|
|
289
|
+
init: async (argv) => {
|
|
290
|
+
const ui = hostLanguage(readConfig().language, process.env);
|
|
291
|
+
const chosen = await initGenre(flag(argv, "--genre"), ui, process.cwd());
|
|
292
|
+
if ("error" in chosen) console.error(chosen.error);
|
|
293
|
+
else runInit(process.cwd(), chosen.genre, ui).forEach((line) => console.log(line));
|
|
294
|
+
return "error" in chosen ? 1 : 0;
|
|
292
295
|
},
|
|
293
296
|
genres: showGenres,
|
|
294
|
-
rules: showRules,
|
|
295
|
-
explain: (argv) => explain(argv[1]),
|
|
296
|
-
eval: (argv) => {
|
|
297
|
-
const config = readConfig();
|
|
298
|
-
return runEval(positional(argv), argv, { config, resolveGenre: genreFrom(argv), flag, ui: hostLanguage(config.language, process.env) });
|
|
299
|
-
},
|
|
297
|
+
rules: (argv) => showRules(flag(argv, "--genre")),
|
|
298
|
+
explain: (argv) => explain(argv[1], flag(argv, "--genre")),
|
|
299
|
+
eval: (argv) => runEval(positional(argv), argv, { ...measureContext(argv), flag }),
|
|
300
300
|
tree: (argv) => runTree(treeTargets(argv), argv, treeContext()),
|
|
301
301
|
cite: (argv) => runCite(citeTargets(argv), argv, treeContext()),
|
|
302
|
-
test: (argv) => runTest(positional(argv), argv, {
|
|
302
|
+
test: (argv) => runTest(positional(argv), argv, { ...measureContext(argv), inspect }),
|
|
303
303
|
baseline: (argv) => runBaseline(positional(argv), argv),
|
|
304
304
|
suppressions: (argv) => runSuppressions(positional(argv), argv),
|
|
305
305
|
relax: (argv) => changeSetting("relaxed", argv[1], flag(argv, "--why")),
|
|
@@ -336,8 +336,9 @@ export const main = async (argv: readonly string[]): Promise<number> => {
|
|
|
336
336
|
console.log(VERSION_LINES.join("\n"));
|
|
337
337
|
return 0;
|
|
338
338
|
}
|
|
339
|
-
//
|
|
340
|
-
const
|
|
339
|
+
// 知らないジャンルではどの rule も当たらず、知らない文書の種類では種類の知識が外れる。どちらも素通りに見えるので、何かする前に止める。
|
|
340
|
+
const config = readConfig();
|
|
341
|
+
const problems = settingProblems(first, flag(argv, "--genre"), config, hostText(config));
|
|
341
342
|
problems.forEach((problem) => console.error(problem));
|
|
342
343
|
if (problems.length > 0) return 1;
|
|
343
344
|
const handler = HANDLERS[first];
|
package/src/commands/cite.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { checkCitations, type Citation, type CitationResult } from "../structure
|
|
|
2
2
|
import { readSource, readTree, type TreeContext } from "./tree.ts";
|
|
3
3
|
import type { Texts, UiLanguage } from "../ui.ts";
|
|
4
4
|
|
|
5
|
-
const FORMATS = ["text", "json"];
|
|
5
|
+
const FORMATS: ReadonlySet<string> = new Set(["text", "json"]);
|
|
6
6
|
const QUOTE_WIDTH = 40;
|
|
7
|
-
const VALUED = ["--format", "--language"];
|
|
7
|
+
const VALUED: ReadonlySet<string> = new Set(["--format", "--language", "--genre"]);
|
|
8
8
|
|
|
9
9
|
export const citeTargets = (argv: readonly string[]): string[] =>
|
|
10
|
-
argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.
|
|
10
|
+
argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
|
|
11
11
|
|
|
12
12
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
13
|
|
|
@@ -88,7 +88,7 @@ const describe = (result: CitationResult, text: CiteText): string => {
|
|
|
88
88
|
export const runCite = async (targets: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
|
|
89
89
|
const [sourcePath, citationsPath] = targets;
|
|
90
90
|
const format = context.flag(argv, "--format") ?? "text";
|
|
91
|
-
if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.
|
|
91
|
+
if (sourcePath === undefined || citationsPath === undefined || targets.length !== 2 || !FORMATS.has(format)) {
|
|
92
92
|
console.error(TEXT[context.ui ?? "ja"].usage);
|
|
93
93
|
return 1;
|
|
94
94
|
}
|
package/src/commands/eval.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const runEval = async (targets: readonly string[], argv: readonly string[
|
|
|
56
56
|
const adapter = await loadAdapter(language);
|
|
57
57
|
const { genre } = resolveGenre(path, source, config);
|
|
58
58
|
await adapter.prepare?.(neededBy(loadRules(language), config.rules, config.experimental, genre, language));
|
|
59
|
-
return { doc: buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language)), language, genre };
|
|
59
|
+
return { doc: buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre)), language, genre };
|
|
60
60
|
}),
|
|
61
61
|
);
|
|
62
62
|
const language = docs[0]?.language ?? "ja";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { createInterface, type Interface } from "node:readline";
|
|
4
|
+
import { CONFIG_FILE } from "../config/load.ts";
|
|
5
|
+
import { loadGenres } from "../genre-load.ts";
|
|
6
|
+
import { choicePrompt, DEFAULT_GENRE, genreChoices, genreFromAnswer, noAnswer, notAGenre } from "../init-choice.ts";
|
|
7
|
+
import type { UiLanguage } from "../ui.ts";
|
|
8
|
+
|
|
9
|
+
type Terminal = { readonly isTTY?: boolean | undefined };
|
|
10
|
+
|
|
11
|
+
/** Where init asks: the terminal, or streams a test gives it. */
|
|
12
|
+
export type Prompter = { readonly input: NodeJS.ReadableStream & Terminal; readonly output: NodeJS.WritableStream & Terminal };
|
|
13
|
+
|
|
14
|
+
/** Ask only at a terminal, and only when init will write chaff.yaml: a script or CI gets the default without waiting. */
|
|
15
|
+
export const shouldAsk = (stdin: Terminal, stdout: Terminal, configExists: boolean): boolean => stdin.isTTY === true && stdout.isTTY === true && !configExists;
|
|
16
|
+
|
|
17
|
+
/** The first line typed, or undefined when the input ends with none (Ctrl-D): waiting for a question's answer would never end then. */
|
|
18
|
+
const answerOf = (reader: Interface, prompt: string): Promise<string | undefined> => {
|
|
19
|
+
const answer = new Promise<string | undefined>((resolve) => {
|
|
20
|
+
reader.once("line", resolve);
|
|
21
|
+
reader.once("close", () => resolve(undefined));
|
|
22
|
+
});
|
|
23
|
+
reader.setPrompt(prompt);
|
|
24
|
+
reader.prompt();
|
|
25
|
+
return answer;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ask = async (ui: UiLanguage, io: Prompter): Promise<{ readonly genre: string | undefined; readonly answer: string | undefined }> => {
|
|
29
|
+
const data = loadGenres();
|
|
30
|
+
io.output.write(`${genreChoices(data, ui).join("\n")}\n`);
|
|
31
|
+
const reader = createInterface({ input: io.input, output: io.output });
|
|
32
|
+
try {
|
|
33
|
+
const answer = await answerOf(reader, choicePrompt(ui));
|
|
34
|
+
const ids = data.genres.map((genre) => genre.id);
|
|
35
|
+
return { genre: answer === undefined ? undefined : genreFromAnswer(answer, ids), answer };
|
|
36
|
+
} finally {
|
|
37
|
+
reader.close();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** The genre init writes: --genre, else the one picked at the prompt, else the default. An answer that is no genre is an error. */
|
|
42
|
+
export const initGenre = async (
|
|
43
|
+
flagged: string | undefined,
|
|
44
|
+
ui: UiLanguage,
|
|
45
|
+
dir: string,
|
|
46
|
+
io: Prompter = { input: process.stdin, output: process.stdout },
|
|
47
|
+
): Promise<{ readonly genre: string } | { readonly error: string }> => {
|
|
48
|
+
if (flagged !== undefined) return { genre: flagged };
|
|
49
|
+
if (!shouldAsk(io.input, io.output, existsSync(join(dir, CONFIG_FILE)))) return { genre: DEFAULT_GENRE };
|
|
50
|
+
const picked = await ask(ui, io);
|
|
51
|
+
if (picked.answer === undefined) return { error: noAnswer(ui) };
|
|
52
|
+
return picked.genre === undefined ? { error: notAGenre(picked.answer.trim(), ui) } : { genre: picked.genre };
|
|
53
|
+
};
|