slopless 0.2.14 → 0.2.15

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/README.md CHANGED
@@ -2,12 +2,48 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/slopless?label=npm)](https://www.npmjs.com/package/slopless)
4
4
  [![downloads](https://img.shields.io/npm/dm/slopless)](https://www.npmjs.com/package/slopless)
5
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/slopless?label=minzip)](https://bundlephobia.com/package/slopless)
6
+ [![install size](https://packagephobia.com/badge?p=slopless)](https://packagephobia.com/result?p=slopless)
5
7
  [![license](https://img.shields.io/npm/l/slopless)](LICENSE)
6
- [![ci](https://img.shields.io/github/actions/workflow/status/seochecks-ai%2Fslopless/ci.yml?branch=main&label=ci)](/actions/workflows/ci.yml)
7
8
  [![node](https://img.shields.io/badge/node-22%2B-brightgreen)](package.json)
8
9
 
10
+ [![ci](https://img.shields.io/github/actions/workflow/status/seochecks-ai%2Fslopless/ci.yml?branch=main&label=ci)](/actions/workflows/ci.yml)
11
+ [![scorecard](https://api.scorecard.dev/projects/github.com/seochecks-ai/slopless/badge)](https://scorecard.dev/viewer/?uri=github.com/seochecks-ai/slopless)
12
+ [![socket](https://socket.dev/api/badge/npm/package/slopless)](https://socket.dev/npm/package/slopless)
13
+ [![snyk advisor](https://snyk.io/advisor/npm-package/slopless/badge.svg)](https://snyk.io/advisor/npm-package/slopless)
14
+
9
15
  Catch AI and human slop in English Markdown without calling an LLM. Slopless ships 50+ deterministic textlint rules and a CLI that emits structured JSON findings.
10
16
 
17
+ ## What it catches
18
+
19
+ Given this paragraph (typical LLM prose):
20
+
21
+ ```markdown
22
+ In today's rapidly evolving landscape, it's important to note that AI tools
23
+ are revolutionizing how we work - truly transforming workflows in
24
+ unprecedented ways. By harnessing the power of large language models, teams
25
+ can delve into complex challenges, streamline operations, and unlock new
26
+ possibilities. But what does this really mean for you? In essence, the
27
+ future belongs to those who embrace these tools today. Not just to keep up,
28
+ not just to stay competitive, but to thrive.
29
+ ```
30
+
31
+ `npx slopless` returns nine findings across seven rules:
32
+
33
+ ```
34
+ [slopless/generic-signposting] it's important to note
35
+ [slopless/prohibited-phrases] it's important to note
36
+ [slopless/llm-vocabulary] landscape
37
+ [slopless/llm-vocabulary] delve
38
+ [slopless/prohibited-words] delve
39
+ [slopless/prohibited-words] unlock
40
+ [slopless/prohibited-phrases] the future belongs to
41
+ [slopless/negation-reframe] Not just X, not just Y, but Z
42
+ [slopless/flesch-kincaid] Flesch Reading Ease 56.05 (keep >= 61)
43
+ ```
44
+
45
+ Full JSON output is the default. See the [Rules](https://github.com/seochecks-ai/slopless/wiki/Rules) page for the complete inventory.
46
+
11
47
  ## Intended Usage Loop
12
48
 
13
49
  ```bash
@@ -91,8 +127,23 @@ Something shifted in the room.
91
127
  - [Behavior](https://github.com/seochecks-ai/slopless/wiki/Behavior) - CLI flags, exit codes, JSON output shape, direct textlint integration.
92
128
  - [Ignore rules](https://github.com/seochecks-ai/slopless/wiki/Ignore-Rules) - inline `textlint-disable` block syntax.
93
129
  - [Thanks](https://github.com/seochecks-ai/slopless/wiki/Thanks) - direct rule sources, dependencies, and acknowledgments.
130
+ - [Roadmap](ROADMAP.md) - near-term direction and links to active plans.
94
131
  - [Contributing](.github/CONTRIBUTING.md) - open a detailed issue first; PRs must pass the G3TS pre-commit gate.
95
132
 
133
+ ## Verify the package
134
+
135
+ Every release since 0.2.13 is published from GitHub Actions via OIDC and carries a [SLSA v1 provenance attestation](https://registry.npmjs.org/-/npm/v1/attestations/slopless@latest) recorded in the [Sigstore](https://www.sigstore.dev/) transparency log.
136
+
137
+ ```bash
138
+ npm audit signatures slopless
139
+ ```
140
+
141
+ This confirms the tarball you installed was built in the published source repository's Actions environment, from the commit the GitHub Release points at.
142
+
143
+ ## Star history
144
+
145
+ [![Star History Chart](https://api.star-history.com/svg?repos=seochecks-ai/slopless&type=Date)](https://star-history.com/#seochecks-ai/slopless&Date)
146
+
96
147
  ---
97
148
 
98
149
  Developed by [seochecks.ai](https://seochecks.ai) to keep content specific, useful, and recognizably human.
package/dist/cli.js CHANGED
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { readFileSync } from "node:fs";
2
+ import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { cp, rm, stat } from "node:fs/promises";
4
- import { dirname, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { dirname, join, relative, resolve } from "node:path";
6
+ import { createRequire } from "node:module";
5
7
  import { fileURLToPath } from "node:url";
6
8
  import { cli } from "textlint/lib/src/cli.js";
7
9
  const FORMAT_FLAGS = new Set(["--format", "-f"]);
@@ -158,6 +160,20 @@ function packageVersion() {
158
160
  function packageNodeModules() {
159
161
  return resolve(packageRoot(), "..");
160
162
  }
163
+ // textlint resolves a config `filters` key by prepending the rules-base-directory,
164
+ // so the comments filter only loads when it sits as a direct sibling of that
165
+ // directory (a flat npm install). Generate the config at runtime with the filter
166
+ // referenced by a path relative to the base directory, which resolves the same
167
+ // filter package under any install layout (flat npm or nested pnpm).
168
+ function writeDefaultConfig() {
169
+ const require = createRequire(import.meta.url);
170
+ const filterDir = dirname(require.resolve("textlint-filter-rule-comments/package.json"));
171
+ const filterKey = relative(packageNodeModules(), filterDir);
172
+ const configDir = mkdtempSync(join(tmpdir(), "slopless-config-"));
173
+ const configPath = join(configDir, "slopless.textlintrc.json");
174
+ writeFileSync(configPath, JSON.stringify({ filters: { [filterKey]: true } }));
175
+ return configPath;
176
+ }
161
177
  async function installSkill(target, force) {
162
178
  const source = resolve(packageRoot(), "skills", "slopless");
163
179
  const destination = resolve(process.cwd(), skillDestination(target));
@@ -227,10 +243,9 @@ async function main() {
227
243
  const args = [
228
244
  "node",
229
245
  "slopless",
230
- "--no-textlintrc",
231
246
  ...(hasFlag(userArgs, CONFIG_FLAGS)
232
247
  ? []
233
- : ["--config", resolve(packageRoot(), "slopless.textlintrc.json")]),
248
+ : ["--config", writeDefaultConfig()]),
234
249
  "--preset",
235
250
  "slopless",
236
251
  "--rules-base-directory",
@@ -1,5 +1,5 @@
1
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";
2
- import { hasAbstractCommaContrast, hasFactualConnectorAfterNegation, hasMetaContext } from "./negation-context-gates.js";
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";
@@ -39,9 +39,15 @@ function inlineNegationContrast(sentence) {
39
39
  INLINE_NON_CONTRAST_NEGATION_FOLLOWERS.has(tokens[negationIndex + 1]?.normalized ?? "")) {
40
40
  return undefined;
41
41
  }
42
+ // The connector branch (not ... but/instead/rather ...) is a normal factual
43
+ // correlative unless the payoff is abstract/evaluative. Requiring an abstract
44
+ // payoff here is what separates the empty reframe ("not a failure, but a
45
+ // signal") from ordinary prose ("not constructed by the king, but by his
46
+ // successor"), which was the dominant false-positive source in the audit.
42
47
  return !hasConcreteCorrectionEvidence(sentence.text) &&
43
48
  (hasAbstractCommaContrast(sentence, tokens, negation.start) ||
44
- hasInlineContrastConnectorAfterNegation(tokens, negationIndex))
49
+ (hasInlineContrastConnectorAfterNegation(tokens, negationIndex) &&
50
+ hasAbstractNegationPayoff(tokens)))
45
51
  ? {
46
52
  end: sentence.end,
47
53
  start: sentence.start,
@@ -14,6 +14,148 @@ const REPEATED_FRAME_PREFIXES = [
14
14
  ["what", "helps", "is"],
15
15
  ["what", "matters", "is"]
16
16
  ];
17
+ // A bare repeated opener is only a signal when the word is distinctive. Three
18
+ // sentences opening with a common function word, pronoun, or transition is
19
+ // normal prose (especially in reference writing) and was the dominant source of
20
+ // false positives in the human-corpus audit. The multi-word frame logic below
21
+ // (REPEATED_FRAME_PREFIXES) still catches the high-precision cases.
22
+ const COMMON_OPENERS = new Set([
23
+ "a",
24
+ "an",
25
+ "the",
26
+ "this",
27
+ "that",
28
+ "these",
29
+ "those",
30
+ "each",
31
+ "every",
32
+ "some",
33
+ "any",
34
+ "no",
35
+ "all",
36
+ "both",
37
+ "another",
38
+ "such",
39
+ "i",
40
+ "you",
41
+ "he",
42
+ "she",
43
+ "it",
44
+ "we",
45
+ "they",
46
+ "me",
47
+ "him",
48
+ "her",
49
+ "us",
50
+ "them",
51
+ "his",
52
+ "hers",
53
+ "its",
54
+ "our",
55
+ "ours",
56
+ "your",
57
+ "yours",
58
+ "their",
59
+ "theirs",
60
+ "my",
61
+ "mine",
62
+ "there",
63
+ "here",
64
+ "in",
65
+ "on",
66
+ "at",
67
+ "of",
68
+ "to",
69
+ "for",
70
+ "from",
71
+ "with",
72
+ "by",
73
+ "as",
74
+ "into",
75
+ "onto",
76
+ "over",
77
+ "under",
78
+ "after",
79
+ "before",
80
+ "during",
81
+ "through",
82
+ "between",
83
+ "among",
84
+ "about",
85
+ "against",
86
+ "within",
87
+ "without",
88
+ "upon",
89
+ "across",
90
+ "around",
91
+ "and",
92
+ "but",
93
+ "or",
94
+ "nor",
95
+ "so",
96
+ "yet",
97
+ "because",
98
+ "although",
99
+ "though",
100
+ "while",
101
+ "if",
102
+ "when",
103
+ "whenever",
104
+ "since",
105
+ "unless",
106
+ "until",
107
+ "whereas",
108
+ "however",
109
+ "therefore",
110
+ "thus",
111
+ "also",
112
+ "moreover",
113
+ "furthermore",
114
+ "meanwhile",
115
+ "instead",
116
+ "still",
117
+ "besides",
118
+ "later",
119
+ "soon",
120
+ "now",
121
+ "today",
122
+ "once",
123
+ "first",
124
+ "second",
125
+ "third",
126
+ "finally",
127
+ "overall",
128
+ "generally",
129
+ "typically",
130
+ "often",
131
+ "usually",
132
+ "sometimes",
133
+ "eventually",
134
+ "initially",
135
+ "subsequently",
136
+ "is",
137
+ "was",
138
+ "are",
139
+ "were",
140
+ "be",
141
+ "being",
142
+ "been",
143
+ "has",
144
+ "have",
145
+ "had",
146
+ "do",
147
+ "does",
148
+ "did",
149
+ "will",
150
+ "would",
151
+ "can",
152
+ "could",
153
+ "should",
154
+ "may",
155
+ "might",
156
+ "must",
157
+ "shall"
158
+ ]);
17
159
  function isWordCharacter(character) {
18
160
  return ((character >= "a" && character <= "z") ||
19
161
  (character >= "0" && character <= "9") ||
@@ -69,6 +211,8 @@ function findTripleRepeats(text) {
69
211
  const secondOpener = firstWord(second.text);
70
212
  const thirdOpener = firstWord(third.text);
71
213
  if (firstOpener === undefined ||
214
+ firstOpener === "" ||
215
+ COMMON_OPENERS.has(firstOpener) ||
72
216
  firstOpener !== secondOpener ||
73
217
  secondOpener !== thirdOpener) {
74
218
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "slopless",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Deterministic textlint rules and CLI for catching prose slop in English Markdown.",
5
5
  "keywords": [
6
6
  "textlint",