chill-agent 1.7.0 → 1.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +40 -40
- package/.codex-plugin/plugin.json +36 -36
- package/.cursor-plugin/plugin.json +35 -35
- package/.devin-plugin/plugin.json +35 -35
- package/.kimi-plugin/plugin.json +17 -17
- package/.qoder-plugin/plugin.json +21 -21
- package/GUIDE.md +116 -47
- package/antislop.md +745 -764
- package/gemini-extension.json +6 -6
- package/package.json +86 -86
- package/plugin.yaml +21 -21
- package/rules/antislop.md +745 -764
- package/rules/antislop.mdc +750 -18
- package/scripts/check-repo.mjs +4 -16
- package/scripts/skill-index.mjs +1 -1
- package/src/auditor.js +557 -116
- package/src/cli.js +44 -18
package/src/auditor.js
CHANGED
|
@@ -1,146 +1,432 @@
|
|
|
1
|
-
// src/auditor.js - Automated Anti-Slop &
|
|
1
|
+
// src/auditor.js - Automated Anti-Slop 2.0 & Ponytail Multi-Page Auditor
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { c } from './prompts.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Full 40 R-Rule and Ponytail YAGNI Detection Matrix.
|
|
8
|
+
* Tiers:
|
|
9
|
+
* 1. Hard Gate (Absolute FAIL, Severity: HIGH)
|
|
10
|
+
* 2. Purpose-Gate (Technique allowed only with written reason, Severity: MEDIUM)
|
|
11
|
+
* 3. Quality Locks & Minimalism (Consistency & YAGNI, Severity: LOW/MEDIUM)
|
|
12
|
+
*/
|
|
13
|
+
export const SLOP_PATTERNS = [
|
|
14
|
+
// ─── TIER 1: HARD GATES (Absolute Violations) ───────────────────────────
|
|
8
15
|
{
|
|
9
|
-
id: '
|
|
10
|
-
|
|
16
|
+
id: 'R-02',
|
|
17
|
+
alias: 'COPY-04',
|
|
18
|
+
category: 'Hard Gate: Copywriting',
|
|
11
19
|
severity: 'HIGH',
|
|
12
|
-
|
|
20
|
+
rule: 'R-02',
|
|
21
|
+
regex: /—/u,
|
|
22
|
+
name: 'Em Dash (—) AI Writing Tic',
|
|
23
|
+
fix: 'R-02 rule: replace em dash (—) with a comma, period, colon, or parentheses.'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'R-03',
|
|
27
|
+
alias: 'VISUAL-04',
|
|
28
|
+
category: 'Hard Gate: Mobile Responsiveness',
|
|
29
|
+
severity: 'HIGH',
|
|
30
|
+
rule: 'R-03',
|
|
31
|
+
regex: /body\s*\{[^}]*overflow-x\s*:\s*hidden|width:\s*(?:[4-9][0-9]{2}|[1-9][0-9]{3,})px(?!.*@media)/i,
|
|
32
|
+
name: 'Mobile Breakpoint Violation / Lazy Overflow Masking',
|
|
33
|
+
fix: 'R-03 rule: ensure layout reflows without horizontal overflow or lazy `overflow-x: hidden` masking.'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'R-17',
|
|
37
|
+
alias: 'COPY-03',
|
|
38
|
+
category: 'Hard Gate: Data & Numbers',
|
|
39
|
+
severity: 'HIGH',
|
|
40
|
+
rule: 'R-17',
|
|
41
|
+
regex: /\b(?:10[kK]\+|99\.9%|500[mM]\+|120\+\s*countries|10,000\+\s*users|trusted\s+by\s+[0-9,]+\s+happy\s+customers)\b/i,
|
|
42
|
+
name: 'Hallucinated / Unverified Statistics',
|
|
43
|
+
fix: 'R-17 rule: show only verified metrics or use explicit `[REAL DATA]` placeholder.'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'R-18',
|
|
47
|
+
category: 'Hard Gate: Testimonials',
|
|
48
|
+
severity: 'HIGH',
|
|
49
|
+
rule: 'R-18',
|
|
50
|
+
regex: /(?:fictional\s+reviews?|placeholder\s+avatar|randomuser\.me\/api\/portraits)/i,
|
|
51
|
+
name: 'Synthetic / Fictional Testimonial',
|
|
52
|
+
fix: 'R-18 rule: do not render testimonials without verifiable social proof.'
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: 'R-24',
|
|
56
|
+
category: 'Hard Gate: Navigation',
|
|
57
|
+
severity: 'HIGH',
|
|
58
|
+
rule: 'R-24',
|
|
59
|
+
regex: /<a[^>]+href=["']#(?:features|about|contact|pricing)?["'][^>]*>(?!.*scroll)/i,
|
|
60
|
+
name: 'Dead Navigation / Ghost Anchor Link',
|
|
61
|
+
fix: 'R-24 rule: every navigation item must resolve to a real, existing destination section or route.'
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'R-25',
|
|
65
|
+
category: 'Hard Gate: Color Contrast',
|
|
66
|
+
severity: 'HIGH',
|
|
67
|
+
rule: 'R-25',
|
|
68
|
+
regex: /(?:text-(?:gray|zinc|slate)-[34]00\s+bg-white|text-(?:gray|zinc|slate)-500\s+bg-(?:black|zinc-900|slate-950)|color:\s*#(?:888|999|aaa|bbb)\s*;\s*background:\s*#(?:fff|fafafa))/i,
|
|
69
|
+
name: 'Sub-WCAG AA Color Contrast (<4.5:1)',
|
|
70
|
+
fix: 'R-25 rule: text must meet minimum 4.5:1 contrast against its background.'
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: 'R-26',
|
|
74
|
+
category: 'Hard Gate: Interactive Elements',
|
|
75
|
+
severity: 'HIGH',
|
|
76
|
+
rule: 'R-26',
|
|
77
|
+
regex: /<button[^>]*>\s*<\/button>|<button(?![^>]*(?:onClick|type=|on:click|@click|v-on))[^>]*>/i,
|
|
78
|
+
name: 'Dead Interactive Control / Non-Functional Button',
|
|
79
|
+
fix: 'R-26 rule: every interactive control must have functional logic or a visible `// TODO` notice.'
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: 'R-32',
|
|
83
|
+
category: 'Hard Gate: Accessibility',
|
|
84
|
+
severity: 'HIGH',
|
|
85
|
+
rule: 'R-32',
|
|
86
|
+
regex: /outline:\s*(?:none|0)(?!\s*;\s*&:focus-visible)|\boutline-none(?!\s+focus-visible:)/i,
|
|
87
|
+
name: 'Stripped Focus Outline Without Replacement',
|
|
88
|
+
fix: 'R-32 rule: never remove default focus ring without providing a visible custom focus indicator.'
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: 'R-33',
|
|
92
|
+
category: 'Hard Gate: Script Patching',
|
|
93
|
+
severity: 'HIGH',
|
|
94
|
+
rule: 'R-33',
|
|
95
|
+
regex: /str\.replace\(.*\.css|rewrite_css|patch_styles/i,
|
|
96
|
+
name: 'External File/CSS Patching via Script',
|
|
97
|
+
fix: 'R-33 rule: build features directly in source code, never through external string-replacement scripts.'
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'R-36',
|
|
101
|
+
category: 'Hard Gate: Trust Claims',
|
|
102
|
+
severity: 'HIGH',
|
|
103
|
+
rule: 'R-36',
|
|
104
|
+
regex: /\b(?:SOC\s*2\s*compliant|ISO\s*27001\s*certified|enterprise-grade\s*security|300%\s*faster\s*than)\b/i,
|
|
105
|
+
name: 'Unsubstantiated Compliance / Performance Claim',
|
|
106
|
+
fix: 'R-36 rule: remove fabricated compliance, security, or performance claims lacking verifiable audit proof.'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'R-39',
|
|
110
|
+
alias: 'A11Y-01',
|
|
111
|
+
category: 'Hard Gate: Semantics & Affordances',
|
|
112
|
+
severity: 'HIGH',
|
|
113
|
+
rule: 'R-39',
|
|
114
|
+
regex: /<button[^>]*>[^<]*<svg[^>]*>[\s\S]*?<\/svg>[^<]*<\/button>(?![^<]*aria-label)|<img(?![^>]*\balt=)[^>]*>|user-scalable\s*=\s*no|maximum-scale\s*=\s*1/i,
|
|
115
|
+
name: 'Missing Accessible Name, Alt Text, or Scalability Block',
|
|
116
|
+
fix: 'R-39 rule: provide aria-label on icon buttons, alt text on images, and do not lock viewport scaling.'
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'R-40',
|
|
120
|
+
category: 'Hard Gate: Media & Performance',
|
|
121
|
+
severity: 'HIGH',
|
|
122
|
+
rule: 'R-40',
|
|
123
|
+
regex: /<(?:img|video|iframe)(?![^>]*(?:width|height|aspect-ratio|style=.*aspect-ratio))[^>]*>|<img[^>]*class=["'][^"']*hero[^"']*["'][^>]*loading=["']lazy["']/i,
|
|
124
|
+
name: 'Unreserved Media Dimensions / Lazy-Loaded Hero',
|
|
125
|
+
fix: 'R-40 rule: declare width/height or aspect-ratio on media elements; hero image must load eagerly.'
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// ─── TIER 2: PURPOSE-GATE (Allowed Only With Stated Purpose) ──────────────
|
|
129
|
+
{
|
|
130
|
+
id: 'R-01',
|
|
131
|
+
alias: 'VISUAL-01',
|
|
132
|
+
category: 'Purpose-Gate: Color & Gradients',
|
|
133
|
+
severity: 'MEDIUM',
|
|
134
|
+
rule: 'R-01',
|
|
135
|
+
regex: /from-purple-[0-9]{3}\s+to-indigo-[0-9]{3}|from-violet-[0-9]{3}\s+to-cyan-[0-9]{3}|from-indigo-[0-9]{3}\s+to-pink-[0-9]{3}|linear-gradient\([^)]*(?:#8b5cf6|#6366f1|#ec4899)[^)]*(?:#06b6d4|#3b82f6)\)/i,
|
|
13
136
|
name: 'Generic AI Purple/Cyan Gradient',
|
|
14
|
-
fix: '
|
|
137
|
+
fix: 'R-01 rule: replace default purple mesh with brand-grounded color or linear-carbon `#09090B`.'
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'R-04',
|
|
141
|
+
category: 'Purpose-Gate: Icons',
|
|
142
|
+
severity: 'MEDIUM',
|
|
143
|
+
rule: 'R-04',
|
|
144
|
+
regex: /(?<![a-zA-Z0-9_-])(?:Sparkles?|MagicWand|Wand2|BotIcon|CpuIcon|AiIcon|SparkleIcon|AiOrb)(?![a-zA-Z0-9_-])(?!\s*\/\/\s*purpose:)/i,
|
|
145
|
+
name: 'Generic AI Glyphs (Sparkle/Robot/Magic)',
|
|
146
|
+
fix: 'R-04 rule: icons must represent actual domain concepts, not generic AI decoration.'
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'R-06',
|
|
150
|
+
category: 'Purpose-Gate: Typography',
|
|
151
|
+
severity: 'MEDIUM',
|
|
152
|
+
rule: 'R-06',
|
|
153
|
+
regex: /font-mono\s+text-(?:4xl|5xl|6xl)|tracking-\[(?:0\.[3-9]em|[4-9]px)\]/i,
|
|
154
|
+
name: 'Over-Styled Monospace Heading / Extreme Tracking',
|
|
155
|
+
fix: 'R-06 rule: reserve monospace for tabular numbers/code; do not use it purely for terminal aesthetics.'
|
|
15
156
|
},
|
|
16
157
|
{
|
|
17
|
-
id: '
|
|
18
|
-
category: '
|
|
158
|
+
id: 'R-07',
|
|
159
|
+
category: 'Purpose-Gate: Background',
|
|
19
160
|
severity: 'MEDIUM',
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
161
|
+
rule: 'R-07',
|
|
162
|
+
regex: /background-image:\s*(?:radial-gradient\(#|linear-gradient\(to right, #(?:e5e7eb|26262b) 1px, transparent 1px\))|bg-\[radial-gradient/i,
|
|
163
|
+
name: 'Unanchored Background Grid / Dot Matrix',
|
|
164
|
+
fix: 'R-07 rule: use background patterns only if they directly reinforce verified brand identity.'
|
|
23
165
|
},
|
|
24
166
|
{
|
|
25
|
-
id: '
|
|
26
|
-
category: '
|
|
167
|
+
id: 'R-08',
|
|
168
|
+
category: 'Purpose-Gate: Button Arrows',
|
|
27
169
|
severity: 'LOW',
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
170
|
+
rule: 'R-08',
|
|
171
|
+
regex: /<button[^>]*>.*(?:→|→|→|↗|<ArrowRight|<ChevronRight).*<\/button>/i,
|
|
172
|
+
name: 'Default Arrow Cliché on Every Button',
|
|
173
|
+
fix: 'R-08 rule: arrows are directional cues, not default decoration on every action button.'
|
|
31
174
|
},
|
|
32
175
|
{
|
|
33
|
-
id: '
|
|
34
|
-
category: '
|
|
176
|
+
id: 'R-09',
|
|
177
|
+
category: 'Purpose-Gate: Badges',
|
|
35
178
|
severity: 'MEDIUM',
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
179
|
+
rule: 'R-09',
|
|
180
|
+
regex: /rounded-full[^>]*border[^>]*>\s*(?:AI Powered|Beta|New|Next Gen|v[0-9]\.[0-9]|Intelligent)/i,
|
|
181
|
+
name: 'AI Capsule Pill Badge Above Headline',
|
|
182
|
+
fix: 'R-09 rule: capsule badges must serve functional status, not redundant hype.'
|
|
39
183
|
},
|
|
40
|
-
|
|
41
|
-
// 2. Over-Engineering & Bloat (Ponytail Minimalism)
|
|
42
184
|
{
|
|
43
|
-
id: '
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
185
|
+
id: 'R-10',
|
|
186
|
+
alias: 'VISUAL-03',
|
|
187
|
+
category: 'Purpose-Gate: Glassmorphism',
|
|
188
|
+
severity: 'LOW',
|
|
189
|
+
rule: 'R-10',
|
|
190
|
+
regex: /backdrop-blur-(?:md|lg|xl|2xl)/i,
|
|
191
|
+
name: 'Excessive Glassmorphic Blur Stacking',
|
|
192
|
+
fix: 'R-10 rule: dose cap blur on maximum 1-2 elements; avoid stacking across cards and navbars.'
|
|
49
193
|
},
|
|
50
194
|
{
|
|
51
|
-
id: '
|
|
52
|
-
category: '
|
|
195
|
+
id: 'R-12',
|
|
196
|
+
category: 'Purpose-Gate: Shadow',
|
|
53
197
|
severity: 'MEDIUM',
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
198
|
+
rule: 'R-12',
|
|
199
|
+
regex: /shadow-(?:2xl|xl)\s+(?!.*hover:)/i,
|
|
200
|
+
name: 'Heavy Soft Floating Shadows',
|
|
201
|
+
fix: 'R-12 rule: use shadows selectively as hierarchy elevation markers, not floating card defaults.'
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'R-13',
|
|
205
|
+
category: 'Purpose-Gate: Glow',
|
|
206
|
+
severity: 'LOW',
|
|
207
|
+
rule: 'R-13',
|
|
208
|
+
regex: /(?:drop-shadow-\[0_0_|box-shadow:\s*0\s*0\s*[1-9][0-9]px|glow-)/i,
|
|
209
|
+
name: 'Pervasive Neon Glow Accent',
|
|
210
|
+
fix: 'R-13 rule: limit glow to at most 1 key focal element; avoid simultaneous glows on cards and buttons.'
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'R-14',
|
|
214
|
+
category: 'Purpose-Gate: Feature Cards',
|
|
215
|
+
severity: 'MEDIUM',
|
|
216
|
+
rule: 'R-14',
|
|
217
|
+
regex: /(?:grid-cols-3.*\{features\.map|\{items\.map.*<Card)/i,
|
|
218
|
+
name: 'Cookie-Cutter Uniform Feature Cards',
|
|
219
|
+
fix: 'R-14 rule: vary card layout and hierarchy to reflect the actual importance of each feature.'
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'R-19',
|
|
223
|
+
category: 'Purpose-Gate: Animations',
|
|
224
|
+
severity: 'LOW',
|
|
225
|
+
rule: 'R-19',
|
|
226
|
+
regex: /(?:animate-bounce|animate-pulse\s+animate-spin|fade-up.*floating.*scale)/i,
|
|
227
|
+
name: 'Stacked Template Animations',
|
|
228
|
+
fix: 'R-19 rule: use purposeful motion tied to declared MOTION dial; avoid simultaneously stacked animations.'
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'R-22',
|
|
232
|
+
category: 'Purpose-Gate: Illustrations',
|
|
233
|
+
severity: 'LOW',
|
|
234
|
+
rule: 'R-22',
|
|
235
|
+
regex: /(?:undraw|storyset|3d-character|blob-scene)/i,
|
|
236
|
+
name: 'Generic 3D Blob / Stock Illustration',
|
|
237
|
+
fix: 'R-22 rule: use real product screenshots, telemetry diagrams, or no illustration at all.'
|
|
57
238
|
},
|
|
58
239
|
|
|
59
|
-
// 3
|
|
240
|
+
// ─── TIER 3: QUALITY LOCKS & PONYTAIL MINIMALISM ────────────────────────
|
|
60
241
|
{
|
|
61
|
-
id: '
|
|
62
|
-
category: '
|
|
242
|
+
id: 'R-05',
|
|
243
|
+
category: 'Quality Lock: Layout Rhythm',
|
|
244
|
+
severity: 'MEDIUM',
|
|
245
|
+
rule: 'R-05',
|
|
246
|
+
regex: /<section[^>]*>.*how-it-works.*step-1.*step-2.*step-3/is,
|
|
247
|
+
name: 'Template "How It Works" 3-Step Rhythm',
|
|
248
|
+
fix: 'R-05 rule: structure page flow around the actual narrative rather than template 3-step sequences.'
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: 'R-11',
|
|
252
|
+
alias: 'VISUAL-02',
|
|
253
|
+
category: 'Quality Lock: Border Radius',
|
|
254
|
+
severity: 'MEDIUM',
|
|
255
|
+
rule: 'R-11',
|
|
256
|
+
regex: /\brounded-3xl\b|\brounded-full\s+.*(?:card|panel|modal|dialog)\b/i,
|
|
257
|
+
name: 'Pill-Shaped Floating Bubble / Excessive Radius',
|
|
258
|
+
fix: 'R-11 rule: maintain geometric design system radius (4px-8px) with 1px hairlines.'
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
id: 'R-15',
|
|
262
|
+
category: 'Quality Lock: Call to Action',
|
|
263
|
+
severity: 'LOW',
|
|
264
|
+
rule: 'R-15',
|
|
265
|
+
regex: />\s*(?:Get Started|Learn More|Try Now|Explore|Discover)\s*</i,
|
|
266
|
+
name: 'Generic Vague CTA Copy',
|
|
267
|
+
fix: 'R-15 rule: use action-specific CTAs (e.g. "Start Project", "View Source", "Open Dashboard").'
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: 'R-16',
|
|
271
|
+
alias: 'COPY-01',
|
|
272
|
+
category: 'Quality Lock: Copywriting Buzzwords',
|
|
63
273
|
severity: 'HIGH',
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
274
|
+
rule: 'R-16',
|
|
275
|
+
regex: /\b(?:unleash\s+the\s+power\s+of|supercharge\s+your|seamlessly\s+integrate|next-generation|game-changer|delve\s+into|dive\s+deep\s+into|cutting-edge|revolutionary|effortless)\b/i,
|
|
276
|
+
name: 'Generic AI Marketing Buzzword',
|
|
277
|
+
fix: 'R-16 rule: replace hype words with concrete technical or functional explanations.'
|
|
67
278
|
},
|
|
68
279
|
{
|
|
69
|
-
id: '
|
|
70
|
-
category: '
|
|
280
|
+
id: 'R-29',
|
|
281
|
+
category: 'Quality Lock: Color Palette',
|
|
71
282
|
severity: 'MEDIUM',
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
283
|
+
rule: 'R-29',
|
|
284
|
+
regex: /(?:bg-(?:red|orange|yellow|green|blue|purple|pink)-[0-9]{3}.*){4,}/i,
|
|
285
|
+
name: 'Palette Explosion (>3 Core Colors)',
|
|
286
|
+
fix: 'R-29 rule: limit palette to maximum 2-3 core brand colors + 1 deliberate accent.'
|
|
75
287
|
},
|
|
76
288
|
{
|
|
77
|
-
id: '
|
|
78
|
-
category: '
|
|
289
|
+
id: 'BLOAT-01',
|
|
290
|
+
category: 'Ponytail Minimalism: Architecture',
|
|
79
291
|
severity: 'HIGH',
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
292
|
+
rule: 'YAGNI',
|
|
293
|
+
regex: /class\s+[A-Za-z0-9]+Factory|class\s+[A-Za-z0-9]+AbstractBuilder|class\s+[A-Za-z0-9]+ProviderManager/i,
|
|
294
|
+
name: 'Premature Factory / Builder Abstraction',
|
|
295
|
+
fix: 'Ponytail YAGNI ladder: replace speculative class wrappers with a direct function or object literal.'
|
|
83
296
|
},
|
|
84
297
|
{
|
|
85
|
-
id: '
|
|
86
|
-
category: '
|
|
298
|
+
id: 'BLOAT-02',
|
|
299
|
+
category: 'Ponytail Minimalism: Dependencies',
|
|
87
300
|
severity: 'MEDIUM',
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
301
|
+
rule: 'STDLIB',
|
|
302
|
+
regex: /import\s+.*lodash\/(?:cloneDeep|isEqual|merge)|import\s+is-number\s+from/i,
|
|
303
|
+
name: 'Unnecessary Micro-Dependency',
|
|
304
|
+
fix: 'Use native JavaScript built-ins (`structuredClone`, standard operators) instead of external packages.'
|
|
91
305
|
},
|
|
92
|
-
|
|
93
|
-
// 4. Code Hygiene Slop
|
|
94
306
|
{
|
|
95
307
|
id: 'CODE-01',
|
|
96
|
-
category: 'Code
|
|
97
|
-
severity: '
|
|
98
|
-
|
|
308
|
+
category: 'Ponytail Hygiene: Code Comments',
|
|
309
|
+
severity: 'LOW',
|
|
310
|
+
rule: 'HYGIENE',
|
|
311
|
+
regex: /\/\/\s*(?:this\s+function\s+calculates|this\s+is\s+the\s+render\s+method|define\s+variables?|initialize\s+state)/i,
|
|
99
312
|
name: 'Redundant Obvious AI Code Comment',
|
|
100
|
-
fix: 'Remove comments that
|
|
313
|
+
fix: 'Remove comments that restate the code. Document the "WHY", not the "WHAT".'
|
|
101
314
|
},
|
|
102
315
|
{
|
|
103
316
|
id: 'CODE-02',
|
|
104
|
-
category: '
|
|
317
|
+
category: 'Ponytail Hygiene: Stubbed Logic',
|
|
105
318
|
severity: 'LOW',
|
|
106
|
-
|
|
319
|
+
rule: 'HYGIENE',
|
|
320
|
+
regex: /\/\/\s*TODO:\s*(?:implement\s+this|add\s+logic\s+later|finish\s+this)/i,
|
|
107
321
|
name: 'Unimplemented Lazy AI Stub',
|
|
108
|
-
fix: 'Provide
|
|
322
|
+
fix: 'Provide working implementation or raise explicit NotImplementedError with actionable detail.'
|
|
109
323
|
},
|
|
110
|
-
|
|
111
|
-
// 5. Human Factors & Accessibility
|
|
112
324
|
{
|
|
113
|
-
id: '
|
|
114
|
-
category: '
|
|
115
|
-
severity: '
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
325
|
+
id: 'COPY-02',
|
|
326
|
+
category: 'Copywriting Cliché: Emoji Overuse',
|
|
327
|
+
severity: 'MEDIUM',
|
|
328
|
+
rule: 'R-02',
|
|
329
|
+
regex: /[✨🚀🎉💡🔥🤖⚡]/u,
|
|
330
|
+
name: 'AI Sparkle / Rocket Emoji Cliché',
|
|
331
|
+
fix: 'Strip decorative emojis from headlines, buttons, and interface labels.'
|
|
119
332
|
}
|
|
120
333
|
];
|
|
121
334
|
|
|
122
335
|
const SCAN_EXTENSIONS = new Set([
|
|
123
|
-
'.
|
|
336
|
+
'.html', '.htm', '.jsx', '.tsx', '.vue', '.svelte', '.astro',
|
|
337
|
+
'.js', '.ts', '.mjs', '.cjs',
|
|
338
|
+
'.css', '.scss', '.sass', '.less',
|
|
339
|
+
'.md', '.mdx'
|
|
124
340
|
]);
|
|
125
341
|
|
|
126
342
|
const IGNORE_DIRS = new Set([
|
|
127
|
-
'node_modules', '.git', '.chillrewind', 'dist', 'build', '.next', '.cache',
|
|
343
|
+
'node_modules', '.git', '.chillrewind', 'dist', 'build', '.next', '.cache',
|
|
344
|
+
'coverage', '.vercel', '.output', '.openclaw', '.claude-plugin', '.codex-plugin',
|
|
345
|
+
'.cursor', '.cursor-plugin', '.windsurf', '.qoder', '.qoder-plugin', '.kimi-plugin',
|
|
346
|
+
'.devin-plugin', '.grok-plugin', '.omp-plugin', 'i18n', 'rules', 'skills'
|
|
128
347
|
]);
|
|
129
348
|
|
|
349
|
+
const IGNORE_FILES = new Set([
|
|
350
|
+
'antislop.md', 'chillslop.md', 'AGENTS.md', 'CLAUDE.md', 'GEMINI.md', 'GUIDE.md',
|
|
351
|
+
'README.md', 'CODE_OF_CONDUCT.md', 'CONTRIBUTING.md', 'SECURITY.md', 'INSTALL.md',
|
|
352
|
+
'after-install.md'
|
|
353
|
+
]);
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Discover all pages, routes, views, layouts, and components across the project.
|
|
357
|
+
*/
|
|
358
|
+
export function discoverPages(rootDir) {
|
|
359
|
+
const pages = [];
|
|
360
|
+
|
|
361
|
+
function walk(currentDir) {
|
|
362
|
+
let entries = [];
|
|
363
|
+
try {
|
|
364
|
+
entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
365
|
+
} catch {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
371
|
+
if (entry.isDirectory()) {
|
|
372
|
+
if (!IGNORE_DIRS.has(entry.name)) {
|
|
373
|
+
walk(fullPath);
|
|
374
|
+
}
|
|
375
|
+
} else if (entry.isFile()) {
|
|
376
|
+
if (IGNORE_FILES.has(entry.name)) continue;
|
|
377
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
378
|
+
if (SCAN_EXTENSIONS.has(ext)) {
|
|
379
|
+
const relPath = path.relative(rootDir, fullPath).replace(/\\/g, '/');
|
|
380
|
+
const isRoute = /\b(pages|app|routes|views)\b/i.test(relPath) || entry.name.startsWith('page.') || entry.name === 'index.html';
|
|
381
|
+
const isComponent = /\b(components|ui|widgets)\b/i.test(relPath);
|
|
382
|
+
const isStyle = ext === '.css' || ext === '.scss';
|
|
383
|
+
const isDoc = ext === '.md' || ext === '.mdx';
|
|
384
|
+
|
|
385
|
+
pages.push({
|
|
386
|
+
file: fullPath,
|
|
387
|
+
relPath,
|
|
388
|
+
name: entry.name,
|
|
389
|
+
ext,
|
|
390
|
+
type: isRoute ? 'PAGE_ROUTE' : isComponent ? 'COMPONENT' : isStyle ? 'STYLESHEET' : isDoc ? 'DOCUMENT' : 'SOURCE'
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
walk(rootDir);
|
|
398
|
+
return pages;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Scan a single file against the full 40 R-rule pattern matrix.
|
|
403
|
+
*/
|
|
130
404
|
export function scanFile(filePath) {
|
|
131
405
|
const findings = [];
|
|
132
406
|
try {
|
|
133
407
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
134
|
-
const lines = content.split(
|
|
408
|
+
const lines = content.split(/\r?\n/);
|
|
135
409
|
|
|
136
410
|
lines.forEach((line, lineIdx) => {
|
|
411
|
+
// Carve-out: skip markdown headings for em-dash check (e.g. `## R-02 — Copywriting`)
|
|
412
|
+
const isHeading = line.trim().startsWith('#') || line.trim().startsWith('//') || line.trim().startsWith('*');
|
|
413
|
+
|
|
137
414
|
for (const pattern of SLOP_PATTERNS) {
|
|
415
|
+
if (pattern.id === 'R-02' && isHeading) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
|
|
138
419
|
if (pattern.regex.test(line)) {
|
|
139
420
|
findings.push({
|
|
140
421
|
file: filePath,
|
|
141
422
|
line: lineIdx + 1,
|
|
142
|
-
snippet: line.trim().slice(0,
|
|
143
|
-
|
|
423
|
+
snippet: line.trim().slice(0, 90),
|
|
424
|
+
id: pattern.alias || pattern.id,
|
|
425
|
+
rule: pattern.rule || pattern.id,
|
|
426
|
+
category: pattern.category,
|
|
427
|
+
severity: pattern.severity,
|
|
428
|
+
name: pattern.name,
|
|
429
|
+
fix: pattern.fix
|
|
144
430
|
});
|
|
145
431
|
}
|
|
146
432
|
}
|
|
@@ -149,32 +435,119 @@ export function scanFile(filePath) {
|
|
|
149
435
|
return findings;
|
|
150
436
|
}
|
|
151
437
|
|
|
438
|
+
/**
|
|
439
|
+
* Audit all pages in repository, grouping results by page and rule tier.
|
|
440
|
+
*/
|
|
441
|
+
export function auditAllPages(rootDir) {
|
|
442
|
+
const pages = discoverPages(rootDir);
|
|
443
|
+
const pageResults = [];
|
|
444
|
+
let totalViolations = 0;
|
|
445
|
+
|
|
446
|
+
for (const page of pages) {
|
|
447
|
+
const findings = scanFile(page.file);
|
|
448
|
+
const hardGateFails = findings.filter((f) => f.severity === 'HIGH').length;
|
|
449
|
+
const purposeGateFails = findings.filter((f) => f.severity === 'MEDIUM').length;
|
|
450
|
+
const qualityLockFails = findings.filter((f) => f.severity === 'LOW').length;
|
|
451
|
+
|
|
452
|
+
// Delivery Gate Score: 100 - (High * 15 + Med * 5 + Low * 2)
|
|
453
|
+
const rawScore = 100 - (hardGateFails * 15 + purposeGateFails * 5 + qualityLockFails * 2);
|
|
454
|
+
const score = Math.max(0, Math.min(100, rawScore));
|
|
455
|
+
const status = hardGateFails === 0 && purposeGateFails === 0 ? 'PASS' : 'FAIL';
|
|
456
|
+
|
|
457
|
+
totalViolations += findings.length;
|
|
458
|
+
|
|
459
|
+
pageResults.push({
|
|
460
|
+
page,
|
|
461
|
+
findings,
|
|
462
|
+
score,
|
|
463
|
+
status,
|
|
464
|
+
counts: {
|
|
465
|
+
total: findings.length,
|
|
466
|
+
high: hardGateFails,
|
|
467
|
+
medium: purposeGateFails,
|
|
468
|
+
low: qualityLockFails
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
rootDir,
|
|
475
|
+
totalPages: pages.length,
|
|
476
|
+
totalViolations,
|
|
477
|
+
pageResults,
|
|
478
|
+
overallStatus: pageResults.every((p) => p.status === 'PASS') ? 'PASS' : 'FAIL'
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Scan directory helper for backward compatibility.
|
|
484
|
+
*/
|
|
485
|
+
export function scanDirectory(dirPath) {
|
|
486
|
+
const audit = auditAllPages(dirPath);
|
|
487
|
+
const all = [];
|
|
488
|
+
for (const res of audit.pageResults) {
|
|
489
|
+
all.push(...res.findings);
|
|
490
|
+
}
|
|
491
|
+
return all;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Automatically remediate fixable slop patterns.
|
|
496
|
+
*/
|
|
152
497
|
export function autoFixFile(filePath) {
|
|
153
498
|
let modified = false;
|
|
154
499
|
try {
|
|
155
500
|
let content = fs.readFileSync(filePath, 'utf8');
|
|
156
501
|
|
|
157
|
-
// 1. Strip
|
|
158
|
-
const strippedEmoji = content.replace(/[
|
|
502
|
+
// 1. Strip emojis (R-02 / COPY-02)
|
|
503
|
+
const strippedEmoji = content.replace(/[✨🚀🎉💡🔥🤖⚡]/gu, '');
|
|
159
504
|
if (strippedEmoji !== content) {
|
|
160
505
|
content = strippedEmoji;
|
|
161
506
|
modified = true;
|
|
162
507
|
}
|
|
163
508
|
|
|
164
|
-
// 2.
|
|
165
|
-
const
|
|
509
|
+
// 2. Replace em dashes with comma/period (R-02) outside headings
|
|
510
|
+
const lines = content.split('\n');
|
|
511
|
+
const fixedLines = lines.map((l) => {
|
|
512
|
+
if (l.trim().startsWith('#') || l.trim().startsWith('//')) return l;
|
|
513
|
+
if (l.includes('—')) {
|
|
514
|
+
modified = true;
|
|
515
|
+
return l.replace(/—/g, ',');
|
|
516
|
+
}
|
|
517
|
+
return l;
|
|
518
|
+
});
|
|
519
|
+
if (modified) content = fixedLines.join('\n');
|
|
520
|
+
|
|
521
|
+
// 3. Fix redundant obvious comments (CODE-01)
|
|
522
|
+
const strippedComments = content.replace(/\/\/\s*(?:this\s+function\s+calculates|this\s+is\s+the\s+render\s+method|define\s+variables?|initialize\s+state).*\n/gi, '');
|
|
166
523
|
if (strippedComments !== content) {
|
|
167
524
|
content = strippedComments;
|
|
168
525
|
modified = true;
|
|
169
526
|
}
|
|
170
527
|
|
|
171
|
-
//
|
|
528
|
+
// 4. Normalize rounded-3xl / rounded-full cards (R-11 / VISUAL-02)
|
|
172
529
|
const fixedRadius = content.replace(/\brounded-3xl\b/g, 'rounded-lg');
|
|
173
530
|
if (fixedRadius !== content) {
|
|
174
531
|
content = fixedRadius;
|
|
175
532
|
modified = true;
|
|
176
533
|
}
|
|
177
534
|
|
|
535
|
+
// 5. Replace generic CTAs (R-15)
|
|
536
|
+
const fixedCtas = content
|
|
537
|
+
.replace(/>\s*Get Started\s*</g, '>Start Project<')
|
|
538
|
+
.replace(/>\s*Learn More\s*</g, '>Documentation<');
|
|
539
|
+
if (fixedCtas !== content) {
|
|
540
|
+
content = fixedCtas;
|
|
541
|
+
modified = true;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// 6. Ensure native button type (R-26)
|
|
545
|
+
const fixedButtons = content.replace(/<button(?!\s+type=)([^>]*)>/g, '<button type="button"$1>');
|
|
546
|
+
if (fixedButtons !== content) {
|
|
547
|
+
content = fixedButtons;
|
|
548
|
+
modified = true;
|
|
549
|
+
}
|
|
550
|
+
|
|
178
551
|
if (modified) {
|
|
179
552
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
180
553
|
}
|
|
@@ -182,42 +555,106 @@ export function autoFixFile(filePath) {
|
|
|
182
555
|
return modified;
|
|
183
556
|
}
|
|
184
557
|
|
|
185
|
-
|
|
186
|
-
|
|
558
|
+
/**
|
|
559
|
+
* Generate standard Mode 2 audit report in `anti-slop/audit-NNN-YYYY-MM-DD.md`.
|
|
560
|
+
*/
|
|
561
|
+
export function generateAuditReport(auditData, rootDir = process.cwd()) {
|
|
562
|
+
const outDir = path.join(rootDir, 'anti-slop');
|
|
563
|
+
if (!fs.existsSync(outDir)) {
|
|
564
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
565
|
+
}
|
|
187
566
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
567
|
+
// Determine increment index
|
|
568
|
+
let maxIndex = 0;
|
|
569
|
+
try {
|
|
570
|
+
const existing = fs.readdirSync(outDir);
|
|
571
|
+
for (const f of existing) {
|
|
572
|
+
const match = f.match(/^audit-(\d{3})-/);
|
|
573
|
+
if (match) {
|
|
574
|
+
maxIndex = Math.max(maxIndex, parseInt(match[1], 10));
|
|
575
|
+
}
|
|
194
576
|
}
|
|
577
|
+
} catch {}
|
|
195
578
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
579
|
+
const nextIndex = String(maxIndex + 1).padStart(3, '0');
|
|
580
|
+
const dateStr = new Date().toISOString().slice(0, 10);
|
|
581
|
+
const fileName = `audit-${nextIndex}-${dateStr}.md`;
|
|
582
|
+
const reportPath = path.join(outDir, fileName);
|
|
583
|
+
|
|
584
|
+
const lines = [
|
|
585
|
+
`# Anti-Slop Audit Report #${nextIndex}`,
|
|
586
|
+
``,
|
|
587
|
+
`> Generated on **${dateStr}** by **ChillAgent Anti-Slop 2.0 Engine**`,
|
|
588
|
+
`> Scope: Full-Page Repository Audit (${auditData.totalPages} pages/components scanned)`,
|
|
589
|
+
``,
|
|
590
|
+
`---`,
|
|
591
|
+
``,
|
|
592
|
+
`## Executive Summary`,
|
|
593
|
+
``,
|
|
594
|
+
`- **Delivery Gate Verdict:** **${auditData.overallStatus}**`,
|
|
595
|
+
`- **Total Pages Audited:** ${auditData.totalPages}`,
|
|
596
|
+
`- **Total Infractions Detected:** ${auditData.totalViolations}`,
|
|
597
|
+
``,
|
|
598
|
+
`### Page-by-Page Audit Ledger`,
|
|
599
|
+
``,
|
|
600
|
+
`| Page / Component Path | Type | Health Score | Gate Status | High | Med | Low |`,
|
|
601
|
+
`| :--- | :--- | :---: | :---: | :---: | :---: | :---: |`
|
|
602
|
+
];
|
|
603
|
+
|
|
604
|
+
for (const pr of auditData.pageResults) {
|
|
605
|
+
if (pr.counts.total > 0 || pr.page.type === 'PAGE_ROUTE') {
|
|
606
|
+
lines.push(
|
|
607
|
+
`| \`${pr.page.relPath}\` | ${pr.page.type} | **${pr.score}/100** | ${pr.status === 'PASS' ? '🟢 PASS' : '🔴 FAIL'} | ${pr.counts.high} | ${pr.counts.medium} | ${pr.counts.low} |`
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
lines.push(``);
|
|
613
|
+
lines.push(`---`);
|
|
614
|
+
lines.push(``);
|
|
615
|
+
lines.push(`## Numbered Findings List (Actionable Remediation)`);
|
|
616
|
+
lines.push(``);
|
|
617
|
+
|
|
618
|
+
let findingCounter = 1;
|
|
619
|
+
for (const pr of auditData.pageResults) {
|
|
620
|
+
if (pr.findings.length === 0) continue;
|
|
621
|
+
|
|
622
|
+
lines.push(`### \`${pr.page.relPath}\``);
|
|
623
|
+
lines.push(``);
|
|
624
|
+
|
|
625
|
+
for (const f of pr.findings) {
|
|
626
|
+
lines.push(`#### Finding #${findingCounter} [${f.severity}] — ${f.name} (${f.rule})`);
|
|
627
|
+
lines.push(`- **Location:** \`${pr.page.relPath}:${f.line}\``);
|
|
628
|
+
lines.push(`- **Category:** ${f.category}`);
|
|
629
|
+
lines.push(`- **Violating Snippet:** \`${f.snippet}\``);
|
|
630
|
+
lines.push(`- **Remediation Advice:** ${f.fix}`);
|
|
631
|
+
lines.push(`- [ ] **Approve fix #${findingCounter}**`);
|
|
632
|
+
lines.push(``);
|
|
633
|
+
findingCounter++;
|
|
208
634
|
}
|
|
209
635
|
}
|
|
210
636
|
|
|
211
|
-
|
|
212
|
-
|
|
637
|
+
lines.push(`---`);
|
|
638
|
+
lines.push(``);
|
|
639
|
+
lines.push(`## Mode 2 Execution Workflow`);
|
|
640
|
+
lines.push(`To automatically remediate approved items:`);
|
|
641
|
+
lines.push(`\`\`\`bash`);
|
|
642
|
+
lines.push(`npx chill-agent audit --fix`);
|
|
643
|
+
lines.push(`\`\`\``);
|
|
644
|
+
|
|
645
|
+
fs.writeFileSync(reportPath, lines.join('\n'), 'utf8');
|
|
646
|
+
return { reportPath, fileName, nextIndex };
|
|
213
647
|
}
|
|
214
648
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
649
|
+
/**
|
|
650
|
+
* Format and print comprehensive audit summary to console.
|
|
651
|
+
*/
|
|
652
|
+
export function printAuditReport(findings, targetDir = process.cwd(), auditData = null) {
|
|
653
|
+
console.log(`\n${c.bold}ChillAgent Anti-Slop 2.0 & Ponytail Comprehensive Audit${c.reset}`);
|
|
654
|
+
console.log(`${c.dim}Target Workspace: ${targetDir}${c.reset}\n`);
|
|
218
655
|
|
|
219
|
-
if (findings.length === 0) {
|
|
220
|
-
console.log(`${c.
|
|
656
|
+
if (!findings || findings.length === 0) {
|
|
657
|
+
console.log(`${c.emerald}${c.bold}✔ Clean Codebase!${c.reset} No AI slop patterns or premature over-engineering found.`);
|
|
221
658
|
return;
|
|
222
659
|
}
|
|
223
660
|
|
|
@@ -226,20 +663,24 @@ export function printAuditReport(findings, targetDir = process.cwd()) {
|
|
|
226
663
|
const lowCount = findings.filter((f) => f.severity === 'LOW').length;
|
|
227
664
|
|
|
228
665
|
console.log(
|
|
229
|
-
`Found ${c.bold}${findings.length} infractions${c.reset} ` +
|
|
230
|
-
`(${c.coral}${highCount} High${c.reset}, ${c.amber}${medCount}
|
|
666
|
+
`Found ${c.bold}${findings.length} infractions${c.reset} across audited files ` +
|
|
667
|
+
`(${c.coral}${highCount} Hard Gate / High${c.reset}, ${c.amber}${medCount} Purpose-Gate / Med${c.reset}, ${c.gray}${lowCount} Quality Locks / Low${c.reset}):\n`
|
|
231
668
|
);
|
|
232
669
|
|
|
233
|
-
findings.forEach((f, idx) => {
|
|
670
|
+
findings.slice(0, 50).forEach((f, idx) => {
|
|
234
671
|
const sevColor = f.severity === 'HIGH' ? c.coral : f.severity === 'MEDIUM' ? c.amber : c.gray;
|
|
235
|
-
const relFile = path.relative(targetDir, f.file);
|
|
672
|
+
const relFile = path.relative(targetDir, f.file).replace(/\\/g, '/');
|
|
236
673
|
|
|
237
674
|
console.log(
|
|
238
675
|
`${c.bold}#${idx + 1}${c.reset} [${sevColor}${f.severity}${c.reset}] ` +
|
|
239
|
-
`${c.cyan}${f.
|
|
676
|
+
`${c.cyan}${f.rule || f.id}${c.reset} — ${c.bold}${f.name}${c.reset}`
|
|
240
677
|
);
|
|
241
|
-
console.log(` ${c.dim}Location:${c.reset} ${relFile}:${
|
|
678
|
+
console.log(` ${c.dim}Location:${c.reset} ${relFile}:${f.line}`);
|
|
242
679
|
console.log(` ${c.dim}Snippet:${c.reset} ${c.italic}"${f.snippet}"${c.reset}`);
|
|
243
680
|
console.log(` ${c.emerald}Fix:${c.reset} ${f.fix}\n`);
|
|
244
681
|
});
|
|
682
|
+
|
|
683
|
+
if (findings.length > 50) {
|
|
684
|
+
console.log(`${c.dim}... and ${findings.length - 50} more findings. Run with --report to generate a markdown audit report.${c.reset}\n`);
|
|
685
|
+
}
|
|
245
686
|
}
|