slopless 0.2.23 → 0.2.24
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/dist/rules/semantic-thinness/patterns/empty-quantification.json +33 -0
- package/dist/rules/semantic-thinness/private/pattern-data-e.js +2 -0
- package/dist/rules/semantic-thinness/private/pattern-matcher.js +6 -1
- package/dist/rules/syntactic-patterns/closers/affirmation-closers.js +13 -3
- package/dist/rules/syntactic-patterns/contrast/private/copular-reframe.js +41 -0
- package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-matcher.js +16 -68
- package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-parts.js +66 -10
- package/dist/rules/syntactic-patterns/contrast/private/negative-slop-frames.js +110 -7
- package/dist/rules/syntactic-patterns/contrast/private/policy-object.js +55 -0
- package/dist/rules/syntactic-patterns/contrast/private/reframe-classification.js +193 -0
- package/dist/rules/syntactic-patterns/lead-ins/generic-signposting.js +14 -83
- package/dist/rules/syntactic-patterns/lead-ins/private/discourse-evaluation.js +149 -4
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "empty-quantification",
|
|
3
|
+
"class": "empty-quantification",
|
|
4
|
+
"purpose": "Catch claims that minimize an unexplained act of quantification instead of naming the metric, measurement, or result.",
|
|
5
|
+
"matchMode": "suffix",
|
|
6
|
+
"maxTokens": 24,
|
|
7
|
+
"templates": ["{minimizer} put a number on {deicticObject}."],
|
|
8
|
+
"slots": {
|
|
9
|
+
"minimizer": ["just", "simply", "merely", "basically"],
|
|
10
|
+
"deicticObject": ["it", "this", "that"]
|
|
11
|
+
},
|
|
12
|
+
"rejectIf": [
|
|
13
|
+
"sentence names the metric, measured value, scale, or numbered label",
|
|
14
|
+
"sentence explains how the number was calculated or where it came from",
|
|
15
|
+
"sentence uses put literally rather than as an empty quantification claim"
|
|
16
|
+
],
|
|
17
|
+
"positiveExamples": [
|
|
18
|
+
"The report just put a number on it.",
|
|
19
|
+
"The dashboard simply put a number on this.",
|
|
20
|
+
"The analysis merely put a number on that.",
|
|
21
|
+
"The model basically put a number on it."
|
|
22
|
+
],
|
|
23
|
+
"negativeExamples": [
|
|
24
|
+
"The analyst labeled the third bar 42.",
|
|
25
|
+
"The meter measured the pressure at 18 PSI.",
|
|
26
|
+
"The report calculated the score from twelve survey responses.",
|
|
27
|
+
"She put the numbered label on the archive box."
|
|
28
|
+
],
|
|
29
|
+
"notes": [
|
|
30
|
+
"The template begins at the minimizer so suffix mode permits arbitrary preceding subjects without enumerating them.",
|
|
31
|
+
"Suffix mode permits arbitrary preceding subjects but requires the vague quantification phrase to end the sentence, excluding measurements, explanations, and literal continuations."
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import emptyQuantification from "../patterns/empty-quantification.json" with { type: "json" };
|
|
1
2
|
import recursiveMeaningFrame from "../patterns/recursive-meaning-frame.json" with { type: "json" };
|
|
2
3
|
export const semanticThinnessPatternSetE = [
|
|
4
|
+
emptyQuantification,
|
|
3
5
|
recursiveMeaningFrame
|
|
4
6
|
];
|
|
5
7
|
//# sourceMappingURL=pattern-data-e.js.map
|
|
@@ -86,7 +86,12 @@ function compileSlotValues(slots) {
|
|
|
86
86
|
return compiled;
|
|
87
87
|
}
|
|
88
88
|
export function compileSemanticThinnessPatterns(patterns) {
|
|
89
|
-
const matchModeFor = (pattern) =>
|
|
89
|
+
const matchModeFor = (pattern) => {
|
|
90
|
+
if (pattern.matchMode === "contains" || pattern.matchMode === "suffix") {
|
|
91
|
+
return pattern.matchMode;
|
|
92
|
+
}
|
|
93
|
+
return "full";
|
|
94
|
+
};
|
|
90
95
|
const defaultMaxTokensFor = (pattern) => matchModeFor(pattern) === "contains"
|
|
91
96
|
? CONTAINS_MAX_TOKENS
|
|
92
97
|
: FULL_MAX_TOKENS;
|
|
@@ -2,6 +2,10 @@ import { defineTextlintRule } from "../../../adapters/textlint/rule.js";
|
|
|
2
2
|
import { sectionLastSentenceUnits, sentenceUnits } from "../../../adapters/textlint/units.js";
|
|
3
3
|
import { wordTokens } from "../../../shared/text/tokens.js";
|
|
4
4
|
const CLOSERS = ["and that's the key.", "that's what matters."];
|
|
5
|
+
const EXACT_FORMULA_LINES = new Set([
|
|
6
|
+
"that's the good stuff",
|
|
7
|
+
"that is the good stuff"
|
|
8
|
+
]);
|
|
5
9
|
const FORMULA_TAILS = [
|
|
6
10
|
"key",
|
|
7
11
|
"point",
|
|
@@ -13,9 +17,15 @@ const FORMULA_TAILS = [
|
|
|
13
17
|
];
|
|
14
18
|
function isFormulaLine(text) {
|
|
15
19
|
const lower = text.toLocaleLowerCase("en");
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
let punctuationStart = lower.length;
|
|
21
|
+
while ([".", "!", "?"].includes(lower[punctuationStart - 1] ?? "")) {
|
|
22
|
+
punctuationStart -= 1;
|
|
23
|
+
}
|
|
24
|
+
const withoutTerminalPunctuation = lower.slice(0, punctuationStart);
|
|
25
|
+
return (EXACT_FORMULA_LINES.has(withoutTerminalPunctuation) ||
|
|
26
|
+
(wordTokens(text).length <= 6 &&
|
|
27
|
+
(lower.startsWith("that's the ") || lower.startsWith("that is the ")) &&
|
|
28
|
+
FORMULA_TAILS.some((tail) => lower.includes(tail))));
|
|
19
29
|
}
|
|
20
30
|
const rule = defineTextlintRule({
|
|
21
31
|
detector: {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NEGATION_WORDS, findCopularNegation, pronounCopulaStart, skipOptionalAdverbs, startsWithPronounCopula, startsWithWords, startsWithSubjectCopula, validSubject, words } from "./negation-reframe-parts.js";
|
|
2
|
+
export function sameSubjectCopularReframe(aTokens, bTokens) {
|
|
3
|
+
const negation = findCopularNegation(aTokens);
|
|
4
|
+
if (negation === undefined || !validSubject(negation.subject)) {
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return startsWithSubjectCopula(bTokens, negation.subject, negation.affirmativeAux);
|
|
8
|
+
}
|
|
9
|
+
export function pronounCopularReframe(aTokens, bTokens) {
|
|
10
|
+
const negation = findCopularNegation(aTokens);
|
|
11
|
+
return (negation !== undefined &&
|
|
12
|
+
validSubject(negation.subject) &&
|
|
13
|
+
!startsWithNegatedPronounCopula(bTokens) &&
|
|
14
|
+
startsWithPronounCopula(bTokens));
|
|
15
|
+
}
|
|
16
|
+
export function progressiveVerbMirror(aTokens, bTokens) {
|
|
17
|
+
const negation = findCopularNegation(aTokens);
|
|
18
|
+
if (negation === undefined || !validSubject(negation.subject)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
const tokenWords = words(aTokens);
|
|
22
|
+
const predicateIndex = skipOptionalAdverbs(tokenWords, negation.negatedPredicateStart);
|
|
23
|
+
const verb = tokenWords[predicateIndex];
|
|
24
|
+
return (verb !== undefined &&
|
|
25
|
+
verb.endsWith("ing") &&
|
|
26
|
+
startsWithWords(bTokens, [
|
|
27
|
+
...negation.subject,
|
|
28
|
+
negation.affirmativeAux,
|
|
29
|
+
verb
|
|
30
|
+
]));
|
|
31
|
+
}
|
|
32
|
+
export function startsWithNegatedPronounCopula(tokens) {
|
|
33
|
+
const start = pronounCopulaStart(tokens);
|
|
34
|
+
if (start === undefined) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
const tokenWords = words(tokens);
|
|
38
|
+
const predicateIndex = skipOptionalAdverbs(tokenWords, start.predicateStart);
|
|
39
|
+
return NEGATION_WORDS.has(tokenWords[predicateIndex] ?? "");
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=copular-reframe.js.map
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DO_NEGATIONS, EXPLICIT_DO_AUXILIARIES,
|
|
1
|
+
import { DO_NEGATIONS, EXPLICIT_DO_AUXILIARIES, contrastPivotSubject, findCopularNegation, findNegationIndex, isCompleteSentence, skipOptionalAdverbs, startsWithSubjectOrPronoun, startsWithSubjectVerb, startsWithWords, stripLeadingPairPivot, validSubject, words } from "./negation-reframe-parts.js";
|
|
2
2
|
import { hasAbstractCommaContrast, hasAbstractNegationPayoff, hasFactualConnectorAfterNegation, hasMetaContext } from "./negation-context-gates.js";
|
|
3
3
|
import { hasInlineContrastConnectorAfterNegation } from "./inline-contrast-connector.js";
|
|
4
4
|
import { inlineNotBecauseReframe } from "./inline-not-because-reframe.js";
|
|
5
5
|
import { inlineNotJustCopularReframe, inlineShortNegatedBeat } from "./inline-short-negation.js";
|
|
6
|
-
import { hasNegativeSlopPairSignal, negativeSlopReframe } from "./negative-slop-frames.js";
|
|
6
|
+
import { hasNegativeSlopPairSignal, negatedActionPronounPayoff, negatedActionSetReplacement, negativeSlopReframe, progressiveVerbMirror, pronounCopularReframe, sameSubjectCopularReframe, shouldReportCopularReframe, startsWithNegatedPronounCopula } from "./negative-slop-frames.js";
|
|
7
7
|
import { makeMeaningReframe, meaningReframe } from "./meaning-reframe.js";
|
|
8
8
|
import { hasConcreteCorrectionEvidence } from "../../../../shared/matchers/concrete-evidence.js";
|
|
9
9
|
import { splitSentences } from "../../../../shared/text/sentences.js";
|
|
@@ -60,51 +60,6 @@ function inlineNegationContrast(sentence) {
|
|
|
60
60
|
}
|
|
61
61
|
: undefined;
|
|
62
62
|
}
|
|
63
|
-
function sameSubjectCopularReframe(aTokens, bTokens) {
|
|
64
|
-
const negation = findCopularNegation(aTokens);
|
|
65
|
-
if (negation === undefined || !validSubject(negation.subject)) {
|
|
66
|
-
return false;
|
|
67
|
-
}
|
|
68
|
-
return startsWithWords(bTokens, [
|
|
69
|
-
...negation.subject,
|
|
70
|
-
negation.affirmativeAux
|
|
71
|
-
]);
|
|
72
|
-
}
|
|
73
|
-
function pronounCopularReframe(aTokens, bTokens) {
|
|
74
|
-
const negation = findCopularNegation(aTokens);
|
|
75
|
-
return (negation !== undefined &&
|
|
76
|
-
validSubject(negation.subject) &&
|
|
77
|
-
!looksLikePassiveDefinition(aTokens, bTokens) &&
|
|
78
|
-
!startsWithNegatedPronounCopula(bTokens) &&
|
|
79
|
-
startsWithAny(bTokens, PRONOUN_REFRAME_STARTS));
|
|
80
|
-
}
|
|
81
|
-
function startsWithNegatedPronounCopula(tokens) {
|
|
82
|
-
for (const start of PRONOUN_REFRAME_STARTS) {
|
|
83
|
-
if (!startsWithWords(tokens, start)) {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
const tokenWords = words(tokens);
|
|
87
|
-
const predicateIndex = skipOptionalAdverbs(tokenWords, start.length);
|
|
88
|
-
return NEGATION_WORDS.has(tokenWords[predicateIndex] ?? "");
|
|
89
|
-
}
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
function progressiveVerbMirror(aTokens, bTokens) {
|
|
93
|
-
const negation = findCopularNegation(aTokens);
|
|
94
|
-
if (negation === undefined || !validSubject(negation.subject)) {
|
|
95
|
-
return false;
|
|
96
|
-
}
|
|
97
|
-
const tokenWords = words(aTokens);
|
|
98
|
-
const predicateIndex = skipOptionalAdverbs(tokenWords, negation.negatedPredicateStart);
|
|
99
|
-
const verb = tokenWords[predicateIndex];
|
|
100
|
-
return (verb !== undefined &&
|
|
101
|
-
verb.endsWith("ing") &&
|
|
102
|
-
startsWithWords(bTokens, [
|
|
103
|
-
...negation.subject,
|
|
104
|
-
negation.affirmativeAux,
|
|
105
|
-
verb
|
|
106
|
-
]));
|
|
107
|
-
}
|
|
108
63
|
function needReframe(aTokens, bTokens) {
|
|
109
64
|
const tokenWords = words(aTokens);
|
|
110
65
|
for (let index = 0; index < tokenWords.length; index += 1) {
|
|
@@ -172,43 +127,36 @@ function startsWithExplicitReplacement(tokens) {
|
|
|
172
127
|
}
|
|
173
128
|
function hasPairNegationSignal(tokens) {
|
|
174
129
|
return (findNegationIndex(tokens) !== undefined ||
|
|
130
|
+
findCopularNegation(tokens) !== undefined ||
|
|
175
131
|
contrastPivotSubject(tokens) !== undefined ||
|
|
176
132
|
hasNegativeSlopPairSignal(tokens));
|
|
177
133
|
}
|
|
178
|
-
function looksLikePassiveDefinition(aTokens, bTokens) {
|
|
179
|
-
const aWords = words(aTokens);
|
|
180
|
-
const bWords = words(bTokens);
|
|
181
|
-
for (let index = 0; index < aWords.length - 2; index += 1) {
|
|
182
|
-
const current = aWords[index];
|
|
183
|
-
if ((current === "has" || current === "have" || current === "had") &&
|
|
184
|
-
aWords[index + 1] === "not" &&
|
|
185
|
-
aWords[index + 2] === "been" &&
|
|
186
|
-
startsWithWords(bTokens, ["it", "is"]) &&
|
|
187
|
-
PASSIVE_DEFINITION_VERBS.has(bWords[2] ?? "")) {
|
|
188
|
-
return true;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return false;
|
|
192
|
-
}
|
|
193
134
|
function sentencePairReframe(a, b) {
|
|
194
135
|
const aTokens = wordTokens(a.text);
|
|
195
136
|
const bTokens = wordTokens(b.text);
|
|
137
|
+
const pairText = `${a.text} ${b.text}`;
|
|
138
|
+
const hasNegatedActionSetReplacement = negatedActionSetReplacement(aTokens, bTokens);
|
|
139
|
+
const hasAllowedReplacementColon = b.text.trimEnd().endsWith(":") && hasNegatedActionSetReplacement;
|
|
196
140
|
if (!isCompleteSentence(a) ||
|
|
197
|
-
!isCompleteSentence(b) ||
|
|
141
|
+
(!isCompleteSentence(b) && !hasAllowedReplacementColon) ||
|
|
198
142
|
!hasPairNegationSignal(aTokens)) {
|
|
199
143
|
return undefined;
|
|
200
144
|
}
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
|
|
145
|
+
if ((shouldReportCopularReframe(aTokens, bTokens, pairText) &&
|
|
146
|
+
(sameSubjectCopularReframe(aTokens, bTokens) ||
|
|
147
|
+
pronounCopularReframe(aTokens, bTokens) ||
|
|
148
|
+
progressiveVerbMirror(aTokens, bTokens))) ||
|
|
204
149
|
(startsWithExplicitReplacement(bTokens) &&
|
|
205
|
-
|
|
150
|
+
shouldReportCopularReframe(aTokens, bTokens, pairText) &&
|
|
151
|
+
!hasConcreteCorrectionEvidence(pairText)) ||
|
|
206
152
|
meaningReframe(aTokens, bTokens) ||
|
|
207
153
|
makeMeaningReframe(aTokens, bTokens) ||
|
|
208
154
|
needReframe(aTokens, bTokens) ||
|
|
209
155
|
actionVerbMirror(aTokens, bTokens) ||
|
|
156
|
+
negatedActionPronounPayoff(aTokens, bTokens) ||
|
|
210
157
|
negativeSlopReframe(aTokens, bTokens) ||
|
|
211
|
-
|
|
158
|
+
(shouldReportCopularReframe(aTokens, bTokens, pairText) &&
|
|
159
|
+
explicitContrastPivotReframe(aTokens, bTokens))) {
|
|
212
160
|
return {
|
|
213
161
|
end: b.end,
|
|
214
162
|
start: a.start,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const NEGATION_WORDS = new Set([
|
|
2
2
|
"not",
|
|
3
|
+
"never",
|
|
3
4
|
"isn't",
|
|
4
5
|
"aren't",
|
|
5
6
|
"wasn't",
|
|
@@ -52,18 +53,22 @@ export const PRONOUN_REFRAME_STARTS = [
|
|
|
52
53
|
["you", "are"],
|
|
53
54
|
["we", "are"]
|
|
54
55
|
];
|
|
55
|
-
export const PASSIVE_DEFINITION_VERBS = new Set([
|
|
56
|
-
"associated",
|
|
57
|
-
"caused",
|
|
58
|
-
"classified",
|
|
59
|
-
"defined",
|
|
60
|
-
"described",
|
|
61
|
-
"linked",
|
|
62
|
-
"marked",
|
|
63
|
-
"produced"
|
|
64
|
-
]);
|
|
65
56
|
const IRREGULAR_PAST_TENSE = new Map([["go", "went"]]);
|
|
66
57
|
const CONTRAST_PIVOT_AFTER_NOT = new Set(["just", "merely", "only"]);
|
|
58
|
+
const NEGATIVE_COPULAR_PREDICATES = new Set([
|
|
59
|
+
"never",
|
|
60
|
+
"nobody",
|
|
61
|
+
"none",
|
|
62
|
+
"nothing",
|
|
63
|
+
"nowhere"
|
|
64
|
+
]);
|
|
65
|
+
const CONTRACTED_PRONOUN_COPULAS = new Map([
|
|
66
|
+
["it's", { affirmativeAux: "is", subject: ["it"] }],
|
|
67
|
+
["that's", { affirmativeAux: "is", subject: ["that"] }],
|
|
68
|
+
["they're", { affirmativeAux: "are", subject: ["they"] }],
|
|
69
|
+
["we're", { affirmativeAux: "are", subject: ["we"] }],
|
|
70
|
+
["you're", { affirmativeAux: "are", subject: ["you"] }]
|
|
71
|
+
]);
|
|
67
72
|
export function findNegationIndex(tokens) {
|
|
68
73
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
69
74
|
const token = tokens[index];
|
|
@@ -121,6 +126,39 @@ export function skipOptionalAdverbs(tokens, start) {
|
|
|
121
126
|
export function startsWithAny(tokens, starts) {
|
|
122
127
|
return starts.some((start) => startsWithWords(tokens, start));
|
|
123
128
|
}
|
|
129
|
+
export function pronounCopulaStart(tokens) {
|
|
130
|
+
const firstToken = tokens[0];
|
|
131
|
+
const contracted = firstToken === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: CONTRACTED_PRONOUN_COPULAS.get(firstToken.normalized);
|
|
134
|
+
if (contracted !== undefined) {
|
|
135
|
+
return { ...contracted, predicateStart: 1 };
|
|
136
|
+
}
|
|
137
|
+
for (const start of PRONOUN_REFRAME_STARTS) {
|
|
138
|
+
if (startsWithWords(tokens, start)) {
|
|
139
|
+
return {
|
|
140
|
+
affirmativeAux: start[1],
|
|
141
|
+
predicateStart: start.length,
|
|
142
|
+
subject: [start[0]]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
export function startsWithPronounCopula(tokens) {
|
|
149
|
+
return pronounCopulaStart(tokens) !== undefined;
|
|
150
|
+
}
|
|
151
|
+
export function startsWithSubjectCopula(tokens, subject, affirmativeAux) {
|
|
152
|
+
if (startsWithWords(tokens, [...subject, affirmativeAux])) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
const contracted = CONTRACTED_PRONOUN_COPULAS.get(tokens[0]?.normalized ?? "");
|
|
156
|
+
if (contracted?.affirmativeAux !== affirmativeAux) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return (subject.length === contracted.subject.length &&
|
|
160
|
+
subject.every((word, index) => word === contracted.subject[index]));
|
|
161
|
+
}
|
|
124
162
|
export function isCompleteSentence(sentence) {
|
|
125
163
|
const trimmed = sentence.text.trim();
|
|
126
164
|
const last = trimmed.at(-1);
|
|
@@ -161,6 +199,24 @@ export function findCopularNegation(tokens) {
|
|
|
161
199
|
subject: tokenWords.slice(0, index)
|
|
162
200
|
};
|
|
163
201
|
}
|
|
202
|
+
if (explicitAux !== undefined &&
|
|
203
|
+
next !== undefined &&
|
|
204
|
+
NEGATIVE_COPULAR_PREDICATES.has(next)) {
|
|
205
|
+
return {
|
|
206
|
+
affirmativeAux: explicitAux,
|
|
207
|
+
negatedPredicateStart: index + 1,
|
|
208
|
+
subject: tokenWords.slice(0, index)
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
if (explicitAux !== undefined &&
|
|
212
|
+
next === "no" &&
|
|
213
|
+
(tokenWords[index + 2] === "one" || tokenWords[index + 2] === "single")) {
|
|
214
|
+
return {
|
|
215
|
+
affirmativeAux: explicitAux,
|
|
216
|
+
negatedPredicateStart: index + 1,
|
|
217
|
+
subject: tokenWords.slice(0, index)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
164
220
|
}
|
|
165
221
|
return undefined;
|
|
166
222
|
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { hasFactualConnectorAfterNegation } from "./negation-context-gates.js";
|
|
2
|
+
import { hasAbstractPolicyDirectObject } from "./policy-object.js";
|
|
3
|
+
export { shouldReportCopularReframe } from "./reframe-classification.js";
|
|
4
|
+
export { progressiveVerbMirror, pronounCopularReframe, sameSubjectCopularReframe, startsWithNegatedPronounCopula } from "./copular-reframe.js";
|
|
1
5
|
import { DO_NEGATIONS, EXPLICIT_DO_AUXILIARIES, FACTUAL_NEGATION_CONNECTORS, PRONOUN_REFRAME_STARTS, hasAnyWord, skipOptionalAdverbs, startsWithAny, startsWithSubjectOrPronoun, startsWithWords, stripLeadingPairPivot, validSubject, words } from "./negation-reframe-parts.js";
|
|
2
6
|
const GENERIC_ACTION_VERBS = new Set([
|
|
3
7
|
"asked",
|
|
@@ -27,6 +31,40 @@ const GENERIC_ACTION_VERBS = new Set([
|
|
|
27
31
|
"walked",
|
|
28
32
|
"went"
|
|
29
33
|
]);
|
|
34
|
+
const NEGATED_ACTION_REFRAME_VERBS = new Set([
|
|
35
|
+
"avoid",
|
|
36
|
+
"change",
|
|
37
|
+
"create",
|
|
38
|
+
"end",
|
|
39
|
+
"erase",
|
|
40
|
+
"fix",
|
|
41
|
+
"guarantee",
|
|
42
|
+
"make",
|
|
43
|
+
"mean",
|
|
44
|
+
"remove",
|
|
45
|
+
"replace",
|
|
46
|
+
"require",
|
|
47
|
+
"skip",
|
|
48
|
+
"solve"
|
|
49
|
+
]);
|
|
50
|
+
const REPLACEMENT_SUBJECT_PRONOUNS = new Set(["he", "she"]);
|
|
51
|
+
const PRONOUN_PAYOFF_VERBS = new Set([
|
|
52
|
+
"becomes",
|
|
53
|
+
"creates",
|
|
54
|
+
"depends",
|
|
55
|
+
"exposes",
|
|
56
|
+
"lands",
|
|
57
|
+
"means",
|
|
58
|
+
"moves",
|
|
59
|
+
"needs",
|
|
60
|
+
"points",
|
|
61
|
+
"requires",
|
|
62
|
+
"reveals",
|
|
63
|
+
"shifts",
|
|
64
|
+
"shows",
|
|
65
|
+
"turns"
|
|
66
|
+
]);
|
|
67
|
+
const PRONOUN_SUBJECTS = new Set(["it", "this", "that", "they", "we", "you"]);
|
|
30
68
|
function startsWithPassiveCopula(tokens) {
|
|
31
69
|
const tokenWords = words(tokens);
|
|
32
70
|
const predicateIndex = startsWithAny(tokens, PRONOUN_REFRAME_STARTS)
|
|
@@ -81,7 +119,34 @@ function negatedActionSubject(tokens) {
|
|
|
81
119
|
}
|
|
82
120
|
return undefined;
|
|
83
121
|
}
|
|
84
|
-
function
|
|
122
|
+
function constrainedSetSubjectLength(tokens, subject) {
|
|
123
|
+
if (startsWithWords(tokens, subject)) {
|
|
124
|
+
return subject.length;
|
|
125
|
+
}
|
|
126
|
+
if (startsWithSubjectOrPronoun(tokens, subject)) {
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
return subject.length > 1 &&
|
|
130
|
+
REPLACEMENT_SUBJECT_PRONOUNS.has(tokens[0]?.normalized ?? "")
|
|
131
|
+
? 1
|
|
132
|
+
: undefined;
|
|
133
|
+
}
|
|
134
|
+
export function negatedActionReplacement(aTokens, bTokens) {
|
|
135
|
+
const tokenWords = words(aTokens);
|
|
136
|
+
const subject = negatedActionSubject(aTokens);
|
|
137
|
+
if (subject === undefined) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const bContentTokens = stripLeadingPairPivot(bTokens);
|
|
141
|
+
const bWords = words(bContentTokens);
|
|
142
|
+
const genericVerbIndex = skipOptionalAdverbs(bWords, subject.length);
|
|
143
|
+
return (tokenWords.length <= 14 &&
|
|
144
|
+
!hasAnyWord(tokenWords, FACTUAL_NEGATION_CONNECTORS) &&
|
|
145
|
+
((startsWithSubjectOrPronoun(bContentTokens, subject) &&
|
|
146
|
+
GENERIC_ACTION_VERBS.has(bWords[genericVerbIndex] ?? "")) ||
|
|
147
|
+
negatedActionSetReplacement(aTokens, bTokens)));
|
|
148
|
+
}
|
|
149
|
+
export function negatedActionSetReplacement(aTokens, bTokens) {
|
|
85
150
|
const tokenWords = words(aTokens);
|
|
86
151
|
const subject = negatedActionSubject(aTokens);
|
|
87
152
|
if (subject === undefined) {
|
|
@@ -89,13 +154,14 @@ function negativeActionReplacement(aTokens, bTokens) {
|
|
|
89
154
|
}
|
|
90
155
|
const bContentTokens = stripLeadingPairPivot(bTokens);
|
|
91
156
|
const bWords = words(bContentTokens);
|
|
92
|
-
const
|
|
93
|
-
const
|
|
157
|
+
const subjectLength = constrainedSetSubjectLength(bContentTokens, subject);
|
|
158
|
+
const verbIndex = subjectLength === undefined
|
|
159
|
+
? undefined
|
|
160
|
+
: skipOptionalAdverbs(bWords, subjectLength);
|
|
94
161
|
return (tokenWords.length <= 14 &&
|
|
95
162
|
!hasAnyWord(tokenWords, FACTUAL_NEGATION_CONNECTORS) &&
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
GENERIC_ACTION_VERBS.has(verb));
|
|
163
|
+
verbIndex !== undefined &&
|
|
164
|
+
hasAbstractPolicyDirectObject(bWords, verbIndex));
|
|
99
165
|
}
|
|
100
166
|
function notBecauseReframe(aTokens, bTokens) {
|
|
101
167
|
return (startsWithWords(aTokens, ["not", "because"]) &&
|
|
@@ -130,10 +196,47 @@ export function hasNegativeSlopPairSignal(tokens) {
|
|
|
130
196
|
hasTrailingProblemFrame(tokenWords) ||
|
|
131
197
|
hasLeadingProblemFrame(tokenWords));
|
|
132
198
|
}
|
|
199
|
+
export function negatedActionPronounPayoff(aTokens, bTokens) {
|
|
200
|
+
const aWords = words(aTokens);
|
|
201
|
+
const bWords = words(stripLeadingPairPivot(bTokens));
|
|
202
|
+
const pronounStart = PRONOUN_SUBJECTS.has(bWords[0] ?? "") ? 1 : undefined;
|
|
203
|
+
if (pronounStart === undefined) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const payoffVerbIndex = skipOptionalAdverbs(bWords, pronounStart);
|
|
207
|
+
if (!PRONOUN_PAYOFF_VERBS.has(bWords[payoffVerbIndex] ?? "")) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
for (let index = 0; index < aWords.length; index += 1) {
|
|
211
|
+
if (hasNegatedReframeVerb(aTokens, aWords, index)) {
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
function hasNegatedReframeVerb(tokens, tokenWords, index) {
|
|
218
|
+
const current = tokenWords[index];
|
|
219
|
+
const next = tokenWords[index + 1];
|
|
220
|
+
const subject = tokenWords.slice(0, index);
|
|
221
|
+
if (!validSubject(subject)) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
const negationIndex = DO_NEGATIONS.has(current ?? "")
|
|
225
|
+
? index
|
|
226
|
+
: EXPLICIT_DO_AUXILIARIES.has(current ?? "") && next === "not"
|
|
227
|
+
? index + 1
|
|
228
|
+
: undefined;
|
|
229
|
+
if (negationIndex === undefined ||
|
|
230
|
+
hasFactualConnectorAfterNegation(tokens, negationIndex)) {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
const verbIndex = skipOptionalAdverbs(tokenWords, negationIndex + 1);
|
|
234
|
+
return NEGATED_ACTION_REFRAME_VERBS.has(tokenWords[verbIndex] ?? "");
|
|
235
|
+
}
|
|
133
236
|
export function negativeSlopReframe(aTokens, bTokens) {
|
|
134
237
|
return (noLongerCopularReframe(aTokens, bTokens) ||
|
|
135
238
|
fragmentDefinitionReframe(aTokens, bTokens) ||
|
|
136
|
-
|
|
239
|
+
negatedActionReplacement(aTokens, bTokens) ||
|
|
137
240
|
notBecauseReframe(aTokens, bTokens) ||
|
|
138
241
|
notProblemReframe(aTokens, bTokens));
|
|
139
242
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const ABSTRACT_POLICY_OBJECTS = new Set([
|
|
2
|
+
"condition",
|
|
3
|
+
"criterion",
|
|
4
|
+
"policy",
|
|
5
|
+
"principle",
|
|
6
|
+
"requirement",
|
|
7
|
+
"rule",
|
|
8
|
+
"standard",
|
|
9
|
+
"threshold"
|
|
10
|
+
]);
|
|
11
|
+
const DIRECT_OBJECT_DETERMINERS = new Set([
|
|
12
|
+
"a",
|
|
13
|
+
"an",
|
|
14
|
+
"one",
|
|
15
|
+
"that",
|
|
16
|
+
"the",
|
|
17
|
+
"this"
|
|
18
|
+
]);
|
|
19
|
+
const POLICY_OBJECT_MODIFIERS = new Set([
|
|
20
|
+
"access",
|
|
21
|
+
"approval",
|
|
22
|
+
"business",
|
|
23
|
+
"content",
|
|
24
|
+
"deployment",
|
|
25
|
+
"editorial",
|
|
26
|
+
"governance",
|
|
27
|
+
"launch",
|
|
28
|
+
"operating",
|
|
29
|
+
"pricing",
|
|
30
|
+
"quality",
|
|
31
|
+
"release",
|
|
32
|
+
"review",
|
|
33
|
+
"safety",
|
|
34
|
+
"security",
|
|
35
|
+
"service",
|
|
36
|
+
"spending",
|
|
37
|
+
"technical",
|
|
38
|
+
"testing",
|
|
39
|
+
"usage"
|
|
40
|
+
]);
|
|
41
|
+
export function hasAbstractPolicyDirectObject(tokenWords, verbIndex) {
|
|
42
|
+
if (!["set", "sets"].includes(tokenWords[verbIndex] ?? "")) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const objectTokens = tokenWords.slice(verbIndex + 1);
|
|
46
|
+
const directObject = DIRECT_OBJECT_DETERMINERS.has(objectTokens[0] ?? "")
|
|
47
|
+
? objectTokens.slice(1)
|
|
48
|
+
: objectTokens;
|
|
49
|
+
const [first, second] = directObject;
|
|
50
|
+
return ((directObject.length === 1 && ABSTRACT_POLICY_OBJECTS.has(first ?? "")) ||
|
|
51
|
+
(directObject.length === 2 &&
|
|
52
|
+
POLICY_OBJECT_MODIFIERS.has(first ?? "") &&
|
|
53
|
+
ABSTRACT_POLICY_OBJECTS.has(second ?? "")));
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=policy-object.js.map
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { FACTUAL_NEGATION_CONNECTORS, findCopularNegation, pronounCopulaStart, stripLeadingPairPivot, words } from "./negation-reframe-parts.js";
|
|
2
|
+
const FRAMING_SUBJECT_NOUNS = new Set([
|
|
3
|
+
"answer",
|
|
4
|
+
"fix",
|
|
5
|
+
"goal",
|
|
6
|
+
"issue",
|
|
7
|
+
"key",
|
|
8
|
+
"point",
|
|
9
|
+
"problem",
|
|
10
|
+
"solution",
|
|
11
|
+
"strategy"
|
|
12
|
+
]);
|
|
13
|
+
const INSTRUCTION_ADJECTIVES = new Set([
|
|
14
|
+
"best",
|
|
15
|
+
"better",
|
|
16
|
+
"critical",
|
|
17
|
+
"essential",
|
|
18
|
+
"important",
|
|
19
|
+
"necessary",
|
|
20
|
+
"recommended",
|
|
21
|
+
"safest"
|
|
22
|
+
]);
|
|
23
|
+
const CONCRETE_PASSIVE_VERBS = new Set([
|
|
24
|
+
"associated",
|
|
25
|
+
"caused",
|
|
26
|
+
"classified",
|
|
27
|
+
"created",
|
|
28
|
+
"entered",
|
|
29
|
+
"linked",
|
|
30
|
+
"owned",
|
|
31
|
+
"paid",
|
|
32
|
+
"passed",
|
|
33
|
+
"produced",
|
|
34
|
+
"recorded",
|
|
35
|
+
"regulated",
|
|
36
|
+
"sent",
|
|
37
|
+
"stored",
|
|
38
|
+
"used"
|
|
39
|
+
]);
|
|
40
|
+
const REFERENCE_PASSIVE_VERBS = new Set(["defined", "described", "marked"]);
|
|
41
|
+
const PASSIVE_EXPLANATION_LINKS = new Set([
|
|
42
|
+
"as",
|
|
43
|
+
"by",
|
|
44
|
+
"for",
|
|
45
|
+
"from",
|
|
46
|
+
"in",
|
|
47
|
+
"to",
|
|
48
|
+
"with"
|
|
49
|
+
]);
|
|
50
|
+
const CLAUSE_BOUNDARIES = new Set([
|
|
51
|
+
"and",
|
|
52
|
+
"but",
|
|
53
|
+
"that",
|
|
54
|
+
"when",
|
|
55
|
+
"where",
|
|
56
|
+
"which",
|
|
57
|
+
"while"
|
|
58
|
+
]);
|
|
59
|
+
const CONCRETE_RELATIONS = [
|
|
60
|
+
["amount", "of"],
|
|
61
|
+
["defined", "to", "be"],
|
|
62
|
+
["derived", "from"],
|
|
63
|
+
["intended", "for"],
|
|
64
|
+
["owned", "by"],
|
|
65
|
+
["property", "of"],
|
|
66
|
+
["symptom", "of"],
|
|
67
|
+
["unit", "of"]
|
|
68
|
+
];
|
|
69
|
+
const MAX_SHORT_SENTENCE_WORDS = 6;
|
|
70
|
+
const QUANTITY_WORDS = new Set([
|
|
71
|
+
"half",
|
|
72
|
+
"one",
|
|
73
|
+
"two",
|
|
74
|
+
"three",
|
|
75
|
+
"four",
|
|
76
|
+
"five",
|
|
77
|
+
"six",
|
|
78
|
+
"seven",
|
|
79
|
+
"eight",
|
|
80
|
+
"nine",
|
|
81
|
+
"ten"
|
|
82
|
+
]);
|
|
83
|
+
function digitTokenCount(tokens) {
|
|
84
|
+
return tokens.filter((token) => [...token.text].some((character) => character >= "0" && character <= "9")).length;
|
|
85
|
+
}
|
|
86
|
+
function quantityTokenCount(tokens) {
|
|
87
|
+
return (digitTokenCount(tokens) +
|
|
88
|
+
tokens.filter((token) => QUANTITY_WORDS.has(token.normalized)).length);
|
|
89
|
+
}
|
|
90
|
+
function acronymCount(tokens) {
|
|
91
|
+
return tokens.filter((token) => token.text.length >= 2 &&
|
|
92
|
+
token.text === token.text.toLocaleUpperCase("en") &&
|
|
93
|
+
token.text !== token.text.toLocaleLowerCase("en")).length;
|
|
94
|
+
}
|
|
95
|
+
function properNameCount(tokens) {
|
|
96
|
+
return tokens.filter((token, index) => {
|
|
97
|
+
const first = token.text[0];
|
|
98
|
+
return (index > 0 &&
|
|
99
|
+
first !== undefined &&
|
|
100
|
+
first >= "A" &&
|
|
101
|
+
first <= "Z" &&
|
|
102
|
+
token.text !== token.text.toLocaleUpperCase("en"));
|
|
103
|
+
}).length;
|
|
104
|
+
}
|
|
105
|
+
function hasReferenceEvidence(aTokens, bTokens, pairText) {
|
|
106
|
+
const digits = digitTokenCount(aTokens) + digitTokenCount(bTokens);
|
|
107
|
+
const quantities = quantityTokenCount(aTokens) + quantityTokenCount(bTokens);
|
|
108
|
+
const names = properNameCount(aTokens) + properNameCount(bTokens);
|
|
109
|
+
return (digits >= 2 ||
|
|
110
|
+
pairText.includes("/") ||
|
|
111
|
+
pairText.includes("_") ||
|
|
112
|
+
pairText.includes("@") ||
|
|
113
|
+
(acronymCount(aTokens) + acronymCount(bTokens) >= 1 && digits >= 1) ||
|
|
114
|
+
names >= 3 ||
|
|
115
|
+
(names >= 2 && quantities >= 1));
|
|
116
|
+
}
|
|
117
|
+
function startsWithSequence(source, sequence) {
|
|
118
|
+
return sequence.every((word, index) => source[index] === word);
|
|
119
|
+
}
|
|
120
|
+
function containsSequence(source, sequence) {
|
|
121
|
+
for (let index = 0; index <= source.length - sequence.length; index += 1) {
|
|
122
|
+
if (startsWithSequence(source.slice(index), sequence)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
function hasConcreteRelation(tokenWords) {
|
|
129
|
+
return CONCRETE_RELATIONS.some((relation) => containsSequence(tokenWords, relation));
|
|
130
|
+
}
|
|
131
|
+
function hasInstruction(tokens) {
|
|
132
|
+
const tokenWords = words(tokens);
|
|
133
|
+
const start = pronounCopulaStart(tokens);
|
|
134
|
+
return (start !== undefined &&
|
|
135
|
+
INSTRUCTION_ADJECTIVES.has(tokenWords[start.predicateStart] ?? "") &&
|
|
136
|
+
tokenWords[start.predicateStart + 1] === "to");
|
|
137
|
+
}
|
|
138
|
+
function hasPassiveExplanation(bTokens) {
|
|
139
|
+
const tokenWords = words(bTokens);
|
|
140
|
+
const start = pronounCopulaStart(bTokens);
|
|
141
|
+
const isConcretePassive = (word) => CONCRETE_PASSIVE_VERBS.has(word) || REFERENCE_PASSIVE_VERBS.has(word);
|
|
142
|
+
if (start !== undefined &&
|
|
143
|
+
isConcretePassive(tokenWords[start.predicateStart] ?? "")) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const boundary = tokenWords.findIndex((word) => CLAUSE_BOUNDARIES.has(word));
|
|
147
|
+
const firstClause = boundary < 0 ? tokenWords : tokenWords.slice(0, boundary);
|
|
148
|
+
return firstClause.some((word, index) => isConcretePassive(word) &&
|
|
149
|
+
PASSIVE_EXPLANATION_LINKS.has(firstClause[index + 1] ?? ""));
|
|
150
|
+
}
|
|
151
|
+
function hasPurposeOrProvenance(tokenWords) {
|
|
152
|
+
return (containsSequence(tokenWords, ["used", "to"]) ||
|
|
153
|
+
(["uses", "used"].includes(tokenWords[1] ?? "") &&
|
|
154
|
+
tokenWords.some((word, index) => ["created", "produced"].includes(word) &&
|
|
155
|
+
["by", "from", "in"].includes(tokenWords[index + 1] ?? ""))));
|
|
156
|
+
}
|
|
157
|
+
function hasCausalPassiveExplanation(aTokens, bTokens) {
|
|
158
|
+
return (words(aTokens).some((word) => FACTUAL_NEGATION_CONNECTORS.has(word)) &&
|
|
159
|
+
hasPassiveExplanation(bTokens));
|
|
160
|
+
}
|
|
161
|
+
function hasDistinctCauseExplanation(aTokens, bTokens) {
|
|
162
|
+
const aWords = words(aTokens);
|
|
163
|
+
const bWords = words(stripLeadingPairPivot(bTokens));
|
|
164
|
+
return (!aWords.includes("caused") && containsSequence(bWords, ["caused", "by"]));
|
|
165
|
+
}
|
|
166
|
+
function hasConcreteExplanatoryEvidence(aTokens, bTokens, pairText) {
|
|
167
|
+
const tokenWords = words(stripLeadingPairPivot(bTokens));
|
|
168
|
+
return (hasInstruction(bTokens) ||
|
|
169
|
+
hasConcreteRelation(tokenWords) ||
|
|
170
|
+
hasPurposeOrProvenance(tokenWords) ||
|
|
171
|
+
hasDistinctCauseExplanation(aTokens, bTokens) ||
|
|
172
|
+
hasCausalPassiveExplanation(aTokens, bTokens) ||
|
|
173
|
+
(hasReferenceEvidence(aTokens, bTokens, pairText) &&
|
|
174
|
+
hasPassiveExplanation(bTokens)));
|
|
175
|
+
}
|
|
176
|
+
function hasFramingNoun(tokens) {
|
|
177
|
+
const negation = findCopularNegation(tokens);
|
|
178
|
+
if (negation === undefined) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
const predicate = words(tokens).slice(negation.negatedPredicateStart, negation.negatedPredicateStart + 4);
|
|
182
|
+
return [...negation.subject, ...predicate].some((word) => FRAMING_SUBJECT_NOUNS.has(word));
|
|
183
|
+
}
|
|
184
|
+
function isShortReversal(aTokens, bTokens) {
|
|
185
|
+
return (aTokens.length <= MAX_SHORT_SENTENCE_WORDS &&
|
|
186
|
+
bTokens.length <= MAX_SHORT_SENTENCE_WORDS);
|
|
187
|
+
}
|
|
188
|
+
export function shouldReportCopularReframe(aTokens, bTokens, pairText) {
|
|
189
|
+
return (isShortReversal(aTokens, bTokens) ||
|
|
190
|
+
hasFramingNoun(aTokens) ||
|
|
191
|
+
!hasConcreteExplanatoryEvidence(aTokens, bTokens, pairText));
|
|
192
|
+
}
|
|
193
|
+
//# sourceMappingURL=reframe-classification.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { hasConcreteImplementationSummary } from "../../../shared/matchers/concrete-evidence.js";
|
|
2
2
|
import { cleanSentence, containsAny, tokens } from "../../../shared/matchers/prose-patterns.js";
|
|
3
3
|
import { oneToOneRule } from "../../private/textlint-rule-builders.js";
|
|
4
|
-
import { matchDiscourseEvaluationFrame } from "./private/discourse-evaluation.js";
|
|
4
|
+
import { isAbstractAuditFrame, matchDiscourseEvaluationFrame, matchExpandedDiscourseFrame } from "./private/discourse-evaluation.js";
|
|
5
5
|
const PREFIXES = ["however, ", "but ", "and ", "so "];
|
|
6
6
|
// "as such" was removed: it is a normal anaphoric connective ("a registered adviser; as
|
|
7
7
|
// such, it must...") and was the dominant false positive, not a signposting frame.
|
|
@@ -47,7 +47,6 @@ const FRAME_PATTERNS = [
|
|
|
47
47
|
"the useful version is",
|
|
48
48
|
"the point is plain enough"
|
|
49
49
|
];
|
|
50
|
-
const DETERMINERS = ["a", "an", "the", "this", "that"];
|
|
51
50
|
const SEQUENCE_PATTERNS = [
|
|
52
51
|
"a simple sequence works well",
|
|
53
52
|
"a simple pattern works well",
|
|
@@ -88,72 +87,9 @@ const WHAT_FRAME_TAIL_STARTERS = [
|
|
|
88
87
|
"true",
|
|
89
88
|
"usually"
|
|
90
89
|
];
|
|
91
|
-
const FRAME_ADJECTIVES = [
|
|
92
|
-
"basic",
|
|
93
|
-
"best",
|
|
94
|
-
"better",
|
|
95
|
-
"biggest",
|
|
96
|
-
"bigger",
|
|
97
|
-
"central",
|
|
98
|
-
"clearest",
|
|
99
|
-
"core",
|
|
100
|
-
"easiest",
|
|
101
|
-
"final",
|
|
102
|
-
"first",
|
|
103
|
-
"hardest",
|
|
104
|
-
"honest",
|
|
105
|
-
"important",
|
|
106
|
-
"main",
|
|
107
|
-
"only",
|
|
108
|
-
"obvious",
|
|
109
|
-
"practical",
|
|
110
|
-
"real",
|
|
111
|
-
"simple",
|
|
112
|
-
"useful"
|
|
113
|
-
];
|
|
114
|
-
const FRAME_NOUNS = [
|
|
115
|
-
"answer",
|
|
116
|
-
"approach",
|
|
117
|
-
"challenge",
|
|
118
|
-
"choice",
|
|
119
|
-
"conclusion",
|
|
120
|
-
"fact",
|
|
121
|
-
"fix",
|
|
122
|
-
"focus",
|
|
123
|
-
"frame",
|
|
124
|
-
"idea",
|
|
125
|
-
"lesson",
|
|
126
|
-
"move",
|
|
127
|
-
"path",
|
|
128
|
-
"point",
|
|
129
|
-
"principle",
|
|
130
|
-
"priority",
|
|
131
|
-
"problem",
|
|
132
|
-
"question",
|
|
133
|
-
"result",
|
|
134
|
-
"rule",
|
|
135
|
-
"shift",
|
|
136
|
-
"signal",
|
|
137
|
-
"strategy",
|
|
138
|
-
"test",
|
|
139
|
-
"thing",
|
|
140
|
-
"tradeoff",
|
|
141
|
-
"truth",
|
|
142
|
-
"version",
|
|
143
|
-
"way",
|
|
144
|
-
"win"
|
|
145
|
-
];
|
|
146
|
-
const ABSTRACT_FRAME_VERBS = ["is", "are", "was"];
|
|
147
90
|
const POINT_NOUNS = ["goal", "job", "key", "point", "takeaway", "trick"];
|
|
148
91
|
function matchModifiedAbstractFrame(words) {
|
|
149
|
-
const [first, second, third, fourth
|
|
150
|
-
if (first === "the" &&
|
|
151
|
-
second === "result" &&
|
|
152
|
-
third === "worth" &&
|
|
153
|
-
fourth === "caring" &&
|
|
154
|
-
fifth === "about") {
|
|
155
|
-
return "the-result-worth-caring-about";
|
|
156
|
-
}
|
|
92
|
+
const [first, second, third, fourth] = words;
|
|
157
93
|
if (first !== "the") {
|
|
158
94
|
return undefined;
|
|
159
95
|
}
|
|
@@ -167,20 +103,6 @@ function matchModifiedAbstractFrame(words) {
|
|
|
167
103
|
]);
|
|
168
104
|
return matches.get(key);
|
|
169
105
|
}
|
|
170
|
-
function matchEvaluativeFrame(words) {
|
|
171
|
-
const [first, second, third, fourth] = words;
|
|
172
|
-
if (first !== undefined &&
|
|
173
|
-
second !== undefined &&
|
|
174
|
-
third !== undefined &&
|
|
175
|
-
fourth !== undefined &&
|
|
176
|
-
DETERMINERS.includes(first) &&
|
|
177
|
-
FRAME_ADJECTIVES.includes(second) &&
|
|
178
|
-
FRAME_NOUNS.includes(third) &&
|
|
179
|
-
ABSTRACT_FRAME_VERBS.includes(fourth)) {
|
|
180
|
-
return `the-${second}-${third}-${fourth}`;
|
|
181
|
-
}
|
|
182
|
-
return undefined;
|
|
183
|
-
}
|
|
184
106
|
function matchPointIsToFrame(words) {
|
|
185
107
|
const [first, second, third, fourth, fifth] = words;
|
|
186
108
|
if (first === "the" &&
|
|
@@ -217,8 +139,8 @@ function matchWhatFrame(words) {
|
|
|
217
139
|
}
|
|
218
140
|
function matchAbstractFrame(text) {
|
|
219
141
|
const words = tokens(text);
|
|
220
|
-
return (
|
|
221
|
-
|
|
142
|
+
return (matchExpandedDiscourseFrame(words) ??
|
|
143
|
+
matchModifiedAbstractFrame(words) ??
|
|
222
144
|
matchDiscourseEvaluationFrame(words) ??
|
|
223
145
|
matchPointIsToFrame(words) ??
|
|
224
146
|
matchWhatFrame(words));
|
|
@@ -266,8 +188,14 @@ function matchGeneratedFormula(text) {
|
|
|
266
188
|
}
|
|
267
189
|
function matchSignposting(sentence) {
|
|
268
190
|
const stripped = cleanSentence(sentence, PREFIXES);
|
|
269
|
-
const concreteImplementation = hasConcreteImplementationSummary(stripped);
|
|
270
191
|
const abstract = matchAbstractFrame(stripped);
|
|
192
|
+
const strippedWords = tokens(stripped);
|
|
193
|
+
const auditStart = isAbstractAuditFrame(strippedWords)
|
|
194
|
+
? stripped.indexOf("audit")
|
|
195
|
+
: -1;
|
|
196
|
+
const concreteImplementation = hasConcreteImplementationSummary(abstract !== undefined && auditStart >= 0
|
|
197
|
+
? stripped.slice(0, auditStart) + stripped.slice(auditStart + 5)
|
|
198
|
+
: stripped);
|
|
271
199
|
const formulaicSetup = matchFormulaicContentSetup(stripped);
|
|
272
200
|
const generatedFormula = matchGeneratedFormula(stripped);
|
|
273
201
|
if (abstract !== undefined && !concreteImplementation) {
|
|
@@ -279,6 +207,9 @@ function matchSignposting(sentence) {
|
|
|
279
207
|
if (formulaicSetup !== undefined) {
|
|
280
208
|
return { kind: "formulaic-content-setup", signal: formulaicSetup };
|
|
281
209
|
}
|
|
210
|
+
if (stripped.startsWith("the fun part is:") && !concreteImplementation) {
|
|
211
|
+
return { kind: "frame-signpost", signal: "the fun part is:" };
|
|
212
|
+
}
|
|
282
213
|
const checks = [
|
|
283
214
|
["transition", TRANSITION_PATTERNS],
|
|
284
215
|
["consultation-signpost", CONSULTATION_PATTERNS],
|
|
@@ -1,4 +1,74 @@
|
|
|
1
1
|
const ABSTRACT_FRAME_VERBS = ["is", "are", "was", "were"];
|
|
2
|
+
const DISCOURSE_WORK_TAILS = [
|
|
3
|
+
["doing", "real", "work"],
|
|
4
|
+
["load", "bearing"]
|
|
5
|
+
];
|
|
6
|
+
const DETERMINERS = new Set(["a", "an", "the", "this", "that"]);
|
|
7
|
+
const FRAME_ADJECTIVES = new Set([
|
|
8
|
+
"basic",
|
|
9
|
+
"best",
|
|
10
|
+
"better",
|
|
11
|
+
"biggest",
|
|
12
|
+
"bigger",
|
|
13
|
+
"central",
|
|
14
|
+
"clearest",
|
|
15
|
+
"core",
|
|
16
|
+
"easiest",
|
|
17
|
+
"final",
|
|
18
|
+
"first",
|
|
19
|
+
"hardest",
|
|
20
|
+
"honest",
|
|
21
|
+
"important",
|
|
22
|
+
"main",
|
|
23
|
+
"only",
|
|
24
|
+
"obvious",
|
|
25
|
+
"practical",
|
|
26
|
+
"real",
|
|
27
|
+
"simple",
|
|
28
|
+
"useful"
|
|
29
|
+
]);
|
|
30
|
+
const FRAME_NOUNS = new Set([
|
|
31
|
+
"answer",
|
|
32
|
+
"approach",
|
|
33
|
+
"audit",
|
|
34
|
+
"challenge",
|
|
35
|
+
"choice",
|
|
36
|
+
"conclusion",
|
|
37
|
+
"diagnosis",
|
|
38
|
+
"fact",
|
|
39
|
+
"fix",
|
|
40
|
+
"focus",
|
|
41
|
+
"frame",
|
|
42
|
+
"idea",
|
|
43
|
+
"lesson",
|
|
44
|
+
"move",
|
|
45
|
+
"path",
|
|
46
|
+
"point",
|
|
47
|
+
"principle",
|
|
48
|
+
"priority",
|
|
49
|
+
"problem",
|
|
50
|
+
"question",
|
|
51
|
+
"result",
|
|
52
|
+
"rule",
|
|
53
|
+
"shift",
|
|
54
|
+
"signal",
|
|
55
|
+
"strategy",
|
|
56
|
+
"test",
|
|
57
|
+
"thing",
|
|
58
|
+
"tradeoff",
|
|
59
|
+
"truth",
|
|
60
|
+
"version",
|
|
61
|
+
"way",
|
|
62
|
+
"win"
|
|
63
|
+
]);
|
|
64
|
+
const VAGUE_FRAME_VERBS = new Set(["begins", "happens", "lives", "starts"]);
|
|
65
|
+
const VAGUE_FRAME_LOCATIONS = new Set([
|
|
66
|
+
"downstream",
|
|
67
|
+
"earlier",
|
|
68
|
+
"here",
|
|
69
|
+
"there",
|
|
70
|
+
"upstream"
|
|
71
|
+
]);
|
|
2
72
|
const EVALUATION_TAILS = new Set([
|
|
3
73
|
"boring",
|
|
4
74
|
"clear",
|
|
@@ -72,15 +142,90 @@ function isDiscourseEvaluationSubject(first, subject) {
|
|
|
72
142
|
}
|
|
73
143
|
return subject.some((word) => DISCOURSE_SUBJECT_HEADS.has(word));
|
|
74
144
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
!
|
|
145
|
+
function matchDiscourseWorkClaim(words, verbIndex, subject) {
|
|
146
|
+
if (verbIndex <= 0 ||
|
|
147
|
+
words[verbIndex] !== "is" ||
|
|
148
|
+
!DISCOURSE_SUBJECT_HEADS.has(subject.at(-1) ?? "")) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const tail = DISCOURSE_WORK_TAILS.find((candidate) => candidate.every((word, index) => words[verbIndex + index + 1] === word));
|
|
152
|
+
return tail === undefined ? undefined : `is-${tail.join("-")}`;
|
|
153
|
+
}
|
|
154
|
+
function frameNounIndex(words) {
|
|
155
|
+
if (FRAME_NOUNS.has(words[1] ?? "")) {
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
return FRAME_ADJECTIVES.has(words[1] ?? "") && FRAME_NOUNS.has(words[2] ?? "")
|
|
159
|
+
? 2
|
|
160
|
+
: -1;
|
|
161
|
+
}
|
|
162
|
+
function matchWorthAttentionFrame(words) {
|
|
163
|
+
if (words[0] !== "the") {
|
|
79
164
|
return undefined;
|
|
80
165
|
}
|
|
166
|
+
const nounIndex = frameNounIndex(words);
|
|
167
|
+
if (nounIndex < 0 || words[nounIndex + 1] !== "worth") {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
const tail = words.slice(nounIndex + 2);
|
|
171
|
+
const matches = tail[0] === "noticing" ||
|
|
172
|
+
tail[0] === "watching" ||
|
|
173
|
+
tail[0] === "tracking" ||
|
|
174
|
+
(tail[0] === "caring" && tail[1] === "about") ||
|
|
175
|
+
(tail[0] === "paying" && tail[1] === "attention" && tail[2] === "to");
|
|
176
|
+
return matches
|
|
177
|
+
? `the-${words.slice(1, nounIndex + 1).join("-")}-worth-attention`
|
|
178
|
+
: undefined;
|
|
179
|
+
}
|
|
180
|
+
function matchVagueFrameLocation(words) {
|
|
181
|
+
const [first, adjective, noun, verb, location] = words;
|
|
182
|
+
return words.length === 5 &&
|
|
183
|
+
first === "the" &&
|
|
184
|
+
FRAME_ADJECTIVES.has(adjective ?? "") &&
|
|
185
|
+
FRAME_NOUNS.has(noun ?? "") &&
|
|
186
|
+
VAGUE_FRAME_VERBS.has(verb ?? "") &&
|
|
187
|
+
VAGUE_FRAME_LOCATIONS.has(location ?? "")
|
|
188
|
+
? `the-${adjective ?? "useful"}-${noun ?? "frame"}-${verb ?? "starts"}-${location ?? "here"}`
|
|
189
|
+
: undefined;
|
|
190
|
+
}
|
|
191
|
+
function matchEvaluativeFrame(words) {
|
|
192
|
+
const [first, adjective, noun, verb] = words;
|
|
193
|
+
return first !== undefined &&
|
|
194
|
+
adjective !== undefined &&
|
|
195
|
+
noun !== undefined &&
|
|
196
|
+
verb !== undefined &&
|
|
197
|
+
DETERMINERS.has(first) &&
|
|
198
|
+
FRAME_ADJECTIVES.has(adjective) &&
|
|
199
|
+
FRAME_NOUNS.has(noun) &&
|
|
200
|
+
["is", "are", "was"].includes(verb)
|
|
201
|
+
? `the-${adjective}-${noun}-${verb}`
|
|
202
|
+
: undefined;
|
|
203
|
+
}
|
|
204
|
+
export function isAbstractAuditFrame(words) {
|
|
205
|
+
return words[0] === "the" && frameNounIndex(words) > 0
|
|
206
|
+
? words[frameNounIndex(words)] === "audit"
|
|
207
|
+
: false;
|
|
208
|
+
}
|
|
209
|
+
export function matchExpandedDiscourseFrame(words) {
|
|
210
|
+
return (matchWorthAttentionFrame(words) ??
|
|
211
|
+
matchVagueFrameLocation(words) ??
|
|
212
|
+
matchEvaluativeFrame(words));
|
|
213
|
+
}
|
|
214
|
+
export function matchDiscourseEvaluationFrame(words) {
|
|
215
|
+
const [first] = words;
|
|
81
216
|
const verbIndex = words.findIndex((word) => ABSTRACT_FRAME_VERBS.includes(word));
|
|
82
217
|
const tail = words.at(-1);
|
|
83
218
|
const subject = verbIndex > 0 ? words.slice(0, verbIndex) : [];
|
|
219
|
+
const workClaim = matchDiscourseWorkClaim(words, verbIndex, subject);
|
|
220
|
+
if (workClaim !== undefined) {
|
|
221
|
+
return workClaim;
|
|
222
|
+
}
|
|
223
|
+
if (words.length > 8) {
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
if (!["the", "this", "that", "it"].includes(first ?? "")) {
|
|
227
|
+
return undefined;
|
|
228
|
+
}
|
|
84
229
|
return verbIndex > 0 &&
|
|
85
230
|
tail !== undefined &&
|
|
86
231
|
EVALUATION_TAILS.has(tail) &&
|