chaffjs 0.24.0 → 0.26.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/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/detect.d.ts.map +1 -1
- package/dist/detect.js +4 -5
- package/dist/detect.js.map +1 -1
- package/dist/detectors/connective-hierarchy.d.ts +16 -0
- package/dist/detectors/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/connective-hierarchy.js +68 -0
- package/dist/detectors/connective-hierarchy.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/missing-final-period.d.ts +17 -0
- package/dist/detectors/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/missing-final-period.js +62 -0
- package/dist/detectors/missing-final-period.js.map +1 -0
- package/dist/detectors/missing-space-after-comma.d.ts +10 -0
- package/dist/detectors/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/missing-space-after-comma.js +42 -0
- package/dist/detectors/missing-space-after-comma.js.map +1 -0
- package/dist/detectors/orthographic-variant.d.ts.map +1 -1
- package/dist/detectors/orthographic-variant.js +44 -17
- 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/connective-hierarchy.d.ts +3 -0
- package/dist/detectors/registry/connective-hierarchy.d.ts.map +1 -0
- package/dist/detectors/registry/connective-hierarchy.js +3 -0
- package/dist/detectors/registry/connective-hierarchy.js.map +1 -0
- package/dist/detectors/registry/missing-final-period.d.ts +3 -0
- package/dist/detectors/registry/missing-final-period.d.ts.map +1 -0
- package/dist/detectors/registry/missing-final-period.js +3 -0
- package/dist/detectors/registry/missing-final-period.js.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts +3 -0
- package/dist/detectors/registry/missing-space-after-comma.d.ts.map +1 -0
- package/dist/detectors/registry/missing-space-after-comma.js +3 -0
- package/dist/detectors/registry/missing-space-after-comma.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/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 +4 -1
- package/dist/detectors/structure-tree.d.ts.map +1 -1
- package/dist/detectors/structure-tree.js +21 -2
- 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/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/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/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/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-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/cliche.yaml +28 -8
- package/rules/connective-hierarchy.yaml +62 -0
- 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/missing-final-period.yaml +76 -0
- package/rules/missing-space-after-comma.yaml +57 -0
- package/rules/no-mixed-desumasu.yaml +2 -2
- package/rules/orthographic-variant.yaml +2 -2
- package/rules/party-role-name.yaml +94 -0
- package/rules/quote-without-source.yaml +61 -0
- package/rules/total-mismatch.yaml +8 -8
- package/rules/vague-deadline.yaml +95 -0
- package/rules/weasel-word.yaml +25 -7
- package/skills/chaff/SKILL.md +32 -5
- 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/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/detect.ts +4 -5
- package/src/detectors/connective-hierarchy.ts +99 -0
- package/src/detectors/date-year.ts +51 -12
- package/src/detectors/defined-term-form.ts +28 -6
- package/src/detectors/missing-final-period.ts +79 -0
- package/src/detectors/missing-space-after-comma.ts +54 -0
- package/src/detectors/orthographic-variant.ts +59 -18
- 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/connective-hierarchy.ts +4 -0
- package/src/detectors/registry/missing-final-period.ts +4 -0
- package/src/detectors/registry/missing-space-after-comma.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/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 +28 -6
- package/src/detectors/unbalanced-bracket.ts +25 -10
- package/src/detectors/vague-deadline.ts +132 -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/date-range.ts +28 -3
- package/src/structure/issues.ts +61 -6
- package/src/structure/party-role.ts +94 -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-shape/baseline-load.ts +4 -5
- package/src/style-load.ts +5 -6
package/src/run.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { presetLevels } from "./genre-load.ts";
|
|
|
15
15
|
import { bodySectionOf } from "./body-section.ts";
|
|
16
16
|
import { optionValues, settleOptions, type OptionLayer } from "./rule-options.ts";
|
|
17
17
|
import { byPosition } from "./finding-order.ts";
|
|
18
|
-
import { PatternTimeout } from "./custom/
|
|
18
|
+
import { PatternTimeout } from "./custom/pattern-timeout.ts";
|
|
19
19
|
import { PluginRuleFailure } from "./extension/module-detector.ts";
|
|
20
20
|
import { failureReason } from "./extension/failure-text.ts";
|
|
21
21
|
import { tagCoverage } from "./tag-coverage.ts";
|
|
@@ -18,6 +18,29 @@ export type RangeWords = {
|
|
|
18
18
|
readonly frames: readonly RangeFrame[];
|
|
19
19
|
/** 同じ文にあれば、組を期間ではなく日付の変更と読む語(moved from … to、… was postponed)。 */
|
|
20
20
|
readonly changes: readonly string[];
|
|
21
|
+
/** 日付の前に添える曜日の名(Wednesday, April 1, 2026)。つなぎ目と前の語を読むときに除く。無ければ空。 */
|
|
22
|
+
readonly weekdays?: readonly string[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 曜日の略し方の短いほう(Tue、Wed)。名の頭のこの字数以上が書いてあれば、その曜日と読む。 */
|
|
26
|
+
const WEEKDAY_ABBREVIATION = 3;
|
|
27
|
+
const LETTER = /\p{L}/u;
|
|
28
|
+
|
|
29
|
+
/** The text's last word (past a full stop) and where it starts. */
|
|
30
|
+
const lastWord = (text: string): { readonly word: string; readonly start: number } => {
|
|
31
|
+
const body = text.endsWith(".") ? text.slice(0, -1) : text;
|
|
32
|
+
const chars = [...body];
|
|
33
|
+
const start = chars.findLastIndex((char) => !LETTER.test(char)) + 1;
|
|
34
|
+
return { word: chars.slice(start).join(""), start: chars.slice(0, start).join("").length };
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 終わりに付いた曜日の名か略し方とその後ろのコンマ("to tuesday," の tuesday,、"from wed." の wed.)を除く。 */
|
|
38
|
+
export const withoutTrailingWeekday = (text: string, weekdays: readonly string[]): string => {
|
|
39
|
+
const trimmed = text.replace(/,$/u, "").trimEnd();
|
|
40
|
+
const { word, start } = lastWord(trimmed);
|
|
41
|
+
const lower = word.toLowerCase();
|
|
42
|
+
const isWeekday = lower.length >= WEEKDAY_ABBREVIATION && weekdays.some((name) => name.toLowerCase().startsWith(lower));
|
|
43
|
+
return isWeekday ? trimmed.slice(0, start).trimEnd() : text;
|
|
21
44
|
};
|
|
22
45
|
|
|
23
46
|
export type RangeFrame = { readonly lead: string; readonly joint: string };
|
|
@@ -87,13 +110,15 @@ const sentenceAround = (source: string, start: DatedSpan, end: DatedSpan): { rea
|
|
|
87
110
|
/** 前の語と間の語の組(from … to)。同じ文のどこかに変更の語(moved、postponed)があれば、日付を動かした文で、期間ではない。 */
|
|
88
111
|
const framed = (source: string, start: DatedSpan, end: DatedSpan, joint: string, words: RangeWords): boolean => {
|
|
89
112
|
const sentence = sentenceAround(source, start, end);
|
|
90
|
-
const
|
|
113
|
+
const before = withoutTrailingWeekday(sentence.before, words.weekdays ?? []);
|
|
114
|
+
const frame = words.frames.find((candidate) => candidate.joint === joint && endsWithWord(before, candidate.lead));
|
|
91
115
|
return frame !== undefined && !words.changes.some((change) => hasWord(sentence.text, change.toLowerCase()));
|
|
92
116
|
};
|
|
93
117
|
|
|
94
118
|
const isRange = (source: string, start: DatedSpan, end: DatedSpan, words: RangeWords): boolean => {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
119
|
+
const written = jointOf(source, start, end);
|
|
120
|
+
if (written === undefined) return false;
|
|
121
|
+
const joint = withoutTrailingWeekday(written, words.weekdays ?? []);
|
|
97
122
|
return (
|
|
98
123
|
isOneOf(joint, words.connectors) || (isOneOf(joint, words.openers) && closedAfter(source, end, words.closers)) || framed(source, start, end, joint, words)
|
|
99
124
|
);
|
package/src/structure/issues.ts
CHANGED
|
@@ -31,8 +31,9 @@ const textOf = (node: StructureNode, key: string): string => {
|
|
|
31
31
|
* 参照がこの文書のどこかを指しているか。fallback は言語パッケージが付ける別の行き先で、
|
|
32
32
|
* 日本語の「第4条第1項」は、番号の無い第 1 項を持つ第4条を指しうる。
|
|
33
33
|
*/
|
|
34
|
-
const resolves = (node: StructureNode, addresses: ReadonlySet<string
|
|
35
|
-
addresses.has(textOf(node, "target")) ||
|
|
34
|
+
const resolves = (node: StructureNode, addresses: ReadonlySet<string>, chapters: Chapters): boolean =>
|
|
35
|
+
addresses.has(textOf(node, "target")) ||
|
|
36
|
+
(node.attrs["fallback"] !== undefined && chapterReading(textOf(node, "target"), chapters).fallback && addresses.has(fallbackKey(node)));
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* fallbackLabel があれば、fallback はその見出しの番号で書かれた節点だけに当たる。
|
|
@@ -48,6 +49,53 @@ const addressesOf = (node: StructureNode): string[] =>
|
|
|
48
49
|
|
|
49
50
|
const keysOf = (node: StructureNode): string[] => addressesOf(node).flatMap((address) => [address, labelled(address, textOf(node, "label"))]);
|
|
50
51
|
|
|
52
|
+
/** An address without its numbers: the kind of unit it names (ch9 and ch10 are both ch). */
|
|
53
|
+
const unitOf = (address: string): string => address.replaceAll(/[\d.]+/gu, "");
|
|
54
|
+
|
|
55
|
+
/** The document's chapters: their addresses, and whether the one chapter is the document itself (its title, first of all). */
|
|
56
|
+
export type Chapters = { readonly addresses: readonly string[]; readonly titled: boolean };
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* How a document's chapters bear on a reference to a chapter (第9章, Chapter 9). Pure.
|
|
60
|
+
* fallback: whether the reference may fall back to a numbered section ("## 9. …"). Only in a document with no chapter:
|
|
61
|
+
* where chapters are written, a chapter reference names a chapter, and section 9 is not chapter 9.
|
|
62
|
+
* elsewhere: the reference names a chapter of another file. A document that is one chapter (第10章 as its title, a series
|
|
63
|
+
* of one chapter per file) holds no other chapter, so 第9章 is the next file's. Its own chapter is still its own.
|
|
64
|
+
*/
|
|
65
|
+
export const chapterReading = (target: string, chapters: Chapters): { readonly fallback: boolean; readonly elsewhere: boolean } => {
|
|
66
|
+
const shortAddresses = chapters.addresses.map((address) => address.slice(address.lastIndexOf("/") + 1));
|
|
67
|
+
const toChapter = new Set(shortAddresses.map(unitOf)).has(unitOf(target));
|
|
68
|
+
const series = chapters.titled && chapters.addresses.length === 1 && !shortAddresses.includes(target);
|
|
69
|
+
return { fallback: !toChapter, elsewhere: toChapter && series };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** A chapter is the document's title when it is not under another heading (ch10, not h1/ch1) and nothing numbered comes before it. */
|
|
73
|
+
const chaptersOf = (nodes: readonly StructureNode[]): Chapters => {
|
|
74
|
+
const chapters = nodes.filter((node) => node.kind === "chapter");
|
|
75
|
+
const first = nodes.find((node) => NUMBERED.has(node.kind));
|
|
76
|
+
const titled = chapters.length === 1 && first === chapters[0] && !(first?.address.includes("/") ?? true);
|
|
77
|
+
return { addresses: chapters.map((node) => node.address), titled };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** What may stand between a chapter and the reference it qualifies: nothing (第1章第2条) or の (第1章の第2条). A list mark (第1章、第2条) names two. */
|
|
81
|
+
const QUALIFIER_GAPS: ReadonlySet<string> = new Set(["", "の"]);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The references that point into another file: a chapter reference read as elsewhere (chapterReading), and the reference
|
|
85
|
+
* right after one, which it qualifies (第1章の第2条 is article 2 of that other chapter).
|
|
86
|
+
*/
|
|
87
|
+
const elsewhereReferences = (nodes: readonly StructureNode[], source: string): ReadonlySet<StructureNode> => {
|
|
88
|
+
const chapters = chaptersOf(nodes);
|
|
89
|
+
const references = nodes.filter((node) => node.kind === "reference");
|
|
90
|
+
const elsewhere = new Set<StructureNode>();
|
|
91
|
+
references.forEach((node, index) => {
|
|
92
|
+
const before = references[index - 1];
|
|
93
|
+
const qualified = before !== undefined && elsewhere.has(before) && QUALIFIER_GAPS.has(source.slice(before.span.end, node.span.start).trim());
|
|
94
|
+
if (qualified || chapterReading(textOf(node, "target"), chapters).elsewhere) elsewhere.add(node);
|
|
95
|
+
});
|
|
96
|
+
return elsewhere;
|
|
97
|
+
};
|
|
98
|
+
|
|
51
99
|
/**
|
|
52
100
|
* 他の文書を指す参照か。citedTag("[HTTP-CACHING]")は、書き方だけでは差し込み欄の "[BUYER-1]" と見分けられないので、
|
|
53
101
|
* 文書がその語を一覧に載せているときだけ他の文書の名とみなす。
|
|
@@ -77,10 +125,12 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
|
|
|
77
125
|
const otherNumbering = (node: StructureNode): boolean =>
|
|
78
126
|
numberings.size > 0 && typeof numbering(node) === "string" && !numberings.has(String(numbering(node))) && citedElsewhere.has(numbering(node));
|
|
79
127
|
const articleLabels = nodes.filter((node) => node.kind === "article").map((node) => textOf(node, "label"));
|
|
128
|
+
const chapters = chaptersOf(nodes);
|
|
129
|
+
const elsewhere = elsewhereReferences(nodes, source);
|
|
80
130
|
return nodes
|
|
81
131
|
.filter((node) => node.kind === "reference" && !citesOther(node) && !otherNumbering(node))
|
|
82
|
-
.filter((node) => !namesAbsentUnit(node, articleLabels))
|
|
83
|
-
.filter((node) => !resolves(node, addresses))
|
|
132
|
+
.filter((node) => !namesAbsentUnit(node, articleLabels) && !elsewhere.has(node))
|
|
133
|
+
.filter((node) => !resolves(node, addresses, chapters))
|
|
84
134
|
.map((node) => ({ offset: node.span.start, values: { label: textOf(node, "label"), target: textOf(node, "target") } }));
|
|
85
135
|
};
|
|
86
136
|
|
|
@@ -88,15 +138,20 @@ export const danglingReferences = (tree: StructureNode, source: string): Structu
|
|
|
88
138
|
* 参照が指す節点。danglingReferences と同じ引き方(番地、章の短い番地、fallback)で、番号の付いたまとまりを引く。
|
|
89
139
|
* 同じ鍵の節点が二つ以上ある(番号の振り直しや別表の条)ときは、どれを指すのか決まらないので undefined。
|
|
90
140
|
*/
|
|
91
|
-
export const referenceResolver = (tree: StructureNode): ((reference: StructureNode) => StructureNode | undefined) => {
|
|
141
|
+
export const referenceResolver = (tree: StructureNode, source: string): ((reference: StructureNode) => StructureNode | undefined) => {
|
|
142
|
+
const nodes = inDocumentOrder(tree);
|
|
143
|
+
const chapters = chaptersOf(nodes);
|
|
144
|
+
const elsewhere = elsewhereReferences(nodes, source);
|
|
92
145
|
const byKey = new Map<string, StructureNode | null>();
|
|
93
146
|
inDocumentOrder(tree)
|
|
94
147
|
.filter((node) => NUMBERED.has(node.kind))
|
|
95
148
|
.forEach((node) => keysOf(node).forEach((key) => byKey.set(key, byKey.has(key) && byKey.get(key) !== node ? null : node)));
|
|
96
149
|
const resolve = (reference: StructureNode): StructureNode | undefined => {
|
|
150
|
+
if (elsewhere.has(reference)) return undefined;
|
|
97
151
|
const target = byKey.get(textOf(reference, "target"));
|
|
98
152
|
if (target !== undefined) return target ?? undefined;
|
|
99
|
-
|
|
153
|
+
const fallback = reference.attrs["fallback"] !== undefined && chapterReading(textOf(reference, "target"), chapters).fallback;
|
|
154
|
+
return fallback ? (byKey.get(fallbackKey(reference)) ?? undefined) : undefined;
|
|
100
155
|
};
|
|
101
156
|
// Article 6 は Section 6 ではない。番号の書き方が両方にあって違えば、別の文書の条かもしれないので引かない。
|
|
102
157
|
return (reference) => {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { BodyText, DefinedTerm } from "./definition-use.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 当事者を、定めた呼び名でなく立場の語で書いた所(「乙」と定めたあとの「受託者」、the "Supplier" のあとの the Vendor)。
|
|
5
|
+
* 当事者の定義は、文の途中で括弧に入れて短い呼び名を定めた所のうち、呼び名が当事者の呼び名の語(甲・乙・当社)か立場の語
|
|
6
|
+
* そのもの(Supplier)であるもの、または括弧のすぐ前に会社の形(株式会社、Ltd)を書いたもの。語は言語パッケージの語彙表から取る。
|
|
7
|
+
* 立場の語は、それ自体を定義した語や、定義した語の一部として書いた所(Receiving Party と定義した所の Party)なら指さない。
|
|
8
|
+
*/
|
|
9
|
+
export type PartyWords = {
|
|
10
|
+
/** 当事者の立場の語(委託者、受託者、Supplier、Recipient)。 */
|
|
11
|
+
readonly roles: readonly string[];
|
|
12
|
+
/** 当事者の呼び名に使う語(甲、乙、当社、we)。 */
|
|
13
|
+
readonly shortNames: readonly string[];
|
|
14
|
+
/** 会社の形(株式会社、Ltd、Inc.)。 */
|
|
15
|
+
readonly companyForms: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type Party = { readonly term: string; readonly end: number };
|
|
19
|
+
export type RoleUse = { readonly offset: number; readonly role: string };
|
|
20
|
+
|
|
21
|
+
/** 会社の形を探す、括弧の前の幅。名前一つ分。 */
|
|
22
|
+
const NAME_REACH = 40;
|
|
23
|
+
/** 語の前後の一字がこれなら、語は長い語の一部(再受託者、受託者側の「受託者」は語の一部でない側も含む)。 */
|
|
24
|
+
const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
|
|
25
|
+
const LATIN_START = /^\p{Script=Latin}/u;
|
|
26
|
+
/** 英語の立場の語は、冠詞か所有の語に続く大文字の語だけ(the Vendor)。小文字の the vendor は普通の名詞。 */
|
|
27
|
+
const ENGLISH_LEAD = /(?:^|[^\p{L}])(?:the|The|each|Each|any|Any|such|Such)\s+$/u;
|
|
28
|
+
const LEAD_REACH = 8;
|
|
29
|
+
|
|
30
|
+
const occurrences = (text: string, word: string): number[] => {
|
|
31
|
+
const found: number[] = [];
|
|
32
|
+
let at = text.indexOf(word);
|
|
33
|
+
while (at !== -1) {
|
|
34
|
+
found.push(at);
|
|
35
|
+
at = text.indexOf(word, at + word.length);
|
|
36
|
+
}
|
|
37
|
+
return found;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const LETTER = /\p{L}/u;
|
|
41
|
+
|
|
42
|
+
/** 英字の語は、英字の続き(Including の Inc)では語と読まない。 */
|
|
43
|
+
const wordAt = (text: string, word: string, at: number): boolean =>
|
|
44
|
+
!LATIN_START.test(word) || (!LETTER.test(text.charAt(at - 1)) && !LETTER.test(text.charAt(at + word.length)));
|
|
45
|
+
|
|
46
|
+
/** 括弧の前の名前に会社の形があるか。 */
|
|
47
|
+
const namedCompanyBefore = (source: string, definition: DefinedTerm, forms: readonly string[]): boolean => {
|
|
48
|
+
const before = source.slice(Math.max(0, definition.span.start - NAME_REACH), definition.span.start);
|
|
49
|
+
return forms.some((form) => form !== "" && occurrences(before, form).some((at) => wordAt(before, form, at)));
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** 文書が当事者として定めた呼び名と、その定義の終わり。 */
|
|
53
|
+
export const partiesOf = (source: string, terms: readonly DefinedTerm[], words: PartyWords): Party[] =>
|
|
54
|
+
terms
|
|
55
|
+
.filter((term) => term.inline)
|
|
56
|
+
.filter((term) => words.shortNames.includes(term.term) || words.roles.includes(term.term) || namedCompanyBefore(source, term, words.companyForms))
|
|
57
|
+
.map((term) => ({ term: term.term, end: term.span.end }));
|
|
58
|
+
|
|
59
|
+
/** 英語の立場の語のあとに付いてよいもの: 複数形と所有の形(Vendors、Vendor's)。 */
|
|
60
|
+
const ENGLISH_ENDING = /^(?:s|['’]s?)?(?![\p{L}\p{N}])/u;
|
|
61
|
+
/** 文や項目の頭("Vendor shall"、"1. Vendor shall")。 */
|
|
62
|
+
const CLAUSE_START = /^[\s\d.()*+-]*$/u;
|
|
63
|
+
|
|
64
|
+
/** 英語の立場の語は、冠詞か所有の語のあと(the Vendor)か、文の頭の大文字の語だけ。小文字の the vendor は普通の名詞。 */
|
|
65
|
+
const englishRoleAt = (text: BodyText, at: number, role: string): boolean => {
|
|
66
|
+
const before = text.text.slice(0, at);
|
|
67
|
+
return ENGLISH_ENDING.test(text.text.slice(at + role.length)) && (CLAUSE_START.test(before) || ENGLISH_LEAD.test(before.slice(-LEAD_REACH)));
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const standsAlone = (source: string, text: BodyText, at: number, role: string): boolean => {
|
|
71
|
+
const offset = text.start + at;
|
|
72
|
+
if (WORD_CHAR.test(source.charAt(offset - 1))) return false;
|
|
73
|
+
return LATIN_START.test(role) ? englishRoleAt(text, at, role) : !/[\p{Script=Han}\p{Script=Katakana}ー]/u.test(source.charAt(offset + role.length));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/** 定義した語がこの位置を覆うか(Receiving Party の Party、本受託者規程の受託者)。 */
|
|
77
|
+
const insideDefined = (source: string, offset: number, role: string, defined: readonly string[]): boolean =>
|
|
78
|
+
defined.some((term) => term !== role && term.includes(role) && occurrences(term, role).some((at) => source.startsWith(term, offset - at)));
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* 当事者を定めた後(after より後)で、立場の語を書いた所。立場の語それ自体を定義した文書では、その語は呼び名なので指さない。
|
|
82
|
+
*/
|
|
83
|
+
export const roleUses = (source: string, texts: readonly BodyText[], roles: readonly string[], defined: readonly string[], after: number): RoleUse[] =>
|
|
84
|
+
roles
|
|
85
|
+
.filter((role) => role !== "" && !defined.includes(role))
|
|
86
|
+
.flatMap((role) =>
|
|
87
|
+
texts.flatMap((text) =>
|
|
88
|
+
occurrences(text.text, role)
|
|
89
|
+
.filter((at) => text.start + at > after && standsAlone(source, text, at, role) && !insideDefined(source, text.start + at, role, defined))
|
|
90
|
+
.map((at) => ({ offset: text.start + at, role })),
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
.toSorted((left, right) => left.offset - right.offset)
|
|
94
|
+
.filter((use, index, all) => all.findIndex((other) => other.offset <= use.offset && use.offset < other.offset + other.role.length) === index);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { StructureIssue } from "./issues.ts";
|
|
2
|
+
import { shownTotal, type Amount } from "./total.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 文の中で書いた合計と内訳。表や箇条書きでなく、一つの文に並べた金額を読む。二つの形がある。
|
|
6
|
+
* 内訳のあとに合計: 「保守費60,000円、運用費30,000円とし、合計100,000円とする」 "$6,000 for hosting and $3,000 for support, for a total of $9,000"
|
|
7
|
+
* 合計のあとに内訳: 「月額150,000円(内訳:保守費90,000円、運用費60,000円)」 "costs $8 per month, made up of a base fee of $6 and a storage fee of $2"
|
|
8
|
+
* 合計の語(total-phrase)はすぐ後ろに合計の金額が来る語、内訳の語(breakdown-phrase)はすぐ後ろから内訳が始まる語。
|
|
9
|
+
* 内訳は合計と同じ単位の金額で、二つ以上。合計の語のすぐ前(内訳の語のすぐ後ろ)から続く並びのどれかの和が合計と合えば
|
|
10
|
+
* 合っているとみなし、どれとも合わないときだけ言う。文の中のほかの金額(単価・上限・遅延損害金)を内訳に数えて誤って言わないため。
|
|
11
|
+
* 値引きの語(discount-word)のすぐ後ろの金額は引く。内訳の語の前の金額は、あいだに並びをつなぐ語(breakdown-gap-joiner)が
|
|
12
|
+
* あれば、別の項目の金額で合計ではない(a $1,000 onboarding fee and a monthly subscription, consisting of …)。
|
|
13
|
+
*/
|
|
14
|
+
export type TotalWords = {
|
|
15
|
+
readonly totals: readonly string[];
|
|
16
|
+
readonly breakdowns: readonly string[];
|
|
17
|
+
readonly discounts: readonly string[];
|
|
18
|
+
readonly joiners: readonly string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type Span = { readonly start: number; readonly end: number };
|
|
22
|
+
|
|
23
|
+
const CENTS = 100;
|
|
24
|
+
const MIN_PARTS = 2;
|
|
25
|
+
|
|
26
|
+
/** 合計の語と金額のあいだに書いてよいもの: 空白と、コロンか読点一つ。 */
|
|
27
|
+
const isTotalGap = (gap: string): boolean => /^[::、,,]?$/u.test(gap.trim());
|
|
28
|
+
const OPENERS = "((";
|
|
29
|
+
const CLOSERS = "))";
|
|
30
|
+
const CLOSER = /[))]/u;
|
|
31
|
+
|
|
32
|
+
const centsOf = (amount: Amount): number => Math.round(amount.value * CENTS);
|
|
33
|
+
|
|
34
|
+
const sumOf = (values: readonly number[]): number => values.reduce((sum, value) => sum + value, 0);
|
|
35
|
+
|
|
36
|
+
/** 英字で始まる語は、英字の続き(subtotal の total)では語と読まない。 */
|
|
37
|
+
const LATIN = /[A-Za-z0-9]/u;
|
|
38
|
+
|
|
39
|
+
const occurrences = (lower: string, phrase: string): number[] =>
|
|
40
|
+
[...lower.matchAll(new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"), "gu"))].map((match) => match.index);
|
|
41
|
+
|
|
42
|
+
/** 語の文の中の位置。重なるときは前から、同じ所から始まるなら長いほうを取る。大文字小文字は区別しない。 */
|
|
43
|
+
export const phraseSpans = (text: string, start: number, phrases: readonly string[]): Span[] => {
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
const found = phrases
|
|
46
|
+
.map((phrase) => phrase.toLowerCase())
|
|
47
|
+
.filter((phrase) => phrase !== "")
|
|
48
|
+
.flatMap((phrase) =>
|
|
49
|
+
occurrences(lower, phrase)
|
|
50
|
+
.filter((at) => !(LATIN.test(phrase[0] ?? "") && LATIN.test(lower[at - 1] ?? "")))
|
|
51
|
+
.map((at) => ({ start: at, end: at + phrase.length })),
|
|
52
|
+
)
|
|
53
|
+
.toSorted((left, right) => left.start - right.start || right.end - left.end);
|
|
54
|
+
return found
|
|
55
|
+
.filter((span, index) => found.slice(0, index).every((before) => before.end <= span.start))
|
|
56
|
+
.map((span) => ({ start: start + span.start, end: start + span.end }));
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/** 金額の前に書いた単位($9,000 の $)を除いた、語と金額のあいだ。 */
|
|
60
|
+
const gapBefore = (source: string, from: number, amount: Amount): string => {
|
|
61
|
+
const gap = source.slice(from, amount.offset);
|
|
62
|
+
const trimmed = gap.trimEnd();
|
|
63
|
+
return trimmed.endsWith(amount.unit) ? trimmed.slice(0, trimmed.length - amount.unit.length) : gap;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** 合計の語に近いほうから続く、二つ以上の並び(後ろ寄せ: 合計の前の内訳、前寄せ: 合計の後ろの内訳)。 */
|
|
67
|
+
const nearRuns = (parts: readonly number[], nearEnd: boolean): number[][] =>
|
|
68
|
+
parts.slice(MIN_PARTS - 1).map((_, index) => (nearEnd ? parts.slice(parts.length - MIN_PARTS - index) : parts.slice(0, MIN_PARTS + index)));
|
|
69
|
+
|
|
70
|
+
/** 内訳の金額。前の金額(並びの始まり)からこの金額までに値引きの語があれば、負の数。 */
|
|
71
|
+
const signedValues = (source: string, parts: readonly Amount[], from: number, discounts: readonly string[]): number[] =>
|
|
72
|
+
parts.map((part, index) => {
|
|
73
|
+
const start = parts[index - 1]?.end ?? from;
|
|
74
|
+
const discounted = phraseSpans(source.slice(start, part.offset), start, discounts).length > 0;
|
|
75
|
+
return discounted ? -centsOf(part) : centsOf(part);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
type Breakdown = { readonly total: Amount; readonly parts: readonly Amount[]; readonly from: number; readonly nearEnd: boolean };
|
|
79
|
+
|
|
80
|
+
const mismatch = (source: string, breakdown: Breakdown, discounts: readonly string[]): StructureIssue[] => {
|
|
81
|
+
const { total, from, nearEnd } = breakdown;
|
|
82
|
+
const parts = breakdown.parts.filter((part) => part.unit === total.unit);
|
|
83
|
+
if (parts.length < MIN_PARTS) return [];
|
|
84
|
+
const values = signedValues(source, parts, from, discounts);
|
|
85
|
+
const written = centsOf(total);
|
|
86
|
+
if (nearRuns(values, nearEnd).some((run) => sumOf(run) === written)) return [];
|
|
87
|
+
return [{ offset: total.offset, values: shownTotal(source, total, sumOf(values)) }];
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** 内訳の語が括弧の中にあれば、その括弧の終わり。無ければ文の終わり。 */
|
|
91
|
+
const breakdownEnd = (source: string, total: Amount, marker: Span, sentence: Span): number => {
|
|
92
|
+
const between = source.slice(total.end, marker.start);
|
|
93
|
+
const opened = [...between].filter((char) => OPENERS.includes(char)).length > [...between].filter((char) => CLOSERS.includes(char)).length;
|
|
94
|
+
if (!opened) return sentence.end;
|
|
95
|
+
const close = CLOSER.exec(source.slice(marker.end, sentence.end));
|
|
96
|
+
return close === null ? sentence.end : marker.end + close.index;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const inside = (amounts: readonly Amount[], start: number, end: number): Amount[] => amounts.filter((amount) => amount.offset >= start && amount.end <= end);
|
|
100
|
+
|
|
101
|
+
/** 合計の語のすぐ後ろの金額を、その前の内訳と比べる。内訳は、前の合計の語か内訳の語より後ろから。 */
|
|
102
|
+
const totalsAfterParts = (source: string, sentence: Span, amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
|
|
103
|
+
const text = source.slice(sentence.start, sentence.end);
|
|
104
|
+
const markers = phraseSpans(text, sentence.start, words.totals);
|
|
105
|
+
const breakdowns = phraseSpans(text, sentence.start, words.breakdowns);
|
|
106
|
+
return markers.flatMap((marker, index) => {
|
|
107
|
+
const total = amounts.find((amount) => amount.offset >= marker.end && isTotalGap(gapBefore(source, marker.end, amount)));
|
|
108
|
+
if (total === undefined) return [];
|
|
109
|
+
const from = Math.max(sentence.start, markers[index - 1]?.end ?? 0, ...breakdowns.filter((span) => span.end <= marker.start).map((span) => span.end));
|
|
110
|
+
return mismatch(source, { total, parts: inside(amounts, from, marker.start), from, nearEnd: true }, words.discounts);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** 内訳の語のすぐ前の金額を合計とし、内訳の語の後ろ(括弧の中なら括弧の終わりまで、次の合計の語まで)の金額と比べる。 */
|
|
115
|
+
const totalsBeforeParts = (source: string, sentence: Span, amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
|
|
116
|
+
const text = source.slice(sentence.start, sentence.end);
|
|
117
|
+
const totalMarkers = phraseSpans(text, sentence.start, words.totals);
|
|
118
|
+
return phraseSpans(text, sentence.start, words.breakdowns).flatMap((marker) => {
|
|
119
|
+
const total = amounts.findLast((amount) => amount.end <= marker.start);
|
|
120
|
+
if (total === undefined || phraseSpans(source.slice(total.end, marker.start), total.end, words.joiners).length > 0) return [];
|
|
121
|
+
const nextTotal = totalMarkers.find((span) => span.start >= marker.end)?.start ?? sentence.end;
|
|
122
|
+
const parts = inside(amounts, marker.end, Math.min(nextTotal, breakdownEnd(source, total, marker, sentence)));
|
|
123
|
+
return mismatch(source, { total, parts, from: marker.end, nearEnd: false }, words.discounts);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** 文ごとに、書いた合計が内訳の和と合わない所。一つの金額は一度だけ言う。 */
|
|
128
|
+
export const proseTotalMismatches = (source: string, sentences: readonly Span[], amounts: readonly Amount[], words: TotalWords): StructureIssue[] => {
|
|
129
|
+
const issues = sentences.flatMap((sentence) => {
|
|
130
|
+
const inSentence = inside(amounts, sentence.start, sentence.end);
|
|
131
|
+
if (inSentence.length <= MIN_PARTS) return [];
|
|
132
|
+
return [...totalsAfterParts(source, sentence, inSentence, words), ...totalsBeforeParts(source, sentence, inSentence, words)];
|
|
133
|
+
});
|
|
134
|
+
return issues.filter((issue, index) => issues.findIndex((other) => other.offset === issue.offset) === index);
|
|
135
|
+
};
|
|
@@ -54,11 +54,24 @@ const nameAfter = (source: string, end: number): { name: string; at: number } |
|
|
|
54
54
|
/** 項や号の札((a)、(ii)、(イ))。名前ではなく、参照の続き。 */
|
|
55
55
|
const ITEM_LABEL = /^(?:[a-z]{1,2}|[ivxlcdm]{1,5}|.)$/iu;
|
|
56
56
|
|
|
57
|
+
const LATIN_ASIDE = /^[\p{Script=Latin}\s.]+$/u;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* The name holds the aside. An English aside is a whole word or phrase: "or" in "Return of Information" is not the aside
|
|
61
|
+
* "or". Other asides are found anywhere (上記 in 上記参照).
|
|
62
|
+
*/
|
|
63
|
+
export const holdsAside = (name: string, aside: string): boolean => {
|
|
64
|
+
if (aside.trim() === "") return false;
|
|
65
|
+
if (!LATIN_ASIDE.test(aside)) return normalized(name).includes(normalized(aside));
|
|
66
|
+
const words = ` ${name
|
|
67
|
+
.normalize("NFKC")
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replaceAll(/[^\p{L}\p{N}]+/gu, " ")} `;
|
|
70
|
+
return aside.trim() !== "" && words.includes(` ${aside.trim().toLowerCase()} `);
|
|
71
|
+
};
|
|
72
|
+
|
|
57
73
|
const isName = (name: string, words: ReferenceWords): boolean =>
|
|
58
|
-
LETTER.test(name) &&
|
|
59
|
-
!DIGIT.test(name) &&
|
|
60
|
-
!ITEM_LABEL.test(name.normalize("NFKC")) &&
|
|
61
|
-
!words.asides.some((aside) => normalized(name).includes(normalized(aside)));
|
|
74
|
+
LETTER.test(name) && !DIGIT.test(name) && !ITEM_LABEL.test(name.normalize("NFKC")) && !words.asides.some((aside) => holdsAside(name, aside));
|
|
62
75
|
|
|
63
76
|
const agrees = (name: string, heading: string): boolean => {
|
|
64
77
|
const [a, b] = [normalized(name), normalized(heading)];
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { BodyText } from "./definition-use.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 定義した語の頭の語(本・本件)を、同じ組のほかの語に替えた形。「本業務」と定義して「本件業務」と書くと、
|
|
5
|
+
* 読み手は別のものを指しているのかと迷う。組は言語パッケージの語彙表(defined-term-prefix)の group から取る。
|
|
6
|
+
*/
|
|
7
|
+
export type PrefixGroups = readonly (readonly string[])[];
|
|
8
|
+
|
|
9
|
+
/** 語の前後の一字がこれなら、語は長い語の一部(見本物品の「本物品」、本業務委託契約の「本業務」)。 */
|
|
10
|
+
const WORD_CHAR = /[\p{Script=Han}\p{Script=Katakana}\p{Script=Latin}\p{N}ー]/u;
|
|
11
|
+
|
|
12
|
+
/** 頭の語のあとがこれより短い語(本件人、本件日)を替えると、普通の語(本人、本日)になる。 */
|
|
13
|
+
const MIN_REST = 2;
|
|
14
|
+
|
|
15
|
+
/** 語彙表の項目を group ごとにまとめる。group の無い項目と、一つしかない組は外す。 */
|
|
16
|
+
export const prefixGroupsOf = (entries: readonly { readonly pattern: string; readonly group?: string | undefined }[]): string[][] => {
|
|
17
|
+
const groups = new Map<string, string[]>();
|
|
18
|
+
entries.forEach((entry) => {
|
|
19
|
+
if (entry.group === undefined || entry.pattern === "") return;
|
|
20
|
+
groups.set(entry.group, [...(groups.get(entry.group) ?? []), entry.pattern]);
|
|
21
|
+
});
|
|
22
|
+
return [...groups.values()].filter((group) => group.length > 1);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** 語の頭の語を同じ組のほかの語に替えた形。頭の語は長いほうから当て、頭の語のあとが一字の語には形が無い。 */
|
|
26
|
+
export const prefixVariants = (term: string, groups: PrefixGroups): string[] =>
|
|
27
|
+
groups.flatMap((group) => {
|
|
28
|
+
const prefix = group
|
|
29
|
+
.filter((candidate) => term.startsWith(candidate) && term.length > candidate.length)
|
|
30
|
+
.toSorted((left, right) => right.length - left.length)[0];
|
|
31
|
+
const rest = prefix === undefined ? "" : term.slice(prefix.length);
|
|
32
|
+
if (prefix === undefined || [...rest].length < MIN_REST) return [];
|
|
33
|
+
return group.filter((other) => other !== prefix).map((other) => `${other}${rest}`);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 定義の後ろ(after より後)で、形の違う語を書いた位置。長い語の一部と、定義した語の始まり(本件物品明細と定義した所の本件物品)は除く。
|
|
38
|
+
*/
|
|
39
|
+
export const variantUses = (source: string, texts: readonly BodyText[], variant: string, after: number, defined: readonly string[]): number[] =>
|
|
40
|
+
texts.flatMap((text) => {
|
|
41
|
+
const offsets: number[] = [];
|
|
42
|
+
let at = text.text.indexOf(variant);
|
|
43
|
+
while (at !== -1) {
|
|
44
|
+
offsets.push(text.start + at);
|
|
45
|
+
at = text.text.indexOf(variant, at + variant.length);
|
|
46
|
+
}
|
|
47
|
+
return offsets.filter(
|
|
48
|
+
(offset) =>
|
|
49
|
+
offset > after &&
|
|
50
|
+
!WORD_CHAR.test(source.charAt(offset - 1)) &&
|
|
51
|
+
!WORD_CHAR.test(source.charAt(offset + variant.length)) &&
|
|
52
|
+
!defined.some((term) => source.startsWith(term, offset)),
|
|
53
|
+
);
|
|
54
|
+
});
|
package/src/structure/total.ts
CHANGED
|
@@ -154,6 +154,10 @@ const shownAmounts = (source: string, total: Placed, sumCents: number): Record<s
|
|
|
154
154
|
return { written, sum: `${sumSign}${sum}${written.endsWith(total.unit) ? total.unit : ""}` };
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
/** 文の中の合計の金額(符号の無い、単位付きの金額)と和を、合計の書き方で見せる。 */
|
|
158
|
+
export const shownTotal = (source: string, total: Amount, sumCents: number): Record<string, string> =>
|
|
159
|
+
shownAmounts(source, { ...total, column: 0, cents: Math.round(total.value * CENTS) }, sumCents);
|
|
160
|
+
|
|
157
161
|
/**
|
|
158
162
|
* 数だけの列で、合計の行の数が項目のどれかより大きくなければ、和ではなく平均や率や年の列と読んで足さない。
|
|
159
163
|
* 項目に負の数があれば、和が項目より小さいこともあるので足す。
|
|
@@ -1,14 +1,13 @@
|
|
|
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";
|
|
3
|
+
import { PACKAGE_DIR, readText } from "../package-files.ts";
|
|
5
4
|
import { parseStructureBaseline, type StructureBaseline } from "./baseline.ts";
|
|
6
5
|
|
|
7
|
-
const BASELINE_FILE = join(
|
|
6
|
+
const BASELINE_FILE = join(PACKAGE_DIR, "structure-baseline.yaml");
|
|
8
7
|
|
|
9
8
|
const readBaseline = (): StructureBaseline => {
|
|
10
9
|
try {
|
|
11
|
-
return parseStructureBaseline(parse(
|
|
10
|
+
return parseStructureBaseline(parse(readText(BASELINE_FILE)));
|
|
12
11
|
} catch (error) {
|
|
13
12
|
throw new Error(`cannot read the structure baseline ${BASELINE_FILE}`, { cause: error });
|
|
14
13
|
}
|
package/src/style-load.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
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";
|
|
3
|
+
import { PACKAGE_DIR, readDir, readText } from "./package-files.ts";
|
|
5
4
|
import { parseStyle, type StyleDefinition } from "./style-parse.ts";
|
|
6
5
|
|
|
7
|
-
const STYLES_DIR = join(
|
|
6
|
+
const STYLES_DIR = join(PACKAGE_DIR, "styles");
|
|
8
7
|
|
|
9
8
|
const loaded: { value: readonly StyleDefinition[] | undefined } = { value: undefined };
|
|
10
9
|
|
|
11
10
|
const readStyle = (dir: string, file: string): StyleDefinition => {
|
|
12
11
|
try {
|
|
13
|
-
return parseStyle(parse(
|
|
12
|
+
return parseStyle(parse(readText(join(dir, file))), file);
|
|
14
13
|
} catch (error) {
|
|
15
14
|
throw new Error(`cannot read ${join(dir, file)}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
16
15
|
}
|
|
@@ -19,7 +18,7 @@ const readStyle = (dir: string, file: string): StyleDefinition => {
|
|
|
19
18
|
/** The bundled house styles (styles/*.yaml), by file name. Read once. */
|
|
20
19
|
export const loadStyles = (dir: string = STYLES_DIR): readonly StyleDefinition[] => {
|
|
21
20
|
if (dir === STYLES_DIR && loaded.value !== undefined) return loaded.value;
|
|
22
|
-
const styles =
|
|
21
|
+
const styles = readDir(dir)
|
|
23
22
|
.filter((file) => file.endsWith(".yaml"))
|
|
24
23
|
.toSorted((left, right) => left.localeCompare(right, "en"))
|
|
25
24
|
.map((file) => readStyle(dir, file));
|