diff --git a/README.md b/README.md index ba3de66c0..bc20c26cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Impeccable -Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 44 deterministic detector rules for AI-generated frontend design. +Design guidance for AI coding agents. 1 skill, 23 commands, live browser iteration, and 45 deterministic detector rules for AI-generated frontend design. > **Quick start:** From your project root, run `npx impeccable install`, then run `/impeccable init` inside your AI coding tool. Full docs: [impeccable.style](https://impeccable.style). @@ -13,7 +13,7 @@ Every model trained on the same SaaS templates. Skip the guidance and you get th Impeccable adds: - **One setup flow.** `/impeccable init` writes `PRODUCT.md` and offers `DESIGN.md`, so later commands know the audience, brand/product lane, voice, anti-references, colors, type, and components. - **23 commands.** A shared design vocabulary with your AI: `polish`, `audit`, `critique`, `distill`, `animate`, `bolder`, `quieter`, and more. -- **44 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. +- **45 deterministic detector rules** plus LLM-only critique checks. The CLI and browser extension run the deterministic rules with no LLM and no API key. ## What's Included @@ -341,7 +341,7 @@ npx impeccable ignores add-file "src/legacy/**" npx impeccable ignores add-value overused-font Inter --reason "Brand font" ``` -The detector catches 44 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more). +The detector catches 45 deterministic issues across AI slop (side-tab borders, purple gradients, bounce easing, dark glows) and general design quality (line length, cramped padding, small touch targets, skipped headings, and more). By default, `detect` respects the same `.impeccable/config.json` and `.impeccable/config.local.json` detector config as the design hook: `detector.ignoreRules`, `detector.ignoreFiles`, `detector.ignoreValues`, and `detector.designSystem.enabled`. Hook lifecycle settings such as `hook.enabled` only affect automatic hook execution. diff --git a/README.npm.md b/README.npm.md index 2142dda11..6255bb353 100644 --- a/README.npm.md +++ b/README.npm.md @@ -1,6 +1,6 @@ # Impeccable CLI -Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 44 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems. +Detect UI anti-patterns and design quality issues from the command line. Scans HTML, CSS, JSX, TSX, Vue, and Svelte files for 45 deterministic rules, including AI-generated UI tells, accessibility violations, and general design quality problems. ## Quick Start @@ -56,7 +56,7 @@ npx impeccable detect --fast src/ **Quality**: tiny body text, cramped padding, long line lengths, small touch targets -44 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop). +45 deterministic detector rules in total. See the full catalog at [impeccable.style/slop](https://impeccable.style/slop). ## Exit Codes diff --git a/cli/engine/detect-antipatterns-browser.js b/cli/engine/detect-antipatterns-browser.js index b2091c609..3bfc2de47 100644 --- a/cli/engine/detect-antipatterns-browser.js +++ b/cli/engine/detect-antipatterns-browser.js @@ -478,6 +478,17 @@ const ANTIPATTERNS = [ skillSection: 'Visual Details', skillGuideline: 'repeating-gradient decorative stripes', }, + { + id: 'codex-grid-background', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Decorative grid-line background', + description: + 'A two-axis grid drawn with hairline linear-gradient layers ("1px, transparent 1px" on both axes) is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.', + skillSection: 'Visual Details', + skillGuideline: 'two-axis grid-line gradient background', + }, { id: 'theater-slop-phrase', category: 'slop', @@ -1172,6 +1183,42 @@ function checkHtmlPatterns(html) { findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); } + // --- Provider tells (gated): two-axis grid-line background (Codex/GPT) --- + // The Codex grid tell is two hairline `linear-gradient(... 1px, + // transparent 1px)` layers (one per axis) tiled by a repeating + // `background-size` cell. Both signals must co-occur in the SAME style block + // (a CSS rule body or one inline `style="..."`): two hairline stops WITHOUT a + // tiling background-size is a fixed crosshair, not a grid, and a single + // hairline is a legitimate ruled line. Scoping to one block also stops + // unrelated single-axis rules on separate elements from adding up across the + // page. Count hairlines only inside `background`/`background-image` values so + // a hairline in an unrelated property (mask-image, border-image) can't stand + // in for the second axis. Colors like `oklch(96% 0.012 82 / 0.055)` carry + // nested parens, so match the hairline stop directly rather than parsing + // whole gradient layers. + { + const hairlineRe = /\b\d{1,3}px\s*,\s*transparent\s+\d{1,3}px/gi; + const gridSizeRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\b/i; + const bgDeclRe = /\bbackground(?:-image)?\s*:\s*([^;{}"']*)/gi; + const blockRe = /\{([^{}]*)\}|style\s*=\s*"([^"]*)"|style\s*=\s*'([^']*)'/gi; + let blk; + while ((blk = blockRe.exec(html)) !== null) { + const block = blk[1] || blk[2] || blk[3] || ''; + if (!gridSizeRe.test(block)) continue; + let hairlineCount = 0; + let bm; + bgDeclRe.lastIndex = 0; + while ((bm = bgDeclRe.exec(block)) !== null) { + const stops = bm[1].match(hairlineRe); + if (stops) hairlineCount += stops.length; + } + if (hairlineCount >= 2) { + findings.push({ id: 'codex-grid-background', snippet: 'two-axis grid-line gradient background' }); + break; + } + } + } + // --- Provider tells (gated): "X theater" framing copy (GPT) --- // Lives here (regex-on-HTML) rather than in the text-content analyzers so it // runs in the bundled browser path too, not just the CLI/static path. diff --git a/cli/engine/registry/antipatterns.mjs b/cli/engine/registry/antipatterns.mjs index 99eb2da04..ec1045022 100644 --- a/cli/engine/registry/antipatterns.mjs +++ b/cli/engine/registry/antipatterns.mjs @@ -376,6 +376,17 @@ const ANTIPATTERNS = [ skillSection: 'Visual Details', skillGuideline: 'repeating-gradient decorative stripes', }, + { + id: 'codex-grid-background', + category: 'slop', + severity: 'advisory', + gated: 'gpt', + name: 'Decorative grid-line background', + description: + 'A two-axis grid drawn with hairline linear-gradient layers ("1px, transparent 1px" on both axes) is a recurring generated-UI signature. Reserve grid overlays for actual canvas, map, blueprint, or measurement surfaces; elsewhere use product structure or a plain surface.', + skillSection: 'Visual Details', + skillGuideline: 'two-axis grid-line gradient background', + }, { id: 'theater-slop-phrase', category: 'slop', diff --git a/cli/engine/rules/checks.mjs b/cli/engine/rules/checks.mjs index 747d9a10f..13bcdc224 100644 --- a/cli/engine/rules/checks.mjs +++ b/cli/engine/rules/checks.mjs @@ -573,6 +573,42 @@ function checkHtmlPatterns(html) { findings.push({ id: 'repeating-stripes-gradient', snippet: 'repeating-gradient decorative stripes' }); } + // --- Provider tells (gated): two-axis grid-line background (Codex/GPT) --- + // The Codex grid tell is two hairline `linear-gradient(... 1px, + // transparent 1px)` layers (one per axis) tiled by a repeating + // `background-size` cell. Both signals must co-occur in the SAME style block + // (a CSS rule body or one inline `style="..."`): two hairline stops WITHOUT a + // tiling background-size is a fixed crosshair, not a grid, and a single + // hairline is a legitimate ruled line. Scoping to one block also stops + // unrelated single-axis rules on separate elements from adding up across the + // page. Count hairlines only inside `background`/`background-image` values so + // a hairline in an unrelated property (mask-image, border-image) can't stand + // in for the second axis. Colors like `oklch(96% 0.012 82 / 0.055)` carry + // nested parens, so match the hairline stop directly rather than parsing + // whole gradient layers. + { + const hairlineRe = /\b\d{1,3}px\s*,\s*transparent\s+\d{1,3}px/gi; + const gridSizeRe = /background-size\s*:[^;{}"']*\b\d{1,3}px\b/i; + const bgDeclRe = /\bbackground(?:-image)?\s*:\s*([^;{}"']*)/gi; + const blockRe = /\{([^{}]*)\}|style\s*=\s*"([^"]*)"|style\s*=\s*'([^']*)'/gi; + let blk; + while ((blk = blockRe.exec(html)) !== null) { + const block = blk[1] || blk[2] || blk[3] || ''; + if (!gridSizeRe.test(block)) continue; + let hairlineCount = 0; + let bm; + bgDeclRe.lastIndex = 0; + while ((bm = bgDeclRe.exec(block)) !== null) { + const stops = bm[1].match(hairlineRe); + if (stops) hairlineCount += stops.length; + } + if (hairlineCount >= 2) { + findings.push({ id: 'codex-grid-background', snippet: 'two-axis grid-line gradient background' }); + break; + } + } + } + // --- Provider tells (gated): "X theater" framing copy (GPT) --- // Lives here (regex-on-HTML) rather than in the text-content analyzers so it // runs in the bundled browser path too, not just the CLI/static path. diff --git a/site/pages/index.astro b/site/pages/index.astro index fa3fd55d6..6d2340392 100644 --- a/site/pages/index.astro +++ b/site/pages/index.astro @@ -521,7 +521,7 @@ import '../styles/testimonials.css';
06

Block slop before it ships.

-

A detector you can wire into PR checks. 44 deterministic rules, no LLM, exit codes the build can read.

+

A detector you can wire into PR checks. 45 deterministic rules, no LLM, exit codes the build can read.

@@ -799,7 +799,7 @@ import '../styles/testimonials.css';
  • CLI for CI - npx impeccable detect src/ in a PR check. 44 deterministic rules. JSON output, exit codes for build gates. + npx impeccable detect src/ in a PR check. 45 deterministic rules. JSON output, exit codes for build gates. View on npm →
  • diff --git a/tests/detect-antipatterns-fixtures.test.mjs b/tests/detect-antipatterns-fixtures.test.mjs index 752342895..573f11c80 100644 --- a/tests/detect-antipatterns-fixtures.test.mjs +++ b/tests/detect-antipatterns-fixtures.test.mjs @@ -744,7 +744,7 @@ describe('detectHtml — cream-palette', () => { }); describe('detectHtml — gated provider tells (--gpt / --gemini)', () => { - const GPT_IDS = ['gpt-thin-border-wide-shadow', 'repeating-stripes-gradient', 'theater-slop-phrase']; + const GPT_IDS = ['gpt-thin-border-wide-shadow', 'repeating-stripes-gradient', 'codex-grid-background', 'theater-slop-phrase']; it('gpt-tells: gated OFF by default — none of the GPT idioms surface', async () => { const f = await detectHtml(path.join(FIXTURES, 'gpt-tells.html')); @@ -756,7 +756,7 @@ describe('detectHtml — gated provider tells (--gpt / --gemini)', () => { } }); - it('gpt-tells: with providers:[gpt], flag column triggers all three, pass column adds none', async () => { + it('gpt-tells: with providers:[gpt], each flag case triggers once, pass column adds none', async () => { const f = await detectHtml(path.join(FIXTURES, 'gpt-tells.html'), { providers: ['gpt'] }); for (const id of GPT_IDS) { assert.equal( diff --git a/tests/fixtures/antipatterns/gpt-tells.html b/tests/fixtures/antipatterns/gpt-tells.html index 9b7cea681..68125799f 100644 --- a/tests/fixtures/antipatterns/gpt-tells.html +++ b/tests/fixtures/antipatterns/gpt-tells.html @@ -9,6 +9,7 @@ .col { padding: 16px; } .card { padding: 24px; margin: 0 0 24px; border-radius: 12px; } .stripes { height: 80px; margin: 0 0 24px; border-radius: 12px; } + .grid { height: 80px; margin: 0 0 24px; border-radius: 12px; } @@ -19,6 +20,7 @@ Thin hairline border paired with a wide diffuse shadow.
  • +

    We retired the growth theater and shipped something that actually works.

    @@ -34,6 +36,10 @@ OKLCH shadow hue values should not be mistaken for shadow blur.
    +
    +
    +
    +

    We measured real outcomes for the people who use this every day.