chaffjs 0.25.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/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/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/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/date-without-year.yaml +28 -3
- package/rules/defined-term-form.yaml +9 -6
- package/rules/demonstrative-opener-run.yaml +24 -7
- 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 +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/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/date-year.ts +51 -12
- package/src/detectors/defined-term-form.ts +28 -6
- 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/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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// A quotation the text gives to someone (「…」と述べている, according to X, "…") in a paragraph that names no source for it:
|
|
2
|
+
// no link, no footnote or numbered citation, no year in brackets. Whether the words are really that person's is for
|
|
3
|
+
// `chaff cite` against the source; this only says that there is nothing to check them against. The words that give a
|
|
4
|
+
// quotation to someone come from the language package's word list quote-attribution. Pure.
|
|
5
|
+
import { QUOTATION_MARKS, quotedSpans } from "../quoted-span.ts";
|
|
6
|
+
import type { Detector, Finding, LexiconEntry, ProseDocument } from "../plugin.ts";
|
|
7
|
+
|
|
8
|
+
export type QuoteText = { readonly start: number; readonly text: string };
|
|
9
|
+
|
|
10
|
+
export type QuoteParagraph = { readonly source: string; readonly sentences: readonly QuoteText[] };
|
|
11
|
+
|
|
12
|
+
export type UnsourcedQuote = { readonly offset: number; readonly quote: string };
|
|
13
|
+
|
|
14
|
+
/** Shorter than this, a quotation is a term or a label (「保存」, "Save"), not someone's words. */
|
|
15
|
+
const MIN_QUOTE_CHARS = 10;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A link, a bare URL, a footnote mark, a numbered citation ([3]) or a year that ends a bracket as a citation's does: alone
|
|
19
|
+
* ((1975), (1999年)), after a comma ((架空の本、1975年)) or after a name ((Brooks 1975)). Not (port 2000).
|
|
20
|
+
*/
|
|
21
|
+
const SOURCE_MARKS = [
|
|
22
|
+
/\]\(/u,
|
|
23
|
+
/https?:\/\//u,
|
|
24
|
+
/\[\^[^\]\s]+\]/u,
|
|
25
|
+
/\[\d{1,3}(?:[,,、–-]\s*\d{1,3})*\]/u,
|
|
26
|
+
/[((](?:1[5-9]|20)\d\d[a-z]?年?[))]/u,
|
|
27
|
+
/[((][^()()\n]*[,、,]\s*(?:1[5-9]|20)\d\d[a-z]?年?[))]/u,
|
|
28
|
+
/[((]\p{Lu}[^()()\n]*\s(?:1[5-9]|20)\d\d[a-z]?[))]/u,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
/** How far before a quotation a cue may end ("According to the study, “…”", 先輩によれば「…」). */
|
|
32
|
+
const CUE_REACH_BEFORE = 40;
|
|
33
|
+
/** How far after a quotation a cue may start: a space ("“…,” said"), or none (「…」と述べた, not 「…」などと書く). */
|
|
34
|
+
const CUE_REACH_AFTER = 1;
|
|
35
|
+
|
|
36
|
+
const LATIN = /\p{Script=Latin}/u;
|
|
37
|
+
const LETTER = /\p{L}/u;
|
|
38
|
+
|
|
39
|
+
const namesSource = (paragraph: string): boolean => SOURCE_MARKS.some((mark) => mark.test(paragraph));
|
|
40
|
+
|
|
41
|
+
const escaped = (word: string): string => word.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Where the cue stands in the sentence, ignoring case, in the sentence's own positions (lowercasing first would move them:
|
|
45
|
+
* İ becomes two units). An English cue is not read inside a longer word (says in essays).
|
|
46
|
+
*/
|
|
47
|
+
const cueStarts = (text: string, cue: string): number[] => {
|
|
48
|
+
if (cue === "") return [];
|
|
49
|
+
return [...text.matchAll(new RegExp(escaped(cue), "giu"))]
|
|
50
|
+
.map((match) => match.index)
|
|
51
|
+
.filter((at) => !LATIN.test(cue) || (!LETTER.test(text.charAt(at - 1)) && !LETTER.test(text.charAt(at + cue.length))));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type Quoted = { readonly start: number; readonly end: number };
|
|
55
|
+
|
|
56
|
+
/** A cue just before the quotation's opening mark, or just after its closing mark. position says which side it may stand on. */
|
|
57
|
+
const isAttributed = (text: string, quoted: Quoted, cues: readonly LexiconEntry[]): boolean =>
|
|
58
|
+
cues.some((cue) =>
|
|
59
|
+
cueStarts(text, cue.pattern).some((at) => {
|
|
60
|
+
const before = cue.position !== "after" && at + cue.pattern.length <= quoted.start - 1 && at + cue.pattern.length >= quoted.start - 1 - CUE_REACH_BEFORE;
|
|
61
|
+
const after = cue.position !== "before" && at >= quoted.end + 1 && at <= quoted.end + 1 + CUE_REACH_AFTER;
|
|
62
|
+
return before || after;
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const quotesIn = (sentence: QuoteText, cues: readonly LexiconEntry[]): UnsourcedQuote[] => {
|
|
67
|
+
return quotedSpans(sentence.text, QUOTATION_MARKS)
|
|
68
|
+
.filter((span) => [...sentence.text.slice(span.start, span.end).trim()].length >= MIN_QUOTE_CHARS && isAttributed(sentence.text, span, cues))
|
|
69
|
+
.map((span) => ({ offset: sentence.start + span.start, quote: sentence.text.slice(span.start, span.end).trim() }));
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Each quotation given to someone, in a paragraph that names no source. cues: the word list quote-attribution. */
|
|
73
|
+
export const unsourcedQuotes = (paragraphs: readonly QuoteParagraph[], cues: readonly LexiconEntry[]): UnsourcedQuote[] =>
|
|
74
|
+
paragraphs.filter((paragraph) => !namesSource(paragraph.source)).flatMap((paragraph) => paragraph.sentences.flatMap((sentence) => quotesIn(sentence, cues)));
|
|
75
|
+
|
|
76
|
+
export const quoteWithoutSource: Detector = (doc: ProseDocument): Finding[] =>
|
|
77
|
+
unsourcedQuotes(
|
|
78
|
+
doc.paragraphs.map((paragraph) => ({
|
|
79
|
+
source: doc.source.slice(paragraph.span.start, paragraph.span.end),
|
|
80
|
+
sentences: paragraph.sentences.map((sentence) => ({ start: sentence.span.start, text: sentence.text })),
|
|
81
|
+
})),
|
|
82
|
+
doc.lexicons["quote-attribution"] ?? [],
|
|
83
|
+
).map((hit) => ({
|
|
84
|
+
rule: "quote-without-source",
|
|
85
|
+
severity: "info",
|
|
86
|
+
line: 0,
|
|
87
|
+
column: 0,
|
|
88
|
+
quote: hit.quote,
|
|
89
|
+
values: { quote: hit.quote, offset: hit.offset },
|
|
90
|
+
}));
|
|
@@ -12,7 +12,7 @@ const inputOf = (doc: ProseDocument): ReferenceInput | undefined => {
|
|
|
12
12
|
return {
|
|
13
13
|
source: doc.source,
|
|
14
14
|
references: inDocumentOrder(tree).filter((node) => node.kind === "reference" && node.attrs["document"] === undefined),
|
|
15
|
-
targetOf: referenceResolver(tree),
|
|
15
|
+
targetOf: referenceResolver(tree, doc.source),
|
|
16
16
|
words: {
|
|
17
17
|
asides: patternsOf(doc, "reference-aside"),
|
|
18
18
|
topicLinks: patternsOf(doc, "reference-topic-link"),
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Span } from "../plugin.ts";
|
|
2
|
+
|
|
3
|
+
/** 行の頭の引用の印と字下げ、箇条の印、チェックボックス。この後ろで太字が開く。 */
|
|
4
|
+
const QUOTE_INDENT = /^(?:[ \t]*>)*[ \t]*/u;
|
|
5
|
+
const ITEM_MARKER = /^(?:[-*+]|\d{1,9}[.)])[ \t]+/u;
|
|
6
|
+
const TASK_BOX = /^\[[ xX]\][ \t]+/u;
|
|
7
|
+
const BOLD = /^(?:\*\*|__)$/u;
|
|
8
|
+
|
|
9
|
+
type Opening = { readonly marker: string; readonly item: boolean };
|
|
10
|
+
|
|
11
|
+
/** 行の頭から文の始まりまで(before)が、印の後ろで開いた太字だけか。 */
|
|
12
|
+
const openingOf = (before: string): Opening | undefined => {
|
|
13
|
+
const unquoted = before.replace(QUOTE_INDENT, "");
|
|
14
|
+
const rest = unquoted.replace(ITEM_MARKER, "").replace(TASK_BOX, "");
|
|
15
|
+
return BOLD.test(rest) ? { marker: rest, item: rest.length < unquoted.length } : undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** 段落の切れ目になる前の行: 空の行(引用の中の空の行も)と見出し。 */
|
|
19
|
+
const BLOCK_BEFORE = /^(?:[ \t]*>)*[ \t]*(?:#.*)?$/u;
|
|
20
|
+
|
|
21
|
+
/** 文の終わりの句読点。札の句点を太字の外に書く形(**速く書ける**。)もある。 */
|
|
22
|
+
const STOP = /[。..!?!?]$/u;
|
|
23
|
+
|
|
24
|
+
const lineAround = (source: string, offset: number): { readonly start: number; readonly end: number } => {
|
|
25
|
+
const end = source.indexOf("\n", offset);
|
|
26
|
+
return { start: source.lastIndexOf("\n", offset - 1) + 1, end: end === -1 ? source.length : end };
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** 太字が閉じる位置。文の直後か、文末の句読点の直前。 */
|
|
30
|
+
const closingOf = (source: string, span: Span, marker: string): number | undefined => {
|
|
31
|
+
if (source.startsWith(marker, span.end)) return span.end;
|
|
32
|
+
const stop = STOP.exec(source.slice(span.start, span.end))?.[0];
|
|
33
|
+
const before = stop === undefined ? -1 : span.end - stop.length - marker.length;
|
|
34
|
+
return before > span.start && source.startsWith(marker, before) ? before : undefined;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** 文全体を一つの太字が包むなら、太字と文の後ろの位置。途中で閉じる太字(**a** と **b。**)は包んでいない。 */
|
|
38
|
+
const closedAt = (source: string, span: Span, marker: string): number | undefined => {
|
|
39
|
+
const closing = closingOf(source, span, marker);
|
|
40
|
+
if (closing === undefined || source.slice(span.start, closing).includes(marker)) return undefined;
|
|
41
|
+
return Math.max(closing + marker.length, span.end);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** 行の頭が段落の頭か。箇条の印のある行は項目の頭。折り返した段落の続きの行(前の行に文がある)は頭ではない。 */
|
|
45
|
+
const opensBlock = (source: string, lineStart: number, item: boolean): boolean => {
|
|
46
|
+
if (item || lineStart === 0) return true;
|
|
47
|
+
return BLOCK_BEFORE.test(source.slice(source.lastIndexOf("\n", lineStart - 2) + 1, lineStart - 1));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 行の頭の太字だけでできた文で、同じ行に続きがあるもの(「- **速く書ける。** 細かな文法を覚えなくても書けます。」の「速く書ける。」)。
|
|
52
|
+
* 項目や段落の札で、見出しと同じく本文の文ではない。太字だけの行(「**注意してください。**」)は札ではなく文。
|
|
53
|
+
*/
|
|
54
|
+
export const isRunInLabel = (source: string, span: Span): boolean => {
|
|
55
|
+
const line = lineAround(source, span.start);
|
|
56
|
+
const opening = openingOf(source.slice(line.start, span.start));
|
|
57
|
+
if (opening === undefined || !opensBlock(source, line.start, opening.item)) return false;
|
|
58
|
+
const after = closedAt(source, span, opening.marker);
|
|
59
|
+
return after !== undefined && after <= line.end && /\S/u.test(source.slice(after, line.end));
|
|
60
|
+
};
|
|
@@ -73,12 +73,20 @@ export const sentenceConjunctiveCount: Detector = (doc, options): Finding[] => {
|
|
|
73
73
|
|
|
74
74
|
type Opened = { readonly sentence: Sentence; readonly opener: string | undefined };
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
/**
|
|
77
|
+
* The lexicon's word the sentence opens with, unless it opens with one of `notOpeners`: "This Agreement" and "This section"
|
|
78
|
+
* point at the document itself, not back at the sentence before.
|
|
79
|
+
*/
|
|
80
|
+
const openerOf = (sentence: Sentence, lexicon: Lexicon, notOpeners: Lexicon): string | undefined =>
|
|
81
|
+
notOpeners.some((entry) => entryOpens(sentence, entry)) ? undefined : lexicon.find((entry) => entryOpens(sentence, entry))?.pattern;
|
|
82
|
+
|
|
83
|
+
/** The phrases that open a sentence with a lexicon word but do not point back (this-document references). */
|
|
84
|
+
export const NOT_OPENER_LEXICON = "demonstrative-not-opener";
|
|
77
85
|
|
|
78
86
|
/** Runs of consecutive sentences in one paragraph that each open with a lexicon word. */
|
|
79
|
-
export const openerRuns = (sentences: readonly Sentence[], lexicon: Lexicon): Opened[][] =>
|
|
87
|
+
export const openerRuns = (sentences: readonly Sentence[], lexicon: Lexicon, notOpeners: Lexicon = []): Opened[][] =>
|
|
80
88
|
sentences
|
|
81
|
-
.map((sentence) => ({ sentence, opener: openerOf(sentence, lexicon) }))
|
|
89
|
+
.map((sentence) => ({ sentence, opener: openerOf(sentence, lexicon, notOpeners) }))
|
|
82
90
|
.reduce<Opened[][]>(
|
|
83
91
|
(runs, opened) => {
|
|
84
92
|
if (opened.opener === undefined) return [...runs, []];
|
|
@@ -104,8 +112,9 @@ const runFinding = (run: readonly Opened[], options: DetectorOptions): Finding =
|
|
|
104
112
|
/** More than limit sentences in a row within one paragraph, each opening with a word from the lexicon (これ, その). */
|
|
105
113
|
export const openerRun: Detector = (doc, options): Finding[] => {
|
|
106
114
|
const lexicon = options.lexicon ?? [];
|
|
115
|
+
const notOpeners = doc.lexicons[NOT_OPENER_LEXICON] ?? [];
|
|
107
116
|
return doc.paragraphs.flatMap((paragraph) =>
|
|
108
|
-
openerRuns(paragraph.sentences, lexicon)
|
|
117
|
+
openerRuns(paragraph.sentences, lexicon, notOpeners)
|
|
109
118
|
.filter((run) => run.length > options.limit)
|
|
110
119
|
.map((run) => runFinding(run, options)),
|
|
111
120
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { endingTokens, isClosed } from "../sentence-shape.ts";
|
|
2
2
|
import { enumeratedRuns, enumeratorStarts, numberedStarts } from "./enumerated-runs.ts";
|
|
3
3
|
import { continuesInto, groupOf, registerOf, slipsOf, type Register } from "./register.ts";
|
|
4
|
+
import { isRunInLabel } from "./run-in-label.ts";
|
|
4
5
|
import type { Detector, Finding, LexiconEntry, ProseDocument, Sentence, Span } from "../plugin.ts";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -8,7 +9,7 @@ import type { Detector, Finding, LexiconEntry, ProseDocument, Sentence, Span } f
|
|
|
8
9
|
*
|
|
9
10
|
* どちらが正しいかは決めない。決めると、方針の違う書き手にそのまま無視される。
|
|
10
11
|
* 見るのは文書の中での一貫性だけで、少数派のほうを指摘する。spec §12.3 の方針を日本語にも。
|
|
11
|
-
* 終止符で終わらないもの(見出しの下の `MaaSサービス`
|
|
12
|
+
* 終止符で終わらないもの(見出しの下の `MaaSサービス` のような名前だけの行)と、項目や段落の頭の太字の札(**速く書ける。**)は文として数えない。
|
|
12
13
|
*/
|
|
13
14
|
type Entry = { readonly sentence: Sentence; readonly register: Register; readonly group: number | undefined };
|
|
14
15
|
|
|
@@ -35,7 +36,7 @@ export const judgedSentences = (doc: ProseDocument, polite: readonly LexiconEntr
|
|
|
35
36
|
const neutral = doc.lexicons["neutral-ending"] ?? [];
|
|
36
37
|
const lists = doc.lists.map((list) => list.span);
|
|
37
38
|
const found = doc.sentences.flatMap((sentence, index): { sentence: Sentence; register: Register }[] => {
|
|
38
|
-
if (!isClosed(sentence) || continuesInto(sentence, doc.sentences[index + 1])) return [];
|
|
39
|
+
if (!isClosed(sentence) || continuesInto(sentence, doc.sentences[index + 1]) || isRunInLabel(doc.source, sentence.span)) return [];
|
|
39
40
|
const register = registerOfSentence(sentence, polite, neutral);
|
|
40
41
|
return register === undefined ? [] : [{ sentence, register }];
|
|
41
42
|
});
|
|
@@ -4,6 +4,7 @@ import { numberingBreaks } from "../structure/numbering.ts";
|
|
|
4
4
|
import { weekdayMismatches } from "../structure/weekday.ts";
|
|
5
5
|
import { dateOrderBreaks } from "../structure/date-order.ts";
|
|
6
6
|
import { totalMismatches, type Amount } from "../structure/total.ts";
|
|
7
|
+
import { proseTotalMismatches } from "../structure/prose-total.ts";
|
|
7
8
|
import { rangeFrameOf, reversedRanges, type DatedSpan, type RangeWords } from "../structure/date-range.ts";
|
|
8
9
|
import { percentSumMismatches, type ShareWords } from "../structure/percent-sum.ts";
|
|
9
10
|
import { isQuotedAlone } from "../quoted-span.ts";
|
|
@@ -104,15 +105,35 @@ const amountsOf = (tree: NonNullable<ProseDocument["structure"]>): Amount[] =>
|
|
|
104
105
|
node.kind === "quantity" ? [{ offset: node.span.start, end: node.span.end, value: Number(node.attrs["value"]), unit: String(node.attrs["unit"]) }] : [],
|
|
105
106
|
);
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
const patternsOf = (doc: ProseDocument, lexicon: string): string[] => (doc.lexicons[lexicon] ?? []).map((entry) => entry.pattern);
|
|
109
|
+
|
|
110
|
+
/** 合計の行と内訳の行、文の中の合計と内訳。同じ金額は一度だけ言う。 */
|
|
111
|
+
const totalIssues = (doc: ProseDocument, tree: NonNullable<ProseDocument["structure"]>): StructureIssue[] => {
|
|
112
|
+
const amounts = amountsOf(tree);
|
|
113
|
+
const lines = totalMismatches(doc.source, amounts, patternsOf(doc, "total-label"));
|
|
114
|
+
const words = {
|
|
115
|
+
totals: patternsOf(doc, "total-phrase"),
|
|
116
|
+
breakdowns: patternsOf(doc, "breakdown-phrase"),
|
|
117
|
+
discounts: patternsOf(doc, "discount-word"),
|
|
118
|
+
joiners: patternsOf(doc, "breakdown-gap-joiner"),
|
|
119
|
+
};
|
|
120
|
+
const prose = proseTotalMismatches(
|
|
121
|
+
doc.source,
|
|
122
|
+
doc.sentences.map((sentence) => sentence.span),
|
|
123
|
+
amounts,
|
|
124
|
+
words,
|
|
125
|
+
);
|
|
126
|
+
return [...lines, ...prose.filter((issue) => !lines.some((line) => line.offset === issue.offset))];
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 合計が内訳の和と合わない。表と箇条書きの合計の行(合計の語は total-label)と、文の中の合計(total-phrase、breakdown-phrase)。
|
|
131
|
+
* 語は言語パッケージの語彙表から取る。
|
|
132
|
+
*/
|
|
108
133
|
export const totalMismatch: Detector = (doc): Finding[] =>
|
|
109
134
|
doc.structure === undefined
|
|
110
135
|
? []
|
|
111
|
-
:
|
|
112
|
-
doc.source,
|
|
113
|
-
amountsOf(doc.structure),
|
|
114
|
-
(doc.lexicons["total-label"] ?? []).map((entry) => entry.pattern),
|
|
115
|
-
).map((issue) => ({
|
|
136
|
+
: totalIssues(doc, doc.structure).map((issue) => ({
|
|
116
137
|
rule: "total-mismatch",
|
|
117
138
|
severity: "error",
|
|
118
139
|
line: 0,
|
|
@@ -127,6 +148,7 @@ const rangeWordsOf = (doc: ProseDocument): RangeWords => ({
|
|
|
127
148
|
closers: (doc.lexicons["range-closer"] ?? []).map((entry) => entry.pattern),
|
|
128
149
|
frames: (doc.lexicons["range-frame"] ?? []).flatMap((entry) => rangeFrameOf(entry.pattern) ?? []),
|
|
129
150
|
changes: (doc.lexicons["date-change-word"] ?? []).map((entry) => entry.pattern),
|
|
151
|
+
weekdays: (doc.lexicons["weekday"] ?? []).map((entry) => entry.pattern),
|
|
130
152
|
});
|
|
131
153
|
|
|
132
154
|
/** 期間の終わりが始まりより前。範囲の記号と語は言語パッケージの語彙表(range-connector、range-opener、range-closer)から取る。 */
|
|
@@ -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
|
+
}));
|
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,
|