slopless 0.2.22 → 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.
@@ -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, fifth] = words;
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 (matchModifiedAbstractFrame(words) ??
221
- matchEvaluativeFrame(words) ??
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
- export function matchDiscourseEvaluationFrame(words) {
76
- const [first] = words;
77
- if (words.length > 8 ||
78
- !["the", "this", "that", "it"].includes(first ?? "")) {
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) &&