slopless 0.2.24 → 0.2.25

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.
Files changed (28) hide show
  1. package/dist/rules/semantic-thinness/patterns/abstract-metaphor-claim.json +83 -1
  2. package/dist/rules/semantic-thinness/patterns/deictic-summary.json +31 -2
  3. package/dist/rules/semantic-thinness/patterns/empty-scene-transition.json +2 -1
  4. package/dist/rules/semantic-thinness/private/concrete-guards.js +2 -0
  5. package/dist/rules/semantic-thinness/private/pattern-matcher.js +22 -6
  6. package/dist/rules/syntactic-patterns/contrast/contrastive-aphorism.js +2 -0
  7. package/dist/rules/syntactic-patterns/contrast/negation-reframe.js +1 -1
  8. package/dist/rules/syntactic-patterns/contrast/private/action-reframe-vocabulary.js +47 -0
  9. package/dist/rules/syntactic-patterns/contrast/private/audience-replacement.js +267 -0
  10. package/dist/rules/syntactic-patterns/contrast/private/copular-reframe.js +39 -1
  11. package/dist/rules/syntactic-patterns/contrast/private/evidence-limitation-pair.js +307 -0
  12. package/dist/rules/syntactic-patterns/contrast/private/inline-semicolon-reframe.js +50 -0
  13. package/dist/rules/syntactic-patterns/contrast/private/inline-short-negation.js +1 -0
  14. package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-matcher.js +64 -7
  15. package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-parts.js +37 -25
  16. package/dist/rules/syntactic-patterns/contrast/private/negation-vocabulary.js +27 -0
  17. package/dist/rules/syntactic-patterns/contrast/private/negative-consequence.js +301 -0
  18. package/dist/rules/syntactic-patterns/contrast/private/negative-slop-frames.js +51 -38
  19. package/dist/rules/syntactic-patterns/contrast/private/reframe-classification.js +21 -0
  20. package/dist/rules/syntactic-patterns/contrast/private/sequence-reframes.js +9 -0
  21. package/dist/rules/syntactic-patterns/contrast/private/temporal-reframe.js +318 -0
  22. package/dist/rules/syntactic-patterns/lead-ins/generic-signposting.js +85 -22
  23. package/dist/rules/syntactic-patterns/lead-ins/private/component-assignment-frame.js +239 -0
  24. package/dist/rules/syntactic-patterns/lead-ins/private/discourse-evaluation.js +1 -0
  25. package/dist/rules/syntactic-patterns/lead-ins/private/evaluative-colon-frame.js +166 -0
  26. package/dist/rules/syntactic-patterns/lead-ins/private/reaction-frame.js +29 -0
  27. package/dist/rules/syntactic-patterns/lead-ins/private/relative-discourse-frame.js +119 -0
  28. package/package.json +1 -1
@@ -0,0 +1,318 @@
1
+ import { wordTokens } from "../../../../shared/text/tokens.js";
2
+ const ABSTRACT_CATEGORY_HEADS = new Set([
3
+ "acquisition",
4
+ "administration",
5
+ "advertising",
6
+ "analytics",
7
+ "answer",
8
+ "automation",
9
+ "awareness",
10
+ "brand",
11
+ "channel",
12
+ "commerce",
13
+ "compliance",
14
+ "content",
15
+ "control",
16
+ "conversion",
17
+ "copy",
18
+ "decision",
19
+ "delivery",
20
+ "demand",
21
+ "discoverability",
22
+ "discovery",
23
+ "distribution",
24
+ "documentation",
25
+ "editorial",
26
+ "enablement",
27
+ "engagement",
28
+ "execution",
29
+ "experience",
30
+ "governance",
31
+ "growth",
32
+ "hygiene",
33
+ "implementation",
34
+ "indexing",
35
+ "infrastructure",
36
+ "management",
37
+ "maintenance",
38
+ "market",
39
+ "marketing",
40
+ "media",
41
+ "merchandising",
42
+ "operations",
43
+ "optimization",
44
+ "page",
45
+ "plumbing",
46
+ "policy",
47
+ "positioning",
48
+ "procurement",
49
+ "product",
50
+ "production",
51
+ "publishing",
52
+ "ranking",
53
+ "recommendation",
54
+ "regulation",
55
+ "reporting",
56
+ "retrieval",
57
+ "retention",
58
+ "revenue",
59
+ "risk",
60
+ "sales",
61
+ "search",
62
+ "security",
63
+ "service",
64
+ "sourcing",
65
+ "storefront",
66
+ "strategy",
67
+ "support",
68
+ "trust",
69
+ "visibility",
70
+ "workflow",
71
+ "work"
72
+ ]);
73
+ const ARTICLES = new Set(["a", "an", "the"]);
74
+ const PRESENT_COPULAS = new Set(["am", "are", "is"]);
75
+ const CONTRACTED_COPULAS = new Map([
76
+ ["i'm", { copula: "am", subject: ["i"] }],
77
+ ["you're", { copula: "are", subject: ["you"] }],
78
+ ["we're", { copula: "are", subject: ["we"] }],
79
+ ["they're", { copula: "are", subject: ["they"] }],
80
+ ["he's", { copula: "is", subject: ["he"] }],
81
+ ["she's", { copula: "is", subject: ["she"] }],
82
+ ["it's", { copula: "is", subject: ["it"] }],
83
+ ["that's", { copula: "is", subject: ["that"] }]
84
+ ]);
85
+ const PRONOUNS = new Set([
86
+ "he",
87
+ "i",
88
+ "it",
89
+ "she",
90
+ "that",
91
+ "they",
92
+ "this",
93
+ "we",
94
+ "you"
95
+ ]);
96
+ const PLURAL_PRONOUNS = new Set(["they", "we", "you"]);
97
+ const SINGULAR_S_SUBJECTS = new Set([
98
+ "analytics",
99
+ "business",
100
+ "economics",
101
+ "logistics",
102
+ "news",
103
+ "operations",
104
+ "sales"
105
+ ]);
106
+ const FACTUAL_CONNECTORS = new Set([
107
+ "after",
108
+ "although",
109
+ "because",
110
+ "before",
111
+ "if",
112
+ "since",
113
+ "therefore",
114
+ "until",
115
+ "when",
116
+ "while"
117
+ ]);
118
+ const DETAIL_PREPOSITIONS = new Set([
119
+ "above",
120
+ "at",
121
+ "behind",
122
+ "below",
123
+ "beside",
124
+ "between",
125
+ "by",
126
+ "from",
127
+ "in",
128
+ "inside",
129
+ "into",
130
+ "near",
131
+ "of",
132
+ "on",
133
+ "outside",
134
+ "through",
135
+ "to",
136
+ "under",
137
+ "with",
138
+ "within"
139
+ ]);
140
+ const PHYSICAL_PROPERTY_WORDS = new Set([
141
+ "blue",
142
+ "cold",
143
+ "dry",
144
+ "empty",
145
+ "full",
146
+ "green",
147
+ "heavy",
148
+ "hot",
149
+ "large",
150
+ "light",
151
+ "local",
152
+ "long",
153
+ "optional",
154
+ "red",
155
+ "required",
156
+ "shared",
157
+ "short",
158
+ "small",
159
+ "tall",
160
+ "warm",
161
+ "weekly",
162
+ "wet",
163
+ "wide"
164
+ ]);
165
+ const FRAME_SUBJECTS = new Set([
166
+ "audit",
167
+ "content",
168
+ "data",
169
+ "feed",
170
+ "index",
171
+ "indexing",
172
+ "metadata",
173
+ "page",
174
+ "product",
175
+ "search",
176
+ "seo",
177
+ "visibility"
178
+ ]);
179
+ function isCompleteSentence(text) {
180
+ return [".", "!", "?"].includes(text.trim().at(-1) ?? "");
181
+ }
182
+ function isLetter(character) {
183
+ return (character.toLocaleLowerCase("en") !== character.toLocaleUpperCase("en"));
184
+ }
185
+ function isInteriorApostrophe(text, index) {
186
+ return isLetter(text[index - 1] ?? "") && isLetter(text[index + 1] ?? "");
187
+ }
188
+ function hasQuotedValueOrNumber(text) {
189
+ for (let index = 0; index < text.length; index += 1) {
190
+ const character = text[index] ?? "";
191
+ if ((character >= "0" && character <= "9") ||
192
+ ['"', "`", "\u201c", "\u201d"].includes(character)) {
193
+ return true;
194
+ }
195
+ if (["'", "\u2018", "\u2019"].includes(character) &&
196
+ !isInteriorApostrophe(text, index)) {
197
+ return true;
198
+ }
199
+ }
200
+ return false;
201
+ }
202
+ function withoutArticle(words) {
203
+ return ARTICLES.has(words[0] ?? "") ? words.slice(1) : words;
204
+ }
205
+ function parsePastClause(text) {
206
+ const words = wordTokens(text).map((token) => token.normalized);
207
+ const usedIndex = words.findIndex((word, index) => word === "used" && words[index + 1] === "to" && words[index + 2] === "be");
208
+ const subject = words.slice(0, usedIndex);
209
+ const label = withoutArticle(words.slice(usedIndex + 3));
210
+ return usedIndex > 0 &&
211
+ subject.length <= 4 &&
212
+ label.length > 0 &&
213
+ label.length <= 4
214
+ ? { label, subject }
215
+ : undefined;
216
+ }
217
+ function parsePresentClause(text) {
218
+ const words = wordTokens(text).map((token) => token.normalized);
219
+ const start = words[0] === "now" ? 1 : 0;
220
+ const contracted = CONTRACTED_COPULAS.get(words[start] ?? "");
221
+ if (contracted !== undefined) {
222
+ const label = withoutArticle(words.slice(start + 1));
223
+ return label.length > 0 && label.length <= 4
224
+ ? { ...contracted, label }
225
+ : undefined;
226
+ }
227
+ const copulaIndex = words.findIndex((word, index) => index >= start && PRESENT_COPULAS.has(word));
228
+ const copula = words[copulaIndex];
229
+ const subject = words.slice(start, copulaIndex);
230
+ const label = withoutArticle(words.slice(copulaIndex + 1));
231
+ return copulaIndex > start &&
232
+ copula !== undefined &&
233
+ subject.length <= 4 &&
234
+ label.length > 0 &&
235
+ label.length <= 4
236
+ ? { copula, label, subject }
237
+ : undefined;
238
+ }
239
+ function sameSubjectOrPronoun(first, second) {
240
+ if (first.length === second.length &&
241
+ first.every((word, index) => word === second[index])) {
242
+ return true;
243
+ }
244
+ if (second.length !== 1 || !PRONOUNS.has(second[0] ?? "")) {
245
+ return false;
246
+ }
247
+ const firstPronoun = first.length === 1 ? first[0] : undefined;
248
+ if (firstPronoun !== undefined && PRONOUNS.has(firstPronoun)) {
249
+ return firstPronoun === second[0];
250
+ }
251
+ const subjectHead = first.at(-1) ?? "";
252
+ const pluralSubject = isPluralSubjectHead(subjectHead);
253
+ return pluralSubject
254
+ ? second[0] === "they"
255
+ : ["it", "that", "this"].includes(second[0] ?? "");
256
+ }
257
+ function isPluralSubjectHead(head) {
258
+ return (head.endsWith("s") && !head.endsWith("ss") && !SINGULAR_S_SUBJECTS.has(head));
259
+ }
260
+ function hasCopulaAgreement(clause) {
261
+ const subject = clause.subject;
262
+ const copula = clause.copula;
263
+ if (copula === undefined) {
264
+ return false;
265
+ }
266
+ if (subject.length === 1 && PRONOUNS.has(subject[0] ?? "")) {
267
+ if (subject[0] === "i") {
268
+ return copula === "am";
269
+ }
270
+ return PLURAL_PRONOUNS.has(subject[0] ?? "")
271
+ ? copula === "are"
272
+ : copula === "is";
273
+ }
274
+ const head = subject.at(-1) ?? "";
275
+ const plural = isPluralSubjectHead(head);
276
+ return copula === (plural ? "are" : "is");
277
+ }
278
+ function isAbstractLabel(label) {
279
+ return ABSTRACT_CATEGORY_HEADS.has(label.at(-1) ?? "");
280
+ }
281
+ function hasBlockedDetail(words) {
282
+ return words.some((word) => FACTUAL_CONNECTORS.has(word) ||
283
+ DETAIL_PREPOSITIONS.has(word) ||
284
+ PHYSICAL_PROPERTY_WORDS.has(word));
285
+ }
286
+ function isPassiveLabel(label) {
287
+ const head = label[0] ?? "";
288
+ return head.endsWith("ed") || head.endsWith("en");
289
+ }
290
+ function isFrameSubject(subject) {
291
+ const head = subject.at(-1) ?? "";
292
+ return (FRAME_SUBJECTS.has(head) ||
293
+ (subject.length === 1 && ["it", "that", "this"].includes(head)));
294
+ }
295
+ export function matchTemporalReframe(first, second) {
296
+ if (!isCompleteSentence(first) ||
297
+ !isCompleteSentence(second) ||
298
+ hasQuotedValueOrNumber(first) ||
299
+ hasQuotedValueOrNumber(second)) {
300
+ return undefined;
301
+ }
302
+ const past = parsePastClause(first);
303
+ const present = parsePresentClause(second);
304
+ if (past === undefined || present === undefined) {
305
+ return undefined;
306
+ }
307
+ const allWords = [...past.subject, ...past.label, ...present.label];
308
+ return sameSubjectOrPronoun(past.subject, present.subject) &&
309
+ isFrameSubject(past.subject) &&
310
+ hasCopulaAgreement(present) &&
311
+ (isAbstractLabel(past.label) || isAbstractLabel(present.label)) &&
312
+ !hasBlockedDetail(allWords) &&
313
+ !isPassiveLabel(past.label) &&
314
+ !isPassiveLabel(present.label)
315
+ ? `${first} ${second}`
316
+ : undefined;
317
+ }
318
+ //# sourceMappingURL=temporal-reframe.js.map
@@ -1,7 +1,12 @@
1
+ import { defineTextlintRule } from "../../../adapters/textlint/rule.js";
2
+ import { paragraphUnits, sentenceUnits } from "../../../adapters/textlint/units.js";
1
3
  import { hasConcreteImplementationSummary } from "../../../shared/matchers/concrete-evidence.js";
2
4
  import { cleanSentence, containsAny, tokens } from "../../../shared/matchers/prose-patterns.js";
3
- import { oneToOneRule } from "../../private/textlint-rule-builders.js";
4
- import { isAbstractAuditFrame, matchDiscourseEvaluationFrame, matchExpandedDiscourseFrame } from "./private/discourse-evaluation.js";
5
+ import { splitSentences } from "../../../shared/text/sentences.js";
6
+ import { matchComponentAssignmentFrame } from "./private/component-assignment-frame.js";
7
+ import { isAbstractAuditFrame, matchReactionFrame, matchDiscourseEvaluationFrame, matchExpandedDiscourseFrame } from "./private/discourse-evaluation.js";
8
+ import { matchEvaluativeColonFrame } from "./private/evaluative-colon-frame.js";
9
+ import { matchRelativeDiscourseFrame } from "./private/relative-discourse-frame.js";
5
10
  const PREFIXES = ["however, ", "but ", "and ", "so "];
6
11
  // "as such" was removed: it is a normal anaphoric connective ("a registered adviser; as
7
12
  // such, it must...") and was the dominant false positive, not a signposting frame.
@@ -87,6 +92,19 @@ const WHAT_FRAME_TAIL_STARTERS = [
87
92
  "true",
88
93
  "usually"
89
94
  ];
95
+ const WHAT_MATTERS_CLAUSE_STARTERS = [
96
+ "how",
97
+ "if",
98
+ "that",
99
+ "what",
100
+ "when",
101
+ "where",
102
+ "whether",
103
+ "which",
104
+ "who",
105
+ "whose",
106
+ "why"
107
+ ];
90
108
  const POINT_NOUNS = ["goal", "job", "key", "point", "takeaway", "trick"];
91
109
  function matchModifiedAbstractFrame(words) {
92
110
  const [first, second, third, fourth] = words;
@@ -123,7 +141,8 @@ function matchWhatFrame(words) {
123
141
  second === "changes") &&
124
142
  third === "is" &&
125
143
  fourth !== undefined &&
126
- WHAT_FRAME_TAIL_STARTERS.includes(fourth)) {
144
+ (WHAT_FRAME_TAIL_STARTERS.includes(fourth) ||
145
+ (second === "matters" && !WHAT_MATTERS_CLAUSE_STARTERS.includes(fourth)))) {
127
146
  return `what-${second}-is`;
128
147
  }
129
148
  if (first === "what" && second === "matters" && third === "most") {
@@ -139,11 +158,13 @@ function matchWhatFrame(words) {
139
158
  }
140
159
  function matchAbstractFrame(text) {
141
160
  const words = tokens(text);
142
- return (matchExpandedDiscourseFrame(words) ??
161
+ return (matchRelativeDiscourseFrame(text, words) ??
162
+ matchExpandedDiscourseFrame(words) ??
143
163
  matchModifiedAbstractFrame(words) ??
144
164
  matchDiscourseEvaluationFrame(words) ??
145
165
  matchPointIsToFrame(words) ??
146
- matchWhatFrame(words));
166
+ matchWhatFrame(words) ??
167
+ matchReactionFrame(words));
147
168
  }
148
169
  function matchFormulaicContentSetup(text) {
149
170
  if (text.startsWith("each ") &&
@@ -198,7 +219,10 @@ function matchSignposting(sentence) {
198
219
  : stripped);
199
220
  const formulaicSetup = matchFormulaicContentSetup(stripped);
200
221
  const generatedFormula = matchGeneratedFormula(stripped);
201
- if (abstract !== undefined && !concreteImplementation) {
222
+ if (abstract !== undefined &&
223
+ (abstract === "what-matters-is" ||
224
+ abstract.startsWith("relative-") ||
225
+ !concreteImplementation)) {
202
226
  return { kind: "abstract-evaluation-frame", signal: abstract };
203
227
  }
204
228
  if (generatedFormula !== undefined && !concreteImplementation) {
@@ -233,24 +257,63 @@ function matchSignposting(sentence) {
233
257
  }
234
258
  return undefined;
235
259
  }
236
- const rule = oneToOneRule({
237
- detect: (unit) => {
238
- const matched = matchSignposting(unit.text);
239
- if (matched === undefined) {
240
- return [];
241
- }
242
- return [
243
- {
244
- evidence: matched.signal,
245
- label: matched.kind,
246
- range: { start: 0, end: unit.text.length }
247
- }
248
- ];
260
+ const rule = defineTextlintRule({
261
+ detector: {
262
+ detect: ({ units }) => {
263
+ const detections = units.flatMap((unit) => {
264
+ if (unit.kind === "paragraph") {
265
+ const sentences = splitSentences(unit.text);
266
+ const pairDetections = [];
267
+ for (let index = 0; index < sentences.length - 1; index += 1) {
268
+ const current = sentences[index];
269
+ const next = sentences[index + 1];
270
+ if (current === undefined || next === undefined) {
271
+ continue;
272
+ }
273
+ const signal = matchComponentAssignmentFrame(current.text, next.text);
274
+ if (signal !== undefined) {
275
+ pairDetections.push({
276
+ evidence: signal,
277
+ label: "component-assignment-frame",
278
+ range: { start: current.start, end: next.end },
279
+ ruleId: "syntactic-patterns:generic-signposting",
280
+ unitId: unit.id
281
+ });
282
+ }
283
+ }
284
+ return pairDetections;
285
+ }
286
+ const componentSignal = matchComponentAssignmentFrame(unit.text);
287
+ const colonSignal = matchEvaluativeColonFrame(unit.text);
288
+ const matched = componentSignal === undefined && colonSignal === undefined
289
+ ? matchSignposting(unit.text)
290
+ : undefined;
291
+ const signal = componentSignal ?? colonSignal ?? matched?.signal;
292
+ if (signal === undefined) {
293
+ return [];
294
+ }
295
+ return [
296
+ {
297
+ evidence: signal,
298
+ label: componentSignal !== undefined
299
+ ? "component-assignment-frame"
300
+ : colonSignal !== undefined
301
+ ? "evaluative-colon-frame"
302
+ : (matched?.kind ?? "generic-signposting"),
303
+ range: { start: 0, end: unit.text.length },
304
+ ruleId: "syntactic-patterns:generic-signposting",
305
+ unitId: unit.id
306
+ }
307
+ ];
308
+ });
309
+ return detections;
310
+ },
311
+ family: "syntactic-patterns",
312
+ id: "syntactic-patterns:generic-signposting"
249
313
  },
250
- family: "syntactic-patterns",
251
314
  formatMessage: (report) => `Generic signposting found: ${report.evidence}. Replace the frame with the concrete claim.`,
252
- ruleId: "syntactic-patterns:generic-signposting",
253
- unitKind: "sentence"
315
+ reportPolicy: { kind: "one-to-one" },
316
+ units: (document) => [...sentenceUnits(document), ...paragraphUnits(document)]
254
317
  });
255
318
  export default rule;
256
319
  //# sourceMappingURL=generic-signposting.js.map