diff --git a/design-html/SKILL.md b/design-html/SKILL.md
index 64b51226f..f26c676c9 100644
--- a/design-html/SKILL.md
+++ b/design-html/SKILL.md
@@ -699,9 +699,15 @@ For framework output, save to:
**Never include (AI slop blacklist):**
- Purple/blue gradients as default
+- Cream-and-serif default palette
+- Gradient text
- Generic 3-column feature grids
+- Identical card grids, nested cards
- Center-everything layouts with no visual hierarchy
+- Kickers or icon tiles above headings
+- Hero metric rows ("10k+ users")
- Decorative blobs, waves, or geometric patterns not in the mockup
+- Glowing edges or pulsing status dots
- Stock photo placeholder divs
- "Get Started" / "Learn More" generic CTAs not from the mockup
- Rounded-corner cards with drop shadows as the default component
@@ -709,7 +715,7 @@ For framework output, save to:
- Generic testimonial sections
- Cookie-cutter hero sections with left-text right-image
-The comment on each line is the pattern's id in gstack's design catalog (`lib/design-catalog.ts`); the catalog is the authoritative name for these, and the same ids come back from the design detector.
+Each `` is the pattern's id in `lib/design-catalog.ts`; the design detector reports the same ids.
---
diff --git a/design-html/SKILL.md.tmpl b/design-html/SKILL.md.tmpl
index 9667ea56f..b5c731eff 100644
--- a/design-html/SKILL.md.tmpl
+++ b/design-html/SKILL.md.tmpl
@@ -279,9 +279,15 @@ For framework output, save to:
**Never include (AI slop blacklist):**
- Purple/blue gradients as default
+- Cream-and-serif default palette
+- Gradient text
- Generic 3-column feature grids
+- Identical card grids, nested cards
- Center-everything layouts with no visual hierarchy
+- Kickers or icon tiles above headings
+- Hero metric rows ("10k+ users")
- Decorative blobs, waves, or geometric patterns not in the mockup
+- Glowing edges or pulsing status dots
- Stock photo placeholder divs
- "Get Started" / "Learn More" generic CTAs not from the mockup
- Rounded-corner cards with drop shadows as the default component
@@ -289,7 +295,7 @@ For framework output, save to:
- Generic testimonial sections
- Cookie-cutter hero sections with left-text right-image
-The comment on each line is the pattern's id in gstack's design catalog (`lib/design-catalog.ts`); the catalog is the authoritative name for these, and the same ids come back from the design detector.
+Each `` is the pattern's id in `lib/design-catalog.ts`; the design detector reports the same ids.
---
diff --git a/design/src/brief.ts b/design/src/brief.ts
index 6ebcae6c8..5748491f0 100644
--- a/design/src/brief.ts
+++ b/design/src/brief.ts
@@ -1,6 +1,7 @@
/**
* Structured design brief — the interface between skill prose and image generation.
*/
+import { MOCKUP_NEVER_NAMES } from "../../lib/design-catalog";
export interface DesignBrief {
goal: string; // "Dashboard for coding assessment tool"
@@ -31,6 +32,10 @@ export function briefToPrompt(brief: DesignBrief): string {
lines.push(`Design reference: ${brief.reference}`);
}
+ // Generation-time slop guard: the catalog's mockupNever names, so the model
+ // never reaches for purple gradients, icon tiles, or cream defaults on its own.
+ lines.push(`Never: ${MOCKUP_NEVER_NAMES.join(", ")}.`);
+
lines.push(
"The mockup should look like a real production UI, not a wireframe or concept art.",
"All text must be readable. Layout must be clean and intentional.",
diff --git a/design/test/brief.test.ts b/design/test/brief.test.ts
new file mode 100644
index 000000000..527443cf0
--- /dev/null
+++ b/design/test/brief.test.ts
@@ -0,0 +1,44 @@
+/**
+ * briefToPrompt carries the catalog's generation-time slop guard.
+ *
+ * The "Never:" line is built from MOCKUP_NEVER_NAMES (lib/design-catalog.ts),
+ * so the image model is told up front what not to reach for. The catalog test
+ * owns the "exactly ten ids" invariant; this one pins the prompt shape.
+ */
+import { describe, expect, test } from "bun:test";
+import { briefToPrompt, type DesignBrief } from "../src/brief";
+import { MOCKUP_NEVER_NAMES } from "../../lib/design-catalog";
+
+const brief: DesignBrief = {
+ goal: "Dashboard for a coding assessment tool",
+ audience: "Technical users",
+ style: "Dark theme, minimal",
+ elements: ["builder name", "score badge"],
+ screenType: "desktop-dashboard",
+};
+
+describe("briefToPrompt", () => {
+ test("carries a Never: line listing every MOCKUP_NEVER_NAMES entry, before the fixed tail", () => {
+ const prompt = briefToPrompt(brief);
+ const never = `Never: ${MOCKUP_NEVER_NAMES.join(", ")}.`;
+ expect(prompt).toContain(never);
+ expect(MOCKUP_NEVER_NAMES.length).toBeGreaterThanOrEqual(8);
+ for (const name of MOCKUP_NEVER_NAMES) expect(prompt).toContain(name);
+ expect(prompt.indexOf(never)).toBeLessThan(prompt.indexOf("The mockup should look like a real production UI"));
+ expect(prompt.indexOf(never)).toBeGreaterThan(prompt.indexOf("Required elements:"));
+ });
+
+ test("names are plain English: no hyphenated rule ids leak into the prompt", () => {
+ const prompt = briefToPrompt(brief);
+ expect(prompt).not.toMatch(/\b[a-z]+(-[a-z]+)+\b(?=[,.])/);
+ for (const name of MOCKUP_NEVER_NAMES) expect(name).not.toMatch(/^[a-z0-9]+(-[a-z0-9]+)+$/);
+ });
+
+ test("optional fields still render around the guard", () => {
+ const prompt = briefToPrompt({ ...brief, constraints: "Max width 1024px", reference: "DESIGN.md excerpt" });
+ expect(prompt).toContain("Constraints: Max width 1024px.");
+ expect(prompt).toContain("Design reference: DESIGN.md excerpt");
+ expect(prompt).toContain("Never: ");
+ expect(prompt.endsWith("1536x1024 pixels.")).toBe(true);
+ });
+});
diff --git a/lib/design-catalog.ts b/lib/design-catalog.ts
index 78fe3adb2..04bae52d9 100644
--- a/lib/design-catalog.ts
+++ b/lib/design-catalog.ts
@@ -84,6 +84,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
category: 'color', kind: 'slop', detect: ['engine', 'grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
heuristic: 'Look for `linear-gradient` with values in the `#6366f1` to `#8b5cf6` range, or CSS custom properties resolving to purple/violet.',
handoff: 'colorize', source: 'both', legacyBlacklist: true,
+ mockupNever: true,
},
{
id: 'feature-grid-3col', name: 'The 3-column feature grid',
@@ -181,18 +182,21 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
category: 'color', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
heuristic: 'Grep for `background-clip: text` next to a gradient background.',
handoff: 'colorize', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'cream-palette', impeccableId: 'cream-palette', name: 'Cream default palette',
prose: 'Cream ground, serif display, terracotta accent: look number one. Fine when the brief asked for it; a default when it did not.',
category: 'color', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
handoff: 'colorize', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'nested-cards', impeccableId: 'nested-cards', name: 'Nested cards',
prose: 'A card inside a card is always wrong. Cards are the lazy container; nesting them is the lazy container squared.',
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
handoff: 'layout', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'monotonous-spacing', impeccableId: 'monotonous-spacing', name: 'Monotonous spacing',
@@ -213,6 +217,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
category: 'motion', kind: 'slop', detect: ['engine', 'grep'], confidence: 'MEDIUM', tier: 'ask', impact: 'polish',
heuristic: 'Grep for infinite keyframe animations on small round elements.',
handoff: 'animate', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'blinking-cursor', impeccableId: 'blinking-cursor', name: 'Blinking cursor effect',
@@ -232,6 +237,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
category: 'surface', kind: 'slop', detect: ['engine', 'grep'], confidence: 'HIGH', tier: 'ask', impact: 'medium',
heuristic: 'Grep `box-shadow` for a zero x/y offset with a large blur and a saturated color.',
handoff: 'colorize', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'radial-halo', impeccableId: 'radial-halo', name: 'Radial halo',
@@ -256,6 +262,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
prose: 'The rounded-square icon above every heading. Try side by side, or drop the container.',
category: 'scaffold', kind: 'slop', detect: ['engine', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
handoff: 'layout', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'italic-serif-display', impeccableId: 'italic-serif-display', name: 'Italic serif display',
@@ -275,6 +282,7 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
category: 'scaffold', kind: 'slop', detect: ['engine', 'grep', 'llm'], confidence: 'MEDIUM', tier: 'ask', impact: 'medium',
heuristic: 'Look for a short uppercase, tracked element immediately before an h1 or h2.',
handoff: 'layout', source: 'impeccable',
+ mockupNever: true,
},
{
id: 'numbered-section-labels', impeccableId: 'numbered-section-labels', name: 'Numbered section labels',
@@ -392,12 +400,14 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
prose: 'Three big numbers with tiny labels under the hero ("10k+ users", "99.9%"). The template counts, not the product.',
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
handoff: 'clarify', source: 'gstack',
+ mockupNever: true,
},
{
id: 'identical-cards', name: 'Identical card grids',
prose: 'A grid of cards with the same shape, the same icon slot, the same two lines. Content of unequal weight given equal boxes.',
category: 'scaffold', kind: 'slop', detect: ['llm'], confidence: 'LOW', tier: 'ask', impact: 'medium',
handoff: 'layout', source: 'gstack',
+ mockupNever: true,
},
{
id: 'glassmorphism', name: 'Glassmorphism',
@@ -636,6 +646,18 @@ export const DESIGN_SLOP_CATALOG: DesignSlopEntry[] = [
},
];
+// ── Mockup prompt ──
+
+/**
+ * Plain-English names of the entries flagged `mockupNever`, deduped: the design
+ * binary appends "Never: ." to every image-generation prompt
+ * (design/src/brief.ts) so mockups stop reaching for these before the
+ * comparison board opens. Exactly ten ids carry the flag (test-enforced).
+ */
+export const MOCKUP_NEVER_NAMES: readonly string[] = [
+ ...new Set(DESIGN_SLOP_CATALOG.filter(e => e.mockupNever).map(e => e.name)),
+];
+
// ── Fonts ──
/** Never the display voice on Persuade/Experience; the detector flags several as `overused-font`. */
diff --git a/test/design-catalog.test.ts b/test/design-catalog.test.ts
index 9a94425ed..f7772456f 100644
--- a/test/design-catalog.test.ts
+++ b/test/design-catalog.test.ts
@@ -12,7 +12,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { spawnSync } from 'child_process';
import {
- DESIGN_SLOP_CATALOG, HANDOFF_COMMANDS, OVERUSED_FONTS_DISPLAY, BANNED_FONTS,
+ DESIGN_SLOP_CATALOG, HANDOFF_COMMANDS, OVERUSED_FONTS_DISPLAY, BANNED_FONTS, MOCKUP_NEVER_NAMES,
FONTS_BODY_UI_OK, FONTS_MONO_OK, FONTS_VERIFIED_FREE,
catalogEntry, entryForImpeccableId, renderCatalog, selectCatalog,
} from '../lib/design-catalog';
@@ -178,18 +178,47 @@ describe('renderCatalog', () => {
});
});
+const MOCKUP_NEVER_IDS = ['kicker-above-heading', 'icon-tile-stack', 'gradient-text', 'ai-color-palette', 'cream-palette', 'nested-cards', 'dark-glow', 'pulsing-dot', 'identical-cards', 'hero-metrics'];
+
+function designHtmlNeverIds(): string[] {
+ const tmpl = fs.readFileSync(path.join(ROOT, 'design-html', 'SKILL.md.tmpl'), 'utf-8');
+ const start = tmpl.indexOf('**Never include (AI slop blacklist):**');
+ expect(start).toBeGreaterThan(0);
+ const block = tmpl.slice(start, tmpl.indexOf('\n\n', start + 10));
+ const lines = block.split('\n').filter(l => l.startsWith('- '));
+ expect(lines.length).toBeGreaterThanOrEqual(10);
+ const ids: string[] = [];
+ for (const line of lines) {
+ const found = [...line.matchAll(//g)].map(m => m[1]);
+ expect(found.length, line).toBeGreaterThan(0);
+ ids.push(...found);
+ }
+ return ids;
+}
+
describe('design-html blacklist is derived-by-test (decision 31)', () => {
test('every on the Never-include list names a catalog entry', () => {
- const tmpl = fs.readFileSync(path.join(ROOT, 'design-html', 'SKILL.md.tmpl'), 'utf-8');
- const start = tmpl.indexOf('**Never include (AI slop blacklist):**');
- expect(start).toBeGreaterThan(0);
- const block = tmpl.slice(start, tmpl.indexOf('\n\n', start + 10));
- const lines = block.split('\n').filter(l => l.startsWith('- '));
- expect(lines.length).toBeGreaterThanOrEqual(10);
- for (const line of lines) {
- const m = line.match(/$/);
- expect(m, line).not.toBeNull();
- expect(catalogEntry(m![1]), m![1]).toBeDefined();
+ for (const id of designHtmlNeverIds()) expect(catalogEntry(id), id).toBeDefined();
+ });
+
+ test('every mockupNever entry appears on the Never-include list', () => {
+ const ids = new Set(designHtmlNeverIds());
+ for (const id of MOCKUP_NEVER_IDS) expect(ids.has(id), id).toBe(true);
+ });
+});
+
+describe('mockupNever → MOCKUP_NEVER_NAMES (generation-time slop guard)', () => {
+ test('exactly the ten agreed ids carry the flag', () => {
+ const flagged = DESIGN_SLOP_CATALOG.filter(e => e.mockupNever).map(e => e.id).sort();
+ expect(flagged).toEqual([...MOCKUP_NEVER_IDS].sort());
+ });
+
+ test('names are deduped plain English with no hyphenated ids', () => {
+ expect(new Set(MOCKUP_NEVER_NAMES).size).toBe(MOCKUP_NEVER_NAMES.length);
+ expect(MOCKUP_NEVER_NAMES.length).toBe(10);
+ for (const n of MOCKUP_NEVER_NAMES) {
+ expect(n).not.toMatch(/^[a-z0-9]+(-[a-z0-9]+)+$/);
+ expect(n[0]).toMatch(/[A-Z"0-9]/);
}
});
});