Add codex-grid-background detector rule (#328)

* Add codex-grid-background detector rule

Detects the Codex two-axis grid-line background tell: a single background
value carrying two or more hairline `linear-gradient(... 1px, transparent
1px)` layers (one per axis), usually paired with a repeating
`background-size` cell. Gated behind --gpt like the sibling codex tells,
off by default.

Counts hairline stops within a single background declaration (not across
the page) so unrelated single-axis ruled lines don't add up to a false
flag, and matches the stop directly rather than parsing whole gradient
layers, since colors like oklch(...) carry nested parens.

Extends the gpt-tells fixture with one flag case and two pass cases
(single-axis rule, two-color blend), regenerates the browser detector
bundle, and bumps the rule count 44 -> 45.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Require tiling background-size for codex-grid-background

Address review: two hairline gradients alone draw a fixed crosshair, not a
grid. Scope detection to a single style block (CSS rule body or inline
style attr) and require both >=2 hairline stops AND a tiling
`background-size` px cell in the same block, matching the skill rule's
"plus background-size" wording. Add a crosshair-without-tiling pass case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Scope codex-grid-background hairline count to background values

Address review: count hairline stops only inside background/background-image
declaration values, not the whole style block, so a hairline in an unrelated
property (mask-image, border-image) can't stand in for the grid's second
axis. Add a bg+mask-image hairline pass case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-07-01 01:29:40 -07:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 5844c40177
commit f5c1bd65ae
8 changed files with 109 additions and 9 deletions
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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
+47
View File
@@ -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(... <color> 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.
+11
View File
@@ -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',
+36
View File
@@ -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(... <color> 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.
+2 -2
View File
@@ -521,7 +521,7 @@ import '../styles/testimonials.css';
<article class="ks-bento-tile ks-bento-tile--span-6" id="why-ci">
<span class="ks-bento-num" data-color="patina">06</span>
<h3 class="why-panel-title">Block slop before it ships.</h3>
<p class="why-panel-body">A detector you can wire into PR checks. 44 deterministic rules, no LLM, exit codes the build can read.</p>
<p class="why-panel-body">A detector you can wire into PR checks. 45 deterministic rules, no LLM, exit codes the build can read.</p>
<div class="why-visual why-visual--ci">
<div class="why-ci-window">
<div class="why-ci-header">
@@ -799,7 +799,7 @@ import '../styles/testimonials.css';
</li>
<li>
<strong>CLI for CI</strong>
<span><code>npx impeccable detect src/</code> in a PR check. 44 deterministic rules. JSON output, exit codes for build gates.</span>
<span><code>npx impeccable detect src/</code> in a PR check. 45 deterministic rules. JSON output, exit codes for build gates.</span>
<a href="https://www.npmjs.com/package/impeccable" target="_blank" rel="noopener">View on npm →</a>
</li>
<li>
+2 -2
View File
@@ -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(
+6
View File
@@ -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; }
</style>
</head>
<body>
@@ -19,6 +20,7 @@
Thin hairline border paired with a wide diffuse shadow.
</div>
<div class="stripes flag-repeating-stripes" style="background: repeating-linear-gradient(45deg, #eee, #eee 10px, #fafafa 10px, #fafafa 20px)"></div>
<div class="grid flag-codex-grid" style="background: linear-gradient(90deg, oklch(96% 0.012 82 / 0.055) 1px, transparent 1px), linear-gradient(180deg, oklch(96% 0.012 82 / 0.05) 1px, transparent 1px); background-size: 78px 78px, 78px 78px;"></div>
<p class="flag-theater">We retired the growth theater and shipped something that actually works.</p>
</section>
@@ -34,6 +36,10 @@
OKLCH shadow hue values should not be mistaken for shadow blur.
</div>
<div class="stripes pass-plain-gradient" style="background: linear-gradient(180deg, #f3f4f6, #ffffff)"></div>
<div class="grid pass-single-axis-rule" style="background: linear-gradient(180deg, rgba(15, 23, 42, 0.06) 1px, transparent 1px); background-size: 100% 32px"></div>
<div class="grid pass-two-color-blend" style="background: linear-gradient(90deg, #f3f4f6, #e5e7eb), linear-gradient(180deg, #eef2ff, #ffffff)"></div>
<div class="grid pass-crosshair-no-tiling" style="background: linear-gradient(90deg, rgba(15, 23, 42, 0.06) 1px, transparent 1px), linear-gradient(180deg, rgba(15, 23, 42, 0.06) 1px, transparent 1px)"></div>
<div class="grid pass-bg-plus-mask-hairline" style="background: linear-gradient(90deg, rgba(15, 23, 42, 0.06) 1px, transparent 1px); background-size: 32px 32px; mask-image: linear-gradient(180deg, #000 1px, transparent 1px)"></div>
<p class="pass-no-theater">We measured real outcomes for the people who use this every day.</p>
</section>
</div>