chaffjs 0.15.0 → 0.17.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 +31 -0
- package/dist/calendar-number.d.ts.map +1 -0
- package/dist/calendar-number.js +58 -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/closing-numbers.d.ts +20 -0
- package/dist/closing-numbers.d.ts.map +1 -0
- package/dist/closing-numbers.js +56 -0
- package/dist/closing-numbers.js.map +1 -0
- 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 +78 -44
- 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/acronym-context.d.ts +2 -0
- package/dist/detectors/acronym-context.d.ts.map +1 -1
- package/dist/detectors/acronym-context.js +13 -0
- package/dist/detectors/acronym-context.js.map +1 -1
- package/dist/detectors/acronym-expansion.d.ts +2 -0
- package/dist/detectors/acronym-expansion.d.ts.map +1 -1
- package/dist/detectors/acronym-expansion.js +104 -1
- package/dist/detectors/acronym-expansion.js.map +1 -1
- package/dist/detectors/agreement-slip.d.ts +32 -0
- package/dist/detectors/agreement-slip.d.ts.map +1 -0
- package/dist/detectors/agreement-slip.js +137 -0
- package/dist/detectors/agreement-slip.js.map +1 -0
- 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/content-morphemes.d.ts +4 -0
- package/dist/detectors/content-morphemes.d.ts.map +1 -0
- package/dist/detectors/content-morphemes.js +12 -0
- package/dist/detectors/content-morphemes.js.map +1 -0
- package/dist/detectors/en-shape.d.ts +0 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +2 -143
- 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/heading-echo.d.ts.map +1 -1
- package/dist/detectors/heading-echo.js +12 -18
- package/dist/detectors/heading-echo.js.map +1 -1
- package/dist/detectors/heading-overlap.d.ts +13 -0
- package/dist/detectors/heading-overlap.d.ts.map +1 -0
- package/dist/detectors/heading-overlap.js +52 -0
- package/dist/detectors/heading-overlap.js.map +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +10 -4
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/letter-joined-name.d.ts +13 -0
- package/dist/detectors/letter-joined-name.d.ts.map +1 -0
- package/dist/detectors/letter-joined-name.js +11 -0
- package/dist/detectors/letter-joined-name.js.map +1 -0
- 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 +51 -5
- 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/oxford-comma.d.ts +3 -0
- package/dist/detectors/oxford-comma.d.ts.map +1 -0
- package/dist/detectors/oxford-comma.js +340 -0
- package/dist/detectors/oxford-comma.js.map +1 -0
- 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 +45 -16
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/stacked-hedge.d.ts +16 -0
- package/dist/detectors/stacked-hedge.d.ts.map +1 -0
- package/dist/detectors/stacked-hedge.js +67 -0
- package/dist/detectors/stacked-hedge.js.map +1 -0
- package/dist/detectors/stray-space.d.ts +7 -0
- package/dist/detectors/stray-space.d.ts.map +1 -0
- package/dist/detectors/stray-space.js +34 -0
- package/dist/detectors/stray-space.js.map +1 -0
- package/dist/detectors/structure.d.ts +3 -1
- package/dist/detectors/structure.d.ts.map +1 -1
- package/dist/detectors/structure.js +4 -2
- package/dist/detectors/structure.js.map +1 -1
- 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 +33 -6
- 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 +44 -0
- package/dist/genre-parse.d.ts.map +1 -0
- package/dist/genre-parse.js +117 -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 +40 -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 +71 -0
- package/dist/line-paragraphs.js.map +1 -0
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +2 -10
- package/dist/mask.js.map +1 -1
- package/dist/number-name.js +1 -1
- package/dist/number-name.js.map +1 -1
- package/dist/opening-line.d.ts +3 -0
- package/dist/opening-line.d.ts.map +1 -0
- package/dist/opening-line.js +14 -0
- package/dist/opening-line.js.map +1 -0
- package/dist/orthography.d.ts +5 -1
- package/dist/orthography.d.ts.map +1 -1
- package/dist/orthography.js +7 -1
- package/dist/orthography.js.map +1 -1
- package/dist/paragraph-length.d.ts +7 -0
- package/dist/paragraph-length.d.ts.map +1 -0
- package/dist/paragraph-length.js +10 -0
- package/dist/paragraph-length.js.map +1 -0
- package/dist/plugin.d.ts +12 -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 +13 -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 +12 -1
- package/dist/rule-load.js.map +1 -1
- package/dist/run-semantic.d.ts +3 -1
- package/dist/run-semantic.d.ts.map +1 -1
- package/dist/run-semantic.js +9 -4
- package/dist/run-semantic.js.map +1 -1
- package/dist/run.d.ts +4 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +29 -16
- package/dist/run.js.map +1 -1
- package/dist/semantic.d.ts +1 -1
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +10 -3
- package/dist/semantic.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/span-merge.d.ts +4 -0
- package/dist/span-merge.d.ts.map +1 -0
- package/dist/span-merge.js +12 -0
- package/dist/span-merge.js.map +1 -0
- package/dist/speaker-labels.d.ts +4 -0
- package/dist/speaker-labels.d.ts.map +1 -0
- package/dist/speaker-labels.js +106 -0
- package/dist/speaker-labels.js.map +1 -0
- package/dist/stray-space.d.ts +17 -0
- package/dist/stray-space.d.ts.map +1 -0
- package/dist/stray-space.js +66 -0
- package/dist/stray-space.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/relative-resolve.d.ts.map +1 -1
- package/dist/structure/relative-resolve.js +26 -20
- package/dist/structure/relative-resolve.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 +31 -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 +395 -0
- package/package.json +4 -3
- package/rules/adverb-overuse.yaml +2 -2
- package/rules/agentless-passive.yaml +1 -1
- package/rules/agreement-slip.yaml +36 -0
- 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 +3 -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 +2 -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 +18 -4
- package/rules/max-sentence-length.yaml +18 -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/stray-space.yaml +38 -0
- 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 +5 -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 +84 -0
- package/src/cli-args.ts +2 -2
- package/src/cli-text.ts +29 -15
- package/src/cli.ts +42 -41
- package/src/closing-numbers.ts +72 -0
- 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 +118 -56
- 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/acronym-context.ts +16 -0
- package/src/detectors/acronym-expansion.ts +132 -1
- package/src/detectors/agreement-slip.ts +191 -0
- package/src/detectors/char-shape.ts +21 -9
- package/src/detectors/content-morphemes.ts +19 -0
- package/src/detectors/en-shape.ts +2 -173
- 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/heading-echo.ts +12 -20
- package/src/detectors/heading-overlap.ts +65 -0
- package/src/detectors/index.ts +10 -4
- package/src/detectors/letter-joined-name.ts +15 -0
- package/src/detectors/lettered-index.ts +78 -0
- package/src/detectors/lexicon.ts +69 -8
- package/src/detectors/orthography.ts +31 -7
- package/src/detectors/oxford-comma.ts +402 -0
- package/src/detectors/phrase-match.ts +23 -12
- package/src/detectors/repeated-head.ts +3 -4
- package/src/detectors/signals.ts +47 -15
- package/src/detectors/stacked-hedge.ts +96 -0
- package/src/detectors/stray-space.ts +40 -0
- package/src/detectors/structure.ts +10 -2
- 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 +54 -7
- 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 +151 -0
- package/src/genre-suggest.ts +50 -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 +86 -0
- package/src/mask.ts +2 -12
- package/src/number-name.ts +1 -1
- package/src/opening-line.ts +17 -0
- package/src/orthography.ts +10 -2
- package/src/paragraph-length.ts +9 -0
- package/src/plugin.ts +12 -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 +14 -1
- package/src/run-semantic.ts +29 -21
- package/src/run.ts +33 -16
- package/src/semantic.ts +11 -3
- package/src/sentence-shape.ts +3 -7
- package/src/setting-problems.ts +24 -0
- package/src/span-merge.ts +13 -0
- package/src/speaker-labels.ts +129 -0
- package/src/stray-space.ts +81 -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/relative-resolve.ts +34 -19
- 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/document.ts
CHANGED
|
@@ -8,12 +8,14 @@ import { hasTitle } from "./heading-title.ts";
|
|
|
8
8
|
import { maskSpans } from "./mask.ts";
|
|
9
9
|
import { spansWithin, unmaskedSoftBreaks } from "./soft-break.ts";
|
|
10
10
|
import { segmentJoined } from "./joined-view.ts";
|
|
11
|
+
import { lineParagraphs } from "./line-paragraphs.ts";
|
|
12
|
+
import { speakerLabels } from "./speaker-labels.ts";
|
|
11
13
|
import { buildTree, type Outline } from "./structure/build.ts";
|
|
12
14
|
import { isMarkdownPath } from "./structure/markdown-path.ts";
|
|
13
15
|
import { pageFurniture, textOutline } from "./page-furniture.ts";
|
|
14
16
|
import { tokenizedLexicons } from "./lexicon-tokens.ts";
|
|
15
17
|
import { inPageAnchors, isInPageNavigation, isNavigationList, type InPageAnchors } from "./in-page-nav.ts";
|
|
16
|
-
import type { BulletList, LanguageAdapter, Paragraph, ProseDocument, Section, Sentence, Span, StructureNode, DocumentProfile } from "./plugin.ts";
|
|
18
|
+
import type { BulletList, LanguageAdapter, Paragraph, ProseDocument, Section, Sentence, Span, StructureNode, DocumentProfile, Token } from "./plugin.ts";
|
|
17
19
|
|
|
18
20
|
type Place = { readonly offset?: number | undefined };
|
|
19
21
|
type Node = {
|
|
@@ -203,7 +205,17 @@ const sentencesOf = (prose: string, paragraphs: readonly Span[], adapter: Langua
|
|
|
203
205
|
})),
|
|
204
206
|
);
|
|
205
207
|
|
|
206
|
-
|
|
208
|
+
/** 見出しの語。品詞を読んでいない文書(文が tokens を持たない)では分けない。 */
|
|
209
|
+
const headingTokensOf = (adapter: LanguageAdapter, tagged: boolean, heading: string): { headingTokens?: readonly Token[] } =>
|
|
210
|
+
tagged && heading !== "" ? { headingTokens: adapter.segment(heading).sentences.flatMap((sentence) => sentence.tokens ?? []) } : {};
|
|
211
|
+
|
|
212
|
+
const sectionsOf = (
|
|
213
|
+
headings: readonly Heading[],
|
|
214
|
+
sentences: readonly Sentence[],
|
|
215
|
+
strongs: readonly Span[],
|
|
216
|
+
length: number,
|
|
217
|
+
tokensOf: (heading: string) => { headingTokens?: readonly Token[] },
|
|
218
|
+
): Section[] => {
|
|
207
219
|
const bounds = headings.map((heading, index) => ({ heading, from: heading.end, to: headings[index + 1]?.start ?? length }));
|
|
208
220
|
const lead = { heading: { depth: 0, text: "", start: 0, end: 0 }, from: 0, to: headings[0]?.start ?? length };
|
|
209
221
|
return [lead, ...bounds]
|
|
@@ -213,6 +225,7 @@ const sectionsOf = (headings: readonly Heading[], sentences: readonly Sentence[]
|
|
|
213
225
|
return {
|
|
214
226
|
depth: heading.depth,
|
|
215
227
|
heading: heading.text,
|
|
228
|
+
...tokensOf(heading.text),
|
|
216
229
|
span: { start: from, end: to },
|
|
217
230
|
sentences: inside,
|
|
218
231
|
strongCount: strongs.filter((span) => within(span, from, to)).length,
|
|
@@ -221,12 +234,32 @@ const sectionsOf = (headings: readonly Heading[], sentences: readonly Sentence[]
|
|
|
221
234
|
});
|
|
222
235
|
};
|
|
223
236
|
|
|
224
|
-
|
|
237
|
+
/** sentences(並び順)の中で、start が offset 以上の最初の添字。1 行 1 段落の何万行でも、段落ごとに全部をなめない。 */
|
|
238
|
+
const firstStartingAt = (sentences: readonly Sentence[], offset: number): number => {
|
|
239
|
+
const search = (low: number, high: number): number => {
|
|
240
|
+
if (low >= high) return low;
|
|
241
|
+
const middle = (low + high) >> 1;
|
|
242
|
+
return (sentences[middle]?.span.start ?? Number.POSITIVE_INFINITY) >= offset ? search(low, middle) : search(middle + 1, high);
|
|
243
|
+
};
|
|
244
|
+
return search(0, sentences.length);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const sentencesWithin = (sentences: readonly Sentence[], span: Span): Sentence[] =>
|
|
248
|
+
sentences.slice(firstStartingAt(sentences, span.start), firstStartingAt(sentences, span.end));
|
|
249
|
+
|
|
250
|
+
const paragraphsOf = (prose: string, spans: readonly Span[], sentences: readonly Sentence[], listSpans: readonly Span[]): Paragraph[] =>
|
|
225
251
|
spans
|
|
226
252
|
// 箇条書きの中の段落は「段落」として数えない。項目 1 つを 1 段落と読むと、
|
|
227
253
|
// 段落あたりの文数も長さのばらつきも、箇条書きの多い文書で壊れる。
|
|
228
254
|
.filter((span) => !listSpans.some((list) => span.start >= list.start && span.start < list.end))
|
|
229
|
-
.
|
|
255
|
+
.flatMap((span) =>
|
|
256
|
+
lineParagraphs(
|
|
257
|
+
prose,
|
|
258
|
+
span,
|
|
259
|
+
sentencesWithin(sentences, span).map((sentence) => sentence.span),
|
|
260
|
+
),
|
|
261
|
+
)
|
|
262
|
+
.map((span) => ({ span, sentences: sentencesWithin(sentences, span) }));
|
|
230
263
|
|
|
231
264
|
/** 箇条書きは list ノードの直下の項目を数える。入れ子の項目は内側の list のものとして数える。目次のような案内だけの箇条書きは数えない。 */
|
|
232
265
|
const listsOf = (root: Node, source: string, anchors: InPageAnchors): BulletList[] => {
|
|
@@ -250,6 +283,8 @@ export type TeamRules = {
|
|
|
250
283
|
readonly requiredSections: readonly string[];
|
|
251
284
|
/** { 使わない書き方: 使う書き方 }。preferred-term が語彙表として読む。 */
|
|
252
285
|
readonly prefer?: Readonly<Record<string, string>>;
|
|
286
|
+
/** チームの固有名詞。1 つの名前として読む rule が見る。 */
|
|
287
|
+
readonly names?: readonly string[];
|
|
253
288
|
};
|
|
254
289
|
|
|
255
290
|
const EMPTY_TEAM: TeamRules = { jargon: [], requiredSections: [] };
|
|
@@ -259,12 +294,23 @@ export const teamRules = (config: {
|
|
|
259
294
|
readonly jargon: readonly string[];
|
|
260
295
|
readonly requiredSections: readonly string[];
|
|
261
296
|
readonly prefer?: Readonly<Record<string, string>>;
|
|
297
|
+
readonly names?: readonly string[];
|
|
262
298
|
}): TeamRules => ({
|
|
263
299
|
jargon: config.jargon,
|
|
264
300
|
prefer: config.prefer ?? {},
|
|
265
301
|
requiredSections: config.requiredSections,
|
|
302
|
+
names: config.names ?? [],
|
|
266
303
|
});
|
|
267
304
|
|
|
305
|
+
/**
|
|
306
|
+
* masked を覆った本文から、話し手の名前(戯曲・議事録)も覆う。名前は誰が話すかの印で、文でも本文でもない。
|
|
307
|
+
* 名前は覆った後の本文で探す。コードや表の中の行頭を話し手と数えない。
|
|
308
|
+
*/
|
|
309
|
+
const proseOf = (source: string, masked: readonly Span[]): string => {
|
|
310
|
+
const unlabelled = maskSpans(source, masked);
|
|
311
|
+
return maskSpans(unlabelled, speakerLabels(unlabelled));
|
|
312
|
+
};
|
|
313
|
+
|
|
268
314
|
export const buildDocument = (
|
|
269
315
|
path: string,
|
|
270
316
|
source: string,
|
|
@@ -279,7 +325,7 @@ export const buildDocument = (
|
|
|
279
325
|
// テキストの文書は、ページのヘッダーとフッターも本文ではない(Markdown には改ページが無い)。
|
|
280
326
|
const blocks = [...collectMasks(root, source, anchors), ...(isMarkdownPath(path) ? [] : pageFurniture(source))];
|
|
281
327
|
const masked = [...blocks, ...emphasisSpans(root, source)];
|
|
282
|
-
const prose =
|
|
328
|
+
const prose = proseOf(source, masked);
|
|
283
329
|
// ページの案内は段落としても数えない。数えると、目次の行が「本題までの段落」に入る。
|
|
284
330
|
const paragraphSpans = spansOfType(root, "paragraph", (node) => !isInPageNavigation(node, anchors));
|
|
285
331
|
const listItems = spansOfType(root, "listItem");
|
|
@@ -302,14 +348,15 @@ export const buildDocument = (
|
|
|
302
348
|
language: adapter.id,
|
|
303
349
|
lengthUnit: adapter.capabilities.lengthUnit,
|
|
304
350
|
capabilities: adapter.capabilities,
|
|
305
|
-
sections: sectionsOf(headingsOf(root, source), sentences, strongSpans(root, blocks), source.length),
|
|
351
|
+
sections: sectionsOf(headingsOf(root, source), sentences, strongSpans(root, blocks), source.length, (heading) => headingTokensOf(adapter, tagged, heading)),
|
|
306
352
|
sentences,
|
|
307
353
|
listSpans: listItems,
|
|
308
|
-
paragraphs: paragraphsOf(paragraphSpans, sentences, listItems),
|
|
354
|
+
paragraphs: paragraphsOf(prose, paragraphSpans, sentences, listItems),
|
|
309
355
|
lists: listsOf(root, source, anchors),
|
|
310
356
|
links: [...spansOfType(root, "link"), ...spansOfType(root, "linkReference")],
|
|
311
357
|
lexicons: tagged ? tokenizedLexicons(lexicons, adapter) : lexicons,
|
|
312
358
|
requiredSections: team.requiredSections,
|
|
359
|
+
names: team.names ?? [],
|
|
313
360
|
// 構造の rule(参照先が無い・番号の抜け)が読む木。どの rule も読まなければ作らない。何万行の契約書で、他の rule の lint に代金を払わせない。
|
|
314
361
|
get structure(): StructureNode | undefined {
|
|
315
362
|
tree.value ??=
|
package/src/eval.ts
CHANGED
|
@@ -41,7 +41,7 @@ const candidates = (rule: RuleDefinition, current: number): number[] => {
|
|
|
41
41
|
const fromLevels = [rule.levels.strict, rule.levels.normal, rule.levels.relaxed].filter((value) => value !== undefined);
|
|
42
42
|
const scaled = MULTIPLIERS.map((factor) => Math.max(1, Math.round(base * factor)));
|
|
43
43
|
// 数値で決めた上限は、段の表に無くても掃引に入れる。入れないと「現在」の行が出ない。
|
|
44
|
-
return [...new Set([...fromLevels, ...scaled, current])].
|
|
44
|
+
return [...new Set([...fromLevels, ...scaled, current])].toSorted((left, right) => left - right);
|
|
45
45
|
};
|
|
46
46
|
|
|
47
47
|
const countAt = (docs: readonly ProseDocument[], rule: RuleDefinition, limit: number): Point => {
|
package/src/feedback/draft.ts
CHANGED
|
@@ -61,7 +61,7 @@ const TEXT: Texts<{
|
|
|
61
61
|
"false-positive": "<!-- Why it is wrong, and how the text should be read -->",
|
|
62
62
|
missed: "<!-- What is wrong in the text, and which rule should have said so -->",
|
|
63
63
|
},
|
|
64
|
-
lines: (from, to) => `lines ${String(from)}-${String(to)}
|
|
64
|
+
lines: (from, to) => (from === to ? `line ${String(from)}` : `lines ${String(from)}-${String(to)}`),
|
|
65
65
|
},
|
|
66
66
|
};
|
|
67
67
|
|
package/src/feedback/excerpt.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/** Lines around a line of a document, 1-based and inclusive. Only these lines go into a report. */
|
|
2
2
|
export type Excerpt = { readonly from: number; readonly to: number; readonly lines: readonly string[] };
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
const EXCERPT_RADIUS = 2;
|
|
5
5
|
|
|
6
6
|
/** Pure: the excerpts around each line, overlapping ones merged, in document order. Lines outside the document are dropped. */
|
|
7
7
|
export const excerptsAround = (source: string, lines: readonly number[], radius: number = EXCERPT_RADIUS): Excerpt[] => {
|
|
8
8
|
const all = source.split("\n");
|
|
9
9
|
const ranges = [...new Set(lines)]
|
|
10
10
|
.filter((line) => line >= 1 && line <= all.length)
|
|
11
|
-
.
|
|
11
|
+
.toSorted((left, right) => left - right)
|
|
12
12
|
.map((line) => ({ from: Math.max(1, line - radius), to: Math.min(all.length, line + radius) }));
|
|
13
13
|
const merged = ranges.reduce<{ from: number; to: number }[]>((acc, range) => {
|
|
14
14
|
const last = acc.at(-1);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { CLI_TEXT, type GenreSource } from "./cli-text.ts";
|
|
2
|
+
import { loadGenres } from "./genre-load.ts";
|
|
3
|
+
import type { GenreDefinition } from "./genre-parse.ts";
|
|
4
|
+
import { suggestGenre } from "./genre-suggest.ts";
|
|
5
|
+
import { loadProfiles } from "./profile/load.ts";
|
|
6
|
+
import { uiLanguageOf } from "./ui.ts";
|
|
7
|
+
|
|
8
|
+
/** The file's first lines, and the notes after its not-run list: the genre it looks like, when none was set. */
|
|
9
|
+
type FileHeader = { readonly header: string; readonly notes: readonly string[] };
|
|
10
|
+
|
|
11
|
+
/** unread: a genre the front matter wrote that chaff does not know (already reported on stderr). */
|
|
12
|
+
type Placed = { readonly genre: string; readonly from: GenreSource; readonly unread?: string | undefined };
|
|
13
|
+
|
|
14
|
+
type Counts = { readonly shelved: number; readonly hushed: number };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The genre the file looks like, only when nothing set or guessed one: suggested on screen, never used for the run.
|
|
18
|
+
* A front matter genre chaff could not read is a genre the writer did set, so the note's "no genre was set" would be untrue.
|
|
19
|
+
*/
|
|
20
|
+
const suggestionFor = (path: string, source: string, language: string, placed: Placed): GenreDefinition | undefined =>
|
|
21
|
+
placed.from === "default" && placed.unread === undefined ? suggestGenre({ path, source, language }, loadGenres(), loadProfiles()) : undefined;
|
|
22
|
+
|
|
23
|
+
/** In the file's language. The note names the genre the file was checked as. */
|
|
24
|
+
export const fileHeader = (path: string, source: string, language: string, placed: Placed, counts: Counts): FileHeader => {
|
|
25
|
+
const text = CLI_TEXT[uiLanguageOf(language)];
|
|
26
|
+
const first = text.header(path, placed.genre, text.languageName(language), text.genreSource[placed.from], counts.shelved, counts.hushed);
|
|
27
|
+
const suggestion = suggestionFor(path, source, language, placed);
|
|
28
|
+
if (suggestion === undefined) return { header: first, notes: [] };
|
|
29
|
+
const name = suggestion.name[uiLanguageOf(language)] ?? suggestion.id;
|
|
30
|
+
return { header: [first, text.suggested(name, suggestion.id)].join("\n"), notes: [text.suggestedNote(name, suggestion.id, placed.genre)] };
|
|
31
|
+
};
|
package/src/files.ts
CHANGED
|
@@ -4,11 +4,11 @@ import { join, relative, sep } from "node:path";
|
|
|
4
4
|
const MARKDOWN = [".md", ".markdown", ".mdx"];
|
|
5
5
|
|
|
6
6
|
/** 走査から外す。ここを通すと node_modules の README を延々と検査することになる。 */
|
|
7
|
-
const SKIP = ["node_modules", "dist", "build", "coverage", ".git", ".chaff-cache"];
|
|
7
|
+
const SKIP: ReadonlySet<string> = new Set(["node_modules", "dist", "build", "coverage", ".git", ".chaff-cache"]);
|
|
8
8
|
|
|
9
9
|
const isMarkdown = (path: string): boolean => MARKDOWN.some((ext) => path.toLowerCase().endsWith(ext));
|
|
10
10
|
|
|
11
|
-
const isSkipped = (path: string): boolean => path.split(sep).some((part) => SKIP.
|
|
11
|
+
const isSkipped = (path: string): boolean => path.split(sep).some((part) => SKIP.has(part));
|
|
12
12
|
|
|
13
13
|
const expand = (target: string): string[] => {
|
|
14
14
|
const stat = statSync(target, { throwIfNoEntry: false });
|
|
@@ -30,5 +30,5 @@ const byPath = (left: string, right: string): number => left.localeCompare(right
|
|
|
30
30
|
|
|
31
31
|
export const collectTargets = (targets: readonly string[]): string[] => {
|
|
32
32
|
const found = targets.flatMap(expand).filter((path) => !isSkipped(path));
|
|
33
|
-
return [...new Set(found)].
|
|
33
|
+
return [...new Set(found)].toSorted((left, right) => byPath(relative(".", left), relative(".", right)));
|
|
34
34
|
};
|
package/src/genre-check.ts
CHANGED
|
@@ -15,7 +15,7 @@ type PathGenre = { readonly files: readonly string[]; readonly genre: string | u
|
|
|
15
15
|
type WrittenGenres = { readonly flag: string | undefined; readonly config: string | undefined; readonly byPath: readonly PathGenre[] };
|
|
16
16
|
|
|
17
17
|
/** Commands that never read a genre from chaff.yaml. A wrong one there must not stop them: genres is how to find the right name. */
|
|
18
|
-
const GENRE_FREE: ReadonlySet<string> = new Set(["init", "genres", "skill", "relax", "strict", "off"
|
|
18
|
+
const GENRE_FREE: ReadonlySet<string> = new Set(["init", "genres", "skill", "relax", "strict", "off"]);
|
|
19
19
|
|
|
20
20
|
/** The genres a command would read: --genre always (every command refuses a wrong one), chaff.yaml's only where the command reads a genre. */
|
|
21
21
|
export const writtenGenres = (
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { parseGenres, presetLevelsOf, presetProfileOf, type GenreData, type PresetLevels } from "./genre-parse.ts";
|
|
6
|
+
|
|
7
|
+
const GENRES_FILE = join(dirname(fileURLToPath(import.meta.url)), "..", "genres.yaml");
|
|
8
|
+
|
|
9
|
+
const loaded: { value: GenreData | undefined } = { value: undefined };
|
|
10
|
+
|
|
11
|
+
/** The bundled genres.yaml. Read once. */
|
|
12
|
+
export const loadGenres = (): GenreData => {
|
|
13
|
+
if (loaded.value !== undefined) return loaded.value;
|
|
14
|
+
try {
|
|
15
|
+
loaded.value = parseGenres(parse(readFileSync(GENRES_FILE, "utf8")));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw new Error(`cannot read ${GENRES_FILE}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
18
|
+
}
|
|
19
|
+
return loaded.value;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const presetLevels = (genre: string): PresetLevels => presetLevelsOf(loadGenres(), genre);
|
|
23
|
+
|
|
24
|
+
export const presetProfile = (genre: string): string | undefined => presetProfileOf(loadGenres(), genre);
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { isLevel } from "./levels.ts";
|
|
2
|
+
import type { Level, Localized } from "./plugin.ts";
|
|
3
|
+
|
|
4
|
+
/** A preset's rules: the level each named rule runs at, under whatever chaff.yaml's rules say. */
|
|
5
|
+
export type PresetLevels = Readonly<Record<string, Level>>;
|
|
6
|
+
|
|
7
|
+
/** A group of genres (legal, literature): its name, and the levels every genre in it shares. */
|
|
8
|
+
export type GenreGroup = { readonly id: string; readonly name: Localized; readonly rules: PresetLevels };
|
|
9
|
+
|
|
10
|
+
/** One genre of genres.yaml: what it is for, the levels it sets on top of its group's, and the profile it reads with. */
|
|
11
|
+
export type GenreDefinition = {
|
|
12
|
+
readonly id: string;
|
|
13
|
+
readonly name: Localized;
|
|
14
|
+
readonly summary: Localized;
|
|
15
|
+
readonly rules: PresetLevels;
|
|
16
|
+
readonly profile: string | undefined;
|
|
17
|
+
readonly suggest: GenreSuggest;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A line shape that marks the genre, and how many lines must have it. */
|
|
21
|
+
type LineCue = { readonly line: RegExp; readonly minLines: number };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* What makes a document look like this genre when no genre is set: its path, or per language, the shape of the body's first
|
|
25
|
+
* line (a letter's salutation) or enough lines of the genre's own shape.
|
|
26
|
+
*/
|
|
27
|
+
export type GenreSuggest = {
|
|
28
|
+
readonly paths: readonly RegExp[];
|
|
29
|
+
readonly lines: Readonly<Record<string, LineCue>>;
|
|
30
|
+
readonly openings: Readonly<Record<string, RegExp>>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type LanguageCue = { readonly line: LineCue | undefined; readonly opening: RegExp | undefined };
|
|
34
|
+
|
|
35
|
+
export type GenreData = { readonly groups: readonly GenreGroup[]; readonly genres: readonly GenreDefinition[] };
|
|
36
|
+
|
|
37
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
|
|
39
|
+
const isLocalized = (value: unknown): value is Localized =>
|
|
40
|
+
isRecord(value) && typeof value["ja"] === "string" && typeof value["en"] === "string" && value["ja"] !== "" && value["en"] !== "";
|
|
41
|
+
|
|
42
|
+
const levelsOf = (value: unknown, where: string): PresetLevels => {
|
|
43
|
+
if (value === undefined) return {};
|
|
44
|
+
if (!isRecord(value)) throw new Error(`${where}: rules must be a map of rule ids to levels`);
|
|
45
|
+
const wrong = Object.entries(value).find(([, level]) => !isLevel(level));
|
|
46
|
+
if (wrong !== undefined) throw new Error(`${where}: ${wrong[0]} has the level ${String(wrong[1])}; use strict, normal, relaxed or off`);
|
|
47
|
+
return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, Level] => isLevel(entry[1])));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const localizedOf = (value: unknown, field: string, where: string): Localized => {
|
|
51
|
+
if (!isLocalized(value)) throw new Error(`${where}: ${field} needs a ja and an en text`);
|
|
52
|
+
return value;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const idOf = (value: unknown, index: number): string => {
|
|
56
|
+
const id = isRecord(value) ? value["id"] : undefined;
|
|
57
|
+
if (typeof id !== "string" || id === "") throw new Error(`entry ${String(index + 1)} has no id`);
|
|
58
|
+
return id;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const groupOf = (value: unknown, index: number): GenreGroup => {
|
|
62
|
+
const id = idOf(value, index);
|
|
63
|
+
const raw = isRecord(value) ? value : {};
|
|
64
|
+
return { id, name: localizedOf(raw["name"], "name", id), rules: levelsOf(raw["rules"], id) };
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const patternOf = (value: unknown, flags: string, where: string): RegExp => {
|
|
68
|
+
if (typeof value !== "string" || value === "") throw new Error(`${where}: a pattern must be a non-empty string`);
|
|
69
|
+
try {
|
|
70
|
+
return new RegExp(value, flags);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(`${where}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const lineCueOf = (value: unknown, where: string): LineCue => {
|
|
77
|
+
if (!isRecord(value)) throw new Error(`${where}: needs line and min_lines`);
|
|
78
|
+
const minLines = value["min_lines"];
|
|
79
|
+
if (typeof minLines !== "number" || !Number.isInteger(minLines) || minLines < 1) throw new Error(`${where}: min_lines must be a whole number of 1 or more`);
|
|
80
|
+
return { line: patternOf(value["line"], "u", where), minLines };
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const hasLineCue = (value: Readonly<Record<string, unknown>>): boolean => value["line"] !== undefined || value["min_lines"] !== undefined;
|
|
84
|
+
|
|
85
|
+
const languageCueOf = (value: unknown, where: string): LanguageCue => {
|
|
86
|
+
if (!isRecord(value) || (!hasLineCue(value) && value["opening"] === undefined)) throw new Error(`${where}: needs line and min_lines, or opening`);
|
|
87
|
+
return {
|
|
88
|
+
line: hasLineCue(value) ? lineCueOf(value, where) : undefined,
|
|
89
|
+
opening: value["opening"] === undefined ? undefined : patternOf(value["opening"], "u", where),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const presentOf = <T>(cues: readonly (readonly [string, T | undefined])[]): Record<string, T> =>
|
|
94
|
+
Object.fromEntries(cues.filter((entry): entry is readonly [string, T] => entry[1] !== undefined));
|
|
95
|
+
|
|
96
|
+
const suggestOf = (value: unknown, where: string): GenreSuggest => {
|
|
97
|
+
if (value === undefined) return { paths: [], lines: {}, openings: {} };
|
|
98
|
+
if (!isRecord(value)) throw new Error(`${where}: suggest must be a map`);
|
|
99
|
+
const paths = value["paths"] ?? [];
|
|
100
|
+
if (!Array.isArray(paths)) throw new Error(`${where}: suggest.paths must be a list`);
|
|
101
|
+
const cues = Object.entries(value)
|
|
102
|
+
.filter(([key]) => key !== "paths")
|
|
103
|
+
.map(([language, cue]) => [language, languageCueOf(cue, `${where} suggest.${language}`)] as const);
|
|
104
|
+
return {
|
|
105
|
+
paths: paths.map((path) => patternOf(path, "iu", `${where} suggest.paths`)),
|
|
106
|
+
lines: presentOf(cues.map(([language, cue]) => [language, cue.line] as const)),
|
|
107
|
+
openings: presentOf(cues.map(([language, cue]) => [language, cue.opening] as const)),
|
|
108
|
+
};
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const genreOf = (value: unknown, index: number): GenreDefinition => {
|
|
112
|
+
const id = idOf(value, index);
|
|
113
|
+
const raw = isRecord(value) ? value : {};
|
|
114
|
+
const profile = raw["profile"];
|
|
115
|
+
if (profile !== undefined && (typeof profile !== "string" || profile === "")) throw new Error(`${id}: profile must be a profile's id`);
|
|
116
|
+
return {
|
|
117
|
+
id,
|
|
118
|
+
name: localizedOf(raw["name"], "name", id),
|
|
119
|
+
summary: localizedOf(raw["summary"], "summary", id),
|
|
120
|
+
rules: levelsOf(raw["rules"], id),
|
|
121
|
+
profile,
|
|
122
|
+
suggest: suggestOf(raw["suggest"], id),
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const listOf = (raw: Record<string, unknown>, key: string): unknown[] => {
|
|
127
|
+
const value = raw[key];
|
|
128
|
+
if (!Array.isArray(value)) throw new Error(`genres.yaml needs a list under ${key}`);
|
|
129
|
+
return value;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/** genres.yaml as read by the yaml package. Throws on anything it cannot read: the file ships with chaff, so a wrong entry is chaff's bug. */
|
|
133
|
+
export const parseGenres = (raw: unknown): GenreData => {
|
|
134
|
+
if (!isRecord(raw)) throw new Error("genres.yaml must be a map with groups and genres");
|
|
135
|
+
const groups = listOf(raw, "groups").map(groupOf);
|
|
136
|
+
const genres = listOf(raw, "genres").map(genreOf);
|
|
137
|
+
const orphan = genres.find((genre) => !groups.some((group) => genre.id.startsWith(`${group.id}/`)));
|
|
138
|
+
if (orphan !== undefined) throw new Error(`${orphan.id}: no group matches the part before its /`);
|
|
139
|
+
return { groups, genres };
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
/** The levels a genre's preset sets: its group's, then its own on top. Empty for a genre chaff does not know. */
|
|
143
|
+
export const presetLevelsOf = (data: GenreData, genre: string): PresetLevels => {
|
|
144
|
+
const own = data.genres.find((entry) => entry.id === genre);
|
|
145
|
+
if (own === undefined) return {};
|
|
146
|
+
const group = data.groups.find((entry) => genre.startsWith(`${entry.id}/`));
|
|
147
|
+
return { ...group?.rules, ...own.rules };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** The profile a genre reads its documents with, when it names one. */
|
|
151
|
+
export const presetProfileOf = (data: GenreData, genre: string): string | undefined => data.genres.find((entry) => entry.id === genre)?.profile;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GenreData, GenreDefinition } from "./genre-parse.ts";
|
|
2
|
+
import { openingLine } from "./opening-line.ts";
|
|
3
|
+
import { detected, type ProfileDefinition } from "./profile/parse.ts";
|
|
4
|
+
|
|
5
|
+
/** The file a genre is suggested for: where it is, what it says, and its language. */
|
|
6
|
+
export type SuggestInput = { readonly path: string; readonly source: string; readonly language: string };
|
|
7
|
+
|
|
8
|
+
/** A path cue names the kind outright, so it outweighs any count of lines. */
|
|
9
|
+
const BY_PATH = Number.POSITIVE_INFINITY;
|
|
10
|
+
|
|
11
|
+
/** How the body opens (Dear ..., 拝啓) says what the document is even when it encloses another kind; a path still says more. */
|
|
12
|
+
const BY_OPENING = Number.MAX_VALUE;
|
|
13
|
+
|
|
14
|
+
const opensAs = (genre: GenreDefinition, input: SuggestInput): boolean => {
|
|
15
|
+
const opening = genre.suggest.openings[input.language];
|
|
16
|
+
const first = openingLine(input.source);
|
|
17
|
+
return opening !== undefined && first !== undefined && opening.test(first);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** How many times over the genre's lines reach their minimum: 1 is just enough, 0 is none of that shape. */
|
|
21
|
+
const linesScore = (genre: GenreDefinition, input: SuggestInput): number => {
|
|
22
|
+
const cue = genre.suggest.lines[input.language];
|
|
23
|
+
if (cue === undefined) return 0;
|
|
24
|
+
return input.source.split("\n").filter((line) => cue.line.test(line)).length / cue.minLines;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** A genre that reads with a profile has the profile's shape (a statute's article lines) as just-enough evidence. */
|
|
28
|
+
const profileScore = (genre: GenreDefinition, input: SuggestInput, profiles: readonly ProfileDefinition[]): number => {
|
|
29
|
+
const profile = profiles.find((definition) => definition.id === genre.profile);
|
|
30
|
+
return profile !== undefined && detected(profile, input.source, input.language) ? 1 : 0;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const scoreOf = (genre: GenreDefinition, input: SuggestInput, profiles: readonly ProfileDefinition[]): number => {
|
|
34
|
+
if (genre.suggest.paths.some((pattern) => pattern.test(input.path))) return BY_PATH;
|
|
35
|
+
if (opensAs(genre, input)) return BY_OPENING;
|
|
36
|
+
return Math.max(linesScore(genre, input), profileScore(genre, input, profiles));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type Scored = { readonly genre: GenreDefinition; readonly score: number };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The genre a file with no genre set looks like, for the screen to suggest. Never used to check the file: switching silently
|
|
43
|
+
* would change what an existing run reports. The genre whose cues the file meets most strongly wins (a transcript that
|
|
44
|
+
* discusses a bill has more speaker lines than lines about "this Act"); a tie goes to the genre listed first in genres.yaml.
|
|
45
|
+
*/
|
|
46
|
+
export const suggestGenre = (input: SuggestInput, data: GenreData, profiles: readonly ProfileDefinition[]): GenreDefinition | undefined =>
|
|
47
|
+
data.genres
|
|
48
|
+
.map((genre): Scored => ({ genre, score: scoreOf(genre, input, profiles) }))
|
|
49
|
+
.filter((entry) => entry.score >= 1)
|
|
50
|
+
.reduce<Scored | undefined>((best, entry) => (best === undefined || entry.score > best.score ? entry : best), undefined)?.genre;
|
package/src/genre.ts
CHANGED
|
@@ -1,16 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"blog/tech",
|
|
6
|
-
"blog/essay",
|
|
7
|
-
"blog/owned-media",
|
|
8
|
-
"business/proposal",
|
|
9
|
-
"business/report",
|
|
10
|
-
"business/email",
|
|
11
|
-
"business/press-release",
|
|
12
|
-
"business/meeting-notes",
|
|
13
|
-
];
|
|
1
|
+
import { loadGenres } from "./genre-load.ts";
|
|
2
|
+
|
|
3
|
+
/** chaff が知っているジャンル。genres.yaml の並び。rule の use_for はこの頭の部分で当てる。 */
|
|
4
|
+
export const GENRES: readonly string[] = loadGenres().genres.map((genre) => genre.id);
|
|
14
5
|
|
|
15
6
|
export type GenreGuess = { readonly genre: string; readonly from: "front-matter" | "path" | "content" };
|
|
16
7
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GenreData } from "./genre-parse.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "./ui.ts";
|
|
3
|
+
|
|
4
|
+
/** The genre init writes when nothing is chosen: the one a check uses when none is set. */
|
|
5
|
+
export const DEFAULT_GENRE = "blog/tech";
|
|
6
|
+
|
|
7
|
+
const TEXT: Texts<{ readonly heading: string; readonly prompt: string; readonly notAGenre: (answer: string) => string; readonly noAnswer: string }> = {
|
|
8
|
+
ja: {
|
|
9
|
+
heading: "どの種類の文書をここに置きますか? 選んだ種類の書き方に合わせて見ます。",
|
|
10
|
+
prompt: `番号か名前を入れてください(Enter で ${DEFAULT_GENRE}): `,
|
|
11
|
+
notAGenre: (answer) => `「${answer}」は一覧にありません。何も作っていません。npx chaffjs init --genre <ジャンル> でも選べます。`,
|
|
12
|
+
noAnswer: "答えが無いまま入力が終わったので、何も作っていません。",
|
|
13
|
+
},
|
|
14
|
+
en: {
|
|
15
|
+
heading: "What kind of document goes here? chaff checks it the way that kind is written.",
|
|
16
|
+
prompt: `Type a number or a name (Enter for ${DEFAULT_GENRE}): `,
|
|
17
|
+
notAGenre: (answer) => `"${answer}" is not in the list. Nothing was created. npx chaffjs init --genre <genre> chooses one too.`,
|
|
18
|
+
noAnswer: "The input ended without an answer, so nothing was created.",
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** The numbered list init shows before it asks: every genre with what it is for, in genres.yaml's order. */
|
|
23
|
+
export const genreChoices = (data: GenreData, ui: UiLanguage): string[] => {
|
|
24
|
+
const width = Math.max(...data.genres.map((genre) => genre.id.length)) + 2;
|
|
25
|
+
const numberWidth = String(data.genres.length).length;
|
|
26
|
+
return [
|
|
27
|
+
"",
|
|
28
|
+
TEXT[ui].heading,
|
|
29
|
+
"",
|
|
30
|
+
...data.genres.map((genre, index) => ` ${String(index + 1).padStart(numberWidth)} ${genre.id.padEnd(width)}${genre.summary[ui] ?? ""}`),
|
|
31
|
+
"",
|
|
32
|
+
];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const choicePrompt = (ui: UiLanguage): string => TEXT[ui].prompt;
|
|
36
|
+
|
|
37
|
+
export const notAGenre = (answer: string, ui: UiLanguage): string => TEXT[ui].notAGenre(answer);
|
|
38
|
+
|
|
39
|
+
export const noAnswer = (ui: UiLanguage): string => TEXT[ui].noAnswer;
|
|
40
|
+
|
|
41
|
+
/** A number from the list, a genre's name, or nothing (the default). undefined for anything else: init then writes nothing. */
|
|
42
|
+
export const genreFromAnswer = (answer: string, ids: readonly string[]): string | undefined => {
|
|
43
|
+
const typed = answer.trim();
|
|
44
|
+
if (typed === "") return DEFAULT_GENRE;
|
|
45
|
+
if (ids.includes(typed)) return typed;
|
|
46
|
+
return /^\d+$/u.test(typed) ? ids[Number(typed) - 1] : undefined;
|
|
47
|
+
};
|
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 度目は問い合わせない。
|