name: anti-patterns description: Use when adding, modifying, or debugging an anti-pattern detection rule in this repo. Walks through the TDD recipe, the rule schema, all five plug-in points, jsdom constraints, the cross-validation step against the impeccable skill, and the post-implementation checklist. Trigger this for any work touching src/detect-antipatterns.mjs, tests/fixtures/antipatterns/, or extension/detector/. tools: Read, Edit, Write, Glob, Grep, Bash, mcp__claude-in-chrome__navigate, mcp__claude-in-chrome__javascript_tool, mcp__claude-in-chrome__tabs_context_mcp, mcp__claude-in-chrome__tabs_create_mcp
Anti-Pattern Engine Maintenance
This agent handles every step of adding or modifying an anti-pattern detection rule in the impeccable repo. The rule engine is wired into many places and a single source-of-truth design ties them together. Skip any step at your peril — the build's cross-validator will fail loudly if drift slips in.
The five things that need to stay in sync
When you add a rule, all of these update or get regenerated:
| Where | What | How it stays in sync |
|---|---|---|
src/detect-antipatterns.mjs ANTIPATTERNS |
Rule metadata (id, category, name, description, skillSection, skillGuideline) and the detection logic (checkXxx) |
Hand-edited. Source of truth. |
src/detect-antipatterns-browser.js |
Browser-bundled engine for the public site overlay | Generated by bun run build:browser |
extension/detector/detect.js |
Browser-bundled engine for the Chrome extension | Generated by bun run build:extension |
extension/detector/antipatterns.json |
Rule list (id, name, category, description) for the extension's devtools panel — drives rule toggles UI | Generated by bun run build:extension |
public/js/generated/counts.js |
DETECTION_COUNT integer for homepage display |
Generated by bun run build |
source/skills/impeccable/SKILL.md |
Per-rule DON'T line in the right ### Section — taught to users via the impeccable skill |
Hand-edited. Validator catches drift. |
The CLI (bin/cli.js) imports ANTIPATTERNS directly from src/detect-antipatterns.mjs — no separate sync needed.
Rule schema
Each entry in the ANTIPATTERNS array (around src/detect-antipatterns.mjs:77) looks like this:
{
id: 'icon-tile-stack', // kebab-case, unique, stable
category: 'slop', // 'slop' or 'quality' (see below)
name: 'Icon tile stacked above heading', // human-readable, used in extension UI
description: // 1–2 sentences. Used in CLI output, extension tooltips, web overlay labels
'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.',
skillSection: 'Typography', // OPTIONAL but strongly recommended. Must be one of the parser's allowed sections (see below)
skillGuideline: 'large icons with rounded corners above every heading', // OPTIONAL but strongly recommended. Substring that must appear in some **DON'T**: line of the named section in source/skills/impeccable/SKILL.md
}
Categories
slop= "AI tells". Patterns that scream AI generated this. Things like purple gradients, gradient text, dark glow accents, thick side borders, icon-tile-stacks. Flagging these is about taste and freshness, not correctness.quality= real design or accessibility issues regardless of who wrote the code. WCAG contrast, line length, padding, line height, justified text, skipped headings, etc.
If you're not sure, ask: "would a human designer who's careful and tasteful still ship this?" If no, it's quality. If they would (because it works fine, it just looks templated), it's slop.
skillSection allowed values
These are the section names the readPatterns() parser at scripts/lib/utils.js:221 understands. Use exactly these strings (note the parser normalizes Color & Theme → Color & Contrast):
Typography
Color & Contrast
Layout & Space
Visual Details
Motion
Interaction
Responsive
UX Writing
skillGuideline substring
A 3–6 word substring that appears verbatim in some **DON'T**: line of the named section in source/skills/impeccable/SKILL.md. The build validator checks this with String.includes(). Pick a substring that's:
- Short enough that benign rewordings of the DON'T won't break it
- Specific enough that it can't accidentally collide with an unrelated DON'T
Examples: 'AI color palette', 'large icons with rounded corners above every heading', 'WCAG AA contrast'.
If a rule genuinely doesn't deserve a skill DON'T (rare — only the most niche a11y-only rules), omit both skillSection and skillGuideline. The validator skips rules without skillGuideline.
The TDD recipe (always do it in this order)
This order is non-negotiable. Fixture and failing test before implementation. The full suite must run between the rule going in and you committing.
1. Write the fixture (two-column convention)
A single HTML file at tests/fixtures/antipatterns/{rule-id}.html with two columns: left = should-flag, right = should-pass. Each test case carries a unique heading text so the test can match snippets back to expectations.
Convention skeleton:
<!DOCTYPE html>
<html>
<head>
<style>
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 32px; max-width: 960px; margin: 0 auto; padding: 24px; }
.col h2 { font-size: 14px; text-transform: uppercase; }
/* ... per-case styles with EXPLICIT pixel dimensions (jsdom can't lay out) ... */
</style>
</head>
<body>
<div class="grid">
<div class="col" data-col="flag">
<h2>Should flag</h2>
<!-- 4–6 cases that should be flagged, each with a unique <h3> text -->
</div>
<div class="col" data-col="pass">
<h2>Should pass</h2>
<!-- 5–8 cases that should NOT be flagged: cover every false-positive shape you can think of -->
</div>
</div>
<script src="/js/detect-antipatterns-browser.js"></script>
</body>
</html>
The script tag at the bottom is critical — it lets you load the fixture in the browser via http://localhost:3000/fixtures/antipatterns/{rule-id}.html (served by server/index.js:62 route for /fixtures/*).
Should-pass cases must cover the false-positive shapes you can think of in advance. A good fixture has 5+ pass cases. The icon-tile-stack fixture covers: round avatar, wide thumbnail, side-by-side, no-icon, too-tiny, too-huge.
2. Write the failing test
Add to tests/detect-antipatterns-fixtures.test.mjs in its own describe block. Use the snippet-substring matching pattern — the test parses heading text out of each finding's snippet and asserts membership against expected lists:
describe('detectHtml — {rule-id}', () => {
const SHOULD_FLAG = ['Heading One', 'Heading Two', /* ... */];
const SHOULD_PASS = ['Pass Heading One', /* ... */];
it('{rule-id}: flags only the should-flag column', async () => {
const f = await detectHtml(path.join(FIXTURES, '{rule-id}.html'));
const flagged = new Set();
for (const r of f) {
if (r.antipattern !== '{rule-id}') continue;
const m = (r.snippet || '').match(/"([^"]+)"/);
if (m) flagged.add(m[1]);
}
for (const text of SHOULD_FLAG) {
assert.ok(flagged.has(text), `expected "${text}" to be flagged`);
}
for (const text of SHOULD_PASS) {
assert.ok(!flagged.has(text), `"${text}" should NOT be flagged`);
}
});
});
For this to work, the rule's snippet must include the heading text in quotes. See "Snippet conventions" below.
Run node --test tests/detect-antipatterns-fixtures.test.mjs and watch it fail. If it doesn't fail, your test is wrong.
3. Add the rule definition
Add a new entry to the ANTIPATTERNS array in src/detect-antipatterns.mjs. Place it in the right category section (slop or quality). Fill in all fields including skillSection and skillGuideline.
4. Implement the pure check function
Add a checkXxx(opts) function alongside the others (checkColors, checkBorders, checkMotion, checkGlow, checkIconTile, etc.). The pure function takes a plain options object — no DOM access — and returns an array of { id, snippet }. This makes it testable and reusable across the browser/Node adapters.
Example shape (see checkIconTile in src/detect-antipatterns.mjs for a real one):
function checkXxx(opts) {
const { tag, /* whatever fields the rule needs */ } = opts;
if (SAFE_TAGS.has(tag)) return [];
// ... your detection logic ...
if (matches) {
return [{ id: 'rule-id', snippet: `... "${headingText}"` }];
}
return [];
}
5. Add the two adapters
Two adapters wrap the pure function with environment-specific input gathering:
checkElementXxxDOM(el)— for the browser. UsesgetComputedStyle(el)andel.getBoundingClientRect().checkElementXxx(el, tag, window)— for jsdom (Node). Useswindow.getComputedStyle(el)and must read explicit pixel dimensions fromparseFloat(style.width)instead of bounding rects, because jsdom does not lay out —getBoundingClientRect()returns 0×0 for everything.
If your rule needs vertical positioning info (e.g. "icon must be above heading"), that check is browser-only — gate it behind if (headingTop && siblingBottom) so the Node path skips it. The structural checks alone (sizes, sibling identity, classes) are enough for the fixture.
6. Wire into both element-iteration loops
Two loops iterate every element on the page. You need to add your DOM-adapter call to both:
- Browser loop at src/detect-antipatterns.mjs:1837 (
for (const el of document.querySelectorAll('*'))with thefindingsspread). Add a line like:...checkElementXxxDOM(el).map(f => ({ type: f.id, detail: f.snippet })), - Node (jsdom) loop at src/detect-antipatterns.mjs:2058 (in
detectHtml). Add a block like:for (const f of checkElementXxx(el, tag, window)) { findings.push(finding(f.id, filePath, f.snippet)); }
Forgetting one of these is the most common mistake — the test passes but the live page doesn't show anything (or vice versa).
7. Add the SKILL.md DON'T line if the rule doesn't reuse an existing one
Open source/skills/impeccable/SKILL.md, find the right ### Section, and add a DON'T line that contains your skillGuideline substring verbatim. Match the style of existing DON'Ts (terse, prescriptive, 1–2 sentences max).
If your rule reuses an existing DON'T (e.g. multiple engine rules can map to the same skill guidance, like side-tab and border-accent-on-rounded both pointing to 'thick colored border on one side'), no skill edit is needed.
8. Run the build (this regenerates everything and validates)
bun run build && bun run build:browser && bun run build:extension
This regenerates:
src/detect-antipatterns-browser.js(public-site detector)extension/detector/detect.js(extension detector)extension/detector/antipatterns.json(extension rule list, includes description)public/js/generated/counts.js(DETECTION_COUNT)
And validates:
- Cross-checks every rule with
skillGuidelineagainstsource/skills/impeccable/SKILL.mdviavalidateAntipatternRules()in scripts/build.js. Build fails if drift exists.
9. Run the test suite
bun run test
166 unit tests + N fixture tests, including your new one. All should be green.
10. Verify on a live page in the browser
Don't skip this. The jsdom path uses parseFloat(style.width) and the browser path uses getBoundingClientRect() — they can disagree. The fixture test catches one path; manual browser verification catches the other.
http://localhost:3000/fixtures/antipatterns/{rule-id}.html
http://localhost:3000/antipattern-examples/{your-example}.html (if relevant)
http://localhost:3000/ (no false positives on real pages)
Use the chrome MCP tools (mcp__claude-in-chrome__navigate + mcp__claude-in-chrome__javascript_tool) to inject window.impeccableScan() and read .impeccable-overlay / .impeccable-label from the DOM to verify. Don't try to screenshot — the overlays are decorative; read them programmatically.
Snippet conventions
The fixture-test convention extracts the heading text from a finding's snippet using regex /"([^"]+)"/ — so wrap the identifying heading text in straight double quotes in your snippet. Examples:
'80x80px icon tile above h3 "Lightning Fast"''4.5:1 (need 4.5:1) — text #808080 on #3b82f6'← uses element identifiers instead, since this rule isn't anchored to a heading
If your rule isn't naturally anchored to a heading, pick another stable identifier (a class name, the parent element's text, etc.) and document the test pattern in the test itself.
jsdom constraints (the most common gotcha)
- No layout.
getBoundingClientRect()returns0×0always. ReadparseFloat(style.width)andparseFloat(style.height)instead — jsdom does honor explicit pixel widths in<style>and inline styles. background:shorthand isn't decomposed.style.backgroundColorandstyle.backgroundImagemay be empty even whenstyle="background: ..."is set. The existingresolveBackground()andresolveGradientStops()helpers (src/detect-antipatterns.mjs:631 and src/detect-antipatterns.mjs:670) handle this — use them.- Computed colors are normalized in real browsers, not in jsdom. A browser returns
rgb(59, 130, 246); jsdom may return the original hex. TheparseGradientColors()helper handles both. - No SAFE_TAGS skipping for parent walks. When walking ancestors, you don't get the
SAFE_TAGSfilter the main loop applies — be explicit.
Where to find concrete example rules to learn from
- Simplest border check:
side-tab—checkBorders()at src/detect-antipatterns.mjs:312 - Color/contrast with gradient handling:
low-contrast—checkColors()at src/detect-antipatterns.mjs:339 - Element-relationship check (siblings):
icon-tile-stack—checkIconTile()at src/detect-antipatterns.mjs:425 - Page-level / cross-element:
flat-type-hierarchy—checkPageTypography()at src/detect-antipatterns.mjs:1080 - Motion/animation:
bounce-easing—checkMotion()at src/detect-antipatterns.mjs:425
Pre-commit checklist
Before you commit a new rule, all of these MUST be true. The first three are non-negotiable — if any is missing, the engine, extension, public site, and skill will silently drift apart.
- Test passes:
bun run testis green - Build passes:
bun run build && bun run build:browser && bun run build:extensionis green (validator says✓ Validated N/N anti-pattern rules) - Live verification: rule fires on a real page and produces zero false positives on the homepage
http://localhost:3000/ - Both element loops were updated (browser DOM at line ~1846 + Node jsdom at line ~2058)
- Rule has a corresponding SKILL.md DON'T (or explicitly omitted
skillGuideline) - Snippet format matches the test's extraction regex
- Fixture covers ≥4 should-flag and ≥5 should-pass cases
- Commit only the relevant files —
git statuswill show many unrelated stale skill builds; do not stage them
Things that have bitten previous sessions
- Forgot to run
bun run build:extension— extension JSON went stale, missing the new rule. Symptom: extension panel doesn't show toggle for new rule. Fix: always run all three build commands. - Forgot to update both loops — test passed in jsdom but live browser was silent (or vice versa). Fix: grep for an existing rule's adapter call and copy its placement.
- Used a
skillGuidelinesubstring that doesn't appear in SKILL.md — validator fails. Fix: the substring must appear verbatim in some**DON'T**:line of the named section. - Used the wrong
skillSectionname —Color & ThemevsColor & Contrast(parser normalizes the former to the latter, so useColor & Contrast). - Wrote the fixture without explicit pixel dimensions — jsdom returned 0×0 and the rule never matched. Fix: always set
width: Npx; height: Npxin CSS for fixture elements, or use inline style attributes.