slopless 0.2.26 → 0.2.28

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.
@@ -1,6 +1,6 @@
1
1
  import { normalizeForMatch } from "../../shared/text/normalize.js";
2
2
  import { allParagraphSentences, allParagraphs, sectionFirstSentences, sectionLastSentences } from "../../shared/text/sections.js";
3
- import { documentText } from "../../shared/text/document.js";
3
+ import { documentSourceText, documentText } from "../../shared/text/document.js";
4
4
  import { sourceText } from "../../shared/text/traverse.js";
5
5
  function isParentNode(node) {
6
6
  return "children" in node;
@@ -44,14 +44,18 @@ function sentenceUnit(id, item) {
44
44
  };
45
45
  }
46
46
  export function documentUnit(document) {
47
- const text = documentText(document);
47
+ const source = documentSourceText(document);
48
+ const { text } = source;
48
49
  return {
49
50
  id: "document:0",
50
51
  kind: "document",
51
52
  node: document,
52
53
  normalizedText: normalizeForMatch(text),
53
54
  range: { end: text.length, start: 0 },
54
- sourceRangeFor: (range) => range,
55
+ sourceRangeFor: (range) => ({
56
+ end: source.originalEndFor(range.end),
57
+ start: source.originalStartFor(range.start)
58
+ }),
55
59
  text
56
60
  };
57
61
  }
@@ -42,6 +42,7 @@ export const everything = {
42
42
  "perception-verb-density": true,
43
43
  "prohibited-phrases": true,
44
44
  "prohibited-words": true,
45
+ "quietly-overuse": true,
45
46
  "recommended-terms": true,
46
47
  redundancy: true,
47
48
  "repeated-predicate-end": true,
@@ -3,6 +3,7 @@ import hedgeStacking from "../rules/words/hedge-stacking.js";
3
3
  import llmVocabularyDensity from "../rules/words/llm-vocabulary-density.js";
4
4
  import llmVocabulary from "../rules/words/llm-vocabulary.js";
5
5
  import prohibitedWords from "../rules/words/prohibited-words.js";
6
+ import quietlyOveruse from "../rules/words/quietly-overuse.js";
6
7
  import simplicity from "../rules/words/simplicity.js";
7
8
  export const wordRules = {
8
9
  "actually-overuse": actuallyOveruse,
@@ -10,6 +11,7 @@ export const wordRules = {
10
11
  "llm-vocabulary-density": llmVocabularyDensity,
11
12
  "llm-vocabulary": llmVocabulary,
12
13
  "prohibited-words": prohibitedWords,
14
+ "quietly-overuse": quietlyOveruse,
13
15
  simplicity
14
16
  };
15
17
  //# sourceMappingURL=words.js.map
@@ -2,14 +2,36 @@
2
2
  "id": "something-shifted",
3
3
  "class": "vague-change",
4
4
  "purpose": "Catch vague change declarations that say something shifted, changed, moved, or crossed a line without naming the concrete change.",
5
- "matchMode": "contains",
5
+ "matchMode": "full",
6
6
  "maxTokens": 80,
7
7
  "templates": [
8
- "{genericSubject} {changeVerb}.",
9
- "{genericSubject} had {changeVerb}.",
10
- "{genericSubject} in {abstractContainer} had {changeVerb}.",
11
- "{concreteSubject} did not {changeVerb}, but {genericSubject} in it did.",
12
- "{genericSubject} {changeVerb} when {genericSubject} {abstractAction}."
8
+ {
9
+ "text": "{genericSubject} {changeVerb}.",
10
+ "matchMode": "contains"
11
+ },
12
+ {
13
+ "text": "{genericSubject} had {changeVerb}.",
14
+ "matchMode": "contains"
15
+ },
16
+ {
17
+ "text": "{genericSubject} in {abstractContainer} had {changeVerb}.",
18
+ "matchMode": "contains"
19
+ },
20
+ {
21
+ "text": "{concreteSubject} did not {changeVerb}, but {genericSubject} in it did.",
22
+ "matchMode": "contains"
23
+ },
24
+ {
25
+ "text": "{genericSubject} {changeVerb} when {genericSubject} {abstractAction}.",
26
+ "matchMode": "contains"
27
+ },
28
+ "{diffuseSubject} {weakMannerAdverb} {finiteChange}.",
29
+ "{diffuseSubject} {progressiveAuxiliary} {weakMannerAdverb} {progressiveChange}.",
30
+ "{diffuseSubject} {perfectAuxiliary} {weakMannerAdverb} {pastChange}.",
31
+ "{diffuseSubject} {finiteChange} {weakMannerAdverb}.",
32
+ "{deicticSubject} {weakMannerAdverb} {emptyChangeObject}.",
33
+ "{shiftDeterminer} {shiftModifier} shift {emptyShiftPredicate}.",
34
+ "{deicticSubject} {signalVerb} {shiftDeterminer} {shiftModifier} shift."
13
35
  ],
14
36
  "slots": {
15
37
  "genericSubject": [
@@ -45,7 +67,103 @@
45
67
  "chose differently",
46
68
  "named the truth",
47
69
  "let go"
48
- ]
70
+ ],
71
+ "diffuseSubject": [
72
+ "the market",
73
+ "the industry",
74
+ "search",
75
+ "the web",
76
+ "the landscape",
77
+ "the platform",
78
+ "the system",
79
+ "the process",
80
+ "the conversation",
81
+ "the work",
82
+ "the strategy",
83
+ "the model",
84
+ "the product",
85
+ "the category",
86
+ "the space"
87
+ ],
88
+ "progressiveAuxiliary": ["is", "was", "has been", "had been"],
89
+ "perfectAuxiliary": ["has", "had"],
90
+ "weakMannerAdverb": [
91
+ "quietly",
92
+ "silently",
93
+ "subtly",
94
+ "gradually",
95
+ "steadily",
96
+ "almost invisibly",
97
+ "under the surface"
98
+ ],
99
+ "finiteChange": [
100
+ "changes",
101
+ "shifts",
102
+ "evolves",
103
+ "transforms",
104
+ "reshapes",
105
+ "redefines",
106
+ "moves",
107
+ "changed",
108
+ "shifted",
109
+ "evolved",
110
+ "transformed",
111
+ "reshaped",
112
+ "redefined",
113
+ "moved"
114
+ ],
115
+ "pastChange": [
116
+ "changed",
117
+ "shifted",
118
+ "evolved",
119
+ "transformed",
120
+ "reshaped",
121
+ "redefined",
122
+ "moved"
123
+ ],
124
+ "progressiveChange": [
125
+ "changing",
126
+ "shifting",
127
+ "evolving",
128
+ "transforming",
129
+ "reshaping",
130
+ "redefining",
131
+ "moving"
132
+ ],
133
+ "deicticSubject": ["this", "that", "it"],
134
+ "emptyChangeObject": [
135
+ "changes everything",
136
+ "changes the game",
137
+ "changes the equation",
138
+ "reshapes everything",
139
+ "redefines everything"
140
+ ],
141
+ "shiftDeterminer": ["a", "the", "this", "that"],
142
+ "shiftModifier": [
143
+ "quiet",
144
+ "subtle",
145
+ "broader",
146
+ "meaningful",
147
+ "important",
148
+ "major",
149
+ "fundamental",
150
+ "seismic",
151
+ "structural",
152
+ "profound",
153
+ "significant",
154
+ "notable"
155
+ ],
156
+ "emptyShiftPredicate": [
157
+ "is underway",
158
+ "is happening",
159
+ "has begun",
160
+ "is already here",
161
+ "is easy to miss",
162
+ "is hard to ignore",
163
+ "matters",
164
+ "changes everything"
165
+ ],
166
+ "signalVerb": ["marks", "signals", "represents", "reflects"]
49
167
  },
50
168
  "rejectIf": [
51
169
  "sentence names what changed",
@@ -159,7 +159,7 @@ function matchWhatFrame(words) {
159
159
  function matchAbstractFrame(text) {
160
160
  const words = tokens(text);
161
161
  return (matchRelativeDiscourseFrame(text, words) ??
162
- matchExpandedDiscourseFrame(words) ??
162
+ matchExpandedDiscourseFrame(text, words) ??
163
163
  matchModifiedAbstractFrame(words) ??
164
164
  matchDiscourseEvaluationFrame(words) ??
165
165
  matchPointIsToFrame(words) ??
@@ -221,6 +221,7 @@ function matchSignposting(sentence) {
221
221
  const generatedFormula = matchGeneratedFormula(stripped);
222
222
  if (abstract !== undefined &&
223
223
  (abstract === "what-matters-is" ||
224
+ abstract.startsWith("deictic-evaluative-") ||
224
225
  abstract.startsWith("relative-") ||
225
226
  !concreteImplementation)) {
226
227
  return { kind: "abstract-evaluation-frame", signal: abstract };
@@ -5,6 +5,53 @@ const DISCOURSE_WORK_TAILS = [
5
5
  ["load", "bearing"]
6
6
  ];
7
7
  const DETERMINERS = new Set(["a", "an", "the", "this", "that"]);
8
+ const DEICTIC_OPENERS = new Set(["here", "this", "that"]);
9
+ const DEICTIC_CONTRACTIONS = new Map([
10
+ ["here's", "here"],
11
+ ["this's", "this"],
12
+ ["that's", "that"]
13
+ ]);
14
+ const DEICTIC_EVALUATIVE_ADJECTIVES = new Set([
15
+ "best",
16
+ "better",
17
+ "biggest",
18
+ "central",
19
+ "core",
20
+ "crucial",
21
+ "funny",
22
+ "good",
23
+ "great",
24
+ "hard",
25
+ "important",
26
+ "interesting",
27
+ "key",
28
+ "main",
29
+ "neat",
30
+ "nice",
31
+ "odd",
32
+ "obvious",
33
+ "remarkable",
34
+ "strange",
35
+ "surprising",
36
+ "tricky",
37
+ "useful",
38
+ "weird",
39
+ "wild"
40
+ ]);
41
+ const DEICTIC_DISCOURSE_NOUNS = new Set([
42
+ "angle",
43
+ "aspect",
44
+ "bit",
45
+ "catch",
46
+ "detail",
47
+ "element",
48
+ "idea",
49
+ "part",
50
+ "piece",
51
+ "point",
52
+ "thing",
53
+ "twist"
54
+ ]);
8
55
  const FRAME_ADJECTIVES = new Set([
9
56
  "basic",
10
57
  "best",
@@ -202,13 +249,45 @@ function matchEvaluativeFrame(words) {
202
249
  ? `the-${adjective}-${noun}-${verb}`
203
250
  : undefined;
204
251
  }
252
+ function matchDeicticEvaluativeFrame(text, words) {
253
+ const contractedOpener = DEICTIC_CONTRACTIONS.get(words[0] ?? "");
254
+ const opener = contractedOpener ?? words[0];
255
+ const determinerIndex = contractedOpener === undefined ? 2 : 1;
256
+ if (opener === undefined ||
257
+ !DEICTIC_OPENERS.has(opener) ||
258
+ (contractedOpener === undefined &&
259
+ !["is", "was"].includes(words[1] ?? "")) ||
260
+ !["a", "an", "the"].includes(words[determinerIndex] ?? "")) {
261
+ return undefined;
262
+ }
263
+ const adjective = words[determinerIndex + 1];
264
+ const noun = words[determinerIndex + 2];
265
+ const frame = adjective !== undefined && noun !== undefined
266
+ ? words.slice(0, determinerIndex + 3).join(" ")
267
+ : undefined;
268
+ const boundary = frame === undefined ? undefined : text.at(frame.length);
269
+ return adjective !== undefined &&
270
+ noun !== undefined &&
271
+ (boundary === undefined ||
272
+ boundary === "." ||
273
+ boundary === "!" ||
274
+ boundary === "?" ||
275
+ boundary === ":" ||
276
+ boundary === ";" ||
277
+ boundary === ",") &&
278
+ DEICTIC_EVALUATIVE_ADJECTIVES.has(adjective) &&
279
+ DEICTIC_DISCOURSE_NOUNS.has(noun)
280
+ ? `deictic-evaluative-${opener}-${adjective}-${noun}`
281
+ : undefined;
282
+ }
205
283
  export function isAbstractAuditFrame(words) {
206
284
  return words[0] === "the" && frameNounIndex(words) > 0
207
285
  ? words[frameNounIndex(words)] === "audit"
208
286
  : false;
209
287
  }
210
- export function matchExpandedDiscourseFrame(words) {
211
- return (matchWorthAttentionFrame(words) ??
288
+ export function matchExpandedDiscourseFrame(text, words) {
289
+ return (matchDeicticEvaluativeFrame(text, words) ??
290
+ matchWorthAttentionFrame(words) ??
212
291
  matchVagueFrameLocation(words) ??
213
292
  matchEvaluativeFrame(words));
214
293
  }
@@ -1,7 +1,5 @@
1
- import { defineTextlintRule } from "../../adapters/textlint/rule.js";
2
- import { documentUnit } from "../../adapters/textlint/units.js";
3
1
  import { ERROR_SEVERITY } from "../../reporting/density.js";
4
- import { wordTokens } from "../../shared/text/tokens.js";
2
+ import { defineExactTokenDensityRule } from "./private/token-density-rule.js";
5
3
  // The rule only detects occurrences of "actually" - one detection per use, no counting,
6
4
  // rate, or severity. The density-rate report policy below lets the reporter judge the
7
5
  // occurrences against the document word count and decide none / warning / error.
@@ -9,20 +7,8 @@ import { wordTokens } from "../../shared/text/tokens.js";
9
7
  // density, which is the reporter's concern, not the detector's. See src/reporting/density.ts.
10
8
  const TARGET = "actually";
11
9
  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
- },
10
+ const rule = defineExactTokenDensityRule({
11
+ errorPerUnit: 2,
26
12
  formatMessage: (report) => {
27
13
  const count = report.metric?.["count"] ?? report.detections.length;
28
14
  const perUnit = report.metric?.["perUnit"] ?? 0;
@@ -31,15 +17,11 @@ const rule = defineTextlintRule({
31
17
  : "above the 1-per-1,000-word warning threshold";
32
18
  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
19
  },
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)]
20
+ minimumOccurrences: 2,
21
+ ruleId: RULE_ID,
22
+ target: TARGET,
23
+ warningPerUnit: 1,
24
+ wordsPerUnit: 1000
43
25
  });
44
26
  export default rule;
45
27
  //# sourceMappingURL=actually-overuse.js.map
@@ -0,0 +1,31 @@
1
+ import { defineTextlintRule } from "../../../adapters/textlint/rule.js";
2
+ import { documentUnit } from "../../../adapters/textlint/units.js";
3
+ import { wordTokens } from "../../../shared/text/tokens.js";
4
+ export function defineExactTokenDensityRule(config) {
5
+ return defineTextlintRule({
6
+ detector: {
7
+ detect: ({ units }) => units.flatMap((unit) => wordTokens(unit.text)
8
+ .filter((token) => token.normalized === config.target)
9
+ .map((token) => ({
10
+ evidence: config.target,
11
+ label: config.target,
12
+ range: { end: token.end, start: token.start },
13
+ ruleId: config.ruleId,
14
+ unitId: unit.id
15
+ }))),
16
+ family: "words",
17
+ id: config.ruleId
18
+ },
19
+ formatMessage: config.formatMessage,
20
+ reportPolicy: {
21
+ errorPerUnit: config.errorPerUnit,
22
+ kind: "density-rate",
23
+ minimumOccurrences: config.minimumOccurrences,
24
+ scope: "document",
25
+ warningPerUnit: config.warningPerUnit,
26
+ wordsPerUnit: config.wordsPerUnit
27
+ },
28
+ units: (document) => [documentUnit(document)]
29
+ });
30
+ }
31
+ //# sourceMappingURL=token-density-rule.js.map
@@ -0,0 +1,22 @@
1
+ import { ERROR_SEVERITY } from "../../reporting/density.js";
2
+ import { defineExactTokenDensityRule } from "./private/token-density-rule.js";
3
+ const TARGET = "quietly";
4
+ const RULE_ID = "words:quietly-overuse";
5
+ const rule = defineExactTokenDensityRule({
6
+ errorPerUnit: 2,
7
+ formatMessage: (report) => {
8
+ const count = report.metric?.["count"] ?? report.detections.length;
9
+ const perUnit = report.metric?.["perUnit"] ?? 0;
10
+ const threshold = report.severity === ERROR_SEVERITY
11
+ ? "above the 2-per-1,000-word error threshold"
12
+ : "above the 1-per-1,000-word warning threshold";
13
+ return `"quietly" used ${count} times (${perUnit} per 1,000 words), ${threshold}. Cut the filler uses; keep at most about one per 1,000 words.`;
14
+ },
15
+ minimumOccurrences: 4,
16
+ ruleId: RULE_ID,
17
+ target: TARGET,
18
+ warningPerUnit: 1,
19
+ wordsPerUnit: 1000
20
+ });
21
+ export default rule;
22
+ //# sourceMappingURL=quietly-overuse.js.map
@@ -1,8 +1,43 @@
1
1
  import { allParagraphs } from "./sections.js";
2
+ function trimStartLength(text) {
3
+ return text.length - text.trimStart().length;
4
+ }
5
+ export function documentSourceText(document) {
6
+ const paragraphs = [];
7
+ const textParts = [];
8
+ let outputStart = 0;
9
+ for (const paragraph of allParagraphs(document)) {
10
+ const text = paragraph.text.trim();
11
+ if (text.length === 0) {
12
+ continue;
13
+ }
14
+ paragraphs.push({
15
+ outputEnd: outputStart + text.length,
16
+ outputStart,
17
+ paragraph,
18
+ trimStart: trimStartLength(paragraph.text)
19
+ });
20
+ textParts.push(text);
21
+ outputStart += text.length + 2;
22
+ }
23
+ const originalOffsetFor = (offset, isEnd) => {
24
+ const mapped = paragraphs.find((paragraph) => offset >= paragraph.outputStart && offset <= paragraph.outputEnd);
25
+ if (mapped === undefined) {
26
+ return offset;
27
+ }
28
+ const paragraphOffset = mapped.trimStart + offset - mapped.outputStart;
29
+ const localOffset = isEnd
30
+ ? mapped.paragraph.source.originalEndFor(paragraphOffset)
31
+ : mapped.paragraph.source.originalStartFor(paragraphOffset);
32
+ return mapped.paragraph.paragraph.range[0] + localOffset;
33
+ };
34
+ return {
35
+ originalEndFor: (end) => originalOffsetFor(end, true),
36
+ originalStartFor: (start) => originalOffsetFor(start, false),
37
+ text: textParts.join("\n\n")
38
+ };
39
+ }
2
40
  export function documentText(document) {
3
- return allParagraphs(document)
4
- .map((paragraph) => paragraph.text.trim())
5
- .filter((text) => text.length > 0)
6
- .join("\n\n");
41
+ return documentSourceText(document).text;
7
42
  }
8
43
  //# sourceMappingURL=document.js.map
@@ -1,26 +1,11 @@
1
1
  import { splitSentences } from "./sentences.js";
2
- import { sourceText } from "./traverse.js";
2
+ import { proseSourceText, sourceText } from "./traverse.js";
3
3
  function isParagraphNode(node) {
4
4
  return node.type === "Paragraph";
5
5
  }
6
6
  function isParentNode(node) {
7
7
  return "children" in node;
8
8
  }
9
- function hasStringValue(node) {
10
- return "value" in node && typeof node.value === "string";
11
- }
12
- function plainText(node) {
13
- if (hasStringValue(node)) {
14
- return node.value;
15
- }
16
- if (node.type === "Break") {
17
- return " ";
18
- }
19
- if (!isParentNode(node)) {
20
- return "";
21
- }
22
- return node.children.map((child) => plainText(child)).join("");
23
- }
24
9
  function collectParagraphs(node, paragraphs) {
25
10
  if (isParagraphNode(node)) {
26
11
  paragraphs.push(node);
@@ -79,10 +64,11 @@ export function allParagraphs(document) {
79
64
  const paragraphs = [];
80
65
  for (const section of documentSections(document)) {
81
66
  for (const paragraph of sectionParagraphs(section)) {
67
+ const source = proseSourceText(paragraph);
82
68
  paragraphs.push({
83
69
  paragraph,
84
- source: sourceText(paragraph),
85
- text: plainText(paragraph)
70
+ source,
71
+ text: source.text
86
72
  });
87
73
  }
88
74
  }
@@ -1,13 +1,58 @@
1
1
  import { StringSource } from "textlint-util-to-string";
2
- export function sourceText(node) {
2
+ function isParentNode(node) {
3
+ return "children" in node;
4
+ }
5
+ function isHtmlNode(node) {
6
+ return (node.type === "Html" && "value" in node && typeof node.value === "string");
7
+ }
8
+ function normalizeNode(node, includeImageAlt) {
9
+ if (node.type === "Break") {
10
+ return {
11
+ ...node,
12
+ type: "Str",
13
+ value: " "
14
+ };
15
+ }
16
+ if ((node.type === "Image" || node.type === "ImageReference") &&
17
+ !includeImageAlt) {
18
+ return {
19
+ ...node,
20
+ type: "Str",
21
+ value: ""
22
+ };
23
+ }
24
+ if (isHtmlNode(node) && !includeImageAlt) {
25
+ return {
26
+ ...node,
27
+ type: "Str",
28
+ value: node.value
29
+ };
30
+ }
31
+ if (!isParentNode(node)) {
32
+ return node;
33
+ }
34
+ return {
35
+ ...node,
36
+ // type-coverage:ignore-next-line
37
+ children: node.children.map((child) => normalizeNode(child, includeImageAlt))
38
+ };
39
+ }
40
+ function mappedSourceText(node, includeImageAlt) {
3
41
  // textlint-util-to-string has not updated its public type for textlint 15.7's
4
42
  // readonly children, but it only reads the node at runtime.
5
- const source = new StringSource(node // type-coverage:ignore-line
6
- );
43
+ const source = new StringSource(
44
+ // type-coverage:ignore-next-line
45
+ normalizeNode(node, includeImageAlt));
7
46
  return {
8
47
  originalEndFor: (end) => source.originalIndexFromIndex(end, true) ?? end,
9
48
  originalStartFor: (start) => source.originalIndexFromIndex(start) ?? start,
10
49
  text: source.toString()
11
50
  };
12
51
  }
52
+ export function proseSourceText(node) {
53
+ return mappedSourceText(node, false);
54
+ }
55
+ export function sourceText(node) {
56
+ return mappedSourceText(node, true);
57
+ }
13
58
  //# sourceMappingURL=traverse.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopless",
3
- "version": "0.2.26",
3
+ "version": "0.2.28",
4
4
  "description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
5
5
  "keywords": [
6
6
  "textlint",
@@ -111,7 +111,8 @@
111
111
  "./rules/words/llm-vocabulary-density": "./dist/rules/words/llm-vocabulary-density.js",
112
112
  "./rules/words/llm-vocabulary": "./dist/rules/words/llm-vocabulary.js",
113
113
  "./rules/words/simplicity": "./dist/rules/words/simplicity.js",
114
- "./rules/words/prohibited-words": "./dist/rules/words/prohibited-words.js"
114
+ "./rules/words/prohibited-words": "./dist/rules/words/prohibited-words.js",
115
+ "./rules/words/quietly-overuse": "./dist/rules/words/quietly-overuse.js"
115
116
  },
116
117
  "dependencies": {
117
118
  "@textlint/ast-node-types": "15.7.1",