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.
@@ -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) => pattern.matchMode === "contains" ? "contains" : "full";
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
- return (wordTokens(text).length <= 6 &&
17
- (lower.startsWith("that's the ") || lower.startsWith("that is the ")) &&
18
- FORMULA_TAILS.some((tail) => lower.includes(tail)));
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, NEGATION_WORDS, PASSIVE_DEFINITION_VERBS, PRONOUN_REFRAME_STARTS, contrastPivotSubject, findCopularNegation, findNegationIndex, isCompleteSentence, skipOptionalAdverbs, startsWithAny, startsWithSubjectOrPronoun, startsWithSubjectVerb, startsWithWords, stripLeadingPairPivot, validSubject, words } from "./negation-reframe-parts.js";
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 (sameSubjectCopularReframe(aTokens, bTokens) ||
202
- pronounCopularReframe(aTokens, bTokens) ||
203
- progressiveVerbMirror(aTokens, bTokens) ||
145
+ if ((shouldReportCopularReframe(aTokens, bTokens, pairText) &&
146
+ (sameSubjectCopularReframe(aTokens, bTokens) ||
147
+ pronounCopularReframe(aTokens, bTokens) ||
148
+ progressiveVerbMirror(aTokens, bTokens))) ||
204
149
  (startsWithExplicitReplacement(bTokens) &&
205
- !hasConcreteCorrectionEvidence(`${a.text} ${b.text}`)) ||
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
- explicitContrastPivotReframe(aTokens, bTokens)) {
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 negativeActionReplacement(aTokens, bTokens) {
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 verbIndex = skipOptionalAdverbs(bWords, subject.length);
93
- const verb = bWords[verbIndex];
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
- startsWithSubjectOrPronoun(bContentTokens, subject) &&
97
- verb !== undefined &&
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
- negativeActionReplacement(aTokens, bTokens) ||
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, 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) &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopless",
3
- "version": "0.2.23",
3
+ "version": "0.2.24",
4
4
  "description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
5
5
  "keywords": [
6
6
  "textlint",