slopless 0.2.18 → 0.2.20
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/adapters/textlint/report.js +9 -0
- package/dist/presets/everything.js +6 -0
- package/dist/registries/phrases/builtin.js +23 -0
- package/dist/registries/phrases/families.js +14 -0
- package/dist/registries/phrases.js +4 -20
- package/dist/registries/syntactic-patterns/lead-ins.js +3 -1
- package/dist/registries/words.js +2 -0
- package/dist/reporting/density.js +23 -0
- package/dist/reporting/reports.js +34 -0
- package/dist/rules/narrative-slop/perception-verb-density.js +16 -21
- package/dist/rules/orthography/fake-timestamps.js +53 -0
- package/dist/rules/phrases/data/cliches.json +62 -3
- package/dist/rules/phrases/data/corporate-speak.json +56 -1
- package/dist/rules/phrases/data/genre-cliches.json +53 -0
- package/dist/rules/phrases/data/self-help-cliches.json +62 -0
- package/dist/rules/phrases/data/seo-filler.json +34 -0
- package/dist/rules/phrases/data/skunked-terms.json +1 -10
- package/dist/rules/phrases/data/weasel-attribution.json +37 -0
- package/dist/rules/phrases/genre-cliches.js +17 -0
- package/dist/rules/phrases/humble-bragger.js +3 -5
- package/dist/rules/phrases/self-help-cliches.js +17 -0
- package/dist/rules/phrases/seo-filler.js +17 -0
- package/dist/rules/phrases/weasel-attribution.js +21 -0
- package/dist/rules/syntactic-patterns/contrast/private/negation-reframe-matcher.js +5 -0
- package/dist/rules/syntactic-patterns/generalization/softening-language.js +7 -1
- package/dist/rules/syntactic-patterns/generalization/universalizing-claims.js +5 -82
- package/dist/rules/syntactic-patterns/lead-ins/boilerplate-framing.js +30 -5
- package/dist/rules/syntactic-patterns/lead-ins/generic-signposting.js +3 -8
- package/dist/rules/syntactic-patterns/lead-ins/significance-density.js +95 -0
- package/dist/rules/syntactic-patterns/llm-artifacts/data/response-wrapper-patterns.json +1 -7
- package/dist/rules/syntactic-patterns/repetition/triple-repeat.js +28 -0
- package/dist/rules/words/actually-overuse.js +45 -0
- package/dist/rules/words/data/prohibited-words.json +0 -1
- package/dist/rules/words/llm-vocabulary-density.js +0 -1
- package/package.json +1 -1
|
@@ -8,6 +8,15 @@ export function emitTextlintReport(context, unitsById, report) {
|
|
|
8
8
|
const padding = report.range.start === report.range.end
|
|
9
9
|
? context.locator.at(report.range.start)
|
|
10
10
|
: context.locator.range([report.range.start, report.range.end]);
|
|
11
|
+
// textlint reads a per-report severity only from a plain reported object, not from a
|
|
12
|
+
// RuleError instance (kernel: `ruleReportedObject.severity || error`). The reporter sets
|
|
13
|
+
// report.severity when a policy judges the level (1 = warning, 2 = error); everything else
|
|
14
|
+
// keeps the default error severity via RuleError.
|
|
15
|
+
const { severity } = report;
|
|
16
|
+
if (typeof severity === "number") {
|
|
17
|
+
context.report(unit.node, { message: report.message, padding, severity });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
11
20
|
context.report(unit.node, new context.RuleError(report.message, {
|
|
12
21
|
padding
|
|
13
22
|
}));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export const everything = {
|
|
2
2
|
rules: {
|
|
3
|
+
"actually-overuse": true,
|
|
3
4
|
"affirmation-closers": true,
|
|
4
5
|
"artifact-placeholders": true,
|
|
5
6
|
"authority-padding": true,
|
|
@@ -49,6 +50,11 @@ export const everything = {
|
|
|
49
50
|
"required-terms": true,
|
|
50
51
|
"sentence-case": true,
|
|
51
52
|
"semantic-thinness": true,
|
|
53
|
+
"significance-density": true,
|
|
54
|
+
"seo-filler": true,
|
|
55
|
+
"self-help-cliches": true,
|
|
56
|
+
"genre-cliches": true,
|
|
57
|
+
"weasel-attribution": true,
|
|
52
58
|
simplicity: true,
|
|
53
59
|
"skunked-terms": true,
|
|
54
60
|
"softening-language": true,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import cliches from "../../rules/phrases/cliches.js";
|
|
2
|
+
import corporateSpeak from "../../rules/phrases/corporate-speak.js";
|
|
3
|
+
import humbleBragger from "../../rules/phrases/humble-bragger.js";
|
|
4
|
+
import jargonFaker from "../../rules/phrases/jargon-faker.js";
|
|
5
|
+
import llmDisclaimer from "../../rules/phrases/llm-disclaimer.js";
|
|
6
|
+
import prohibitedPhrases from "../../rules/phrases/prohibited-phrases.js";
|
|
7
|
+
import redundancy from "../../rules/phrases/redundancy.js";
|
|
8
|
+
import skunkedTerms from "../../rules/phrases/skunked-terms.js";
|
|
9
|
+
import uncomparables from "../../rules/phrases/uncomparables.js";
|
|
10
|
+
import wordiness from "../../rules/phrases/wordiness.js";
|
|
11
|
+
export const phraseBuiltinRules = {
|
|
12
|
+
cliches,
|
|
13
|
+
"corporate-speak": corporateSpeak,
|
|
14
|
+
"humble-bragger": humbleBragger,
|
|
15
|
+
"jargon-faker": jargonFaker,
|
|
16
|
+
"llm-disclaimer": llmDisclaimer,
|
|
17
|
+
"prohibited-phrases": prohibitedPhrases,
|
|
18
|
+
redundancy,
|
|
19
|
+
"skunked-terms": skunkedTerms,
|
|
20
|
+
uncomparables,
|
|
21
|
+
wordiness
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=builtin.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import genreCliches from "../../rules/phrases/genre-cliches.js";
|
|
2
|
+
import seoFiller from "../../rules/phrases/seo-filler.js";
|
|
3
|
+
import selfHelpCliches from "../../rules/phrases/self-help-cliches.js";
|
|
4
|
+
import weaselAttribution from "../../rules/phrases/weasel-attribution.js";
|
|
5
|
+
// New slop families added from the corpus-driven expansion. Each is a phrase-list
|
|
6
|
+
// rule (findUnquotedPhraseMatches over a data JSON), screened for low false positives
|
|
7
|
+
// against a multi-topic human corpus.
|
|
8
|
+
export const phraseFamilyRules = {
|
|
9
|
+
"genre-cliches": genreCliches,
|
|
10
|
+
"seo-filler": seoFiller,
|
|
11
|
+
"self-help-cliches": selfHelpCliches,
|
|
12
|
+
"weasel-attribution": weaselAttribution
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=families.js.map
|
|
@@ -1,23 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import humbleBragger from "../rules/phrases/humble-bragger.js";
|
|
4
|
-
import jargonFaker from "../rules/phrases/jargon-faker.js";
|
|
5
|
-
import llmDisclaimer from "../rules/phrases/llm-disclaimer.js";
|
|
6
|
-
import prohibitedPhrases from "../rules/phrases/prohibited-phrases.js";
|
|
7
|
-
import redundancy from "../rules/phrases/redundancy.js";
|
|
8
|
-
import skunkedTerms from "../rules/phrases/skunked-terms.js";
|
|
9
|
-
import uncomparables from "../rules/phrases/uncomparables.js";
|
|
10
|
-
import wordiness from "../rules/phrases/wordiness.js";
|
|
1
|
+
import { phraseBuiltinRules } from "./phrases/builtin.js";
|
|
2
|
+
import { phraseFamilyRules } from "./phrases/families.js";
|
|
11
3
|
export const phraseRules = {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"humble-bragger": humbleBragger,
|
|
15
|
-
"jargon-faker": jargonFaker,
|
|
16
|
-
"llm-disclaimer": llmDisclaimer,
|
|
17
|
-
"prohibited-phrases": prohibitedPhrases,
|
|
18
|
-
redundancy,
|
|
19
|
-
"skunked-terms": skunkedTerms,
|
|
20
|
-
uncomparables,
|
|
21
|
-
wordiness
|
|
4
|
+
...phraseBuiltinRules,
|
|
5
|
+
...phraseFamilyRules
|
|
22
6
|
};
|
|
23
7
|
//# sourceMappingURL=phrases.js.map
|
|
@@ -4,12 +4,14 @@ import genericSignposting from "../../rules/syntactic-patterns/lead-ins/generic-
|
|
|
4
4
|
import lessonFraming from "../../rules/syntactic-patterns/lead-ins/lesson-framing.js";
|
|
5
5
|
import llmOpeners from "../../rules/syntactic-patterns/lead-ins/llm-openers.js";
|
|
6
6
|
import observerGuidance from "../../rules/syntactic-patterns/lead-ins/observer-guidance.js";
|
|
7
|
+
import significanceDensity from "../../rules/syntactic-patterns/lead-ins/significance-density.js";
|
|
7
8
|
export const leadInRules = {
|
|
8
9
|
"boilerplate-framing": boilerplateFraming,
|
|
9
10
|
"formal-transition-density": formalTransitionDensity,
|
|
10
11
|
"generic-signposting": genericSignposting,
|
|
11
12
|
"lesson-framing": lessonFraming,
|
|
12
13
|
"llm-openers": llmOpeners,
|
|
13
|
-
"observer-guidance": observerGuidance
|
|
14
|
+
"observer-guidance": observerGuidance,
|
|
15
|
+
"significance-density": significanceDensity
|
|
14
16
|
};
|
|
15
17
|
//# sourceMappingURL=lead-ins.js.map
|
package/dist/registries/words.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import actuallyOveruse from "../rules/words/actually-overuse.js";
|
|
1
2
|
import hedgeStacking from "../rules/words/hedge-stacking.js";
|
|
2
3
|
import llmVocabularyDensity from "../rules/words/llm-vocabulary-density.js";
|
|
3
4
|
import llmVocabulary from "../rules/words/llm-vocabulary.js";
|
|
4
5
|
import prohibitedWords from "../rules/words/prohibited-words.js";
|
|
5
6
|
import simplicity from "../rules/words/simplicity.js";
|
|
6
7
|
export const wordRules = {
|
|
8
|
+
"actually-overuse": actuallyOveruse,
|
|
7
9
|
"hedge-stacking": hedgeStacking,
|
|
8
10
|
"llm-vocabulary-density": llmVocabularyDensity,
|
|
9
11
|
"llm-vocabulary": llmVocabulary,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Density calculation used by the reporter to judge raw detections. Rules emit occurrences
|
|
2
|
+
// only; the reporter hands the occurrence count and the unit word count here, and this
|
|
3
|
+
// module decides whether the density is acceptable, a warning, or an error. It has no rule
|
|
4
|
+
// or textlint knowledge so it stays reusable and unit-testable on its own.
|
|
5
|
+
export const WARNING_SEVERITY = 1;
|
|
6
|
+
export const ERROR_SEVERITY = 2;
|
|
7
|
+
// Occurrences per `wordsPerUnit` words. Below the warning rate (or below the minimum count)
|
|
8
|
+
// there is no finding; above the error rate it is an error; otherwise a warning. A single
|
|
9
|
+
// occurrence never fires when minimumOccurrences is 2 or more.
|
|
10
|
+
export function rateVerdict(occurrences, words, thresholds) {
|
|
11
|
+
if (occurrences < thresholds.minimumOccurrences || words <= 0) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const perUnit = (occurrences * thresholds.wordsPerUnit) / words;
|
|
15
|
+
if (perUnit <= thresholds.warningPerUnit) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
perUnit,
|
|
20
|
+
severity: perUnit > thresholds.errorPerUnit ? ERROR_SEVERITY : WARNING_SEVERITY
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=density.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { splitSentences } from "../shared/text/sentences.js";
|
|
2
2
|
import { wordTokens } from "../shared/text/tokens.js";
|
|
3
|
+
import { rateVerdict } from "./density.js";
|
|
3
4
|
function detectionWithin(detection, span) {
|
|
4
5
|
return (detection.group !== undefined &&
|
|
5
6
|
detection.range.start >= span.start &&
|
|
@@ -89,6 +90,37 @@ export function densityReports(unit, detections, config) {
|
|
|
89
90
|
}
|
|
90
91
|
];
|
|
91
92
|
}
|
|
93
|
+
export function densityRateReports(units, detections, policy, formatMessage) {
|
|
94
|
+
const reports = [];
|
|
95
|
+
for (const unit of units) {
|
|
96
|
+
const unitDetections = detections.filter((detection) => detection.unitId === unit.id);
|
|
97
|
+
const first = unitDetections[0];
|
|
98
|
+
const verdict = rateVerdict(unitDetections.length, wordTokens(unit.text).length, {
|
|
99
|
+
errorPerUnit: policy.errorPerUnit,
|
|
100
|
+
minimumOccurrences: policy.minimumOccurrences,
|
|
101
|
+
warningPerUnit: policy.warningPerUnit,
|
|
102
|
+
wordsPerUnit: policy.wordsPerUnit
|
|
103
|
+
});
|
|
104
|
+
if (verdict === undefined || first === undefined) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const report = {
|
|
108
|
+
detections: unitDetections,
|
|
109
|
+
evidence: first.evidence,
|
|
110
|
+
message: "",
|
|
111
|
+
metric: {
|
|
112
|
+
count: unitDetections.length,
|
|
113
|
+
perUnit: Math.round(verdict.perUnit * 10) / 10
|
|
114
|
+
},
|
|
115
|
+
range: unit.sourceRangeFor(first.range),
|
|
116
|
+
ruleId: first.ruleId,
|
|
117
|
+
severity: verdict.severity,
|
|
118
|
+
unitId: unit.id
|
|
119
|
+
};
|
|
120
|
+
reports.push({ ...report, message: formatMessage(report) });
|
|
121
|
+
}
|
|
122
|
+
return reports;
|
|
123
|
+
}
|
|
92
124
|
export function reportsForPolicy(units, detections, policy, formatMessage) {
|
|
93
125
|
switch (policy.kind) {
|
|
94
126
|
case "one-to-one": {
|
|
@@ -137,6 +169,8 @@ export function reportsForPolicy(units, detections, policy, formatMessage) {
|
|
|
137
169
|
message: formatMessage(report)
|
|
138
170
|
}));
|
|
139
171
|
}
|
|
172
|
+
case "density-rate":
|
|
173
|
+
return densityRateReports(units, detections, policy, formatMessage);
|
|
140
174
|
}
|
|
141
175
|
}
|
|
142
176
|
//# sourceMappingURL=reports.js.map
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { defineTextlintRule } from "../../adapters/textlint/rule.js";
|
|
2
2
|
import { paragraphUnits } from "../../adapters/textlint/units.js";
|
|
3
3
|
import { wordTokens } from "../../shared/text/tokens.js";
|
|
4
|
+
// Dropped the tokens whose dominant sense is non-narrative and which drove the bulk of
|
|
5
|
+
// the human-corpus mis-fires: "see/sees/seeing" (generic: "you can see that"), the present
|
|
6
|
+
// "focus"/"focuses" ("focus on strategy"; narrative "focused"/"focusing" are kept),
|
|
7
|
+
// "peer/peers/peering" (the colleague noun and "peer review"),
|
|
8
|
+
// and the plural nouns "studies"/"notices" ("studies show", "post notices"). The narrative
|
|
9
|
+
// perception verbs - including observe/study/notice/scan, which genuine scene-slop stacks
|
|
10
|
+
// ("she scanned the room, noticed the door, studied his face") - are kept.
|
|
4
11
|
const PERCEPTION_VERBS = new Set([
|
|
5
|
-
"focus",
|
|
6
|
-
"focuses",
|
|
7
|
-
"focused",
|
|
8
|
-
"focusing",
|
|
9
12
|
"look",
|
|
10
13
|
"looks",
|
|
11
14
|
"looked",
|
|
@@ -26,34 +29,26 @@ const PERCEPTION_VERBS = new Set([
|
|
|
26
29
|
"glances",
|
|
27
30
|
"glanced",
|
|
28
31
|
"glancing",
|
|
29
|
-
"observe",
|
|
30
|
-
"observes",
|
|
31
|
-
"observed",
|
|
32
|
-
"observing",
|
|
33
|
-
"scan",
|
|
34
|
-
"scans",
|
|
35
|
-
"scanned",
|
|
36
|
-
"scanning",
|
|
37
|
-
"peer",
|
|
38
|
-
"peers",
|
|
39
32
|
"peered",
|
|
40
|
-
"peering",
|
|
41
33
|
"squint",
|
|
42
34
|
"squints",
|
|
43
35
|
"squinted",
|
|
44
36
|
"squinting",
|
|
37
|
+
"saw",
|
|
38
|
+
"focused",
|
|
39
|
+
"focusing",
|
|
40
|
+
"observe",
|
|
41
|
+
"observed",
|
|
42
|
+
"observing",
|
|
45
43
|
"study",
|
|
46
|
-
"studies",
|
|
47
44
|
"studied",
|
|
48
45
|
"studying",
|
|
49
46
|
"notice",
|
|
50
|
-
"notices",
|
|
51
47
|
"noticed",
|
|
52
48
|
"noticing",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"seeing"
|
|
49
|
+
"scan",
|
|
50
|
+
"scanned",
|
|
51
|
+
"scanning"
|
|
57
52
|
]);
|
|
58
53
|
const PURPOSE_LOOK_PARTICLES = new Set(["for", "up", "under", "into"]);
|
|
59
54
|
const MAX_PARAGRAPH_TOKENS = 80;
|
|
@@ -3,6 +3,56 @@ import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
|
3
3
|
function isAsciiDigit(character) {
|
|
4
4
|
return character !== undefined && character >= "0" && character <= "9";
|
|
5
5
|
}
|
|
6
|
+
// A timestamp accompanied by a real date (weekday, month, year, or a date label) is a
|
|
7
|
+
// genuine timestamp - an email header, a logged event, a scheduled time - not fabricated
|
|
8
|
+
// AI clock specificity. Skip those.
|
|
9
|
+
const DATE_WORDS = [
|
|
10
|
+
"january",
|
|
11
|
+
"february",
|
|
12
|
+
"march",
|
|
13
|
+
"april",
|
|
14
|
+
"june",
|
|
15
|
+
"july",
|
|
16
|
+
"august",
|
|
17
|
+
"september",
|
|
18
|
+
"october",
|
|
19
|
+
"november",
|
|
20
|
+
"december",
|
|
21
|
+
"monday",
|
|
22
|
+
"tuesday",
|
|
23
|
+
"wednesday",
|
|
24
|
+
"thursday",
|
|
25
|
+
"friday",
|
|
26
|
+
"saturday",
|
|
27
|
+
"sunday"
|
|
28
|
+
];
|
|
29
|
+
const DATE_LABELS = [
|
|
30
|
+
"date:",
|
|
31
|
+
"sent:",
|
|
32
|
+
"received:",
|
|
33
|
+
"posted",
|
|
34
|
+
"published",
|
|
35
|
+
"updated",
|
|
36
|
+
"timestamp"
|
|
37
|
+
];
|
|
38
|
+
function hasFourDigitYear(text) {
|
|
39
|
+
for (let i = 0; i + 3 < text.length; i += 1) {
|
|
40
|
+
const a = text[i];
|
|
41
|
+
const b = text[i + 1];
|
|
42
|
+
if (((a === "1" && b === "9") || (a === "2" && b === "0")) &&
|
|
43
|
+
isAsciiDigit(text[i + 2]) &&
|
|
44
|
+
isAsciiDigit(text[i + 3])) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
function hasDateContext(sentence) {
|
|
51
|
+
const lower = sentence.toLocaleLowerCase("en");
|
|
52
|
+
return (DATE_LABELS.some((label) => lower.includes(label)) ||
|
|
53
|
+
DATE_WORDS.some((word) => lower.includes(word)) ||
|
|
54
|
+
hasFourDigitYear(lower));
|
|
55
|
+
}
|
|
6
56
|
function isPeriodMarker(first, second) {
|
|
7
57
|
if (first === undefined || second === undefined) {
|
|
8
58
|
return false;
|
|
@@ -56,6 +106,9 @@ function findTimestampMatches(text) {
|
|
|
56
106
|
function findSentenceTimestampMatches(text) {
|
|
57
107
|
const matches = [];
|
|
58
108
|
for (const sentence of splitSentences(text)) {
|
|
109
|
+
if (hasDateContext(sentence.text)) {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
59
112
|
const timestampMatches = findTimestampMatches(sentence.text);
|
|
60
113
|
if (timestampMatches.length === 0) {
|
|
61
114
|
continue;
|
|
@@ -331,7 +331,6 @@
|
|
|
331
331
|
"in a pinch",
|
|
332
332
|
"in a word",
|
|
333
333
|
"in hot water",
|
|
334
|
-
"in the final analysis",
|
|
335
334
|
"in the gutter",
|
|
336
335
|
"in the last analysis",
|
|
337
336
|
"in the nick of time",
|
|
@@ -339,7 +338,6 @@
|
|
|
339
338
|
"in your dreams",
|
|
340
339
|
"innocent bystander",
|
|
341
340
|
"it ain't over till the fat lady sings",
|
|
342
|
-
"it goes without saying",
|
|
343
341
|
"it takes all kinds",
|
|
344
342
|
"it takes one to know one",
|
|
345
343
|
"it's a small world",
|
|
@@ -634,6 +632,29 @@
|
|
|
634
632
|
"spring to life",
|
|
635
633
|
"squeaky wheel gets the grease",
|
|
636
634
|
"stands as a testament to",
|
|
635
|
+
"serves as a reminder that",
|
|
636
|
+
"serves as a testament to",
|
|
637
|
+
"paves the way for",
|
|
638
|
+
"ushers in a new era",
|
|
639
|
+
"speaks volumes about",
|
|
640
|
+
"reflects a broader",
|
|
641
|
+
"stands as a beacon of",
|
|
642
|
+
"represents a paradigm shift",
|
|
643
|
+
"embodies the spirit of",
|
|
644
|
+
"cements its place as",
|
|
645
|
+
"leaves an indelible mark",
|
|
646
|
+
"heralds a new era",
|
|
647
|
+
"signals a seismic shift",
|
|
648
|
+
"drives home the point",
|
|
649
|
+
"brings to the fore",
|
|
650
|
+
"takes center stage",
|
|
651
|
+
"reshapes the landscape",
|
|
652
|
+
"charts a new course",
|
|
653
|
+
"shines a spotlight on",
|
|
654
|
+
"amplifies the need for",
|
|
655
|
+
"redefines the boundaries of",
|
|
656
|
+
"paints a vivid picture of",
|
|
657
|
+
"holds the key to",
|
|
637
658
|
"stands out like a sore thumb",
|
|
638
659
|
"start from scratch",
|
|
639
660
|
"stick in the mud",
|
|
@@ -768,5 +789,43 @@
|
|
|
768
789
|
"you only live once",
|
|
769
790
|
"you're the boss",
|
|
770
791
|
"young and foolish",
|
|
771
|
-
"young and vibrant"
|
|
792
|
+
"young and vibrant",
|
|
793
|
+
"a watershed moment",
|
|
794
|
+
"as we move forward",
|
|
795
|
+
"at a pivotal moment",
|
|
796
|
+
"beacon of hope",
|
|
797
|
+
"contributing to the fabric of",
|
|
798
|
+
"discover the wonders of",
|
|
799
|
+
"drive engagement",
|
|
800
|
+
"embrace the journey",
|
|
801
|
+
"empower individuals to",
|
|
802
|
+
"everything happens for a reason",
|
|
803
|
+
"gain a comprehensive understanding",
|
|
804
|
+
"gain valuable insights",
|
|
805
|
+
"harness the power of",
|
|
806
|
+
"has something for everyone",
|
|
807
|
+
"has transformed the landscape of",
|
|
808
|
+
"immerse yourself in",
|
|
809
|
+
"in the not-too-distant future",
|
|
810
|
+
"leave no stone unturned",
|
|
811
|
+
"left an indelible mark",
|
|
812
|
+
"navigate the complexities of",
|
|
813
|
+
"nestled in the heart of",
|
|
814
|
+
"offers valuable insights",
|
|
815
|
+
"plays a pivotal role in shaping",
|
|
816
|
+
"resonate with audiences",
|
|
817
|
+
"revolutionized the way we",
|
|
818
|
+
"rich cultural tapestry",
|
|
819
|
+
"sheds light on",
|
|
820
|
+
"significant contributions to the field",
|
|
821
|
+
"step out of your comfort zone",
|
|
822
|
+
"the stakes have never been higher",
|
|
823
|
+
"there is no one-size-fits-all answer",
|
|
824
|
+
"this changes everything",
|
|
825
|
+
"thriving ecosystem of",
|
|
826
|
+
"treasure trove of",
|
|
827
|
+
"unlock the potential of",
|
|
828
|
+
"unlock your true potential",
|
|
829
|
+
"vibrant hub of",
|
|
830
|
+
"we stand at a crossroads"
|
|
772
831
|
]
|
|
@@ -45,5 +45,60 @@
|
|
|
45
45
|
"unlock our full potential",
|
|
46
46
|
"unlock their full potential",
|
|
47
47
|
"unlock your full potential",
|
|
48
|
-
"win-win"
|
|
48
|
+
"win-win",
|
|
49
|
+
"secret sauce",
|
|
50
|
+
"force multiplier",
|
|
51
|
+
"boil the ocean",
|
|
52
|
+
"best-of-breed",
|
|
53
|
+
"go-to-market motion",
|
|
54
|
+
"bleeding-edge",
|
|
55
|
+
"drive value",
|
|
56
|
+
"deliver impact",
|
|
57
|
+
"move fast and break things",
|
|
58
|
+
"10x your",
|
|
59
|
+
"data-driven insights",
|
|
60
|
+
"actionable insights",
|
|
61
|
+
"empower your team",
|
|
62
|
+
"ecosystem of partners",
|
|
63
|
+
"innovate at the speed of",
|
|
64
|
+
"reimagine the future of",
|
|
65
|
+
"built for the enterprise",
|
|
66
|
+
"10x growth",
|
|
67
|
+
"30,000-foot view",
|
|
68
|
+
"agile methodology",
|
|
69
|
+
"brand storytelling",
|
|
70
|
+
"core competencies",
|
|
71
|
+
"cross-functional alignment",
|
|
72
|
+
"cross-pollination of ideas",
|
|
73
|
+
"data-driven decisions",
|
|
74
|
+
"digital transformation",
|
|
75
|
+
"ducks in a row",
|
|
76
|
+
"eating your own dog food",
|
|
77
|
+
"end-to-end solution",
|
|
78
|
+
"excited to share",
|
|
79
|
+
"future-proof your",
|
|
80
|
+
"headwinds",
|
|
81
|
+
"ideation session",
|
|
82
|
+
"keep all stakeholders aligned",
|
|
83
|
+
"learnings",
|
|
84
|
+
"leverage synergies",
|
|
85
|
+
"move at the speed of business",
|
|
86
|
+
"open the kimono",
|
|
87
|
+
"out-of-the-box thinking",
|
|
88
|
+
"revolutionizing the way",
|
|
89
|
+
"rightsizing",
|
|
90
|
+
"robust ecosystem",
|
|
91
|
+
"robust pipeline",
|
|
92
|
+
"run it up the flagpole",
|
|
93
|
+
"scale our operations",
|
|
94
|
+
"seamless integration",
|
|
95
|
+
"stakeholder buy-in",
|
|
96
|
+
"strategic alignment",
|
|
97
|
+
"streamline processes",
|
|
98
|
+
"tailwinds",
|
|
99
|
+
"talent pipeline",
|
|
100
|
+
"that's where we come in",
|
|
101
|
+
"thought leader",
|
|
102
|
+
"thought leadership",
|
|
103
|
+
"tiger team"
|
|
49
104
|
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[
|
|
2
|
+
"a culinary journey",
|
|
3
|
+
"a feast for the senses",
|
|
4
|
+
"a must for any itinerary",
|
|
5
|
+
"a true gem",
|
|
6
|
+
"after using this for a few weeks",
|
|
7
|
+
"best-kept secret",
|
|
8
|
+
"bucket list destination",
|
|
9
|
+
"bursting with flavor",
|
|
10
|
+
"bustling streets",
|
|
11
|
+
"charming village",
|
|
12
|
+
"city of contrasts",
|
|
13
|
+
"come hungry",
|
|
14
|
+
"completely transformed how i",
|
|
15
|
+
"crystal-clear waters",
|
|
16
|
+
"escape the crowds",
|
|
17
|
+
"even picky eaters will love it",
|
|
18
|
+
"exceeded my expectations",
|
|
19
|
+
"experience the magic of",
|
|
20
|
+
"final verdict",
|
|
21
|
+
"foodie paradise",
|
|
22
|
+
"great for weeknight dinners",
|
|
23
|
+
"hands-down the best",
|
|
24
|
+
"hidden gem",
|
|
25
|
+
"leave room for dessert",
|
|
26
|
+
"local cuisine",
|
|
27
|
+
"local gems",
|
|
28
|
+
"look no further",
|
|
29
|
+
"lose yourself in",
|
|
30
|
+
"my grandmother used to make this",
|
|
31
|
+
"nestled among",
|
|
32
|
+
"no product is perfect, but",
|
|
33
|
+
"off the beaten path",
|
|
34
|
+
"off the tourist trail",
|
|
35
|
+
"soak up the atmosphere",
|
|
36
|
+
"step back in time",
|
|
37
|
+
"stunning scenery",
|
|
38
|
+
"tantalizing flavors",
|
|
39
|
+
"the perfect blend of",
|
|
40
|
+
"the results speak for themselves",
|
|
41
|
+
"this dish brings back so many memories",
|
|
42
|
+
"this place has it all",
|
|
43
|
+
"this recipe is perfect for",
|
|
44
|
+
"trust me, you won't be disappointed",
|
|
45
|
+
"undiscovered paradise",
|
|
46
|
+
"unparalleled views",
|
|
47
|
+
"vibrant culture",
|
|
48
|
+
"what makes this recipe special is",
|
|
49
|
+
"whether you're a food lover or",
|
|
50
|
+
"world-class dining",
|
|
51
|
+
"you won't regret it",
|
|
52
|
+
"you'll love this"
|
|
53
|
+
]
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[
|
|
2
|
+
"a transformative experience",
|
|
3
|
+
"abundance mindset",
|
|
4
|
+
"align with your values",
|
|
5
|
+
"be authentically you",
|
|
6
|
+
"believe in yourself",
|
|
7
|
+
"break free from your limitations",
|
|
8
|
+
"choose yourself",
|
|
9
|
+
"create the life you want",
|
|
10
|
+
"deserve success",
|
|
11
|
+
"do the inner work",
|
|
12
|
+
"embrace change",
|
|
13
|
+
"embrace who you are",
|
|
14
|
+
"embrace your potential",
|
|
15
|
+
"find your purpose",
|
|
16
|
+
"growth mindset",
|
|
17
|
+
"heal your relationship with",
|
|
18
|
+
"honor your feelings",
|
|
19
|
+
"inner work",
|
|
20
|
+
"just stay positive",
|
|
21
|
+
"life is a journey, not a destination",
|
|
22
|
+
"live with intention",
|
|
23
|
+
"live your best life",
|
|
24
|
+
"live your purpose",
|
|
25
|
+
"manifest your dreams",
|
|
26
|
+
"not a destination but a journey",
|
|
27
|
+
"own your story",
|
|
28
|
+
"pour into yourself",
|
|
29
|
+
"practice gratitude",
|
|
30
|
+
"push past your limits",
|
|
31
|
+
"put in the work",
|
|
32
|
+
"radical self-love",
|
|
33
|
+
"reclaim your power",
|
|
34
|
+
"self-care is not selfish",
|
|
35
|
+
"set your intentions",
|
|
36
|
+
"show up for yourself",
|
|
37
|
+
"small changes can make a big difference",
|
|
38
|
+
"start where you are",
|
|
39
|
+
"stepping into your power",
|
|
40
|
+
"take control of your health",
|
|
41
|
+
"tap into your potential",
|
|
42
|
+
"the best version of yourself",
|
|
43
|
+
"the power within",
|
|
44
|
+
"this journey of self-discovery",
|
|
45
|
+
"thrive, not just survive",
|
|
46
|
+
"transformative power of",
|
|
47
|
+
"trust the process",
|
|
48
|
+
"unleash your potential",
|
|
49
|
+
"unlock your potential",
|
|
50
|
+
"where you are meant to be",
|
|
51
|
+
"you are capable of more than you think",
|
|
52
|
+
"you are enough",
|
|
53
|
+
"you are exactly where you need to be",
|
|
54
|
+
"you can't pour from an empty cup",
|
|
55
|
+
"you'll get through this",
|
|
56
|
+
"your authentic self",
|
|
57
|
+
"your best self",
|
|
58
|
+
"your best version",
|
|
59
|
+
"your higher self",
|
|
60
|
+
"your personal journey",
|
|
61
|
+
"your wellness journey"
|
|
62
|
+
]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[
|
|
2
|
+
"as the world continues to evolve",
|
|
3
|
+
"as we look to the future",
|
|
4
|
+
"as we navigate",
|
|
5
|
+
"as we've explored",
|
|
6
|
+
"has a profound impact",
|
|
7
|
+
"in an era characterized by",
|
|
8
|
+
"in an ever-evolving landscape",
|
|
9
|
+
"in the digital age",
|
|
10
|
+
"in this comprehensive guide",
|
|
11
|
+
"in today's competitive landscape",
|
|
12
|
+
"in today's digital age",
|
|
13
|
+
"in today's fast-paced",
|
|
14
|
+
"in today's rapidly evolving landscape",
|
|
15
|
+
"it is no secret that",
|
|
16
|
+
"it is vital for",
|
|
17
|
+
"it's no secret that",
|
|
18
|
+
"join us as we",
|
|
19
|
+
"join us as we explore",
|
|
20
|
+
"let's deep dive",
|
|
21
|
+
"let's delve into",
|
|
22
|
+
"more than ever before",
|
|
23
|
+
"navigating the complexities of",
|
|
24
|
+
"navigating the world of",
|
|
25
|
+
"take a deep dive into",
|
|
26
|
+
"the ultimate guide to",
|
|
27
|
+
"this guide will cover",
|
|
28
|
+
"this has never been more important",
|
|
29
|
+
"we hope this article",
|
|
30
|
+
"we hope this guide",
|
|
31
|
+
"welcome to this article",
|
|
32
|
+
"welcome to this guide",
|
|
33
|
+
"whether you're a beginner or"
|
|
34
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[
|
|
2
|
+
"a growing body of evidence suggests",
|
|
3
|
+
"a growing body of research suggests",
|
|
4
|
+
"according to health professionals",
|
|
5
|
+
"according to medical experts",
|
|
6
|
+
"analysts suggest",
|
|
7
|
+
"countless studies have shown",
|
|
8
|
+
"doctors say",
|
|
9
|
+
"economists say",
|
|
10
|
+
"health experts say",
|
|
11
|
+
"industry experts say",
|
|
12
|
+
"industry insiders say",
|
|
13
|
+
"industry leaders agree",
|
|
14
|
+
"it has been demonstrated that",
|
|
15
|
+
"it is commonly understood that",
|
|
16
|
+
"it is generally understood that",
|
|
17
|
+
"it is universally accepted that",
|
|
18
|
+
"leading experts say",
|
|
19
|
+
"many experts agree",
|
|
20
|
+
"many experts believe",
|
|
21
|
+
"most experts agree",
|
|
22
|
+
"multiple studies have found",
|
|
23
|
+
"new research suggests",
|
|
24
|
+
"new studies show",
|
|
25
|
+
"numerous studies have shown",
|
|
26
|
+
"numerous studies suggest",
|
|
27
|
+
"nutritionists say",
|
|
28
|
+
"observers have suggested",
|
|
29
|
+
"psychologists say",
|
|
30
|
+
"recent research has found",
|
|
31
|
+
"science confirms",
|
|
32
|
+
"science tells us",
|
|
33
|
+
"some researchers argue",
|
|
34
|
+
"some scientists believe",
|
|
35
|
+
"some would argue",
|
|
36
|
+
"the scientific community agrees"
|
|
37
|
+
]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import genreCliches from "./data/genre-cliches.json" with { type: "json" };
|
|
2
|
+
import { findUnquotedPhraseMatches } from "../../shared/matchers/phrases.js";
|
|
3
|
+
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
4
|
+
const rule = oneToOneRule({
|
|
5
|
+
detect: (unit) => findUnquotedPhraseMatches(unit.text, genreCliches).map((match) => ({
|
|
6
|
+
evidence: match.text,
|
|
7
|
+
label: match.text,
|
|
8
|
+
range: { start: match.start, end: match.end }
|
|
9
|
+
})),
|
|
10
|
+
family: "phrases",
|
|
11
|
+
formatMessage: (report) => `Genre cliche found: "${report.evidence}". Replace it with a concrete, specific detail.`,
|
|
12
|
+
ignoredAncestorTypes: ["Link", "LinkReference"],
|
|
13
|
+
ruleId: "phrases:genre-cliches",
|
|
14
|
+
unitKind: "str"
|
|
15
|
+
});
|
|
16
|
+
export default rule;
|
|
17
|
+
//# sourceMappingURL=genre-cliches.js.map
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
import { findPhraseMatches } from "../../shared/matchers/phrases.js";
|
|
2
2
|
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
"having worked with"
|
|
7
|
-
];
|
|
3
|
+
// "in my experience" was removed: it is a normal epistemic qualifier (common in Q&A
|
|
4
|
+
// and essays), not credentialing - it was a mis-fire, not a humble-brag.
|
|
5
|
+
const HUMBLE_BRAGGER_PHRASES = ["as someone who has", "having worked with"];
|
|
8
6
|
const rule = oneToOneRule({
|
|
9
7
|
detect: (unit) => findPhraseMatches(unit.text, HUMBLE_BRAGGER_PHRASES).map((match) => ({
|
|
10
8
|
evidence: match.text,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import selfHelpCliches from "./data/self-help-cliches.json" with { type: "json" };
|
|
2
|
+
import { findUnquotedPhraseMatches } from "../../shared/matchers/phrases.js";
|
|
3
|
+
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
4
|
+
const rule = oneToOneRule({
|
|
5
|
+
detect: (unit) => findUnquotedPhraseMatches(unit.text, selfHelpCliches).map((match) => ({
|
|
6
|
+
evidence: match.text,
|
|
7
|
+
label: match.text,
|
|
8
|
+
range: { start: match.start, end: match.end }
|
|
9
|
+
})),
|
|
10
|
+
family: "phrases",
|
|
11
|
+
formatMessage: (report) => `Self-help cliche found: "${report.evidence}". Replace it with a specific claim.`,
|
|
12
|
+
ignoredAncestorTypes: ["Link", "LinkReference"],
|
|
13
|
+
ruleId: "phrases:self-help-cliches",
|
|
14
|
+
unitKind: "str"
|
|
15
|
+
});
|
|
16
|
+
export default rule;
|
|
17
|
+
//# sourceMappingURL=self-help-cliches.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import seoFiller from "./data/seo-filler.json" with { type: "json" };
|
|
2
|
+
import { findUnquotedPhraseMatches } from "../../shared/matchers/phrases.js";
|
|
3
|
+
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
4
|
+
const rule = oneToOneRule({
|
|
5
|
+
detect: (unit) => findUnquotedPhraseMatches(unit.text, seoFiller).map((match) => ({
|
|
6
|
+
evidence: match.text,
|
|
7
|
+
label: match.text,
|
|
8
|
+
range: { start: match.start, end: match.end }
|
|
9
|
+
})),
|
|
10
|
+
family: "phrases",
|
|
11
|
+
formatMessage: (report) => `SEO filler found: "${report.evidence}". Cut the filler and lead with the specific point.`,
|
|
12
|
+
ignoredAncestorTypes: ["Link", "LinkReference"],
|
|
13
|
+
ruleId: "phrases:seo-filler",
|
|
14
|
+
unitKind: "str"
|
|
15
|
+
});
|
|
16
|
+
export default rule;
|
|
17
|
+
//# sourceMappingURL=seo-filler.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import weaselAttribution from "./data/weasel-attribution.json" with { type: "json" };
|
|
2
|
+
import { findUnquotedPhraseMatches } from "../../shared/matchers/phrases.js";
|
|
3
|
+
import { oneToOneRule } from "../private/textlint-rule-builders.js";
|
|
4
|
+
// Vague attribution: "studies show", "experts agree", "a growing body of evidence
|
|
5
|
+
// suggests" with no named source. Slop regardless of author; if the source is real,
|
|
6
|
+
// name it. The phrase list deliberately excludes attributions that are commonly used
|
|
7
|
+
// with a real citation alongside (those were screened out for false positives).
|
|
8
|
+
const rule = oneToOneRule({
|
|
9
|
+
detect: (unit) => findUnquotedPhraseMatches(unit.text, weaselAttribution).map((match) => ({
|
|
10
|
+
evidence: match.text,
|
|
11
|
+
label: match.text,
|
|
12
|
+
range: { start: match.start, end: match.end }
|
|
13
|
+
})),
|
|
14
|
+
family: "phrases",
|
|
15
|
+
formatMessage: (report) => `Vague attribution found: "${report.evidence}". Name the specific source or cut the claim.`,
|
|
16
|
+
ignoredAncestorTypes: ["Link", "LinkReference"],
|
|
17
|
+
ruleId: "phrases:weasel-attribution",
|
|
18
|
+
unitKind: "str"
|
|
19
|
+
});
|
|
20
|
+
export default rule;
|
|
21
|
+
//# sourceMappingURL=weasel-attribution.js.map
|
|
@@ -8,10 +8,15 @@ 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";
|
|
10
10
|
import { wordTokens } from "../../../../shared/text/tokens.js";
|
|
11
|
+
// "not only X but Y" is an additive correlative (it affirms both X and Y), not the
|
|
12
|
+
// replacement reframe this rule targets ("it is not X, it is Y"). It was a dominant
|
|
13
|
+
// false positive ("not only makes rounds harder but can trigger a down round"), so the
|
|
14
|
+
// "only" follower is excluded here.
|
|
11
15
|
const INLINE_NON_CONTRAST_NEGATION_FOLLOWERS = new Set([
|
|
12
16
|
"all",
|
|
13
17
|
"any",
|
|
14
18
|
"every",
|
|
19
|
+
"only",
|
|
15
20
|
"too"
|
|
16
21
|
]);
|
|
17
22
|
function inlineNegationContrast(sentence) {
|
|
@@ -82,7 +82,13 @@ function matchSoftening(sentence) {
|
|
|
82
82
|
const qualifier = containsAny(normalized, QUALIFIER_PATTERNS);
|
|
83
83
|
const variability = containsAny(normalized, VARIABILITY_PATTERNS);
|
|
84
84
|
const reporting = containsAny(normalized, REPORTING_PATTERNS);
|
|
85
|
-
|
|
85
|
+
// "in some cases"/"in some people" already count as a variability hedge; strip the
|
|
86
|
+
// matched phrase so its embedded "some cases"/"some people" is not double-counted as a
|
|
87
|
+
// separate quantifier signal (that self-overlap was the dominant false positive).
|
|
88
|
+
const quantifierText = variability === undefined
|
|
89
|
+
? normalized
|
|
90
|
+
: normalized.split(variability).join(" ");
|
|
91
|
+
const quantifier = findQuantifierPair(quantifierText);
|
|
86
92
|
const signalCount = Number(modal !== undefined) +
|
|
87
93
|
Number(qualifier !== undefined) +
|
|
88
94
|
Number(variability !== undefined) +
|
|
@@ -9,6 +9,11 @@ const PREFIXES = [
|
|
|
9
9
|
"after all, ",
|
|
10
10
|
"in the end, "
|
|
11
11
|
];
|
|
12
|
+
// Real universalizing claims: a broad human-group subject + a desire/certainty verb
|
|
13
|
+
// ("most people want", "everyone knows", "for many parents ... hope"). The previous
|
|
14
|
+
// vague-quantifier + abstract-noun arm ("many reasons", "some things", "several
|
|
15
|
+
// challenges") was removed: those are ordinary counts, not universalizing claims, and
|
|
16
|
+
// were the dominant false positive.
|
|
12
17
|
const SUBJECT_PATTERNS = [
|
|
13
18
|
["everyone"],
|
|
14
19
|
["everybody"],
|
|
@@ -82,54 +87,6 @@ const GROUP_BEHAVIOR_GERUNDS = [
|
|
|
82
87
|
"wondering"
|
|
83
88
|
];
|
|
84
89
|
const BROAD_GROUP_LEADS = ["many", "most"];
|
|
85
|
-
const VAGUE_QUANTIFIERS = ["a lot of", "many", "several", "some", "various"];
|
|
86
|
-
const ABSTRACT_NOUNS = [
|
|
87
|
-
"areas",
|
|
88
|
-
"aspects",
|
|
89
|
-
"benefits",
|
|
90
|
-
"challenges",
|
|
91
|
-
"changes",
|
|
92
|
-
"concerns",
|
|
93
|
-
"factors",
|
|
94
|
-
"impacts",
|
|
95
|
-
"issues",
|
|
96
|
-
"outcomes",
|
|
97
|
-
"reasons",
|
|
98
|
-
"results",
|
|
99
|
-
"stakeholders",
|
|
100
|
-
"themes",
|
|
101
|
-
"things"
|
|
102
|
-
];
|
|
103
|
-
const ABSTRACT_MODIFIERS = [
|
|
104
|
-
"important",
|
|
105
|
-
"key",
|
|
106
|
-
"major",
|
|
107
|
-
"meaningful",
|
|
108
|
-
"notable",
|
|
109
|
-
"significant",
|
|
110
|
-
"various"
|
|
111
|
-
];
|
|
112
|
-
const EVIDENCE_MARKERS = [
|
|
113
|
-
"for example",
|
|
114
|
-
"including",
|
|
115
|
-
"such as",
|
|
116
|
-
"because",
|
|
117
|
-
"from ",
|
|
118
|
-
"after ",
|
|
119
|
-
"before ",
|
|
120
|
-
"when "
|
|
121
|
-
];
|
|
122
|
-
function hasDigit(text) {
|
|
123
|
-
for (const character of text) {
|
|
124
|
-
if (character >= "0" && character <= "9") {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
function hasEvidenceMarker(text) {
|
|
131
|
-
return (hasDigit(text) || EVIDENCE_MARKERS.some((marker) => text.includes(marker)));
|
|
132
|
-
}
|
|
133
90
|
function matchGroupBehavior(words) {
|
|
134
91
|
const [first, subject, third, gerund] = words;
|
|
135
92
|
if (first !== undefined &&
|
|
@@ -161,40 +118,6 @@ function matchUniversalizing(sentence) {
|
|
|
161
118
|
return `${subject.join(" ")} ${verb}`;
|
|
162
119
|
}
|
|
163
120
|
}
|
|
164
|
-
const vagueQuantifier = matchVagueQuantifierAbstractNoun(words, cleaned);
|
|
165
|
-
if (vagueQuantifier !== undefined) {
|
|
166
|
-
return vagueQuantifier;
|
|
167
|
-
}
|
|
168
|
-
return undefined;
|
|
169
|
-
}
|
|
170
|
-
function matchVagueQuantifierAbstractNoun(words, text) {
|
|
171
|
-
if (hasEvidenceMarker(text)) {
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
for (let index = 0; index < words.length; index += 1) {
|
|
175
|
-
const single = words[index];
|
|
176
|
-
const twoWord = `${words[index] ?? ""} ${words[index + 1] ?? ""}`;
|
|
177
|
-
const quantifier = VAGUE_QUANTIFIERS.includes(twoWord)
|
|
178
|
-
? twoWord
|
|
179
|
-
: VAGUE_QUANTIFIERS.find((candidate) => candidate === single);
|
|
180
|
-
if (quantifier === undefined) {
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
const nounStart = quantifier.includes(" ") ? index + 2 : index + 1;
|
|
184
|
-
const first = words[nounStart];
|
|
185
|
-
const second = words[nounStart + 1];
|
|
186
|
-
const noun = first !== undefined && ABSTRACT_NOUNS.includes(first)
|
|
187
|
-
? first
|
|
188
|
-
: second !== undefined &&
|
|
189
|
-
first !== undefined &&
|
|
190
|
-
ABSTRACT_MODIFIERS.includes(first) &&
|
|
191
|
-
ABSTRACT_NOUNS.includes(second)
|
|
192
|
-
? second
|
|
193
|
-
: undefined;
|
|
194
|
-
if (noun !== undefined) {
|
|
195
|
-
return `${quantifier} ${noun}`;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
121
|
return undefined;
|
|
199
122
|
}
|
|
200
123
|
const rule = oneToOneRule({
|
|
@@ -49,7 +49,30 @@ const FILLER_OPENERS = [
|
|
|
49
49
|
"here's the kicker",
|
|
50
50
|
"honestly",
|
|
51
51
|
"frankly",
|
|
52
|
-
"candidly"
|
|
52
|
+
"candidly",
|
|
53
|
+
"truth be told",
|
|
54
|
+
"if i'm being honest",
|
|
55
|
+
"if i'm being perfectly honest",
|
|
56
|
+
"let me level with you",
|
|
57
|
+
"i'll be straight with you",
|
|
58
|
+
"i'll be frank",
|
|
59
|
+
"no sugarcoating",
|
|
60
|
+
"i won't sugarcoat it",
|
|
61
|
+
"not to put too fine a point on it",
|
|
62
|
+
"let's be real",
|
|
63
|
+
"here's the deal",
|
|
64
|
+
"here's what's really going on",
|
|
65
|
+
"the fact of the matter is",
|
|
66
|
+
"the bottom line is",
|
|
67
|
+
"here's the truth",
|
|
68
|
+
"here's where it gets interesting",
|
|
69
|
+
"in the grand scheme of things",
|
|
70
|
+
"it is worth considering that",
|
|
71
|
+
"it's fair to say",
|
|
72
|
+
"let's be honest",
|
|
73
|
+
"there are no easy answers",
|
|
74
|
+
"this is the important part",
|
|
75
|
+
"we are at an inflection point"
|
|
53
76
|
];
|
|
54
77
|
function matchEnumerationPreface(words) {
|
|
55
78
|
if (words.some((word) => VAGUE_INTROS.includes(word)) &&
|
|
@@ -73,11 +96,16 @@ function matchStarterFrame(words) {
|
|
|
73
96
|
function matchBoilerplateFraming(sentence) {
|
|
74
97
|
const stripped = cleanSentence(sentence, PREFIXES);
|
|
75
98
|
const words = tokens(stripped);
|
|
99
|
+
const lowered = stripped
|
|
100
|
+
.toLocaleLowerCase("en")
|
|
101
|
+
.replaceAll(String.fromCharCode(0x2019), "'");
|
|
76
102
|
const matches = [];
|
|
77
103
|
if (tokensContainInOrder(words, [["following"], PREVIEW_OBJECTS, PREVIEW_VERBS])) {
|
|
78
104
|
matches.push("following + explore");
|
|
79
105
|
}
|
|
80
|
-
|
|
106
|
+
// Opener only: "When it comes to X, ..." is a weak filler lead-in. Mid-sentence
|
|
107
|
+
// ("... tells us little when it comes to Y") is ordinary usage, not boilerplate.
|
|
108
|
+
if (lowered.startsWith("when it comes to")) {
|
|
81
109
|
matches.push("when it comes to");
|
|
82
110
|
}
|
|
83
111
|
if (tokensContainInOrder(words, [
|
|
@@ -96,9 +124,6 @@ function matchBoilerplateFraming(sentence) {
|
|
|
96
124
|
if (starter !== undefined) {
|
|
97
125
|
matches.push(starter);
|
|
98
126
|
}
|
|
99
|
-
const lowered = stripped
|
|
100
|
-
.toLocaleLowerCase("en")
|
|
101
|
-
.replaceAll(String.fromCharCode(0x2019), "'");
|
|
102
127
|
const filler = FILLER_OPENERS.find((opener) => lowered.startsWith(opener));
|
|
103
128
|
if (filler !== undefined) {
|
|
104
129
|
matches.push(filler);
|
|
@@ -3,13 +3,9 @@ import { cleanSentence, containsAny, tokens } from "../../../shared/matchers/pro
|
|
|
3
3
|
import { oneToOneRule } from "../../private/textlint-rule-builders.js";
|
|
4
4
|
import { matchDiscourseEvaluationFrame } from "./private/discourse-evaluation.js";
|
|
5
5
|
const PREFIXES = ["however, ", "but ", "and ", "so "];
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"it's important to remember",
|
|
10
|
-
"it is important to remember"
|
|
11
|
-
];
|
|
12
|
-
const TRANSITION_PATTERNS = ["that being said", "as such"];
|
|
6
|
+
// "as such" was removed: it is a normal anaphoric connective ("a registered adviser; as
|
|
7
|
+
// such, it must...") and was the dominant false positive, not a signposting frame.
|
|
8
|
+
const TRANSITION_PATTERNS = ["that being said"];
|
|
13
9
|
const CONSULTATION_PATTERNS = [
|
|
14
10
|
"it's always best to consult",
|
|
15
11
|
"it is always best to consult",
|
|
@@ -284,7 +280,6 @@ function matchSignposting(sentence) {
|
|
|
284
280
|
return { kind: "formulaic-content-setup", signal: formulaicSetup };
|
|
285
281
|
}
|
|
286
282
|
const checks = [
|
|
287
|
-
["important-to", IMPORTANT_TO_PATTERNS],
|
|
288
283
|
["transition", TRANSITION_PATTERNS],
|
|
289
284
|
["consultation-signpost", CONSULTATION_PATTERNS],
|
|
290
285
|
["note-signpost", NOTE_PATTERNS],
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { splitSentences } from "../../../shared/text/sentences.js";
|
|
2
|
+
import { wordTokens } from "../../../shared/text/tokens.js";
|
|
3
|
+
import { isWhitespace } from "../../../shared/text/whitespace.js";
|
|
4
|
+
import { oneToOneRule } from "../../private/textlint-rule-builders.js";
|
|
5
|
+
// Document-level significance signposting. A single "Notably," or "It's worth noting
|
|
6
|
+
// that" is legitimate (technical and academic writing use them), so this is NOT a
|
|
7
|
+
// per-instance flag. The slop tic is sprinkling several across a short piece: AI prose
|
|
8
|
+
// does this constantly, technical prose almost never (0% of 600 PEP/MDN/RFC docs reach
|
|
9
|
+
// the threshold; ~90% of synthetic marketing slop does). Fires once per document when
|
|
10
|
+
// there are enough sentence-initial significance markers AND they are dense enough.
|
|
11
|
+
const MARKERS = [
|
|
12
|
+
["notably"],
|
|
13
|
+
["importantly"],
|
|
14
|
+
["crucially"],
|
|
15
|
+
["significantly"],
|
|
16
|
+
["interestingly"],
|
|
17
|
+
["remarkably"],
|
|
18
|
+
["curiously"],
|
|
19
|
+
["tellingly"],
|
|
20
|
+
["strikingly"],
|
|
21
|
+
["worth", "noting"],
|
|
22
|
+
["it's", "worth", "noting"],
|
|
23
|
+
["it", "is", "worth", "noting"],
|
|
24
|
+
["it's", "important", "to", "note"],
|
|
25
|
+
["it", "is", "important", "to", "note"],
|
|
26
|
+
["it's", "important", "to", "remember"],
|
|
27
|
+
["it", "is", "important", "to", "remember"],
|
|
28
|
+
["it", "bears", "mentioning"]
|
|
29
|
+
];
|
|
30
|
+
const MINIMUM_COUNT = 3;
|
|
31
|
+
const MINIMUM_RATE_PER_1000 = 3;
|
|
32
|
+
function markerLength(tokens) {
|
|
33
|
+
for (const marker of MARKERS) {
|
|
34
|
+
if (marker.every((word, index) => tokens[index]?.normalized === word)) {
|
|
35
|
+
return marker.length;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
function countWords(text) {
|
|
41
|
+
let count = 0;
|
|
42
|
+
let inWord = false;
|
|
43
|
+
for (const character of text) {
|
|
44
|
+
if (isWhitespace(character)) {
|
|
45
|
+
inWord = false;
|
|
46
|
+
}
|
|
47
|
+
else if (!inWord) {
|
|
48
|
+
count += 1;
|
|
49
|
+
inWord = true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return count;
|
|
53
|
+
}
|
|
54
|
+
const rule = oneToOneRule({
|
|
55
|
+
detect: (unit) => {
|
|
56
|
+
const hits = [];
|
|
57
|
+
for (const sentence of splitSentences(unit.text)) {
|
|
58
|
+
const tokens = wordTokens(sentence.text);
|
|
59
|
+
const length = markerLength(tokens);
|
|
60
|
+
if (length === 0) {
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const first = tokens[0];
|
|
64
|
+
const last = tokens[length - 1];
|
|
65
|
+
if (first === undefined || last === undefined) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
hits.push({
|
|
69
|
+
end: sentence.start + last.end,
|
|
70
|
+
start: sentence.start + first.start
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const words = countWords(unit.text);
|
|
74
|
+
const rate = words === 0 ? 0 : (hits.length * 1000) / words;
|
|
75
|
+
const firstHit = hits[0];
|
|
76
|
+
if (hits.length < MINIMUM_COUNT ||
|
|
77
|
+
rate < MINIMUM_RATE_PER_1000 ||
|
|
78
|
+
firstHit === undefined) {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
{
|
|
83
|
+
evidence: `${hits.length} significance signposts in ${words} words`,
|
|
84
|
+
label: "significance-signposting",
|
|
85
|
+
range: { end: firstHit.end, start: firstHit.start }
|
|
86
|
+
}
|
|
87
|
+
];
|
|
88
|
+
},
|
|
89
|
+
family: "syntactic-patterns",
|
|
90
|
+
formatMessage: (report) => `Significance signposting density: ${report.evidence}. Cut the signposting and lead with the point.`,
|
|
91
|
+
ruleId: "syntactic-patterns:significance-density",
|
|
92
|
+
unitKind: "document"
|
|
93
|
+
});
|
|
94
|
+
export default rule;
|
|
95
|
+
//# sourceMappingURL=significance-density.js.map
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
"let's break this down",
|
|
7
7
|
"break this down step by step",
|
|
8
8
|
"break it down step by step",
|
|
9
|
-
"certainly",
|
|
10
9
|
"feel free to ask",
|
|
11
10
|
"great question",
|
|
12
11
|
"here are a few options",
|
|
@@ -19,12 +18,7 @@
|
|
|
19
18
|
"hope this message finds you well",
|
|
20
19
|
"this is not an exhaustive list"
|
|
21
20
|
],
|
|
22
|
-
"paragraphStartsWith": [
|
|
23
|
-
"it is important to note",
|
|
24
|
-
"it's important to note",
|
|
25
|
-
"it is worth noting",
|
|
26
|
-
"it's worth noting"
|
|
27
|
-
],
|
|
21
|
+
"paragraphStartsWith": [],
|
|
28
22
|
"contains": [
|
|
29
23
|
"want me to delve deeper",
|
|
30
24
|
"would you like me to delve",
|
|
@@ -156,6 +156,33 @@ const COMMON_OPENERS = new Set([
|
|
|
156
156
|
"must",
|
|
157
157
|
"shall"
|
|
158
158
|
]);
|
|
159
|
+
// Enumerators and document-structure openers: list markers ("2.", "b)") and
|
|
160
|
+
// reference words that head TOC entries, headings, and procedural steps. Three
|
|
161
|
+
// sentences opening this way is structure, not repetitive prose, and was a
|
|
162
|
+
// dominant source of false positives in the human-corpus audit.
|
|
163
|
+
const STRUCTURAL_OPENERS = new Set([
|
|
164
|
+
"chapter",
|
|
165
|
+
"section",
|
|
166
|
+
"part",
|
|
167
|
+
"appendix",
|
|
168
|
+
"figure",
|
|
169
|
+
"table",
|
|
170
|
+
"page",
|
|
171
|
+
"step",
|
|
172
|
+
"verse",
|
|
173
|
+
"item"
|
|
174
|
+
]);
|
|
175
|
+
function isStructuralOpener(opener) {
|
|
176
|
+
if (STRUCTURAL_OPENERS.has(opener) || opener.length === 1) {
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
for (const character of opener) {
|
|
180
|
+
if (character < "0" || character > "9") {
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
159
186
|
function isWordCharacter(character) {
|
|
160
187
|
return ((character >= "a" && character <= "z") ||
|
|
161
188
|
(character >= "0" && character <= "9") ||
|
|
@@ -213,6 +240,7 @@ function findTripleRepeats(text) {
|
|
|
213
240
|
if (firstOpener === undefined ||
|
|
214
241
|
firstOpener === "" ||
|
|
215
242
|
COMMON_OPENERS.has(firstOpener) ||
|
|
243
|
+
isStructuralOpener(firstOpener) ||
|
|
216
244
|
firstOpener !== secondOpener ||
|
|
217
245
|
secondOpener !== thirdOpener) {
|
|
218
246
|
continue;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { defineTextlintRule } from "../../adapters/textlint/rule.js";
|
|
2
|
+
import { documentUnit } from "../../adapters/textlint/units.js";
|
|
3
|
+
import { ERROR_SEVERITY } from "../../reporting/density.js";
|
|
4
|
+
import { wordTokens } from "../../shared/text/tokens.js";
|
|
5
|
+
// The rule only detects occurrences of "actually" - one detection per use, no counting,
|
|
6
|
+
// rate, or severity. The density-rate report policy below lets the reporter judge the
|
|
7
|
+
// occurrences against the document word count and decide none / warning / error.
|
|
8
|
+
// "actually" is a normal contrastive adverb at low frequency; AI overuse shows up as
|
|
9
|
+
// density, which is the reporter's concern, not the detector's. See src/reporting/density.ts.
|
|
10
|
+
const TARGET = "actually";
|
|
11
|
+
const RULE_ID = "words:actually-overuse";
|
|
12
|
+
const rule = defineTextlintRule({
|
|
13
|
+
detector: {
|
|
14
|
+
detect: ({ units }) => units.flatMap((unit) => wordTokens(unit.text)
|
|
15
|
+
.filter((token) => token.normalized === TARGET)
|
|
16
|
+
.map((token) => ({
|
|
17
|
+
evidence: TARGET,
|
|
18
|
+
label: TARGET,
|
|
19
|
+
range: { end: token.end, start: token.start },
|
|
20
|
+
ruleId: RULE_ID,
|
|
21
|
+
unitId: unit.id
|
|
22
|
+
}))),
|
|
23
|
+
family: "words",
|
|
24
|
+
id: RULE_ID
|
|
25
|
+
},
|
|
26
|
+
formatMessage: (report) => {
|
|
27
|
+
const count = report.metric?.["count"] ?? report.detections.length;
|
|
28
|
+
const perUnit = report.metric?.["perUnit"] ?? 0;
|
|
29
|
+
const threshold = report.severity === ERROR_SEVERITY
|
|
30
|
+
? "above the 2-per-1,000-word error threshold"
|
|
31
|
+
: "above the 1-per-1,000-word warning threshold";
|
|
32
|
+
return `"actually" used ${count} times (${perUnit} per 1,000 words), ${threshold}. Cut the filler uses; keep at most about one per 1,000 words.`;
|
|
33
|
+
},
|
|
34
|
+
reportPolicy: {
|
|
35
|
+
errorPerUnit: 2,
|
|
36
|
+
kind: "density-rate",
|
|
37
|
+
minimumOccurrences: 2,
|
|
38
|
+
scope: "document",
|
|
39
|
+
warningPerUnit: 1,
|
|
40
|
+
wordsPerUnit: 1000
|
|
41
|
+
},
|
|
42
|
+
units: (document) => [documentUnit(document)]
|
|
43
|
+
});
|
|
44
|
+
export default rule;
|
|
45
|
+
//# sourceMappingURL=actually-overuse.js.map
|