chaffjs 0.25.0 → 0.27.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 +179 -42
- package/dist/adapter-import.d.ts +2 -0
- package/dist/adapter-import.d.ts.map +1 -0
- package/dist/adapter-import.js +11 -0
- package/dist/adapter-import.js.map +1 -0
- package/dist/adapter-load.d.ts.map +1 -1
- package/dist/adapter-load.js +2 -9
- package/dist/adapter-load.js.map +1 -1
- package/dist/ai-score/human-shares.d.ts +4 -0
- package/dist/ai-score/human-shares.d.ts.map +1 -0
- package/dist/ai-score/human-shares.js +205 -0
- package/dist/ai-score/human-shares.js.map +1 -0
- package/dist/ai-score/lint-line.d.ts +4 -0
- package/dist/ai-score/lint-line.d.ts.map +1 -0
- package/dist/ai-score/lint-line.js +9 -0
- package/dist/ai-score/lint-line.js.map +1 -0
- package/dist/ai-score/of-document.d.ts +7 -0
- package/dist/ai-score/of-document.d.ts.map +1 -0
- package/dist/ai-score/of-document.js +40 -0
- package/dist/ai-score/of-document.js.map +1 -0
- package/dist/ai-score/render.d.ts +24 -0
- package/dist/ai-score/render.d.ts.map +1 -0
- package/dist/ai-score/render.js +100 -0
- package/dist/ai-score/render.js.map +1 -0
- package/dist/ai-score/score.d.ts +77 -0
- package/dist/ai-score/score.d.ts.map +1 -0
- package/dist/ai-score/score.js +52 -0
- package/dist/ai-score/score.js.map +1 -0
- package/dist/ai-score/signals.d.ts +19 -0
- package/dist/ai-score/signals.d.ts.map +1 -0
- package/dist/ai-score/signals.js +30 -0
- package/dist/ai-score/signals.js.map +1 -0
- package/dist/ai-score/text.d.ts +37 -0
- package/dist/ai-score/text.d.ts.map +1 -0
- package/dist/ai-score/text.js +59 -0
- package/dist/ai-score/text.js.map +1 -0
- package/dist/ai-score/view.d.ts +5 -0
- package/dist/ai-score/view.d.ts.map +1 -0
- package/dist/ai-score/view.js +19 -0
- package/dist/ai-score/view.js.map +1 -0
- package/dist/backends/anthropic.d.ts +0 -1
- package/dist/backends/anthropic.d.ts.map +1 -1
- package/dist/backends/anthropic.js +0 -1
- package/dist/backends/anthropic.js.map +1 -1
- package/dist/backends/default-model.d.ts +3 -0
- package/dist/backends/default-model.d.ts.map +1 -0
- package/dist/backends/default-model.js +5 -0
- package/dist/backends/default-model.js.map +1 -0
- package/dist/backends/openai.d.ts +0 -1
- package/dist/backends/openai.d.ts.map +1 -1
- package/dist/backends/openai.js +0 -1
- package/dist/backends/openai.js.map +1 -1
- package/dist/browser/adapter-import.d.ts +2 -0
- package/dist/browser/adapter-import.d.ts.map +1 -0
- package/dist/browser/adapter-import.js +17 -0
- package/dist/browser/adapter-import.js.map +1 -0
- package/dist/browser/bounded-match.d.ts +3 -0
- package/dist/browser/bounded-match.d.ts.map +1 -0
- package/dist/browser/bounded-match.js +4 -0
- package/dist/browser/bounded-match.js.map +1 -0
- package/dist/browser/check-text.d.ts +43 -0
- package/dist/browser/check-text.d.ts.map +1 -0
- package/dist/browser/check-text.js +50 -0
- package/dist/browser/check-text.js.map +1 -0
- package/dist/browser/detectors.d.ts +4 -0
- package/dist/browser/detectors.d.ts.map +1 -0
- package/dist/browser/detectors.js +22 -0
- package/dist/browser/detectors.js.map +1 -0
- package/dist/browser/package-files.d.ts +5 -0
- package/dist/browser/package-files.d.ts.map +1 -0
- package/dist/browser/package-files.js +8 -0
- package/dist/browser/package-files.js.map +1 -0
- package/dist/browser/path.d.ts +9 -0
- package/dist/browser/path.d.ts.map +1 -0
- package/dist/browser/path.js +70 -0
- package/dist/browser/path.js.map +1 -0
- package/dist/browser/working-dir.d.ts +2 -0
- package/dist/browser/working-dir.d.ts.map +1 -0
- package/dist/browser/working-dir.js +3 -0
- package/dist/browser/working-dir.js.map +1 -0
- package/dist/browser-files.d.ts +21 -0
- package/dist/browser-files.d.ts.map +1 -0
- package/dist/browser-files.js +65 -0
- package/dist/browser-files.js.map +1 -0
- package/dist/browser.d.ts +9 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +13 -0
- package/dist/browser.js.map +1 -0
- package/dist/capitals-with-small.d.ts +8 -0
- package/dist/capitals-with-small.d.ts.map +1 -0
- package/dist/capitals-with-small.js +20 -0
- package/dist/capitals-with-small.js.map +1 -0
- package/dist/cli-text.d.ts.map +1 -1
- package/dist/cli-text.js +2 -0
- package/dist/cli-text.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands/ai-score.d.ts +8 -0
- package/dist/commands/ai-score.d.ts.map +1 -0
- package/dist/commands/ai-score.js +59 -0
- package/dist/commands/ai-score.js.map +1 -0
- package/dist/commands/document-commands.d.ts.map +1 -1
- package/dist/commands/document-commands.js +2 -0
- package/dist/commands/document-commands.js.map +1 -1
- package/dist/config/by-path-problems.d.ts +5 -0
- package/dist/config/by-path-problems.d.ts.map +1 -0
- package/dist/config/by-path-problems.js +11 -0
- package/dist/config/by-path-problems.js.map +1 -0
- package/dist/config/load.d.ts +9 -2
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +21 -8
- package/dist/config/load.js.map +1 -1
- package/dist/config/read.d.ts +2 -0
- package/dist/config/read.d.ts.map +1 -1
- package/dist/config/read.js +4 -1
- package/dist/config/read.js.map +1 -1
- package/dist/config/warnings.d.ts +1 -1
- package/dist/config/warnings.d.ts.map +1 -1
- package/dist/config/warnings.js +3 -1
- package/dist/config/warnings.js.map +1 -1
- package/dist/config/working-dir.d.ts +2 -0
- package/dist/config/working-dir.d.ts.map +1 -0
- package/dist/config/working-dir.js +3 -0
- package/dist/config/working-dir.js.map +1 -0
- package/dist/custom/bounded-match.d.ts +1 -12
- package/dist/custom/bounded-match.d.ts.map +1 -1
- package/dist/custom/bounded-match.js +1 -11
- package/dist/custom/bounded-match.js.map +1 -1
- package/dist/custom/pattern-timeout.d.ts +13 -0
- package/dist/custom/pattern-timeout.d.ts.map +1 -0
- package/dist/custom/pattern-timeout.js +13 -0
- package/dist/custom/pattern-timeout.js.map +1 -0
- package/dist/derived/relative-weekdays.d.ts +16 -0
- package/dist/derived/relative-weekdays.d.ts.map +1 -0
- package/dist/derived/relative-weekdays.js +52 -0
- package/dist/derived/relative-weekdays.js.map +1 -0
- package/dist/detect.d.ts.map +1 -1
- package/dist/detect.js +4 -5
- package/dist/detect.js.map +1 -1
- package/dist/detectors/change-rate.d.ts +3 -0
- package/dist/detectors/change-rate.d.ts.map +1 -0
- package/dist/detectors/change-rate.js +102 -0
- package/dist/detectors/change-rate.js.map +1 -0
- package/dist/detectors/date-year.d.ts +10 -2
- package/dist/detectors/date-year.d.ts.map +1 -1
- package/dist/detectors/date-year.js +44 -8
- package/dist/detectors/date-year.js.map +1 -1
- package/dist/detectors/defined-term-form.d.ts +5 -0
- package/dist/detectors/defined-term-form.d.ts.map +1 -1
- package/dist/detectors/defined-term-form.js +22 -5
- package/dist/detectors/defined-term-form.js.map +1 -1
- package/dist/detectors/derived-numbers.d.ts.map +1 -1
- package/dist/detectors/derived-numbers.js +3 -7
- package/dist/detectors/derived-numbers.js.map +1 -1
- package/dist/detectors/document-date.d.ts +4 -0
- package/dist/detectors/document-date.d.ts.map +1 -0
- package/dist/detectors/document-date.js +12 -0
- package/dist/detectors/document-date.js.map +1 -0
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +60 -28
- package/dist/detectors/orthographic-variant.js.map +1 -1
- package/dist/detectors/party-role-name.d.ts +3 -0
- package/dist/detectors/party-role-name.d.ts.map +1 -0
- package/dist/detectors/party-role-name.js +35 -0
- package/dist/detectors/party-role-name.js.map +1 -0
- package/dist/detectors/quote-without-source.d.ts +17 -0
- package/dist/detectors/quote-without-source.d.ts.map +1 -0
- package/dist/detectors/quote-without-source.js +64 -0
- package/dist/detectors/quote-without-source.js.map +1 -0
- package/dist/detectors/reference-text.js +1 -1
- package/dist/detectors/reference-text.js.map +1 -1
- package/dist/detectors/registry/change-rate.d.ts +3 -0
- package/dist/detectors/registry/change-rate.d.ts.map +1 -0
- package/dist/detectors/registry/change-rate.js +3 -0
- package/dist/detectors/registry/change-rate.js.map +1 -0
- package/dist/detectors/registry/party-role-name.d.ts +3 -0
- package/dist/detectors/registry/party-role-name.d.ts.map +1 -0
- package/dist/detectors/registry/party-role-name.js +3 -0
- package/dist/detectors/registry/party-role-name.js.map +1 -0
- package/dist/detectors/registry/quote-without-source.d.ts +3 -0
- package/dist/detectors/registry/quote-without-source.d.ts.map +1 -0
- package/dist/detectors/registry/quote-without-source.js +3 -0
- package/dist/detectors/registry/quote-without-source.js.map +1 -0
- package/dist/detectors/registry/vague-deadline.d.ts +3 -0
- package/dist/detectors/registry/vague-deadline.d.ts.map +1 -0
- package/dist/detectors/registry/vague-deadline.js +3 -0
- package/dist/detectors/registry/vague-deadline.js.map +1 -0
- package/dist/detectors/registry/version-mismatch.d.ts +3 -0
- package/dist/detectors/registry/version-mismatch.d.ts.map +1 -0
- package/dist/detectors/registry/version-mismatch.js +3 -0
- package/dist/detectors/registry/version-mismatch.js.map +1 -0
- package/dist/detectors/relative-dates.d.ts.map +1 -1
- package/dist/detectors/relative-dates.js +3 -1
- package/dist/detectors/relative-dates.js.map +1 -1
- package/dist/detectors/relative-day-words.d.ts +4 -0
- package/dist/detectors/relative-day-words.d.ts.map +1 -0
- package/dist/detectors/relative-day-words.js +105 -0
- package/dist/detectors/relative-day-words.js.map +1 -0
- package/dist/detectors/run-in-label.d.ts +7 -0
- package/dist/detectors/run-in-label.d.ts.map +1 -0
- package/dist/detectors/run-in-label.js +53 -0
- package/dist/detectors/run-in-label.js.map +1 -0
- package/dist/detectors/sentence-counts.d.ts +3 -1
- package/dist/detectors/sentence-counts.d.ts.map +1 -1
- package/dist/detectors/sentence-counts.js +11 -4
- package/dist/detectors/sentence-counts.js.map +1 -1
- package/dist/detectors/sentence-ending.d.ts +1 -1
- package/dist/detectors/sentence-ending.d.ts.map +1 -1
- package/dist/detectors/sentence-ending.js +2 -1
- package/dist/detectors/sentence-ending.js.map +1 -1
- package/dist/detectors/structure-tree.d.ts +8 -2
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +86 -5
- package/dist/detectors/structure-tree.js.map +1 -1
- package/dist/detectors/unbalanced-bracket.d.ts +1 -1
- package/dist/detectors/unbalanced-bracket.d.ts.map +1 -1
- package/dist/detectors/unbalanced-bracket.js +23 -11
- package/dist/detectors/unbalanced-bracket.js.map +1 -1
- package/dist/detectors/vague-deadline.d.ts +25 -0
- package/dist/detectors/vague-deadline.d.ts.map +1 -0
- package/dist/detectors/vague-deadline.js +97 -0
- package/dist/detectors/vague-deadline.js.map +1 -0
- package/dist/detectors/version-mismatch.d.ts +3 -0
- package/dist/detectors/version-mismatch.d.ts.map +1 -0
- package/dist/detectors/version-mismatch.js +19 -0
- package/dist/detectors/version-mismatch.js.map +1 -0
- package/dist/furigana.d.ts +7 -0
- package/dist/furigana.d.ts.map +1 -0
- package/dist/furigana.js +16 -0
- package/dist/furigana.js.map +1 -0
- package/dist/genre-load.d.ts.map +1 -1
- package/dist/genre-load.js +5 -7
- package/dist/genre-load.js.map +1 -1
- package/dist/grade/ai-score.d.ts +16 -0
- package/dist/grade/ai-score.d.ts.map +1 -0
- package/dist/grade/ai-score.js +17 -0
- package/dist/grade/ai-score.js.map +1 -0
- package/dist/grade/finding.d.ts +6 -0
- package/dist/grade/finding.d.ts.map +1 -0
- package/dist/grade/finding.js +13 -0
- package/dist/grade/finding.js.map +1 -0
- package/dist/grade/grade-item.d.ts.map +1 -1
- package/dist/grade/grade-item.js +4 -11
- package/dist/grade/grade-item.js.map +1 -1
- package/dist/grade/render-variants.d.ts.map +1 -1
- package/dist/grade/render-variants.js +3 -0
- package/dist/grade/render-variants.js.map +1 -1
- package/dist/grade/render.d.ts.map +1 -1
- package/dist/grade/render.js +1 -0
- package/dist/grade/render.js.map +1 -1
- package/dist/grade/result.d.ts +3 -0
- package/dist/grade/result.d.ts.map +1 -1
- package/dist/grade/results-read.d.ts.map +1 -1
- package/dist/grade/results-read.js +13 -2
- package/dist/grade/results-read.js.map +1 -1
- package/dist/grade/summary.d.ts +3 -0
- package/dist/grade/summary.d.ts.map +1 -1
- package/dist/grade/summary.js +6 -0
- package/dist/grade/summary.js.map +1 -1
- package/dist/grade/text.d.ts +2 -0
- package/dist/grade/text.d.ts.map +1 -1
- package/dist/grade/text.js +2 -0
- package/dist/grade/text.js.map +1 -1
- package/dist/grade/variants-text.d.ts +3 -0
- package/dist/grade/variants-text.d.ts.map +1 -1
- package/dist/grade/variants-text.js +4 -0
- package/dist/grade/variants-text.js.map +1 -1
- package/dist/grade/variants.d.ts +3 -0
- package/dist/grade/variants.d.ts.map +1 -1
- package/dist/grade/variants.js +1 -0
- package/dist/grade/variants.js.map +1 -1
- package/dist/grade-api.d.ts +1 -0
- package/dist/grade-api.d.ts.map +1 -1
- package/dist/grade-api.js +2 -1
- package/dist/grade-api.js.map +1 -1
- package/dist/judge.d.ts +0 -1
- package/dist/judge.d.ts.map +1 -1
- package/dist/judge.js +0 -1
- package/dist/judge.js.map +1 -1
- package/dist/known-genres.js +2 -2
- package/dist/known-genres.js.map +1 -1
- package/dist/package-files.d.ts +6 -0
- package/dist/package-files.d.ts.map +1 -0
- package/dist/package-files.js +11 -0
- package/dist/package-files.js.map +1 -0
- package/dist/profile/load.d.ts.map +1 -1
- package/dist/profile/load.js +5 -6
- package/dist/profile/load.js.map +1 -1
- package/dist/rule-load.d.ts.map +1 -1
- package/dist/rule-load.js +5 -6
- package/dist/rule-load.js.map +1 -1
- package/dist/rule-offs.js +3 -3
- package/dist/rule-offs.js.map +1 -1
- package/dist/run.js +1 -1
- package/dist/run.js.map +1 -1
- package/dist/structure/change-rate.d.ts +33 -0
- package/dist/structure/change-rate.d.ts.map +1 -0
- package/dist/structure/change-rate.js +71 -0
- package/dist/structure/change-rate.js.map +1 -0
- package/dist/structure/date-range.d.ts +4 -0
- package/dist/structure/date-range.d.ts.map +1 -1
- package/dist/structure/date-range.js +23 -3
- package/dist/structure/date-range.js.map +1 -1
- package/dist/structure/issues.d.ts +17 -1
- package/dist/structure/issues.d.ts.map +1 -1
- package/dist/structure/issues.js +54 -5
- package/dist/structure/issues.js.map +1 -1
- package/dist/structure/list-numbering.d.ts +17 -0
- package/dist/structure/list-numbering.d.ts.map +1 -0
- package/dist/structure/list-numbering.js +27 -0
- package/dist/structure/list-numbering.js.map +1 -0
- package/dist/structure/party-role.d.ts +30 -0
- package/dist/structure/party-role.d.ts.map +1 -0
- package/dist/structure/party-role.js +58 -0
- package/dist/structure/party-role.js.map +1 -0
- package/dist/structure/percent-sum-prose.d.ts +7 -0
- package/dist/structure/percent-sum-prose.d.ts.map +1 -0
- package/dist/structure/percent-sum-prose.js +44 -0
- package/dist/structure/percent-sum-prose.js.map +1 -0
- package/dist/structure/prose-total.d.ts +28 -0
- package/dist/structure/prose-total.d.ts.map +1 -0
- package/dist/structure/prose-total.js +99 -0
- package/dist/structure/prose-total.js.map +1 -0
- package/dist/structure/reference-text.d.ts +5 -0
- package/dist/structure/reference-text.d.ts.map +1 -1
- package/dist/structure/reference-text.js +17 -4
- package/dist/structure/reference-text.js.map +1 -1
- package/dist/structure/term-prefix.d.ts +18 -0
- package/dist/structure/term-prefix.d.ts.map +1 -0
- package/dist/structure/term-prefix.js +40 -0
- package/dist/structure/term-prefix.js.map +1 -0
- package/dist/structure/total.d.ts +2 -0
- package/dist/structure/total.d.ts.map +1 -1
- package/dist/structure/total.js +2 -0
- package/dist/structure/total.js.map +1 -1
- package/dist/structure/version-mismatch.d.ts +23 -0
- package/dist/structure/version-mismatch.d.ts.map +1 -0
- package/dist/structure/version-mismatch.js +74 -0
- package/dist/structure/version-mismatch.js.map +1 -0
- package/dist/structure/weekday.d.ts +21 -2
- package/dist/structure/weekday.d.ts.map +1 -1
- package/dist/structure/weekday.js +70 -8
- package/dist/structure/weekday.js.map +1 -1
- package/dist/structure-shape/baseline-load.d.ts.map +1 -1
- package/dist/structure-shape/baseline-load.js +4 -5
- package/dist/structure-shape/baseline-load.js.map +1 -1
- package/dist/style-load.d.ts.map +1 -1
- package/dist/style-load.js +5 -6
- package/dist/style-load.js.map +1 -1
- package/package.json +24 -3
- package/rules/agreement-slip.yaml +2 -2
- package/rules/change-rate-mismatch.yaml +44 -0
- package/rules/cliche.yaml +28 -8
- package/rules/date-weekday-mismatch.yaml +4 -4
- package/rules/date-without-year.yaml +28 -3
- package/rules/defined-term-form.yaml +9 -6
- package/rules/demonstrative-opener-run.yaml +24 -7
- package/rules/max-sentence-length.yaml +1 -0
- package/rules/no-mixed-desumasu.yaml +2 -2
- package/rules/numbering-gap.yaml +4 -4
- package/rules/one-sentence-paragraph-run.yaml +1 -0
- package/rules/orthographic-variant.yaml +3 -3
- package/rules/party-role-name.yaml +94 -0
- package/rules/percent-sum-mismatch.yaml +4 -4
- package/rules/quote-without-source.yaml +61 -0
- package/rules/relative-date-mismatch.yaml +4 -4
- package/rules/total-mismatch.yaml +8 -8
- package/rules/undefined-acronym.yaml +1 -0
- package/rules/vague-deadline.yaml +95 -0
- package/rules/version-mismatch.yaml +77 -0
- package/rules/weasel-word.yaml +25 -7
- package/skills/chaff/SKILL.md +12 -2
- package/src/adapter-import.ts +3 -0
- package/src/adapter-load.ts +2 -1
- package/src/ai-score/human-shares.ts +207 -0
- package/src/ai-score/lint-line.ts +10 -0
- package/src/ai-score/of-document.ts +46 -0
- package/src/ai-score/render.ts +124 -0
- package/src/ai-score/score.ts +121 -0
- package/src/ai-score/signals.ts +39 -0
- package/src/ai-score/text.ts +103 -0
- package/src/ai-score/view.ts +21 -0
- package/src/backends/anthropic.ts +0 -2
- package/src/backends/default-model.ts +9 -0
- package/src/backends/openai.ts +0 -2
- package/src/browser/adapter-import.ts +19 -0
- package/src/browser/bounded-match.ts +9 -0
- package/src/browser/check-text.ts +106 -0
- package/src/browser/detectors.ts +40 -0
- package/src/browser/import-meta-glob.d.ts +5 -0
- package/src/browser/package-files.ts +13 -0
- package/src/browser/path.ts +73 -0
- package/src/browser/working-dir.ts +3 -0
- package/src/browser-files.ts +87 -0
- package/src/browser.ts +19 -0
- package/src/capitals-with-small.ts +23 -0
- package/src/cli-text.ts +2 -0
- package/src/cli.ts +4 -1
- package/src/commands/ai-score.ts +77 -0
- package/src/commands/document-commands.ts +2 -0
- package/src/config/by-path-problems.ts +17 -0
- package/src/config/load.ts +21 -8
- package/src/config/read.ts +5 -1
- package/src/config/warnings.ts +3 -1
- package/src/config/working-dir.ts +3 -0
- package/src/custom/bounded-match.ts +1 -16
- package/src/custom/pattern-timeout.ts +17 -0
- package/src/derived/relative-weekdays.ts +65 -0
- package/src/detect.ts +4 -5
- package/src/detectors/change-rate.ts +122 -0
- package/src/detectors/date-year.ts +51 -12
- package/src/detectors/defined-term-form.ts +28 -6
- package/src/detectors/derived-numbers.ts +3 -11
- package/src/detectors/document-date.ts +18 -0
- package/src/detectors/orthographic-variant.ts +67 -22
- package/src/detectors/party-role-name.ts +44 -0
- package/src/detectors/quote-without-source.ts +90 -0
- package/src/detectors/reference-text.ts +1 -1
- package/src/detectors/registry/change-rate.ts +4 -0
- package/src/detectors/registry/party-role-name.ts +4 -0
- package/src/detectors/registry/quote-without-source.ts +4 -0
- package/src/detectors/registry/vague-deadline.ts +4 -0
- package/src/detectors/registry/version-mismatch.ts +4 -0
- package/src/detectors/relative-dates.ts +3 -1
- package/src/detectors/relative-day-words.ts +126 -0
- package/src/detectors/run-in-label.ts +60 -0
- package/src/detectors/sentence-counts.ts +13 -4
- package/src/detectors/sentence-ending.ts +3 -2
- package/src/detectors/structure-tree.ts +104 -10
- package/src/detectors/unbalanced-bracket.ts +25 -10
- package/src/detectors/vague-deadline.ts +132 -0
- package/src/detectors/version-mismatch.ts +23 -0
- package/src/furigana.ts +20 -0
- package/src/genre-load.ts +5 -7
- package/src/grade/ai-score.ts +32 -0
- package/src/grade/finding.ts +16 -0
- package/src/grade/grade-item.ts +4 -13
- package/src/grade/render-variants.ts +5 -0
- package/src/grade/render.ts +1 -0
- package/src/grade/result.ts +3 -0
- package/src/grade/results-read.ts +17 -2
- package/src/grade/summary.ts +9 -0
- package/src/grade/text.ts +6 -0
- package/src/grade/variants-text.ts +7 -0
- package/src/grade/variants.ts +4 -0
- package/src/grade-api.ts +3 -1
- package/src/judge.ts +0 -2
- package/src/known-genres.ts +2 -2
- package/src/package-files.ts +15 -0
- package/src/profile/load.ts +5 -6
- package/src/rule-load.ts +5 -6
- package/src/rule-offs.ts +3 -3
- package/src/run.ts +1 -1
- package/src/structure/change-rate.ts +103 -0
- package/src/structure/date-range.ts +28 -3
- package/src/structure/issues.ts +61 -6
- package/src/structure/list-numbering.ts +34 -0
- package/src/structure/party-role.ts +94 -0
- package/src/structure/percent-sum-prose.ts +54 -0
- package/src/structure/prose-total.ts +135 -0
- package/src/structure/reference-text.ts +17 -4
- package/src/structure/term-prefix.ts +54 -0
- package/src/structure/total.ts +4 -0
- package/src/structure/version-mismatch.ts +103 -0
- package/src/structure/weekday.ts +83 -6
- package/src/structure-shape/baseline-load.ts +4 -5
- package/src/style-load.ts +5 -6
package/src/config/load.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname } from "node:path";
|
|
2
|
-
import {
|
|
2
|
+
import { workingDir } from "./working-dir.ts";
|
|
3
3
|
import { isLevel } from "../levels.ts";
|
|
4
|
-
import { defaultModel } from "../
|
|
4
|
+
import { defaultModel } from "../backends/default-model.ts";
|
|
5
5
|
import { isBackend, type BackendName } from "../backends/types.ts";
|
|
6
6
|
import type { Level } from "../plugin.ts";
|
|
7
7
|
import type { PathRule } from "./by-path.ts";
|
|
@@ -40,6 +40,8 @@ export type Config = {
|
|
|
40
40
|
readonly include?: readonly string[];
|
|
41
41
|
/** パスごとの上書き。設定ファイルのある場所からの相対で照合する。 */
|
|
42
42
|
readonly byPath: readonly PathRule[];
|
|
43
|
+
/** by_path entries that could not be read (a map instead of a list, an entry without files), as written. */
|
|
44
|
+
readonly unreadableByPath?: readonly string[];
|
|
43
45
|
readonly baseDir: string;
|
|
44
46
|
/** Options set on rules that take them: { rule id: { option: value } }, as written. rule-options.ts checks them against the rules. */
|
|
45
47
|
readonly options?: Readonly<Record<string, unknown>>;
|
|
@@ -95,7 +97,7 @@ export const EMPTY: Config = {
|
|
|
95
97
|
names: [],
|
|
96
98
|
unreadableNames: [],
|
|
97
99
|
byPath: [],
|
|
98
|
-
baseDir:
|
|
100
|
+
baseDir: workingDir(),
|
|
99
101
|
};
|
|
100
102
|
|
|
101
103
|
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -169,15 +171,25 @@ const namesOf = (raw: unknown): { readonly names: string[]; readonly unreadable:
|
|
|
169
171
|
};
|
|
170
172
|
};
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
/** by_path は files を持つ項目の並び。並びでない値と、読めない項目は読めないものとして返す。黙って捨てると、書いた人は効いたつもりでいる。 */
|
|
175
|
+
export const byPathOf = (raw: unknown): { readonly rules: PathRule[]; readonly unreadable: string[] } => {
|
|
176
|
+
if (raw === undefined || raw === null) return { rules: [], unreadable: [] };
|
|
177
|
+
if (!Array.isArray(raw)) return { rules: [], unreadable: [printed(raw)] };
|
|
178
|
+
const entries: unknown[] = raw;
|
|
179
|
+
const read = entries.map((entry) => ({ entry, rule: toPathRule(entry) }));
|
|
180
|
+
return {
|
|
181
|
+
rules: read.flatMap(({ rule }) => (rule === undefined ? [] : [rule])),
|
|
182
|
+
unreadable: read.filter(({ rule }) => rule === undefined).map(({ entry }) => printed(entry)),
|
|
183
|
+
};
|
|
184
|
+
};
|
|
173
185
|
|
|
174
|
-
/**
|
|
175
|
-
export const
|
|
176
|
-
const raw: unknown = readYamlFile(path);
|
|
186
|
+
/** 読んだ chaff.yaml の中身(raw)を設定にする。ファイルは読まない。path は raw を読んだ場所。 */
|
|
187
|
+
export const configOf = (raw: unknown, path: string): Config => {
|
|
177
188
|
if (!isRecord(raw)) return { ...EMPTY, path };
|
|
178
189
|
const declared: unknown = raw["ai_backend"];
|
|
179
190
|
const backend: BackendName = isBackend(declared) ? declared : DEFAULT_BACKEND;
|
|
180
191
|
const names = namesOf(raw["names"]);
|
|
192
|
+
const byPath = byPathOf(raw["by_path"]);
|
|
181
193
|
const options: unknown = raw["options"];
|
|
182
194
|
return {
|
|
183
195
|
genre: str(raw["genre"]),
|
|
@@ -197,7 +209,8 @@ export const loadConfig = (path: string): Config => {
|
|
|
197
209
|
names: names.names,
|
|
198
210
|
unreadableNames: names.unreadable,
|
|
199
211
|
include: globsOf(raw["include"]),
|
|
200
|
-
byPath:
|
|
212
|
+
byPath: byPath.rules,
|
|
213
|
+
unreadableByPath: byPath.unreadable,
|
|
201
214
|
baseDir: dirname(path),
|
|
202
215
|
options: isRecord(options) ? options : {},
|
|
203
216
|
unreadableOptions: options === undefined || options === null || isRecord(options) ? undefined : printed(options),
|
package/src/config/read.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { CONFIG_FILE, EMPTY,
|
|
3
|
+
import { CONFIG_FILE, EMPTY, configOf, type Config } from "./load.ts";
|
|
4
4
|
import { withStyle } from "./style.ts";
|
|
5
|
+
import { readYamlFile } from "./yaml-file.ts";
|
|
5
6
|
import { loadStyles } from "../style-load.ts";
|
|
6
7
|
|
|
8
|
+
/** 設定ファイルが無くても動く。あっても、既定から変えたものだけが書かれている。spec §18。 */
|
|
9
|
+
export const loadConfig = (path: string): Config => configOf(readYamlFile(path), path);
|
|
10
|
+
|
|
7
11
|
/** The chaff.yaml in dir with its style applied, or the defaults when there is none. */
|
|
8
12
|
export const readConfigIn = (dir: string): Config => (existsSync(join(dir, CONFIG_FILE)) ? withStyle(loadConfig(join(dir, CONFIG_FILE)), loadStyles()) : EMPTY);
|
package/src/config/warnings.ts
CHANGED
|
@@ -3,13 +3,15 @@ import type { RuleDefinition } from "../plugin.ts";
|
|
|
3
3
|
import type { UiLanguage } from "../ui.ts";
|
|
4
4
|
import { ruleProblems } from "./rule-problems.ts";
|
|
5
5
|
import { nameProblems } from "./name-problems.ts";
|
|
6
|
+
import { byPathProblems } from "./by-path-problems.ts";
|
|
6
7
|
import { configOptionProblems } from "./option-problems.ts";
|
|
7
8
|
import { configGuideProblems } from "../genre-guide/of-config.ts";
|
|
8
9
|
|
|
9
|
-
/** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names, options that do not apply, guides that cannot be read. Said before the results. */
|
|
10
|
+
/** Everything in chaff.yaml that has no effect: unknown rules or levels, unreadable names or by_path entries, options that do not apply, guides that cannot be read. Said before the results. */
|
|
10
11
|
export const settingWarnings = (config: Config, rules: readonly RuleDefinition[], ui: UiLanguage): string[] => [
|
|
11
12
|
...ruleProblems(config, rules, ui),
|
|
12
13
|
...nameProblems(config, ui),
|
|
14
|
+
...byPathProblems(config, ui),
|
|
13
15
|
...configOptionProblems(config, rules, ui),
|
|
14
16
|
...configGuideProblems(config, ui),
|
|
15
17
|
];
|
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
import { createContext, Script } from "node:vm";
|
|
2
|
+
import { PATTERN_BUDGET_MS, PatternTimeout, type TextMatches } from "./pattern-timeout.ts";
|
|
2
3
|
|
|
3
4
|
// A team's regular expression, run with a time limit. regex-safety.ts refuses the shapes known to run away before a pattern
|
|
4
5
|
// is used; no reading of the pattern can prove every shape safe, so this is the backstop. V8 can interrupt a regular
|
|
5
6
|
// expression only through vm's timeout, so the matching runs in a vm context.
|
|
6
7
|
|
|
7
|
-
/** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
|
|
8
|
-
export const PATTERN_BUDGET_MS = 1000;
|
|
9
|
-
|
|
10
|
-
/** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
|
|
11
|
-
export class PatternTimeout extends Error {
|
|
12
|
-
readonly budget_ms: number;
|
|
13
|
-
constructor(budgetMs: number) {
|
|
14
|
-
super(`the pattern did not finish within ${String(budgetMs)} ms`);
|
|
15
|
-
this.name = "PatternTimeout";
|
|
16
|
-
this.budget_ms = budgetMs;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Each text's matches: where it starts and what it matched. Empty matches are left out. */
|
|
21
|
-
export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
|
|
22
|
-
|
|
23
8
|
const SCRIPT = new Script(
|
|
24
9
|
`texts.map((text) => [...text.matchAll(new RegExp(source, flags))].filter((m) => m[0] !== "").map((m) => ({ index: m.index, text: m[0] })))`,
|
|
25
10
|
);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// What a team's pattern may take, and what it is when it takes longer: the same wherever the pattern runs.
|
|
2
|
+
|
|
3
|
+
/** How long one rule's pattern may take over one document. Far above what a sane pattern needs on any real document. */
|
|
4
|
+
export const PATTERN_BUDGET_MS = 1000;
|
|
5
|
+
|
|
6
|
+
/** A pattern that did not finish within its budget. run.ts lists the rule as not run, with this as the reason. */
|
|
7
|
+
export class PatternTimeout extends Error {
|
|
8
|
+
readonly budget_ms: number;
|
|
9
|
+
constructor(budgetMs: number) {
|
|
10
|
+
super(`the pattern did not finish within ${String(budgetMs)} ms`);
|
|
11
|
+
this.name = "PatternTimeout";
|
|
12
|
+
this.budget_ms = budgetMs;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Each text's matches: where it starts and what it matched. Empty matches are left out. */
|
|
17
|
+
export type TextMatches = readonly (readonly { readonly index: number; readonly text: string }[])[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// A date named by a weekday or a day relative to the document's date (「来週月曜(10月12日)」"next Monday (October 12)",
|
|
2
|
+
// 「明日(10月7日)」) that the absolute date beside it contradicts. Pure; the detector finds the words and the dates.
|
|
3
|
+
import type { Span } from "../plugin.ts";
|
|
4
|
+
|
|
5
|
+
/** 「来週月曜」"next Monday": weeks from the base's week (来週 is 1), and the weekday (Sunday is 0). */
|
|
6
|
+
export type WeekdayRelative = Span & { readonly kind: "weekday"; readonly weeks: number; readonly weekday: number };
|
|
7
|
+
/** 「明日」"tomorrow": days from the base. */
|
|
8
|
+
export type DayRelative = Span & { readonly kind: "day"; readonly days: number };
|
|
9
|
+
export type DayWord = WeekdayRelative | DayRelative;
|
|
10
|
+
|
|
11
|
+
const DAY_MS = 86_400_000;
|
|
12
|
+
const WEEK = 7;
|
|
13
|
+
const MONDAY = 1;
|
|
14
|
+
const FULL = /^(\d{4})-(\d{2})-(\d{2})$/u;
|
|
15
|
+
const MONTH_DAY = /^(\d{2})-(\d{2})$/u;
|
|
16
|
+
|
|
17
|
+
const timeOf = (value: string): number | undefined => {
|
|
18
|
+
const found = FULL.exec(value);
|
|
19
|
+
if (found === null) return undefined;
|
|
20
|
+
const [year, month, day] = [Number(found[1]), Number(found[2]), Number(found[3])];
|
|
21
|
+
const time = Date.UTC(year, month - 1, day);
|
|
22
|
+
const date = new Date(time);
|
|
23
|
+
return date.getUTCMonth() === month - 1 && date.getUTCDate() === day ? time : undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const valueOf = (time: number): string => new Date(time).toISOString().slice(0, "2026-10-06".length);
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The days a weekday word can mean. Weeks are counted from Monday or from Sunday, and "next Monday" can also mean the
|
|
30
|
+
* coming Monday: every reading is allowed, so only a date that fits none of them is wrong.
|
|
31
|
+
*/
|
|
32
|
+
const weekdayCandidates = (base: number, relative: WeekdayRelative): number[] => {
|
|
33
|
+
const baseDay = new Date(base).getUTCDay();
|
|
34
|
+
const fromMonday = base - ((baseDay - MONDAY + WEEK) % WEEK) * DAY_MS;
|
|
35
|
+
const fromSunday = base - baseDay * DAY_MS;
|
|
36
|
+
const inWeek = (weekStart: number, firstDay: number): number => weekStart + (relative.weeks * WEEK + ((relative.weekday - firstDay + WEEK) % WEEK)) * DAY_MS;
|
|
37
|
+
// "next Monday" and "this Monday" can mean the coming one; "last Monday" the one just past.
|
|
38
|
+
const coming = base + (((relative.weekday - baseDay + WEEK - 1) % WEEK) + 1) * DAY_MS;
|
|
39
|
+
const past = base - (((baseDay - relative.weekday + WEEK - 1) % WEEK) + 1) * DAY_MS;
|
|
40
|
+
const nearest = new Map([
|
|
41
|
+
[0, [coming]],
|
|
42
|
+
[1, [coming]],
|
|
43
|
+
[-1, [past]],
|
|
44
|
+
]).get(relative.weeks);
|
|
45
|
+
return [inWeek(fromMonday, MONDAY), inWeek(fromSunday, 0), ...(nearest ?? [])];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const candidatesOf = (base: number, relative: DayWord): number[] =>
|
|
49
|
+
relative.kind === "day" ? [base + relative.days * DAY_MS] : weekdayCandidates(base, relative);
|
|
50
|
+
|
|
51
|
+
/** Whether the written date (2026-10-12, or 10-12 without its year) is one of the days the word can mean. */
|
|
52
|
+
const fits = (written: string, candidates: readonly number[]): boolean => {
|
|
53
|
+
const monthDay = MONTH_DAY.exec(written);
|
|
54
|
+
return candidates.some((time) => (monthDay === null ? valueOf(time) === written : valueOf(time).slice("2026-".length) === written));
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** The day the word means, as the message shows it: the first reading, written as the date was (with or without the year). */
|
|
58
|
+
export const relativeDayMismatch = (base: string, relative: DayWord, written: string): string | undefined => {
|
|
59
|
+
const baseTime = timeOf(base);
|
|
60
|
+
if (baseTime === undefined || (!FULL.test(written) && !MONTH_DAY.test(written))) return undefined;
|
|
61
|
+
const candidates = candidatesOf(baseTime, relative);
|
|
62
|
+
const [first] = candidates;
|
|
63
|
+
if (first === undefined || fits(written, candidates)) return undefined;
|
|
64
|
+
return MONTH_DAY.test(written) ? valueOf(first).slice("2026-".length) : valueOf(first);
|
|
65
|
+
};
|
package/src/detect.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
1
|
+
import { join } from "node:path";
|
|
4
2
|
import { parse } from "yaml";
|
|
5
3
|
import { japaneseRatio, latinRatio } from "./detect-language.ts";
|
|
6
4
|
import { referenceListSpans } from "./reference-lists.ts";
|
|
7
5
|
import { withoutSpans } from "./soft-break.ts";
|
|
8
6
|
import { languageSample } from "./language-sample.ts";
|
|
7
|
+
import { PACKAGE_DIR, readText } from "./package-files.ts";
|
|
9
8
|
|
|
10
9
|
export type LanguageGuess = { readonly language: string; readonly confidence: number; readonly from: string };
|
|
11
10
|
|
|
@@ -15,12 +14,12 @@ export type LanguageGuess = { readonly language: string; readonly confidence: nu
|
|
|
15
14
|
*/
|
|
16
15
|
const JAPANESE_FLOOR = 0.15;
|
|
17
16
|
|
|
18
|
-
const HEADINGS_FILE = join(
|
|
17
|
+
const HEADINGS_FILE = join(PACKAGE_DIR, "reference-headings.yaml");
|
|
19
18
|
|
|
20
19
|
const isStringList = (value: unknown): value is string[] => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
21
20
|
|
|
22
21
|
const readHeadings = (): string[] => {
|
|
23
|
-
const raw: unknown = parse(
|
|
22
|
+
const raw: unknown = parse(readText(HEADINGS_FILE));
|
|
24
23
|
const headings: unknown = typeof raw === "object" && raw !== null && "headings" in raw ? raw.headings : undefined;
|
|
25
24
|
if (!isStringList(headings)) throw new Error(`${HEADINGS_FILE}: headings は文字列の並びであること`);
|
|
26
25
|
return headings;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// change-rate-mismatch: the reading half. Builds the figures, the rates and the words of structure/change-rate.ts from the
|
|
2
|
+
// tree's quantities, the numbers counted with a word ("1,200 companies"), and the lexicons change-direction, change-base,
|
|
3
|
+
// percent-unit and amount-multiplier.
|
|
4
|
+
import type { Detector, Finding, ProseDocument, Span, StructureNode } from "../plugin.ts";
|
|
5
|
+
import { inDocumentOrder } from "../structure/issues.ts";
|
|
6
|
+
import { changeRateMismatches, type BaseMark, type Direction, type Figure, type Rate } from "../structure/change-rate.ts";
|
|
7
|
+
import { escapeRegExp } from "../orthography.ts";
|
|
8
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
9
|
+
|
|
10
|
+
const patternsOf = (doc: ProseDocument, id: string): string[] => (doc.lexicons[id] ?? []).map((entry) => entry.pattern);
|
|
11
|
+
|
|
12
|
+
const DIGITS = /\d[\d,]*(?:\.(\d+))?/u;
|
|
13
|
+
const DECIMAL_BASE = 10;
|
|
14
|
+
const LATIN = /^[A-Za-z]/u;
|
|
15
|
+
|
|
16
|
+
/** The step a number is written to: its last digit, times the magnitude the tree folded into the value (12億 is 12 × 1億). */
|
|
17
|
+
const stepOf = (written: string, value: number): number | undefined => {
|
|
18
|
+
const found = DIGITS.exec(written.normalize("NFKC"));
|
|
19
|
+
const shown = found === null ? Number.NaN : Number(found[0].replaceAll(",", ""));
|
|
20
|
+
if (!Number.isFinite(shown) || shown === 0) return undefined;
|
|
21
|
+
return (value / shown) * DECIMAL_BASE ** -(found?.[1]?.length ?? 0);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** A word of magnitude right after the number ("$12 million"): the values compared must be written to the same one. */
|
|
25
|
+
const WORD_AFTER = /^\s+([A-Za-z]+)/u;
|
|
26
|
+
const WORDS_AFTER = /^\s+([A-Za-z]+)(?:\s+([A-Za-z]+))?/u;
|
|
27
|
+
|
|
28
|
+
const multiplierAfter = (source: string, end: number, multipliers: readonly string[]): string => {
|
|
29
|
+
const after = WORD_AFTER.exec(source.slice(end))?.[1]?.toLowerCase() ?? "";
|
|
30
|
+
return multipliers.includes(after) ? after : "";
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const quantityNodes = (tree: StructureNode): StructureNode[] => inDocumentOrder(tree).filter((node) => node.kind === "quantity" || node.kind === "date");
|
|
34
|
+
|
|
35
|
+
const ratesOf = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[]): Rate[] =>
|
|
36
|
+
nodes.flatMap((node) => {
|
|
37
|
+
const unit = String(node.attrs["unit"] ?? "");
|
|
38
|
+
if (node.kind !== "quantity" || !units.includes(unit)) return [];
|
|
39
|
+
const decimals = DIGITS.exec(doc.source.slice(node.span.start, node.span.end).normalize("NFKC"))?.[1]?.length ?? 0;
|
|
40
|
+
return [{ start: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), decimals }];
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const treeFigures = (doc: ProseDocument, nodes: readonly StructureNode[], units: readonly string[], multipliers: readonly string[]): Figure[] =>
|
|
44
|
+
nodes.flatMap((node) => {
|
|
45
|
+
const unit = String(node.attrs["unit"] ?? "");
|
|
46
|
+
const value = Number(node.attrs["value"]);
|
|
47
|
+
const step = stepOf(doc.source.slice(node.span.start, node.span.end), value);
|
|
48
|
+
if (node.kind !== "quantity" || units.includes(unit) || step === undefined) return [];
|
|
49
|
+
return [{ start: node.span.start, end: node.span.end, value, step, unit: `${unit}|${multiplierAfter(doc.source, node.span.end, multipliers)}` }];
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** A number with no unit the tree reads, counted with the word after it ("1,200 companies", "2.4 million users"). */
|
|
53
|
+
const COUNTED = /(?<![\w.,$€£¥])(\d{1,3}(?:,\d{3})+|\d+)(?:\.(\d+))?(?=\s+[A-Za-z])/gu;
|
|
54
|
+
/** A year written alone ("2024 revenue"): a point in time, not a value. */
|
|
55
|
+
const YEAR = /^(?:1[89]|2[01])\d{2}$/u;
|
|
56
|
+
|
|
57
|
+
/** The unit a counted number is compared in: the word after it, with a word of magnitude before that word ("million users"). */
|
|
58
|
+
const countedUnit = (source: string, end: number, multipliers: readonly string[]): string | undefined => {
|
|
59
|
+
const words = WORDS_AFTER.exec(source.slice(end));
|
|
60
|
+
const first = words?.[1]?.toLowerCase() ?? "";
|
|
61
|
+
const multiplier = multipliers.includes(first) ? first : "";
|
|
62
|
+
const noun = multiplier === "" ? first : (words?.[2]?.toLowerCase() ?? "");
|
|
63
|
+
return LATIN.test(noun) ? `${noun}|${multiplier}` : undefined;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const countedFigures = (doc: ProseDocument, taken: readonly Span[], multipliers: readonly string[]): Figure[] =>
|
|
67
|
+
[...doc.source.matchAll(COUNTED)].flatMap((match) => {
|
|
68
|
+
const start = match.index;
|
|
69
|
+
const end = start + match[0].length;
|
|
70
|
+
const unit = countedUnit(doc.source, end, multipliers);
|
|
71
|
+
if (unit === undefined || taken.some((span) => span.start < end && start < span.end) || YEAR.test(match[0])) return [];
|
|
72
|
+
const decimals = match[2] ?? "";
|
|
73
|
+
const value = Number([(match[1] ?? "").replaceAll(",", ""), decimals].filter((part) => part !== "").join("."));
|
|
74
|
+
return [{ start, end, value, step: DECIMAL_BASE ** -decimals.length, unit }];
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
/** Where a word of the lexicon stands in the source: a Latin word as a whole word, anything else as written. */
|
|
78
|
+
const spansOf = (source: string, word: string): Span[] => {
|
|
79
|
+
const pattern = LATIN.test(word) ? new RegExp(`\\b${escapeRegExp(word)}\\b`, "giu") : new RegExp(escapeRegExp(word), "gu");
|
|
80
|
+
return [...source.matchAll(pattern)].map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const SIGNS: Readonly<Record<string, 1 | -1>> = { rise: 1, fall: -1 };
|
|
84
|
+
|
|
85
|
+
/** Phrases that hold a direction word but give no direction ("up to 25%" is a ceiling): lexicon change-direction-not. */
|
|
86
|
+
const directionsOf = (doc: ProseDocument): Direction[] => {
|
|
87
|
+
const not = patternsOf(doc, "change-direction-not").flatMap((phrase) => spansOf(doc.source, phrase));
|
|
88
|
+
return (doc.lexicons["change-direction"] ?? []).flatMap((entry) => {
|
|
89
|
+
const sign = SIGNS[entry.group ?? ""];
|
|
90
|
+
const spans = spansOf(doc.source, entry.pattern).filter((span) => !not.some((phrase) => phrase.start <= span.start && span.end <= phrase.end));
|
|
91
|
+
return sign === undefined ? [] : spans.map((span) => ({ ...span, sign }));
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const marksOf = (doc: ProseDocument, lexicon: string): BaseMark[] =>
|
|
96
|
+
(doc.lexicons[lexicon] ?? []).flatMap((entry) =>
|
|
97
|
+
entry.position === undefined ? [] : spansOf(doc.source, entry.pattern).map((span) => ({ ...span, position: entry.position ?? "before" })),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
export const changeRate: Detector = (doc): Finding[] => {
|
|
101
|
+
if (doc.structure === undefined) return [];
|
|
102
|
+
const nodes = quantityNodes(doc.structure);
|
|
103
|
+
const units = patternsOf(doc, "percent-unit");
|
|
104
|
+
const multipliers = patternsOf(doc, "amount-multiplier").map((word) => word.toLowerCase());
|
|
105
|
+
const taken = nodes.map((node) => node.span);
|
|
106
|
+
const text = {
|
|
107
|
+
sentences: doc.sentences.map((sentence) => sentence.span),
|
|
108
|
+
figures: [...treeFigures(doc, nodes, units, multipliers), ...countedFigures(doc, taken, multipliers)],
|
|
109
|
+
rates: ratesOf(doc, nodes, units),
|
|
110
|
+
directions: directionsOf(doc),
|
|
111
|
+
marks: marksOf(doc, "change-base"),
|
|
112
|
+
targets: marksOf(doc, "change-target"),
|
|
113
|
+
};
|
|
114
|
+
return changeRateMismatches(text).map((issue) => ({
|
|
115
|
+
rule: "change-rate-mismatch",
|
|
116
|
+
severity: "warning",
|
|
117
|
+
line: 0,
|
|
118
|
+
column: 0,
|
|
119
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
120
|
+
values: { ...issue.values, offset: issue.offset },
|
|
121
|
+
}));
|
|
122
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// A date written without its year where the document's other dates carry one, or where the year cannot be read off
|
|
2
2
|
// because the document's dates cross the turn of a year. Pure; reads the date nodes of the structure tree.
|
|
3
|
-
import type { Detector, Finding, ProseDocument, StructureNode } from "../plugin.ts";
|
|
3
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument, StructureNode } from "../plugin.ts";
|
|
4
4
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
5
5
|
import { quoteAt } from "./structure-tree.ts";
|
|
6
6
|
|
|
@@ -9,11 +9,19 @@ export type DatePoint = { readonly offset: number; readonly end: number; readonl
|
|
|
9
9
|
|
|
10
10
|
export type YearGap = { readonly point: DatePoint; readonly kind: "minority" | "boundary"; readonly dated: number };
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The words beside a date that give its year in words (その年の, 同年, 毎年; "of each year"): the date has a year, or means
|
|
14
|
+
* every year. An entry stands before the date unless it says `position: after`.
|
|
15
|
+
*/
|
|
13
16
|
export const YEAR_REFERENCE_LEXICON = "year-reference";
|
|
14
17
|
|
|
18
|
+
/** A year word as the lexicon gives it. `before`: the word ends within the reach before the date ("annually, by"). */
|
|
19
|
+
export type YearWord = { readonly word: string; readonly position?: "before" | "after" | undefined };
|
|
20
|
+
|
|
15
21
|
const FULL = /^\d{4}-(\d{2})-\d{2}$/u;
|
|
16
22
|
const MONTH_DAY = /^(\d{2})-\d{2}$/u;
|
|
23
|
+
/** The month of any date with one: 2026-12-01, 2026-12 or 12-01. */
|
|
24
|
+
const ANY_MONTH = /^(?:\d{4}-(\d{2})(?:-\d{2})?|(\d{2})-\d{2})$/u;
|
|
17
25
|
|
|
18
26
|
/** How far before a date a year word may stand (「同年12月」「その年の12月」). */
|
|
19
27
|
const YEAR_WORD_REACH = 6;
|
|
@@ -33,24 +41,53 @@ const sideOf = (month: number | undefined): "end" | "start" | undefined => {
|
|
|
33
41
|
return YEAR_START.has(month) ? "start" : undefined;
|
|
34
42
|
};
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
const
|
|
44
|
+
const anyMonthOf = (value: string): number | undefined => {
|
|
45
|
+
const found = ANY_MONTH.exec(value);
|
|
46
|
+
const month = found?.[1] ?? found?.[2];
|
|
47
|
+
return month === undefined ? undefined : Number(month);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const opposite = (side: "end" | "start"): "end" | "start" => (side === "end" ? "start" : "end");
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A yearless date where the dates turn over the year: it sits at one turn, the date just before it at the other
|
|
54
|
+
* (12月10日 then 1月15日), and a dated one sits at that other turn too (2026年12月1日). A 2月15日 far from any 12月 is a
|
|
55
|
+
* day of the year, not a crossing.
|
|
56
|
+
*/
|
|
57
|
+
const crossesYear = (point: DatePoint, previous: DatePoint | undefined, datedSides: ReadonlySet<string>): boolean => {
|
|
38
58
|
const side = sideOf(monthOf(MONTH_DAY, point.value));
|
|
39
|
-
|
|
59
|
+
if (side === undefined || !datedSides.has(opposite(side))) return false;
|
|
60
|
+
return previous !== undefined && sideOf(anyMonthOf(previous.value)) === opposite(side);
|
|
40
61
|
};
|
|
41
62
|
|
|
42
63
|
const lineStart = (source: string, offset: number): number => source.lastIndexOf("\n", offset - 1) + 1;
|
|
43
64
|
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
return
|
|
65
|
+
const lineEnd = (source: string, offset: number): number => {
|
|
66
|
+
const end = source.indexOf("\n", offset);
|
|
67
|
+
return end === -1 ? source.length : end;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A word without a position is found anywhere in the reach before the date (「同年12月」). One that says `before` may end
|
|
72
|
+
* there and start further back ("annually, by June 30"); one that says `after` starts within the reach after the date
|
|
73
|
+
* ("June 30 of each year").
|
|
74
|
+
*/
|
|
75
|
+
const hasYearWord = (point: DatePoint, source: string, yearWords: readonly YearWord[]): boolean => {
|
|
76
|
+
const start = lineStart(source, point.offset);
|
|
77
|
+
const near = source.slice(Math.max(start, point.offset - YEAR_WORD_REACH), point.offset);
|
|
78
|
+
return yearWords.some(({ word, position }) => {
|
|
79
|
+
if (word === "") return false;
|
|
80
|
+
if (position === "after") return source.slice(point.end, Math.min(lineEnd(source, point.end), point.end + YEAR_WORD_REACH + word.length)).includes(word);
|
|
81
|
+
if (position === "before") return source.slice(Math.max(start, point.offset - YEAR_WORD_REACH - word.length), point.offset).includes(word);
|
|
82
|
+
return near.includes(word);
|
|
83
|
+
});
|
|
47
84
|
};
|
|
48
85
|
|
|
49
86
|
/**
|
|
50
87
|
* A yearless date whose year the text gives anyway: a year word just before it (「同年12月28日」「その年の12月31日」), or,
|
|
51
88
|
* earlier on the same line, a date with a year or with a year word (「2026年9月25日から10月7日まで」「その年の1月1日から12月31日まで」).
|
|
52
89
|
*/
|
|
53
|
-
const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly
|
|
90
|
+
const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: string, yearWords: readonly YearWord[]): boolean => {
|
|
54
91
|
const start = lineStart(source, point.offset);
|
|
55
92
|
return hasYearWord(point, source, yearWords) || anchors.some((other) => other.offset >= start && other.offset < point.offset);
|
|
56
93
|
};
|
|
@@ -60,22 +97,24 @@ const yearGiven = (point: DatePoint, anchors: readonly DatePoint[], source: stri
|
|
|
60
97
|
* one is the odd one out. Otherwise writing the year once is the document's style, and only a date across the turn of a
|
|
61
98
|
* year is reported. A date whose year the text gives in words or earlier on its line is never reported, nor counted.
|
|
62
99
|
*/
|
|
63
|
-
export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly
|
|
100
|
+
export const yearGaps = (points: readonly DatePoint[], source = "", yearWords: readonly YearWord[] = []): YearGap[] => {
|
|
64
101
|
const dated = points.filter((point) => FULL.test(point.value));
|
|
65
102
|
const anchors = [...dated, ...points.filter((point) => hasYearWord(point, source, yearWords))];
|
|
66
103
|
const yearless = points.filter((point) => MONTH_DAY.test(point.value) && !yearGiven(point, anchors, source, yearWords));
|
|
67
104
|
const minority = dated.length >= 2 && yearless.length < dated.length;
|
|
68
105
|
const datedSides = new Set(dated.flatMap((point) => sideOf(monthOf(FULL, point.value)) ?? []));
|
|
106
|
+
const previousOf = new Map(points.map((point, index) => [point, points[index - 1]]));
|
|
69
107
|
return yearless.flatMap((point): YearGap[] => {
|
|
70
108
|
if (minority) return [{ point, kind: "minority", dated: dated.length }];
|
|
71
|
-
return crossesYear(point, datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
|
|
109
|
+
return crossesYear(point, previousOf.get(point), datedSides) ? [{ point, kind: "boundary", dated: dated.length }] : [];
|
|
72
110
|
});
|
|
73
111
|
};
|
|
74
112
|
|
|
75
113
|
export const datePoints = (tree: StructureNode): DatePoint[] =>
|
|
76
114
|
inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ offset: node.span.start, end: node.span.end, value: String(node.attrs["value"]) }] : []));
|
|
77
115
|
|
|
78
|
-
const yearWordsOf = (doc: ProseDocument):
|
|
116
|
+
const yearWordsOf = (doc: ProseDocument): YearWord[] =>
|
|
117
|
+
(doc.lexicons[YEAR_REFERENCE_LEXICON] ?? []).map((entry: LexiconEntry) => ({ word: entry.pattern, position: entry.position }));
|
|
79
118
|
|
|
80
119
|
export const dateWithoutYear: Detector = (doc: ProseDocument): Finding[] =>
|
|
81
120
|
doc.structure === undefined
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// name is read from the tokens' parts of speech.
|
|
4
4
|
import { definedTerms, usesOf, type BodyText, type DefinedTerm } from "../structure/definition-use.ts";
|
|
5
5
|
import { escapeRegExp } from "../orthography.ts";
|
|
6
|
+
import { prefixGroupsOf, prefixVariants, variantUses } from "../structure/term-prefix.ts";
|
|
6
7
|
import { quoteAt } from "./structure-tree.ts";
|
|
7
8
|
import type { Detector, Finding, ProseDocument, Sentence, Span, Token } from "../plugin.ts";
|
|
8
9
|
|
|
@@ -91,10 +92,22 @@ const isVerbAt = (doc: ProseDocument, offset: number): boolean => {
|
|
|
91
92
|
return VERBAL.has(token.pos) || afterSubject;
|
|
92
93
|
};
|
|
93
94
|
|
|
95
|
+
/** The term written with another prefix of its group (本件業務 where 本業務 is defined), after the sentence that defines it. */
|
|
96
|
+
const prefixFindings = (doc: ProseDocument, texts: readonly BodyText[], term: DefinedTerm, definingEnd: number, defined: readonly string[]): Finding[] =>
|
|
97
|
+
prefixVariants(term.term, prefixGroupsOf(doc.lexicons["defined-term-prefix"] ?? [])).flatMap((variant) =>
|
|
98
|
+
defined.includes(variant)
|
|
99
|
+
? []
|
|
100
|
+
: variantUses(doc.source, texts, variant, definingEnd, defined).map((offset) =>
|
|
101
|
+
finding(doc, "defined-term-form", offset, "prefix", { term: term.term, written: variant, line: term.line }),
|
|
102
|
+
),
|
|
103
|
+
);
|
|
104
|
+
|
|
94
105
|
export const definedTermForm: Detector = (doc, options): Finding[] => {
|
|
95
106
|
if (doc.structure === undefined) return [];
|
|
96
107
|
const texts = bodyOf(doc);
|
|
97
|
-
|
|
108
|
+
const terms = definedTerms(doc.structure);
|
|
109
|
+
const defined = terms.map((term) => term.term);
|
|
110
|
+
return firstDefinitions(terms).flatMap(({ term, spans }) => {
|
|
98
111
|
const uses = usesOf(term.term, texts, spans, "loose");
|
|
99
112
|
const definingEnd = definingSentenceEnd(doc, term.span);
|
|
100
113
|
const quoted = quotedUses(doc.source, term.term, uses, definingEnd).map((offset) =>
|
|
@@ -104,7 +117,7 @@ export const definedTermForm: Detector = (doc, options): Finding[] => {
|
|
|
104
117
|
const lower = lowerCaseUses(doc.source, term.term, nounUses, definingEnd, options.limit).map((offset) =>
|
|
105
118
|
finding(doc, "defined-term-form", offset, "case", { term: term.term, line: term.line }),
|
|
106
119
|
);
|
|
107
|
-
return [...quoted, ...lower];
|
|
120
|
+
return [...quoted, ...lower, ...prefixFindings(doc, texts, term, definingEnd, defined)];
|
|
108
121
|
});
|
|
109
122
|
};
|
|
110
123
|
|
|
@@ -162,13 +175,21 @@ const isNameLine = (source: string, offset: number, name: string, term: string):
|
|
|
162
175
|
const end = source.indexOf("\n", offset);
|
|
163
176
|
const line = source.slice(start, end === -1 ? source.length : end).trim();
|
|
164
177
|
const SLACK = 12;
|
|
165
|
-
|
|
178
|
+
// The short name inside the long one ("Pinecone" in "Pinecone Software Ltd") is not the short name written beside it.
|
|
179
|
+
return line.replaceAll(name, "").includes(term) || [...line].length <= [...name].length + SLACK;
|
|
166
180
|
};
|
|
167
181
|
|
|
168
182
|
/** A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. */
|
|
169
183
|
const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
|
|
170
|
-
|
|
171
|
-
|
|
184
|
+
/**
|
|
185
|
+
* A name inside a longer word (東京大学 in 東京大学大学院, Acme in Acmeware) is part of another name. A joining word right after
|
|
186
|
+
* it (北浜精機株式会社及び乙) starts the next item of a list, and the name stands alone.
|
|
187
|
+
*/
|
|
188
|
+
export const standsAlone = (source: string, offset: number, name: string, joiners: readonly string[] = []): boolean => {
|
|
189
|
+
const after = offset + name.length;
|
|
190
|
+
const joined = joiners.some((joiner) => joiner !== "" && source.startsWith(joiner, after));
|
|
191
|
+
return !WORD_CHAR.test(source.charAt(offset - 1)) && (joined || !WORD_CHAR.test(source.charAt(after)));
|
|
192
|
+
};
|
|
172
193
|
|
|
173
194
|
/**
|
|
174
195
|
* Each use of a long name after the definition gave it a short name, outside a signature line. A name that is itself a
|
|
@@ -176,6 +197,7 @@ const standsAlone = (source: string, offset: number, name: string): boolean =>
|
|
|
176
197
|
*/
|
|
177
198
|
export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[]): RepeatedName[] => {
|
|
178
199
|
const defined = new Set(terms.map((term) => term.term));
|
|
200
|
+
const joiners = (doc.lexicons["enumeration-joiner"] ?? []).map((entry) => entry.pattern);
|
|
179
201
|
return terms
|
|
180
202
|
.filter((term) => term.inline)
|
|
181
203
|
.flatMap((term) => {
|
|
@@ -187,7 +209,7 @@ export const repeatedNames = (doc: ProseDocument, terms: readonly DefinedTerm[])
|
|
|
187
209
|
return doc.sentences
|
|
188
210
|
.filter((later) => later.span.start > term.span.end)
|
|
189
211
|
.flatMap((later) => [...later.text.matchAll(pattern)].map((match) => later.span.start + match.index))
|
|
190
|
-
.filter((offset) => standsAlone(doc.source, offset, name) && !isNameLine(doc.source, offset, name, term.term))
|
|
212
|
+
.filter((offset) => standsAlone(doc.source, offset, name, joiners) && !isNameLine(doc.source, offset, name, term.term))
|
|
191
213
|
.map((offset) => ({ offset, name, term: term.term, line: term.line }));
|
|
192
214
|
});
|
|
193
215
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Detector, Finding, ProseDocument, Span, StructureNode } from "../plugin.ts";
|
|
2
2
|
import { inDocumentOrder } from "../structure/issues.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { documentDateOf } from "./document-date.ts";
|
|
4
4
|
import { overlapsAny, spanIndex } from "../compare/spans.ts";
|
|
5
5
|
import { yearOf, type DurationUnit } from "../derived/date-arithmetic.ts";
|
|
6
6
|
import { durationMismatches, type DatedValue, type Duration } from "../derived/durations.ts";
|
|
@@ -142,16 +142,8 @@ const markedAges = (doc: ProseDocument): Elapsed[] =>
|
|
|
142
142
|
|
|
143
143
|
/** 文書の日付の年: 日付だけの段落か「更新日:」の段落(date-stamp)の、最初の日付。 */
|
|
144
144
|
const referenceYear = (doc: ProseDocument, tree: StructureNode): number | undefined => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
doc.source,
|
|
148
|
-
doc.paragraphs.map((paragraph) => paragraph.span),
|
|
149
|
-
dates,
|
|
150
|
-
);
|
|
151
|
-
const stamps = dateStampIndexes(candidates, patternsOf(doc, "date-stamp-label"));
|
|
152
|
-
const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
|
|
153
|
-
const date = paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end);
|
|
154
|
-
return date === undefined ? undefined : yearOf(date.value);
|
|
145
|
+
const date = documentDateOf(doc, tree);
|
|
146
|
+
return date === undefined ? undefined : yearOf(date);
|
|
155
147
|
};
|
|
156
148
|
|
|
157
149
|
/** 起点の年から数えた年数が、文書の日付と合わない。 */
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The document's own date: the first date of its date-stamp paragraph (a paragraph that is only a date, or 「更新日:」).
|
|
2
|
+
import type { ProseDocument, StructureNode } from "../plugin.ts";
|
|
3
|
+
import { inDocumentOrder } from "../structure/issues.ts";
|
|
4
|
+
import { dateStampIndexes, stampCandidates } from "../date-stamp.ts";
|
|
5
|
+
|
|
6
|
+
/** The date of the document's date stamp as the tree reads it ("2026-10-06", or "2026" for a year alone), when it has one. */
|
|
7
|
+
export const documentDateOf = (doc: ProseDocument, tree: StructureNode): string | undefined => {
|
|
8
|
+
const dates = inDocumentOrder(tree).flatMap((node) => (node.kind === "date" ? [{ ...node.span, value: String(node.attrs["value"]) }] : []));
|
|
9
|
+
const candidates = stampCandidates(
|
|
10
|
+
doc.source,
|
|
11
|
+
doc.paragraphs.map((paragraph) => paragraph.span),
|
|
12
|
+
dates,
|
|
13
|
+
);
|
|
14
|
+
const labels = (doc.lexicons["date-stamp-label"] ?? []).map((entry) => entry.pattern);
|
|
15
|
+
const stamps = dateStampIndexes(candidates, labels);
|
|
16
|
+
const paragraph = doc.paragraphs.find((_, index) => stamps.has(index));
|
|
17
|
+
return paragraph === undefined ? undefined : dates.find((candidate) => candidate.start >= paragraph.span.start && candidate.end <= paragraph.span.end)?.value;
|
|
18
|
+
};
|