slopfighter 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.
@@ -0,0 +1,65 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // `!!x` inside an `if`/`while`/`?:`/`&&`/`||` — already coerces.
4
+ // `Boolean(x)` in same contexts — same.
5
+ // AI loves to add these for "explicitness".
6
+ export default {
7
+ id: 'redundant-boolean-cast',
8
+ severity: 'info',
9
+ description: '`!!x` or `Boolean(x)` where the value is already coerced',
10
+ check(ctx) {
11
+ const { sourceFile, ts } = ctx;
12
+ const findings = [];
13
+
14
+ walk(sourceFile, (node) => {
15
+ if (!isBooleanContext(node, ts)) return;
16
+ // for if/while/?: the expression is `node.expression` or `node.condition`
17
+ const expr =
18
+ node.expression || node.condition;
19
+ if (!expr) return;
20
+ const reason = describeRedundantCast(expr, ts);
21
+ if (!reason) return;
22
+ const range = nodeRange(sourceFile, expr);
23
+ findings.push({
24
+ message: `${reason} — already coerced to boolean by the surrounding context`,
25
+ line: range.line,
26
+ column: range.column,
27
+ });
28
+ });
29
+ return findings;
30
+ },
31
+ };
32
+
33
+ function isBooleanContext(node, ts) {
34
+ return (
35
+ node.kind === ts.SyntaxKind.IfStatement ||
36
+ node.kind === ts.SyntaxKind.WhileStatement ||
37
+ node.kind === ts.SyntaxKind.DoStatement ||
38
+ node.kind === ts.SyntaxKind.ConditionalExpression ||
39
+ (node.kind === ts.SyntaxKind.PrefixUnaryExpression && node.operator === ts.SyntaxKind.ExclamationToken)
40
+ );
41
+ }
42
+
43
+ function describeRedundantCast(expr, ts) {
44
+ // `!!x`
45
+ if (
46
+ expr.kind === ts.SyntaxKind.PrefixUnaryExpression &&
47
+ expr.operator === ts.SyntaxKind.ExclamationToken &&
48
+ expr.operand &&
49
+ expr.operand.kind === ts.SyntaxKind.PrefixUnaryExpression &&
50
+ expr.operand.operator === ts.SyntaxKind.ExclamationToken
51
+ ) {
52
+ return '`!!x`';
53
+ }
54
+ // `Boolean(x)`
55
+ if (
56
+ expr.kind === ts.SyntaxKind.CallExpression &&
57
+ expr.expression &&
58
+ expr.expression.kind === ts.SyntaxKind.Identifier &&
59
+ expr.expression.text === 'Boolean' &&
60
+ (expr.arguments || []).length === 1
61
+ ) {
62
+ return '`Boolean(x)`';
63
+ }
64
+ return null;
65
+ }
@@ -0,0 +1,44 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // `throw new Error(err.message)` — strips stack and type for no reason.
4
+ // `throw new Error(String(err))` — same.
5
+ export default {
6
+ id: 'redundant-error-rethrow',
7
+ severity: 'warn',
8
+ description: 'Re-wrapping an Error in `new Error(err.message)` destroys the stack',
9
+ check(ctx) {
10
+ const { sourceFile, ts } = ctx;
11
+ const findings = [];
12
+
13
+ walk(sourceFile, (node) => {
14
+ if (node.kind !== ts.SyntaxKind.ThrowStatement) return;
15
+ const expr = node.expression;
16
+ if (!expr || expr.kind !== ts.SyntaxKind.NewExpression) return;
17
+ const callee = expr.expression;
18
+ if (callee.kind !== ts.SyntaxKind.Identifier) return;
19
+ if (callee.text !== 'Error') return;
20
+ const args = expr.arguments || [];
21
+ if (args.length !== 1) return;
22
+ const arg = args[0];
23
+ // foo.message
24
+ const isDotMessage =
25
+ arg.kind === ts.SyntaxKind.PropertyAccessExpression &&
26
+ arg.name &&
27
+ arg.name.text === 'message';
28
+ // String(foo)
29
+ const isStringWrap =
30
+ arg.kind === ts.SyntaxKind.CallExpression &&
31
+ arg.expression &&
32
+ arg.expression.kind === ts.SyntaxKind.Identifier &&
33
+ arg.expression.text === 'String';
34
+ if (!isDotMessage && !isStringWrap) return;
35
+
36
+ const range = nodeRange(sourceFile, node);
37
+ findings.push({
38
+ message: 'rewrapping an Error destroys the stack — just `throw err` or use `{ cause: err }`',
39
+ ...range,
40
+ });
41
+ });
42
+ return findings;
43
+ },
44
+ };
@@ -0,0 +1,35 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // `return undefined` — equivalent to `return` and `return undefined` is
4
+ // pure noise that AI loves to add for "explicitness".
5
+ // Also catches `return void 0;`.
6
+ export default {
7
+ id: 'return-undefined',
8
+ severity: 'info',
9
+ description: '`return undefined` is identical to bare `return`',
10
+ check(ctx) {
11
+ const { sourceFile, ts } = ctx;
12
+ const findings = [];
13
+
14
+ walk(sourceFile, (node) => {
15
+ if (node.kind !== ts.SyntaxKind.ReturnStatement) return;
16
+ const e = node.expression;
17
+ if (!e) return;
18
+ const isUndefined = e.kind === ts.SyntaxKind.Identifier && e.text === 'undefined';
19
+ const isVoid0 =
20
+ e.kind === ts.SyntaxKind.VoidExpression &&
21
+ e.expression &&
22
+ e.expression.kind === ts.SyntaxKind.NumericLiteral &&
23
+ e.expression.text === '0';
24
+ if (!isUndefined && !isVoid0) return;
25
+ const range = nodeRange(sourceFile, node);
26
+ findings.push({
27
+ message: 'drop the explicit `undefined` — bare `return;` is equivalent',
28
+ line: range.line,
29
+ column: range.column,
30
+ fix: { start: node.getStart(sourceFile), end: node.getEnd(), replacement: 'return;' },
31
+ });
32
+ });
33
+ return findings;
34
+ },
35
+ };
@@ -0,0 +1,49 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // Class with no fields and exactly one public method that doesn't reference
4
+ // `this` — it's just a namespace for a function. Convert to a function.
5
+ export default {
6
+ id: 'single-method-class',
7
+ severity: 'info',
8
+ description: 'Class with one method that doesn\'t use `this` — should be a function',
9
+ check(ctx) {
10
+ const { sourceFile, ts } = ctx;
11
+ const findings = [];
12
+
13
+ walk(sourceFile, (node) => {
14
+ if (node.kind !== ts.SyntaxKind.ClassDeclaration) return;
15
+ if (!node.name) return;
16
+ // subclasses and interface implementations are intentionally classes
17
+ if (node.heritageClauses && node.heritageClauses.length > 0) return;
18
+ const members = node.members || [];
19
+ const props = members.filter((m) => m.kind === ts.SyntaxKind.PropertyDeclaration);
20
+ const methods = members.filter((m) => m.kind === ts.SyntaxKind.MethodDeclaration);
21
+ const ctors = members.filter((m) => m.kind === ts.SyntaxKind.Constructor);
22
+ if (props.length > 0) return;
23
+ if (methods.length !== 1) return;
24
+ if (ctors.length > 0 && ctors[0].body && ctors[0].body.statements.length > 0) return;
25
+
26
+ const method = methods[0];
27
+ if (usesThis(method, ts)) return;
28
+
29
+ const range = nodeRange(sourceFile, node);
30
+ findings.push({
31
+ message: `class "${node.name.text}" wraps a single this-free method — make it a function`,
32
+ line: range.line,
33
+ column: range.column,
34
+ });
35
+ });
36
+ return findings;
37
+ },
38
+ };
39
+
40
+ function usesThis(node, ts) {
41
+ let found = false;
42
+ visit(node);
43
+ return found;
44
+ function visit(n) {
45
+ if (found || !n) return;
46
+ if (n.kind === ts.SyntaxKind.ThisKeyword) { found = true; return; }
47
+ n.forEachChild(visit);
48
+ }
49
+ }
@@ -0,0 +1,70 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // `arr.map(x => fn(x))` — useless lambda, just pass `fn`.
4
+ // Catches `(x) => fn(x)` and `(x, y) => fn(x, y)` style wrappers where
5
+ // param list exactly matches the call arg list.
6
+ export default {
7
+ id: 'trivial-arrow-wrapper',
8
+ severity: 'info',
9
+ description: 'Arrow function that just forwards its args to another call',
10
+ check(ctx) {
11
+ const { sourceFile, ts } = ctx;
12
+ const findings = [];
13
+
14
+ walk(sourceFile, (node) => {
15
+ if (node.kind !== ts.SyntaxKind.ArrowFunction) return;
16
+ const params = node.parameters || [];
17
+ if (params.length === 0) return;
18
+ // skip params with default values, destructuring, rest, type-only modifiers
19
+ for (const p of params) {
20
+ if (p.dotDotDotToken) return;
21
+ if (p.initializer) return;
22
+ if (p.name && p.name.kind !== ts.SyntaxKind.Identifier) return;
23
+ }
24
+
25
+ // body must be a single call expression (or block with `return call;`)
26
+ const call = unwrapCall(node.body, ts);
27
+ if (!call) return;
28
+
29
+ const args = call.arguments || [];
30
+ if (args.length !== params.length) return;
31
+ for (let i = 0; i < args.length; i++) {
32
+ const arg = args[i];
33
+ if (arg.kind !== ts.SyntaxKind.Identifier) return;
34
+ if (arg.text !== params[i].name.text) return;
35
+ }
36
+ // callee can be any expression; we'll just suggest replacing the wrapper
37
+ const range = nodeRange(sourceFile, node);
38
+ const calleeText = call.expression.getText(sourceFile);
39
+ findings.push({
40
+ message: `trivial wrapper — replace \`${truncate(node.getText(sourceFile), 40)}\` with \`${truncate(calleeText, 40)}\``,
41
+ line: range.line,
42
+ column: range.column,
43
+ });
44
+ });
45
+ return findings;
46
+ },
47
+ };
48
+
49
+ function unwrapCall(body, ts) {
50
+ if (!body) return null;
51
+ if (body.kind === ts.SyntaxKind.CallExpression) return body;
52
+ if (body.kind === ts.SyntaxKind.Block) {
53
+ const stmts = body.statements;
54
+ if (stmts.length !== 1) return null;
55
+ const s = stmts[0];
56
+ if (s.kind === ts.SyntaxKind.ReturnStatement && s.expression && s.expression.kind === ts.SyntaxKind.CallExpression) {
57
+ return s.expression;
58
+ }
59
+ if (s.kind === ts.SyntaxKind.ExpressionStatement && s.expression.kind === ts.SyntaxKind.CallExpression) {
60
+ // (x) => { fn(x); } — only flag if return type is void-ish; we'll be conservative and skip
61
+ return null;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function truncate(s, n) {
68
+ s = s.replace(/\s+/g, ' ');
69
+ return s.length > n ? s.slice(0, n - 1) + '…' : s;
70
+ }
@@ -0,0 +1,61 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // `async function foo() { return x }` — async with no await inside, no
4
+ // Promise return needed beyond what `async` adds. Either drop `async`
5
+ // or there's a missing await.
6
+ export default {
7
+ id: 'unnecessary-async',
8
+ severity: 'info',
9
+ description: 'async function/method with no await in its body',
10
+ check(ctx) {
11
+ const { sourceFile, ts } = ctx;
12
+ const findings = [];
13
+
14
+ walk(sourceFile, (node) => {
15
+ const isAsync =
16
+ node.modifiers &&
17
+ node.modifiers.some((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
18
+ if (!isAsync) return;
19
+
20
+ const isFn =
21
+ node.kind === ts.SyntaxKind.FunctionDeclaration ||
22
+ node.kind === ts.SyntaxKind.MethodDeclaration ||
23
+ node.kind === ts.SyntaxKind.ArrowFunction ||
24
+ node.kind === ts.SyntaxKind.FunctionExpression;
25
+ if (!isFn) return;
26
+ if (!node.body) return;
27
+
28
+ if (!hasAwait(node.body, ts)) {
29
+ const range = nodeRange(sourceFile, node);
30
+ const name = node.name && node.name.text ? `"${node.name.text}" ` : '';
31
+ findings.push({
32
+ message: `async ${name}has no \`await\` — drop \`async\` or add the missing await`,
33
+ line: range.line,
34
+ column: range.column,
35
+ });
36
+ }
37
+ });
38
+ return findings;
39
+ },
40
+ };
41
+
42
+ function hasAwait(node, ts) {
43
+ let found = false;
44
+ visit(node);
45
+ return found;
46
+
47
+ function visit(n) {
48
+ if (found || !n) return;
49
+ // do not descend into nested function bodies — their awaits are theirs
50
+ if (
51
+ n !== node &&
52
+ (n.kind === ts.SyntaxKind.FunctionDeclaration ||
53
+ n.kind === ts.SyntaxKind.FunctionExpression ||
54
+ n.kind === ts.SyntaxKind.ArrowFunction ||
55
+ n.kind === ts.SyntaxKind.MethodDeclaration)
56
+ ) return;
57
+ if (n.kind === ts.SyntaxKind.AwaitExpression) { found = true; return; }
58
+ if (n.kind === ts.SyntaxKind.ForOfStatement && n.awaitModifier) { found = true; return; }
59
+ n.forEachChild(visit);
60
+ }
61
+ }
@@ -0,0 +1,62 @@
1
+ import { walk, nodeRange } from './_util.mjs';
2
+
3
+ // catch (e) { throw e } — useless wrapping.
4
+ // catch (e) { console.log(e); throw e } — only slightly less useless.
5
+ export default {
6
+ id: 'useless-try-catch',
7
+ severity: 'warn',
8
+ description: 'try/catch that just rethrows or logs-then-rethrows',
9
+ check(ctx) {
10
+ const { sourceFile, ts } = ctx;
11
+ const findings = [];
12
+
13
+ walk(sourceFile, (node) => {
14
+ if (node.kind !== ts.SyntaxKind.TryStatement) return;
15
+ const cc = node.catchClause;
16
+ if (!cc || !cc.block) return;
17
+ const stmts = cc.block.statements || [];
18
+ if (stmts.length === 0) {
19
+ // swallow: also bad but a different smell — skip here
20
+ return;
21
+ }
22
+ const last = stmts[stmts.length - 1];
23
+ const errName = cc.variableDeclaration && cc.variableDeclaration.name && cc.variableDeclaration.name.text;
24
+
25
+ // is the last statement `throw e` referencing the catch var?
26
+ const lastIsRethrow =
27
+ last.kind === ts.SyntaxKind.ThrowStatement &&
28
+ last.expression &&
29
+ last.expression.kind === ts.SyntaxKind.Identifier &&
30
+ last.expression.text === errName;
31
+
32
+ if (!lastIsRethrow) return;
33
+
34
+ const others = stmts.slice(0, -1);
35
+ const allOthersAreLogs = others.every((s) => isJustLog(s, ts));
36
+ if (others.length === 0 || allOthersAreLogs) {
37
+ const range = nodeRange(sourceFile, cc);
38
+ findings.push({
39
+ message:
40
+ others.length === 0
41
+ ? `catch only rethrows ${errName} — drop the try/catch entirely.`
42
+ : `catch logs and rethrows — caller can handle it, the log just spams noise.`,
43
+ ...range,
44
+ });
45
+ }
46
+ });
47
+ return findings;
48
+ },
49
+ };
50
+
51
+ function isJustLog(stmt, ts) {
52
+ if (stmt.kind !== ts.SyntaxKind.ExpressionStatement) return false;
53
+ const expr = stmt.expression;
54
+ if (expr.kind !== ts.SyntaxKind.CallExpression) return false;
55
+ const callee = expr.expression;
56
+ // console.<anything>(...)
57
+ if (callee.kind === ts.SyntaxKind.PropertyAccessExpression) {
58
+ const obj = callee.expression;
59
+ if (obj.kind === ts.SyntaxKind.Identifier && obj.text === 'console') return true;
60
+ }
61
+ return false;
62
+ }
@@ -0,0 +1,122 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import ts from 'typescript';
4
+ import { allRules } from './rules/index.mjs';
5
+
6
+ const DEFAULT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
7
+ const SKIP_DIRS = new Set([
8
+ 'node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', '.cache',
9
+ 'vendor', 'vendored', 'third_party', 'thirdparty', 'generated', '__generated__',
10
+ '.svelte-kit', '.nuxt', '.turbo', '.parcel-cache',
11
+ ]);
12
+
13
+ export async function scanPath(target, flags = {}) {
14
+ const stat = fs.statSync(target);
15
+ const files = stat.isDirectory() ? collectFiles(target, flags.ignore || []) : [target];
16
+ const activeRules = filterRules(allRules, flags);
17
+
18
+ const findings = [];
19
+ for (const file of files) {
20
+ const fileFindings = scanFile(file, activeRules);
21
+ findings.push(...fileFindings);
22
+ }
23
+ // Return both findings (array — callers that destructure or just use
24
+ // .length keep working) AND a side-channel fileCount via a non-enumerable
25
+ // property, so the score can normalize correctly.
26
+ Object.defineProperty(findings, 'fileCount', { value: files.length, enumerable: false });
27
+ return findings;
28
+ }
29
+
30
+ function collectFiles(dir, ignorePatterns) {
31
+ const out = [];
32
+ walk(dir);
33
+ return out;
34
+
35
+ function walk(d) {
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(d, { withFileTypes: true });
39
+ } catch {
40
+ return;
41
+ }
42
+ for (const e of entries) {
43
+ const full = path.join(d, e.name);
44
+ if (e.isDirectory()) {
45
+ if (SKIP_DIRS.has(e.name)) continue;
46
+ walk(full);
47
+ } else if (e.isFile()) {
48
+ const ext = path.extname(e.name);
49
+ if (!DEFAULT_EXTENSIONS.has(ext)) continue;
50
+ if (shouldIgnore(full, ignorePatterns)) continue;
51
+ out.push(full);
52
+ }
53
+ }
54
+ }
55
+ }
56
+
57
+ function shouldIgnore(file, patterns) {
58
+ if (!patterns.length) return false;
59
+ const norm = file.replace(/\\/g, '/');
60
+ return patterns.some((p) => norm.includes(p));
61
+ }
62
+
63
+ function filterRules(rules, flags) {
64
+ let r = rules;
65
+ if (flags.rules && flags.rules.length) {
66
+ const set = new Set(flags.rules);
67
+ r = r.filter((rule) => set.has(rule.id));
68
+ }
69
+ if (!flags.strict) {
70
+ r = r.filter((rule) => !rule.strictOnly);
71
+ }
72
+ return r;
73
+ }
74
+
75
+ function scanFile(file, rules) {
76
+ let source;
77
+ try {
78
+ source = fs.readFileSync(file, 'utf8');
79
+ } catch {
80
+ return [];
81
+ }
82
+ const ext = path.extname(file);
83
+ const scriptKind =
84
+ ext === '.tsx' ? ts.ScriptKind.TSX :
85
+ ext === '.ts' ? ts.ScriptKind.TS :
86
+ ext === '.jsx' ? ts.ScriptKind.JSX : ts.ScriptKind.JS;
87
+ const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true, scriptKind);
88
+ const lines = source.split(/\r?\n/);
89
+
90
+ const ctx = { file, source, lines, sourceFile, ts };
91
+ const findings = [];
92
+ for (const rule of rules) {
93
+ try {
94
+ const results = rule.check(ctx) || [];
95
+ for (const r of results) {
96
+ findings.push({
97
+ ruleId: rule.id,
98
+ severity: r.severity || rule.severity || 'warn',
99
+ message: r.message,
100
+ file,
101
+ line: r.line,
102
+ column: r.column || 1,
103
+ endLine: r.endLine || r.line,
104
+ endColumn: r.endColumn || (r.column || 1) + 1,
105
+ fixable: !!r.fix,
106
+ fix: r.fix || null,
107
+ });
108
+ }
109
+ } catch (err) {
110
+ // never crash the scan on a buggy rule
111
+ findings.push({
112
+ ruleId: rule.id,
113
+ severity: 'error',
114
+ message: `rule crashed: ${err.message}`,
115
+ file,
116
+ line: 1,
117
+ column: 1,
118
+ });
119
+ }
120
+ }
121
+ return findings;
122
+ }
package/src/score.mjs ADDED
@@ -0,0 +1,44 @@
1
+ const WEIGHTS = { error: 5, warn: 2, info: 1 };
2
+
3
+ // Score is computed on penalty-per-file so a clean 2000-file repo isn't
4
+ // punished by sheer volume. Single-file scans use the penalty directly.
5
+ export function computeScore(findings, options = {}) {
6
+ const counts = { error: 0, warn: 0, info: 0 };
7
+ const byRule = {};
8
+ const files = new Set();
9
+ for (const f of findings) {
10
+ counts[f.severity] = (counts[f.severity] || 0) + 1;
11
+ byRule[f.ruleId] = (byRule[f.ruleId] || 0) + 1;
12
+ if (f.file) files.add(f.file);
13
+ }
14
+
15
+ const penalty =
16
+ counts.error * WEIGHTS.error +
17
+ counts.warn * WEIGHTS.warn +
18
+ counts.info * WEIGHTS.info;
19
+
20
+ // normalize: prefer caller-supplied fileCount, else side-channel on the
21
+ // findings array (set by scanner), else dirty-file count, else 1.
22
+ const fileCount =
23
+ options.fileCount || findings.fileCount || Math.max(files.size, 1);
24
+ const perFilePenalty = penalty / fileCount;
25
+ // tuned against real codebases: hallmark ~0.5 fpf → B, cline ~1.4 → D,
26
+ // continue ~3.0 → F. Divisor 3.0 keeps clean codebases in A/B range.
27
+ const value = Math.max(0, Math.round(100 * Math.exp(-perFilePenalty / 3)));
28
+ let grade;
29
+ if (value >= 90) grade = 'A';
30
+ else if (value >= 75) grade = 'B';
31
+ else if (value >= 60) grade = 'C';
32
+ else if (value >= 40) grade = 'D';
33
+ else grade = 'F';
34
+
35
+ return {
36
+ value,
37
+ grade,
38
+ total: findings.length,
39
+ counts,
40
+ byRule,
41
+ fileCount,
42
+ perFilePenalty: +perFilePenalty.toFixed(2),
43
+ };
44
+ }
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: slopfighter
3
+ description: Detect and clean AI-generated code slop (padding comments, useless try/catch, dead imports, explicit any, premature abstractions, commented-out code, unnecessary async, and more) in JS/TS code. Use after a set of edits or before committing.
4
+ ---
5
+
6
+ # slopfighter
7
+
8
+ You are using **slopfighter**, an anti-AI-slop linter that catches the structural bloat patterns LLMs love to generate.
9
+
10
+ ## When to run
11
+
12
+ - After completing a chunk of code generation (≥3 edits to one file, or any new file).
13
+ - Before staging changes for a commit.
14
+ - When the user says "clean this up", "remove the slop", "tighten this".
15
+ - Whenever the user explicitly invokes you.
16
+
17
+ ## How to run
18
+
19
+ ```
20
+ npx slopfighter scan <path> # human-readable report
21
+ npx slopfighter score <path> --json # just the score (0-100), useful in checks
22
+ npx slopfighter fix <path> --safe # apply unambiguous fixes (e.g. drop padding comments)
23
+ ```
24
+
25
+ Default to scanning the files you just touched, not the whole repo. For a fresh module, scan its directory.
26
+
27
+ ## How to act on findings
28
+
29
+ 1. **`warn` severity**: surface to the user with the rule id and a one-line fix suggestion. Don't silently rewrite — these need judgement.
30
+ 2. **`info` severity**: mention briefly but don't make it the headline.
31
+ 3. **fixable findings** (auto-fix safe): run `npx slopfighter fix <path> --safe` after confirming with user, then re-scan.
32
+ 4. **score < 60**: treat as a fail. Don't hand the work back to the user as "done" — list the top 3 issues and ask whether to fix.
33
+
34
+ ## What the rules mean (cheat-sheet)
35
+
36
+ | Rule | Why it matters |
37
+ |---|---|
38
+ | `padding-comments` | Comments restating the function name add noise, not info. |
39
+ | `useless-try-catch` | Logging-then-rethrowing spams; pure rethrow is dead code. |
40
+ | `dead-imports` | Unused imports = stale context; often a sign of half-finished refactor. |
41
+ | `explicit-any` | `any` defeats TS; either pick a real type or use `unknown`. |
42
+ | `future-proof-naming` | `XManager`/`XHelper` with one method is just a function. |
43
+ | `commented-out-code` | "Let me also try" leftovers — either remove or restore. |
44
+ | `unnecessary-async` | `async` with no `await` adds a microtask for nothing. |
45
+ | `trivial-arrow-wrapper` | `(x) => fn(x)` is just `fn`. |
46
+ | `redundant-await` | `return await x` outside try/catch is a wasted frame. |
47
+ | `return-undefined` | `return undefined` is bare `return`. |
48
+ | `redundant-error-rethrow` | `throw new Error(e.message)` destroys the stack. |
49
+ | `excessive-jsdoc` | 8-line JSDoc on a 2-line body is padding. |
50
+ | `over-defensive-null-check` | Triple-checking the same var is one check. |
51
+ | `always-true-conditional` | `if (true)` / `if (x === x)` is dead branching. |
52
+ | `single-method-class` | One method that doesn't use `this` → make it a function. |
53
+
54
+ ## Output format
55
+
56
+ When you report findings to the user, prefer this shape:
57
+
58
+ ```
59
+ slopfighter scan: <score>/100 (<grade>) — N findings in M files
60
+
61
+ Top issues:
62
+ src/foo.ts:42 [warn] padding-comments: "Get user name" just restates "getUserName"
63
+ src/bar.ts:18 [warn] explicit-any: avoid `any` — pick a real type
64
+ ...
65
+ ```
66
+
67
+ Don't dump the full report verbatim — summarize, then offer "want me to fix the auto-fixable ones?"
@@ -0,0 +1,7 @@
1
+ <!-- slopfighter:start — do not edit between markers, regenerated by `npx slopfighter install` -->
2
+ ## Code quality (slopfighter)
3
+
4
+ After editing code, run `npx slopfighter scan <changed-paths>`. Treat score < 60 as a failed change — surface top warnings to the user before declaring done. Use `npx slopfighter fix --safe` for auto-fixable items only after confirming.
5
+
6
+ Rules covered: padding-comments, useless-try-catch, dead-imports, explicit-any, future-proof-naming, commented-out-code, unnecessary-async, trivial-arrow-wrapper, redundant-await, return-undefined, redundant-error-rethrow, excessive-jsdoc, over-defensive-null-check, always-true-conditional, single-method-class.
7
+ <!-- slopfighter:end -->
@@ -0,0 +1,23 @@
1
+ # /slopfighter
2
+
3
+ Run `npx slopfighter scan .` on the current workspace. Summarize the score, the top 3 findings (rule id + file:line + one-line fix), and ask whether to auto-fix.
4
+
5
+ If the user says yes to auto-fix, run `npx slopfighter fix . --safe` then re-scan. Surface anything that's still warn-level after the fix pass.
6
+
7
+ Rules cheat-sheet:
8
+
9
+ - `padding-comments` — comments restating the function name
10
+ - `useless-try-catch` — rethrow-only or log-then-rethrow
11
+ - `dead-imports` — unused imports
12
+ - `explicit-any` — `any` annotations
13
+ - `future-proof-naming` — `XManager`/`XHelper` with one method
14
+ - `commented-out-code` — old code blocks left in
15
+ - `unnecessary-async` — `async` with no `await`
16
+ - `trivial-arrow-wrapper` — `(x) => fn(x)` instead of `fn`
17
+ - `redundant-await` — `return await x` outside try
18
+ - `return-undefined` — `return undefined` instead of bare `return`
19
+ - `redundant-error-rethrow` — `throw new Error(e.message)`
20
+ - `excessive-jsdoc` — JSDoc longer than the body
21
+ - `over-defensive-null-check` — triple null-checks
22
+ - `always-true-conditional` — `if (true)` / `if (x === x)`
23
+ - `single-method-class` — single-method class without `this`