返回 AiToEarn
antipatterns.mjs
1 const ANTIPATTERNS = [
2 // ── AI slop: tells that something was AI-generated ──
3 {
4 id: 'side-tab',
5 category: 'slop',
6 name: 'Side-tab accent border',
7 description:
8 'Thick colored border on one side of a card — the most recognizable tell of AI-generated UIs. Use a subtler accent or remove it entirely.',
9 skillSection: 'Visual Details',
10 skillGuideline: 'colored accent stripe',
11 },
12 {
13 id: 'border-accent-on-rounded',
14 category: 'slop',
15 name: 'Border accent on rounded element',
16 description:
17 'Thick accent border on a rounded card — the border clashes with the rounded corners. Remove the border or the border-radius.',
18 skillSection: 'Visual Details',
19 skillGuideline: 'colored accent stripe',
20 },
21 {
22 id: 'overused-font',
23 category: 'slop',
24 name: 'Overused font',
25 description:
26 'Inter, Roboto, Fraunces, Geist, Plus Jakarta Sans, and Space Grotesk are used on so many sites they no longer feel distinctive. Each new wave of AI-generated UIs converges on the same handful of faces. Choose a face that gives your interface personality.',
27 skillSection: 'Typography',
28 skillGuideline: 'overused fonts like Inter',
29 },
30 {
31 id: 'single-font',
32 category: 'slop',
33 name: 'Single font for everything',
34 description:
35 'Only one font family is used for the entire page. Pair a distinctive display font with a refined body font to create typographic hierarchy.',
36 skillSection: 'Typography',
37 skillGuideline: 'only one font family for the entire page',
38 },
39 {
40 id: 'flat-type-hierarchy',
41 category: 'slop',
42 name: 'Flat type hierarchy',
43 description:
44 'Font sizes are too close together — no clear visual hierarchy. Use fewer sizes with more contrast (aim for at least a 1.25 ratio between steps).',
45 skillSection: 'Typography',
46 skillGuideline: 'flat type hierarchy',
47 },
48 {
49 id: 'gradient-text',
50 category: 'slop',
51 name: 'Gradient text',
52 description:
53 'Gradient text is decorative rather than meaningful — a common AI tell, especially on headings and metrics. Use solid colors for text.',
54 skillSection: 'Color & Contrast',
55 skillGuideline: 'gradient text for',
56 },
57 {
58 id: 'ai-color-palette',
59 category: 'slop',
60 name: 'AI color palette',
61 description:
62 'Purple/violet gradients and cyan-on-dark are the most recognizable tells of AI-generated UIs. Choose a distinctive, intentional palette.',
63 skillSection: 'Color & Contrast',
64 skillGuideline: 'AI color palette',
65 },
66 {
67 id: 'cream-palette',
68 category: 'slop',
69 name: 'Cream / beige palette',
70 description:
71 'A warm cream or beige page background has become the default "tasteful" AI surface, reached for by reflex. Choose a background that comes from a deliberate palette, not the safe warm off-white.',
72 skillSection: 'Color & Contrast',
73 skillGuideline: 'cream and beige as the default surface',
74 },
75 {
76 id: 'nested-cards',
77 category: 'slop',
78 name: 'Nested cards',
79 description:
80 'Cards inside cards create visual noise and excessive depth. Flatten the hierarchy — use spacing, typography, and dividers instead of nesting containers.',
81 skillSection: 'Layout & Space',
82 skillGuideline: 'Nest cards inside cards',
83 },
84 {
85 id: 'monotonous-spacing',
86 category: 'slop',
87 name: 'Monotonous spacing',
88 description:
89 'The same spacing value used everywhere — no rhythm, no variation. Use tight groupings for related items and generous separations between sections.',
90 skillSection: 'Layout & Space',
91 skillGuideline: 'same spacing everywhere',
92 },
93 {
94 id: 'bounce-easing',
95 category: 'slop',
96 name: 'Bounce or elastic easing',
97 description:
98 'Bounce and elastic easing feel dated and tacky. Real objects decelerate smoothly — use exponential easing (ease-out-quart/quint/expo) instead.',
99 skillSection: 'Motion',
100 skillGuideline: 'bounce or elastic easing',
101 },
102 {
103 id: 'dark-glow',
104 category: 'slop',
105 name: 'Dark mode with glowing accents',
106 description:
107 'Dark backgrounds with colored box-shadow glows are the default "cool" look of AI-generated UIs. Use subtle, purposeful lighting instead — or skip the dark theme entirely.',
108 skillSection: 'Color & Contrast',
109 skillGuideline: 'dark mode with glowing accents',
110 },
111 {
112 id: 'icon-tile-stack',
113 category: 'slop',
114 name: 'Icon tile stacked above heading',
115 description:
116 'A small rounded-square icon container above a heading is the universal AI feature-card template — every generator outputs this exact shape. Try a side-by-side icon and heading, or let the icon sit in flow without its own container.',
117 skillSection: 'Typography',
118 skillGuideline: 'large icons with rounded corners above every heading',
119 },
120 {
121 id: 'italic-serif-display',
122 category: 'slop',
123 name: 'Italic serif display headline',
124 description:
125 'Oversized italic serif (Fraunces, Recoleta, Playfair, Newsreader-italic) as the primary hero headline reads as taste in isolation but has become the universal AI-startup landing page hero. Set roman, or move to a non-serif display face. Editorial / magazine register may legitimately want this — judge by context.',
126 skillSection: 'Typography',
127 skillGuideline: 'oversized italic serif as the hero headline',
128 },
129 {
130 id: 'hero-eyebrow-chip',
131 category: 'slop',
132 name: 'Hero eyebrow / pill chip',
133 description:
134 'A tiny uppercase letter-spaced label sitting immediately above an oversized hero headline — or the same shape rendered as a pill chip — is now the default AI SaaS hero. Drop the eyebrow, integrate the kicker into the headline, or run it as a navigation breadcrumb instead.',
135 skillSection: 'Typography',
136 skillGuideline: 'tiny uppercase tracked label above the hero headline',
137 },
138 {
139 id: 'repeated-section-kickers',
140 category: 'slop',
141 severity: 'advisory',
142 name: 'Repeated section kicker labels',
143 description:
144 'Repeating tiny uppercase tracked labels above section headings turns a brand page into AI editorial scaffolding. Replace them with stronger structure, artifacts, imagery, or a deliberate brand system.',
145 skillSection: 'Typography',
146 skillGuideline: 'repeated eyebrow or kicker labels as section scaffolding',
147 },
148 {
149 id: 'numbered-section-markers',
150 category: 'slop',
151 severity: 'advisory',
152 name: 'Numbered section markers (01 / 02 / 03)',
153 description:
154 'Numbered display markers as section labels (01, 02, 03) are the AI editorial scaffold one tier deeper than tracked eyebrow chips. If you find yourself reaching for them, choose a different section cadence.',
155 skillSection: 'Layout & Space',
156 skillGuideline: 'numbered section markers',
157 },
158 {
159 id: 'em-dash-overuse',
160 category: 'slop',
161 name: 'Em-dash overuse',
162 description:
163 'More than two em-dashes (— or --) in body copy is an AI cadence tell. Use commas, colons, periods, or parentheses instead.',
164 skillSection: 'Copy',
165 skillGuideline: 'no em dashes',
166 },
167 {
168 id: 'marketing-buzzword',
169 category: 'slop',
170 name: 'Marketing buzzword',
171 description:
172 'Generic SaaS phrases (streamline / empower / supercharge / world-class / enterprise-grade / next-generation / cutting-edge / etc) are instant AI tells. Pick a specific verb and noun that says what the product literally does.',
173 skillSection: 'Copy',
174 skillGuideline: 'marketing buzzwords',
175 },
176 {
177 id: 'aphoristic-cadence',
178 category: 'slop',
179 name: 'Aphoristic-cadence copy',
180 description:
181 'Three or more sections landing on a short rebuttal sentence ("X. No Y." / "X. Just Y.") or a manufactured-contrast aphorism ("Not a feature. A platform.") reads as AI cadence, not voice. Once is fine; the pattern is the tell.',
182 skillSection: 'Copy',
183 skillGuideline: 'aphoristic cadence',
184 },
185 {
186 id: 'oversized-h1',
187 category: 'slop',
188 name: 'Oversized hero headline',
189 description:
190 'A full-sentence headline set at display size ends up dominating the viewport, leaving no room for anything else above the fold. A punchy one- or two-word headline at that size is fine — the problem is a long headline blown up too large. Set long headlines smaller, or tighten the copy.',
191 skillSection: 'Typography',
192 skillGuideline: 'long headline set at display size',
193 },
194 {
195 id: 'extreme-negative-tracking',
196 category: 'slop',
197 name: 'Crushed letter spacing',
198 description:
199 'Letter-spacing pulled tighter than the point where characters keep their own shapes costs legibility. Tighten display type optically, not destructively.',
200 skillSection: 'Typography',
201 skillGuideline: 'letter spacing crushed past legibility',
202 },
203 {
204 id: 'broken-image',
205 category: 'quality',
206 name: 'Broken or placeholder image',
207 description:
208 '<img> tags with empty src, missing src, or placeholder values ship as broken-image boxes. Use real images, generated assets, or remove the tag.',
209 skillSection: 'Imagery',
210 skillGuideline: 'broken image references',
211 },
212
213 // ── Quality: general design and accessibility issues ──
214 {
215 id: 'gray-on-color',
216 category: 'quality',
217 name: 'Gray text on colored background',
218 description:
219 'Gray text looks washed out on colored backgrounds. Use a darker shade of the background color instead, or white/near-white for contrast.',
220 skillSection: 'Color & Contrast',
221 skillGuideline: 'gray text on colored backgrounds',
222 },
223 {
224 id: 'low-contrast',
225 category: 'quality',
226 name: 'Low contrast text',
227 description:
228 'Text does not meet WCAG AA contrast requirements (4.5:1 for body, 3:1 for large text). Increase the contrast between text and background.',
229 },
230 {
231 id: 'layout-transition',
232 category: 'quality',
233 name: 'Layout property animation',
234 description:
235 'Animating width, height, padding, or margin causes layout thrash and janky performance. Use transform and opacity instead, or grid-template-rows for height animations.',
236 skillSection: 'Motion',
237 skillGuideline: 'Animate layout properties',
238 },
239 {
240 id: 'line-length',
241 category: 'quality',
242 name: 'Line length too long',
243 description:
244 'Text lines wider than ~80 characters are hard to read. The eye loses its place tracking back to the start of the next line. Add a max-width (65ch to 75ch) to text containers.',
245 skillSection: 'Layout & Space',
246 skillGuideline: 'wrap beyond ~80 characters',
247 },
248 {
249 id: 'cramped-padding',
250 category: 'quality',
251 name: 'Cramped padding',
252 description:
253 'Text is too close to the edge of its container. Two shapes: (1) an element with its own text where the padding is too low for the font size, and (2) a wrapper with text-bearing children and near-zero padding against a visible boundary (border, outline, or non-transparent background) — children land flush against the boundary line. Add at least 8px (ideally 12–16px) of padding inside bordered, outlined, or colored containers.',
254 skillSection: 'Layout & Space',
255 skillGuideline: 'inside bordered or colored containers',
256 },
257 {
258 id: 'body-text-viewport-edge',
259 category: 'quality',
260 name: 'Body text touching viewport edge',
261 description:
262 'Body paragraphs render flush against the left or right viewport edge with no container providing horizontal padding. Wrap content in a container with at least 16px (ideally 24-32px) of horizontal padding, or apply max-width with mx-auto.',
263 },
264 {
265 id: 'tight-leading',
266 category: 'quality',
267 name: 'Tight line height',
268 description:
269 'Line height below 1.3x the font size makes multi-line text hard to read. Use 1.5 to 1.7 for body text so lines have room to breathe.',
270 },
271 {
272 id: 'skipped-heading',
273 category: 'quality',
274 name: 'Skipped heading level',
275 description:
276 'Heading levels should not skip (e.g. h1 then h3 with no h2). Screen readers use heading hierarchy for navigation. Skipping levels breaks the document outline.',
277 },
278 {
279 id: 'justified-text',
280 category: 'quality',
281 name: 'Justified text',
282 description:
283 'Justified text without hyphenation creates uneven word spacing ("rivers of white"). Use text-align: left for body text, or enable hyphens: auto if you must justify.',
284 },
285 {
286 id: 'tiny-text',
287 category: 'quality',
288 name: 'Tiny body text',
289 description:
290 'Body text below 12px is hard to read, especially on high-DPI screens. Use at least 14px for body content, 16px is ideal.',
291 },
292 {
293 id: 'all-caps-body',
294 category: 'quality',
295 name: 'All-caps body text',
296 description:
297 'Long passages in uppercase are hard to read. We recognize words by shape (ascenders and descenders), which all-caps removes. Reserve uppercase for short labels and headings.',
298 skillSection: 'Typography',
299 skillGuideline: 'long body passages in uppercase',
300 },
301 {
302 id: 'wide-tracking',
303 category: 'quality',
304 name: 'Wide letter spacing on body text',
305 description:
306 'Letter spacing above 0.05em on body text disrupts natural character groupings and slows reading. Reserve wide tracking for short uppercase labels only.',
307 },
308 {
309 id: 'text-overflow',
310 category: 'quality',
311 name: 'Content overflowing its container',
312 description:
313 'Content renders wider than its container, spilling out or forcing a horizontal scrollbar. Let text wrap, constrain widths, or give the region a deliberate scroll affordance.',
314 skillSection: 'Layout & Space',
315 skillGuideline: 'content wider than its container',
316 },
317 {
318 id: 'clipped-overflow-container',
319 category: 'quality',
320 name: 'Positioned child clipped by overflow container',
321 description:
322 'A clipping container (overflow hidden or clip) wrapping an absolutely-positioned child cuts off tooltips, menus, and popovers that need to escape. Let the overflow be visible, or move the positioned layer out of the clip.',
323 skillSection: 'Layout & Space',
324 skillGuideline: 'overflow container clipping positioned children',
325 },
326
327 // ── Provider tells: opt-in via --gpt / --gemini (gated off by default) ──
328 {
329 id: 'gpt-thin-border-wide-shadow',
330 category: 'slop',
331 severity: 'advisory',
332 gated: 'gpt',
333 name: 'Hairline border with wide shadow',
334 description:
335 'A hairline border paired with a wide, diffuse shadow is a recurring generated-UI signature. Commit to one — a defined edge or a soft elevation — rather than both at once.',
336 skillSection: 'Visual Details',
337 skillGuideline: 'hairline border plus wide diffuse shadow',
338 },
339 {
340 id: 'repeating-stripes-gradient',
341 category: 'slop',
342 severity: 'advisory',
343 gated: 'gpt',
344 name: 'Repeating-gradient stripes',
345 description:
346 'Repeating-gradient stripes used as surface decoration are a recurring generated-UI signature. Reach for a deliberate texture or leave the surface plain.',
347 skillSection: 'Visual Details',
348 skillGuideline: 'repeating-gradient decorative stripes',
349 },
350 {
351 id: 'theater-slop-phrase',
352 category: 'slop',
353 severity: 'advisory',
354 gated: 'gpt',
355 name: 'Theater framing copy',
356 description:
357 'Dismissing something as "theater" is a recurring generated-copy tic. Say plainly what the thing does or does not do.',
358 skillSection: 'Copy',
359 skillGuideline: 'theater framing copy',
360 },
361 {
362 id: 'image-hover-transform',
363 category: 'slop',
364 severity: 'advisory',
365 gated: 'gemini',
366 name: 'Image hover transform',
367 description:
368 'Scaling or rotating an image on hover is a recurring generated-UI signature. Let imagery sit still, or use a subtler, purposeful interaction.',
369 skillSection: 'Motion',
370 skillGuideline: 'image scale or rotate on hover',
371 },
372 ];
373
374 const RULE_ENGINE_SUPPORT = {
375 regex: new Set(['source', 'page-analyzer']),
376 'static-html': new Set(['element', 'page']),
377 browser: new Set(['element', 'page', 'layout']),
378 visual: new Set(['visual-contrast']),
379 };
380
381 function getAntipattern(id) {
382 return ANTIPATTERNS.find(rule => rule.id === id);
383 }
384
385 function getRulesForCategory(category) {
386 return ANTIPATTERNS.filter(rule => rule.category === category);
387 }
388
389 function getRuleEngineSupport(engine) {
390 return RULE_ENGINE_SUPPORT[engine] || new Set();
391 }
392
393 // Set of provider tags that gate rules off by default (e.g. 'gpt', 'gemini').
394 const GATED_PROVIDERS = new Set(
395 ANTIPATTERNS.map(rule => rule.gated).filter(Boolean),
396 );
397
398 // Drop findings for rules gated behind a provider tag unless that provider
399 // was explicitly enabled (CLI --gpt / --gemini). Non-gated findings always
400 // pass through. `findings` carry the rule id on `.antipattern`.
401 function filterByProviders(findings, providers = []) {
402 const enabled = new Set(providers || []);
403 if (!GATED_PROVIDERS.size) return findings;
404 return findings.filter(f => {
405 const rule = getAntipattern(f.antipattern);
406 if (!rule || !rule.gated) return true;
407 return enabled.has(rule.gated);
408 });
409 }
410
411 export {
412 ANTIPATTERNS,
413 RULE_ENGINE_SUPPORT,
414 GATED_PROVIDERS,
415 getAntipattern,
416 getRulesForCategory,
417 getRuleEngineSupport,
418 filterByProviders,
419 };
420
420 lines Plain Text