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
|
[](https://www.npmjs.com/package/slopless)
|
|
4
4
|
[](https://www.npmjs.com/package/slopless)
|
|
5
|
+
[](https://bundlephobia.com/package/slopless)
|
|
6
|
+
[](https://packagephobia.com/result?p=slopless)
|
|
5
7
|
[](LICENSE)
|
|
6
|
-
[](/actions/workflows/ci.yml)
|
|
7
8
|
[](package.json)
|
|
8
9
|
|
|
10
|
+
[](/actions/workflows/ci.yml)
|
|
11
|
+
[](https://scorecard.dev/viewer/?uri=github.com/seochecks-ai/slopless)
|
|
12
|
+
[](https://socket.dev/npm/package/slopless)
|
|
13
|
+
[](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
|
+
[](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 {
|
|
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",
|
|
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;
|