chaffjs 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/address-chain.js +1 -1
- package/dist/address-chain.js.map +1 -1
- package/dist/backends/anthropic.d.ts +2 -1
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +4 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/openai.d.ts +2 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/openai.js +1 -1
- package/dist/backends/openai.js.map +1 -1
- package/dist/backends/types.d.ts +2 -1
- package/dist/backends/types.d.ts.map +1 -1
- package/dist/backends/types.js +1 -1
- package/dist/backends/types.js.map +1 -1
- package/dist/baseline.js +1 -1
- package/dist/baseline.js.map +1 -1
- package/dist/calendar-number.d.ts +26 -0
- package/dist/calendar-number.d.ts.map +1 -0
- package/dist/calendar-number.js +51 -0
- package/dist/calendar-number.js.map +1 -0
- package/dist/cli-args.d.ts +0 -5
- package/dist/cli-args.d.ts.map +1 -1
- package/dist/cli-args.js +2 -2
- package/dist/cli-args.js.map +1 -1
- package/dist/cli-text.d.ts +6 -2
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +19 -12
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +43 -37
- package/dist/cli.js.map +1 -1
- package/dist/commands/cite.d.ts.map +1 -1
- package/dist/commands/cite.js +4 -4
- package/dist/commands/cite.js.map +1 -1
- package/dist/commands/eval.js +1 -1
- package/dist/commands/eval.js.map +1 -1
- package/dist/commands/init-ask.d.ts +19 -0
- package/dist/commands/init-ask.d.ts.map +1 -0
- package/dist/commands/init-ask.js +43 -0
- package/dist/commands/init-ask.js.map +1 -0
- package/dist/commands/test.d.ts +8 -0
- package/dist/commands/test.d.ts.map +1 -1
- package/dist/commands/test.js +70 -38
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/tree.d.ts +1 -8
- package/dist/commands/tree.d.ts.map +1 -1
- package/dist/commands/tree.js +7 -6
- package/dist/commands/tree.js.map +1 -1
- package/dist/config/by-path.d.ts.map +1 -1
- package/dist/config/by-path.js +10 -9
- package/dist/config/by-path.js.map +1 -1
- package/dist/config/load.d.ts +4 -0
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +19 -0
- package/dist/config/load.js.map +1 -1
- package/dist/config/name-problems.d.ts +5 -0
- package/dist/config/name-problems.d.ts.map +1 -0
- package/dist/config/name-problems.js +11 -0
- package/dist/config/name-problems.js.map +1 -0
- package/dist/date-stamp.js +1 -1
- package/dist/date-stamp.js.map +1 -1
- package/dist/detectors/char-shape.d.ts.map +1 -1
- package/dist/detectors/char-shape.js +16 -6
- package/dist/detectors/char-shape.js.map +1 -1
- package/dist/detectors/en-shape.d.ts.map +1 -1
- package/dist/detectors/en-shape.js +101 -7
- package/dist/detectors/en-shape.js.map +1 -1
- package/dist/detectors/enumerated-runs.d.ts.map +1 -1
- package/dist/detectors/enumerated-runs.js +4 -2
- package/dist/detectors/enumerated-runs.js.map +1 -1
- package/dist/detectors/gram-place.js +1 -1
- package/dist/detectors/gram-place.js.map +1 -1
- package/dist/detectors/heading-case.d.ts.map +1 -1
- package/dist/detectors/heading-case.js +3 -1
- package/dist/detectors/heading-case.js.map +1 -1
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -3
- package/dist/detectors/index.js.map +1 -1
- package/dist/detectors/lettered-index.d.ts +9 -0
- package/dist/detectors/lettered-index.d.ts.map +1 -0
- package/dist/detectors/lettered-index.js +54 -0
- package/dist/detectors/lettered-index.js.map +1 -0
- package/dist/detectors/lexicon.d.ts +5 -1
- package/dist/detectors/lexicon.d.ts.map +1 -1
- package/dist/detectors/lexicon.js +46 -3
- package/dist/detectors/lexicon.js.map +1 -1
- package/dist/detectors/orthography.d.ts.map +1 -1
- package/dist/detectors/orthography.js +24 -6
- package/dist/detectors/orthography.js.map +1 -1
- package/dist/detectors/phrase-match.d.ts +3 -1
- package/dist/detectors/phrase-match.d.ts.map +1 -1
- package/dist/detectors/phrase-match.js +20 -15
- package/dist/detectors/phrase-match.js.map +1 -1
- package/dist/detectors/repeated-head.d.ts.map +1 -1
- package/dist/detectors/repeated-head.js +5 -4
- package/dist/detectors/repeated-head.js.map +1 -1
- package/dist/detectors/signals.d.ts.map +1 -1
- package/dist/detectors/signals.js +16 -6
- package/dist/detectors/signals.js.map +1 -1
- package/dist/detectors/stacked-hedge.d.ts +23 -0
- package/dist/detectors/stacked-hedge.d.ts.map +1 -0
- package/dist/detectors/stacked-hedge.js +64 -0
- package/dist/detectors/stacked-hedge.js.map +1 -0
- package/dist/detectors/superlative-amount.d.ts +9 -0
- package/dist/detectors/superlative-amount.d.ts.map +1 -0
- package/dist/detectors/superlative-amount.js +20 -0
- package/dist/detectors/superlative-amount.js.map +1 -0
- package/dist/detectors/superlative-clause.d.ts +18 -0
- package/dist/detectors/superlative-clause.d.ts.map +1 -0
- package/dist/detectors/superlative-clause.js +61 -0
- package/dist/detectors/superlative-clause.js.map +1 -0
- package/dist/detectors/superlative-scope.d.ts +6 -0
- package/dist/detectors/superlative-scope.d.ts.map +1 -1
- package/dist/detectors/superlative-scope.js +3 -3
- package/dist/detectors/superlative-scope.js.map +1 -1
- package/dist/detectors/team.d.ts +0 -6
- package/dist/detectors/team.d.ts.map +1 -1
- package/dist/detectors/team.js +17 -1
- package/dist/detectors/team.js.map +1 -1
- package/dist/detectors/token-shape.d.ts.map +1 -1
- package/dist/detectors/token-shape.js +8 -6
- package/dist/detectors/token-shape.js.map +1 -1
- package/dist/document.d.ts +3 -0
- package/dist/document.d.ts.map +1 -1
- package/dist/document.js +18 -3
- package/dist/document.js.map +1 -1
- package/dist/eval.js +1 -1
- package/dist/eval.js.map +1 -1
- package/dist/feedback/draft.js +1 -1
- package/dist/feedback/draft.js.map +1 -1
- package/dist/feedback/excerpt.d.ts +0 -1
- package/dist/feedback/excerpt.d.ts.map +1 -1
- package/dist/feedback/excerpt.js +2 -2
- package/dist/feedback/excerpt.js.map +1 -1
- package/dist/file-header.d.ts +20 -0
- package/dist/file-header.d.ts.map +1 -0
- package/dist/file-header.js +21 -0
- package/dist/file-header.js.map +1 -0
- package/dist/files.js +3 -3
- package/dist/files.js.map +1 -1
- package/dist/genre-check.js +1 -1
- package/dist/genre-check.js.map +1 -1
- package/dist/genre-load.d.ts +6 -0
- package/dist/genre-load.d.ts.map +1 -0
- package/dist/genre-load.js +22 -0
- package/dist/genre-load.js.map +1 -0
- package/dist/genre-parse.d.ts +40 -0
- package/dist/genre-parse.d.ts.map +1 -0
- package/dist/genre-parse.js +104 -0
- package/dist/genre-parse.js.map +1 -0
- package/dist/genre-suggest.d.ts +15 -0
- package/dist/genre-suggest.d.ts.map +1 -0
- package/dist/genre-suggest.js +26 -0
- package/dist/genre-suggest.js.map +1 -0
- package/dist/genre.d.ts +1 -1
- package/dist/genre.d.ts.map +1 -1
- package/dist/genre.js +3 -13
- package/dist/genre.js.map +1 -1
- package/dist/init-choice.d.ts +12 -0
- package/dist/init-choice.d.ts.map +1 -0
- package/dist/init-choice.js +41 -0
- package/dist/init-choice.js.map +1 -0
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +16 -5
- package/dist/init.js.map +1 -1
- package/dist/judge.d.ts +2 -1
- package/dist/judge.d.ts.map +1 -1
- package/dist/judge.js +1 -1
- package/dist/judge.js.map +1 -1
- package/dist/line-paragraphs.d.ts +4 -0
- package/dist/line-paragraphs.d.ts.map +1 -0
- package/dist/line-paragraphs.js +69 -0
- package/dist/line-paragraphs.js.map +1 -0
- package/dist/mask.d.ts.map +1 -1
- package/dist/mask.js +7 -6
- package/dist/mask.js.map +1 -1
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/profile/check.d.ts +28 -0
- package/dist/profile/check.d.ts.map +1 -0
- package/dist/profile/check.js +12 -0
- package/dist/profile/check.js.map +1 -0
- package/dist/profile/for-file.d.ts +2 -2
- package/dist/profile/for-file.d.ts.map +1 -1
- package/dist/profile/for-file.js +9 -2
- package/dist/profile/for-file.js.map +1 -1
- package/dist/profile/load.js +1 -1
- package/dist/profile/load.js.map +1 -1
- package/dist/profile/select.d.ts +4 -2
- package/dist/profile/select.d.ts.map +1 -1
- package/dist/profile/select.js +8 -3
- package/dist/profile/select.js.map +1 -1
- package/dist/quoted-span.d.ts +15 -3
- package/dist/quoted-span.d.ts.map +1 -1
- package/dist/quoted-span.js +16 -11
- package/dist/quoted-span.js.map +1 -1
- package/dist/reasons.d.ts +1 -0
- package/dist/reasons.d.ts.map +1 -1
- package/dist/reasons.js +4 -1
- package/dist/reasons.js.map +1 -1
- package/dist/render/compact.d.ts.map +1 -1
- package/dist/render/compact.js +1 -2
- package/dist/render/compact.js.map +1 -1
- package/dist/render/eval.d.ts.map +1 -1
- package/dist/render/eval.js +4 -3
- package/dist/render/eval.js.map +1 -1
- package/dist/render/friendly.d.ts +2 -1
- package/dist/render/friendly.d.ts.map +1 -1
- package/dist/render/friendly.js +14 -6
- package/dist/render/friendly.js.map +1 -1
- package/dist/render/genres.d.ts +5 -0
- package/dist/render/genres.d.ts.map +1 -0
- package/dist/render/genres.js +28 -0
- package/dist/render/genres.js.map +1 -0
- package/dist/render/plural.d.ts +3 -0
- package/dist/render/plural.d.ts.map +1 -0
- package/dist/render/plural.js +5 -0
- package/dist/render/plural.js.map +1 -0
- package/dist/render/rules-json.d.ts.map +1 -1
- package/dist/render/rules-json.js +9 -3
- package/dist/render/rules-json.js.map +1 -1
- package/dist/render/semantic.d.ts +3 -2
- package/dist/render/semantic.d.ts.map +1 -1
- package/dist/render/semantic.js +94 -34
- package/dist/render/semantic.js.map +1 -1
- package/dist/render/summary.d.ts.map +1 -1
- package/dist/render/summary.js +4 -3
- package/dist/render/summary.js.map +1 -1
- package/dist/render/suppressions.d.ts.map +1 -1
- package/dist/render/suppressions.js +3 -2
- package/dist/render/suppressions.js.map +1 -1
- package/dist/render/text.d.ts +2 -0
- package/dist/render/text.d.ts.map +1 -1
- package/dist/render/text.js +11 -3
- package/dist/render/text.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +4 -1
- package/dist/rule-load.js.map +1 -1
- package/dist/run-semantic.js +1 -1
- package/dist/run-semantic.js.map +1 -1
- package/dist/run.d.ts +2 -0
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +25 -15
- package/dist/run.js.map +1 -1
- package/dist/sentence-shape.d.ts +0 -1
- package/dist/sentence-shape.d.ts.map +1 -1
- package/dist/sentence-shape.js +3 -5
- package/dist/sentence-shape.js.map +1 -1
- package/dist/setting-problems.d.ts +5 -0
- package/dist/setting-problems.d.ts.map +1 -0
- package/dist/setting-problems.js +15 -0
- package/dist/setting-problems.js.map +1 -0
- package/dist/structure/build.js +5 -5
- package/dist/structure/build.js.map +1 -1
- package/dist/structure/cite.js +1 -1
- package/dist/structure/cite.js.map +1 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +5 -5
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/markdown-path.d.ts.map +1 -1
- package/dist/structure/markdown-path.js +2 -2
- package/dist/structure/markdown-path.js.map +1 -1
- package/dist/structure/numbering.d.ts.map +1 -1
- package/dist/structure/numbering.js +1 -1
- package/dist/structure/numbering.js.map +1 -1
- package/dist/structure/relative-find.js +1 -1
- package/dist/structure/relative-find.js.map +1 -1
- package/dist/structure/sexp.js +1 -1
- package/dist/structure/sexp.js.map +1 -1
- package/dist/structure/unnumbered.js +2 -2
- package/dist/structure/unnumbered.js.map +1 -1
- package/dist/team-names.d.ts +9 -0
- package/dist/team-names.d.ts.map +1 -0
- package/dist/team-names.js +34 -0
- package/dist/team-names.js.map +1 -0
- package/dist/ui.d.ts +2 -0
- package/dist/ui.d.ts.map +1 -1
- package/dist/ui.js +6 -0
- package/dist/ui.js.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/watch.d.ts +2 -2
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +5 -5
- package/dist/watch.js.map +1 -1
- package/genres.yaml +363 -0
- package/package.json +4 -3
- package/rules/adverb-overuse.yaml +2 -2
- package/rules/agentless-passive.yaml +1 -1
- package/rules/ai-generated-composite.yaml +2 -2
- package/rules/ai-tell.yaml +2 -2
- package/rules/bold-density.yaml +2 -2
- package/rules/closing-cliche.yaml +1 -1
- package/rules/concrete-evidence-density.yaml +2 -2
- package/rules/contraction-consistency.yaml +1 -1
- package/rules/cushion-phrase-density.yaml +1 -1
- package/rules/dangling-reference.yaml +1 -1
- package/rules/date-order.yaml +1 -1
- package/rules/date-weekday-mismatch.yaml +1 -1
- package/rules/double-keigo.yaml +1 -1
- package/rules/doubled-word.yaml +1 -1
- package/rules/duplicate-definition.yaml +1 -1
- package/rules/emoji-density.yaml +1 -1
- package/rules/empty-conclusion.yaml +2 -1
- package/rules/empty-intensifier.yaml +1 -1
- package/rules/excessive-hedging.yaml +16 -6
- package/rules/expletive-construction.yaml +2 -2
- package/rules/heading-echo.yaml +1 -1
- package/rules/hiragana-fukushi.yaml +1 -1
- package/rules/internal-jargon.yaml +2 -2
- package/rules/latin-spacing.yaml +5 -4
- package/rules/max-kanji-continuous.yaml +4 -3
- package/rules/max-paragraph-length.yaml +9 -2
- package/rules/max-sentence-length.yaml +14 -2
- package/rules/ngram-repetition.yaml +4 -4
- package/rules/no-doubled-joshi.yaml +2 -2
- package/rules/no-em-dash.yaml +2 -2
- package/rules/no-mixed-desumasu.yaml +1 -1
- package/rules/no-nakaguro-parallel.yaml +2 -2
- package/rules/numbering-gap.yaml +1 -1
- package/rules/oxford-comma-consistency.yaml +2 -1
- package/rules/padded-intro.yaml +2 -1
- package/rules/paragraph-length-variance.yaml +1 -1
- package/rules/preamble-length.yaml +2 -2
- package/rules/preferred-term.yaml +1 -1
- package/rules/proper-noun-density.yaml +3 -2
- package/rules/repeated-conjunction.yaml +2 -2
- package/rules/repeated-sentence-head.yaml +2 -2
- package/rules/required-sections.yaml +1 -1
- package/rules/rule-of-three.yaml +2 -2
- package/rules/sasete-itadaku.yaml +3 -3
- package/rules/section-length-uniformity.yaml +1 -1
- package/rules/sentence-initial-conjunction-run.yaml +2 -2
- package/rules/sentence-rhythm.yaml +1 -1
- package/rules/taigen-dome-in-prose.yaml +2 -2
- package/rules/title-case-consistency.yaml +1 -1
- package/rules/total-mismatch.yaml +1 -1
- package/rules/undefined-acronym.yaml +4 -4
- package/rules/unqualified-superlative.yaml +2 -2
- package/skills/chaff/SKILL.md +28 -9
- package/src/address-chain.ts +1 -1
- package/src/backends/anthropic.ts +5 -1
- package/src/backends/openai.ts +2 -1
- package/src/backends/types.ts +3 -2
- package/src/baseline.ts +1 -1
- package/src/calendar-number.ts +75 -0
- package/src/cli-args.ts +2 -2
- package/src/cli-text.ts +29 -15
- package/src/cli.ts +42 -41
- package/src/commands/cite.ts +4 -4
- package/src/commands/eval.ts +1 -1
- package/src/commands/init-ask.ts +53 -0
- package/src/commands/test.ts +102 -45
- package/src/commands/tree.ts +7 -6
- package/src/config/by-path.ts +8 -9
- package/src/config/load.ts +24 -0
- package/src/config/name-problems.ts +15 -0
- package/src/date-stamp.ts +1 -1
- package/src/detectors/char-shape.ts +21 -9
- package/src/detectors/en-shape.ts +106 -8
- package/src/detectors/enumerated-runs.ts +3 -2
- package/src/detectors/gram-place.ts +1 -1
- package/src/detectors/heading-case.ts +4 -1
- package/src/detectors/index.ts +4 -3
- package/src/detectors/lettered-index.ts +78 -0
- package/src/detectors/lexicon.ts +63 -6
- package/src/detectors/orthography.ts +31 -7
- package/src/detectors/phrase-match.ts +23 -12
- package/src/detectors/repeated-head.ts +3 -4
- package/src/detectors/signals.ts +15 -6
- package/src/detectors/stacked-hedge.ts +93 -0
- package/src/detectors/superlative-amount.ts +24 -0
- package/src/detectors/superlative-clause.ts +82 -0
- package/src/detectors/superlative-scope.ts +3 -3
- package/src/detectors/team.ts +18 -2
- package/src/detectors/token-shape.ts +8 -10
- package/src/document.ts +29 -3
- package/src/eval.ts +1 -1
- package/src/feedback/draft.ts +1 -1
- package/src/feedback/excerpt.ts +2 -2
- package/src/file-header.ts +31 -0
- package/src/files.ts +3 -3
- package/src/genre-check.ts +1 -1
- package/src/genre-load.ts +24 -0
- package/src/genre-parse.ts +126 -0
- package/src/genre-suggest.ts +37 -0
- package/src/genre.ts +4 -13
- package/src/init-choice.ts +47 -0
- package/src/init.ts +20 -6
- package/src/judge.ts +3 -1
- package/src/line-paragraphs.ts +83 -0
- package/src/mask.ts +5 -6
- package/src/plugin.ts +6 -0
- package/src/profile/check.ts +34 -0
- package/src/profile/for-file.ts +10 -3
- package/src/profile/load.ts +1 -1
- package/src/profile/select.ts +11 -4
- package/src/quoted-span.ts +26 -12
- package/src/reasons.ts +5 -1
- package/src/render/compact.ts +1 -3
- package/src/render/eval.ts +4 -3
- package/src/render/friendly.ts +21 -6
- package/src/render/genres.ts +31 -0
- package/src/render/plural.ts +6 -0
- package/src/render/rules-json.ts +10 -3
- package/src/render/semantic.ts +117 -34
- package/src/render/summary.ts +4 -3
- package/src/render/suppressions.ts +3 -2
- package/src/render/text.ts +18 -3
- package/src/rule-load.ts +6 -1
- package/src/run-semantic.ts +1 -1
- package/src/run.ts +28 -15
- package/src/sentence-shape.ts +3 -7
- package/src/setting-problems.ts +24 -0
- package/src/structure/build.ts +5 -5
- package/src/structure/cite.ts +1 -1
- package/src/structure/issues.ts +6 -5
- package/src/structure/markdown-path.ts +2 -2
- package/src/structure/numbering.ts +1 -1
- package/src/structure/relative-find.ts +1 -1
- package/src/structure/sexp.ts +1 -1
- package/src/structure/unnumbered.ts +2 -2
- package/src/team-names.ts +39 -0
- package/src/ui.ts +7 -0
- package/src/version.ts +1 -1
- package/src/watch.ts +5 -5
package/src/commands/test.ts
CHANGED
|
@@ -7,20 +7,80 @@ import { buildDocument, teamRules } from "../document.ts";
|
|
|
7
7
|
import { guessLanguage } from "../detect.ts";
|
|
8
8
|
import { collectTargets } from "../files.ts";
|
|
9
9
|
import { loadRules } from "../rule-load.ts";
|
|
10
|
+
import { counted } from "../render/plural.ts";
|
|
10
11
|
import { CHECKS_FILE, loadChecks, type UserCheck } from "../checks.ts";
|
|
11
|
-
import { CACHE_DIR, credentialHint, describeFailure, hasCredentials } from "../judge.ts";
|
|
12
|
+
import { CACHE_DIR, credentialHint, describeFailure, hasCredentials, type JudgeOptions } from "../judge.ts";
|
|
12
13
|
import { ENV_FILE, loadEnvFile } from "../env.ts";
|
|
13
14
|
import { planSemantic, runSemantic } from "../run-semantic.ts";
|
|
14
|
-
import {
|
|
15
|
+
import { machineBanner, renderPlan, renderSemantic } from "../render/semantic.ts";
|
|
15
16
|
import type { Config } from "../config/load.ts";
|
|
16
17
|
import type { Finding, RuleDefinition } from "../plugin.ts";
|
|
17
|
-
import type { Failure } from "../backends/types.ts";
|
|
18
|
+
import type { BackendName, Failure } from "../backends/types.ts";
|
|
18
19
|
import { profileFor } from "../profile/for-file.ts";
|
|
20
|
+
import { CLI_TEXT } from "../cli-text.ts";
|
|
21
|
+
import { sharedLanguage, type Texts, type UiLanguage } from "../ui.ts";
|
|
22
|
+
|
|
23
|
+
const TEXT: Texts<{
|
|
24
|
+
readonly total: (total: number) => string;
|
|
25
|
+
readonly sentTo: (backend: string, model: string, found: boolean) => string;
|
|
26
|
+
readonly envRead: (path: string) => string;
|
|
27
|
+
readonly envWhere: (found: string | undefined) => string;
|
|
28
|
+
readonly notRun: (why: string, what: string, where: string) => readonly string[];
|
|
29
|
+
readonly noCredentials: (backend: BackendName) => string;
|
|
30
|
+
readonly refused: (backend: BackendName, status: string) => string;
|
|
31
|
+
readonly checkKey: string;
|
|
32
|
+
readonly quota: (backend: BackendName, status: string) => string;
|
|
33
|
+
readonly returned: (backend: BackendName, status: string) => string;
|
|
34
|
+
readonly noCause: string;
|
|
35
|
+
readonly tally: (machine: number, ai: number) => string;
|
|
36
|
+
readonly untouched: string;
|
|
37
|
+
}> = {
|
|
38
|
+
ja: {
|
|
39
|
+
total: (total) => ` 合計 ${total} 箇所を送ります。--dry-run なので API は呼んでいません。`,
|
|
40
|
+
sentTo: (backend, model, found) => ` 送り先: ${backend} / ${model}(認証${found ? "あり" : "なし"})`,
|
|
41
|
+
envRead: (path) => ` ${path} を読みました。`,
|
|
42
|
+
envWhere: (found) => (found === undefined ? `${ENV_FILE} に書いても読みます(いまは ${ENV_FILE} がありません)` : `${found} は読みました`),
|
|
43
|
+
notRun: (why, what, where) => [` 意味を読む検査は動かしていません。${why}`, ` ${what}`, ` ${where}。`, " 機械による判定はすべて動いています。"],
|
|
44
|
+
noCredentials: (backend) => `${backend} の認証情報がありません。`,
|
|
45
|
+
refused: (backend, status) => `${backend} が鍵を受け付けませんでした(${status})。`,
|
|
46
|
+
checkKey: "鍵が正しいか、その provider のものかを確かめてください。",
|
|
47
|
+
quota: (backend, status) => `${backend} の残高か上限に達しました(${status})。`,
|
|
48
|
+
returned: (backend, status) => `${backend} が ${status} を返しました。`,
|
|
49
|
+
noCause: "原因は返ってきませんでした",
|
|
50
|
+
tally: (machine, ai) => ` 機械 ${machine} 件 / AI ${ai} 件`,
|
|
51
|
+
untouched: " 文章は書き換えていません。直すのは書いた人です。",
|
|
52
|
+
},
|
|
53
|
+
en: {
|
|
54
|
+
total: (total) => ` ${counted(total, "passage")} in all would be sent. With --dry-run the API was not called.`,
|
|
55
|
+
sentTo: (backend, model, found) => ` Sent to: ${backend} / ${model} (${found ? "credentials found" : "no credentials"})`,
|
|
56
|
+
envRead: (path) => ` Read ${path}.`,
|
|
57
|
+
envWhere: (found) => (found === undefined ? `A key written in ${ENV_FILE} is read too (there is no ${ENV_FILE} now)` : `${found} was read`),
|
|
58
|
+
notRun: (why, what, where) => [` The checks that read meaning did not run. ${why}`, ` ${what}`, ` ${where}.`, " Every machine check ran."],
|
|
59
|
+
noCredentials: (backend) => `There are no credentials for ${backend}.`,
|
|
60
|
+
refused: (backend, status) => `${backend} did not accept the key (${status}).`,
|
|
61
|
+
checkKey: "Check that the key is right and that it is for that provider.",
|
|
62
|
+
quota: (backend, status) => `${backend} has run out of balance or hit a limit (${status}).`,
|
|
63
|
+
returned: (backend, status) => `${backend} returned ${status}.`,
|
|
64
|
+
noCause: "No reason was given.",
|
|
65
|
+
tally: (machine, ai) => ` ${machine} by machine / ${ai} by AI`,
|
|
66
|
+
untouched: " The text was not changed. Fixing it is the writer's job.",
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const DIVIDER = "─".repeat(60);
|
|
19
71
|
|
|
20
72
|
export type TestContext = {
|
|
21
73
|
readonly config: Config;
|
|
22
74
|
readonly resolveGenre: (path: string, source: string, config: Config) => { genre: string; from: string };
|
|
23
|
-
readonly inspect: (
|
|
75
|
+
readonly inspect: (
|
|
76
|
+
path: string,
|
|
77
|
+
config: Config,
|
|
78
|
+
argv: readonly string[],
|
|
79
|
+
) => Promise<{ text: string; language: string; outcome: { findings: readonly Finding[] } }>;
|
|
80
|
+
/** The language of what is not about the documents: chaff.yaml's language, else the locale. */
|
|
81
|
+
readonly ui: UiLanguage;
|
|
82
|
+
/** The judges to call instead of the providers' own clients. Tests pass stand-ins; the command line never does. */
|
|
83
|
+
readonly clients?: Pick<JudgeOptions, "anthropicClient" | "openaiClient">;
|
|
24
84
|
};
|
|
25
85
|
|
|
26
86
|
type Judged = { path: string; outcome: Awaited<ReturnType<typeof runSemantic>>; rules: RuleDefinition[]; language: string };
|
|
@@ -37,8 +97,8 @@ const judgeAll = async (
|
|
|
37
97
|
const source = plainSource(await readFile(path, "utf8"));
|
|
38
98
|
const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
|
|
39
99
|
const adapter = await loadAdapter(language);
|
|
40
|
-
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
|
|
41
100
|
const { genre } = resolveGenre(path, source, config);
|
|
101
|
+
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
|
|
42
102
|
const rules = loadRules(language);
|
|
43
103
|
return { path, outcome: await runSemantic(doc, rules, checks, config.rules, genre, options), rules, language };
|
|
44
104
|
}),
|
|
@@ -50,43 +110,51 @@ const dryRun = async (
|
|
|
50
110
|
config: Config,
|
|
51
111
|
checks: readonly UserCheck[],
|
|
52
112
|
resolveGenre: TestContext["resolveGenre"],
|
|
53
|
-
envFile: string | undefined,
|
|
113
|
+
closing: { readonly envFile: string | undefined; readonly language: UiLanguage },
|
|
54
114
|
): Promise<void> => {
|
|
55
115
|
const plans = await Promise.all(
|
|
56
116
|
paths.map(async (path) => {
|
|
57
117
|
const source = plainSource(await readFile(path, "utf8"));
|
|
58
118
|
const language = applyByPath(config.byPath, config.baseDir, path).language ?? config.language ?? guessLanguage(source).language;
|
|
59
119
|
const adapter = await loadAdapter(language);
|
|
60
|
-
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language));
|
|
61
120
|
const { genre } = resolveGenre(path, source, config);
|
|
62
|
-
|
|
121
|
+
const doc = buildDocument(path, source, adapter, teamRules(config), profileFor(config, path, source, language, genre));
|
|
122
|
+
return { path, jobs: planSemantic(doc, loadRules(language), checks, config.rules, genre), sentences: doc.sentences.length, language };
|
|
63
123
|
}),
|
|
64
124
|
);
|
|
65
|
-
plans.forEach(({ path, jobs, sentences }) => console.log(renderPlan(path, jobs, sentences).join("\n")));
|
|
125
|
+
plans.forEach(({ path, jobs, sentences, language }) => console.log(renderPlan(path, jobs, sentences, language).join("\n")));
|
|
66
126
|
const total = plans.reduce((sum, plan) => sum + plan.jobs.reduce((count, job) => count + job.candidates.length, 0), 0);
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
console.log(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
);
|
|
127
|
+
const text = TEXT[closing.language];
|
|
128
|
+
const from = closing.envFile === undefined ? [] : [text.envRead(closing.envFile)];
|
|
129
|
+
console.log(["", DIVIDER, "", text.total(total), text.sentTo(config.aiBackend, config.aiModel, hasCredentials(config.aiBackend)), ...from, ""].join("\n"));
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* API が返した失敗を人の言葉にする。生のスタックトレースを人に見せない。
|
|
134
|
+
* 鍵はあるのに弾かれたときに「ありません」と言うと、設定済みの鍵をもう一度設定しに行かせる。
|
|
135
|
+
* 残高切れも同じで、「鍵を確かめてください」と言われても鍵は正しい。
|
|
136
|
+
*/
|
|
137
|
+
const whyFailed = (failure: Failure, backend: BackendName, language: UiLanguage): { why: string; what: string } => {
|
|
138
|
+
const text = TEXT[language];
|
|
139
|
+
const status = String(failure.status ?? "?");
|
|
140
|
+
if (failure.kind === "auth") return { why: text.refused(backend, status), what: text.checkKey };
|
|
141
|
+
const what = failure.message ?? text.noCause;
|
|
142
|
+
return failure.kind === "quota" ? { why: text.quota(backend, status), what } : { why: text.returned(backend, status), what };
|
|
80
143
|
};
|
|
81
144
|
|
|
145
|
+
/** Why the checks that read meaning did not run, and where a key would be read from. */
|
|
146
|
+
const notRunNotice = (why: string, what: string, envFile: string | undefined, language: UiLanguage): string =>
|
|
147
|
+
["", ...TEXT[language].notRun(why, what, TEXT[language].envWhere(envFile)), ""].join("\n");
|
|
148
|
+
|
|
82
149
|
/**
|
|
83
150
|
* 機械と AI を見出しで分けて出す。読む人が「これは揺れる判定か」を知らないと、
|
|
84
151
|
* AI の誤検知に振り回される。workflow spec §7、samples/README の軸 2。
|
|
152
|
+
* 見出しと指摘は文書の言語、締めは文書がそろっていればその言語、混ざっていれば ui の言語。
|
|
85
153
|
*/
|
|
86
154
|
export const runTest = async (targets: readonly string[], argv: readonly string[], context: TestContext): Promise<number> => {
|
|
87
155
|
const paths = collectTargets(targets.length > 0 ? targets : ["."]);
|
|
88
156
|
if (paths.length === 0) {
|
|
89
|
-
console.error(
|
|
157
|
+
console.error(CLI_TEXT[context.ui].noMarkdown(targets.join(", ")));
|
|
90
158
|
return 1;
|
|
91
159
|
}
|
|
92
160
|
const { config, resolveGenre, inspect } = context;
|
|
@@ -95,31 +163,20 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
|
|
|
95
163
|
const checks = loadChecks(join(process.cwd(), CHECKS_FILE));
|
|
96
164
|
const results = await Promise.all(paths.map((path) => inspect(path, config, argv)));
|
|
97
165
|
results.forEach((result) => {
|
|
98
|
-
console.log(result.text.replace(/\n/u, `\n${
|
|
166
|
+
console.log(result.text.replace(/\n/u, `\n${machineBanner(result.language).join("\n")}`));
|
|
99
167
|
});
|
|
168
|
+
const closing = sharedLanguage(
|
|
169
|
+
results.map((result) => result.language),
|
|
170
|
+
context.ui,
|
|
171
|
+
);
|
|
100
172
|
const machineOnly = results.some((result) => result.outcome.findings.some((finding) => finding.severity === "error")) ? 1 : 0;
|
|
101
|
-
const where = envFile === undefined ? `${ENV_FILE} に書いても読みます(いまは ${ENV_FILE} がありません)` : `${envFile} は読みました`;
|
|
102
|
-
const notRun = (why: string, what: string): string =>
|
|
103
|
-
["", ` 意味を読む検査は動かしていません。${why}`, ` ${what}`, ` ${where}。`, " 機械による判定はすべて動いています。", ""].join("\n");
|
|
104
|
-
const noCredentials = notRun(`${config.aiBackend} の認証情報がありません。`, credentialHint(config.aiBackend));
|
|
105
|
-
/**
|
|
106
|
-
* API が返した失敗を人の言葉にする。生のスタックトレースを人に見せない。
|
|
107
|
-
* 鍵はあるのに弾かれたときに「ありません」と言うと、設定済みの鍵をもう一度設定しに行かせる。
|
|
108
|
-
* 残高切れも同じで、「鍵を確かめてください」と言われても鍵は正しい。
|
|
109
|
-
*/
|
|
110
|
-
const failed = (failure: Failure): string => {
|
|
111
|
-
if (failure.kind === "auth")
|
|
112
|
-
return notRun(`${config.aiBackend} が鍵を受け付けませんでした(${failure.status ?? "?"})。`, "鍵が正しいか、その provider のものかを確かめてください。");
|
|
113
|
-
if (failure.kind === "quota") return notRun(`${config.aiBackend} の残高か上限に達しました(${failure.status ?? "?"})。`, failure.message);
|
|
114
|
-
return notRun(`${config.aiBackend} が ${failure.status ?? "?"} を返しました。`, failure.message);
|
|
115
|
-
};
|
|
116
173
|
if (argv.includes("--dry-run")) {
|
|
117
|
-
await dryRun(paths, config, checks, resolveGenre, envFile);
|
|
174
|
+
await dryRun(paths, config, checks, resolveGenre, { envFile, language: closing });
|
|
118
175
|
return machineOnly;
|
|
119
176
|
}
|
|
120
177
|
// 呼んでから落ちるのを待たない。認証が無いときの SDK の例外は型で判別できない。
|
|
121
178
|
if (!hasCredentials(config.aiBackend)) {
|
|
122
|
-
console.log(noCredentials);
|
|
179
|
+
console.log(notRunNotice(TEXT[closing].noCredentials(config.aiBackend), credentialHint(config.aiBackend, closing), envFile, closing));
|
|
123
180
|
return machineOnly;
|
|
124
181
|
}
|
|
125
182
|
const options = {
|
|
@@ -127,12 +184,14 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
|
|
|
127
184
|
backend: config.aiBackend,
|
|
128
185
|
cacheDir: join(process.cwd(), CACHE_DIR),
|
|
129
186
|
confidenceThreshold: config.confidenceThreshold,
|
|
187
|
+
...context.clients,
|
|
130
188
|
};
|
|
131
189
|
const semantic = await judgeAll(paths, config, checks, options, resolveGenre).catch((error: unknown) => {
|
|
132
190
|
// API が返した失敗だけを受け止める。ネットワーク断やコードの誤りは握りつぶさない。
|
|
133
191
|
const failure = describeFailure(config.aiBackend, error);
|
|
134
192
|
if (failure === undefined) throw error;
|
|
135
|
-
|
|
193
|
+
const { why, what } = whyFailed(failure, config.aiBackend, closing);
|
|
194
|
+
console.log(notRunNotice(why, what, envFile, closing));
|
|
136
195
|
return undefined;
|
|
137
196
|
});
|
|
138
197
|
if (semantic === undefined) return machineOnly;
|
|
@@ -142,8 +201,6 @@ export const runTest = async (targets: readonly string[], argv: readonly string[
|
|
|
142
201
|
});
|
|
143
202
|
const machine = results.flatMap((result) => result.outcome.findings);
|
|
144
203
|
const ai = semantic.flatMap((entry) => entry.outcome.findings);
|
|
145
|
-
console.log(
|
|
146
|
-
["", "─".repeat(60), "", ` 機械 ${machine.length} 件 / AI ${ai.length} 件`, " 文章は書き換えていません。直すのは書いた人です。", ""].join("\n"),
|
|
147
|
-
);
|
|
204
|
+
console.log(["", DIVIDER, "", TEXT[closing].tally(machine.length, ai.length), TEXT[closing].untouched, ""].join("\n"));
|
|
148
205
|
return [...machine, ...ai].some((finding) => finding.severity === "error") ? 1 : 0;
|
|
149
206
|
};
|
package/src/commands/tree.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFile } from "node:fs/promises";
|
|
|
3
3
|
import { loadAdapter } from "../adapter-load.ts";
|
|
4
4
|
import { applyByPath } from "../config/by-path.ts";
|
|
5
5
|
import { profileFor } from "../profile/for-file.ts";
|
|
6
|
+
import { resolveGenre } from "../resolve-genre.ts";
|
|
6
7
|
import { guessLanguage } from "../detect.ts";
|
|
7
8
|
import { isMarkdownPath } from "../structure/markdown-path.ts";
|
|
8
9
|
import { buildStructure } from "../structure/of.ts";
|
|
@@ -35,15 +36,15 @@ const TEXT: Texts<{
|
|
|
35
36
|
},
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
const treeText = (context: TreeContext): (typeof TEXT)["ja"] => TEXT[context.ui ?? "ja"];
|
|
39
40
|
|
|
40
|
-
const FORMATS = ["sexp", "json"];
|
|
41
|
+
const FORMATS: ReadonlySet<string> = new Set(["sexp", "json"]);
|
|
41
42
|
|
|
42
43
|
/** 値を取るフラグ。その次の引数は値で、対象のファイルではない。 */
|
|
43
|
-
const VALUED = ["--format", "--language"];
|
|
44
|
+
const VALUED: ReadonlySet<string> = new Set(["--format", "--language", "--genre"]);
|
|
44
45
|
|
|
45
46
|
export const treeTargets = (argv: readonly string[]): string[] =>
|
|
46
|
-
argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.
|
|
47
|
+
argv.slice(1).filter((arg, index, all) => !arg.startsWith("--") && !VALUED.has(all[index - 1] ?? ""));
|
|
47
48
|
|
|
48
49
|
export const readSource = async (path: string, context: TreeContext): Promise<string | undefined> => {
|
|
49
50
|
try {
|
|
@@ -81,7 +82,7 @@ export const readTree = async (path: string, argv: readonly string[], context: T
|
|
|
81
82
|
}
|
|
82
83
|
// 日本語は形態素で数量と日付を読む。解析器が無ければ単位の表で読むので、木は作れる。
|
|
83
84
|
await adapter.prepare?.({ pos: true });
|
|
84
|
-
const profile = profileFor(context.config, path, source, language);
|
|
85
|
+
const profile = profileFor(context.config, path, source, language, resolveGenre(path, source, context.config, context.flag(argv, "--genre")).genre);
|
|
85
86
|
return { source, tree: buildStructure({ path, source, language, markdown: isMarkdownPath(path), profile }, adapter.structure) };
|
|
86
87
|
};
|
|
87
88
|
|
|
@@ -108,7 +109,7 @@ export const inOrder = async (paths: readonly string[], each: (path: string) =>
|
|
|
108
109
|
*/
|
|
109
110
|
export const runTree = async (paths: readonly string[], argv: readonly string[], context: TreeContext): Promise<number> => {
|
|
110
111
|
const format = context.flag(argv, "--format") ?? "sexp";
|
|
111
|
-
if (paths.length === 0 || !FORMATS.
|
|
112
|
+
if (paths.length === 0 || !FORMATS.has(format)) {
|
|
112
113
|
console.error(treeText(context).usage);
|
|
113
114
|
return 1;
|
|
114
115
|
}
|
package/src/config/by-path.ts
CHANGED
|
@@ -44,12 +44,11 @@ export const matches = (glob: string, base: string, path: string): boolean => to
|
|
|
44
44
|
type PathSettings = { genre?: string; language?: string; profile?: string };
|
|
45
45
|
|
|
46
46
|
export const applyByPath = (rules: readonly PathRule[], base: string, path: string): PathSettings =>
|
|
47
|
-
rules
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
};
|
|
55
|
-
}, {});
|
|
47
|
+
rules
|
|
48
|
+
.filter((rule) => rule.files.some((glob) => matches(glob, base, path)))
|
|
49
|
+
.reduce<PathSettings>((settings, rule) => {
|
|
50
|
+
if (rule.genre !== undefined) settings.genre = rule.genre;
|
|
51
|
+
if (rule.language !== undefined) settings.language = rule.language;
|
|
52
|
+
if (rule.profile !== undefined) settings.profile = rule.profile;
|
|
53
|
+
return settings;
|
|
54
|
+
}, {});
|
package/src/config/load.ts
CHANGED
|
@@ -30,6 +30,10 @@ export type Config = {
|
|
|
30
30
|
readonly prefer: Readonly<Record<string, string>>;
|
|
31
31
|
/** この種類の文書に無いと困る見出し。チームが自分で決める。 */
|
|
32
32
|
readonly requiredSections: readonly string[];
|
|
33
|
+
/** チームの固有名詞(組織名・製品名)。1 つの名前として読み、漢字の連なりや繰り返しに数えない。 */
|
|
34
|
+
readonly names: readonly string[];
|
|
35
|
+
/** names に書かれていたが名前として読めなかった値。黙って捨てると、並べたつもりの名前が効いていないことに気づけない。 */
|
|
36
|
+
readonly unreadableNames: readonly string[];
|
|
33
37
|
/** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
|
|
34
38
|
readonly byPath: readonly PathRule[];
|
|
35
39
|
readonly baseDir: string;
|
|
@@ -54,6 +58,8 @@ export const EMPTY: Config = {
|
|
|
54
58
|
jargon: [],
|
|
55
59
|
prefer: {},
|
|
56
60
|
requiredSections: [],
|
|
61
|
+
names: [],
|
|
62
|
+
unreadableNames: [],
|
|
57
63
|
byPath: [],
|
|
58
64
|
baseDir: process.cwd(),
|
|
59
65
|
};
|
|
@@ -114,6 +120,21 @@ const preferOf = (raw: unknown): Record<string, string> =>
|
|
|
114
120
|
)
|
|
115
121
|
: {};
|
|
116
122
|
|
|
123
|
+
const isNameEntry = (value: unknown): value is string | number => typeof value === "string" || typeof value === "number";
|
|
124
|
+
|
|
125
|
+
const printed = (value: unknown): string => JSON.stringify(value) ?? String(value);
|
|
126
|
+
|
|
127
|
+
/** 名前は文字列か数(2025 のような名前)の並び。並びでない値と、並びの中の名前でない項目は読めないものとして返す。 */
|
|
128
|
+
const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable: string[] } => {
|
|
129
|
+
if (raw === undefined || raw === null) return { names: [], unreadable: [] };
|
|
130
|
+
if (!Array.isArray(raw)) return { names: [], unreadable: [printed(raw)] };
|
|
131
|
+
const entries: unknown[] = raw;
|
|
132
|
+
return {
|
|
133
|
+
names: wordsOf(entries.filter(isNameEntry)),
|
|
134
|
+
unreadable: entries.filter((entry) => !isNameEntry(entry)).map(printed),
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
117
138
|
const byPathOf = (raw: unknown): PathRule[] => (Array.isArray(raw) ? raw.map(toPathRule).filter((rule) => rule !== undefined) : []);
|
|
118
139
|
|
|
119
140
|
/** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
|
|
@@ -122,6 +143,7 @@ export const loadConfig = (path: string): Config => {
|
|
|
122
143
|
if (!isRecord(raw)) return { ...EMPTY, path };
|
|
123
144
|
const declared: unknown = raw["ai_backend"];
|
|
124
145
|
const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
|
|
146
|
+
const names = namesOf(raw["names"]);
|
|
125
147
|
return {
|
|
126
148
|
genre: str(raw["genre"]),
|
|
127
149
|
profile: str(raw["profile"]),
|
|
@@ -137,6 +159,8 @@ export const loadConfig = (path: string): Config => {
|
|
|
137
159
|
jargon: wordsOf(raw["jargon"]),
|
|
138
160
|
prefer: preferOf(raw["prefer"]),
|
|
139
161
|
requiredSections: wordsOf(raw["required_sections"]),
|
|
162
|
+
names: names.names,
|
|
163
|
+
unreadableNames: names.unreadable,
|
|
140
164
|
byPath: byPathOf(raw["by_path"]),
|
|
141
165
|
baseDir: dirname(path),
|
|
142
166
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Config } from "./load.ts";
|
|
2
|
+
import type { Texts, UiLanguage } from "../ui.ts";
|
|
3
|
+
|
|
4
|
+
const TEXT: Texts<{ readonly unreadable: (where: string, value: string) => string }> = {
|
|
5
|
+
ja: {
|
|
6
|
+
unreadable: (where, value) => `${where}: names の ${value} は名前として読めません。names には名前を 1 行に 1 つずつ並べてください(- 個人情報保護委員会)`,
|
|
7
|
+
},
|
|
8
|
+
en: {
|
|
9
|
+
unreadable: (where, value) => `${where}: cannot read ${value} under names as a name. Write names as a list, one name per line (- Bank of England)`,
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** chaff.yaml の names のうち、名前として読めなかったもの。読めないまま黙ると、書いた人は名前を並べたつもりで何も変わっていない。 */
|
|
14
|
+
export const nameProblems = (config: Pick<Config, "unreadableNames" | "path">, ui: UiLanguage = "ja"): string[] =>
|
|
15
|
+
config.unreadableNames.map((value) => TEXT[ui].unreadable(config.path ?? "chaff.yaml", value));
|
package/src/date-stamp.ts
CHANGED
|
@@ -21,7 +21,7 @@ const LABEL_EDGE = /[\s::]/u;
|
|
|
21
21
|
const datesOf = (candidate: StampCandidate): Span[] => candidate.dates.filter((date) => date.end > date.start);
|
|
22
22
|
|
|
23
23
|
const withoutDates = (candidate: StampCandidate): string => {
|
|
24
|
-
const ordered = datesOf(candidate).
|
|
24
|
+
const ordered = datesOf(candidate).toSorted((left, right) => left.start - right.start);
|
|
25
25
|
const gaps = ordered.reduce<{ readonly parts: readonly string[]; readonly from: number }>(
|
|
26
26
|
(acc, date) => ({ parts: [...acc.parts, candidate.text.slice(acc.from, date.start)], from: Math.max(acc.from, date.end) }),
|
|
27
27
|
{ parts: [], from: 0 },
|
|
@@ -6,6 +6,9 @@ import { isAddressRun } from "./place-run.ts";
|
|
|
6
6
|
import { isOneName } from "./name-run.ts";
|
|
7
7
|
import { proseText } from "../measure.ts";
|
|
8
8
|
import { parallelDotCount } from "./middle-dot.ts";
|
|
9
|
+
import { maskSpans } from "../mask.ts";
|
|
10
|
+
import { quotedSpans } from "../quoted-span.ts";
|
|
11
|
+
import { nameSpans } from "../team-names.ts";
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* 文字の並びを見る検出。漢字の連なりが住所か名前かだけは、形態素解析の固有名詞と数で決める。
|
|
@@ -13,8 +16,8 @@ import { parallelDotCount } from "./middle-dot.ts";
|
|
|
13
16
|
*/
|
|
14
17
|
const KANJI_RUN = /[一-鿿]+/gu;
|
|
15
18
|
|
|
16
|
-
/** 漢字の連なりと、その文書全体の座標での範囲。 */
|
|
17
|
-
type KanjiRun = { readonly text: string; readonly span: Span | undefined };
|
|
19
|
+
/** 漢字の連なりと、その文書全体の座標での範囲。quoted は鉤括弧の中身がまるごとこの連なりであること。 */
|
|
20
|
+
type KanjiRun = { readonly text: string; readonly span: Span | undefined; readonly quoted: boolean };
|
|
18
21
|
|
|
19
22
|
const SPACE = /\s/gu;
|
|
20
23
|
|
|
@@ -22,14 +25,22 @@ const SPACE = /\s/gu;
|
|
|
22
25
|
* 同じ連なりが文に二度出ても、それぞれの位置で読む。proseText は空白を詰めたり除いたりするだけなので、
|
|
23
26
|
* 空白でない k 文字目は元の文でも空白でない k 文字目。番地の覆いは長さを保つので、proseText の位置がそのまま使える。
|
|
24
27
|
*/
|
|
25
|
-
|
|
28
|
+
/** 番地と、チームが並べた名前を空白で覆う。名前は読み手が 1 語と知っているので、前後は別の連なりになる。 */
|
|
29
|
+
const maskedProse = (prose: string, profile: DocumentProfile | undefined, names: readonly string[]): string =>
|
|
30
|
+
maskSpans(maskAddresses(prose, profile), nameSpans(prose, names));
|
|
31
|
+
|
|
32
|
+
/** 鉤括弧の中身が、ちょうどこの範囲(文の先頭からの位置)か。括弧でくくった漢字だけの語は、書き手が 1 つの名前として示したもの。 */
|
|
33
|
+
const fillsQuote = (sentence: Sentence, start: number, end: number): boolean =>
|
|
34
|
+
quotedSpans(sentence.text).some((span) => span.start === start && span.end === end);
|
|
35
|
+
|
|
36
|
+
const runsOf = (sentence: Sentence, profile: DocumentProfile | undefined, names: readonly string[]): KanjiRun[] => {
|
|
26
37
|
const prose = proseText(sentence);
|
|
27
38
|
const printed = compacted(sentence.text, "char").offsets;
|
|
28
|
-
return [...
|
|
39
|
+
return [...maskedProse(prose, profile, names).matchAll(KANJI_RUN)].map((match) => {
|
|
29
40
|
const before = prose.slice(0, match.index).replace(SPACE, "").length;
|
|
30
41
|
const [start, last] = [printed[before], printed[before + match[0].length - 1]];
|
|
31
|
-
|
|
32
|
-
return { text: match[0], span };
|
|
42
|
+
if (start === undefined || last === undefined) return { text: match[0], span: undefined, quoted: false };
|
|
43
|
+
return { text: match[0], span: { start: sentence.span.start + start, end: sentence.span.start + last + 1 }, quoted: fillsQuote(sentence, start, last + 1) };
|
|
33
44
|
});
|
|
34
45
|
};
|
|
35
46
|
|
|
@@ -44,14 +55,15 @@ const isPlaceName = (tokens: readonly Token[], span: Span, topUnits: ReadonlySet
|
|
|
44
55
|
* 住所か名前かは、助数詞を外した残りで決める(1日日本銀行の「日本銀行」)。品詞が無ければ判定しない。
|
|
45
56
|
*/
|
|
46
57
|
const measuredRun = (tokens: readonly Token[] | undefined, run: KanjiRun, topUnits: ReadonlySet<string>): string => {
|
|
58
|
+
if (run.quoted) return "";
|
|
47
59
|
if (tokens === undefined || run.span === undefined) return run.text;
|
|
48
60
|
const counter = leadingCounter(tokens, run.span.start, run.text);
|
|
49
61
|
const rest = counter === undefined ? run.span : { start: counter.span.end, end: run.span.end };
|
|
50
62
|
return isPlaceName(tokens, rest, topUnits) ? "" : run.text.slice(counter?.surface.length ?? 0);
|
|
51
63
|
};
|
|
52
64
|
|
|
53
|
-
const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string
|
|
54
|
-
runsOf(sentence, profile)
|
|
65
|
+
const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined, topUnits: ReadonlySet<string>, names: readonly string[]): string =>
|
|
66
|
+
runsOf(sentence, profile, names)
|
|
55
67
|
.map((run) => measuredRun(sentence.tokens, run, topUnits))
|
|
56
68
|
.reduce((longest, run) => (run.length > longest.length ? run : longest), "");
|
|
57
69
|
|
|
@@ -62,7 +74,7 @@ const longestKanji = (sentence: Sentence, profile: DocumentProfile | undefined,
|
|
|
62
74
|
export const kanjiRun: Detector = (doc, options): Finding[] => {
|
|
63
75
|
const topUnits = new Set((doc.lexicons["prefecture-unit"] ?? []).map((entry) => entry.pattern));
|
|
64
76
|
return doc.sentences
|
|
65
|
-
.map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits) }))
|
|
77
|
+
.map((sentence) => ({ sentence, run: longestKanji(sentence, doc.profile, topUnits, doc.names ?? []) }))
|
|
66
78
|
.filter(({ run }) => run.length > options.limit)
|
|
67
79
|
.map(({ sentence, run }) => ({
|
|
68
80
|
rule: "max-kanji-continuous",
|
|
@@ -79,11 +79,11 @@ const firstWord = (sentence: Sentence): string =>
|
|
|
79
79
|
* 何が主張なのかが分からなくなる。
|
|
80
80
|
*/
|
|
81
81
|
export const conjunctionRun: Detector = (doc, options): Finding[] => {
|
|
82
|
-
const heads = (options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase());
|
|
82
|
+
const heads = new Set((options.lexicon ?? []).map((entry) => entry.pattern.toLowerCase()));
|
|
83
83
|
const runs = doc.sentences.filter(isClosed).reduce<Sentence[][]>(
|
|
84
84
|
(acc, sentence) => {
|
|
85
85
|
const last = acc.at(-1) ?? [];
|
|
86
|
-
if (!heads.
|
|
86
|
+
if (!heads.has(firstWord(sentence).toLowerCase())) return [...acc.slice(0, -1), last, []];
|
|
87
87
|
return [...acc.slice(0, -1), [...last, sentence]];
|
|
88
88
|
},
|
|
89
89
|
[[]],
|
|
@@ -153,7 +153,8 @@ const depthsOf = (tokens: readonly Token[]): number[] =>
|
|
|
153
153
|
{ depths: [], open: 0 },
|
|
154
154
|
).depths;
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
/** participleWords は、解析器が分詞と読まないが読点のあとで分詞の句を始める語(meaning)。 */
|
|
157
|
+
type Clause = { readonly tokens: readonly Token[]; readonly depths: readonly number[]; readonly participleWords: ReadonlySet<string> };
|
|
157
158
|
|
|
158
159
|
/** 冠詞や引用符を飛ばした、項目の頭の品詞。the parser と an exporter と samples を同じ形と見る。 */
|
|
159
160
|
const NOMINAL = new Set(["NOUN", "PROPN", "PRON", "NUM", "ADJ"]);
|
|
@@ -191,6 +192,21 @@ const itemsBefore = (clause: Clause, at: number): Token[][] => {
|
|
|
191
192
|
.filter((item) => item.length > 0);
|
|
192
193
|
};
|
|
193
194
|
|
|
195
|
+
/**
|
|
196
|
+
* and / or で始まる項目で、前の並びは閉じている(offering, giving, or receiving, directly or indirectly)。
|
|
197
|
+
* 次の並びはその項目から始まる。項目が動詞で始まれば、その目的語から(Develop, maintain, and track courses, materials and events)。
|
|
198
|
+
*/
|
|
199
|
+
const afterClosedList = (items: readonly Token[][]): Token[][] => {
|
|
200
|
+
const closed = items.findLastIndex((item) => LIST_CONJUNCTION.has(openingOf(item)?.surface.toLowerCase() ?? ""));
|
|
201
|
+
const closing = items[closed];
|
|
202
|
+
if (closing === undefined) return [...items];
|
|
203
|
+
const body = closing.slice(closing.findIndex((token) => LIST_CONJUNCTION.has(token.surface.toLowerCase())) + 1);
|
|
204
|
+
const head = body.findIndex(isContent);
|
|
205
|
+
const rest = VERBAL.has(body[head]?.pos ?? "") ? body.slice(head + 1) : body;
|
|
206
|
+
// 項目が残らなければ、後ろは新しい句の頭。導入の句(directly or indirectly,)を外す。
|
|
207
|
+
return rest.some(isContent) ? [rest, ...items.slice(closed + 1)] : [...withoutLead(items.slice(closed + 1))];
|
|
208
|
+
};
|
|
209
|
+
|
|
194
210
|
/** and / or の後ろの項目。次の読点か節の切れ目まで。 */
|
|
195
211
|
const itemAfter = (tokens: readonly Token[], at: number): Token[] => {
|
|
196
212
|
const end = tokens.findIndex((token, index) => index > at && (isComma(token) || CLAUSE_BREAK.has(token.surface)));
|
|
@@ -232,7 +248,9 @@ const joinAdjectives = (items: readonly Token[][]): Token[][] =>
|
|
|
232
248
|
items.reduce<Token[][]>((joined, item) => {
|
|
233
249
|
const previous = joined.at(-1);
|
|
234
250
|
const stacked = previous !== undefined && lastContent(previous)?.pos === "ADJ" && lastContent(item)?.pos !== "ADJ";
|
|
235
|
-
|
|
251
|
+
if (stacked) joined[joined.length - 1] = [...previous, ...item];
|
|
252
|
+
else joined.push(item);
|
|
253
|
+
return joined;
|
|
236
254
|
}, []);
|
|
237
255
|
|
|
238
256
|
/**
|
|
@@ -255,14 +273,93 @@ const hasVerb = (item: readonly Token[]): boolean => item.some((token) => VERBAL
|
|
|
255
273
|
|
|
256
274
|
const predicatesAgree = (items: readonly Token[][], shape: string): boolean => !VERBAL.has(shape) || items.every(hasVerb);
|
|
257
275
|
|
|
276
|
+
/** -ing 形(VerbForm=Ger)と過去分詞(Part)。filling the gaps と sentenced to prison は並べても並列にならない。 */
|
|
277
|
+
const VERB_FORMS = new Set(["Ger", "Part"]);
|
|
278
|
+
|
|
279
|
+
const verbFormOf = (token: Token): string | undefined => {
|
|
280
|
+
const form = token.features?.["VerbForm"];
|
|
281
|
+
return form !== undefined && VERB_FORMS.has(form) ? form : undefined;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** 項目の頭の分詞とその形。副詞は飛ばす(originally written)。引用符の中の語(‘modelling’)は語の例なので分詞と読まない。 */
|
|
285
|
+
const participleOpening = (item: readonly Token[], words: ReadonlySet<string>): { readonly at: number; readonly form: string } | undefined => {
|
|
286
|
+
const at = item.findIndex((token) => token.pos !== "PUNCT" && token.pos !== "ADV");
|
|
287
|
+
const opening = item[at];
|
|
288
|
+
if (opening === undefined) return undefined;
|
|
289
|
+
const form = words.has(opening.surface.toLowerCase()) ? "Ger" : verbFormOf(opening);
|
|
290
|
+
return form === undefined ? undefined : { at, form };
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const hasParticiple = (item: readonly Token[], form: string): boolean => item.some((token) => verbFormOf(token) === form);
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* 最初より後ろの項目が分詞で始まるとき。最初の項目にも同じ形の分詞があれば分詞の並び
|
|
297
|
+
* (are hiring engineers, buying laptops and renting desks / Tested, reviewed and approved)。後ろの項目が動詞で始まれば述語の並びで、
|
|
298
|
+
* 分詞は解析器の読み違い(the jury, cross-examined the witnesses, presented witnesses, and made)。そうでなければ分詞の句
|
|
299
|
+
* (from Latin, meaning ship or boat / the judge, sitting without a jury, and sentenced)で、並びがあるならその句の中から始まる。
|
|
300
|
+
* 分詞より前の項目と分詞そのものは項目ではない(programs, including the office, the desk and the team)。句の中の項目の形が揃わなければ
|
|
301
|
+
* 句は挿入で、並びは句の後ろだけ(relief, including an injunction, in any court ... and without / including before the date, to a
|
|
302
|
+
* Recipient ... “confidential”, “proprietary”, or the like)。
|
|
303
|
+
*/
|
|
304
|
+
/** 解析器は過去形と過去分詞(fixed)を見分けられない。動詞のある項目のあとの「過去分詞」の並びは、述語の並びと読む。 */
|
|
305
|
+
const pastPredicates = (first: readonly Token[], form: string, after: readonly Token[], words: ReadonlySet<string>): boolean =>
|
|
306
|
+
form === "Part" && participleOpening(after, words)?.form === "Part" && hasVerb(first);
|
|
307
|
+
|
|
308
|
+
const fromParticiplePhrase = (items: readonly Token[][], after: readonly Token[], words: ReadonlySet<string>): readonly Token[][] => {
|
|
309
|
+
const at = items.findIndex((item, index) => index > 0 && participleOpening(item, words) !== undefined);
|
|
310
|
+
const phrase = items[at];
|
|
311
|
+
const first = items[0];
|
|
312
|
+
const opening = phrase === undefined ? undefined : participleOpening(phrase, words);
|
|
313
|
+
if (first === undefined || phrase === undefined || opening === undefined) return items;
|
|
314
|
+
const rest = items.slice(at + 1);
|
|
315
|
+
const predicates = rest.some((item) => VERBAL.has(shapeOf(item) ?? "")) || pastPredicates(first, opening.form, after, words);
|
|
316
|
+
if (hasParticiple(first, opening.form) || predicates) return items;
|
|
317
|
+
const inPhrase = withoutLead([phrase.slice(opening.at + 1), ...rest].filter((item) => item.some(isContent)));
|
|
318
|
+
return new Set(inPhrase.map(shapeOf)).size > 1 ? rest : inPhrase;
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const APPOSITIVE_ANCHOR = new Set(["NOUN", "PROPN"]);
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 名詞のすぐ後ろの、読点で挟まれた X and Y は同格(two HPV types, HPV16 and HPV18, that account / Governments, both state and
|
|
325
|
+
* federal, spend)。項目が 2 つに見え、and の前に読点が無く、and の後ろの項目が読点で閉じ、後ろの 2 つに動詞が無いときに同格と読む。
|
|
326
|
+
* 2 つを結ぶ同格に Oxford comma は打たないので、and の前に読点があれば 3 つの並び(the subcommittee, full committee, and
|
|
327
|
+
* chamber levels, as well as)。読点の無い本当の並び(apples, pears and plums, then ...)はこの形と見分けられず、判定から外れる。
|
|
328
|
+
*/
|
|
329
|
+
const isAppositive = (clause: Clause, at: number, items: readonly Token[][], after: readonly Token[]): boolean => {
|
|
330
|
+
const [anchor, head] = items;
|
|
331
|
+
if (items.length !== 2 || anchor === undefined || head === undefined || commaBefore(clause.tokens, at)) return false;
|
|
332
|
+
const end = at + 1 + after.length;
|
|
333
|
+
// 閉じた読点のあとに and / or が続くなら、並びのあとに節をつないでいる(apples, pears and plums, and went home)。
|
|
334
|
+
const joinsClause = LIST_CONJUNCTION.has(clause.tokens[end + 1]?.surface.toLowerCase() ?? "");
|
|
335
|
+
const closed = clause.tokens[end]?.surface === "," && clause.depths[end] === clause.depths[at] && !joinsClause;
|
|
336
|
+
return closed && APPOSITIVE_ANCHOR.has(lastContent(anchor)?.pos ?? "") && !hasVerb(head) && !hasVerb(after);
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
/** 副詞を飛ばした頭の形。explain and justify と ultimately ensure は同じ動詞の項目。 */
|
|
340
|
+
const leadShape = (item: readonly Token[]): string | undefined => shapeOf(item.filter((token) => token.pos !== "ADV"));
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* and の後ろの項目に読点と、同じ形の項目を連れた and / or が続くなら、この and は項目の中にある(searches and seizures, and
|
|
344
|
+
* the Eighth's ban)。頭の形か、節かどうかが違えば別の節をつなぐ and で、この and までが並び(apples, pears and plums, and went home / and figs fell)。
|
|
345
|
+
*/
|
|
346
|
+
const listContinues = (clause: Clause, at: number, after: readonly Token[]): boolean => {
|
|
347
|
+
const next = at + 1 + after.length;
|
|
348
|
+
if (clause.tokens[next]?.surface !== "," || !LIST_CONJUNCTION.has(clause.tokens[next + 1]?.surface.toLowerCase() ?? "")) return false;
|
|
349
|
+
const following = itemAfter(clause.tokens, next + 1);
|
|
350
|
+
const shape = leadShape(following);
|
|
351
|
+
return shape !== undefined && shape === leadShape(after) && isClause(following) === isClause(after);
|
|
352
|
+
};
|
|
353
|
+
|
|
258
354
|
/**
|
|
259
355
|
* 読点で区切った項目が and / or の前に 2 つ以上あり、最後の項目と and の後ろが同じ形のときだけ並列。
|
|
260
356
|
* 導入の読点(After the review, the team fixed the bug and shipped it.)や、節をつなぐ読点
|
|
261
357
|
* (We tested it, and the team shipped it.)は、Oxford comma を打つかどうかの選択を見せない。
|
|
262
358
|
*/
|
|
263
359
|
const listAt = (clause: Clause, at: number): boolean | undefined => {
|
|
264
|
-
const items = withoutLead(joinAdjectives(itemsBefore(clause, at)));
|
|
265
360
|
const after = itemAfter(clause.tokens, at);
|
|
361
|
+
const items = fromParticiplePhrase(afterClosedList(withoutLead(joinAdjectives(itemsBefore(clause, at)))), after, clause.participleWords);
|
|
362
|
+
if (listContinues(clause, at, after) || isAppositive(clause, at, items, after)) return undefined;
|
|
266
363
|
const last = items.at(-1);
|
|
267
364
|
const shape = last === undefined ? undefined : shapeOf(last);
|
|
268
365
|
if (items.length < 2 || shape === undefined || shape !== shapeOf(after)) return undefined;
|
|
@@ -276,8 +373,8 @@ const listAt = (clause: Clause, at: number): boolean | undefined => {
|
|
|
276
373
|
* どちらが正しいかは決めない。スタイルガイドで割れる論点に立場を取ると rule ごと無視される。
|
|
277
374
|
* 見るのは 1 つの文書で揃っているかだけ。spec §12.3。
|
|
278
375
|
*/
|
|
279
|
-
const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
|
|
280
|
-
const clause = { tokens, depths: depthsOf(tokens) };
|
|
376
|
+
const oxfordIn = (tokens: readonly Token[], participleWords: ReadonlySet<string>): boolean | undefined => {
|
|
377
|
+
const clause = { tokens, depths: depthsOf(tokens), participleWords };
|
|
281
378
|
return tokens.reduce<boolean | undefined>(
|
|
282
379
|
(found, token, at) => found ?? (at > 0 && LIST_CONJUNCTION.has(token.surface.toLowerCase()) ? listAt(clause, at) : undefined),
|
|
283
380
|
undefined,
|
|
@@ -285,8 +382,9 @@ const oxfordIn = (tokens: readonly Token[]): boolean | undefined => {
|
|
|
285
382
|
};
|
|
286
383
|
|
|
287
384
|
export const oxfordComma: Detector = (doc, options): Finding[] => {
|
|
385
|
+
const participleWords = new Set((doc.lexicons["participle-word"] ?? []).map((entry) => entry.pattern.toLowerCase()));
|
|
288
386
|
const judged = doc.sentences.flatMap((sentence) => {
|
|
289
|
-
const oxford = oxfordIn(sentence.tokens ?? []);
|
|
387
|
+
const oxford = oxfordIn(sentence.tokens ?? [], participleWords);
|
|
290
388
|
return oxford === undefined ? [] : [{ sentence, oxford }];
|
|
291
389
|
});
|
|
292
390
|
const withComma = judged.filter((entry) => entry.oxford).length;
|