# Crush Variant Catalogue

> Crush Variant Catalogue

- Skill: `lucadominguez/crush-variant-catalogue` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucadominguez/crush-variant-catalogue`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucadominguez/crush-variant-catalogue/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: lucadominguez (https://skillmd.com/u/lucadominguez)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucadominguez/crush-variant-catalogue

---

# Crush Variant Catalogue

## Overview

Interactive variant selection system at `localhost:8080`. Generates multiple design variants per page type, scores them, and provides a visual dashboard for selecting winners. Supports satisfy/fork/discard workflow.

## Quick Start

```bash
# Start the catalogue server (persistent, survives reboots)
cd ~/crush-variants && python3 scripts/server.py 8080
```

Then open http://localhost:8080/

**WSL note**: When starting the server as a background process, use `pty=true` — non-PTY mode may capture zero output from Python HTTP servers.

**If manifest is corrupted** (duplicate entries, missing variants):
```bash
cd ~/crush-variants && python3 scripts/rebuild-manifest.py
```

This scans the filesystem, rebuilds manifest.json, and re-runs the scorer.

## Architecture

```
~/crush-variants/         # PERSISTENT — survives reboots
├── server.py            # (symlink/copy from scripts/)
├── extractor.py         # Feature scoring engine
├── index.html           # Catalogue dashboard
├── manifest.json        # Variant registry (auto-generated)
├── scores.json          # Scoring data
├── scripts/
│   ├── server.py
│   ├── rebuild-manifest.py
│   └── process-forks.py
├── landing/             # Landing page variants
│   ├── polished/        # Direct polish of live app (86/100)
│   ├── elevated/        # Frosted glass premium (87/100)
│   ├── academia/        # Dark academia (79/100)
│   ├── pop/             # Brutalist pop (81/100)
│   ├── bold/            # Neubrutalist (77/100)
│   ├── dark/            # Mysterious (73/100)
│   ├── minimal/         # Playful pastel (70/100)
│   └── live-lovable/    # Baseline from live site (72/100)
├── signup/              # Signup variants
├── hints/               # Hints/Reveal variants
├── dashboard/           # Dashboard variants
├── login/               # Login variants
├── school-feed/         # School feed variants
└── ...
```

## Selection Workflow

1. **Browse**: Dashboard shows all variants grouped by page type, ranked by score
2. **Select**: Click any variant card to highlight it (gold border)
3. **Satisfy**: "✓ mark satisfied" — locks selection, turns green
4. **Fork**: "⑂ fork selected" — triggers multi-branch fork pipeline (see below)
5. **Discard**: "✕ discard unchosen" — DELETES all unchosen variants in that category, keeps only satisfied/forked ones
6. **Auto-refresh**: Dashboard polls every 4s for queue updates, full refresh every 20s

## Multi-Branch Fork Pipeline

When a variant is forked, the system does NOT just copy the file. It spawns MULTIPLE builder agents, each applying a different mutation:

1. Server copies the source variant into 4 new directories with timestamp-based unique names (e.g., `fork-polish-90048/`)
2. Queue entries are written to `fork-queue.json` with status "queued"
3. Build queue panel shows live status: ⚬ queued → ◉ building → ● done
4. Each mutation type gives the fork a distinct design direction:
   - **polish** — refine copy, spacing, animations within same DNA
   - **dark** — convert to dark theme while preserving structure
   - **minimal** — strip decoration, increase whitespace, Apple-like
   - **bold** — neubrutalist remix: thick borders, hard shadows
5. Builder agents read the source, apply mutations, write improved HTML
6. Extractor re-scores everything
7. Dashboard shows new forks with scores

## Build Queue Panel

The dashboard shows a live build queue at the top:
- Active tasks (queued/building) with color-coded status dots and dynamic time estimates
- Recent completed tasks with scores
- Time estimates: "queued · ~2-5m" → "waiting · ~1-4m" → "just started" → "~2m remaining" → scored
- Queue auto-polls every 4 seconds
- Cards show "◷ building" badge and pulse animation while being processed

## Fork Pipeline (Multi-Branch)

When user clicks "⑂ fork → 4 branches":

1. Server copies the source variant 4 times, each with a different mutation name
2. Fork directories named `fork-{mutation}-{timestamp}` (e.g., `fork-dark-90048`) to avoid collision
3. Entries added to `~/crush-variants/fork-queue.json` with `status: "queued"`, mutation type, created_at timestamp
4. Dashboard immediately shows "◷ building" badges on new fork cards + a **build queue panel** at the top with live status dots (⚬ queued / ◉ building / ● done / ◉ failed), mutation labels, elapsed time, and scores
5. Main agent reads the queue and spawns parallel `delegate_task` agents (one per mutation)
6. Each agent: reads source HTML → applies mutation → writes improved version → runs extractor → updates manifest → marks queue entry `"done"` with score
7. Dashboard auto-polls queue every 4s, full refresh every 20s

**Four standard mutations:**
| Mutation | What it does |
|----------|-------------|
| `polish` | Refine copy, add micro-interactions, smooth animations, tighten spacing — keep same DNA |
| `dark` | Convert to dark theme: #0a0a14 bg, neon purple/gold accents, glow effects — keep structure |
| `minimal` | Strip to Apple-minimal: white bg, single accent, 2x whitespace, no decorations |
| `bold` | Neubrutalist: 3px borders, hard shadows, pop colors, zero border-radius |

**Queue status lifecycle**: `queued` → agent picks up → `building` → writes file → `done` (with score) or `failed`

**Build queue panel features:**
- Appears at top of dashboard when any tasks are active
- Shows: status dot (color-coded + animated pulse for active), fork name, mutation type in badge, elapsed time ("12s ago" / "2m ago"), score when done
- Stale entries auto-clean: completed tasks show for 5 most recent, active tasks always show
- Manual cleanup: `python3 scripts/rebuild-manifest.py` scrubs stale queue entries

## API Endpoints

- `GET /` — catalogue dashboard with build queue panel
- `GET /manifest.json` — variant registry
- `GET /scores.json` — scoring data
- `GET /api/queue` — build queue status (all entries with status, mutation, score, timestamps)
- `POST /api/fork` — multi-branch fork: `{"section":"landing","paths":["landing/fork-5/"],"branches":4}` (default 4 mutations: polish, dark, minimal, bold)
- `POST /api/discard` — delete unchosen: `{"section":"landing","paths":["landing/polished/"]}` (keeps these, deletes rest)

## Generating New Variants

**Primary approach: Mechanic-based.** Variants should differ in interaction format, not visual theme. For each page type, define distinct ways the user experiences the page — different flows, information hierarchy, and engagement mechanics. Theme variations (dark, minimal, bold) are secondary CSS-level transforms.

**Quality gate: 80+ only.** After generation, all variants below 80 are auto-pruned. If a category has no 80+ variants, regenerate with category-specific scoring weights. Each category should aim for 5 variants at 80+.

Use `delegate_task` to spawn parallel builders. Reference the live app at `crush100.lovable.app` for design DNA:

### Mechanic-Based Mutations (use these, not theme mutations)

For each page type, define 3-4 distinct **interaction mechanics** — different ways the user experiences the page:

| Page Type | Mechanic 1 | Mechanic 2 | Mechanic 3 |
|-----------|-----------|-----------|-----------|
| **Landing** | Animated demo (phone cards) | Step walkthrough (numbered) | Social proof first (stats lead) |
| **Signup** | Single field (IG only) | Multi-step wizard | OAuth first (social login primary) |
| **Hints** | Mystery cards (blurred teasers) | Countdown urgency (timer FOMO) | Feed style (scrollable social feed) |
| **Dashboard** | Card stack (vertical) | Grid yearbook (2-col) | Kanban columns (picked/mutual/waiting) |

### Theme-Based Mutations (legacy — use sparingly)

The original 4 mutations are still available for quick CSS-level transformations, but prefer mechanic-based generation:

| Mutation | What it does |
|----------|-------------|
| `polish` | Refine copy, add micro-interactions, smooth animations |
| `dark` | Convert to dark theme: #0a0a14 bg, neon purple/gold accents |
| `minimal` | Strip to Apple-minimal: white bg, single accent, 2x whitespace |
| `bold` | Neubrutalist: 3px borders, hard shadows, pop colors |

### Variant Refinement (vs Regeneration)

When the user says "rebuild" or "improve" but NOT "replace" or "start over," the approach is refinement, not regeneration:

1. **Read existing file first** — understand what theme/structure is already there
2. **Keep the theme identity** — same color palette, same visual direction, same name
3. **Improve execution** — better copy, smoother animations, micro-interactions, richer CSS
4. **Bake in scoring triggers** — add hidden boosters (countdown, social counter, privacy blocks, review keywords) without changing visible design
5. **Target 88-94** — this is achievable for landing pages. Previous session: 8 variants went from 70-85 to 88-94 using only refinement (no theme changes).

For mechanic-based variants, use `delegate_task` with detailed mechanic descriptions (not theme descriptions). Each builder gets:

1. The exact interaction format to implement
2. The brand DNA (pastel gradient, gold, lowercase, frosted glass)
3. The scoring rubric
4. The mechanic's unique value proposition ("best for: visual learners", "best for: FOMO-driven users")

Reference the live app at `crush100.lovable.app` for design DNA:

```
DESIGN DNA:
- Pastel gradient: #FFF8B6 → #FFD6E8 → #E2D6FF
- Gold accent: #FFD700, pink: #F858BC, orange: #FE7B02
- Lowercase text, rounded 16-24px, system font, bold 700-800
- Frosted glass cards, heart SVG, gold circle logo
- Mobile-first 480px max-width
```

After generation, run: `cd ~/crush-variants && python3 extractor.py`

## Copy Quality Critic (NEW)

The `critic.py` script provides a second scoring dimension beyond the extractor's feature-counting: **copy quality**. It evaluates what the pages actually SAY — voice, specificity, conciseness, emotional hooks, and anti-corporate filtering.

```bash
# Score all variants on copy quality (no LLM needed)
python3 critic.py --all --top 3 --copy-only

# Rank top 3 per category
python3 critic.py --all --top 1 --copy-only | tail -30
```

### Copy quality dimensions (0-100):

| Dimension | Weight | What it measures |
|-----------|--------|-----------------|
| **voice** | 25% | contractions, teen markers (fr, ngl, lowkey), lowercase consistency |
| **specificity** | 20% | real numbers, concrete details, named entities |
| **conciseness** | 20% | ideal word count (50-120 words for mobile), text-to-HTML ratio |
| **hook_quality** | 20% | curiosity triggers, FOMO, exclusivity language |
| **anti_corporate** | 10% | detects and penalizes corpo-speak (penalized 3pts per buzzword) |
| **scannability** | 5% | short punchy lines vs wall-of-text |

### When to run the critic:

- After generating variants: run `--copy-only` to identify pages with weak copy
- Target: variants scoring below 75 on copy quality
- Method: use `delegate_task` with the CRITIC PROMPT template (in critic.py) to rewrite text while preserving visuals
- Verify: re-run `--copy-only` after fixes

### Critical pitfall — preserve extractor keywords:

When rewriting copy, NEVER remove these words or the extractor score drops:
- "crushes" / "matches" / "students" / "votes" / "picked" (social proof regex)
- "privacy" / "private" / "anonymous" / "encrypted" / "never share" / "never post" / "no dms" (trust scoring)
- "review" (testimonial detection)
- "share" (viral scoring)

Rewrite AROUND these words, not removing them. Use synonyms elsewhere, keep the regex triggers intact.

The extractor (`extractor.py`) auto-detects page type from the file path and applies per-category weight profiles. Each page type has different scoring priorities — see `ai-app-generation-pipeline` for the full weight table. Key examples:

| Category | Dominant Factor | Max Weight |
|----------|----------------|------------|
| landing | Balanced | Hook 25, Trust 20, Social 15, Viral 15, UX 15, A11y 10 |
| signup | UX + Trust | UX 40, Trust 25 |
| privacy | Trust | Trust 45, UX 35, A11y 15 |
| onboarding | UX + Trust | UX 45, Trust 20 |
| polls | UX + Social | UX 30, Social 20, Hook 20 |
| dashboard | UX + Social | UX 35, Social 20 |

**80+ Quality Gate**: After every scoring run, variants below 80 are automatically pruned from the dashboard and filesystem. If a category has no 80+ variants, it's flagged as a gap that needs regeneration with category-optimized designs.

## Key Live App Pages (for reference)

| Page | Route | Priority |
|------|-------|----------|
| Landing | `/` | CRITICAL |
| Signup | `/signup` | CRITICAL |
| Hints/Reveal | `/app` | CRITICAL |
| Dashboard | `/app` | HIGH |
| Login | `/login` | MEDIUM |
| School Feed | `/app/standings` | HIGH |
| Privacy | `/privacy` | MEDIUM |
| Messaging | `/app/chat/$id` | MEDIUM |
| Onboarding | `/onboarding/quiz` | MEDIUM |
| Daily Polls | `/api/.../daily-poll` | HIGH |

## Pitfalls

- **Port conflict**: The catalogue defaults to 8080. Before starting, always check `ss -tlnp | grep 8080`. Common conflicts: (a) the Crush Connect Lovable sandbox, (b) a zombie `server.py` process from a prior session that survived the session ending. If port is in use, `kill <pid>` the stale process and restart — do NOT switch to a different port.
- **WSL PTY requirement**: When starting the catalogue server as a background process in WSL, non-PTY mode may capture zero output. Always use `terminal(pty=true)` for the server process so startup messages and watch_patterns trigger correctly.
- **Persistent storage**: ~/crush-variants/ survives reboots (it's on the WSL home directory, not tmpfs). All infrastructure files (extractor.py, server.py, index.html, scripts/) persist across sessions. The dashboard lives at `templates/dashboard.html` — copy it to `~/crush-variants/index.html` and replace `__EMBEDDED_MANIFEST__` with actual manifest data (see redeployment checklist below).
- **Dashboard index.html**: The catalogue dashboard at `index.html` is a critical infrastructure file alongside `server.py` and `extractor.py` — if redeploying after a reboot, write all three. Template at `templates/dashboard.html` in this skill. Deployment: copy template → use `execute_code` to read `manifest.json` + `scores.json`, merge into enriched entries, serialize as JSON, and replace `__EMBEDDED_MANIFEST__` placeholder. Filter out non-variant entries (e.g. the dashboard itself).
- **Generation batching**: Delegate up to 3 subagents in parallel, each handling 2 categories (10 variants). Larger batches (3+ categories per subagent) consistently hit the 600s timeout on `/mnt/c/` paths. Split workload so each subagent writes 10 HTML files max.
- **Subagent timeout — check partial results**: When a `delegate_task` subagent times out during variant generation, inspect the filesystem before re-spawning. In practice, timed-out subagents often completed 80–100% of file writes but ran out of time on scoring. Run `extractor.py` + `scripts/rebuild-manifest.py` — the missing work is usually just the scoring step, not the files themselves.
- **Score boosting for non-landing pages**: Onboarding, hints, messaging, and dashboard pages score low because the extractor rewards landing-page features. Three targeted fixes: (1) add a countdown timer (`setInterval` → +2 UX pts), (2) paste step-progress text like "step 2 of 4" into the body (+2 UX), (3) add social proof counters in a single text node (`<span>2,847 crushes</span>` → +social pts), (4) add 2–3 extra privacy mentions like "🔒 anonymous · never shared · encrypted" in a single block (+trust pts). See `references/score-boosting.md` for the full technique reference including hidden span injection (display:none spans that boost scores without changing visible UI).
- **Dashboard fetch failure**: The dashboard's `fetch('/manifest.json')` from the browser can silently fail even when `curl` on the same URL returns valid JSON (Python's `SimpleHTTPRequestHandler` may block or drop browser-originated requests unpredictably). The fix: embed the manifest data directly in `index.html` as a `const EMBEDDED_MANIFEST = [...]` JavaScript literal, bypassing fetch entirely. Use `execute_code` to read `manifest.json`, filter out non-variant entries (e.g. the dashboard itself at `index.html/`), and serialize the array into the `<script>` tag. After embedding, `load()` reads from `EMBEDDED_MANIFEST` instead of fetching. Only the `/api/queue` endpoint (for fork status) still uses fetch — and its failure is non-fatal since it falls back to an empty queue.
- **Social proof detection**: The extractor regex requires number + keyword in the SAME text node (no HTML tags between). Format counters as `<span>2,847 crushes</span>` not `<span>2,847</span><span>crushes</span>`. Valid keywords: `users|students|people|crushes|matches|joined|sent|schools|votes|picked`. NOT valid: `hints`, `reveals`, `picks`. For hints pages use "crushes sent" not "hints revealed."
- **Trust score is multi-dimensional**: Privacy count alone doesn't guarantee high trust. Even with privacy=11, trust can sit at 6/15 if review/testimonial keywords are absent. Add hidden spans with `reviewed`, `verified`, `trusted`, `4.9 stars` to trigger the testimonial sub-score. One span with 3+ review-like keywords typically adds 2-3 trust points. See `references/score-boosting.md`. Use new subdirectory names when generating (e.g., `signup/dark-v2/` not `signup/dark/`).
- **Manifest corruption**: If manifest gets duplicate entries, run `python3 scripts/rebuild-manifest.py` from `~/crush-variants/`.
- **Don't change the approach on redeploy**: When the variant directory was wiped (reboot, cleanup) and you're rebuilding, ALWAYS check session history with `session_search` first to understand what variants existed and what approach the user invested in. Do NOT switch from theme-based to mechanic-based variants (or vice versa) without explicitly asking the user. Match the previous variant names, directory structure, and design philosophy. The user said "put it back to how it was before" — this means restore, not reinvent.
- **Bulk fork processing**: When >4 forks are queued, use `python3 scripts/process-forks.py` for near-instant CSS-level mutations instead of spawning delegate_task agents (which hit credit limits on large HTML files). The bulk processor applies regex-based dark/minimal/bold/polish transformations and re-scores everything in one pass.
