clean-slop 2.0.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,3239 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import path3 from 'path';
4
+ import process from 'process';
5
+ import { cosmiconfig } from 'cosmiconfig';
6
+ import fg from 'fast-glob';
7
+ import ignore from 'ignore';
8
+ import fs4 from 'fs/promises';
9
+ import isBinaryPath from 'is-binary-path';
10
+ import { parse } from '@typescript-eslint/typescript-estree';
11
+ import fs3 from 'fs';
12
+ import { execSync } from 'child_process';
13
+
14
+ // src/version.ts
15
+ var PACKAGE_VERSION = "1.0.0";
16
+ var DEFAULT_INCLUDE = [
17
+ "**/*.js",
18
+ "**/*.jsx",
19
+ "**/*.ts",
20
+ "**/*.tsx",
21
+ "**/*.mjs",
22
+ "**/*.cjs"
23
+ ];
24
+ var DEFAULT_EXCLUDE = [
25
+ "**/node_modules/**",
26
+ "**/dist/**",
27
+ "**/build/**",
28
+ "**/.next/**",
29
+ "**/coverage/**",
30
+ "**/.turbo/**",
31
+ "**/*.min.js",
32
+ "**/*.bundle.js",
33
+ "**/*.generated.*",
34
+ "**/__generated__/**"
35
+ ];
36
+ var DEFAULT_IGNORE_PATTERNS = [];
37
+ var DEFAULT_CATEGORIES = {
38
+ "ai-slop": true,
39
+ "security": true,
40
+ "reliability": true,
41
+ "maintainability": true,
42
+ "production-readiness": true
43
+ };
44
+ var DEFAULT_MAX_ISSUES = {};
45
+ function computeGrade(score) {
46
+ if (score >= 90) return "A";
47
+ if (score >= 75) return "B";
48
+ if (score >= 60) return "C";
49
+ if (score >= 40) return "D";
50
+ return "F";
51
+ }
52
+ async function loadConfig(cwd, configPath) {
53
+ const explorer = cosmiconfig("clean-slop", {
54
+ searchPlaces: [
55
+ "clean-slop.config.js",
56
+ "clean-slop.config.ts",
57
+ "clean-slop.config.mjs",
58
+ "clean-slop.config.cjs",
59
+ ".clean-slop.js",
60
+ ".clean-slop.json",
61
+ ".clean-slop.yaml",
62
+ ".clean-slop.yml",
63
+ "package.json"
64
+ ],
65
+ packageProp: "clean-slop"
66
+ });
67
+ let userConfig = {};
68
+ try {
69
+ const result = configPath ? await explorer.load(configPath) : await explorer.search(cwd);
70
+ if (result && !result.isEmpty) {
71
+ userConfig = result.config;
72
+ }
73
+ } catch {
74
+ }
75
+ return resolveConfig(userConfig, cwd);
76
+ }
77
+ function resolveConfig(userConfig, cwd) {
78
+ return {
79
+ root: path3.resolve(cwd),
80
+ version: PACKAGE_VERSION,
81
+ include: userConfig.include ?? DEFAULT_INCLUDE,
82
+ exclude: userConfig.exclude ?? DEFAULT_EXCLUDE,
83
+ categories: {
84
+ ...DEFAULT_CATEGORIES,
85
+ ...userConfig.categories
86
+ },
87
+ rules: userConfig.rules ?? {},
88
+ failThreshold: userConfig.failThreshold ?? 70,
89
+ maxIssues: {
90
+ ...DEFAULT_MAX_ISSUES,
91
+ ...userConfig.maxIssues
92
+ },
93
+ plugins: userConfig.plugins ?? [],
94
+ reporter: userConfig.reporter ?? "text",
95
+ output: userConfig.output ?? null,
96
+ verbose: userConfig.verbose ?? false,
97
+ ignorePatterns: userConfig.ignorePatterns ?? DEFAULT_IGNORE_PATTERNS,
98
+ tsConfigPath: userConfig.tsConfigPath ?? null
99
+ };
100
+ }
101
+ function generateDefaultConfig() {
102
+ return `/** @type {import('clean-slop').UserConfig} */
103
+ export default {
104
+ // Files to include in scanning
105
+ include: [
106
+ '**/*.js',
107
+ '**/*.jsx',
108
+ '**/*.ts',
109
+ '**/*.tsx',
110
+ '**/*.mjs',
111
+ '**/*.cjs',
112
+ ],
113
+
114
+ // Files to exclude from scanning
115
+ exclude: [
116
+ '**/node_modules/**',
117
+ '**/dist/**',
118
+ '**/build/**',
119
+ '**/.next/**',
120
+ '**/coverage/**',
121
+ '**/*.min.js',
122
+ '**/*.generated.*',
123
+ '**/__generated__/**',
124
+ ],
125
+
126
+ // Enable or disable entire rule categories
127
+ categories: {
128
+ 'ai-slop': true,
129
+ 'security': true,
130
+ 'reliability': true,
131
+ 'maintainability': true,
132
+ 'production-readiness': true,
133
+ },
134
+
135
+ // Per-rule severity overrides or 'off' to disable
136
+ rules: {
137
+ // 'security/hardcoded-secrets': 'critical',
138
+ // 'ai-slop/empty-catch': 'off',
139
+ },
140
+
141
+ // Minimum overall score before CI fails (0-100)
142
+ failThreshold: 70,
143
+
144
+ // Maximum allowed issues per severity
145
+ maxIssues: {
146
+ critical: 0,
147
+ high: 5,
148
+ },
149
+
150
+ // Output reporter: 'text' | 'json' | 'html' | 'markdown' | 'sarif'
151
+ reporter: 'text',
152
+
153
+ // Output file path (null = stdout)
154
+ // output: './reports/clean-slop-report.html',
155
+
156
+ // Additional ignore patterns
157
+ ignorePatterns: [],
158
+ };
159
+ `;
160
+ }
161
+ var ParseError = class extends Error {
162
+ constructor(message, filePath, cause) {
163
+ super(message);
164
+ this.filePath = filePath;
165
+ this.cause = cause;
166
+ this.name = "ParseError";
167
+ }
168
+ filePath;
169
+ cause;
170
+ };
171
+ function detectLanguage(filePath) {
172
+ const ext = path3.extname(filePath).toLowerCase();
173
+ switch (ext) {
174
+ case ".ts":
175
+ return "typescript";
176
+ case ".tsx":
177
+ return "tsx";
178
+ case ".jsx":
179
+ return "jsx";
180
+ default:
181
+ return "javascript";
182
+ }
183
+ }
184
+ function isTypeScriptLike(language) {
185
+ return language === "typescript" || language === "tsx";
186
+ }
187
+ function parseSource(filePath, source) {
188
+ const language = detectLanguage(filePath);
189
+ try {
190
+ const ast = parse(source, {
191
+ jsx: language === "jsx" || language === "tsx",
192
+ tsx: language === "tsx",
193
+ loc: true,
194
+ range: true,
195
+ comment: true,
196
+ tokens: false,
197
+ errorOnUnknownASTType: false,
198
+ allowInvalidAST: true,
199
+ suppressDeprecatedPropertyWarnings: true
200
+ });
201
+ return { filePath, source, ast, language };
202
+ } catch (err) {
203
+ if (!isTypeScriptLike(language)) {
204
+ try {
205
+ const ast = parse(source, {
206
+ jsx: language === "jsx",
207
+ loc: true,
208
+ range: true,
209
+ comment: true,
210
+ tokens: false,
211
+ errorOnUnknownASTType: false,
212
+ allowInvalidAST: true,
213
+ suppressDeprecatedPropertyWarnings: true
214
+ });
215
+ return { filePath, source, ast, language };
216
+ } catch {
217
+ }
218
+ }
219
+ const message = err instanceof Error ? err.message : "Unknown parse error";
220
+ throw new ParseError(
221
+ `Failed to parse ${filePath}: ${message}`,
222
+ filePath,
223
+ err
224
+ );
225
+ }
226
+ }
227
+ function extractSnippet(source, line, contextLines = 2) {
228
+ const lines = source.split("\n");
229
+ const start = Math.max(0, line - 1 - contextLines);
230
+ const end = Math.min(lines.length, line + contextLines);
231
+ return lines.slice(start, end).map((l, i) => {
232
+ const lineNum = start + i + 1;
233
+ const marker = lineNum === line ? ">" : " ";
234
+ return `${marker} ${String(lineNum).padStart(4, " ")} | ${l}`;
235
+ }).join("\n");
236
+ }
237
+
238
+ // src/utils/constants.ts
239
+ var DOCS_BASE_URL = "https://clean-slop.dev/docs";
240
+ var SEVERITY_ORDER = {
241
+ critical: 5,
242
+ high: 4,
243
+ medium: 3,
244
+ low: 2,
245
+ info: 1
246
+ };
247
+ var SEVERITY_COLORS = {
248
+ critical: "\x1B[31m",
249
+ // red
250
+ high: "\x1B[91m",
251
+ // bright red
252
+ medium: "\x1B[33m",
253
+ // yellow
254
+ low: "\x1B[36m",
255
+ // cyan
256
+ info: "\x1B[37m"
257
+ // gray
258
+ };
259
+ var RESET = "\x1B[0m";
260
+ var BOLD = "\x1B[1m";
261
+ var DIM = "\x1B[2m";
262
+ var GREEN = "\x1B[32m";
263
+ var YELLOW = "\x1B[33m";
264
+ var RED = "\x1B[31m";
265
+ var CYAN = "\x1B[36m";
266
+ var GRAY = "\x1B[90m";
267
+
268
+ // src/rules/engine.ts
269
+ var RuleEngine = class {
270
+ rules = /* @__PURE__ */ new Map();
271
+ register(rule23) {
272
+ if (this.rules.has(rule23.meta.id)) {
273
+ throw new Error(`Rule "${rule23.meta.id}" is already registered.`);
274
+ }
275
+ this.rules.set(rule23.meta.id, rule23);
276
+ }
277
+ registerAll(rules) {
278
+ for (const rule23 of rules) {
279
+ this.register(rule23);
280
+ }
281
+ }
282
+ getRule(id) {
283
+ return this.rules.get(id);
284
+ }
285
+ getRules() {
286
+ return Array.from(this.rules.values());
287
+ }
288
+ getRulesByCategory(category) {
289
+ return this.getRules().filter((r) => r.meta.category === category);
290
+ }
291
+ runOnFile(parsedFile, config) {
292
+ const issues = [];
293
+ for (const rule23 of this.rules.values()) {
294
+ if (!config.categories[rule23.meta.category]) continue;
295
+ const ruleConfig = config.rules[rule23.meta.id];
296
+ const severityOverride = this.resolveSeverityOverride(ruleConfig);
297
+ if (severityOverride === "off") continue;
298
+ const effectiveSeverity = severityOverride ?? rule23.meta.severity;
299
+ const ruleIssues = [];
300
+ const context = {
301
+ filePath: parsedFile.filePath,
302
+ source: parsedFile.source,
303
+ ast: parsedFile.ast,
304
+ config,
305
+ report(partial) {
306
+ const snippet = partial.snippet ?? extractSnippet(parsedFile.source, partial.location.line);
307
+ ruleIssues.push({
308
+ ruleId: rule23.meta.id,
309
+ ruleName: rule23.meta.name,
310
+ category: rule23.meta.category,
311
+ severity: effectiveSeverity,
312
+ confidence: rule23.meta.confidence,
313
+ docsUrl: rule23.meta.docsUrl ?? `${DOCS_BASE_URL}/rules/${rule23.meta.id}`,
314
+ snippet,
315
+ ...partial
316
+ });
317
+ }
318
+ };
319
+ try {
320
+ rule23.create(context);
321
+ } catch {
322
+ }
323
+ issues.push(...ruleIssues);
324
+ }
325
+ return issues;
326
+ }
327
+ resolveSeverityOverride(ruleConfig) {
328
+ if (ruleConfig === void 0) return null;
329
+ if (typeof ruleConfig === "string") {
330
+ return ruleConfig;
331
+ }
332
+ if (typeof ruleConfig === "object" && ruleConfig !== null) {
333
+ if ("severity" in ruleConfig && ruleConfig.severity) {
334
+ return ruleConfig.severity;
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ };
340
+
341
+ // src/utils/ast.ts
342
+ function traverse(ast, visitor) {
343
+ if (!ast || typeof ast !== "object") return;
344
+ const node = ast;
345
+ if (typeof node.type === "string") {
346
+ const handler = visitor[node.type];
347
+ if (handler) handler(node);
348
+ const wildcard = visitor["*"];
349
+ if (wildcard) wildcard(node);
350
+ }
351
+ for (const key of Object.keys(node)) {
352
+ if (key === "parent") continue;
353
+ const child = node[key];
354
+ if (Array.isArray(child)) {
355
+ for (const item of child) {
356
+ traverse(item, visitor);
357
+ }
358
+ } else if (child && typeof child === "object" && "type" in child) {
359
+ traverse(child, visitor);
360
+ }
361
+ }
362
+ }
363
+ function findAll(ast, nodeType) {
364
+ const results = [];
365
+ traverse(ast, {
366
+ [nodeType](node) {
367
+ results.push(node);
368
+ }
369
+ });
370
+ return results;
371
+ }
372
+ function getLocation(node, filePath) {
373
+ return {
374
+ file: filePath,
375
+ line: node.loc?.start.line ?? 1,
376
+ column: node.loc?.start.column ?? 0
377
+ };
378
+ }
379
+ function getCalleeName(node) {
380
+ const callee = node.callee;
381
+ if (!callee) return null;
382
+ if (callee.type === "Identifier") {
383
+ return callee.name;
384
+ }
385
+ if (callee.type === "MemberExpression") {
386
+ const obj = callee.object;
387
+ const prop = callee.property;
388
+ if (obj && prop) {
389
+ const objName = obj.name;
390
+ const propName = prop.name;
391
+ if (objName && propName) return `${objName}.${propName}`;
392
+ }
393
+ }
394
+ return null;
395
+ }
396
+ function isFunctionNode(node) {
397
+ return node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression";
398
+ }
399
+ function countFunctionLines(node) {
400
+ if (!node.loc) return 0;
401
+ return node.loc.end.line - node.loc.start.line + 1;
402
+ }
403
+ function cyclomaticComplexity(funcNode) {
404
+ let complexity = 1;
405
+ traverse(funcNode, {
406
+ IfStatement() {
407
+ complexity++;
408
+ },
409
+ SwitchCase(node) {
410
+ if (node.test !== null) complexity++;
411
+ },
412
+ ConditionalExpression() {
413
+ complexity++;
414
+ },
415
+ LogicalExpression(node) {
416
+ if (node.operator === "&&" || node.operator === "||" || node.operator === "??") {
417
+ complexity++;
418
+ }
419
+ },
420
+ ForStatement() {
421
+ complexity++;
422
+ },
423
+ ForInStatement() {
424
+ complexity++;
425
+ },
426
+ ForOfStatement() {
427
+ complexity++;
428
+ },
429
+ WhileStatement() {
430
+ complexity++;
431
+ },
432
+ DoWhileStatement() {
433
+ complexity++;
434
+ },
435
+ CatchClause() {
436
+ complexity++;
437
+ }
438
+ });
439
+ return complexity;
440
+ }
441
+ function maxNestingDepth(ast, startDepth = 0) {
442
+ if (!ast || typeof ast !== "object") return startDepth;
443
+ const node = ast;
444
+ const nestingTypes = /* @__PURE__ */ new Set([
445
+ "IfStatement",
446
+ "ForStatement",
447
+ "ForInStatement",
448
+ "ForOfStatement",
449
+ "WhileStatement",
450
+ "DoWhileStatement",
451
+ "SwitchStatement",
452
+ "TryStatement",
453
+ "WithStatement"
454
+ ]);
455
+ let max = startDepth;
456
+ const walk = (n, depth) => {
457
+ if (!n || typeof n !== "object") return;
458
+ const nn = n;
459
+ const currentDepth = nestingTypes.has(nn.type) ? depth + 1 : depth;
460
+ if (currentDepth > max) max = currentDepth;
461
+ for (const key of Object.keys(nn)) {
462
+ if (key === "parent") continue;
463
+ const child = nn[key];
464
+ if (Array.isArray(child)) {
465
+ for (const item of child) walk(item, currentDepth);
466
+ } else if (child && typeof child === "object" && "type" in child) {
467
+ walk(child, currentDepth);
468
+ }
469
+ }
470
+ };
471
+ walk(node, startDepth);
472
+ return max;
473
+ }
474
+
475
+ // src/rules/ai-slop/empty-catch.ts
476
+ var rule = {
477
+ meta: {
478
+ id: "ai-slop/empty-catch",
479
+ name: "Empty Catch Block",
480
+ category: "ai-slop",
481
+ severity: "high",
482
+ confidence: "certain",
483
+ description: "Detects catch blocks that silently swallow errors.",
484
+ rationale: "Empty catch blocks are a hallmark of AI-generated code that handles the error path syntactically but not semantically. They hide failures, making bugs invisible in production.",
485
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/empty-catch",
486
+ fixable: false
487
+ },
488
+ create(context) {
489
+ traverse(context.ast, {
490
+ CatchClause(node) {
491
+ const body = node.body;
492
+ if (!body) return;
493
+ const bodyNode = body;
494
+ const statements = bodyNode.body ?? [];
495
+ const hasStatements = statements.some((stmt) => {
496
+ const s = stmt;
497
+ return s.type !== "EmptyStatement";
498
+ });
499
+ if (!hasStatements) {
500
+ context.report({
501
+ message: "Catch block is empty and silently swallows errors.",
502
+ explanation: "This catch block catches exceptions but does nothing with them. Errors are silently discarded, making it impossible to detect or diagnose failures.",
503
+ impact: "Silent error swallowing causes mysterious failures in production. Debugging becomes extremely difficult because the exception trail disappears.",
504
+ location: getLocation(node, context.filePath),
505
+ fix: {
506
+ description: "At minimum, log the error. Consider re-throwing if the caller should know about the failure.",
507
+ code: 'catch (err) {\n console.error("Unexpected error:", err);\n // or: throw err;\n}'
508
+ }
509
+ });
510
+ }
511
+ }
512
+ });
513
+ }
514
+ };
515
+ var empty_catch_default = rule;
516
+
517
+ // src/rules/ai-slop/todo-implementation.ts
518
+ var TODO_PATTERN = /\b(TODO|FIXME|HACK|XXX|TEMP|TEMPORARY|PLACEHOLDER|NOT IMPLEMENTED|NOT_IMPLEMENTED)\b/i;
519
+ var THROW_NOT_IMPLEMENTED = /not.?implemented|to.?do|todo/i;
520
+ var rule2 = {
521
+ meta: {
522
+ id: "ai-slop/todo-implementation",
523
+ name: "TODO / Placeholder Implementation",
524
+ category: "ai-slop",
525
+ severity: "high",
526
+ confidence: "high",
527
+ description: "Detects TODO comments, placeholder implementations, and unfinished code.",
528
+ rationale: 'AI code generators frequently produce functions with TODO bodies, placeholder returns, or throw new Error("Not implemented"). These skeletons look complete but break at runtime.',
529
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/todo-implementation",
530
+ fixable: false
531
+ },
532
+ create(context) {
533
+ const astWithComments = context.ast;
534
+ const comments = astWithComments.comments ?? [];
535
+ for (const comment of comments) {
536
+ const value = comment.value ?? "";
537
+ if (TODO_PATTERN.test(value)) {
538
+ context.report({
539
+ message: `${comment.type === "Line" ? "//" : "/*"} comment contains TODO/FIXME marker.`,
540
+ explanation: "This comment indicates unfinished work. In production builds, TODO markers represent missing functionality that was never implemented.",
541
+ impact: "Incomplete implementations cause runtime failures, data corruption, or silent no-ops when the unfinished code path is reached.",
542
+ location: getLocation(comment, context.filePath),
543
+ fix: {
544
+ description: "Implement the described functionality or create a tracked issue and remove the inline marker."
545
+ }
546
+ });
547
+ }
548
+ }
549
+ traverse(context.ast, {
550
+ ThrowStatement(node) {
551
+ const argument = node.argument;
552
+ if (!argument) return;
553
+ if (argument.type === "NewExpression" || argument.type === "CallExpression") {
554
+ const callee = argument.callee;
555
+ if (!callee) return;
556
+ const calleeName = callee.type === "Identifier" ? callee.name : null;
557
+ if (calleeName !== "Error") return;
558
+ const args = argument.arguments ?? [];
559
+ if (args.length === 0) return;
560
+ const firstArg = args[0];
561
+ const msgValue = firstArg.type === "Literal" ? String(firstArg.value) : "";
562
+ if (THROW_NOT_IMPLEMENTED.test(msgValue)) {
563
+ context.report({
564
+ message: `Placeholder implementation: throw new Error("${msgValue}")`,
565
+ explanation: 'This function throws "Not implemented" or a similar placeholder. It was likely auto-generated and never completed.',
566
+ impact: "Calling this function in production causes an immediate unhandled exception.",
567
+ location: getLocation(node, context.filePath),
568
+ fix: {
569
+ description: "Implement the function body or remove it from the codebase."
570
+ }
571
+ });
572
+ }
573
+ }
574
+ }
575
+ });
576
+ }
577
+ };
578
+ var todo_implementation_default = rule2;
579
+
580
+ // src/rules/ai-slop/giant-function.ts
581
+ var MAX_LINES = 80;
582
+ var rule3 = {
583
+ meta: {
584
+ id: "ai-slop/giant-function",
585
+ name: "Giant Function",
586
+ category: "ai-slop",
587
+ severity: "medium",
588
+ confidence: "certain",
589
+ description: `Detects functions exceeding ${MAX_LINES} lines, a common sign of AI-generated monoliths.`,
590
+ rationale: "AI code generators tend to dump entire workflows into single functions without decomposing them. Functions longer than 80 lines are difficult to understand, test, and maintain.",
591
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/giant-function",
592
+ fixable: false
593
+ },
594
+ create(context) {
595
+ traverse(context.ast, {
596
+ FunctionDeclaration: checkFunction,
597
+ FunctionExpression: checkFunction,
598
+ ArrowFunctionExpression: checkFunction
599
+ });
600
+ function checkFunction(node) {
601
+ if (!isFunctionNode(node)) return;
602
+ const lines = countFunctionLines(node);
603
+ if (lines <= MAX_LINES) return;
604
+ const idNode = node.id;
605
+ const name = idNode ? String(idNode.name) : "<anonymous>";
606
+ context.report({
607
+ message: `Function "${name}" is ${lines} lines long (limit: ${MAX_LINES}).`,
608
+ explanation: `This function is ${lines} lines long. Long functions are a strong indicator of AI-generated code that placed all logic in one place without decomposition.`,
609
+ impact: "Giant functions are untestable, unreadable, and violate the single-responsibility principle. They make bug-fixing and code review slow and error-prone.",
610
+ location: getLocation(node, context.filePath),
611
+ fix: {
612
+ description: "Break the function into smaller, focused functions. Aim for functions under 40 lines that do one thing well."
613
+ },
614
+ metadata: {
615
+ lines
616
+ }
617
+ });
618
+ }
619
+ }
620
+ };
621
+ var giant_function_default = rule3;
622
+
623
+ // src/rules/ai-slop/excessive-nesting.ts
624
+ var MAX_DEPTH = 4;
625
+ var rule4 = {
626
+ meta: {
627
+ id: "ai-slop/excessive-nesting",
628
+ name: "Excessive Nesting",
629
+ category: "ai-slop",
630
+ severity: "medium",
631
+ confidence: "certain",
632
+ description: `Detects functions with control-flow nesting deeper than ${MAX_DEPTH} levels.`,
633
+ rationale: "Deeply nested code is a strong sign of AI-generated logic that was not refactored. Early returns, guard clauses, and extracted helper functions eliminate deep nesting.",
634
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/excessive-nesting",
635
+ fixable: false
636
+ },
637
+ create(context) {
638
+ traverse(context.ast, {
639
+ FunctionDeclaration: checkFunction,
640
+ FunctionExpression: checkFunction,
641
+ ArrowFunctionExpression: checkFunction
642
+ });
643
+ function checkFunction(node) {
644
+ if (!isFunctionNode(node)) return;
645
+ const depth = maxNestingDepth(node.body, 0);
646
+ if (depth <= MAX_DEPTH) return;
647
+ const idNode = node.id;
648
+ const name = idNode ? String(idNode.name) : "<anonymous>";
649
+ context.report({
650
+ message: `Function "${name}" has nesting depth of ${depth} (limit: ${MAX_DEPTH}).`,
651
+ explanation: `This function contains ${depth} levels of nested blocks. Each additional level of nesting makes logic exponentially harder to follow.`,
652
+ impact: "Deeply nested code is error-prone, difficult to test, and nearly impossible to review. Bugs introduced in deep nesting often escape code review.",
653
+ location: getLocation(node, context.filePath),
654
+ fix: {
655
+ description: "Use early returns to flatten nesting. Extract deeply-nested logic into named helper functions. Consider inversion of conditions to reduce if/else chains."
656
+ },
657
+ metadata: { depth }
658
+ });
659
+ }
660
+ }
661
+ };
662
+ var excessive_nesting_default = rule4;
663
+
664
+ // src/rules/ai-slop/fake-validation.ts
665
+ var VALIDATION_NAME_PATTERN = /^(validate|isValid|check|verify|sanitize|assert)/i;
666
+ var rule5 = {
667
+ meta: {
668
+ id: "ai-slop/fake-validation",
669
+ name: "Fake Validation",
670
+ category: "ai-slop",
671
+ severity: "high",
672
+ confidence: "medium",
673
+ description: "Detects validation functions that trivially return true without meaningful checks.",
674
+ rationale: "AI code generators often produce validation functions with names that imply safety but bodies that unconditionally approve all input. This creates a false sense of security.",
675
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/fake-validation",
676
+ fixable: false
677
+ },
678
+ create(context) {
679
+ traverse(context.ast, {
680
+ FunctionDeclaration: checkFunction,
681
+ FunctionExpression: checkFunction,
682
+ ArrowFunctionExpression: checkFunction
683
+ });
684
+ function checkFunction(node) {
685
+ const idNode = node.id ?? node.parent?.id;
686
+ let name = null;
687
+ if (idNode?.type === "Identifier") {
688
+ name = String(idNode.name);
689
+ }
690
+ const parent = node.parent;
691
+ if (!name && parent?.type === "VariableDeclarator") {
692
+ const idPart = parent.id;
693
+ if (idPart?.type === "Identifier") {
694
+ name = String(idPart.name);
695
+ }
696
+ }
697
+ if (!name || !VALIDATION_NAME_PATTERN.test(name)) return;
698
+ const body = node.body;
699
+ if (!body) return;
700
+ if (body.type !== "BlockStatement") {
701
+ if (body.type === "Literal" && body.value === true) {
702
+ reportFakeValidation(node, name);
703
+ }
704
+ return;
705
+ }
706
+ const statements = body.body ?? [];
707
+ if (statements.length === 1) {
708
+ const stmt = statements[0];
709
+ if (stmt.type === "ReturnStatement") {
710
+ const arg = stmt.argument;
711
+ if (arg?.type === "Literal" && arg.value === true) {
712
+ reportFakeValidation(node, name);
713
+ }
714
+ }
715
+ }
716
+ }
717
+ function reportFakeValidation(node, name) {
718
+ context.report({
719
+ message: `"${name}" appears to be a fake validation function that always returns true.`,
720
+ explanation: `The function "${name}" has a name suggesting it performs validation, but its body unconditionally returns true. All inputs will pass validation.`,
721
+ impact: "Fake validation creates false security. Malicious or malformed input bypasses what appears to be a safety check, potentially causing data corruption or security breaches.",
722
+ location: getLocation(node, context.filePath),
723
+ fix: {
724
+ description: "Implement real validation logic that checks the input against expected constraints. Use a validation library such as zod, joi, or yup for complex schemas."
725
+ }
726
+ });
727
+ }
728
+ }
729
+ };
730
+ var fake_validation_default = rule5;
731
+
732
+ // src/rules/ai-slop/high-complexity.ts
733
+ var COMPLEXITY_THRESHOLD = 10;
734
+ var COMPLEXITY_HIGH = 20;
735
+ var rule6 = {
736
+ meta: {
737
+ id: "ai-slop/high-complexity",
738
+ name: "High Cyclomatic Complexity",
739
+ category: "ai-slop",
740
+ severity: "medium",
741
+ confidence: "certain",
742
+ description: `Detects functions with cyclomatic complexity exceeding ${COMPLEXITY_THRESHOLD}.`,
743
+ rationale: "High complexity is a reliable indicator of AI-generated code that concatenated multiple responsibilities into a single function. Complex functions are difficult to test and almost impossible to reason about correctly.",
744
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/high-complexity",
745
+ fixable: false
746
+ },
747
+ create(context) {
748
+ traverse(context.ast, {
749
+ FunctionDeclaration: checkFunction,
750
+ FunctionExpression: checkFunction,
751
+ ArrowFunctionExpression: checkFunction
752
+ });
753
+ function checkFunction(node) {
754
+ if (!isFunctionNode(node)) return;
755
+ const complexity = cyclomaticComplexity(node);
756
+ if (complexity <= COMPLEXITY_THRESHOLD) return;
757
+ const idNode = node.id;
758
+ const name = idNode ? String(idNode.name) : "<anonymous>";
759
+ const isHigh = complexity >= COMPLEXITY_HIGH;
760
+ context.report({
761
+ message: `Function "${name}" has cyclomatic complexity of ${complexity} (limit: ${COMPLEXITY_THRESHOLD}).`,
762
+ explanation: `Cyclomatic complexity measures the number of independent paths through code. A complexity of ${complexity} means this function has at least ${complexity} independent execution paths that all need to be understood and tested.`,
763
+ impact: isHigh ? "Extreme complexity. This function cannot be reliably tested or maintained. Defects introduced here will be very difficult to locate." : "High complexity makes this function difficult to test thoroughly. Each untested path is a potential production defect.",
764
+ location: getLocation(node, context.filePath),
765
+ fix: {
766
+ description: "Extract independent logic branches into well-named helper functions. Aim for functions with complexity under 5. Consider strategy or command patterns for functions that switch on a type discriminant."
767
+ },
768
+ metadata: { complexity }
769
+ });
770
+ }
771
+ }
772
+ };
773
+ var high_complexity_default = rule6;
774
+
775
+ // src/rules/ai-slop/dead-code.ts
776
+ var rule7 = {
777
+ meta: {
778
+ id: "ai-slop/dead-code",
779
+ name: "Dead Code After Return",
780
+ category: "ai-slop",
781
+ severity: "medium",
782
+ confidence: "certain",
783
+ description: "Detects unreachable code that follows a return, throw, break, or continue statement.",
784
+ rationale: "Dead code after early exits is a common artifact of AI code generation. It indicates that the generator added logic without tracking control flow, producing code that never executes.",
785
+ docsUrl: "https://clean-slop.dev/docs/rules/ai-slop/dead-code",
786
+ fixable: true
787
+ },
788
+ create(context) {
789
+ const terminatingTypes = /* @__PURE__ */ new Set([
790
+ "ReturnStatement",
791
+ "ThrowStatement",
792
+ "BreakStatement",
793
+ "ContinueStatement"
794
+ ]);
795
+ const blocks = findAll(context.ast, "BlockStatement");
796
+ for (const block of blocks) {
797
+ const statements = block.body ?? [];
798
+ for (let i = 0; i < statements.length - 1; i++) {
799
+ const stmt = statements[i];
800
+ if (!stmt) continue;
801
+ if (terminatingTypes.has(stmt.type)) {
802
+ const nextStmt = statements[i + 1];
803
+ if (!nextStmt) continue;
804
+ if (nextStmt.type === "IfStatement") continue;
805
+ context.report({
806
+ message: `Unreachable code after ${stmt.type}.`,
807
+ explanation: `The statement at this line is unreachable because a ${stmt.type} on the previous line will always exit this block before this code can execute.`,
808
+ impact: "Dead code bloats the bundle, misleads maintainers, and indicates logic errors. Code that was intended to run but is unreachable represents a latent bug.",
809
+ location: getLocation(nextStmt, context.filePath),
810
+ fix: {
811
+ description: "Remove the unreachable code. If it was intended to run, move it before the terminating statement."
812
+ }
813
+ });
814
+ break;
815
+ }
816
+ }
817
+ }
818
+ }
819
+ };
820
+ var dead_code_default = rule7;
821
+
822
+ // src/rules/security/unsafe-eval.ts
823
+ var DANGEROUS_EVAL_APIS = /* @__PURE__ */ new Set([
824
+ "eval",
825
+ "Function",
826
+ "execScript"
827
+ ]);
828
+ var rule8 = {
829
+ meta: {
830
+ id: "security/unsafe-eval",
831
+ name: "Unsafe eval / Function Constructor",
832
+ category: "security",
833
+ severity: "critical",
834
+ confidence: "high",
835
+ description: "Detects use of eval() and the Function constructor, which execute arbitrary code.",
836
+ rationale: "eval() and new Function() execute strings as JavaScript code. When any part of the string comes from user input or external data, this is a code injection vulnerability.",
837
+ docsUrl: "https://clean-slop.dev/docs/rules/security/unsafe-eval",
838
+ fixable: false
839
+ },
840
+ create(context) {
841
+ traverse(context.ast, {
842
+ CallExpression(node) {
843
+ const name = getCalleeName(node);
844
+ if (name && DANGEROUS_EVAL_APIS.has(name)) {
845
+ context.report({
846
+ message: `Unsafe use of ${name}().`,
847
+ explanation: `${name}() evaluates its argument as JavaScript code at runtime. If any part of the evaluated string is derived from external data, an attacker can inject and execute arbitrary code.`,
848
+ impact: "Code injection via eval is one of the highest-severity vulnerabilities in web applications. It allows complete application compromise, data exfiltration, and remote code execution.",
849
+ location: getLocation(node, context.filePath),
850
+ fix: {
851
+ description: "Replace eval() with explicit logic. If you need to parse JSON, use JSON.parse(). If you need dynamic behavior, use a lookup table or strategy pattern."
852
+ }
853
+ });
854
+ }
855
+ },
856
+ NewExpression(node) {
857
+ const callee = node.callee;
858
+ if (callee?.type === "Identifier" && callee.name === "Function") {
859
+ context.report({
860
+ message: "Unsafe use of new Function() constructor.",
861
+ explanation: "new Function() constructs and executes JavaScript from a string at runtime. This is functionally equivalent to eval() and shares the same injection risks.",
862
+ impact: "Allows arbitrary code execution if the string argument contains any external data. Bypasses Content Security Policy directives and static analysis.",
863
+ location: getLocation(node, context.filePath),
864
+ fix: {
865
+ description: "Eliminate the dynamic code construction. Use explicit functions, closures, or a sandboxed evaluation library (e.g., vm2) if dynamic execution is genuinely required."
866
+ }
867
+ });
868
+ }
869
+ }
870
+ });
871
+ }
872
+ };
873
+ var unsafe_eval_default = rule8;
874
+
875
+ // src/rules/security/hardcoded-secrets.ts
876
+ var SECRET_PATTERNS = [
877
+ {
878
+ name: "API Key",
879
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9_-]{20,}['"]/i,
880
+ description: "hardcoded API key"
881
+ },
882
+ {
883
+ name: "AWS Access Key",
884
+ pattern: /AKIA[0-9A-Z]{16}/,
885
+ description: "AWS access key ID"
886
+ },
887
+ {
888
+ name: "AWS Secret",
889
+ pattern: /aws[_-]?secret[_-]?(?:access[_-]?)?key\s*[:=]\s*['"][^'"]{20,}['"]/i,
890
+ description: "AWS secret access key"
891
+ },
892
+ {
893
+ name: "Private Key",
894
+ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/,
895
+ description: "PEM private key"
896
+ },
897
+ {
898
+ name: "Password",
899
+ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{4,}['"]/i,
900
+ description: "hardcoded password"
901
+ },
902
+ {
903
+ name: "JWT Secret",
904
+ pattern: /(?:jwt[_-]?secret|token[_-]?secret)\s*[:=]\s*['"][^'"]{8,}['"]/i,
905
+ description: "hardcoded JWT signing secret"
906
+ },
907
+ {
908
+ name: "Database URL",
909
+ pattern: /(?:mongo(?:db)?|postgres(?:ql)?|mysql|redis):\/\/[^:'"]+:[^@'"]+@/i,
910
+ description: "database connection string with credentials"
911
+ },
912
+ {
913
+ name: "Generic Secret",
914
+ pattern: /(?:secret|token)\s*[:=]\s*['"][a-zA-Z0-9+/=_-]{20,}['"]/i,
915
+ description: "hardcoded secret or token"
916
+ },
917
+ {
918
+ name: "GitHub Token",
919
+ pattern: /ghp_[a-zA-Z0-9]{36}/,
920
+ description: "GitHub personal access token"
921
+ },
922
+ {
923
+ name: "Stripe Key",
924
+ pattern: /(?:sk|pk)_(?:live|test)_[a-zA-Z0-9]{24,}/,
925
+ description: "Stripe API key"
926
+ }
927
+ ];
928
+ var PLACEHOLDER_PATTERNS = [
929
+ /^(?:your[-_]?)?(?:api[-_]?)?(?:key|secret|token|password)$/i,
930
+ /^<.+>$/,
931
+ /^xxx/i,
932
+ /^placeholder/i,
933
+ /^\*+$/,
934
+ /^test$/i,
935
+ /^example$/i,
936
+ /^change[-_]?me/i,
937
+ /process\.env\./
938
+ ];
939
+ function isPlaceholder(value) {
940
+ return PLACEHOLDER_PATTERNS.some((p) => p.test(value));
941
+ }
942
+ var rule9 = {
943
+ meta: {
944
+ id: "security/hardcoded-secrets",
945
+ name: "Hardcoded Secrets",
946
+ category: "security",
947
+ severity: "critical",
948
+ confidence: "high",
949
+ description: "Detects API keys, passwords, tokens, and secrets hardcoded in source files.",
950
+ rationale: "Hardcoded credentials are one of the most common causes of security breaches. When code is committed to version control, credentials are permanently exposed in history even if removed later.",
951
+ docsUrl: "https://clean-slop.dev/docs/rules/security/hardcoded-secrets",
952
+ fixable: false
953
+ },
954
+ create(context) {
955
+ const lines = context.source.split("\n");
956
+ lines.forEach((line, index) => {
957
+ for (const { name, pattern, description } of SECRET_PATTERNS) {
958
+ if (!pattern.test(line)) continue;
959
+ const valueMatch = line.match(/[:=]\s*['"]([^'"]+)['"]/);
960
+ if (valueMatch && valueMatch[1] && isPlaceholder(valueMatch[1])) continue;
961
+ context.report({
962
+ message: `Possible hardcoded ${name} detected.`,
963
+ explanation: `A potential ${description} was found hardcoded in source. Credentials in source files are committed to version control and accessible to anyone with repository access, including in private repositories through history and forks.`,
964
+ impact: "Exposed credentials can be used to access cloud infrastructure, databases, and third-party services. Even after removal, they remain in git history. Rotation is required immediately.",
965
+ location: {
966
+ file: context.filePath,
967
+ line: index + 1,
968
+ column: 0
969
+ },
970
+ fix: {
971
+ description: "Remove the credential from source. Store secrets in environment variables (process.env.SECRET_NAME) or a secrets manager (AWS Secrets Manager, HashiCorp Vault, Doppler). Rotate the exposed credential immediately.",
972
+ code: "const apiKey = process.env.API_KEY;\nif (!apiKey) throw new Error('API_KEY environment variable is required.');"
973
+ }
974
+ });
975
+ break;
976
+ }
977
+ });
978
+ traverse2(context.ast, {
979
+ Literal(node) {
980
+ if (typeof node.value !== "string") return;
981
+ const val = node.value;
982
+ if (val.length < 20) return;
983
+ if (isPlaceholder(val)) return;
984
+ for (const { name, pattern, description } of SECRET_PATTERNS) {
985
+ if (pattern.test(val)) {
986
+ context.report({
987
+ message: `String literal appears to be a hardcoded ${name}.`,
988
+ explanation: `The string "${val.slice(0, 8)}..." matches a pattern for a ${description}. Hardcoding credentials in string literals exposes them in source control.`,
989
+ impact: "Immediate rotation of the credential is required. All historical commits containing this value must be treated as compromised.",
990
+ location: getLocation2(node, context.filePath),
991
+ fix: {
992
+ description: "Replace with an environment variable reference: process.env.SECRET_NAME"
993
+ }
994
+ });
995
+ break;
996
+ }
997
+ }
998
+ }
999
+ });
1000
+ function traverse2(ast, visitor) {
1001
+ if (!ast || typeof ast !== "object") return;
1002
+ const node = ast;
1003
+ if (typeof node.type === "string") {
1004
+ visitor[node.type]?.(node);
1005
+ }
1006
+ for (const key of Object.keys(node)) {
1007
+ if (key === "parent") continue;
1008
+ const child = node[key];
1009
+ if (Array.isArray(child)) {
1010
+ for (const item of child) traverse2(item, visitor);
1011
+ } else if (child && typeof child === "object" && "type" in child) {
1012
+ traverse2(child, visitor);
1013
+ }
1014
+ }
1015
+ }
1016
+ function getLocation2(node, filePath) {
1017
+ return {
1018
+ file: filePath,
1019
+ line: node.loc?.start.line ?? 1,
1020
+ column: node.loc?.start.column ?? 0
1021
+ };
1022
+ }
1023
+ }
1024
+ };
1025
+ var hardcoded_secrets_default = rule9;
1026
+
1027
+ // src/rules/security/sql-injection.ts
1028
+ var SQL_KEYWORDS = /\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|EXEC|EXECUTE|UNION|FROM|WHERE|JOIN)\b/i;
1029
+ var SQL_CALL_PATTERNS = [
1030
+ "query",
1031
+ "execute",
1032
+ "exec",
1033
+ "run",
1034
+ "prepare",
1035
+ "raw",
1036
+ "db.query",
1037
+ "db.execute",
1038
+ "pool.query",
1039
+ "connection.query",
1040
+ "knex.raw",
1041
+ "sequelize.query"
1042
+ ];
1043
+ function looksLikeSqlCall(node) {
1044
+ const callee = node.callee;
1045
+ if (!callee) return false;
1046
+ if (callee.type === "Identifier") {
1047
+ return SQL_CALL_PATTERNS.includes(String(callee.name));
1048
+ }
1049
+ if (callee.type === "MemberExpression") {
1050
+ const prop = callee.property;
1051
+ if (prop?.type === "Identifier") {
1052
+ return SQL_CALL_PATTERNS.some((p) => p.endsWith(`.${prop.name}`) || p === prop.name);
1053
+ }
1054
+ }
1055
+ return false;
1056
+ }
1057
+ function containsSqlKeywords(str) {
1058
+ return SQL_KEYWORDS.test(str);
1059
+ }
1060
+ function getTemplateRawContent(node) {
1061
+ const quasis = node.quasis ?? [];
1062
+ return quasis.map((q) => {
1063
+ const val = q.value;
1064
+ return String(val?.cooked ?? val?.raw ?? "");
1065
+ }).join("...");
1066
+ }
1067
+ var rule10 = {
1068
+ meta: {
1069
+ id: "security/sql-injection",
1070
+ name: "SQL Injection",
1071
+ category: "security",
1072
+ severity: "critical",
1073
+ confidence: "high",
1074
+ description: "Detects SQL queries built with string concatenation or template literals containing variables.",
1075
+ rationale: "SQL injection remains the most prevalent web application vulnerability. Embedding JavaScript variables directly into SQL strings gives attackers control over query structure.",
1076
+ docsUrl: "https://clean-slop.dev/docs/rules/security/sql-injection",
1077
+ fixable: false
1078
+ },
1079
+ create(context) {
1080
+ traverse(context.ast, {
1081
+ CallExpression(node) {
1082
+ if (!looksLikeSqlCall(node)) return;
1083
+ const args = node.arguments ?? [];
1084
+ const firstArg = args[0];
1085
+ if (!firstArg) return;
1086
+ if (firstArg.type === "TemplateLiteral") {
1087
+ const expressions = firstArg.expressions ?? [];
1088
+ if (expressions.length === 0) return;
1089
+ const rawContent = getTemplateRawContent(firstArg);
1090
+ if (!containsSqlKeywords(rawContent)) return;
1091
+ context.report({
1092
+ message: "Possible SQL injection: template literal with expressions passed to query function.",
1093
+ explanation: "This SQL query is built using a template literal that embeds JavaScript variables. If any of the interpolated values derive from user input, an attacker can alter the query structure by injecting SQL syntax.",
1094
+ impact: "SQL injection can lead to unauthorized data access, data modification, authentication bypass, and in some configurations, remote code execution on the database server.",
1095
+ location: getLocation(node, context.filePath),
1096
+ fix: {
1097
+ description: "Use parameterized queries or prepared statements. Pass user-controlled values as parameters, never as part of the SQL string.",
1098
+ code: '// Instead of:\n// db.query(`SELECT * FROM users WHERE id = ${userId}`)\n\n// Use parameterized queries:\ndb.query("SELECT * FROM users WHERE id = ?", [userId])'
1099
+ }
1100
+ });
1101
+ }
1102
+ if (firstArg.type === "BinaryExpression" && firstArg.operator === "+") {
1103
+ const leftStr = extractLeftmostString(firstArg);
1104
+ if (leftStr && containsSqlKeywords(leftStr)) {
1105
+ context.report({
1106
+ message: "Possible SQL injection: string concatenation in SQL query.",
1107
+ explanation: "This SQL query is constructed by concatenating strings and variables. Variables injected into a SQL string allow attackers to manipulate the query.",
1108
+ impact: "SQL injection can expose the entire database, bypass authentication, and enable destructive operations (DROP TABLE, etc.).",
1109
+ location: getLocation(node, context.filePath),
1110
+ fix: {
1111
+ description: "Replace string concatenation with parameterized queries. Never construct SQL from variables.",
1112
+ code: 'db.query(\n "SELECT * FROM users WHERE name = ? AND role = ?",\n [userName, userRole]\n)'
1113
+ }
1114
+ });
1115
+ }
1116
+ }
1117
+ }
1118
+ });
1119
+ function extractLeftmostString(node) {
1120
+ if (node.type === "Literal" && typeof node.value === "string") {
1121
+ return node.value;
1122
+ }
1123
+ if (node.type === "BinaryExpression" && node.operator === "+") {
1124
+ return extractLeftmostString(node.left);
1125
+ }
1126
+ return null;
1127
+ }
1128
+ }
1129
+ };
1130
+ var sql_injection_default = rule10;
1131
+
1132
+ // src/rules/security/command-injection.ts
1133
+ var DANGEROUS_CHILD_PROCESS = /* @__PURE__ */ new Set([
1134
+ "exec",
1135
+ "execSync",
1136
+ "spawn",
1137
+ "spawnSync",
1138
+ "execFile",
1139
+ "execFileSync"
1140
+ ]);
1141
+ function looksLikeChildProcessCall(node) {
1142
+ const callee = node.callee;
1143
+ if (!callee) return false;
1144
+ if (callee.type === "MemberExpression") {
1145
+ const prop = callee.property;
1146
+ if (prop?.type === "Identifier") {
1147
+ return DANGEROUS_CHILD_PROCESS.has(String(prop.name));
1148
+ }
1149
+ }
1150
+ if (callee.type === "Identifier") {
1151
+ return DANGEROUS_CHILD_PROCESS.has(String(callee.name));
1152
+ }
1153
+ return false;
1154
+ }
1155
+ function firstArgIsDynamic(node) {
1156
+ const args = node.arguments ?? [];
1157
+ const first = args[0];
1158
+ if (!first) return false;
1159
+ if (first.type === "Literal") return false;
1160
+ if (first.type === "TemplateLiteral") {
1161
+ const expressions = first.expressions ?? [];
1162
+ return expressions.length > 0;
1163
+ }
1164
+ return true;
1165
+ }
1166
+ var rule11 = {
1167
+ meta: {
1168
+ id: "security/command-injection",
1169
+ name: "Command Injection",
1170
+ category: "security",
1171
+ severity: "critical",
1172
+ confidence: "high",
1173
+ description: "Detects child_process calls that build shell commands from variables, enabling command injection.",
1174
+ rationale: "Passing user-controlled data to exec(), spawn(), or similar APIs allows an attacker to inject shell metacharacters that execute arbitrary system commands.",
1175
+ docsUrl: "https://clean-slop.dev/docs/rules/security/command-injection",
1176
+ fixable: false
1177
+ },
1178
+ create(context) {
1179
+ traverse(context.ast, {
1180
+ CallExpression(node) {
1181
+ if (!looksLikeChildProcessCall(node)) return;
1182
+ if (!firstArgIsDynamic(node)) return;
1183
+ const callee = node.callee;
1184
+ const prop = callee.type === "MemberExpression" ? callee.property.name : callee.name;
1185
+ context.report({
1186
+ message: `Possible command injection: dynamic argument passed to child_process.${prop}().`,
1187
+ explanation: `The first argument to ${prop}() appears to be dynamically constructed. If any part of this argument derives from external input, an attacker can inject shell metacharacters (e.g. ; rm -rf /) to execute arbitrary commands on the server.`,
1188
+ impact: "Command injection on a server results in full system compromise: data theft, ransomware deployment, lateral movement, and persistent access.",
1189
+ location: getLocation(node, context.filePath),
1190
+ fix: {
1191
+ description: "Use execFile() or spawn() with an argument array instead of exec() with a shell string. Never pass user input to a shell command. Validate and whitelist all inputs.",
1192
+ code: "// Dangerous:\n// exec(`ls ${userInput}`)\n\n// Safe:\nimport { execFile } from 'child_process';\nexecFile('ls', [sanitizedPath], callback);"
1193
+ }
1194
+ });
1195
+ }
1196
+ });
1197
+ }
1198
+ };
1199
+ var command_injection_default = rule11;
1200
+
1201
+ // src/rules/security/path-traversal.ts
1202
+ var FS_WRITE_METHODS = /* @__PURE__ */ new Set([
1203
+ "readFile",
1204
+ "readFileSync",
1205
+ "writeFile",
1206
+ "writeFileSync",
1207
+ "readdir",
1208
+ "readdirSync",
1209
+ "createReadStream",
1210
+ "createWriteStream",
1211
+ "unlink",
1212
+ "unlinkSync",
1213
+ "stat",
1214
+ "statSync",
1215
+ "access",
1216
+ "accessSync",
1217
+ "open",
1218
+ "openSync"
1219
+ ]);
1220
+ function isFsCallWithDynamicPath(node) {
1221
+ const callee = node.callee;
1222
+ if (!callee) return false;
1223
+ let methodName = null;
1224
+ if (callee.type === "MemberExpression") {
1225
+ const prop = callee.property;
1226
+ if (prop?.type === "Identifier") methodName = String(prop.name);
1227
+ } else if (callee.type === "Identifier") {
1228
+ methodName = String(callee.name);
1229
+ }
1230
+ if (!methodName || !FS_WRITE_METHODS.has(methodName)) return false;
1231
+ const args = node.arguments ?? [];
1232
+ const first = args[0];
1233
+ if (!first) return false;
1234
+ return first.type !== "Literal";
1235
+ }
1236
+ var rule12 = {
1237
+ meta: {
1238
+ id: "security/path-traversal",
1239
+ name: "Path Traversal",
1240
+ category: "security",
1241
+ severity: "critical",
1242
+ confidence: "medium",
1243
+ description: "Detects file system operations with dynamic paths that may allow path traversal attacks.",
1244
+ rationale: 'Path traversal occurs when an attacker uses "../" sequences in user-controlled input to escape the intended directory and access arbitrary files.',
1245
+ docsUrl: "https://clean-slop.dev/docs/rules/security/path-traversal",
1246
+ fixable: false
1247
+ },
1248
+ create(context) {
1249
+ traverse(context.ast, {
1250
+ CallExpression(node) {
1251
+ if (isFsCallWithDynamicPath(node)) {
1252
+ const callee = node.callee;
1253
+ const prop = callee.type === "MemberExpression" ? String(callee.property.name) : String(callee.name);
1254
+ context.report({
1255
+ message: `Possible path traversal: dynamic path passed to fs.${prop}().`,
1256
+ explanation: `${prop}() is called with a non-literal path argument. If this path is influenced by user input without sanitization, an attacker can use "../" sequences to escape the intended directory.`,
1257
+ impact: "Successful path traversal allows reading sensitive system files (/etc/passwd, .env, private keys), writing malicious content to arbitrary locations, or deleting critical files.",
1258
+ location: getLocation(node, context.filePath),
1259
+ fix: {
1260
+ description: "Resolve the path and verify it begins with the intended base directory before use.",
1261
+ code: "import path from 'path';\n\nconst baseDir = path.resolve('./uploads');\nconst safePath = path.resolve(baseDir, userInput);\n\nif (!safePath.startsWith(baseDir + path.sep)) {\n throw new Error('Path traversal detected');\n}\n\nfs.readFile(safePath, 'utf-8');"
1262
+ }
1263
+ });
1264
+ }
1265
+ }
1266
+ });
1267
+ }
1268
+ };
1269
+ var path_traversal_default = rule12;
1270
+
1271
+ // src/rules/security/prototype-pollution.ts
1272
+ var MERGE_FUNCTIONS = /* @__PURE__ */ new Set([
1273
+ "Object.assign",
1274
+ "_.merge",
1275
+ "_.extend",
1276
+ "_.defaultsDeep",
1277
+ "merge",
1278
+ "deepMerge",
1279
+ "deepExtend"
1280
+ ]);
1281
+ function getFullCalleeName(node) {
1282
+ const callee = node.callee;
1283
+ if (!callee) return null;
1284
+ if (callee.type === "Identifier") return String(callee.name);
1285
+ if (callee.type === "MemberExpression") {
1286
+ const obj = callee.object;
1287
+ const prop = callee.property;
1288
+ if (obj?.type === "Identifier" && prop?.type === "Identifier") {
1289
+ return `${obj.name}.${prop.name}`;
1290
+ }
1291
+ }
1292
+ return null;
1293
+ }
1294
+ var rule13 = {
1295
+ meta: {
1296
+ id: "security/prototype-pollution",
1297
+ name: "Prototype Pollution",
1298
+ category: "security",
1299
+ severity: "high",
1300
+ confidence: "medium",
1301
+ description: "Detects patterns that may allow attackers to pollute Object.prototype.",
1302
+ rationale: "Prototype pollution allows an attacker to inject properties into Object.prototype, affecting all objects in the application. This can bypass security checks, cause denial of service, or enable remote code execution.",
1303
+ docsUrl: "https://clean-slop.dev/docs/rules/security/prototype-pollution",
1304
+ fixable: false
1305
+ },
1306
+ create(context) {
1307
+ traverse(context.ast, {
1308
+ // Detect: obj[variable] = value where variable could be __proto__
1309
+ AssignmentExpression(node) {
1310
+ const left = node.left;
1311
+ if (!left) return;
1312
+ if (left.type !== "MemberExpression") return;
1313
+ if (!left.computed) return;
1314
+ const prop = left.property;
1315
+ if (!prop) return;
1316
+ if (prop.type === "Literal") {
1317
+ const key = String(prop.value);
1318
+ if (key === "__proto__" || key === "constructor" || key === "prototype") {
1319
+ context.report({
1320
+ message: `Assignment to dangerous property "${key}" may cause prototype pollution.`,
1321
+ explanation: `Assigning to obj["${key}"] can modify Object.prototype when "obj" is a user-controlled JSON payload or when the key is not validated.`,
1322
+ impact: "Prototype pollution can override security checks that compare against default values, corrupt application state, and in some environments enable code execution.",
1323
+ location: getLocation(node, context.filePath),
1324
+ fix: {
1325
+ description: "Never assign to __proto__, constructor, or prototype from external input. Validate object keys against an allowlist before assignment."
1326
+ }
1327
+ });
1328
+ }
1329
+ } else {
1330
+ context.report({
1331
+ message: "Dynamic bracket assignment may allow prototype pollution if key is not validated.",
1332
+ explanation: "Writing to obj[dynamicKey] with an unvalidated key allows an attacker to set __proto__ or constructor, polluting Object.prototype for all objects.",
1333
+ impact: "If the key comes from user-controlled input (query params, JSON body, headers), this is an exploitable prototype pollution vulnerability.",
1334
+ location: getLocation(node, context.filePath),
1335
+ fix: {
1336
+ description: "Validate the key against an allowlist before assignment: if (ALLOWED_KEYS.has(key)) obj[key] = value; Or use Object.create(null) for dictionary objects."
1337
+ }
1338
+ });
1339
+ }
1340
+ },
1341
+ // Detect Object.assign(target, untrustedSource) and lodash merge
1342
+ CallExpression(node) {
1343
+ const name = getFullCalleeName(node);
1344
+ if (!name || !MERGE_FUNCTIONS.has(name)) return;
1345
+ const args = node.arguments ?? [];
1346
+ if (args.length < 2) return;
1347
+ const source = args[1];
1348
+ if (source.type !== "ObjectExpression") {
1349
+ context.report({
1350
+ message: `${name}() with a non-literal source may introduce prototype pollution.`,
1351
+ explanation: `${name}() recursively copies properties from the source object. If the source derives from user input and contains __proto__ or constructor keys, it will pollute the target object's prototype chain.`,
1352
+ impact: "All objects in the application share Object.prototype. Polluting it can override security-relevant default values and cause widespread application misbehavior.",
1353
+ location: getLocation(node, context.filePath),
1354
+ fix: {
1355
+ description: `Sanitize the source before passing it to ${name}(). Use a library like defu or structuredClone with property filtering, or validate the source against a schema before merging.`
1356
+ }
1357
+ });
1358
+ }
1359
+ }
1360
+ });
1361
+ }
1362
+ };
1363
+ var prototype_pollution_default = rule13;
1364
+
1365
+ // src/rules/security/weak-crypto.ts
1366
+ var WEAK_HASH_ALGORITHMS = /* @__PURE__ */ new Set(["md5", "sha1", "sha-1", "md4", "rc4", "des", "3des"]);
1367
+ var WEAK_CIPHER_MODES = /* @__PURE__ */ new Set(["ecb", "cbc"]);
1368
+ var MATH_RANDOM_DESC = "Math.random() is a pseudo-random number generator not suitable for cryptographic use.";
1369
+ var rule14 = {
1370
+ meta: {
1371
+ id: "security/weak-crypto",
1372
+ name: "Weak Cryptography",
1373
+ category: "security",
1374
+ severity: "high",
1375
+ confidence: "high",
1376
+ description: "Detects use of weak or broken cryptographic algorithms (MD5, SHA1, ECB mode, Math.random).",
1377
+ rationale: "MD5 and SHA1 are cryptographically broken. Using them for password hashing or data integrity provides no real security. Math.random() is predictable and must not be used for tokens, keys, or any security-sensitive value.",
1378
+ docsUrl: "https://clean-slop.dev/docs/rules/security/weak-crypto",
1379
+ fixable: false
1380
+ },
1381
+ create(context) {
1382
+ traverse(context.ast, {
1383
+ CallExpression(node) {
1384
+ const callee = node.callee;
1385
+ if (!callee) return;
1386
+ if (callee.type === "MemberExpression" && callee.object?.type === "Identifier" && String(callee.object.name) === "Math" && callee.property?.type === "Identifier" && String(callee.property.name) === "random") {
1387
+ context.report({
1388
+ message: "Math.random() is not cryptographically secure.",
1389
+ explanation: MATH_RANDOM_DESC,
1390
+ impact: "Using Math.random() for security tokens, session IDs, or passwords produces predictable values that attackers can guess or enumerate.",
1391
+ location: getLocation(node, context.filePath),
1392
+ fix: {
1393
+ description: "Use the Web Crypto API or Node.js crypto module for secure random values.",
1394
+ code: "// Node.js:\nimport { randomBytes } from 'crypto';\nconst token = randomBytes(32).toString('hex');\n\n// Browser:\nconst array = new Uint8Array(32);\ncrypto.getRandomValues(array);"
1395
+ }
1396
+ });
1397
+ }
1398
+ if (callee.type === "MemberExpression" && callee.property?.type === "Identifier" && String(callee.property.name) === "createHash") {
1399
+ const args = node.arguments ?? [];
1400
+ const algoArg = args[0];
1401
+ if (algoArg?.type === "Literal") {
1402
+ const algo = String(algoArg.value).toLowerCase();
1403
+ if (WEAK_HASH_ALGORITHMS.has(algo)) {
1404
+ context.report({
1405
+ message: `Weak hash algorithm "${algoArg.value}" used in createHash().`,
1406
+ explanation: `${algoArg.value} is a broken hash algorithm. It is vulnerable to collision attacks and should not be used for data integrity checks, password hashing, or digital signatures.`,
1407
+ impact: "Broken hash algorithms allow attackers to forge hashes, bypass integrity checks, and crack password hashes rapidly using precomputed tables.",
1408
+ location: getLocation(node, context.filePath),
1409
+ fix: {
1410
+ description: "Replace with SHA-256 or SHA-3 for general hashing: crypto.createHash('sha256'). For password hashing, use bcrypt, argon2, or scrypt instead of any raw hash."
1411
+ }
1412
+ });
1413
+ }
1414
+ }
1415
+ }
1416
+ if (callee.type === "MemberExpression" && callee.property?.type === "Identifier" && String(callee.property.name) === "createCipheriv") {
1417
+ const args = node.arguments ?? [];
1418
+ const algoArg = args[0];
1419
+ if (algoArg?.type === "Literal") {
1420
+ const algo = String(algoArg.value).toLowerCase();
1421
+ if ([...WEAK_CIPHER_MODES].some((m) => algo.includes(m))) {
1422
+ context.report({
1423
+ message: `Insecure cipher mode detected: "${algoArg.value}".`,
1424
+ explanation: `AES-ECB produces identical ciphertext for identical plaintext blocks, revealing patterns in encrypted data. AES-CBC is also prone to padding oracle attacks.`,
1425
+ impact: "Insecure cipher modes can expose plaintext structure to passive observers and may be fully decryptable by an active attacker.",
1426
+ location: getLocation(node, context.filePath),
1427
+ fix: {
1428
+ description: "Use AES-GCM (Authenticated Encryption): crypto.createCipheriv('aes-256-gcm', key, iv). Always use a unique IV per encryption operation."
1429
+ }
1430
+ });
1431
+ }
1432
+ }
1433
+ }
1434
+ }
1435
+ });
1436
+ }
1437
+ };
1438
+ var weak_crypto_default = rule14;
1439
+
1440
+ // src/rules/security/dangerous-cors.ts
1441
+ function getPropertyValue(props, keyName) {
1442
+ for (const prop of props) {
1443
+ if (prop.type !== "Property") continue;
1444
+ const key = prop.key;
1445
+ const keyStr = key?.type === "Identifier" ? String(key.name) : key?.type === "Literal" ? String(key.value) : null;
1446
+ if (keyStr === keyName) {
1447
+ return prop.value;
1448
+ }
1449
+ }
1450
+ return void 0;
1451
+ }
1452
+ function getLiteralString(node) {
1453
+ if (!node) return null;
1454
+ if (node.type === "Literal" && typeof node.value === "string") return node.value;
1455
+ return null;
1456
+ }
1457
+ function getLiteralBool(node) {
1458
+ if (!node) return null;
1459
+ if (node.type === "Literal" && typeof node.value === "boolean") return node.value;
1460
+ return null;
1461
+ }
1462
+ var rule15 = {
1463
+ meta: {
1464
+ id: "security/dangerous-cors",
1465
+ name: "Dangerous CORS / Cookie Configuration",
1466
+ category: "security",
1467
+ severity: "high",
1468
+ confidence: "high",
1469
+ description: "Detects wildcard CORS origins, insecure cookie settings, and overly permissive configurations.",
1470
+ rationale: "CORS misconfiguration and insecure cookie attributes are among the most common security mistakes in Node.js backends. They enable CSRF, session hijacking, and cross-origin data leakage.",
1471
+ docsUrl: "https://clean-slop.dev/docs/rules/security/dangerous-cors",
1472
+ fixable: false
1473
+ },
1474
+ create(context) {
1475
+ traverse(context.ast, {
1476
+ CallExpression(node) {
1477
+ const callee = node.callee;
1478
+ if (!callee) return;
1479
+ const isCorsCall = callee.type === "Identifier" && String(callee.name) === "cors" || callee.type === "MemberExpression" && String(callee.property.name) === "cors";
1480
+ if (isCorsCall) {
1481
+ const args = node.arguments ?? [];
1482
+ const configArg = args[0];
1483
+ if (!configArg || configArg.type !== "ObjectExpression") return;
1484
+ const props = configArg.properties ?? [];
1485
+ const originNode = getPropertyValue(props, "origin");
1486
+ if (!originNode) return;
1487
+ const originStr = getLiteralString(originNode);
1488
+ const originBool = getLiteralBool(originNode);
1489
+ if (originStr === "*" || originBool === true) {
1490
+ context.report({
1491
+ message: `CORS configured with permissive origin: ${JSON.stringify(originStr ?? originBool)}.`,
1492
+ explanation: 'Setting CORS origin to "*" or true allows any website to make cross-origin requests to this API. When combined with cookies or Authorization headers, this enables cross-site request forgery (CSRF) attacks.',
1493
+ impact: "Malicious websites can make authenticated requests on behalf of logged-in users, read sensitive API responses, and exfiltrate data.",
1494
+ location: getLocation(originNode, context.filePath),
1495
+ fix: {
1496
+ description: "Specify an explicit allowlist of trusted origins. Use an environment variable to configure origins per deployment environment.",
1497
+ code: "cors({\n origin: process.env.ALLOWED_ORIGINS?.split(',') ?? [],\n credentials: true,\n})"
1498
+ }
1499
+ });
1500
+ }
1501
+ }
1502
+ },
1503
+ ObjectExpression(node) {
1504
+ const props = node.properties ?? [];
1505
+ const httpOnlyNode = getPropertyValue(props, "httpOnly");
1506
+ if (httpOnlyNode && getLiteralBool(httpOnlyNode) === false) {
1507
+ context.report({
1508
+ message: "Cookie configured with httpOnly: false.",
1509
+ explanation: "Setting httpOnly: false makes cookies accessible to JavaScript via document.cookie. This allows cross-site scripting (XSS) attacks to steal session cookies.",
1510
+ impact: "A single XSS vulnerability in your application can lead to session hijacking for all users if cookies are not protected with httpOnly.",
1511
+ location: getLocation(httpOnlyNode, context.filePath),
1512
+ fix: {
1513
+ description: "Set httpOnly: true for all session and authentication cookies. Never set httpOnly: false unless the cookie is explicitly intended for JavaScript access."
1514
+ }
1515
+ });
1516
+ }
1517
+ const secureNode = getPropertyValue(props, "secure");
1518
+ if (secureNode && getLiteralBool(secureNode) === false) {
1519
+ context.report({
1520
+ message: "Cookie configured with secure: false.",
1521
+ explanation: "Setting secure: false allows cookies to be transmitted over unencrypted HTTP connections. This exposes session tokens to network interception.",
1522
+ impact: "Cookies sent over HTTP can be captured by network observers, enabling session hijacking and man-in-the-middle attacks.",
1523
+ location: getLocation(secureNode, context.filePath),
1524
+ fix: {
1525
+ description: "Set secure: true for all session and authentication cookies. Use a conditional based on NODE_ENV only in local development.",
1526
+ code: "secure: process.env.NODE_ENV === 'production'"
1527
+ }
1528
+ });
1529
+ }
1530
+ const sameSiteNode = getPropertyValue(props, "sameSite");
1531
+ if (sameSiteNode && getLiteralString(sameSiteNode)?.toLowerCase() === "none") {
1532
+ const secureValue = getPropertyValue(props, "secure");
1533
+ if (!secureValue || getLiteralBool(secureValue) !== true) {
1534
+ context.report({
1535
+ message: "Cookie has sameSite: 'none' without secure: true.",
1536
+ explanation: "sameSite: 'none' is required for cross-site cookies but is only valid when combined with secure: true. Without it, the cookie is rejected by modern browsers.",
1537
+ impact: "Cross-origin requests will fail in production because the browser will reject the cookie. This may also enable CSRF if the application is served over HTTP.",
1538
+ location: getLocation(sameSiteNode, context.filePath),
1539
+ fix: {
1540
+ description: "Add secure: true when using sameSite: 'none'. Cross-site cookies require HTTPS."
1541
+ }
1542
+ });
1543
+ }
1544
+ }
1545
+ }
1546
+ });
1547
+ }
1548
+ };
1549
+ var dangerous_cors_default = rule15;
1550
+
1551
+ // src/rules/reliability/unhandled-promise.ts
1552
+ var KNOWN_ASYNC_APIS = /* @__PURE__ */ new Set([
1553
+ "fetch",
1554
+ "axios",
1555
+ "mongoose.connect",
1556
+ "connect",
1557
+ "disconnect",
1558
+ "save",
1559
+ "create",
1560
+ "find",
1561
+ "findOne",
1562
+ "findById",
1563
+ "update",
1564
+ "updateOne",
1565
+ "deleteOne",
1566
+ "remove",
1567
+ "sendMail",
1568
+ "publish",
1569
+ "subscribe",
1570
+ "emit",
1571
+ "send",
1572
+ "write",
1573
+ "close",
1574
+ "end",
1575
+ "open",
1576
+ "connect",
1577
+ "mkdir",
1578
+ "unlink",
1579
+ "copyFile",
1580
+ "rename",
1581
+ "readFile",
1582
+ "writeFile",
1583
+ "appendFile"
1584
+ ]);
1585
+ function getCallName(node) {
1586
+ const callee = node.callee;
1587
+ if (!callee) return null;
1588
+ if (callee.type === "Identifier") return String(callee.name);
1589
+ if (callee.type === "MemberExpression") {
1590
+ const prop = callee.property;
1591
+ if (prop?.type === "Identifier") return String(prop.name);
1592
+ }
1593
+ return null;
1594
+ }
1595
+ function isPromiseChained(node) {
1596
+ const parent = node.parent;
1597
+ if (!parent) return false;
1598
+ if (parent.type === "MemberExpression" || parent.type === "CallExpression") {
1599
+ return true;
1600
+ }
1601
+ if (parent.type === "AwaitExpression") return true;
1602
+ if (parent.type === "ReturnStatement") return true;
1603
+ if (parent.type === "VariableDeclarator") return true;
1604
+ if (parent.type === "AssignmentExpression") return true;
1605
+ return false;
1606
+ }
1607
+ var rule16 = {
1608
+ meta: {
1609
+ id: "reliability/unhandled-promise",
1610
+ name: "Unhandled Promise",
1611
+ category: "reliability",
1612
+ severity: "high",
1613
+ confidence: "medium",
1614
+ description: "Detects Promise-returning function calls that are not awaited, returned, or error-handled.",
1615
+ rationale: "Unhandled Promise rejections crash Node.js processes in versions >= 15 and cause silent failures in older versions. Fire-and-forget async calls hide errors that should be surfaced to callers.",
1616
+ docsUrl: "https://clean-slop.dev/docs/rules/reliability/unhandled-promise",
1617
+ fixable: false
1618
+ },
1619
+ create(context) {
1620
+ traverse(context.ast, {
1621
+ ExpressionStatement(node) {
1622
+ const expr = node.expression;
1623
+ if (!expr || expr.type !== "CallExpression") return;
1624
+ const name = getCallName(expr);
1625
+ if (!name) return;
1626
+ if (!KNOWN_ASYNC_APIS.has(name)) return;
1627
+ if (isPromiseChained(expr)) return;
1628
+ context.report({
1629
+ message: `Possible unhandled Promise: result of ${name}() is not awaited or handled.`,
1630
+ explanation: `${name}() likely returns a Promise, but the result is discarded. If this Promise rejects, the error will be swallowed or crash the process.`,
1631
+ impact: "Unhandled rejections in production cause process crashes (Node.js >= 15) or silent data loss. Operations you expect to complete may silently fail.",
1632
+ location: getLocation(expr, context.filePath),
1633
+ fix: {
1634
+ description: "Await the Promise inside an async function, or add .catch() to handle rejection explicitly.",
1635
+ code: '// Option 1: await in an async function\nawait someAsyncFn();\n\n// Option 2: explicit catch\nsomeAsyncFn().catch((err) => {\n logger.error("Operation failed:", err);\n});'
1636
+ }
1637
+ });
1638
+ }
1639
+ });
1640
+ }
1641
+ };
1642
+ var unhandled_promise_default = rule16;
1643
+
1644
+ // src/rules/reliability/missing-await.ts
1645
+ function isAsyncFunction(node) {
1646
+ return (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.async === true;
1647
+ }
1648
+ var LIKELY_ASYNC = /* @__PURE__ */ new Set([
1649
+ "fetch",
1650
+ "readFile",
1651
+ "writeFile",
1652
+ "connect",
1653
+ "query",
1654
+ "execute",
1655
+ "findOne",
1656
+ "find",
1657
+ "save",
1658
+ "create",
1659
+ "update",
1660
+ "delete",
1661
+ "sendMail",
1662
+ "publish",
1663
+ "subscribe"
1664
+ ]);
1665
+ function looksAsync(node) {
1666
+ if (node.type !== "CallExpression") return false;
1667
+ const callee = node.callee;
1668
+ if (!callee) return false;
1669
+ if (callee.type === "Identifier" && LIKELY_ASYNC.has(String(callee.name))) return true;
1670
+ if (callee.type === "MemberExpression") {
1671
+ const prop = callee.property;
1672
+ if (prop?.type === "Identifier" && LIKELY_ASYNC.has(String(prop.name))) return true;
1673
+ }
1674
+ return false;
1675
+ }
1676
+ var rule17 = {
1677
+ meta: {
1678
+ id: "reliability/missing-await",
1679
+ name: "Missing Await",
1680
+ category: "reliability",
1681
+ severity: "high",
1682
+ confidence: "medium",
1683
+ description: "Detects async functions that return un-awaited Promises or call async APIs without await.",
1684
+ rationale: "Forgetting await is one of the most common async bugs in JavaScript. The function appears to return a value but actually returns a Promise, causing unexpected behavior in callers that expect a resolved value.",
1685
+ docsUrl: "https://clean-slop.dev/docs/rules/reliability/missing-await",
1686
+ fixable: true
1687
+ },
1688
+ create(context) {
1689
+ traverse(context.ast, {
1690
+ FunctionDeclaration: checkAsyncFn,
1691
+ FunctionExpression: checkAsyncFn,
1692
+ ArrowFunctionExpression: checkAsyncFn
1693
+ });
1694
+ function checkAsyncFn(fn) {
1695
+ if (!isAsyncFunction(fn)) return;
1696
+ const body = fn.body;
1697
+ if (!body) return;
1698
+ if (body.type !== "BlockStatement") {
1699
+ if (looksAsync(body)) {
1700
+ context.report({
1701
+ message: "Async arrow function returns a Promise without await.",
1702
+ explanation: "The expression body of this async arrow function appears to return a Promise directly. The outer async function wraps this in another Promise, which may not be what was intended.",
1703
+ impact: "Callers awaiting this function will receive a resolved Promise<Promise<T>> rather than T. This causes subtle type errors and may result in unhandled rejections.",
1704
+ location: getLocation(fn, context.filePath),
1705
+ fix: {
1706
+ description: "Add await to the expression body: async () => await fetch(...)",
1707
+ code: 'const getData = async () => await fetch("/api/data");'
1708
+ }
1709
+ });
1710
+ }
1711
+ return;
1712
+ }
1713
+ const statements = body.body ?? [];
1714
+ for (const stmt of statements) {
1715
+ if (stmt.type !== "ReturnStatement") continue;
1716
+ const returnArg = stmt.argument;
1717
+ if (!returnArg) continue;
1718
+ if (looksAsync(returnArg)) {
1719
+ context.report({
1720
+ message: "Async function returns a Promise without await.",
1721
+ explanation: "This async function returns a likely-async call without awaiting it. While this compiles correctly, it loses error context and may cause confusing stack traces. In try/catch blocks, the catch will not trigger for the returned Promise.",
1722
+ impact: "In a try/catch block, exceptions from the returned Promise will not be caught by the surrounding catch clause. This creates invisible error-handling gaps.",
1723
+ location: getLocation(stmt, context.filePath),
1724
+ fix: {
1725
+ description: "Add await before the return value to ensure errors are caught properly.",
1726
+ code: "return await someAsyncOperation();"
1727
+ }
1728
+ });
1729
+ }
1730
+ }
1731
+ }
1732
+ }
1733
+ };
1734
+ var missing_await_default = rule17;
1735
+
1736
+ // src/rules/reliability/infinite-loop.ts
1737
+ function hasExitStatement(body) {
1738
+ const exits = findAll(body, "BreakStatement");
1739
+ if (exits.length > 0) return true;
1740
+ const returns = findAll(body, "ReturnStatement");
1741
+ if (returns.length > 0) return true;
1742
+ const throws = findAll(body, "ThrowStatement");
1743
+ if (throws.length > 0) return true;
1744
+ return false;
1745
+ }
1746
+ function isLiteralTrue(node) {
1747
+ if (!node) return false;
1748
+ return node.type === "Literal" && node.value === true;
1749
+ }
1750
+ var rule18 = {
1751
+ meta: {
1752
+ id: "reliability/infinite-loop",
1753
+ name: "Potential Infinite Loop",
1754
+ category: "reliability",
1755
+ severity: "high",
1756
+ confidence: "medium",
1757
+ description: "Detects while(true) and for(;;) loops without reachable exit conditions.",
1758
+ rationale: "Infinite loops without an exit condition hang server processes, exhaust CPU, and cause denial of service. They are commonly introduced by AI-generated code that models polling or retry logic incorrectly.",
1759
+ docsUrl: "https://clean-slop.dev/docs/rules/reliability/infinite-loop",
1760
+ fixable: false
1761
+ },
1762
+ create(context) {
1763
+ traverse(context.ast, {
1764
+ WhileStatement(node) {
1765
+ const test = node.test;
1766
+ if (!isLiteralTrue(test)) return;
1767
+ const body = node.body;
1768
+ if (!body) return;
1769
+ if (!hasExitStatement(body)) {
1770
+ context.report({
1771
+ message: "while(true) loop with no break, return, or throw detected.",
1772
+ explanation: "This while(true) loop has no reachable exit statement (break, return, or throw). It will run indefinitely, blocking the event loop or hanging a thread.",
1773
+ impact: "An infinite loop blocks the Node.js event loop, rendering the server completely unresponsive. This is an exploitable denial-of-service condition.",
1774
+ location: getLocation(node, context.filePath),
1775
+ fix: {
1776
+ description: "Add an explicit exit condition or convert to a recursive function with a base case. For polling, use setInterval() instead of an infinite loop."
1777
+ }
1778
+ });
1779
+ }
1780
+ },
1781
+ ForStatement(node) {
1782
+ const test = node.test;
1783
+ if (test !== null && test !== void 0) return;
1784
+ const body = node.body;
1785
+ if (!body) return;
1786
+ if (!hasExitStatement(body)) {
1787
+ context.report({
1788
+ message: "for(;;) loop with no break, return, or throw detected.",
1789
+ explanation: "This for(;;) loop has no termination condition and no exit statement. It will run indefinitely.",
1790
+ impact: "Blocks the Node.js event loop and causes complete server unresponsiveness.",
1791
+ location: getLocation(node, context.filePath),
1792
+ fix: {
1793
+ description: "Add a break condition or convert to a while loop with an explicit termination check."
1794
+ }
1795
+ });
1796
+ }
1797
+ }
1798
+ });
1799
+ }
1800
+ };
1801
+ var infinite_loop_default = rule18;
1802
+
1803
+ // src/rules/maintainability/giant-file.ts
1804
+ var MAX_LINES2 = 400;
1805
+ var EXTREME_LINES = 1e3;
1806
+ var rule19 = {
1807
+ meta: {
1808
+ id: "maintainability/giant-file",
1809
+ name: "Giant File",
1810
+ category: "maintainability",
1811
+ severity: "medium",
1812
+ confidence: "certain",
1813
+ description: `Detects source files exceeding ${MAX_LINES2} lines.`,
1814
+ rationale: "Large files concentrate unrelated logic, creating merge conflicts, slow code review, and unclear ownership. They are a common artifact of AI code generation that did not decompose responsibilities into modules.",
1815
+ docsUrl: "https://clean-slop.dev/docs/rules/maintainability/giant-file",
1816
+ fixable: false
1817
+ },
1818
+ create(context) {
1819
+ const lineCount = context.source.split("\n").length;
1820
+ if (lineCount <= MAX_LINES2) return;
1821
+ const isExtreme = lineCount > EXTREME_LINES;
1822
+ context.report({
1823
+ message: `File is ${lineCount} lines long (limit: ${MAX_LINES2}).`,
1824
+ explanation: `This file contains ${lineCount} lines. Files this large violate the single-responsibility principle and make it difficult for contributors to understand the module boundaries.`,
1825
+ impact: isExtreme ? "Extremely large files cause slow IDE performance, painful code reviews, and high cognitive load. They are effectively unmaintainable." : "Large files slow code review and increase the chance that related changes land in the wrong file, leading to tangled responsibility.",
1826
+ location: {
1827
+ file: context.filePath,
1828
+ line: 1,
1829
+ column: 0
1830
+ },
1831
+ fix: {
1832
+ description: "Split the file into focused modules. Group related functions into a dedicated file. A file should do one thing and export a cohesive set of related functionality."
1833
+ },
1834
+ metadata: { lineCount }
1835
+ });
1836
+ }
1837
+ };
1838
+ var giant_file_default = rule19;
1839
+ var rule20 = {
1840
+ meta: {
1841
+ id: "maintainability/circular-imports",
1842
+ name: "Potential Circular Import",
1843
+ category: "maintainability",
1844
+ severity: "medium",
1845
+ confidence: "low",
1846
+ description: "Identifies import patterns that commonly cause circular dependency cycles.",
1847
+ rationale: "Circular imports cause initialization order issues, undefined module exports at runtime, and confusing bugs that differ between bundlers. They are a sign of poor module boundary design.",
1848
+ docsUrl: "https://clean-slop.dev/docs/rules/maintainability/circular-imports",
1849
+ fixable: false
1850
+ },
1851
+ create(context) {
1852
+ const dir = path3.dirname(context.filePath);
1853
+ const base = path3.basename(context.filePath, path3.extname(context.filePath));
1854
+ const imports = findAll(context.ast, "ImportDeclaration");
1855
+ for (const imp of imports) {
1856
+ const src = imp.source;
1857
+ if (!src || src.type !== "Literal") continue;
1858
+ const importPath = String(src.value);
1859
+ if (!importPath.startsWith(".")) continue;
1860
+ const resolved = path3.resolve(dir, importPath);
1861
+ const resolvedBase = path3.basename(resolved);
1862
+ if (resolvedBase === "index" && path3.dirname(resolved) === dir && base !== "index") {
1863
+ context.report({
1864
+ message: `Import from "${importPath}" may create a circular dependency.`,
1865
+ explanation: `This file imports from "${importPath}", which is the index barrel of the same directory. If the index file re-exports this module, a circular dependency exists.`,
1866
+ impact: "Circular dependencies can cause modules to initialize with undefined exports, leading to hard-to-debug runtime errors that only appear in specific import orders.",
1867
+ location: {
1868
+ file: context.filePath,
1869
+ line: imp.loc?.start.line ?? 1,
1870
+ column: imp.loc?.start.column ?? 0
1871
+ },
1872
+ fix: {
1873
+ description: "Import directly from the source module rather than through the barrel index. Instead of `import { foo } from './index'`, use `import { foo } from './foo'`."
1874
+ }
1875
+ });
1876
+ }
1877
+ }
1878
+ }
1879
+ };
1880
+ var circular_imports_default = rule20;
1881
+
1882
+ // src/rules/production-readiness/no-console-log.ts
1883
+ var DEBUG_METHODS = /* @__PURE__ */ new Set([
1884
+ "console.log",
1885
+ "console.debug",
1886
+ "console.dir",
1887
+ "console.trace",
1888
+ "console.table"
1889
+ ]);
1890
+ var ALLOWED_CONSOLE = /* @__PURE__ */ new Set([
1891
+ "console.error",
1892
+ "console.warn",
1893
+ "console.info"
1894
+ ]);
1895
+ var rule21 = {
1896
+ meta: {
1897
+ id: "production-readiness/no-console-log",
1898
+ name: "Console Debug Statement",
1899
+ category: "production-readiness",
1900
+ severity: "low",
1901
+ confidence: "certain",
1902
+ description: "Detects console.log and other debug console methods left in production code.",
1903
+ rationale: "Debug console statements leak internal application data, degrade performance, and indicate code that was not reviewed before shipping.",
1904
+ docsUrl: "https://clean-slop.dev/docs/rules/production-readiness/no-console-log",
1905
+ fixable: true
1906
+ },
1907
+ create(context) {
1908
+ traverse(context.ast, {
1909
+ CallExpression(node) {
1910
+ const callee = node.callee;
1911
+ if (!callee || callee.type !== "MemberExpression") return;
1912
+ const obj = callee.object;
1913
+ const prop = callee.property;
1914
+ if (obj?.type !== "Identifier" || String(obj.name) !== "console" || prop?.type !== "Identifier") {
1915
+ return;
1916
+ }
1917
+ const method = String(prop.name);
1918
+ const fullName = `console.${method}`;
1919
+ if (ALLOWED_CONSOLE.has(fullName)) return;
1920
+ if (DEBUG_METHODS.has(fullName)) {
1921
+ context.report({
1922
+ message: `${fullName}() should not be present in production code.`,
1923
+ explanation: `${fullName}() is a debug statement that was likely left over during development. Console output pollutes server logs, leaks internal state, and can expose sensitive data (tokens, user objects, query results) to log aggregation systems.`,
1924
+ impact: "Debug logs appearing in production obscure real errors, violate data handling requirements, and may trigger compliance alerts for PII exposure.",
1925
+ location: getLocation(node, context.filePath),
1926
+ fix: {
1927
+ description: "Remove the console statement. For intentional logging, use a structured logger (pino, winston, bunyan) that supports log levels and structured output.",
1928
+ code: "import pino from 'pino';\n\nconst logger = pino();\nlogger.debug({ userId }, 'User lookup completed');"
1929
+ }
1930
+ });
1931
+ }
1932
+ }
1933
+ });
1934
+ }
1935
+ };
1936
+ var no_console_log_default = rule21;
1937
+
1938
+ // src/rules/production-readiness/no-localhost-urls.ts
1939
+ var LOCALHOST_PATTERN = /https?:\/\/(?:localhost|127\.0\.0\.1|0\.0\.0\.0)(?::\d+)?/i;
1940
+ var DEBUG_FLAG_NAMES = /^(?:debug|DEBUG|isDebug|IS_DEBUG|debugMode|DEBUG_MODE|devMode|DEV_MODE|testMode|TEST_MODE)$/;
1941
+ var MOCK_FUNCTION_NAMES = /^(?:mock|fake|stub|dummy|placeholder)[\w_]*/i;
1942
+ var TEST_CREDENTIAL_PATTERNS = [
1943
+ /test[_-]?(password|secret|token|key)/i,
1944
+ /password\s*[:=]\s*['"](?:test|password|1234|admin|secret)['"]/i,
1945
+ /secret\s*[:=]\s*['"](?:test|secret|dev|development)['"]/i
1946
+ ];
1947
+ var rule22 = {
1948
+ meta: {
1949
+ id: "production-readiness/no-localhost-urls",
1950
+ name: "Localhost URL / Debug Flag / Mock Implementation",
1951
+ category: "production-readiness",
1952
+ severity: "medium",
1953
+ confidence: "high",
1954
+ description: "Detects hardcoded localhost URLs, debug flags set to true, and mock/stub implementations.",
1955
+ rationale: "Localhost URLs, active debug flags, and mock implementations in source code are telltale signs that development shortcuts were committed to production.",
1956
+ docsUrl: "https://clean-slop.dev/docs/rules/production-readiness/no-localhost-urls",
1957
+ fixable: false
1958
+ },
1959
+ create(context) {
1960
+ const lines = context.source.split("\n");
1961
+ lines.forEach((line, index) => {
1962
+ if (LOCALHOST_PATTERN.test(line)) {
1963
+ context.report({
1964
+ message: "Hardcoded localhost URL detected.",
1965
+ explanation: "This line contains a localhost URL. In production, this will fail to connect to the intended service because localhost resolves to the server itself.",
1966
+ impact: "API calls to localhost in production silently fail or hit incorrect services, causing user-facing errors and data integrity issues.",
1967
+ location: { file: context.filePath, line: index + 1, column: 0 },
1968
+ fix: {
1969
+ description: "Replace with an environment variable: process.env.API_BASE_URL or similar.",
1970
+ code: "const apiUrl = process.env.API_BASE_URL ?? 'https://api.yourdomain.com';"
1971
+ }
1972
+ });
1973
+ }
1974
+ for (const pattern of TEST_CREDENTIAL_PATTERNS) {
1975
+ if (pattern.test(line)) {
1976
+ context.report({
1977
+ message: "Test or placeholder credential detected.",
1978
+ explanation: 'This line appears to contain a test credential with an obvious value (e.g., "password", "secret"). Test credentials committed to source indicate authentication logic that was never hardened.',
1979
+ impact: 'Test credentials in production allow unauthorized access. Obvious values like "test" or "1234" are among the first tried in credential stuffing attacks.',
1980
+ location: { file: context.filePath, line: index + 1, column: 0 },
1981
+ fix: {
1982
+ description: "Use environment variables for all credentials. Rotate any credential that has been committed."
1983
+ }
1984
+ });
1985
+ break;
1986
+ }
1987
+ }
1988
+ });
1989
+ traverse2(context.ast, {
1990
+ // debug = true or DEBUG_MODE = true
1991
+ AssignmentExpression(node) {
1992
+ const left = node.left;
1993
+ const right = node.right;
1994
+ if (left?.type !== "Identifier") return;
1995
+ if (!DEBUG_FLAG_NAMES.test(String(left.name))) return;
1996
+ if (right?.type !== "Literal" || right.value !== true) return;
1997
+ context.report({
1998
+ message: `Debug flag "${left.name}" is set to true.`,
1999
+ explanation: `The variable "${left.name}" suggests a debug mode toggle that is hardcoded to true. Debug mode typically enables verbose logging, disables security checks, and activates development-only code paths.`,
2000
+ impact: "Active debug flags in production expose internal state, disable security controls, and degrade performance through excessive logging.",
2001
+ location: getLocation2(node, context.filePath),
2002
+ fix: {
2003
+ description: 'Drive debug mode from an environment variable: const debug = process.env.DEBUG === "true";'
2004
+ }
2005
+ });
2006
+ },
2007
+ // Variable initializer: const debug = true
2008
+ VariableDeclarator(node) {
2009
+ const id = node.id;
2010
+ const init = node.init;
2011
+ if (id?.type !== "Identifier") return;
2012
+ if (!DEBUG_FLAG_NAMES.test(String(id.name))) return;
2013
+ if (init?.type !== "Literal" || init.value !== true) return;
2014
+ context.report({
2015
+ message: `Debug flag "${id.name}" initialized to true.`,
2016
+ explanation: `"${id.name}" is a debug flag initialized with true. This likely enables a development-only code path.`,
2017
+ impact: "Debug mode active in production may disable security checks, expose stack traces to end users, or generate excessive log output.",
2018
+ location: getLocation2(node, context.filePath),
2019
+ fix: {
2020
+ description: 'Use an environment variable: const debug = process.env.NODE_ENV !== "production";'
2021
+ }
2022
+ });
2023
+ },
2024
+ // Function named mockX, fakeX, stubX
2025
+ FunctionDeclaration(node) {
2026
+ const id = node.id;
2027
+ if (!id || !MOCK_FUNCTION_NAMES.test(String(id.name))) return;
2028
+ context.report({
2029
+ message: `Function "${id.name}" appears to be a mock or stub implementation.`,
2030
+ explanation: `The name "${id.name}" suggests this is a placeholder implementation that was never replaced with a real one.`,
2031
+ impact: "Mock implementations in production return fabricated data, bypassing real logic and producing incorrect results for end users.",
2032
+ location: getLocation2(node, context.filePath),
2033
+ fix: {
2034
+ description: "Implement the real function or remove it entirely. Mock functions belong in test files only."
2035
+ }
2036
+ });
2037
+ }
2038
+ });
2039
+ function traverse2(ast, visitor) {
2040
+ if (!ast || typeof ast !== "object") return;
2041
+ const node = ast;
2042
+ if (typeof node.type === "string") visitor[node.type]?.(node);
2043
+ for (const key of Object.keys(node)) {
2044
+ if (key === "parent") continue;
2045
+ const child = node[key];
2046
+ if (Array.isArray(child)) {
2047
+ for (const item of child) traverse2(item, visitor);
2048
+ } else if (child && typeof child === "object" && "type" in child) {
2049
+ traverse2(child, visitor);
2050
+ }
2051
+ }
2052
+ }
2053
+ function getLocation2(node, filePath) {
2054
+ return { file: filePath, line: node.loc?.start.line ?? 1, column: node.loc?.start.column ?? 0 };
2055
+ }
2056
+ }
2057
+ };
2058
+ var no_localhost_urls_default = rule22;
2059
+
2060
+ // src/rules/index.ts
2061
+ var BUILT_IN_RULES = [
2062
+ // AI Slop
2063
+ empty_catch_default,
2064
+ todo_implementation_default,
2065
+ giant_function_default,
2066
+ excessive_nesting_default,
2067
+ fake_validation_default,
2068
+ high_complexity_default,
2069
+ dead_code_default,
2070
+ // Security
2071
+ unsafe_eval_default,
2072
+ hardcoded_secrets_default,
2073
+ sql_injection_default,
2074
+ command_injection_default,
2075
+ path_traversal_default,
2076
+ prototype_pollution_default,
2077
+ weak_crypto_default,
2078
+ dangerous_cors_default,
2079
+ // Reliability
2080
+ unhandled_promise_default,
2081
+ missing_await_default,
2082
+ infinite_loop_default,
2083
+ // Maintainability
2084
+ giant_file_default,
2085
+ circular_imports_default,
2086
+ // Production Readiness
2087
+ no_console_log_default,
2088
+ no_localhost_urls_default
2089
+ ];
2090
+
2091
+ // src/scanners/scanner.ts
2092
+ async function loadGitignore(root) {
2093
+ const ig = ignore();
2094
+ const gitignorePath = path3.join(root, ".gitignore");
2095
+ try {
2096
+ const content = await fs4.readFile(gitignorePath, "utf-8");
2097
+ ig.add(content);
2098
+ } catch {
2099
+ }
2100
+ return ig;
2101
+ }
2102
+ async function discoverFiles(config) {
2103
+ const files = await fg(config.include, {
2104
+ cwd: config.root,
2105
+ ignore: config.exclude,
2106
+ absolute: true,
2107
+ followSymbolicLinks: false,
2108
+ onlyFiles: true
2109
+ });
2110
+ const ig = await loadGitignore(config.root);
2111
+ if (config.ignorePatterns.length > 0) {
2112
+ ig.add(config.ignorePatterns);
2113
+ }
2114
+ return files.filter((f) => {
2115
+ if (isBinaryPath(f)) return false;
2116
+ const relative = path3.relative(config.root, f);
2117
+ try {
2118
+ return !ig.ignores(relative);
2119
+ } catch {
2120
+ return true;
2121
+ }
2122
+ });
2123
+ }
2124
+ function buildEngine(_config) {
2125
+ const engine = new RuleEngine();
2126
+ engine.registerAll(BUILT_IN_RULES);
2127
+ return engine;
2128
+ }
2129
+ function computeScore(issues, config) {
2130
+ const categories = [
2131
+ "ai-slop",
2132
+ "security",
2133
+ "reliability",
2134
+ "maintainability",
2135
+ "production-readiness"
2136
+ ];
2137
+ const categoryScores = categories.map((cat) => {
2138
+ const catIssues = issues.filter((i) => i.category === cat);
2139
+ const deductions = {
2140
+ critical: 20,
2141
+ high: 10,
2142
+ medium: 4,
2143
+ low: 1,
2144
+ info: 0
2145
+ };
2146
+ const totalDeduction = catIssues.reduce(
2147
+ (sum, i) => sum + (deductions[i.severity] ?? 0),
2148
+ 0
2149
+ );
2150
+ const raw = Math.max(0, 100 - totalDeduction);
2151
+ return {
2152
+ category: cat,
2153
+ score: raw,
2154
+ issueCount: catIssues.length,
2155
+ criticalCount: catIssues.filter((i) => i.severity === "critical").length,
2156
+ highCount: catIssues.filter((i) => i.severity === "high").length,
2157
+ mediumCount: catIssues.filter((i) => i.severity === "medium").length,
2158
+ lowCount: catIssues.filter((i) => i.severity === "low").length
2159
+ };
2160
+ });
2161
+ const overall = categoryScores.reduce((sum, c) => sum + c.score, 0) / categoryScores.length;
2162
+ const grade = computeGrade(overall);
2163
+ return {
2164
+ overall: Math.round(overall),
2165
+ categories: categoryScores,
2166
+ grade,
2167
+ productionReady: overall >= config.failThreshold && issues.filter((i) => i.severity === "critical").length === 0
2168
+ };
2169
+ }
2170
+ async function scan(options) {
2171
+ const { config, onFile } = options;
2172
+ const startTime = Date.now();
2173
+ const engine = buildEngine();
2174
+ const files = await discoverFiles(config);
2175
+ const fileResults = [];
2176
+ const allIssues = [];
2177
+ for (let i = 0; i < files.length; i++) {
2178
+ const filePath = files[i];
2179
+ if (!filePath) continue;
2180
+ onFile?.(filePath, i, files.length);
2181
+ let parsed;
2182
+ let source;
2183
+ try {
2184
+ source = await fs4.readFile(filePath, "utf-8");
2185
+ if (!source.trim()) {
2186
+ fileResults.push({ file: filePath, issues: [], skipped: true, skipReason: "empty file" });
2187
+ continue;
2188
+ }
2189
+ if (source.length > 2e6) {
2190
+ fileResults.push({ file: filePath, issues: [], skipped: true, skipReason: "file too large" });
2191
+ continue;
2192
+ }
2193
+ parsed = parseSource(filePath, source);
2194
+ } catch (err) {
2195
+ const parseError = err instanceof ParseError ? err.message : String(err);
2196
+ fileResults.push({ file: filePath, issues: [], parseError });
2197
+ continue;
2198
+ }
2199
+ const issues = engine.runOnFile(parsed, config);
2200
+ const sortedIssues = issues.sort((a, b) => {
2201
+ return (SEVERITY_ORDER[b.severity] ?? 0) - (SEVERITY_ORDER[a.severity] ?? 0);
2202
+ });
2203
+ fileResults.push({ file: filePath, issues: sortedIssues });
2204
+ allIssues.push(...sortedIssues);
2205
+ options.onIssue && sortedIssues.forEach(options.onIssue);
2206
+ }
2207
+ const score = computeScore(allIssues, config);
2208
+ return {
2209
+ root: config.root,
2210
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2211
+ version: PACKAGE_VERSION,
2212
+ files: fileResults,
2213
+ issues: allIssues.sort(
2214
+ (a, b) => (SEVERITY_ORDER[b.severity] ?? 0) - (SEVERITY_ORDER[a.severity] ?? 0)
2215
+ ),
2216
+ score,
2217
+ durationMs: Date.now() - startTime,
2218
+ config
2219
+ };
2220
+ }
2221
+ function pad(str, width) {
2222
+ return str.padEnd(width);
2223
+ }
2224
+ function formatSeverity(severity) {
2225
+ const color = SEVERITY_COLORS[severity] ?? "";
2226
+ const label = severity.toUpperCase().padEnd(8);
2227
+ return `${color}${label}${RESET}`;
2228
+ }
2229
+ function formatScore(score) {
2230
+ if (score >= 90) return `${GREEN}${score}${RESET}`;
2231
+ if (score >= 70) return `${YELLOW}${score}${RESET}`;
2232
+ return `${RED}${score}${RESET}`;
2233
+ }
2234
+ function formatGrade(grade) {
2235
+ if (grade === "A") return `${GREEN}${grade}${RESET}`;
2236
+ if (grade === "B") return `${GREEN}${grade}${RESET}`;
2237
+ if (grade === "C") return `${YELLOW}${grade}${RESET}`;
2238
+ if (grade === "D") return `${YELLOW}${grade}${RESET}`;
2239
+ return `${RED}${grade}${RESET}`;
2240
+ }
2241
+ function formatDuration(ms) {
2242
+ if (ms < 1e3) return `${ms}ms`;
2243
+ return `${(ms / 1e3).toFixed(2)}s`;
2244
+ }
2245
+ function formatRelativePath(root, filePath) {
2246
+ return path3.relative(root, filePath);
2247
+ }
2248
+ function renderSeparator(char = "-", width = 80) {
2249
+ return GRAY + char.repeat(width) + RESET;
2250
+ }
2251
+ function renderIssue(issue, root, verbose) {
2252
+ const lines = [];
2253
+ const relPath2 = formatRelativePath(root, issue.location.file);
2254
+ const loc = `${relPath2}:${issue.location.line}:${issue.location.column}`;
2255
+ lines.push(
2256
+ ` ${formatSeverity(issue.severity)} ${BOLD}${issue.message}${RESET}`
2257
+ );
2258
+ lines.push(` ${DIM}${loc}${RESET} ${GRAY}[${issue.ruleId}]${RESET}`);
2259
+ if (verbose) {
2260
+ lines.push("");
2261
+ lines.push(` ${DIM}${issue.explanation}${RESET}`);
2262
+ if (issue.impact) {
2263
+ lines.push("");
2264
+ lines.push(` ${YELLOW}Impact:${RESET} ${issue.impact}`);
2265
+ }
2266
+ if (issue.snippet) {
2267
+ lines.push("");
2268
+ lines.push(
2269
+ issue.snippet.split("\n").map((l) => ` ${GRAY}${l}${RESET}`).join("\n")
2270
+ );
2271
+ }
2272
+ if (issue.fix) {
2273
+ lines.push("");
2274
+ lines.push(` ${CYAN}Fix:${RESET} ${issue.fix.description}`);
2275
+ if (issue.fix.code) {
2276
+ lines.push("");
2277
+ lines.push(
2278
+ issue.fix.code.split("\n").map((l) => ` ${DIM}${l}${RESET}`).join("\n")
2279
+ );
2280
+ }
2281
+ }
2282
+ if (issue.docsUrl) {
2283
+ lines.push("");
2284
+ lines.push(` ${DIM}${issue.docsUrl}${RESET}`);
2285
+ }
2286
+ }
2287
+ return lines.join("\n");
2288
+ }
2289
+ function generateTextReport(result) {
2290
+ const { score, issues, files, durationMs, config, root, version } = result;
2291
+ const verbose = config.verbose;
2292
+ const lines = [];
2293
+ lines.push("");
2294
+ lines.push(`${BOLD}clean-slop v${version}${RESET} ${DIM}Production Readiness Engine${RESET}`);
2295
+ lines.push(renderSeparator("=", 80));
2296
+ lines.push("");
2297
+ const fileGroups = /* @__PURE__ */ new Map();
2298
+ for (const issue of issues) {
2299
+ const key = issue.location.file;
2300
+ if (!fileGroups.has(key)) fileGroups.set(key, []);
2301
+ fileGroups.get(key).push(issue);
2302
+ }
2303
+ if (issues.length === 0) {
2304
+ lines.push(`${GREEN}No issues found.${RESET}`);
2305
+ } else {
2306
+ for (const [filePath, fileIssues] of fileGroups) {
2307
+ const relPath2 = formatRelativePath(root, filePath);
2308
+ lines.push(`${BOLD}${relPath2}${RESET}`);
2309
+ lines.push(renderSeparator("-", 80));
2310
+ for (const issue of fileIssues) {
2311
+ lines.push(renderIssue(issue, root, verbose));
2312
+ lines.push("");
2313
+ }
2314
+ }
2315
+ }
2316
+ lines.push(renderSeparator("=", 80));
2317
+ lines.push(`${BOLD}Score Card${RESET}`);
2318
+ lines.push(renderSeparator("-", 80));
2319
+ for (const cat of score.categories) {
2320
+ const label = pad(cat.category, 24);
2321
+ const catScore = formatScore(cat.score);
2322
+ const counts = DIM + (cat.criticalCount > 0 ? ` ${cat.criticalCount} critical` : "") + (cat.highCount > 0 ? ` ${cat.highCount} high` : "") + (cat.mediumCount > 0 ? ` ${cat.mediumCount} medium` : "") + (cat.lowCount > 0 ? ` ${cat.lowCount} low` : "") + RESET;
2323
+ lines.push(` ${label} ${catScore}/100${counts}`);
2324
+ }
2325
+ lines.push("");
2326
+ lines.push(
2327
+ ` ${pad("Overall Score", 24)} ${formatScore(score.overall)}/100 Grade: ${formatGrade(score.grade)}`
2328
+ );
2329
+ lines.push("");
2330
+ const criticalCount = issues.filter((i) => i.severity === "critical").length;
2331
+ const highCount = issues.filter((i) => i.severity === "high").length;
2332
+ const mediumCount = issues.filter((i) => i.severity === "medium").length;
2333
+ const lowCount = issues.filter((i) => i.severity === "low").length;
2334
+ lines.push(renderSeparator("-", 80));
2335
+ lines.push(
2336
+ ` ${issues.length} issue${issues.length !== 1 ? "s" : ""} found across ${files.filter((f) => !f.skipped && !f.parseError).length} files (${formatDuration(durationMs)})`
2337
+ );
2338
+ if (criticalCount > 0) lines.push(` ${RED}${criticalCount} critical${RESET}`);
2339
+ if (highCount > 0) lines.push(` ${YELLOW}${highCount} high${RESET}`);
2340
+ if (mediumCount > 0) lines.push(` ${YELLOW}${mediumCount} medium${RESET}`);
2341
+ if (lowCount > 0) lines.push(` ${DIM}${lowCount} low${RESET}`);
2342
+ lines.push("");
2343
+ if (score.productionReady) {
2344
+ lines.push(` ${GREEN}PRODUCTION READY${RESET} Score above threshold (${config.failThreshold})`);
2345
+ } else {
2346
+ lines.push(
2347
+ ` ${RED}NOT PRODUCTION READY${RESET} Score ${score.overall} below threshold (${config.failThreshold})`
2348
+ );
2349
+ }
2350
+ lines.push("");
2351
+ const parseErrors = files.filter((f) => f.parseError);
2352
+ if (parseErrors.length > 0) {
2353
+ lines.push(`${YELLOW}Parse errors in ${parseErrors.length} file(s):${RESET}`);
2354
+ for (const f of parseErrors) {
2355
+ lines.push(` ${DIM}${f.file}: ${f.parseError}${RESET}`);
2356
+ }
2357
+ lines.push("");
2358
+ }
2359
+ return lines.join("\n");
2360
+ }
2361
+
2362
+ // src/reporters/json-reporter.ts
2363
+ function generateJsonReport(result) {
2364
+ return JSON.stringify(result, null, 2);
2365
+ }
2366
+ var SEVERITY_EMOJI = {
2367
+ critical: "CRITICAL",
2368
+ high: "HIGH",
2369
+ medium: "MEDIUM",
2370
+ low: "LOW",
2371
+ info: "INFO"
2372
+ };
2373
+ function scoreBadge(score) {
2374
+ const color = score >= 90 ? "brightgreen" : score >= 70 ? "yellow" : "red";
2375
+ return `![score](https://img.shields.io/badge/score-${score}%2F100-${color})`;
2376
+ }
2377
+ function gradeBadge(grade) {
2378
+ const color = grade === "A" || grade === "B" ? "brightgreen" : grade === "C" ? "yellow" : "red";
2379
+ return `![grade](https://img.shields.io/badge/grade-${grade}-${color})`;
2380
+ }
2381
+ function formatDuration2(ms) {
2382
+ if (ms < 1e3) return `${ms}ms`;
2383
+ return `${(ms / 1e3).toFixed(2)}s`;
2384
+ }
2385
+ function relPath(root, filePath) {
2386
+ return path3.relative(root, filePath);
2387
+ }
2388
+ function renderIssueMd(issue, root) {
2389
+ const lines = [];
2390
+ const loc = `${relPath(root, issue.location.file)}:${issue.location.line}`;
2391
+ lines.push(`#### ${SEVERITY_EMOJI[issue.severity]}: ${issue.message}`);
2392
+ lines.push("");
2393
+ lines.push(`- **Rule:** \`${issue.ruleId}\``);
2394
+ lines.push(`- **Location:** \`${loc}\``);
2395
+ lines.push(`- **Confidence:** ${issue.confidence}`);
2396
+ lines.push("");
2397
+ lines.push(issue.explanation);
2398
+ if (issue.impact) {
2399
+ lines.push("");
2400
+ lines.push(`**Impact:** ${issue.impact}`);
2401
+ }
2402
+ if (issue.snippet) {
2403
+ lines.push("");
2404
+ lines.push("```");
2405
+ lines.push(issue.snippet);
2406
+ lines.push("```");
2407
+ }
2408
+ if (issue.fix) {
2409
+ lines.push("");
2410
+ lines.push(`**Fix:** ${issue.fix.description}`);
2411
+ if (issue.fix.code) {
2412
+ lines.push("");
2413
+ lines.push("```javascript");
2414
+ lines.push(issue.fix.code);
2415
+ lines.push("```");
2416
+ }
2417
+ }
2418
+ if (issue.docsUrl) {
2419
+ lines.push("");
2420
+ lines.push(`[Documentation](${issue.docsUrl})`);
2421
+ }
2422
+ lines.push("");
2423
+ lines.push("---");
2424
+ return lines.join("\n");
2425
+ }
2426
+ function generateMarkdownReport(result) {
2427
+ const { score, issues, files, durationMs, config, root, version, timestamp } = result;
2428
+ const lines = [];
2429
+ lines.push("# clean-slop Report");
2430
+ lines.push("");
2431
+ lines.push(`> Generated by clean-slop v${version} on ${new Date(timestamp).toUTCString()}`);
2432
+ lines.push("");
2433
+ lines.push("## Score Summary");
2434
+ lines.push("");
2435
+ lines.push(`${scoreBadge(score.overall)} ${gradeBadge(score.grade)}`);
2436
+ lines.push("");
2437
+ lines.push("| Category | Score | Issues |");
2438
+ lines.push("|----------|-------|--------|");
2439
+ for (const cat of score.categories) {
2440
+ const emoji = cat.score >= 90 ? "\u2705" : cat.score >= 70 ? "\u26A0\uFE0F" : "\u274C";
2441
+ lines.push(
2442
+ `| ${cat.category} | ${emoji} ${cat.score}/100 | ${cat.issueCount} |`
2443
+ );
2444
+ }
2445
+ lines.push("");
2446
+ lines.push(`**Overall Score:** ${score.overall}/100 &nbsp; **Grade:** ${score.grade}`);
2447
+ lines.push("");
2448
+ const productionStatus = score.productionReady ? "\u2705 **PRODUCTION READY**" : "\u274C **NOT PRODUCTION READY**";
2449
+ lines.push(productionStatus);
2450
+ lines.push("");
2451
+ const criticalIssues = issues.filter((i) => i.severity === "critical");
2452
+ const highIssues = issues.filter((i) => i.severity === "high");
2453
+ const mediumIssues = issues.filter((i) => i.severity === "medium");
2454
+ const lowIssues = issues.filter((i) => i.severity === "low");
2455
+ lines.push("## Issue Breakdown");
2456
+ lines.push("");
2457
+ lines.push(`| Severity | Count |`);
2458
+ lines.push(`|----------|-------|`);
2459
+ lines.push(`| Critical | ${criticalIssues.length} |`);
2460
+ lines.push(`| High | ${highIssues.length} |`);
2461
+ lines.push(`| Medium | ${mediumIssues.length} |`);
2462
+ lines.push(`| Low | ${lowIssues.length} |`);
2463
+ lines.push(`| **Total** | **${issues.length}** |`);
2464
+ lines.push("");
2465
+ if (criticalIssues.length > 0) {
2466
+ lines.push("## Critical Issues");
2467
+ lines.push("");
2468
+ for (const issue of criticalIssues) {
2469
+ lines.push(renderIssueMd(issue, root));
2470
+ }
2471
+ }
2472
+ if (highIssues.length > 0) {
2473
+ lines.push("## High Severity Issues");
2474
+ lines.push("");
2475
+ for (const issue of highIssues) {
2476
+ lines.push(renderIssueMd(issue, root));
2477
+ }
2478
+ }
2479
+ if (mediumIssues.length > 0) {
2480
+ lines.push("<details>");
2481
+ lines.push("<summary>Medium Severity Issues</summary>");
2482
+ lines.push("");
2483
+ for (const issue of mediumIssues) {
2484
+ lines.push(renderIssueMd(issue, root));
2485
+ }
2486
+ lines.push("</details>");
2487
+ lines.push("");
2488
+ }
2489
+ if (lowIssues.length > 0) {
2490
+ lines.push("<details>");
2491
+ lines.push("<summary>Low Severity Issues</summary>");
2492
+ lines.push("");
2493
+ for (const issue of lowIssues) {
2494
+ lines.push(renderIssueMd(issue, root));
2495
+ }
2496
+ lines.push("</details>");
2497
+ lines.push("");
2498
+ }
2499
+ lines.push("## Scan Statistics");
2500
+ lines.push("");
2501
+ lines.push(`- **Files scanned:** ${files.filter((f) => !f.skipped && !f.parseError).length}`);
2502
+ lines.push(`- **Files skipped:** ${files.filter((f) => f.skipped).length}`);
2503
+ lines.push(`- **Parse errors:** ${files.filter((f) => f.parseError).length}`);
2504
+ lines.push(`- **Duration:** ${formatDuration2(durationMs)}`);
2505
+ lines.push(`- **Fail threshold:** ${config.failThreshold}/100`);
2506
+ lines.push("");
2507
+ const parseErrors = files.filter((f) => f.parseError);
2508
+ if (parseErrors.length > 0) {
2509
+ lines.push("### Parse Errors");
2510
+ lines.push("");
2511
+ for (const f of parseErrors) {
2512
+ lines.push(`- \`${relPath(root, f.file)}\`: ${f.parseError}`);
2513
+ }
2514
+ lines.push("");
2515
+ }
2516
+ lines.push("---");
2517
+ lines.push("");
2518
+ lines.push("*Generated by [clean-slop](https://github.com/clean-slop/clean-slop)*");
2519
+ return lines.join("\n");
2520
+ }
2521
+ function severityToSarifLevel(severity) {
2522
+ switch (severity) {
2523
+ case "critical":
2524
+ case "high":
2525
+ return "error";
2526
+ case "medium":
2527
+ return "warning";
2528
+ case "low":
2529
+ return "note";
2530
+ case "info":
2531
+ return "none";
2532
+ }
2533
+ }
2534
+ function toSarifSecuritySeverity(severity) {
2535
+ switch (severity) {
2536
+ case "critical":
2537
+ return "9.5";
2538
+ case "high":
2539
+ return "7.5";
2540
+ case "medium":
2541
+ return "5.0";
2542
+ case "low":
2543
+ return "2.5";
2544
+ case "info":
2545
+ return "0.0";
2546
+ }
2547
+ }
2548
+ function fileUri(filePath) {
2549
+ const normalized = filePath.replace(/\\/g, "/");
2550
+ return normalized.startsWith("/") ? `file://${normalized}` : `file:///${normalized}`;
2551
+ }
2552
+ function generateSarifReport(result) {
2553
+ const { issues, root } = result;
2554
+ const ruleMap = /* @__PURE__ */ new Map();
2555
+ for (const issue of issues) {
2556
+ if (!ruleMap.has(issue.ruleId)) {
2557
+ ruleMap.set(issue.ruleId, issue);
2558
+ }
2559
+ }
2560
+ const sarifRules = Array.from(ruleMap.values()).map((issue) => ({
2561
+ id: issue.ruleId,
2562
+ name: issue.ruleName.replace(/\s+/g, ""),
2563
+ shortDescription: {
2564
+ text: issue.message
2565
+ },
2566
+ fullDescription: {
2567
+ text: issue.explanation
2568
+ },
2569
+ help: {
2570
+ text: issue.fix?.description ?? "See documentation for details.",
2571
+ markdown: issue.fix?.code ? `${issue.fix.description}
2572
+
2573
+ \`\`\`javascript
2574
+ ${issue.fix.code}
2575
+ \`\`\`` : issue.fix?.description ?? "See documentation for details."
2576
+ },
2577
+ helpUri: issue.docsUrl ?? `https://clean-slop.dev/docs/rules/${issue.ruleId}`,
2578
+ properties: {
2579
+ tags: [issue.category],
2580
+ "security-severity": toSarifSecuritySeverity(issue.severity),
2581
+ precision: issue.confidence === "certain" ? "very-high" : issue.confidence,
2582
+ "problem.severity": issue.severity
2583
+ }
2584
+ }));
2585
+ const sarifResults = issues.map((issue) => ({
2586
+ ruleId: issue.ruleId,
2587
+ level: severityToSarifLevel(issue.severity),
2588
+ message: {
2589
+ text: `${issue.message}
2590
+
2591
+ ${issue.explanation}${issue.impact ? `
2592
+
2593
+ Impact: ${issue.impact}` : ""}`
2594
+ },
2595
+ locations: [
2596
+ {
2597
+ physicalLocation: {
2598
+ artifactLocation: {
2599
+ uri: path3.relative(root, issue.location.file).replace(/\\/g, "/"),
2600
+ uriBaseId: "%SRCROOT%"
2601
+ },
2602
+ region: {
2603
+ startLine: issue.location.line,
2604
+ startColumn: issue.location.column + 1,
2605
+ endLine: issue.location.endLine ?? issue.location.line,
2606
+ endColumn: (issue.location.endColumn ?? issue.location.column) + 1
2607
+ }
2608
+ }
2609
+ }
2610
+ ],
2611
+ partialFingerprints: {
2612
+ primaryLocationLineHash: Buffer.from(
2613
+ `${issue.ruleId}:${issue.location.file}:${issue.location.line}`
2614
+ ).toString("base64")
2615
+ },
2616
+ properties: {
2617
+ confidence: issue.confidence,
2618
+ category: issue.category
2619
+ }
2620
+ }));
2621
+ const sarif = {
2622
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
2623
+ version: "2.1.0",
2624
+ runs: [
2625
+ {
2626
+ tool: {
2627
+ driver: {
2628
+ name: "clean-slop",
2629
+ version: PACKAGE_VERSION,
2630
+ informationUri: "https://github.com/clean-slop/clean-slop",
2631
+ semanticVersion: PACKAGE_VERSION,
2632
+ rules: sarifRules
2633
+ }
2634
+ },
2635
+ originalUriBaseIds: {
2636
+ "%SRCROOT%": {
2637
+ uri: fileUri(root) + "/"
2638
+ }
2639
+ },
2640
+ results: sarifResults,
2641
+ columnKind: "utf16CodeUnits"
2642
+ }
2643
+ ]
2644
+ };
2645
+ return JSON.stringify(sarif, null, 2);
2646
+ }
2647
+ function escapeHtml(str) {
2648
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
2649
+ }
2650
+ function severityColor(severity) {
2651
+ switch (severity) {
2652
+ case "critical":
2653
+ return "#dc2626";
2654
+ case "high":
2655
+ return "#ea580c";
2656
+ case "medium":
2657
+ return "#d97706";
2658
+ case "low":
2659
+ return "#2563eb";
2660
+ case "info":
2661
+ return "#6b7280";
2662
+ }
2663
+ }
2664
+ function scoreColor(score) {
2665
+ if (score >= 90) return "#16a34a";
2666
+ if (score >= 70) return "#d97706";
2667
+ return "#dc2626";
2668
+ }
2669
+ function renderIssueCard(issue, root) {
2670
+ const relPath2 = path3.relative(root, issue.location.file).replace(/\\/g, "/");
2671
+ const color = severityColor(issue.severity);
2672
+ return `
2673
+ <div class="issue-card" data-severity="${issue.severity}" data-category="${issue.category}">
2674
+ <div class="issue-header">
2675
+ <span class="severity-badge" style="background:${color}">${issue.severity.toUpperCase()}</span>
2676
+ <span class="issue-rule">${escapeHtml(issue.ruleId)}</span>
2677
+ <span class="issue-confidence">${escapeHtml(issue.confidence)} confidence</span>
2678
+ </div>
2679
+ <div class="issue-message">${escapeHtml(issue.message)}</div>
2680
+ <div class="issue-location">${escapeHtml(relPath2)}:${issue.location.line}</div>
2681
+ <div class="issue-explanation">${escapeHtml(issue.explanation)}</div>
2682
+ ${issue.impact ? `<div class="issue-impact"><strong>Impact:</strong> ${escapeHtml(issue.impact)}</div>` : ""}
2683
+ ${issue.snippet ? `<pre class="issue-snippet"><code>${escapeHtml(issue.snippet)}</code></pre>` : ""}
2684
+ ${issue.fix ? `
2685
+ <div class="issue-fix">
2686
+ <strong>Fix:</strong> ${escapeHtml(issue.fix.description)}
2687
+ ${issue.fix.code ? `<pre class="fix-code"><code>${escapeHtml(issue.fix.code)}</code></pre>` : ""}
2688
+ </div>
2689
+ ` : ""}
2690
+ ${issue.docsUrl ? `<a class="docs-link" href="${escapeHtml(issue.docsUrl)}" target="_blank" rel="noopener">Documentation</a>` : ""}
2691
+ </div>`;
2692
+ }
2693
+ function generateHtmlReport(result) {
2694
+ const { score, issues, files, durationMs, root, version, timestamp } = result;
2695
+ const criticalCount = issues.filter((i) => i.severity === "critical").length;
2696
+ const highCount = issues.filter((i) => i.severity === "high").length;
2697
+ const mediumCount = issues.filter((i) => i.severity === "medium").length;
2698
+ const lowCount = issues.filter((i) => i.severity === "low").length;
2699
+ const scannedCount = files.filter((f) => !f.skipped && !f.parseError).length;
2700
+ const issueCards = issues.map((i) => renderIssueCard(i, root)).join("\n");
2701
+ const categoryRows = score.categories.map((cat) => {
2702
+ const color = scoreColor(cat.score);
2703
+ return `
2704
+ <tr>
2705
+ <td>${escapeHtml(cat.category)}</td>
2706
+ <td><span style="color:${color};font-weight:600">${cat.score}/100</span></td>
2707
+ <td>${cat.issueCount}</td>
2708
+ <td>${cat.criticalCount}</td>
2709
+ <td>${cat.highCount}</td>
2710
+ <td>${cat.mediumCount}</td>
2711
+ <td>${cat.lowCount}</td>
2712
+ </tr>`;
2713
+ }).join("\n");
2714
+ const durationStr = durationMs < 1e3 ? `${durationMs}ms` : `${(durationMs / 1e3).toFixed(2)}s`;
2715
+ const overallColor = scoreColor(score.overall);
2716
+ const readyBg = score.productionReady ? "#16a34a" : "#dc2626";
2717
+ const readyText = score.productionReady ? "PRODUCTION READY" : "NOT PRODUCTION READY";
2718
+ return `<!DOCTYPE html>
2719
+ <html lang="en">
2720
+ <head>
2721
+ <meta charset="UTF-8" />
2722
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
2723
+ <title>clean-slop Report</title>
2724
+ <style>
2725
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2726
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; line-height: 1.6; }
2727
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
2728
+ header { border-bottom: 1px solid #1e293b; padding-bottom: 2rem; margin-bottom: 2rem; }
2729
+ header h1 { font-size: 1.75rem; font-weight: 700; color: #f8fafc; letter-spacing: -0.025em; }
2730
+ header p { color: #64748b; margin-top: 0.25rem; font-size: 0.875rem; }
2731
+ .score-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
2732
+ .score-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.25rem; }
2733
+ .score-card .label { font-size: 0.75rem; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.05em; }
2734
+ .score-card .value { font-size: 2rem; font-weight: 700; margin-top: 0.25rem; }
2735
+ .ready-badge { display: inline-block; background: ${readyBg}; color: #fff; font-size: 0.8rem; font-weight: 600; padding: 0.35rem 0.9rem; border-radius: 9999px; margin-bottom: 2rem; letter-spacing: 0.05em; }
2736
+ table { width: 100%; border-collapse: collapse; background: #1e293b; border-radius: 8px; overflow: hidden; margin-bottom: 2rem; font-size: 0.875rem; }
2737
+ th { background: #0f172a; text-align: left; padding: 0.75rem 1rem; color: #94a3b8; font-weight: 500; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.05em; }
2738
+ td { padding: 0.75rem 1rem; border-top: 1px solid #334155; }
2739
+ .filters { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
2740
+ .filter-btn { background: #1e293b; border: 1px solid #334155; color: #94a3b8; padding: 0.35rem 0.9rem; border-radius: 6px; cursor: pointer; font-size: 0.8rem; transition: all 0.15s; }
2741
+ .filter-btn:hover, .filter-btn.active { background: #334155; color: #f1f5f9; border-color: #475569; }
2742
+ .issue-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
2743
+ .issue-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; flex-wrap: wrap; }
2744
+ .severity-badge { font-size: 0.7rem; font-weight: 700; color: #fff; padding: 0.2rem 0.6rem; border-radius: 4px; letter-spacing: 0.05em; }
2745
+ .issue-rule { font-family: monospace; font-size: 0.8rem; color: #64748b; }
2746
+ .issue-confidence { font-size: 0.75rem; color: #475569; margin-left: auto; }
2747
+ .issue-message { font-weight: 600; color: #f1f5f9; margin-bottom: 0.5rem; }
2748
+ .issue-location { font-family: monospace; font-size: 0.8rem; color: #64748b; margin-bottom: 0.75rem; }
2749
+ .issue-explanation { font-size: 0.875rem; color: #94a3b8; margin-bottom: 0.75rem; }
2750
+ .issue-impact { font-size: 0.875rem; color: #fbbf24; margin-bottom: 0.75rem; }
2751
+ .issue-snippet { background: #0f172a; border: 1px solid #334155; border-radius: 6px; padding: 1rem; margin-bottom: 0.75rem; overflow-x: auto; }
2752
+ .issue-snippet code, .fix-code code { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.8rem; color: #94a3b8; white-space: pre; }
2753
+ .issue-fix { font-size: 0.875rem; color: #86efac; margin-bottom: 0.75rem; }
2754
+ .fix-code { background: #0f172a; border: 1px solid #334155; border-radius: 6px; padding: 1rem; margin-top: 0.5rem; overflow-x: auto; }
2755
+ .docs-link { font-size: 0.8rem; color: #60a5fa; text-decoration: none; }
2756
+ .docs-link:hover { text-decoration: underline; }
2757
+ .section-title { font-size: 1.1rem; font-weight: 600; color: #f8fafc; margin-bottom: 1rem; border-bottom: 1px solid #1e293b; padding-bottom: 0.5rem; }
2758
+ .empty-state { text-align: center; padding: 3rem; color: #64748b; }
2759
+ .stat-row { display: flex; gap: 2rem; flex-wrap: wrap; color: #64748b; font-size: 0.875rem; margin-bottom: 2rem; }
2760
+ .hidden { display: none; }
2761
+ </style>
2762
+ </head>
2763
+ <body>
2764
+ <div class="container">
2765
+ <header>
2766
+ <h1>clean-slop Report</h1>
2767
+ <p>v${escapeHtml(version)} &nbsp;\xB7&nbsp; ${escapeHtml(new Date(timestamp).toUTCString())} &nbsp;\xB7&nbsp; ${escapeHtml(root)}</p>
2768
+ </header>
2769
+
2770
+ <div class="score-grid">
2771
+ <div class="score-card">
2772
+ <div class="label">Overall Score</div>
2773
+ <div class="value" style="color:${overallColor}">${score.overall}<span style="font-size:1rem;color:#64748b">/100</span></div>
2774
+ </div>
2775
+ <div class="score-card">
2776
+ <div class="label">Grade</div>
2777
+ <div class="value" style="color:${overallColor}">${escapeHtml(score.grade)}</div>
2778
+ </div>
2779
+ <div class="score-card">
2780
+ <div class="label">Total Issues</div>
2781
+ <div class="value" style="color:#f1f5f9">${issues.length}</div>
2782
+ </div>
2783
+ <div class="score-card">
2784
+ <div class="label">Critical</div>
2785
+ <div class="value" style="color:#dc2626">${criticalCount}</div>
2786
+ </div>
2787
+ <div class="score-card">
2788
+ <div class="label">High</div>
2789
+ <div class="value" style="color:#ea580c">${highCount}</div>
2790
+ </div>
2791
+ <div class="score-card">
2792
+ <div class="label">Files Scanned</div>
2793
+ <div class="value" style="color:#f1f5f9">${scannedCount}</div>
2794
+ </div>
2795
+ </div>
2796
+
2797
+ <div class="ready-badge">${readyText}</div>
2798
+
2799
+ <div class="section-title">Category Scores</div>
2800
+ <table>
2801
+ <thead>
2802
+ <tr>
2803
+ <th>Category</th><th>Score</th><th>Total</th><th>Critical</th><th>High</th><th>Medium</th><th>Low</th>
2804
+ </tr>
2805
+ </thead>
2806
+ <tbody>${categoryRows}</tbody>
2807
+ </table>
2808
+
2809
+ <div class="section-title">Issues</div>
2810
+ <div class="stat-row">
2811
+ <span>${issues.length} issue${issues.length !== 1 ? "s" : ""} &nbsp;\xB7&nbsp; ${scannedCount} files &nbsp;\xB7&nbsp; ${durationStr}</span>
2812
+ </div>
2813
+
2814
+ <div class="filters">
2815
+ <button class="filter-btn active" onclick="filterIssues('all')">All (${issues.length})</button>
2816
+ <button class="filter-btn" onclick="filterIssues('critical')">Critical (${criticalCount})</button>
2817
+ <button class="filter-btn" onclick="filterIssues('high')">High (${highCount})</button>
2818
+ <button class="filter-btn" onclick="filterIssues('medium')">Medium (${mediumCount})</button>
2819
+ <button class="filter-btn" onclick="filterIssues('low')">Low (${lowCount})</button>
2820
+ </div>
2821
+
2822
+ <div id="issues-container">
2823
+ ${issues.length === 0 ? '<div class="empty-state">No issues found.</div>' : issueCards}
2824
+ </div>
2825
+ </div>
2826
+
2827
+ <script>
2828
+ function filterIssues(severity) {
2829
+ document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
2830
+ event.target.classList.add('active');
2831
+ document.querySelectorAll('.issue-card').forEach(card => {
2832
+ if (severity === 'all' || card.dataset.severity === severity) {
2833
+ card.classList.remove('hidden');
2834
+ } else {
2835
+ card.classList.add('hidden');
2836
+ }
2837
+ });
2838
+ }
2839
+ </script>
2840
+ </body>
2841
+ </html>`;
2842
+ }
2843
+
2844
+ // src/reporters/index.ts
2845
+ function generate(result, reporter = "text") {
2846
+ switch (reporter) {
2847
+ case "json":
2848
+ return generateJsonReport(result);
2849
+ case "html":
2850
+ return generateHtmlReport(result);
2851
+ case "markdown":
2852
+ return generateMarkdownReport(result);
2853
+ case "sarif":
2854
+ return generateSarifReport(result);
2855
+ case "text":
2856
+ default:
2857
+ return generateTextReport(result);
2858
+ }
2859
+ }
2860
+ async function writeReport(content, outputPath) {
2861
+ const dir = path3.dirname(outputPath);
2862
+ await fs4.mkdir(dir, { recursive: true });
2863
+ await fs4.writeFile(outputPath, content, "utf-8");
2864
+ }
2865
+
2866
+ // src/cli/commands/scan.ts
2867
+ function clearLine() {
2868
+ if (process.stdout.isTTY) {
2869
+ process.stdout.write("\r\x1B[K");
2870
+ }
2871
+ }
2872
+ function printProgress(filePath, index, total) {
2873
+ if (!process.stdout.isTTY) return;
2874
+ const pct = Math.round((index + 1) / total * 100);
2875
+ const short = filePath.length > 60 ? "..." + filePath.slice(-57) : filePath;
2876
+ process.stdout.write(`\r${DIM}Scanning [${pct}%] ${short}${RESET}`);
2877
+ }
2878
+ async function runScan(directory, options) {
2879
+ const ci = options.ci ?? !process.stdout.isTTY;
2880
+ const cwd = directory ? path3.resolve(directory) : process.cwd();
2881
+ const baseConfig = await loadConfig(cwd, options.config);
2882
+ const config = {
2883
+ ...baseConfig,
2884
+ verbose: options.verbose ?? baseConfig.verbose,
2885
+ reporter: options.reporter ?? baseConfig.reporter,
2886
+ output: options.output ?? baseConfig.output,
2887
+ failThreshold: options.failThreshold ? parseInt(options.failThreshold, 10) : baseConfig.failThreshold,
2888
+ categories: {
2889
+ "ai-slop": options.aiSlop === false ? false : baseConfig.categories["ai-slop"] ?? true,
2890
+ "security": options.security === false ? false : baseConfig.categories["security"] ?? true,
2891
+ "reliability": options.reliability === false ? false : baseConfig.categories["reliability"] ?? true,
2892
+ "maintainability": options.maintainability === false ? false : baseConfig.categories["maintainability"] ?? true,
2893
+ "production-readiness": options.productionReadiness === false ? false : baseConfig.categories["production-readiness"] ?? true
2894
+ }
2895
+ };
2896
+ if (options.maxCritical !== void 0) {
2897
+ config.maxIssues = { ...config.maxIssues, critical: parseInt(options.maxCritical, 10) };
2898
+ }
2899
+ if (options.maxHigh !== void 0) {
2900
+ config.maxIssues = { ...config.maxIssues, high: parseInt(options.maxHigh, 10) };
2901
+ }
2902
+ if (!ci && !options.quiet) {
2903
+ console.log(`
2904
+ ${BOLD}clean-slop${RESET} ${DIM}Production Readiness Engine${RESET}`);
2905
+ console.log(`${DIM}Scanning ${cwd}${RESET}
2906
+ `);
2907
+ }
2908
+ const scanOptions = { config };
2909
+ if (!options.quiet && !ci) scanOptions.onFile = printProgress;
2910
+ const result = await scan(scanOptions);
2911
+ if (!ci && !options.quiet && process.stdout.isTTY) {
2912
+ clearLine();
2913
+ }
2914
+ const reporterName = config.reporter;
2915
+ const reportContent = generate(result, reporterName);
2916
+ if (config.output) {
2917
+ await writeReport(reportContent, config.output);
2918
+ if (!options.quiet) {
2919
+ console.log(`${GREEN}Report written to ${config.output}${RESET}`);
2920
+ }
2921
+ } else {
2922
+ process.stdout.write(reportContent);
2923
+ }
2924
+ let shouldFail = false;
2925
+ if (result.score.overall < config.failThreshold) {
2926
+ shouldFail = true;
2927
+ }
2928
+ for (const [sev, max] of Object.entries(config.maxIssues)) {
2929
+ if (max === void 0) continue;
2930
+ const count = result.issues.filter((i) => i.severity === sev).length;
2931
+ if (count > max) {
2932
+ shouldFail = true;
2933
+ if (!options.quiet && !ci) {
2934
+ console.error(
2935
+ `${RED}${count} ${sev} issue${count !== 1 ? "s" : ""} found (max allowed: ${max})${RESET}`
2936
+ );
2937
+ }
2938
+ }
2939
+ }
2940
+ if (shouldFail) {
2941
+ process.exit(1);
2942
+ }
2943
+ }
2944
+ async function runCheck(directory, options) {
2945
+ const cwd = directory ? path3.resolve(directory) : process.cwd();
2946
+ const baseConfig = await loadConfig(cwd, options.config);
2947
+ const config = {
2948
+ ...baseConfig,
2949
+ failThreshold: options.failThreshold ? parseInt(options.failThreshold, 10) : baseConfig.failThreshold,
2950
+ verbose: false
2951
+ };
2952
+ if (options.maxCritical !== void 0) {
2953
+ config.maxIssues = {
2954
+ ...config.maxIssues,
2955
+ critical: parseInt(options.maxCritical, 10)
2956
+ };
2957
+ }
2958
+ const result = await scan({ config });
2959
+ const criticalCount = result.issues.filter((i) => i.severity === "critical").length;
2960
+ const maxCritical = config.maxIssues.critical ?? 0;
2961
+ const criticalExceeded = criticalCount > maxCritical;
2962
+ const scorePassed = result.score.overall >= config.failThreshold;
2963
+ const passed = scorePassed && !criticalExceeded;
2964
+ if (passed) {
2965
+ console.log(
2966
+ `${GREEN}${BOLD}PASS${RESET} Score: ${result.score.overall}/100 Grade: ${result.score.grade} ${DIM}(threshold: ${config.failThreshold})${RESET}`
2967
+ );
2968
+ process.exit(0);
2969
+ } else {
2970
+ const reasons = [];
2971
+ if (!scorePassed) {
2972
+ reasons.push(
2973
+ `score ${result.score.overall} below threshold ${config.failThreshold}`
2974
+ );
2975
+ }
2976
+ if (criticalExceeded) {
2977
+ reasons.push(
2978
+ `${criticalCount} critical issue${criticalCount !== 1 ? "s" : ""} (max: ${maxCritical})`
2979
+ );
2980
+ }
2981
+ console.error(
2982
+ `${RED}${BOLD}FAIL${RESET} Score: ${result.score.overall}/100 Grade: ${result.score.grade}`
2983
+ );
2984
+ for (const reason of reasons) {
2985
+ console.error(` ${RED}${reason}${RESET}`);
2986
+ }
2987
+ process.exit(1);
2988
+ }
2989
+ }
2990
+ var DEBOUNCE_MS = 500;
2991
+ async function runWatch(directory, options) {
2992
+ const cwd = directory ? path3.resolve(directory) : process.cwd();
2993
+ const config = await loadConfig(cwd, options.config);
2994
+ if (options.verbose) config.verbose = true;
2995
+ console.log(
2996
+ `
2997
+ ${BOLD}clean-slop watch${RESET} ${DIM}Watching ${cwd}${RESET}`
2998
+ );
2999
+ console.log(`${DIM}Press Ctrl+C to stop.${RESET}
3000
+ `);
3001
+ let debounceTimer = null;
3002
+ let running = false;
3003
+ async function runScanCycle(changedFile) {
3004
+ if (running) return;
3005
+ running = true;
3006
+ if (changedFile) {
3007
+ const rel = path3.relative(cwd, changedFile);
3008
+ console.log(`${CYAN}Changed:${RESET} ${rel}`);
3009
+ }
3010
+ const startMsg = `${DIM}Scanning...${RESET}`;
3011
+ process.stdout.write(startMsg);
3012
+ try {
3013
+ const result = await scan({ config });
3014
+ process.stdout.write("\r\x1B[K");
3015
+ const report = generateTextReport(result);
3016
+ console.log(report);
3017
+ if (result.score.productionReady) {
3018
+ console.log(`${GREEN}Score: ${result.score.overall}/100${RESET}
3019
+ `);
3020
+ } else {
3021
+ console.log(`${RED}Score: ${result.score.overall}/100${RESET}
3022
+ `);
3023
+ }
3024
+ } catch (err) {
3025
+ process.stdout.write("\r\x1B[K");
3026
+ console.error(
3027
+ `${RED}Scan error:${RESET} ${err instanceof Error ? err.message : String(err)}`
3028
+ );
3029
+ }
3030
+ running = false;
3031
+ }
3032
+ await runScanCycle();
3033
+ const watcher = fs3.watch(
3034
+ cwd,
3035
+ { recursive: true },
3036
+ (event, filename) => {
3037
+ if (!filename) return;
3038
+ if (!/\.(js|jsx|ts|tsx|mjs|cjs)$/.test(filename)) return;
3039
+ if (filename.includes("node_modules") || filename.includes("dist") || filename.includes(".next")) return;
3040
+ if (debounceTimer) clearTimeout(debounceTimer);
3041
+ debounceTimer = setTimeout(() => {
3042
+ void runScanCycle(path3.join(cwd, filename));
3043
+ }, DEBOUNCE_MS);
3044
+ }
3045
+ );
3046
+ process.on("SIGINT", () => {
3047
+ watcher.close();
3048
+ console.log(`
3049
+ ${DIM}Watch stopped.${RESET}
3050
+ `);
3051
+ process.exit(0);
3052
+ });
3053
+ await new Promise(() => {
3054
+ });
3055
+ }
3056
+ function check(label, ok, detail) {
3057
+ const icon = ok ? `${GREEN}\u2713${RESET}` : `${RED}\u2717${RESET}`;
3058
+ const msg = detail ? ` ${icon} ${label} ${DIM}${detail}${RESET}` : ` ${icon} ${label}`;
3059
+ console.log(msg);
3060
+ }
3061
+ function warn(label, detail) {
3062
+ const msg = detail ? ` ${YELLOW}!${RESET} ${label} ${DIM}${detail}${RESET}` : ` ${YELLOW}!${RESET} ${label}`;
3063
+ console.log(msg);
3064
+ }
3065
+ function info(label, detail) {
3066
+ const msg = detail ? ` ${CYAN}i${RESET} ${label} ${DIM}${detail}${RESET}` : ` ${CYAN}i${RESET} ${label}`;
3067
+ console.log(msg);
3068
+ }
3069
+ function getNodeVersion() {
3070
+ return process.version;
3071
+ }
3072
+ function meetsNodeMinimum() {
3073
+ const [major] = process.version.replace("v", "").split(".").map(Number);
3074
+ return (major ?? 0) >= 18;
3075
+ }
3076
+ function getNpmVersion() {
3077
+ try {
3078
+ return execSync("npm --version", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
3079
+ } catch {
3080
+ return null;
3081
+ }
3082
+ }
3083
+ async function runDoctor() {
3084
+ const cwd = process.cwd();
3085
+ console.log(`
3086
+ ${BOLD}clean-slop doctor${RESET}
3087
+ `);
3088
+ console.log(`${DIM}Diagnosing environment and configuration...${RESET}
3089
+ `);
3090
+ console.log(`${BOLD}Environment${RESET}`);
3091
+ const nodeVersion = getNodeVersion();
3092
+ const nodeOk = meetsNodeMinimum();
3093
+ check("Node.js version", nodeOk, `${nodeVersion} (requires >= 18)`);
3094
+ const npmVersion = getNpmVersion();
3095
+ check("npm available", npmVersion !== null, npmVersion ?? "not found");
3096
+ info("Platform", process.platform);
3097
+ info("Architecture", process.arch);
3098
+ info("clean-slop version", PACKAGE_VERSION);
3099
+ console.log();
3100
+ console.log(`${BOLD}Configuration${RESET}`);
3101
+ let config;
3102
+ try {
3103
+ config = await loadConfig(cwd);
3104
+ check("Configuration loaded", true, `root: ${config.root}`);
3105
+ info("Fail threshold", `${config.failThreshold}/100`);
3106
+ info("Reporter", config.reporter);
3107
+ info("Output", config.output ?? "stdout");
3108
+ const enabledCategories = Object.entries(config.categories).filter(([, v]) => v).map(([k]) => k);
3109
+ info("Enabled categories", enabledCategories.join(", "));
3110
+ const ruleOverrides = Object.keys(config.rules).length;
3111
+ info("Rule overrides", `${ruleOverrides}`);
3112
+ } catch (err) {
3113
+ check("Configuration loaded", false, err instanceof Error ? err.message : String(err));
3114
+ }
3115
+ console.log();
3116
+ console.log(`${BOLD}Project${RESET}`);
3117
+ const pkgPath = path3.join(cwd, "package.json");
3118
+ try {
3119
+ const raw = await fs4.readFile(pkgPath, "utf-8");
3120
+ const pkg = JSON.parse(raw);
3121
+ check("package.json found", true, String(pkg.name ?? "(no name)"));
3122
+ const allDeps = {
3123
+ ...pkg.dependencies,
3124
+ ...pkg.devDependencies
3125
+ };
3126
+ const hasTs = "typescript" in allDeps;
3127
+ info("TypeScript project", String(hasTs));
3128
+ } catch {
3129
+ check("package.json found", false, "Could not read package.json");
3130
+ }
3131
+ const tsconfigPath = path3.join(cwd, "tsconfig.json");
3132
+ try {
3133
+ await fs4.access(tsconfigPath);
3134
+ check("tsconfig.json found", true);
3135
+ } catch {
3136
+ warn("tsconfig.json not found", "TypeScript projects should include tsconfig.json");
3137
+ }
3138
+ const gitPath = path3.join(cwd, ".git");
3139
+ try {
3140
+ await fs4.access(gitPath);
3141
+ check("Git repository", true);
3142
+ } catch {
3143
+ warn("Not a git repository", "clean-slop works best in a git repository");
3144
+ }
3145
+ const gitignorePath = path3.join(cwd, ".gitignore");
3146
+ try {
3147
+ await fs4.access(gitignorePath);
3148
+ check(".gitignore found", true);
3149
+ } catch {
3150
+ warn(".gitignore not found", "Add a .gitignore to exclude build artifacts");
3151
+ }
3152
+ console.log();
3153
+ console.log(`${BOLD}Rules${RESET}`);
3154
+ info("Built-in rules loaded", `${BUILT_IN_RULES.length}`);
3155
+ const byCategory = /* @__PURE__ */ new Map();
3156
+ for (const rule23 of BUILT_IN_RULES) {
3157
+ byCategory.set(rule23.meta.category, (byCategory.get(rule23.meta.category) ?? 0) + 1);
3158
+ }
3159
+ for (const [cat, count] of byCategory) {
3160
+ info(` ${cat}`, `${count} rules`);
3161
+ }
3162
+ console.log();
3163
+ if (!meetsNodeMinimum()) {
3164
+ console.log(
3165
+ `${RED}Node.js >= 18 is required. Please upgrade your Node.js installation.${RESET}
3166
+ `
3167
+ );
3168
+ process.exit(1);
3169
+ }
3170
+ console.log(`${GREEN}Environment looks good.${RESET}
3171
+ `);
3172
+ }
3173
+ async function runInit(options) {
3174
+ const cwd = process.cwd();
3175
+ const configPath = path3.join(cwd, "clean-slop.config.js");
3176
+ try {
3177
+ await fs4.access(configPath);
3178
+ if (!options.force) {
3179
+ console.log(
3180
+ `${YELLOW}clean-slop.config.js already exists.${RESET} Use ${BOLD}--force${RESET} to overwrite it.`
3181
+ );
3182
+ process.exit(1);
3183
+ }
3184
+ } catch {
3185
+ }
3186
+ const content = generateDefaultConfig();
3187
+ try {
3188
+ await fs4.writeFile(configPath, content, "utf-8");
3189
+ console.log(`${GREEN}Created clean-slop.config.js${RESET}`);
3190
+ console.log(`${RESET}Edit it to customize rules, categories, and thresholds.
3191
+ `);
3192
+ } catch (err) {
3193
+ console.error(
3194
+ `${RED}Failed to write config file: ${err instanceof Error ? err.message : String(err)}${RESET}`
3195
+ );
3196
+ process.exit(1);
3197
+ }
3198
+ }
3199
+ async function runReport(directory, options) {
3200
+ const cwd = directory ? path3.resolve(directory) : process.cwd();
3201
+ const baseConfig = await loadConfig(cwd, options.config);
3202
+ const reporterName = options.reporter ?? baseConfig.reporter;
3203
+ const outputPath = options.output ?? baseConfig.output;
3204
+ if (!outputPath && reporterName === "html") {
3205
+ const defaultOut = path3.join(cwd, "clean-slop-report.html");
3206
+ baseConfig.output = defaultOut;
3207
+ } else if (outputPath) {
3208
+ baseConfig.output = outputPath;
3209
+ }
3210
+ const config = { ...baseConfig, reporter: reporterName };
3211
+ console.log(`Scanning ${cwd}...`);
3212
+ const result = await scan({ config });
3213
+ const content = generate(result, reporterName);
3214
+ if (config.output) {
3215
+ await writeReport(content, config.output);
3216
+ console.log(`${GREEN}Report written to ${config.output}${RESET}`);
3217
+ } else {
3218
+ process.stdout.write(content);
3219
+ }
3220
+ }
3221
+
3222
+ // src/cli/index.ts
3223
+ function createCLI() {
3224
+ const program = new Command();
3225
+ program.name("clean-slop").description(
3226
+ "Production Readiness Engine for JavaScript and TypeScript projects.\nPrevents low-quality, insecure, and AI-generated code from reaching production."
3227
+ ).version(PACKAGE_VERSION, "-v, --version", "Print the current version").helpOption("-h, --help", "Display help information");
3228
+ program.command("scan [directory]", { isDefault: true }).description("Scan a directory for issues (default command)").option("-c, --config <path>", "Path to configuration file").option("--reporter <name>", "Reporter to use: text, json, html, markdown, sarif", "text").option("-o, --output <file>", "Write report to a file instead of stdout").option("--fail-threshold <score>", "Minimum score before exiting with code 1", "70").option("--max-critical <n>", "Maximum allowed critical issues (0 = none)", "0").option("--max-high <n>", "Maximum allowed high issues").option("--no-ai-slop", "Disable AI slop rules").option("--no-security", "Disable security rules").option("--no-reliability", "Disable reliability rules").option("--no-maintainability", "Disable maintainability rules").option("--no-production-readiness", "Disable production readiness rules").option("--verbose", "Print full issue details including snippets and fixes").option("--quiet", "Only print the score summary, suppress issue list").option("--ci", "CI mode: machine-readable exit codes, no color").action(runScan);
3229
+ program.command("check [directory]").description("Quick check: exit 0 if production ready, 1 if not").option("-c, --config <path>", "Path to configuration file").option("--fail-threshold <score>", "Minimum passing score", "70").option("--max-critical <n>", "Maximum critical issues allowed", "0").action(runCheck);
3230
+ program.command("watch [directory]").description("Watch for file changes and re-scan automatically").option("-c, --config <path>", "Path to configuration file").option("--verbose", "Print full issue details").action(runWatch);
3231
+ program.command("report [directory]").description("Generate a report from the last scan or run a fresh scan").option("-c, --config <path>", "Path to configuration file").option("--reporter <name>", "Reporter: text, json, html, markdown, sarif", "html").option("-o, --output <file>", "Output file path").action(runReport);
3232
+ program.command("doctor").description("Diagnose the current environment, config, and installation").action(runDoctor);
3233
+ program.command("init").description("Create a clean-slop.config.js in the current directory").option("--force", "Overwrite existing config file").action(runInit);
3234
+ return program;
3235
+ }
3236
+
3237
+ export { createCLI };
3238
+ //# sourceMappingURL=index.js.map
3239
+ //# sourceMappingURL=index.js.map