# Detector rules

> Deterministic rule registry: ids, slop vs quality categories, advisory rules, scopes, engines (jsdom, browser, regex), and finding shape for CLI and extension.

- Repository: pbakaus/impeccable
- GitHub: https://github.com/pbakaus/impeccable
- Human docs: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4
- Complete Markdown: https://grok-wiki.com/public/docs/pbakaus-impeccable-adadc04d8de4/llms-full.txt

## Source Files

- `cli/engine/registry/antipatterns.mjs`
- `cli/engine/rules/checks.mjs`
- `cli/engine/detect-antipatterns.mjs`
- `cli/engine/findings.mjs`
- `cli/engine/engines/static-html/detect-html.mjs`
- `cli/engine/engines/regex/detect-text.mjs`
- `tests/detect-antipatterns-fixtures.test.mjs`

---

---
title: Detector rules
description: Deterministic rule registry — ids, slop vs quality categories, advisory behavior, scopes, engines, and finding shape for CLI and extension.
---

The detector is a deterministic anti-pattern registry plus pure checks, wired into three scan engines (static HTML, regex source, and browser/URL) and packaged for the CLI, Chrome extension, and design hook. Rule metadata lives in one place; check logic and adapters stay separate.

## Source of truth

| Surface | Role |
|---|---|
| `cli/engine/registry/antipatterns.mjs` | Canonical `ANTIPATTERNS` array, scopes, advisory set, engine capability tags |
| `cli/engine/rules/checks.mjs` | Pure `checkXxx` functions plus DOM/static adapters |
| `cli/engine/findings.mjs` | Builds the shared finding object from a rule id |
| `cli/engine/detect-antipatterns.mjs` | Public facade re-exporting registry, engines, and helpers |
| `cli/engine/engines/static-html/detect-html.mjs` | Default path for `.html` / `.htm` |
| `cli/engine/engines/regex/detect-text.mjs` | Source/CSS/JSX path and page-level text analyzers |
| `cli/engine/engines/browser/detect-url.mjs` | Puppeteer URL scans via injected browser bundle |
| `cli/engine/engines/visual/screenshot-contrast.mjs` | Pixel contrast fallback for `low-contrast` |
| `cli/engine/detect-antipatterns-browser.js` | Generated browser bundle (`bun run build:browser`) |
| `extension/detector/` | Generated extension detector + `antipatterns.json` (`bun run build:extension`) |

Do not edit generated browser or extension detector files by hand. Change the registry and checks, then rebuild.

## Architecture

```mermaid
flowchart LR
  CLI["impeccable detect"] --> Main["cli/engine/cli/main.mjs"]
  Main -->|".html / .htm"| Static["static-html engine"]
  Main -->|"other scannable files"| Regex["regex engine"]
  Main -->|"http(s) / file URLs"| Browser["browser engine"]
  Static --> Checks["rules/checks.mjs"]
  Regex --> Checks
  Browser --> Bundle["detect-antipatterns-browser.js"]
  Bundle --> Checks
  Browser --> Visual["visual contrast"]
  Checks --> Finding["findings.mjs"]
  Finding --> Registry["registry/antipatterns.mjs"]
  Finding --> Out["text / JSON findings"]
```

Pure checks return lightweight hits `{ id, snippet }` (sometimes with per-finding `severity`). `finding(id, filePath, snippet, line)` expands each hit with registry metadata.

## Registry entry shape

Each rule in `ANTIPATTERNS` is a plain object:

| Field | Required | Meaning |
|---|---|---|
| `id` | yes | Stable kebab-case rule id (used in CLI output, ignores, and JSON) |
| `category` | yes | `slop` or `quality` |
| `name` | yes | Short human title |
| `description` | yes | What was found and what to do instead |
| `scopes` | no | Domain tags: `type`, `layout` (used by `--scope`) |
| `severity` | no | Default finding severity: `warning` (default), `error`, or `advisory` |
| `advisory` | no | When `true`, finding is partitioned as non-failing advisory |
| `skillSection` | no | Optional skill-doc section label |
| `skillGuideline` | no | Optional skill-doc guideline phrase |

Helpers exported with the registry:

- `getAntipattern(id)`
- `getRulesForCategory(category)`
- `isAdvisoryRule(id)` / `ADVISORY_RULE_IDS`
- `filterByScopes(findings, scopes)`
- `RULE_SCOPES` — currently `type` and `layout`
- `RULE_ENGINE_SUPPORT` — which engine phases each engine claims

## Categories

| Category | Intent | Count (current registry) |
|---|---|---|
| `slop` | Recognizable AI-generated UI or copy tells | 32 |
| `quality` | Design, legibility, a11y, or correctness issues | 27 |

**Total: 59 rules.** Counts are whatever `ANTIPATTERNS.length` is in source; do not hard-code them in feature PRs if the registry has moved.

### Slop rules

`side-tab`, `border-accent-on-rounded`, `overused-font`, `flat-type-hierarchy`, `gradient-text`, `ai-color-palette`, `cream-palette`, `nested-cards`, `monotonous-spacing`, `bounce-easing`, `pulsing-dot`, `blinking-cursor`, `shape-assembled-illustration`, `dark-glow`, `radial-halo`, `radial-spotlight-glow`, `marquee`, `icon-tile-stack`, `italic-serif-display`, `hero-eyebrow-chip`, `kicker-above-heading`, `numbered-section-labels`, `em-dash-overuse`, `marketing-buzzword`, `aphoristic-cadence`, `oversized-h1`, `extreme-negative-tracking`, `gpt-thin-border-wide-shadow`, `repeating-stripes-gradient`, `codex-grid-background`, `theater-slop-phrase`, `image-hover-transform`

### Quality rules

`broken-image`, `script-error`, `content-hidden-at-rest`, `edge-flush-cards`, `text-occlusion`, `first-viewport-column-overflow`, `gray-on-color`, `low-contrast`, `layout-transition`, `line-length`, `cramped-padding`, `body-text-viewport-edge`, `tight-leading`, `skipped-heading`, `heading-rhythm`, `justified-text`, `tiny-text`, `undersized-ui-text`, `all-caps-body`, `wide-tracking`, `text-overflow`, `repeated-container-text`, `clipped-overflow-container`, `design-system-font`, `design-system-color`, `design-system-radius`, `design-system-font-size`

## Severity vs advisory

Two separate registry fields. Do not collapse them.

### `severity` (label)

Default is `warning` when omitted.

| Value | Typical use |
|---|---|
| `warning` | Most rules |
| `error` | Hard failures: `script-error`, `content-hidden-at-rest` |
| `advisory` | Soft label on several generated-UI / design-system rules (still **can** count as failures unless also `advisory: true`) |

Rules with `severity: 'advisory'` today include: `blinking-cursor`, `shape-assembled-illustration`, `numbered-section-labels`, `design-system-color`, `design-system-radius`, `design-system-font-size`, `gpt-thin-border-wide-shadow`, `repeating-stripes-gradient`, `codex-grid-background`, `theater-slop-phrase`, `image-hover-transform`.

Checks may **promote** severity per finding (for example a pulsing status dot in a header/nav landmark), overriding the registry default on that finding only.

### `advisory: true` (non-failing partition)

Only rules with `advisory: true` are treated as automation-safe notes:

- Listed under a separate “Advisory (not counted as failures)” section in text output
- Excluded from the failure count that drives exit code
- Carry `"advisory": true` in JSON
- Skipped by the design hook unless the project sets `detector.advisoryRules: "include"`
- Hidden entirely with CLI `--no-advisory`

Current registry member: **`em-dash-overuse` only**.

```bash
# Advisory-only findings still exit 0
impeccable detect path/to/file.html

# Hide advisory notes completely
impeccable detect --no-advisory .
```

## Scopes

Optional `scopes` tags on rules enable CLI filtering:

```bash
impeccable detect --scope type src/
impeccable detect --scope layout,type .
```

| Scope | Domain |
|---|---|
| `type` | Typography / copy hierarchy rules |
| `layout` | Spacing, containers, overflow, column balance |

Rules without `scopes` never match `--scope` filters (they are dropped when any scope is requested). Empty scope list means no filtering.

Valid values come from `RULE_SCOPES`. Unknown values error the CLI.

## Engines

`RULE_ENGINE_SUPPORT` documents engine phases, not a per-rule matrix:

| Engine | Phases | When it runs |
|---|---|---|
| `regex` | `source`, `page-analyzer` | Non-HTML scannable files; also style blocks / CSS-in-JS / text analyzers |
| `static-html` | `element`, `page` | `.html` / `.htm` via custom cascade (not a live browser layout) |
| `browser` | `element`, `page`, `layout` | `http(s)://` and `file://` targets via Puppeteer |
| `visual` | `visual-contrast` | Browser URL path: contrast candidates + optional screenshot pixel check |

### Target routing (CLI)

| Target | Engine |
|---|---|
| `.html`, `.htm` | `detectHtml` (static-html) |
| Other scannable extensions | `detectText` (regex) |
| URL | `detectUrl` (browser + optional visual) |

Scannable extensions include: `.html`, `.htm`, `.css`, `.scss`, `.sass`, `.less`, `.jsx`, `.tsx`, `.js`, `.ts`, `.vue`, `.svelte`, `.astro`, `.blade.php`.

### Engine capabilities in practice

**Static HTML**

- Parses HTML, collects local CSS, builds a static style map
- Runs element rules (borders, colors, glow, motion, icon tiles, quality, and related)
- On full pages: typography, kickers, numbered labels, layout, cream palette, HTML pattern scans, and text-content analyzers
- Line numbers are generally absent; whole-file inline disables still apply
- Falls back to regex if parser imports fail

**Regex**

- Line and block matchers for classes, CSS, Tailwind, CSS-in-JS
- Extracts `<style>` blocks from Astro/Vue/Svelte and CSS-in-JS templates
- Page analyzers on full-page markup for selected rules (for example flat type hierarchy, em-dash, buzzwords, dark-glow)
- Preserves line numbers for ignore-line comments

**Browser**

- Injects the generated browser bundle and calls `window.impeccableDetect`
- Layout-dependent rules (occlusion, edge-flush cards, first-viewport column overflow, true computed layout)
- `script-error` from `pageerror` events
- `content-hidden-at-rest` after a reveal sweep
- Visual contrast fallback when computed color analysis is insufficient

**Deprecated CLI flags**

- `--fast` is accepted but ignored (full scan is the only path)
- `--gpt` / `--gemini` are ignored; generated-UI tells run by default

## Finding shape

Every consumer (CLI text/JSON, hook, tests) uses the same base object from `finding()`:

<ResponseField name="antipattern" type="string">
Rule id from the registry.
</ResponseField>

<ResponseField name="name" type="string">
Registry display name.
</ResponseField>

<ResponseField name="description" type="string">
Registry remediation-oriented description.
</ResponseField>

<ResponseField name="severity" type="string">
`warning` by default, or registry / per-finding override (`error`, `advisory`, …).
</ResponseField>

<ResponseField name="category" type="string | null">
`slop` or `quality`.
</ResponseField>

<ResponseField name="file" type="string">
File path or URL scanned.
</ResponseField>

<ResponseField name="line" type="number">
1-based line when known; `0` when unknown (common for static-html).
</ResponseField>

<ResponseField name="snippet" type="string">
Short evidence string; fixture tests often quote a heading inside straight double quotes.
</ResponseField>

<ResponseField name="advisory" type="boolean">
Present only when `true` (registry `advisory: true`).
</ResponseField>

Optional extras some paths attach:

- `ignoreValue` — value-scoped ignore token from browser serialization
- `importedBy` — import-graph note when a finding is attributed via imports

### Example JSON finding

```json
{
  "antipattern": "side-tab",
  "name": "Side-tab accent border",
  "description": "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.",
  "severity": "warning",
  "category": "slop",
  "file": "src/components/Card.tsx",
  "line": 42,
  "snippet": "border-left: 4px solid #7c3aed"
}
```

Advisory example (`em-dash-overuse`):

```json
{
  "antipattern": "em-dash-overuse",
  "name": "Em-dash overuse",
  "description": "Em-dash saturation in body copy is an AI cadence tell. ...",
  "severity": "warning",
  "category": "slop",
  "file": "index.html",
  "line": 0,
  "snippet": "12 em-dashes in body text",
  "advisory": true
}
```

### Text output shape

```text
path/to/file.html
  line 12: [side-tab] "Feature Card" — thick left border
    → Thick colored border on one side of a card …

2 anti-patterns found.

── Advisory (not counted as failures) ──
path/to/file.html
  [em-dash-overuse] 9 em-dashes in body text
    → Em-dash saturation in body copy …

1 advisory note. Suppress with --no-advisory.
```

### Exit codes

| Code | Meaning |
|---|---|
| `0` | No non-advisory findings (advisory-only still exits 0) |
| `2` | One or more non-advisory findings |
| `1` | CLI usage / argument errors |

## Design-system rules

When DESIGN.md / `.impeccable/design.json` is loaded (default; disable with `--no-design-system` or config):

| Id | Severity | Notes |
|---|---|---|
| `design-system-font` | warning | Font not declared in DESIGN.md |
| `design-system-color` | severity advisory | Color outside palette/ramps |
| `design-system-radius` | severity advisory | Radius outside scale |
| `design-system-font-size` | severity advisory | Size off type ramp |

These are quality-category design-system drift checks, not the non-failing `advisory: true` partition (unless a future registry change sets that flag).

## Browser-only or layout-heavy rules

Some quality rules need real layout or runtime behavior and primarily appear on URL scans:

| Id | Why browser |
|---|---|
| `script-error` | Uncaught page errors during load |
| `content-hidden-at-rest` | Opacity/visibility after reveal sweep |
| `text-occlusion` | Overlapping painted text |
| `edge-flush-cards` | Scroller edge clipping |
| `first-viewport-column-overflow` | Multi-column fold imbalance |
| `low-contrast` (visual path) | Screenshot / visual fallback when computed contrast is incomplete |

Static HTML still covers many quality rules that only need computed style from the cascade (for example `tiny-text`, `cramped-padding`, `skipped-heading`).

## Extension packaging

`bun run build:extension` regenerates:

- `extension/detector/detect.js` — browser detector logic
- `extension/detector/antipatterns.json` — registry metadata for the DevTools panel UI

The extension panel loads `antipatterns.json` for names/descriptions; scan logic uses the same rule ids as the CLI.

## Check implementation pattern

Rules follow a fixed TDD shape (see contributing docs for the full order):

1. Fixture under `tests/fixtures/antipatterns/{rule-id}.html` with should-flag / should-pass columns
2. Fixture assertions in `tests/detect-antipatterns-fixtures.test.mjs` (snippet-substring via quoted headings)
3. Registry entry in `ANTIPATTERNS`
4. Pure `checkXxx(opts)` returning `[{ id, snippet }]`
5. Two adapters: DOM (`getComputedStyle` / geometry) and static/jsdom-style (explicit CSS dimensions; no layout)
6. Wire both adapters into **both** element loops (browser bundle path and static-html path)

Reference implementations in `checks.mjs`: `side-tab` (border), `low-contrast` (color), `icon-tile-stack` (sibling), `flat-type-hierarchy` (page-level), `kicker-above-heading` (heading-anchored).

After rule changes:

```bash
bun run build && bun run build:browser && bun run build:extension && bun run test
```

Fixture suite (Node, not Bun for the jsdom-era HTML fixtures):

```bash
node --test tests/detect-antipatterns-fixtures.test.mjs
```

## CLI usage related to rules

```bash
# Full tree scan
impeccable detect src/

# Domain filter
impeccable detect --scope type components/

# Machine-readable findings (single array; advisory flagged in place)
impeccable detect --json --quiet .

# Drop advisory partition
impeccable detect --no-advisory src/

# Bypass config, DESIGN.md, and inline disables
impeccable detect --no-config page.html

# Live page (requires puppeteer)
impeccable detect --viewport 390x844 https://example.com
```

Ignores (project config and inline comments) filter by rule id and optional value tokens. See the ignores and configuration pages for `ignoreRules`, `ignoreFiles`, `ignoreValues`, and `impeccable-disable*` comments.

## Troubleshooting

| Symptom | Likely cause |
|---|---|
| Rule fires in browser but not on static HTML | Needs layout/runtime (occlusion, script errors, reveal state) |
| Rule fires on CSS file but not HTML | HTML path uses different adapters; check static element/page wiring |
| Advisory note still appears but CI is green | Expected for `advisory: true` findings |
| `severity: "advisory"` finding fails CI | Severity label ≠ non-failing partition; only `advisory: true` is excluded |
| Hook silent on em-dash findings | Hook defaults `detector.advisoryRules: "exclude"` |
| `--scope type` drops many findings | Unscoped rules are filtered out when any scope is set |
| URL scan fails with puppeteer missing | Install puppeteer; URL mode is optional for file scans |
| Extension / browser missing new rule | Forgot `build:browser` / `build:extension` after registry change |

## Related pages

<CardGroup cols={2}>
  <Card title="CLI reference" href="/cli-reference">
    detect flags, JSON output, exit codes, and URL scanning.
  </Card>
  <Card title="Manage detector ignores" href="/manage-detector-ignores">
    ignoreRules, ignoreFiles, ignoreValues, and inline disable comments.
  </Card>
  <Card title="Detect in CI" href="/detect-in-ci">
    Non-interactive detect, exit codes 0 and 2, optional Puppeteer.
  </Card>
  <Card title="Design hook" href="/design-hook">
    Per-edit scans, advisory exclusion, and intentional-finding workflow.
  </Card>
  <Card title="Configuration reference" href="/configuration-reference">
    detector.advisoryRules and related config keys.
  </Card>
  <Card title="Contributing" href="/contributing">
    Anti-pattern TDD order and build obligations for new rules.
  </Card>
</CardGroup>
