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
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import type { Detector, Finding, ProseDocument } from "../plugin.ts";
|
|
1
|
+
import type { Detector, Finding, ProseDocument, StructureNode } from "../plugin.ts";
|
|
2
2
|
import { danglingReferences, duplicateDefinitions, inDocumentOrder, type StructureIssue } from "../structure/issues.ts";
|
|
3
3
|
import { numberingBreaks } from "../structure/numbering.ts";
|
|
4
|
+
import { listNumberBreaks, writtenNumbers } from "../structure/list-numbering.ts";
|
|
4
5
|
import { weekdayMismatches } from "../structure/weekday.ts";
|
|
6
|
+
import { documentDateOf } from "./document-date.ts";
|
|
5
7
|
import { dateOrderBreaks } from "../structure/date-order.ts";
|
|
6
8
|
import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
9
|
+
import { proseTotalMismatches } from "../structure/prose-total.ts";
|
|
7
10
|
import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
|
|
8
11
|
import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
|
|
12
|
+
import { proseShareMismatches } from "../structure/percent-sum-prose.ts";
|
|
9
13
|
import { isQuotedAlone } from "../quoted-span.ts";
|
|
10
14
|
|
|
11
15
|
const QUOTE_LENGTH = 80;
|
|
@@ -34,7 +38,39 @@ const findingsOf =
|
|
|
34
38
|
}));
|
|
35
39
|
|
|
36
40
|
export const danglingReference: Detector = findingsOf("dangling-reference", danglingReferences);
|
|
37
|
-
|
|
41
|
+
const structureNumberingGap: Detector = findingsOf("numbering-gap", numberingBreaks);
|
|
42
|
+
|
|
43
|
+
const lineStartAt = (source: string, offset: number): number => source.lastIndexOf("\n", offset - 1) + 1;
|
|
44
|
+
|
|
45
|
+
/** The written numbers of the document's Markdown ordered lists that skip or repeat (structure/list-numbering.ts). */
|
|
46
|
+
const QUOTED_LINE = /^[ \t]*>/u;
|
|
47
|
+
|
|
48
|
+
/** 引用の中の箇条書きは、ほかの文書の例で、この文書の番号ではない。 */
|
|
49
|
+
const isQuotedList = (source: string, start: number): boolean => QUOTED_LINE.test(source.slice(lineStartAt(source, start), start));
|
|
50
|
+
|
|
51
|
+
const listNumberingGaps = (doc: ProseDocument): Finding[] =>
|
|
52
|
+
doc.lists
|
|
53
|
+
.filter((list) => !isQuotedList(doc.source, list.span.start))
|
|
54
|
+
.flatMap((list) => listNumberBreaks(writtenNumbers(doc.source, list.itemSpans) ?? []))
|
|
55
|
+
.map((issue) => ({
|
|
56
|
+
rule: "numbering-gap",
|
|
57
|
+
severity: "error",
|
|
58
|
+
line: 0,
|
|
59
|
+
column: 0,
|
|
60
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
61
|
+
values: { ...issue.values, offset: issue.offset },
|
|
62
|
+
}));
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 番号の抜けと重なり: 番地の木の並び(条、項、号)と、Markdown の番号付きの箇条書きに書いた番号。条の中の「1.」「2.」は
|
|
66
|
+
* 両方に読まれるので、木が指した行の箇条書きは重ねて言わない。
|
|
67
|
+
*/
|
|
68
|
+
export const numberingGap: Detector = (doc, options): Finding[] => {
|
|
69
|
+
const fromTree = structureNumberingGap(doc, options);
|
|
70
|
+
const reported = new Set(fromTree.map((finding) => lineStartAt(doc.source, Number(finding.values["offset"]))));
|
|
71
|
+
const fromLists = listNumberingGaps(doc).filter((finding) => !reported.has(lineStartAt(doc.source, Number(finding.values["offset"]))));
|
|
72
|
+
return [...fromTree, ...fromLists].toSorted((left, right) => Number(left.values["offset"]) - Number(right.values["offset"]));
|
|
73
|
+
};
|
|
38
74
|
export const duplicateDefinition: Detector = findingsOf("duplicate-definition", duplicateDefinitions);
|
|
39
75
|
|
|
40
76
|
/** 曜日の名前は言語パッケージの語彙表(weekday、日曜日から順)から取る。無ければ番号のまま。 */
|
|
@@ -58,10 +94,33 @@ const quotedDates = (doc: ProseDocument, from: DatedSpan | undefined, until: Dat
|
|
|
58
94
|
|
|
59
95
|
const dateAt = (dates: readonly DatedSpan[], offset: number): number => dates.findIndex((date) => date.offset === offset);
|
|
60
96
|
|
|
97
|
+
/** A year written as figures in a heading: 2024年度, FY2024, "2024 events". */
|
|
98
|
+
const YEAR_FIGURES = /(?<![0-90-9])(?:1[89]|20)\d{2}(?![0-90-9])/u;
|
|
99
|
+
const ERA_YEAR_NUMBER = /^[ \t]?(?:[0-90-9]{1,2}|元)/u;
|
|
100
|
+
|
|
101
|
+
/** 見出しが年を名指すか: 数字の年(2024年度、FY2024)か、暦の元号の年(語彙表 calendar-era、令和6年度)。 */
|
|
102
|
+
const namesYear = (heading: string, eras: readonly string[]): boolean =>
|
|
103
|
+
YEAR_FIGURES.test(heading) ||
|
|
104
|
+
eras.some((era) => {
|
|
105
|
+
const at = heading.indexOf(era);
|
|
106
|
+
return at !== -1 && ERA_YEAR_NUMBER.test(heading.slice(at + era.length));
|
|
107
|
+
});
|
|
108
|
+
|
|
61
109
|
export const dateWeekdayMismatch: Detector = (doc): Finding[] => {
|
|
62
110
|
if (doc.structure === undefined) return [];
|
|
63
111
|
const dates = datedSpans(doc.structure);
|
|
64
|
-
|
|
112
|
+
const sections = doc.sections.filter((section) => section.depth > 0);
|
|
113
|
+
const eras = (doc.lexicons["calendar-era"] ?? []).map((entry) => entry.pattern);
|
|
114
|
+
const context = {
|
|
115
|
+
stamp: documentDateOf(doc, doc.structure),
|
|
116
|
+
sectionStarts: sections.map((section) => section.span.start),
|
|
117
|
+
yearHeadings: sections.filter((section) => namesYear(section.heading, eras)).map((section) => section.span.start),
|
|
118
|
+
anchorable: (node: StructureNode): boolean => {
|
|
119
|
+
const date = dates[dateAt(dates, node.span.start)];
|
|
120
|
+
return !quotedDates(doc, date, date, WEEKDAY_WORDS);
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
return weekdayMismatches(doc.structure, context)
|
|
65
124
|
.filter((issue) => {
|
|
66
125
|
const date = dates[dateAt(dates, issue.offset)];
|
|
67
126
|
return !quotedDates(doc, date, date, WEEKDAY_WORDS);
|
|
@@ -104,15 +163,35 @@ const amountsOf = (tree: NonNullable<ProseDocument["structure"]>): Amount[] =>
|
|
|
104
163
|
node.kind === "quantity" ? [{ offset: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), unit: String(node.attrs["unit"]) }] : [],
|
|
105
164
|
);
|
|
106
165
|
|
|
107
|
-
|
|
166
|
+
const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
|
|
167
|
+
|
|
168
|
+
/** 合計の行と内訳の行、文の中の合計と内訳。同じ金額は一度だけ言う。 */
|
|
169
|
+
const totalIssues = (doc: ProseDocument, tree: NonNullable<ProseDocument["structure"]>): StructureIssue[] => {
|
|
170
|
+
const amounts = amountsOf(tree);
|
|
171
|
+
const lines = totalMismatches(doc.source, amounts, patternsOf(doc, "total-label"));
|
|
172
|
+
const words = {
|
|
173
|
+
totals: patternsOf(doc, "total-phrase"),
|
|
174
|
+
breakdowns: patternsOf(doc, "breakdown-phrase"),
|
|
175
|
+
discounts: patternsOf(doc, "discount-word"),
|
|
176
|
+
joiners: patternsOf(doc, "breakdown-gap-joiner"),
|
|
177
|
+
};
|
|
178
|
+
const prose = proseTotalMismatches(
|
|
179
|
+
doc.source,
|
|
180
|
+
doc.sentences.map((sentence) => sentence.span),
|
|
181
|
+
amounts,
|
|
182
|
+
words,
|
|
183
|
+
);
|
|
184
|
+
return [...lines, ...prose.filter((issue) => !lines.some((line) => line.offset === issue.offset))];
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 合計が内訳の和と合わない。表と箇条書きの合計の行(合計の語は total-label)と、文の中の合計(total-phrase、breakdown-phrase)。
|
|
189
|
+
* 語は言語パッケージの語彙表から取る。
|
|
190
|
+
*/
|
|
108
191
|
export const totalMismatch: Detector = (doc): Finding[] =>
|
|
109
192
|
doc.structure === undefined
|
|
110
193
|
? []
|
|
111
|
-
:
|
|
112
|
-
doc.source,
|
|
113
|
-
amountsOf(doc.structure),
|
|
114
|
-
(doc.lexicons["total-label"] ?? []).map((entry) => entry.pattern),
|
|
115
|
-
).map((issue) => ({
|
|
194
|
+
: totalIssues(doc, doc.structure).map((issue) => ({
|
|
116
195
|
rule: "total-mismatch",
|
|
117
196
|
severity: "error",
|
|
118
197
|
line: 0,
|
|
@@ -127,6 +206,7 @@ const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
|
127
206
|
closers: (doc.lexicons["range-closer"] ?? []).map((entry) => entry.pattern),
|
|
128
207
|
frames: (doc.lexicons["range-frame"] ?? []).flatMap((entry) => rangeFrameOf(entry.pattern) ?? []),
|
|
129
208
|
changes: (doc.lexicons["date-change-word"] ?? []).map((entry) => entry.pattern),
|
|
209
|
+
weekdays: (doc.lexicons["weekday"] ?? []).map((entry) => entry.pattern),
|
|
130
210
|
});
|
|
131
211
|
|
|
132
212
|
/** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
|
|
@@ -156,10 +236,24 @@ const shareWordsOf = (doc: ProseDocument): ShareWords => ({
|
|
|
156
236
|
});
|
|
157
237
|
|
|
158
238
|
/** 構成比や内訳の百分率の和が 100% にならない。割合の語と百分率の単位は言語パッケージの語彙表から取る。 */
|
|
239
|
+
/**
|
|
240
|
+
* 表や箇条書きの内訳と、一つの文に並べた内訳(「構成比は、Aが50%、Bが30%、Cが20%」)。箇条書きの項目の文は両方に読まれうるので、
|
|
241
|
+
* 表や箇条書きが指した行の文は重ねて言わない。
|
|
242
|
+
*/
|
|
243
|
+
const percentSumIssues = (doc: ProseDocument, tree: NonNullable<ProseDocument["structure"]>): StructureIssue[] => {
|
|
244
|
+
const amounts = amountsOf(tree);
|
|
245
|
+
const words = shareWordsOf(doc);
|
|
246
|
+
const runs = percentSumMismatches(doc.source, amounts, words);
|
|
247
|
+
const reported = new Set(runs.map((issue) => lineStartAt(doc.source, issue.offset)));
|
|
248
|
+
const sentences = doc.sentences.map((sentence) => sentence.span);
|
|
249
|
+
const prose = proseShareMismatches(doc.source, sentences, amounts, words).filter((issue) => !reported.has(lineStartAt(doc.source, issue.offset)));
|
|
250
|
+
return [...runs, ...prose].toSorted((left, right) => left.offset - right.offset);
|
|
251
|
+
};
|
|
252
|
+
|
|
159
253
|
export const percentSumMismatch: Detector = (doc): Finding[] =>
|
|
160
254
|
doc.structure === undefined
|
|
161
255
|
? []
|
|
162
|
-
:
|
|
256
|
+
: percentSumIssues(doc, doc.structure).map((issue) => ({
|
|
163
257
|
rule: "percent-sum-mismatch",
|
|
164
258
|
severity: "warning",
|
|
165
259
|
line: 0,
|
|
@@ -108,25 +108,40 @@ export const bracketProblems = (text: string): BracketProblem[] => {
|
|
|
108
108
|
return [...scan.problems, ...scan.open.map(unclosedOf)].toSorted((left, right) => left.offset - right.offset);
|
|
109
109
|
};
|
|
110
110
|
|
|
111
|
-
/** sorted
|
|
112
|
-
const
|
|
113
|
-
const
|
|
114
|
-
|
|
111
|
+
/** sorted(昇順・接しもしない)で、offset の後ろの最初の境目: offset を含む範囲の終わりか、次の範囲の始まり。 */
|
|
112
|
+
const borderAfter = (sorted: readonly Span[], offset: number): number => {
|
|
113
|
+
const next = sorted[firstEndingAfter(sorted, offset)];
|
|
114
|
+
if (next === undefined) return Number.POSITIVE_INFINITY;
|
|
115
|
+
return next.start <= offset ? next.end : next.start;
|
|
115
116
|
};
|
|
116
117
|
|
|
118
|
+
/** 覆いの中で URL そのものが終わる位置。ASCII でない字か、コードと字のまま見える範囲の境目(「`https://…`)」の「`」)で終わる。 */
|
|
119
|
+
const urlProperEnd = (written: string, start: number, readable: readonly Span[]): number => {
|
|
120
|
+
const nonAscii = written.search(/[^\p{ASCII}]/u);
|
|
121
|
+
return Math.min(nonAscii === -1 ? written.length : nonAscii, borderAfter(readable, start) - start, written.length);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/** sorted(昇順・重ならない)のうち range に重なるものを、range の中に切り詰めたもの。何万もの字の節があっても、URL ごとに全部をなめない。 */
|
|
125
|
+
const clippedTo = (sorted: readonly Span[], range: Span): Span[] =>
|
|
126
|
+
sorted
|
|
127
|
+
.slice(firstEndingAfter(sorted, range.start), firstEndingAfter(sorted, range.end) + 1)
|
|
128
|
+
.filter((span) => span.start < range.end)
|
|
129
|
+
.map((span) => ({ start: Math.max(span.start, range.start), end: Math.min(span.end, range.end) }));
|
|
130
|
+
|
|
117
131
|
/**
|
|
118
132
|
* 本文(prose)では URL を覆うが、覆いは空白まで取るので、URL の直後に続けて書いた字(「(https://example.jp/)」の「)」)まで消える。
|
|
119
|
-
* 読み手には見えている字なので、
|
|
133
|
+
* 読み手には見えている字なので、URL が終わった後ろの、字のまま見える範囲(texts)を元に戻す。コードの中の URL の後ろも同じ。
|
|
120
134
|
*/
|
|
121
135
|
export const withRunOnRestored = (prose: string, source: string, texts: readonly Span[]): string => {
|
|
122
|
-
const readable = mergeSpans(texts,
|
|
136
|
+
const readable = mergeSpans(texts, true);
|
|
123
137
|
const parts: string[] = [];
|
|
124
138
|
const cursor = { at: 0 };
|
|
125
139
|
[...source.matchAll(BARE_URL)].forEach((match) => {
|
|
126
|
-
const tail = match[0].
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
140
|
+
const tail = { start: match.index + urlProperEnd(match[0], match.index, readable), end: match.index + match[0].length };
|
|
141
|
+
clippedTo(readable, tail).forEach((span) => {
|
|
142
|
+
parts.push(prose.slice(cursor.at, span.start), source.slice(span.start, span.end));
|
|
143
|
+
cursor.at = span.end;
|
|
144
|
+
});
|
|
130
145
|
});
|
|
131
146
|
parts.push(prose.slice(cursor.at));
|
|
132
147
|
return parts.join("");
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// A time limit a contract leaves to the reader (速やかに, 遅滞なく, promptly, within a reasonable time) where the same
|
|
2
|
+
// contract writes most of its limits as numbers (7日以内, within thirty days): who decides whether 速やかに was met?
|
|
3
|
+
// Which way to write limits is the drafter's choice; only the minority in one document is pointed at. A sentence that
|
|
4
|
+
// also gives a number of days (速やかに、遅くとも7日以内に) has a deadline, and a sentence that grants a right (may
|
|
5
|
+
// promptly suspend, 速やかに解除することができる) sets no deadline. The words come from the language package's word
|
|
6
|
+
// lists: vague-deadline, concrete-deadline (a stated limit: 日以内, "within … days") and deadline-permission. Pure.
|
|
7
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
8
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument } from "../plugin.ts";
|
|
9
|
+
|
|
10
|
+
export type VagueDeadline = { readonly offset: number; readonly word: string };
|
|
11
|
+
|
|
12
|
+
type Text = { readonly start: number; readonly text: string };
|
|
13
|
+
|
|
14
|
+
type Hit = { readonly at: number; readonly word: string };
|
|
15
|
+
|
|
16
|
+
/** What the three word lists give the decision. */
|
|
17
|
+
export type DeadlineWords = {
|
|
18
|
+
readonly vague: readonly string[];
|
|
19
|
+
/** A phrase, or a frame "within … days": the second part within FRAME_REACH characters after the first. */
|
|
20
|
+
readonly concrete: readonly string[];
|
|
21
|
+
/** A right, not a duty: a word anywhere in the sentence, or (position: after) the sentence's ending. */
|
|
22
|
+
readonly permissions: readonly LexiconEntry[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const LATIN = /\p{Script=Latin}/u;
|
|
26
|
+
const LETTER = /\p{L}/u;
|
|
27
|
+
const FRAME = "…";
|
|
28
|
+
/** How far after "within" the unit of a frame may come: "within fourteen (14) business days". */
|
|
29
|
+
const FRAME_REACH = 30;
|
|
30
|
+
const SENTENCE_MARKS = new Set(["。", ".", ".", "!", "?", "!", "?"]);
|
|
31
|
+
|
|
32
|
+
/** The sentence without its closing marks and spaces. */
|
|
33
|
+
const withoutEnding = (text: string): string => {
|
|
34
|
+
const chars = [...text.trimEnd()];
|
|
35
|
+
const last = chars.findLastIndex((char) => !SENTENCE_MARKS.has(char) && char.trim() !== "");
|
|
36
|
+
return chars.slice(0, last + 1).join("");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** An English word is not read inside a longer one (promptly in unpromptly, may in mayor). */
|
|
40
|
+
const isWordAt = (text: string, word: string, at: number): boolean =>
|
|
41
|
+
!LATIN.test(word) || (!LETTER.test(text.charAt(at - 1)) && !LETTER.test(text.charAt(at + word.length)));
|
|
42
|
+
|
|
43
|
+
const indexesOf = (text: string, word: string): number[] => {
|
|
44
|
+
const found: number[] = [];
|
|
45
|
+
if (word === "") return found;
|
|
46
|
+
let at = text.indexOf(word);
|
|
47
|
+
while (at !== -1) {
|
|
48
|
+
found.push(at);
|
|
49
|
+
at = text.indexOf(word, at + word.length);
|
|
50
|
+
}
|
|
51
|
+
return found;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const wordIn = (lower: string, word: string): boolean => indexesOf(lower, word.toLowerCase()).some((at) => isWordAt(lower, word, at));
|
|
55
|
+
|
|
56
|
+
/** A phrase, or a frame whose second part follows its first within reach. */
|
|
57
|
+
const concreteIn = (lower: string, entry: string): boolean => {
|
|
58
|
+
const [lead = "", tail, ...rest] = entry
|
|
59
|
+
.toLowerCase()
|
|
60
|
+
.split(FRAME)
|
|
61
|
+
.map((part) => part.trim());
|
|
62
|
+
if (rest.length > 0) return false;
|
|
63
|
+
if (tail === undefined) return lead !== "" && lower.includes(lead);
|
|
64
|
+
return (
|
|
65
|
+
lead !== "" &&
|
|
66
|
+
tail !== "" &&
|
|
67
|
+
indexesOf(lower, lead).some((at) => isWordAt(lower, lead, at) && lower.slice(at + lead.length, at + lead.length + FRAME_REACH).includes(tail))
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const statesConcrete = (text: Text, concrete: readonly string[]): boolean => {
|
|
72
|
+
const lower = text.text.toLowerCase();
|
|
73
|
+
return concrete.some((entry) => concreteIn(lower, entry));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const grantsRight = (text: Text, permissions: readonly LexiconEntry[]): boolean => {
|
|
77
|
+
const lower = text.text.toLowerCase();
|
|
78
|
+
const ending = withoutEnding(lower);
|
|
79
|
+
return permissions
|
|
80
|
+
.filter((entry) => entry.pattern !== "")
|
|
81
|
+
.some((entry) => (entry.position === "after" ? ending.endsWith(entry.pattern.toLowerCase()) : wordIn(lower, entry.pattern)));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** A hit inside a longer hit at the same place (速やかに in 可及的速やかに) is that longer word. */
|
|
85
|
+
const isInsideLonger = (hit: Hit, hits: readonly Hit[]): boolean =>
|
|
86
|
+
hits.some((other) => other.word.length > hit.word.length && other.at <= hit.at && hit.at + hit.word.length <= other.at + other.word.length);
|
|
87
|
+
|
|
88
|
+
/** The vague limits in one sentence, in order. Matching ignores case. */
|
|
89
|
+
const vagueIn = (text: Text, vague: readonly string[]): VagueDeadline[] => {
|
|
90
|
+
const lower = text.text.toLowerCase();
|
|
91
|
+
const hits = vague
|
|
92
|
+
.filter((word) => word !== "")
|
|
93
|
+
.flatMap((word) =>
|
|
94
|
+
indexesOf(lower, word.toLowerCase())
|
|
95
|
+
.filter((at) => isWordAt(lower, word, at))
|
|
96
|
+
.map((at) => ({ at, word })),
|
|
97
|
+
);
|
|
98
|
+
return hits
|
|
99
|
+
.filter((hit) => !isInsideLonger(hit, hits))
|
|
100
|
+
.toSorted((left, right) => left.at - right.at)
|
|
101
|
+
.map((hit) => ({ offset: text.start + hit.at, word: text.text.slice(hit.at, hit.at + hit.word.length) }));
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Each vague limit in a sentence that states no concrete one and grants no right, when more sentences of the document
|
|
106
|
+
* state a concrete one than hold such a vague one: the vague limits are then the document's minority.
|
|
107
|
+
*/
|
|
108
|
+
export const vagueDeadlines = (texts: readonly Text[], words: DeadlineWords): VagueDeadline[] => {
|
|
109
|
+
const perSentence = texts.map((text) => (statesConcrete(text, words.concrete) || grantsRight(text, words.permissions) ? [] : vagueIn(text, words.vague)));
|
|
110
|
+
const vagueSentences = perSentence.filter((hits) => hits.length > 0).length;
|
|
111
|
+
const concreteSentences = texts.filter((text) => statesConcrete(text, words.concrete)).length;
|
|
112
|
+
return concreteSentences > vagueSentences ? perSentence.flat() : [];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
|
|
116
|
+
|
|
117
|
+
export const vagueDeadline: Detector = (doc): Finding[] =>
|
|
118
|
+
vagueDeadlines(
|
|
119
|
+
doc.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text })),
|
|
120
|
+
{
|
|
121
|
+
vague: patternsOf(doc, "vague-deadline"),
|
|
122
|
+
concrete: patternsOf(doc, "concrete-deadline"),
|
|
123
|
+
permissions: doc.lexicons["deadline-permission"] ?? [],
|
|
124
|
+
},
|
|
125
|
+
).map((hit) => ({
|
|
126
|
+
rule: "vague-deadline",
|
|
127
|
+
severity: "info",
|
|
128
|
+
line: 0,
|
|
129
|
+
column: 0,
|
|
130
|
+
quote: quoteAt(doc.source, hit.offset),
|
|
131
|
+
values: { word: hit.word, offset: hit.offset },
|
|
132
|
+
}));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// version-mismatch: the version a code block's lead-in names against the one the block shows (structure/version-mismatch.ts).
|
|
2
|
+
// The words that make a version a bound (「から」「以上」, "from", "or later") are the lexicon version-range; the words that
|
|
3
|
+
// lead to the version installed (version, install) are version-lead.
|
|
4
|
+
import type { Detector, Finding } from "../plugin.ts";
|
|
5
|
+
import { versionMismatches, type RangeWord, type VersionWords } from "../structure/version-mismatch.ts";
|
|
6
|
+
import { codeFences } from "./code-fences.ts";
|
|
7
|
+
import { quoteAt } from "./structure-tree.ts";
|
|
8
|
+
|
|
9
|
+
export const versionMismatch: Detector = (doc): Finding[] => {
|
|
10
|
+
const words: VersionWords = {
|
|
11
|
+
ranges: (doc.lexicons["version-range"] ?? []).map((entry): RangeWord => ({ word: entry.pattern, position: entry.position ?? "before" })),
|
|
12
|
+
versionWords: (doc.lexicons["version-lead"] ?? []).map((entry) => entry.pattern),
|
|
13
|
+
};
|
|
14
|
+
const blocks = codeFences(doc.source).map((fence) => ({ start: fence.start, code: fence.lines.join("\n") }));
|
|
15
|
+
return versionMismatches(doc.source, doc.prose ?? doc.source, blocks, words).map((issue) => ({
|
|
16
|
+
rule: "version-mismatch",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
line: 0,
|
|
19
|
+
column: 0,
|
|
20
|
+
quote: quoteAt(doc.source, issue.offset),
|
|
21
|
+
values: { ...issue.values, offset: issue.offset },
|
|
22
|
+
}));
|
|
23
|
+
};
|
package/src/furigana.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Span } from "./plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** ひらがなと、読みの中に書く長音・中黒・空白だけの括弧書き。 */
|
|
4
|
+
const KANA_ASIDE = /[((][\p{Script=Hiragana}ー・ \u3000]+[))]/gu;
|
|
5
|
+
|
|
6
|
+
const HIRAGANA = /\p{Script=Hiragana}/u;
|
|
7
|
+
/** 読みを付ける語の終わりの字。ひらがなの後ろの括弧書きは読みではなく挟んだ言葉(「これは(たぶん)」)。 */
|
|
8
|
+
const WORD_END = /[\p{L}\p{N}]/u;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* text の中の、語の直後のふりがなの括弧(「HTTP(えいちてぃーてぃーぴー)」「脆弱性(ぜいじゃくせい)」)の範囲。括弧を含む。
|
|
12
|
+
* 語の読みであって、語の書き方ではない。位置は text の先頭を 0 とする。
|
|
13
|
+
*/
|
|
14
|
+
export const furiganaSpans = (text: string): Span[] =>
|
|
15
|
+
[...text.matchAll(KANA_ASIDE)]
|
|
16
|
+
.filter((match) => {
|
|
17
|
+
const before = text.charAt(match.index - 1);
|
|
18
|
+
return HIRAGANA.test(match[0]) && WORD_END.test(before) && !HIRAGANA.test(before);
|
|
19
|
+
})
|
|
20
|
+
.map((match) => ({ start: match.index, end: match.index + match[0].length }));
|
package/src/genre-load.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
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 { parseGenres, presetLevelsOf, presetProfileOf, withRuleOffs, type GenreData, type PresetLevels } from "./genre-parse.ts";
|
|
6
4
|
import { loadRuleOffs } from "./rule-offs.ts";
|
|
5
|
+
import { PACKAGE_DIR, readText } from "./package-files.ts";
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const RULES_DIR = join(PACKAGE, "rules");
|
|
7
|
+
const GENRES_FILE = join(PACKAGE_DIR, "genres.yaml");
|
|
8
|
+
const RULES_DIR = join(PACKAGE_DIR, "rules");
|
|
11
9
|
|
|
12
10
|
const loaded: { value: GenreData | undefined } = { value: undefined };
|
|
13
11
|
|
|
@@ -15,7 +13,7 @@ const loaded: { value: GenreData | undefined } = { value: undefined };
|
|
|
15
13
|
export const loadGenres = (): GenreData => {
|
|
16
14
|
if (loaded.value !== undefined) return loaded.value;
|
|
17
15
|
try {
|
|
18
|
-
loaded.value = withRuleOffs(parseGenres(parse(
|
|
16
|
+
loaded.value = withRuleOffs(parseGenres(parse(readText(GENRES_FILE))), loadRuleOffs(RULES_DIR));
|
|
19
17
|
} catch (error) {
|
|
20
18
|
throw new Error(`cannot read ${GENRES_FILE}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
21
19
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { shownSignsOf, type AiLevel, type AiScore, type NotScored } from "../ai-score/score.ts";
|
|
2
|
+
|
|
3
|
+
// The AI-likeness quick score as one output's result carries it, and its levels counted over many outputs. Pure.
|
|
4
|
+
|
|
5
|
+
/** One output's quick score: its level (null when not scored, and why), the signs, and which showed. */
|
|
6
|
+
export type GradeAiScore = {
|
|
7
|
+
readonly level: AiLevel | null;
|
|
8
|
+
readonly notScored: NotScored["reason"] | null;
|
|
9
|
+
readonly signs: number;
|
|
10
|
+
readonly compared: number;
|
|
11
|
+
/** The signs that counted: rule ids, and structure measures as structure:<id>. */
|
|
12
|
+
readonly shown: readonly string[];
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** How many outputs reached each level, and how many were not scored. */
|
|
16
|
+
export type AiLevelCounts = Readonly<Record<AiLevel | "notScored", number>>;
|
|
17
|
+
|
|
18
|
+
export const gradeAiScoreOf = (score: AiScore): GradeAiScore => ({
|
|
19
|
+
level: score.level ?? null,
|
|
20
|
+
notScored: score.notScored?.reason ?? null,
|
|
21
|
+
signs: score.signs,
|
|
22
|
+
compared: score.compared,
|
|
23
|
+
shown: shownSignsOf(score),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** The levels of the outputs that carry a score, or undefined when none does (results written before the score existed). */
|
|
27
|
+
export const aiLevelCountsOf = (scores: readonly (GradeAiScore | undefined)[]): AiLevelCounts | undefined => {
|
|
28
|
+
const scored = scores.filter((score) => score !== undefined);
|
|
29
|
+
if (scored.length === 0) return undefined;
|
|
30
|
+
const count = (level: AiLevel): number => scored.filter((score) => score.level === level).length;
|
|
31
|
+
return { low: count("low"), medium: count("medium"), high: count("high"), notScored: scored.filter((score) => score.level === null).length };
|
|
32
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SourceCheck } from "../check-source.ts";
|
|
2
|
+
import type { Finding } from "../plugin.ts";
|
|
3
|
+
import { messageOf } from "../render/text.ts";
|
|
4
|
+
import type { GradeFinding } from "./result.ts";
|
|
5
|
+
|
|
6
|
+
/** One finding as `chaff grade` writes it: the rule, the level, where, and the message the command line prints. */
|
|
7
|
+
export const findingOf = (finding: Finding, check: SourceCheck): GradeFinding => {
|
|
8
|
+
const rule = check.rules.find((entry) => entry.id === finding.rule);
|
|
9
|
+
return {
|
|
10
|
+
rule: finding.rule,
|
|
11
|
+
level: finding.severity,
|
|
12
|
+
line: finding.line,
|
|
13
|
+
column: finding.column,
|
|
14
|
+
message: rule === undefined ? finding.rule : messageOf(rule, finding, check.language),
|
|
15
|
+
};
|
|
16
|
+
};
|
package/src/grade/grade-item.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { checkSource, type SourceCheck } from "../check-source.ts";
|
|
2
2
|
import type { Allowed } from "../compare/outcome.ts";
|
|
3
3
|
import { missingSections } from "../detectors/team.ts";
|
|
4
|
-
import { messageOf } from "../render/text.ts";
|
|
5
4
|
import { uiLanguageOf, type UiLanguage } from "../ui.ts";
|
|
6
|
-
import
|
|
5
|
+
import { findingOf } from "./finding.ts";
|
|
7
6
|
import { citationsAgainst, factsAgainst, OUTPUT_PATH, type CitationsRead, type ItemReading } from "./checks.ts";
|
|
8
7
|
import { contextsAgainst, type ContextsRead } from "./contexts.ts";
|
|
9
8
|
import type { GradeItem } from "./item.ts";
|
|
@@ -13,22 +12,13 @@ import type { Rubric } from "./rubric.ts";
|
|
|
13
12
|
import { rubricVerdict } from "./rubric-verdict.ts";
|
|
14
13
|
import type { GradeSettings } from "./stamp.ts";
|
|
15
14
|
import { GRADE_TEXT } from "./text.ts";
|
|
15
|
+
import { aiScoreOfDocument } from "../ai-score/of-document.ts";
|
|
16
|
+
import { gradeAiScoreOf } from "./ai-score.ts";
|
|
16
17
|
import { defaultVerdict, type Verdict } from "./verdict.ts";
|
|
17
18
|
|
|
18
19
|
/** One run's settings, its `grade:` rubric when chaff.yaml has one, and its stamp, which every result carries. */
|
|
19
20
|
export type GradeSetup = GradeSettings & { readonly stamp: Stamp; readonly rubric?: Rubric | undefined };
|
|
20
21
|
|
|
21
|
-
const findingOf = (finding: Finding, check: SourceCheck): GradeFinding => {
|
|
22
|
-
const rule = check.rules.find((entry) => entry.id === finding.rule);
|
|
23
|
-
return {
|
|
24
|
-
rule: finding.rule,
|
|
25
|
-
level: finding.severity,
|
|
26
|
-
line: finding.line,
|
|
27
|
-
column: finding.column,
|
|
28
|
-
message: rule === undefined ? finding.rule : messageOf(rule, finding, check.language),
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
22
|
/** The fact kinds the rubric lets change (`allow_dropped`, `allow_added`), as compare's --allow-dropped and --allow-added. */
|
|
33
23
|
const allowedBy = (rubric: Rubric | undefined): Allowed => ({
|
|
34
24
|
dropped: new Set(rubric?.facts?.allowDropped ?? []),
|
|
@@ -126,6 +116,7 @@ export const gradeItem = async (item: GradeItem, setup: GradeSetup): Promise<Gra
|
|
|
126
116
|
citations: citationsOf(checked),
|
|
127
117
|
...(checked.contexts === undefined ? {} : { contexts: checked.contexts.contexts }),
|
|
128
118
|
...(score === undefined ? {} : { score }),
|
|
119
|
+
aiScore: gradeAiScoreOf(aiScoreOfDocument(checked.check.doc, checked.check.rules, checked.check.genre.genre)),
|
|
129
120
|
pass: verdict.pass,
|
|
130
121
|
failedBecause: verdict.failedBecause,
|
|
131
122
|
stamp: setup.stamp,
|
|
@@ -14,11 +14,15 @@ type Rows = readonly (readonly string[])[];
|
|
|
14
14
|
const passedCell = (column: VariantColumn, text: VariantText): string =>
|
|
15
15
|
text.passedCell(column.passed, column.outputs, column.passRate === undefined ? "–" : `${String(column.passRate)}%`);
|
|
16
16
|
|
|
17
|
+
const aiScoreCellOf = (column: VariantColumn, text: VariantText): string =>
|
|
18
|
+
column.aiScore === undefined ? "–" : text.aiScoreCell(column.aiScore.low, column.aiScore.medium, column.aiScore.high, column.aiScore.notScored);
|
|
19
|
+
|
|
17
20
|
/** The header and one row per measure, a column per variant. */
|
|
18
21
|
const measureRows = (comparison: VariantComparison, text: VariantText): Rows => {
|
|
19
22
|
const { columns } = comparison;
|
|
20
23
|
const scored = columns.some((column) => column.penalty !== undefined);
|
|
21
24
|
const grounded = columns.some((column) => column.contexts !== undefined);
|
|
25
|
+
const aiScored = columns.some((column) => column.aiScore !== undefined);
|
|
22
26
|
return [
|
|
23
27
|
["", ...comparison.variants],
|
|
24
28
|
[text.passed, ...columns.map((column) => passedCell(column, text))],
|
|
@@ -29,6 +33,7 @@ const measureRows = (comparison: VariantComparison, text: VariantText): Rows =>
|
|
|
29
33
|
? [[text.unsupportedFacts, ...columns.map((column) => text.citationsCell(column.contexts?.unsupported ?? 0, column.contexts?.checked ?? 0))]]
|
|
30
34
|
: []),
|
|
31
35
|
...(scored ? [[text.penalty, ...columns.map((column) => String(column.penalty ?? 0))]] : []),
|
|
36
|
+
...(aiScored ? [[text.aiScore, ...columns.map((column) => aiScoreCellOf(column, text))]] : []),
|
|
32
37
|
];
|
|
33
38
|
};
|
|
34
39
|
|
package/src/grade/render.ts
CHANGED
|
@@ -65,6 +65,7 @@ export const renderSummary = (path: string, summary: GradeSummary, text: GradeTe
|
|
|
65
65
|
text.citations(summary.citations.checked, summary.citations.failed),
|
|
66
66
|
...contextsLine(summary, text),
|
|
67
67
|
...(summary.penalty === undefined ? [] : [text.penalty(summary.penalty)]),
|
|
68
|
+
...(summary.aiScore === undefined ? [] : [text.aiScore(summary.aiScore.low, summary.aiScore.medium, summary.aiScore.high, summary.aiScore.notScored)]),
|
|
68
69
|
...block(text.notRunHeading(summary.notRun.length), table(summary.notRun.map((entry) => [entry.rule, text.inOutputs(entry.outputs), entry.reason]))),
|
|
69
70
|
...stampLines(summary, text),
|
|
70
71
|
].join("\n");
|
package/src/grade/result.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { AtomKind } from "../compare/atom.ts";
|
|
|
2
2
|
import type { CitationStatus } from "../structure/cite.ts";
|
|
3
3
|
import type { Severity } from "../plugin.ts";
|
|
4
4
|
import type { OutputSize } from "./rates.ts";
|
|
5
|
+
import type { GradeAiScore } from "./ai-score.ts";
|
|
5
6
|
|
|
6
7
|
// The shape of one line of `chaff grade --out`, and of what `grade()` returns. Spec §29.3.
|
|
7
8
|
|
|
@@ -75,6 +76,8 @@ export type GradeResult = {
|
|
|
75
76
|
readonly contexts?: GradeContexts | undefined;
|
|
76
77
|
/** Only with a `grade:` rubric. */
|
|
77
78
|
readonly score?: GradeScore | undefined;
|
|
79
|
+
/** The AI-likeness quick score. Absent only in results written before it existed. Never part of pass or fail. */
|
|
80
|
+
readonly aiScore?: GradeAiScore | undefined;
|
|
78
81
|
readonly pass: boolean;
|
|
79
82
|
/** Each condition the output failed, with how far: `facts.dropped 3 > 0`. */
|
|
80
83
|
readonly failedBecause: readonly string[];
|
|
@@ -52,6 +52,19 @@ const isScoreItem = (value: unknown): boolean => isRecord(value) && isNumber(val
|
|
|
52
52
|
|
|
53
53
|
const isScore = (value: unknown): boolean => value === undefined || (isRecord(value) && isNumber(value["penalty"]) && isArrayOf(value["items"], isScoreItem));
|
|
54
54
|
|
|
55
|
+
const AI_LEVELS: ReadonlySet<unknown> = new Set(["low", "medium", "high", null]);
|
|
56
|
+
const NOT_SCORED: ReadonlySet<unknown> = new Set(["too-short", "no-baseline", null]);
|
|
57
|
+
|
|
58
|
+
/** Absent in results written before the quick score existed. */
|
|
59
|
+
const isAiScore = (value: unknown): boolean =>
|
|
60
|
+
value === undefined ||
|
|
61
|
+
(isRecord(value) &&
|
|
62
|
+
AI_LEVELS.has(value["level"]) &&
|
|
63
|
+
NOT_SCORED.has(value["notScored"]) &&
|
|
64
|
+
isNumber(value["signs"]) &&
|
|
65
|
+
isNumber(value["compared"]) &&
|
|
66
|
+
isArrayOf(value["shown"], isString));
|
|
67
|
+
|
|
55
68
|
const isNotRun = (value: unknown): boolean => isRecord(value) && isString(value["rule"]) && isString(value["reason"]);
|
|
56
69
|
|
|
57
70
|
const isRates = (value: unknown): boolean => isRecord(value) && Object.values(value).every(isNumber);
|
|
@@ -59,6 +72,9 @@ const isRates = (value: unknown): boolean => isRecord(value) && Object.values(va
|
|
|
59
72
|
/** The id, and the variant when the output had one. */
|
|
60
73
|
const isLabel = (value: Record<string, unknown>): boolean => isString(value["id"]) && (value["variant"] === undefined || isString(value["variant"]));
|
|
61
74
|
|
|
75
|
+
/** The parts a result carries only sometimes: contexts, a rubric's score, the quick score. Each is checked when present. */
|
|
76
|
+
const hasOptionalParts = (value: Record<string, unknown>): boolean => isContexts(value["contexts"]) && isScore(value["score"]) && isAiScore(value["aiScore"]);
|
|
77
|
+
|
|
62
78
|
/** A line `--out` writes, every field checked: a line that only looks like one would be compared as if it had been graded. */
|
|
63
79
|
export const isGradeResult = (value: unknown): value is GradeResult =>
|
|
64
80
|
isRecord(value) &&
|
|
@@ -71,8 +87,7 @@ export const isGradeResult = (value: unknown): value is GradeResult =>
|
|
|
71
87
|
isArrayOf(value["notRun"], isNotRun) &&
|
|
72
88
|
isFacts(value["facts"]) &&
|
|
73
89
|
isCitations(value["citations"]) &&
|
|
74
|
-
|
|
75
|
-
isScore(value["score"]) &&
|
|
90
|
+
hasOptionalParts(value) &&
|
|
76
91
|
typeof value["pass"] === "boolean" &&
|
|
77
92
|
isArrayOf(value["failedBecause"], isString) &&
|
|
78
93
|
isStamp(value["stamp"]);
|
package/src/grade/summary.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { LengthUnit } from "../plugin.ts";
|
|
|
2
2
|
import { compareText, sortedByKey, tally } from "./order.ts";
|
|
3
3
|
import { rateOf } from "./rates.ts";
|
|
4
4
|
import type { GradeFact, GradeResult, NotRunEntry, Stamp } from "./result.ts";
|
|
5
|
+
import { aiLevelCountsOf, type AiLevelCounts } from "./ai-score.ts";
|
|
5
6
|
|
|
6
7
|
// The summary of a run (spec §29.3), from the per-output results alone, so the same results always sum the same way.
|
|
7
8
|
|
|
@@ -23,6 +24,8 @@ export type GradeSummary = {
|
|
|
23
24
|
readonly notRun: readonly (NotRunEntry & { readonly outputs: number })[];
|
|
24
25
|
/** The penalty points of every output, added up. Only with a `grade:` rubric. */
|
|
25
26
|
readonly penalty?: number | undefined;
|
|
27
|
+
/** How many outputs reached each level of the AI-likeness quick score. Absent when no result carries one. */
|
|
28
|
+
readonly aiScore?: AiLevelCounts | undefined;
|
|
26
29
|
readonly stamp: Stamp | undefined;
|
|
27
30
|
};
|
|
28
31
|
|
|
@@ -86,6 +89,11 @@ const contextsOf = (results: readonly GradeResult[]): Pick<GradeSummary, "contex
|
|
|
86
89
|
};
|
|
87
90
|
};
|
|
88
91
|
|
|
92
|
+
const aiScoreOf = (results: readonly GradeResult[]): Pick<GradeSummary, "aiScore"> => {
|
|
93
|
+
const counts = aiLevelCountsOf(results.map((result) => result.aiScore));
|
|
94
|
+
return counts === undefined ? {} : { aiScore: counts };
|
|
95
|
+
};
|
|
96
|
+
|
|
89
97
|
export const summaryOf = (results: readonly GradeResult[]): GradeSummary => {
|
|
90
98
|
const sizes = sizesOf(results);
|
|
91
99
|
return {
|
|
@@ -107,6 +115,7 @@ export const summaryOf = (results: readonly GradeResult[]): GradeSummary => {
|
|
|
107
115
|
...contextsOf(results),
|
|
108
116
|
notRun: notRunOf(results),
|
|
109
117
|
...penaltyOf(results),
|
|
118
|
+
...aiScoreOf(results),
|
|
110
119
|
stamp: results[0]?.stamp,
|
|
111
120
|
};
|
|
112
121
|
};
|