slopfix 0.0.0-stage → 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Zwarmex
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.fr.md ADDED
@@ -0,0 +1,56 @@
1
+ <div align="center">
2
+
3
+ # slopfix
4
+
5
+ **Votre texte sonne comme du ChatGPT. slopfix trouve pourquoi, et le corrige.**
6
+
7
+ Un linter sans dépendance pour les tics d'écriture IA : tirets cadratins, `ce n'est pas X, c'est Y`, `plongeons dans`, `j'espère que cela vous aide !`, emojis en guise de puces et plus de 200 autres. Français et anglais. CLI, librairie, GitHub Action, skill pour agents IA et [playground dans le navigateur](https://zwarmex.github.io/slopfix/).
8
+
9
+ [Essayer dans le navigateur](https://zwarmex.github.io/slopfix/) · [English](README.md)
10
+
11
+ </div>
12
+
13
+ ```bash
14
+ npx slopfix article.md
15
+ ```
16
+
17
+ ## Pourquoi
18
+
19
+ Tout le monde repère un texte généré en deux secondes, et arrête de lire. Les recruteurs zappent la lettre de motivation, les lecteurs quittent l'article, les mainteneurs ferment la pull request. Les tics sont toujours les mêmes, et la plupart sont mécaniques :
20
+
21
+ <!-- slopfix-disable -->
22
+
23
+ | Tic | Exemple | slopfix |
24
+ | --- | --- | --- |
25
+ | Tirets cadratins partout | `rapide (tiret) et simple (tiret) par nature` | signale, remplace par `, ` |
26
+ | Parallélisme négatif | « Ce n'est pas un outil, c'est une révolution. » | signale |
27
+ | Formules de remplissage | « Il est important de noter que... » | supprime, remet la majuscule |
28
+ | Mots surutilisés | incontournable, pierre angulaire, s'avère être | signale, remplace les cas sûrs |
29
+ | Restes de chatbot | « Bien sûr ! », « J'espère que cela vous aide ! » | supprime ou signale |
30
+ | Grandiloquence | « change la donne », « joue un rôle crucial », « plongeons dans » | signale |
31
+ | Mise en forme de chatbot | emojis en puces, listes de **Libellé gras :** | supprime les emojis, signale les listes |
32
+
33
+ <!-- slopfix-enable -->
34
+
35
+ slopfix est déterministe. Il n'appelle aucun LLM, ne devine pas qui a écrit le texte et n'envoie rien nulle part. Il montre les tics, donne à chaque fichier un **score de slop** de 0 (propre) à 100 (pur slop), et corrige ce qui peut l'être sans risque.
36
+
37
+ ## Utilisation
38
+
39
+ ```bash
40
+ npx slopfix docs/ # analyser un dossier
41
+ npx slopfix article.md --fix # corriger le fichier
42
+ npx slopfix . --max-score 20 # échouer seulement au-dessus de 20
43
+ npx slopfix . --lang fr # forcer le français
44
+ ```
45
+
46
+ La langue est détectée automatiquement pour chaque fichier. Les blocs de code, le code inline, les URLs, le HTML et le front matter sont ignorés.
47
+
48
+ Configuration, GitHub Action, pre-commit et API : tout est dans le [README anglais](README.md).
49
+
50
+ ## Contribuer
51
+
52
+ Ajouter une expression française, c'est une ligne dans [`src/rules/fr.js`](src/rules/fr.js). Vous voyez passer un tic IA tous les jours sur LinkedIn ? Ouvrez une pull request. Voir [CONTRIBUTING.md](CONTRIBUTING.md).
53
+
54
+ ## Licence
55
+
56
+ MIT
package/README.md CHANGED
@@ -1,3 +1,223 @@
1
- # Temporary Holding Version
1
+ <div align="center">
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ # slopfix
4
+
5
+ **Your text reads like ChatGPT wrote it. slopfix finds why, and fixes it.**
6
+
7
+ A zero dependency linter for AI writing tells: em dashes, `it's not X, it's Y`, `delve`, `I hope this helps!`, emoji bullets and 200+ more. English and French. CLI, library, GitHub Action, agent skill and a [browser playground](https://zwarmex.github.io/slopfix/).
8
+
9
+ [![CI](https://github.com/Zwarmex/slopfix/actions/workflows/ci.yml/badge.svg)](https://github.com/Zwarmex/slopfix/actions/workflows/ci.yml)
10
+ [![npm](https://img.shields.io/npm/v/slopfix.svg)](https://www.npmjs.com/package/slopfix)
11
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
12
+ ![Dependencies: 0](https://img.shields.io/badge/dependencies-0-brightgreen.svg)
13
+
14
+ [Try it in your browser](https://zwarmex.github.io/slopfix/) · [Français](README.fr.md)
15
+
16
+ </div>
17
+
18
+ ```bash
19
+ npx slopfix README.md
20
+ ```
21
+
22
+ ```
23
+ README.md en · 130 words · slop score 94/100 (pure slop)
24
+ 3:1 error Certainly! → (remove) Sycophantic chatbot opener.
25
+ 5:1 warning In today's fast-paced world, → (remove) Filler phrase. Delete it and say the thing.
26
+ 5:91 warning FlowSync is not just another app, it's "It's not X, it's Y" construction.
27
+ 7:45 warning (em dash) → "," Em dash. The most recognizable AI writing tell.
28
+ 7:117 warning delve into → "dig into" Word that is heavily overused by language models.
29
+ 15:78 error I hope this helps! → (remove) Chatbot sign-off.
30
+
31
+ ✖ 24 tells (4 errors, 16 warnings, 4 info) · 13 fixable with --fix
32
+ ```
33
+
34
+ ## Why
35
+
36
+ People now spot generated text in two seconds, and they stop reading. Recruiters skip the cover letter, readers skip the blog post, maintainers close the pull request. The tells are always the same, and most of them are mechanical:
37
+
38
+ <!-- slopfix-disable -->
39
+
40
+ | Tell | Example | slopfix |
41
+ | --- | --- | --- |
42
+ | Em dashes everywhere | `fast (em dash) and simple (em dash) by design` | flags, fixes to `, ` |
43
+ | Negative parallelism | "It's not a tool, it's a revolution." | flags |
44
+ | Filler openers | "It's important to note that..." | removes, re-capitalizes |
45
+ | Overused words | delve, utilize, tapestry, a myriad of | flags, replaces the safe ones |
46
+ | Chatbot leftovers | "Certainly!", "I hope this helps!", "As an AI language model" | removes or flags |
47
+ | Hype | "game-changer", "unlock the full potential", "stands as a testament" | flags |
48
+ | Chatbot formatting | emoji bullets, lists of **Bold label:** items | removes emojis, flags lists |
49
+
50
+ <!-- slopfix-enable -->
51
+
52
+ slopfix is deterministic. It does not call an LLM, it does not guess who wrote the text and it does not send anything anywhere. It shows the tells, gives each file a **slop score** from 0 (clean) to 100 (pure slop), and fixes what is safe to fix.
53
+
54
+ ## Install
55
+
56
+ ```bash
57
+ # run once
58
+ npx slopfix docs/
59
+
60
+ # or install
61
+ npm install --save-dev slopfix
62
+ ```
63
+
64
+ Node 18 or newer. No dependencies.
65
+
66
+ ## CLI
67
+
68
+ ```bash
69
+ slopfix README.md # lint a file
70
+ slopfix docs blog # lint folders (md, mdx, markdown, txt)
71
+ slopfix docs --fix # fix every safe tell in place
72
+ slopfix . --max-score 20 # only fail when a file scores above 20
73
+ slopfix . --format github # annotations for GitHub Actions
74
+ pbpaste | slopfix --fix | pbcopy # clean your clipboard (macOS)
75
+ ```
76
+
77
+ | Option | Description |
78
+ | --- | --- |
79
+ | `--fix` | Rewrite files with every safe fix. With stdin, print the fixed text. |
80
+ | `--lang <code>` | `en`, `fr`, `auto` (default, per file) or `all` |
81
+ | `--format <name>` | `pretty` (default), `compact`, `json`, `github` |
82
+ | `--max-score <n>` | Fail only when a file's score is above `n` |
83
+ | `--quiet` | Hide info level tells |
84
+ | `--dash <text>` | What replaces an em dash with `--fix` (default `", "`) |
85
+ | `--ext <list>` | Extensions to scan in folders |
86
+ | `--plain` | Treat input as plain text instead of Markdown |
87
+ | `--list-rules` | Print every rule |
88
+
89
+ Exit code is `0` when clean, `1` when tells are found, `2` on usage errors.
90
+
91
+ Code blocks, inline code, URLs, HTML tags and front matter are skipped, so your docs can talk about `delve` without being flagged.
92
+
93
+ ## Configuration
94
+
95
+ Create `.slopfixrc.json` (or a `slopfix` key in `package.json`):
96
+
97
+ ```json
98
+ {
99
+ "lang": "auto",
100
+ "dashReplacement": " - ",
101
+ "maxScore": 25,
102
+ "rules": {
103
+ "en/weasel-vocabulary": "off",
104
+ "em-dash": "error"
105
+ },
106
+ "ban": [
107
+ "synergy",
108
+ { "phrase": "blazingly fast", "replacement": "fast" }
109
+ ],
110
+ "ignore": ["CHANGELOG.md", "docs/legacy/**"]
111
+ }
112
+ ```
113
+
114
+ Silence a passage inside a Markdown file:
115
+
116
+ ```md
117
+ <!-- slopfix-disable -->
118
+ This paragraph can delve as much as it wants.
119
+ <!-- slopfix-enable -->
120
+
121
+ <!-- slopfix-disable-next-line -->
122
+ So can this line.
123
+ ```
124
+
125
+ ## GitHub Action
126
+
127
+ Annotates the tells right on the pull request diff.
128
+
129
+ ```yaml
130
+ # .github/workflows/slopfix.yml
131
+ name: slopfix
132
+ on: pull_request
133
+ jobs:
134
+ slopfix:
135
+ runs-on: ubuntu-latest
136
+ steps:
137
+ - uses: actions/checkout@v7
138
+ - uses: actions/setup-node@v7
139
+ with:
140
+ node-version: 22
141
+ - uses: Zwarmex/slopfix@v0
142
+ with:
143
+ paths: docs README.md
144
+ max-score: 25
145
+ ```
146
+
147
+ ## pre-commit
148
+
149
+ ```yaml
150
+ repos:
151
+ - repo: https://github.com/Zwarmex/slopfix
152
+ rev: v0.1.0
153
+ hooks:
154
+ - id: slopfix
155
+ ```
156
+
157
+ ## Library
158
+
159
+ ```js
160
+ import { lint, fix } from 'slopfix';
161
+
162
+ const result = lint('Certainly! We delve into the data.');
163
+ result.score; // 0 to 100
164
+ result.issues; // [{ ruleId, severity, message, line, column, quote, fix }, ...]
165
+
166
+ fix('Certainly! We delve into the data.').output;
167
+ // "We dig into the data."
168
+ ```
169
+
170
+ Types are included. It runs in Node and in the browser (the playground uses the same code).
171
+
172
+ ## Make your AI agent check itself
173
+
174
+ Copy [`skill/SKILL.md`](skill/SKILL.md) into your agent's skills folder (for example `.claude/skills/slopfix/SKILL.md`). Agents that support skills (Claude Code, Codex, Cursor and others) will run slopfix on the prose they write before handing it to you. You can also add one line to your `AGENTS.md`:
175
+
176
+ ```md
177
+ Before you finish, run `npx slopfix --quiet` on every Markdown file you changed and fix what it reports.
178
+ ```
179
+
180
+ ## Rules
181
+
182
+ Run `slopfix --list-rules` for the full list.
183
+
184
+ | Rule | Lang | Default | Autofix |
185
+ | --- | --- | --- | --- |
186
+ | `em-dash` | all | warning | yes |
187
+ | `spaced-en-dash` | all | warning | yes |
188
+ | `emoji-bullet` | all | warning | yes |
189
+ | `bold-label-list` | all | info | no |
190
+ | `en/chatbot-artifact`, `fr/chatbot-artifact` | en, fr | error | no |
191
+ | `en/sycophantic-opener`, `fr/sycophantic-opener` | en, fr | error | yes |
192
+ | `en/chatbot-sign-off`, `fr/chatbot-sign-off` | en, fr | error | yes |
193
+ | `en/filler-opener`, `fr/filler-opener` | en, fr | warning | yes |
194
+ | `en/hype-intro`, `fr/hype-intro` | en, fr | warning | no |
195
+ | `en/ai-vocabulary`, `fr/ai-vocabulary` | en, fr | warning | partly |
196
+ | `en/weasel-vocabulary`, `fr/weasel-vocabulary` | en, fr | info | no |
197
+ | `en/not-x-but-y`, `fr/not-x-but-y` | en, fr | warning | no |
198
+ | `en/whether-youre`, `fr/que-vous-soyez` | en, fr | info | no |
199
+
200
+ Rewrites that need judgment (negative parallelism, hype) are reported but never rewritten for you. A linter that silently rewrites your argument is worse than slop.
201
+
202
+ ## The slop score
203
+
204
+ Each tell has a weight (error 5, warning 2, info 1). The score takes the weighted density per 100 words and maps it onto a 0 to 100 curve. Rough guide:
205
+
206
+ | Score | Label |
207
+ | --- | --- |
208
+ | 0 to 9 | clean |
209
+ | 10 to 29 | a few tells |
210
+ | 30 to 59 | sloppy |
211
+ | 60 to 100 | pure slop |
212
+
213
+ It is a writing quality signal, not an AI detector. Humans write `crucial` too. A high score means readers will *think* a model wrote it, which is the problem you want to fix.
214
+
215
+ ## Contributing
216
+
217
+ The best contributions are new phrases and new languages. Adding a phrase is one line in [`src/rules/en.js`](src/rules/en.js) or [`src/rules/fr.js`](src/rules/fr.js), adding a language is one new file. See [CONTRIBUTING.md](CONTRIBUTING.md).
218
+
219
+ Wanted: Spanish, German, Portuguese, Italian, Dutch. Open a pull request with the tells you see every day in your language.
220
+
221
+ ## License
222
+
223
+ MIT
package/bin/slopfix.js ADDED
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from 'node:fs';
3
+ import { join, relative, extname, resolve } from 'node:path';
4
+ import { lint, fix, rules } from '../src/index.js';
5
+ import { formatters, c } from '../src/format.js';
6
+
7
+ const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
8
+
9
+ const HELP = `
10
+ ${c.bold('slopfix')} ${pkg.version} Find and fix AI writing tells in your docs, posts and READMEs.
11
+
12
+ ${c.bold('Usage')}
13
+ slopfix [files or folders...] [options]
14
+ cat post.md | slopfix [options]
15
+
16
+ ${c.bold('Options')}
17
+ --fix Rewrite files with every safe fix (stdin: print the fixed text)
18
+ --lang <code> en, fr, auto (default) or all
19
+ --format <name> pretty (default), compact, json, github
20
+ --max-score <n> Only fail when a file's slop score is above n (0 to 100)
21
+ --quiet Hide info level tells
22
+ --dash <text> What replaces an em dash with --fix (default ", ")
23
+ --ext <list> Extensions to scan in folders (default md,mdx,markdown,txt)
24
+ --plain Treat input as plain text, not Markdown
25
+ --config <path> Config file (default .slopfixrc.json if present)
26
+ --list-rules Print every rule and exit
27
+ -v, --version Print the version
28
+ -h, --help Print this help
29
+
30
+ ${c.bold('Examples')}
31
+ npx slopfix README.md
32
+ npx slopfix docs --fix
33
+ npx slopfix . --max-score 20 --format github
34
+ pbpaste | npx slopfix --fix | pbcopy
35
+
36
+ ${c.bold('Exit codes')}
37
+ 0 no errors or warnings (or every file under --max-score), 1 tells found, 2 usage error
38
+ `;
39
+
40
+ const IGNORED_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'out', 'vendor', 'coverage', '.next', '.nuxt', '.cache', '.venv', 'venv']);
41
+
42
+ function parseArgs(argv) {
43
+ const args = { files: [], fix: false, format: 'pretty', quiet: false, listRules: false, plain: false };
44
+ const takesValue = new Set(['--lang', '--format', '--max-score', '--dash', '--ext', '--config']);
45
+ for (let i = 0; i < argv.length; i++) {
46
+ let a = argv[i];
47
+ let value;
48
+ if (a.startsWith('--') && a.includes('=')) [a, value] = [a.slice(0, a.indexOf('=')), a.slice(a.indexOf('=') + 1)];
49
+ if (takesValue.has(a) && value === undefined) {
50
+ value = argv[++i];
51
+ if (value === undefined) throw new Error(`${a} needs a value`);
52
+ }
53
+ switch (a) {
54
+ case '-h':
55
+ case '--help':
56
+ args.help = true;
57
+ break;
58
+ case '-v':
59
+ case '--version':
60
+ args.version = true;
61
+ break;
62
+ case '--fix':
63
+ args.fix = true;
64
+ break;
65
+ case '--quiet':
66
+ args.quiet = true;
67
+ break;
68
+ case '--plain':
69
+ args.plain = true;
70
+ break;
71
+ case '--list-rules':
72
+ args.listRules = true;
73
+ break;
74
+ case '--lang':
75
+ args.lang = value;
76
+ break;
77
+ case '--format':
78
+ if (!formatters[value]) throw new Error(`Unknown format "${value}". Use ${Object.keys(formatters).join(', ')}.`);
79
+ args.format = value;
80
+ break;
81
+ case '--max-score':
82
+ args.maxScore = Number(value);
83
+ if (!Number.isFinite(args.maxScore)) throw new Error('--max-score needs a number');
84
+ break;
85
+ case '--dash':
86
+ args.dash = value;
87
+ break;
88
+ case '--ext':
89
+ args.ext = value.split(',').map((e) => e.trim().replace(/^\./, ''));
90
+ break;
91
+ case '--config':
92
+ args.config = value;
93
+ break;
94
+ default:
95
+ if (a.startsWith('-') && a !== '-') throw new Error(`Unknown option ${a}`);
96
+ args.files.push(a);
97
+ }
98
+ }
99
+ return args;
100
+ }
101
+
102
+ function loadConfig(path) {
103
+ const file = path || ['.slopfixrc.json', '.slopfixrc'].find((f) => existsSync(f));
104
+ if (file) {
105
+ try {
106
+ return JSON.parse(readFileSync(file, 'utf8'));
107
+ } catch (e) {
108
+ throw new Error(`Could not read config ${file}: ${e.message}`);
109
+ }
110
+ }
111
+ if (existsSync('package.json')) {
112
+ try {
113
+ return JSON.parse(readFileSync('package.json', 'utf8')).slopfix || {};
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+ return {};
119
+ }
120
+
121
+ function globToRegex(glob) {
122
+ const re = glob
123
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
124
+ .replace(/\*\*\/?/g, '\u0000')
125
+ .replace(/\*/g, '[^/]*')
126
+ .replace(/\?/g, '[^/]')
127
+ .replace(/\u0000/g, '.*');
128
+ return new RegExp(`(^|/)${re}($|/)`);
129
+ }
130
+
131
+ function collect(paths, extensions, ignore) {
132
+ const files = [];
133
+ const isIgnored = (p) => ignore.some((re) => re.test(p.split('\\').join('/')));
134
+ const walk = (dir) => {
135
+ for (const name of readdirSync(dir).sort()) {
136
+ if (IGNORED_DIRS.has(name)) continue;
137
+ const full = join(dir, name);
138
+ const rel = relative(process.cwd(), full);
139
+ if (isIgnored(rel)) continue;
140
+ const st = statSync(full);
141
+ if (st.isDirectory()) walk(full);
142
+ else if (extensions.includes(extname(name).slice(1).toLowerCase())) files.push(full);
143
+ }
144
+ };
145
+ for (const p of paths) {
146
+ if (!existsSync(p)) throw new Error(`No such file or folder: ${p}`);
147
+ if (statSync(p).isDirectory()) walk(p);
148
+ else files.push(p);
149
+ }
150
+ return files;
151
+ }
152
+
153
+ function readStdin() {
154
+ return readFileSync(0, 'utf8');
155
+ }
156
+
157
+ function listRules() {
158
+ const rows = rules.map((r) => `${r.id.padEnd(26)} ${r.lang.padEnd(3)} ${r.severity.padEnd(8)} ${r.message}`);
159
+ return [`${'RULE'.padEnd(26)} ${'LANG'.padEnd(3)} SEVERITY ${'DESCRIPTION'}`, ...rows].join('\n');
160
+ }
161
+
162
+ function main() {
163
+ let args;
164
+ try {
165
+ args = parseArgs(process.argv.slice(2));
166
+ } catch (e) {
167
+ console.error(c.red(`slopfix: ${e.message}`));
168
+ return 2;
169
+ }
170
+ if (args.help) return console.log(HELP), 0;
171
+ if (args.version) return console.log(pkg.version), 0;
172
+ if (args.listRules) return console.log(listRules()), 0;
173
+
174
+ const config = loadConfig(args.config);
175
+ const options = {
176
+ lang: args.lang || config.lang || 'auto',
177
+ rules: config.rules || {},
178
+ ban: config.ban || [],
179
+ dashReplacement: args.dash ?? config.dashReplacement ?? ', ',
180
+ markdown: !args.plain && config.markdown !== false,
181
+ };
182
+ const maxScore = args.maxScore ?? config.maxScore;
183
+ const extensions = args.ext || config.extensions || ['md', 'mdx', 'markdown', 'txt'];
184
+ const ignore = (config.ignore || []).map(globToRegex);
185
+
186
+ const fromStdin = args.files.length === 0 || (args.files.length === 1 && args.files[0] === '-');
187
+ if (fromStdin && process.stdin.isTTY) {
188
+ console.log(HELP);
189
+ return 2;
190
+ }
191
+
192
+ const results = [];
193
+ try {
194
+ if (fromStdin) {
195
+ const text = readStdin();
196
+ if (args.fix) {
197
+ process.stdout.write(fix(text, options).output);
198
+ return 0;
199
+ }
200
+ results.push({ file: '<stdin>', ...lint(text, options) });
201
+ } else {
202
+ for (const file of collect(args.files, extensions, ignore)) {
203
+ const text = readFileSync(file, 'utf8');
204
+ const name = relative(process.cwd(), resolve(file)).split('\\').join('/');
205
+ if (args.fix) {
206
+ const { output, fixed, result } = fix(text, options);
207
+ if (fixed > 0 && output !== text) writeFileSync(file, output);
208
+ results.push({ file: name, fixed, ...result });
209
+ } else {
210
+ results.push({ file: name, ...lint(text, options) });
211
+ }
212
+ }
213
+ }
214
+ } catch (e) {
215
+ console.error(c.red(`slopfix: ${e.message}`));
216
+ return 2;
217
+ }
218
+
219
+ const shown = args.quiet ? results.map((r) => ({ ...r, issues: r.issues.filter((i) => i.severity !== 'info') })) : results;
220
+ const output = formatters[args.format](shown);
221
+ if (output) console.log(output);
222
+ if (args.fix && args.format === 'pretty') {
223
+ const total = results.reduce((n, r) => n + (r.fixed || 0), 0);
224
+ console.log(c.green(`\u2714 Fixed ${total} tells in ${results.filter((r) => r.fixed).length} files.`));
225
+ }
226
+
227
+ const failed = results.some((r) =>
228
+ maxScore !== undefined ? r.score > maxScore : r.issues.some((i) => i.severity === 'error' || i.severity === 'warning'),
229
+ );
230
+ return failed ? 1 : 0;
231
+ }
232
+
233
+ process.exitCode = main();
package/index.d.ts ADDED
@@ -0,0 +1,93 @@
1
+ export type Severity = 'error' | 'warning' | 'info';
2
+ export type Language = 'en' | 'fr';
3
+
4
+ export interface Options {
5
+ /** 'auto' (default) detects the language, 'all' runs every language's rules. */
6
+ lang?: Language | 'auto' | 'all';
7
+ /** Turn a rule off or change its severity, e.g. { 'em-dash': 'off', 'en/weasel-vocabulary': 'warning' }. */
8
+ rules?: Record<string, Severity | 'off' | false>;
9
+ /** Extra phrases to flag. A replacement makes them fixable. */
10
+ ban?: Array<string | { phrase: string; replacement?: string }>;
11
+ /** What replaces an em dash when fixing. Default ', '. */
12
+ dashReplacement?: string;
13
+ /** Skip code, URLs, HTML and front matter. Default true. */
14
+ markdown?: boolean;
15
+ }
16
+
17
+ export interface Fix {
18
+ range: [number, number];
19
+ text: string;
20
+ label?: string;
21
+ }
22
+
23
+ export interface Issue {
24
+ ruleId: string;
25
+ category: string;
26
+ severity: Severity;
27
+ message: string;
28
+ quote: string;
29
+ index: number;
30
+ length: number;
31
+ line: number;
32
+ column: number;
33
+ endLine: number;
34
+ endColumn: number;
35
+ fix?: Fix;
36
+ }
37
+
38
+ export interface LintResult {
39
+ language: Language;
40
+ words: number;
41
+ /** 0 is clean, 100 is pure slop. */
42
+ score: number;
43
+ label: 'clean' | 'a few tells' | 'sloppy' | 'pure slop';
44
+ issues: Issue[];
45
+ }
46
+
47
+ export interface FixResult {
48
+ output: string;
49
+ fixed: number;
50
+ result: LintResult;
51
+ }
52
+
53
+ export interface Hit {
54
+ index: number;
55
+ length: number;
56
+ fix?: string | null;
57
+ fixLabel?: string;
58
+ message?: string;
59
+ quote?: string;
60
+ }
61
+
62
+ export interface Rule {
63
+ id: string;
64
+ lang: Language | '*';
65
+ category: string;
66
+ severity: Severity;
67
+ message: string;
68
+ find(text: string, ctx: { options: Required<Options>; language: Language }): Iterable<Hit>;
69
+ }
70
+
71
+ export function lint(text: string, options?: Options): LintResult;
72
+ export function fix(text: string, options?: Options): FixResult;
73
+ export function detectLanguage(text: string): Language;
74
+ export function computeScore(issues: Issue[], words: number): number;
75
+ export function scoreLabel(score: number): LintResult['label'];
76
+ export function mask(text: string, options?: { markdown?: boolean }): string;
77
+ export const rules: Rule[];
78
+ export const languages: Language[];
79
+ export const weights: Record<Severity, number>;
80
+ export const defaults: Required<Options>;
81
+
82
+ declare const slopfix: {
83
+ lint: typeof lint;
84
+ fix: typeof fix;
85
+ rules: typeof rules;
86
+ detectLanguage: typeof detectLanguage;
87
+ computeScore: typeof computeScore;
88
+ scoreLabel: typeof scoreLabel;
89
+ mask: typeof mask;
90
+ languages: typeof languages;
91
+ defaults: typeof defaults;
92
+ };
93
+ export default slopfix;
package/package.json CHANGED
@@ -1,6 +1,58 @@
1
1
  {
2
2
  "name": "slopfix",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Find and fix AI writing tells (em dashes, \"it's not X, it's Y\", delve, chatbot leftovers) in English and French. Zero dependencies. CLI, library, GitHub Action.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./index.d.ts",
11
+ "default": "./src/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "bin": {
16
+ "slopfix": "bin/slopfix.js"
17
+ },
18
+ "files": [
19
+ "bin",
20
+ "src",
21
+ "index.d.ts"
22
+ ],
23
+ "scripts": {
24
+ "test": "node --test",
25
+ "lint:docs": "node bin/slopfix.js README.md README.fr.md CONTRIBUTING.md CHANGELOG.md skill/SKILL.md",
26
+ "demo": "node examples/make-examples.js && node bin/slopfix.js examples"
27
+ },
28
+ "keywords": [
29
+ "ai",
30
+ "llm",
31
+ "chatgpt",
32
+ "claude",
33
+ "slop",
34
+ "ai-slop",
35
+ "ai-writing",
36
+ "humanizer",
37
+ "linter",
38
+ "prose",
39
+ "writing",
40
+ "markdown",
41
+ "em-dash",
42
+ "cli",
43
+ "github-action"
44
+ ],
45
+ "author": "Zwarmex",
46
+ "license": "MIT",
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "git+https://github.com/Zwarmex/slopfix.git"
50
+ },
51
+ "homepage": "https://zwarmex.github.io/slopfix/",
52
+ "bugs": {
53
+ "url": "https://github.com/Zwarmex/slopfix/issues"
54
+ },
55
+ "engines": {
56
+ "node": ">=18"
57
+ }
58
+ }