mirror of
https://github.com/garrytan/gstack.git
synced 2026-09-09 06:28:59 +02:00
feat(design): catalog never-lines in the mockup prompt
Ten catalog ids carry `mockupNever` (kicker-above-heading, icon-tile-stack, gradient-text, ai-color-palette, cream-palette, nested-cards, dark-glow, pulsing-dot, identical-cards, hero-metrics) and lib/design-catalog.ts exports their deduped plain-English names as MOCKUP_NEVER_NAMES. briefToPrompt() in the design binary appends "Never: <names>." before its fixed tail, so `$D generate | variants | evolve` stop reaching for purple gradients, icon tiles, and cream defaults before the comparison board opens. The binary still bundles (`bun build --compile design/src/cli.ts`); ./setup rebuilds it. design-html's Never-include list now covers every mockupNever id (kicker / icon tile, hero metric rows, gradient text, cream palette, nested and identical cards, glow and pulsing dots), each line tagged with its catalog ids; test/design-catalog.test.ts pins the exact ten flags, the deduped names, and that the template list is a superset. New design/test/brief.test.ts pins the prompt shape. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3522073ef0
commit
d3c3ea27b9
@@ -699,9 +699,15 @@ For framework output, save to:
|
||||
|
||||
**Never include (AI slop blacklist):**
|
||||
- Purple/blue gradients as default <!-- ai-color-palette -->
|
||||
- Cream-and-serif default palette <!-- cream-palette -->
|
||||
- Gradient text <!-- gradient-text -->
|
||||
- Generic 3-column feature grids <!-- feature-grid-3col -->
|
||||
- Identical card grids, nested cards <!-- identical-cards --> <!-- nested-cards -->
|
||||
- Center-everything layouts with no visual hierarchy <!-- centered-everything -->
|
||||
- Kickers or icon tiles above headings <!-- kicker-above-heading --> <!-- icon-tile-stack -->
|
||||
- Hero metric rows ("10k+ users") <!-- hero-metrics -->
|
||||
- Decorative blobs, waves, or geometric patterns not in the mockup <!-- decorative-blobs -->
|
||||
- Glowing edges or pulsing status dots <!-- dark-glow --> <!-- pulsing-dot -->
|
||||
- Stock photo placeholder divs <!-- stock-photo-hero -->
|
||||
- "Get Started" / "Learn More" generic CTAs not from the mockup <!-- generic-cta-copy -->
|
||||
- Rounded-corner cards with drop shadows as the default component <!-- card-default-component -->
|
||||
@@ -709,7 +715,7 @@ For framework output, save to:
|
||||
- Generic testimonial sections <!-- generic-testimonials -->
|
||||
- Cookie-cutter hero sections with left-text right-image <!-- split-hero-template -->
|
||||
|
||||
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 `<!-- id -->` is the pattern's id in `lib/design-catalog.ts`; the design detector reports the same ids.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -279,9 +279,15 @@ For framework output, save to:
|
||||
|
||||
**Never include (AI slop blacklist):**
|
||||
- Purple/blue gradients as default <!-- ai-color-palette -->
|
||||
- Cream-and-serif default palette <!-- cream-palette -->
|
||||
- Gradient text <!-- gradient-text -->
|
||||
- Generic 3-column feature grids <!-- feature-grid-3col -->
|
||||
- Identical card grids, nested cards <!-- identical-cards --> <!-- nested-cards -->
|
||||
- Center-everything layouts with no visual hierarchy <!-- centered-everything -->
|
||||
- Kickers or icon tiles above headings <!-- kicker-above-heading --> <!-- icon-tile-stack -->
|
||||
- Hero metric rows ("10k+ users") <!-- hero-metrics -->
|
||||
- Decorative blobs, waves, or geometric patterns not in the mockup <!-- decorative-blobs -->
|
||||
- Glowing edges or pulsing status dots <!-- dark-glow --> <!-- pulsing-dot -->
|
||||
- Stock photo placeholder divs <!-- stock-photo-hero -->
|
||||
- "Get Started" / "Learn More" generic CTAs not from the mockup <!-- generic-cta-copy -->
|
||||
- Rounded-corner cards with drop shadows as the default component <!-- card-default-component -->
|
||||
@@ -289,7 +295,7 @@ For framework output, save to:
|
||||
- Generic testimonial sections <!-- generic-testimonials -->
|
||||
- Cookie-cutter hero sections with left-text right-image <!-- split-hero-template -->
|
||||
|
||||
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 `<!-- id -->` is the pattern's id in `lib/design-catalog.ts`; the design detector reports the same ids.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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: <names>." 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`. */
|
||||
|
||||
+40
-11
@@ -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(/<!-- ([a-z0-9-]+) -->/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 <!-- id --> 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(/<!-- ([a-z0-9-]+) -->$/);
|
||||
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]/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user