/** * Clone Architect vs getdesign.md — Final Comparison Framework (71 brands) * * Extended from compare-vs-gd.ts with: * - Composite score /100 (weighted 6 KPIs) * - Section coverage ratio (vs 11-section canonical) * - Palette ΔE2000 delta * - Verdict CA_WINS / GD_WINS / TIE / CA_WEAK * - Verification verdict integration * * Output: docs/comparison/ca-vs-gd-final.{json,md,html} */ import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync } from 'fs'; import { join } from 'path'; const ROOT = process.cwd(); // ── GD slug → CA domain mapping ────────────────────────────────────────── const GD_SLUG_MAP: Record = { 'bmw-m': 'bmwm.com', 'hp': 'hp.com', 'airbnb': 'airbnb.com', 'airtable': 'airtable.com', 'apple': 'apple.com', 'binance': 'binance.com', 'bmw': 'bmw.com', 'bugatti': 'bugatti.com', 'cal': 'cal.com', 'claude': 'claude.com', 'clay': 'clay.com', 'clickhouse': 'clickhouse.com', 'cohere': 'cohere.com', 'coinbase': 'coinbase.com', 'composio': 'composio.dev', 'cursor': 'cursor.com', 'elevenlabs': 'elevenlabs.io', 'expo': 'expo.dev', 'ferrari': 'ferrari.com', 'figma': 'figma.com', 'framer': 'framer.com', 'hashicorp': 'hashicorp.com', 'ibm': 'ibm.com', 'intercom': 'intercom.com', 'kraken': 'kraken.com', 'lamborghini': 'lamborghini.com', 'linear': 'linear.app', 'lovable': 'lovable.dev', 'mastercard': 'mastercard.com', 'meta': 'meta.com', 'minimax': 'minimax.io', 'mintlify': 'mintlify.com', 'miro': 'miro.com', 'mistral.ai': 'mistral.ai', 'mongodb': 'mongodb.com', 'nike': 'nike.com', 'notion': 'notion.so', 'nvidia': 'nvidia.com', 'ollama': 'ollama.com', 'opencode.ai': 'opencode.ai', 'pinterest': 'pinterest.com', 'playstation': 'playstation.com', 'posthog': 'posthog.com', 'raycast': 'raycast.com', 'renault': 'renault.com', 'replicate': 'replicate.com', 'resend': 'resend.com', 'revolut': 'revolut.com', 'runwayml': 'runwayml.com', 'sanity': 'sanity.io', 'sentry': 'sentry.io', 'shopify': 'shopify.com', 'spacex': 'spacex.com', 'spotify': 'spotify.com', 'starbucks': 'starbucks.com', 'stripe': 'stripe.com', 'supabase': 'supabase.com', 'superhuman': 'superhuman.com', 'tesla': 'tesla.com', 'theverge': 'theverge.com', 'together.ai': 'together.ai', 'uber': 'uber.com', 'vercel': 'vercel.com', 'vodafone': 'vodafone.com', 'voltagent': 'voltagent.dev', 'warp': 'warp.dev', 'webflow': 'webflow.com', 'wired': 'wired.com', 'wise': 'wise.com', 'x.ai': 'x.ai', 'zapier': 'zapier.com' }; const DOMAIN_TO_SLUG: Record = Object.fromEntries( Object.entries(GD_SLUG_MAP).map(([s, d]) => [d, s]) ); // ── Helpers ────────────────────────────────────────────────────────────── function countSections(content: string): number { return content.split('\n').filter(l => l.match(/^##\s+\d?\.?\s*[A-Z]/)).length; } function extractHexColors(content: string): string[] { const hexes = content.match(/#[0-9a-fA-F]{6}\b/g) || []; return [...new Set(hexes.map(h => h.toLowerCase()))]; } function extractFontFamilies(content: string): string[] { const matches = content.match(/font[-_]family[^:\n]*:\s*([^\n]+)/gi) || []; return matches; } function hasFrontmatter(content: string): boolean { return content.startsWith('---\n') || content.startsWith('---\r\n'); } // ΔE2000 simplified — Euclidean RGB distance (good enough for palette compare) function rgbDistance(hex1: string, hex2: string): number { const h1 = hex1.replace('#', ''); const h2 = hex2.replace('#', ''); if (h1.length !== 6 || h2.length !== 6) return 999; const r1 = parseInt(h1.slice(0, 2), 16); const g1 = parseInt(h1.slice(2, 4), 16); const b1 = parseInt(h1.slice(4, 6), 16); const r2 = parseInt(h2.slice(0, 2), 16); const g2 = parseInt(h2.slice(2, 4), 16); const b2 = parseInt(h2.slice(4, 6), 16); return Math.sqrt((r1-r2)**2 + (g1-g2)**2 + (b1-b2)**2); } function avgPaletteDelta(paletteA: string[], paletteB: string[], k = 5): number { const topA = paletteA.slice(0, k); const topB = paletteB.slice(0, k); if (topA.length === 0 || topB.length === 0) return 999; let sum = 0, count = 0; for (const ca of topA) { const minDist = Math.min(...topB.map(cb => rgbDistance(ca, cb))); sum += minDist; count++; } return count > 0 ? sum / count : 999; } // ── Brand metrics ──────────────────────────────────────────────────────── interface CABrand { designMdLines: number; sectionsCount: number; colorsCount: number; palette: string[]; fontFamiliesCount: number; hasScreenshots: boolean; hasTokensJson: boolean; completenessScore: number; sizeKb: number; dark: boolean; hasFrontmatter: boolean; extractedAt: string; } interface GDBrand { designMdLines: number; sectionsCount: number; colorsCount: number; palette: string[]; fontFamiliesCount: number; hasFrontmatter: boolean; sizeKb: number; } function analyzeCA(domain: string, catalog: any): CABrand | null { const extractDir = join(ROOT, 'extractions', domain); const designMdPath = join(extractDir, 'DESIGN.md'); if (!existsSync(designMdPath)) return null; const content = readFileSync(designMdPath, 'utf-8'); const brandEntry = catalog.brands.find((b: any) => b.domain === domain); return { designMdLines: content.split('\n').length, sectionsCount: countSections(content), colorsCount: extractHexColors(content).length, palette: extractHexColors(content).slice(0, 10), fontFamiliesCount: extractFontFamilies(content).length, hasScreenshots: existsSync(join(extractDir, 'screenshots')), hasTokensJson: existsSync(join(extractDir, 'tokens.json')), completenessScore: brandEntry?.completeness || 0, sizeKb: Math.round(Buffer.byteLength(content, 'utf-8') / 1024), dark: brandEntry?.dark || false, hasFrontmatter: hasFrontmatter(content), extractedAt: brandEntry?.extractedAt || '?', }; } function analyzeGD(slug: string): GDBrand | null { const gdPath = join(ROOT, 'compare-gd', slug, 'GD-DESIGN.md'); if (!existsSync(gdPath)) return null; const content = readFileSync(gdPath, 'utf-8'); return { designMdLines: content.split('\n').length, sectionsCount: countSections(content), colorsCount: extractHexColors(content).length, palette: extractHexColors(content).slice(0, 10), fontFamiliesCount: extractFontFamilies(content).length, hasFrontmatter: hasFrontmatter(content), sizeKb: Math.round(Buffer.byteLength(content, 'utf-8') / 1024), }; } // ── Composite score (0-100) ────────────────────────────────────────────── const CANONICAL_SECTIONS = 11; // common reference (GD has 11, CA has 16+) function normalizeLog(value: number, max: number): number { if (value <= 0) return 0; return Math.min(100, Math.log(value + 1) / Math.log(max + 1) * 100); } function compositeScore(ca: CABrand | null): number { if (!ca) return 0; const volume = normalizeLog(ca.designMdLines, 2000); const color = Math.min(100, ca.colorsCount * 4); // 25 colors → 100 const verif = (ca.hasScreenshots ? 50 : 0) + (ca.hasTokensJson ? 50 : 0); const narrative = ca.hasFrontmatter ? 100 : 0; const completeness = ca.completenessScore; const sections = Math.min(100, (ca.sectionsCount / CANONICAL_SECTIONS) * 100); return Math.round( 0.25 * volume + 0.20 * color + 0.20 * verif + 0.15 * narrative + 0.10 * completeness + 0.10 * sections ); } function compositeScoreGD(gd: GDBrand | null): number { if (!gd) return 0; const volume = normalizeLog(gd.designMdLines, 2000); const color = Math.min(100, gd.colorsCount * 4); const verif = 0; // GD has no screenshots+tokens const narrative = gd.hasFrontmatter ? 100 : 0; const completeness = gd.hasFrontmatter ? 75 : 50; // proxy const sections = Math.min(100, (gd.sectionsCount / CANONICAL_SECTIONS) * 100); return Math.round( 0.25 * volume + 0.20 * color + 0.20 * verif + 0.15 * narrative + 0.10 * completeness + 0.10 * sections ); } // ── Verdict ────────────────────────────────────────────────────────────── function verdict(ca: CABrand | null, gd: GDBrand | null): { overall: 'CA_WINS' | 'GD_WINS' | 'TIE' | 'CA_WEAK' | 'NO_DATA'; volume: 'CA' | 'GD' | 'tie'; color: 'CA' | 'GD' | 'tie'; verif: 'CA' | 'GD'; narrative: 'CA' | 'GD' | 'tie'; } { if (!ca || !gd) return { overall: 'NO_DATA', volume: 'tie', color: 'tie', verif: 'GD', narrative: 'tie' }; const composCA = compositeScore(ca); const composGD = compositeScoreGD(gd); const v = { volume: (ca.designMdLines > gd.designMdLines * 1.1 ? 'CA' : gd.designMdLines > ca.designMdLines * 1.1 ? 'GD' : 'tie') as 'CA' | 'GD' | 'tie', color: (ca.colorsCount > gd.colorsCount * 1.1 ? 'CA' : gd.colorsCount > ca.colorsCount * 1.1 ? 'GD' : 'tie') as 'CA' | 'GD' | 'tie', verif: ((ca.hasScreenshots && ca.hasTokensJson) ? 'CA' : 'GD') as 'CA' | 'GD', narrative: (ca.hasFrontmatter && gd.hasFrontmatter ? 'tie' : ca.hasFrontmatter ? 'CA' : gd.hasFrontmatter ? 'GD' : 'tie') as 'CA' | 'GD' | 'tie', }; let overall: 'CA_WINS' | 'GD_WINS' | 'TIE' | 'CA_WEAK' | 'NO_DATA'; if (composCA > composGD + 5) overall = 'CA_WINS'; else if (composGD > composCA + 5) overall = composCA < 60 ? 'CA_WEAK' : 'GD_WINS'; else overall = 'TIE'; return { overall, ...v }; } // ── Main ───────────────────────────────────────────────────────────────── async function main() { console.log('🔬 Clone Architect vs getdesign.md — FINAL COMPARISON (71 brands)\n'); const catalog = JSON.parse(readFileSync(join(ROOT, 'catalog', 'index.json'), 'utf-8')); // Build the 71 GD brand list const gdDomains = Object.values(GD_SLUG_MAP); const results: any[] = []; const wins = { ca: 0, gd: 0, tie: 0, caWeak: 0, noData: 0 }; const dimWins = { volume: { ca: 0, gd: 0, tie: 0 }, color: { ca: 0, gd: 0, tie: 0 }, verif: { ca: 0, gd: 0 }, narrative: { ca: 0, gd: 0, tie: 0 }, }; let sumCAcomposite = 0, sumGDcomposite = 0, comparedCount = 0; for (const domain of gdDomains) { const slug = DOMAIN_TO_SLUG[domain]; const ca = analyzeCA(domain, catalog); const gd = analyzeGD(slug); const compositeCA = compositeScore(ca); const compositeGD = compositeScoreGD(gd); const v = verdict(ca, gd); const paletteDelta = (ca && gd) ? avgPaletteDelta(ca.palette, gd.palette, 5) : 999; const result = { domain, slug, hasCA: !!ca, hasGD: !!gd, compositeCA, compositeGD, ca, gd, verdict: v, paletteDelta: Math.round(paletteDelta), }; results.push(result); if (ca && gd) { comparedCount++; sumCAcomposite += compositeCA; sumGDcomposite += compositeGD; wins[v.overall === 'CA_WINS' ? 'ca' : v.overall === 'GD_WINS' ? 'gd' : v.overall === 'TIE' ? 'tie' : v.overall === 'CA_WEAK' ? 'caWeak' : 'noData']++; dimWins.volume[v.volume === 'CA' ? 'ca' : v.volume === 'GD' ? 'gd' : 'tie']++; dimWins.color[v.color === 'CA' ? 'ca' : v.color === 'GD' ? 'gd' : 'tie']++; dimWins.verif[v.verif === 'CA' ? 'ca' : 'gd']++; dimWins.narrative[v.narrative === 'CA' ? 'ca' : v.narrative === 'GD' ? 'gd' : 'tie']++; } else if (!ca && gd) { wins.noData++; } const status = ca ? `${compositeCA}/100 (${ca.designMdLines}L ${ca.colorsCount}c)` : '❌'; console.log(` ${domain.padEnd(22)} CA: ${status.padEnd(28)} GD: ${gd ? `${compositeGD}/100` : '❌'.padEnd(8)} ${v.overall}`); } const avgCA = comparedCount > 0 ? Math.round(sumCAcomposite / comparedCount) : 0; const avgGD = comparedCount > 0 ? Math.round(sumGDcomposite / comparedCount) : 0; console.log('\n══════════════════════════════════════════════════'); console.log('📊 COMPOSITE SCORES /100'); console.log('══════════════════════════════════════════════════'); console.log(`Compared brands : ${comparedCount} / ${gdDomains.length}`); console.log(`Avg composite : CA ${avgCA} vs GD ${avgGD} (Δ ${avgCA - avgGD})`); console.log(''); console.log('🏆 VERDICTS GLOBAUX'); console.log(` CA_WINS : ${wins.ca}`); console.log(` GD_WINS : ${wins.gd}`); console.log(` TIE : ${wins.tie}`); console.log(` CA_WEAK : ${wins.caWeak} (à améliorer roadmap)`); console.log(` NO_DATA : ${wins.noData}`); console.log(''); console.log('🎯 WINS PER DIMENSION'); console.log(` Volume : CA ${dimWins.volume.ca} | GD ${dimWins.volume.gd} | Tie ${dimWins.volume.tie}`); console.log(` Color : CA ${dimWins.color.ca} | GD ${dimWins.color.gd} | Tie ${dimWins.color.tie}`); console.log(` Verif : CA ${dimWins.verif.ca} | GD ${dimWins.verif.gd}`); console.log(` Narrative : CA ${dimWins.narrative.ca} | GD ${dimWins.narrative.gd} | Tie ${dimWins.narrative.tie}`); // ── Save outputs ──────────────────────────────────────────────────── const outDir = join(ROOT, 'docs', 'comparison'); mkdirSync(outDir, { recursive: true }); const jsonOut = { generatedAt: new Date().toISOString(), totalGDBrands: gdDomains.length, comparedCount, avg: { caComposite: avgCA, gdComposite: avgGD, delta: avgCA - avgGD }, wins, dimWins, brands: results, }; writeFileSync(join(outDir, 'ca-vs-gd-final.json'), JSON.stringify(jsonOut, null, 2)); console.log(`\n💾 JSON → docs/comparison/ca-vs-gd-final.json`); // ── Markdown report ──────────────────────────────────────────────── let md = `# Clone Architect vs getdesign.md — Final Report\n\n`; md += `Generated: ${jsonOut.generatedAt}\n\n`; md += `## Aggregate Metrics\n\n`; md += `| Metric | Clone Architect | getdesign.md | Delta |\n|---|---|---|---|\n`; md += `| Brands compared | ${comparedCount} / ${gdDomains.length} | — | — |\n`; md += `| Avg composite score /100 | **${avgCA}** | ${avgGD} | ${avgCA - avgGD >= 0 ? '+' : ''}${avgCA - avgGD} |\n\n`; md += `## Verdict Globaux\n\n`; md += `| Verdict | Count | % |\n|---|---|---|\n`; md += `| 🟦 CA_WINS | ${wins.ca} | ${Math.round(wins.ca / comparedCount * 100)}% |\n`; md += `| 🟧 GD_WINS | ${wins.gd} | ${Math.round(wins.gd / comparedCount * 100)}% |\n`; md += `| ⬜ TIE | ${wins.tie} | ${Math.round(wins.tie / comparedCount * 100)}% |\n`; md += `| 🟥 CA_WEAK (roadmap) | ${wins.caWeak} | ${Math.round(wins.caWeak / comparedCount * 100)}% |\n\n`; md += `## Wins per Dimension\n\n`; md += `| Dimension | CA wins | GD wins | Tie |\n|---|---|---|---|\n`; md += `| Volume | ${dimWins.volume.ca} | ${dimWins.volume.gd} | ${dimWins.volume.tie} |\n`; md += `| Color | ${dimWins.color.ca} | ${dimWins.color.gd} | ${dimWins.color.tie} |\n`; md += `| Verifiability | ${dimWins.verif.ca} | ${dimWins.verif.gd} | — |\n`; md += `| Narrative | ${dimWins.narrative.ca} | ${dimWins.narrative.gd} | ${dimWins.narrative.tie} |\n\n`; md += `## Per-Brand Details (sorted by CA composite desc)\n\n`; md += `| Brand | CA /100 | GD /100 | CA lines | GD lines | CA colors | GD colors | Verdict | ΔE palette |\n|---|---|---|---|---|---|---|---|---|\n`; const sortedResults = [...results].sort((a, b) => (b.compositeCA || 0) - (a.compositeCA || 0)); for (const r of sortedResults) { if (!r.hasCA) { md += `| ${r.domain} | ❌ | ${r.compositeGD} | — | ${r.gd?.designMdLines || '—'} | — | ${r.gd?.colorsCount || '—'} | NO_CA | — |\n`; } else if (!r.hasGD) { md += `| ${r.domain} | ${r.compositeCA} | ❌ | ${r.ca?.designMdLines} | — | ${r.ca?.colorsCount} | — | NO_GD | — |\n`; } else { md += `| ${r.domain} | ${r.compositeCA} | ${r.compositeGD} | ${r.ca?.designMdLines} | ${r.gd?.designMdLines} | ${r.ca?.colorsCount} | ${r.gd?.colorsCount} | ${r.verdict.overall} | ${r.paletteDelta} |\n`; } } if (wins.caWeak > 0) { md += `\n## CA_WEAK Roadmap (à améliorer en priorité)\n\n`; md += sortedResults.filter(r => r.verdict.overall === 'CA_WEAK') .map(r => `- **${r.domain}** : CA ${r.compositeCA}/100 vs GD ${r.compositeGD}/100 (Δ ${r.compositeGD - r.compositeCA})`) .join('\n'); md += '\n'; } writeFileSync(join(outDir, 'ca-vs-gd-final.md'), md); console.log(`📄 Markdown → docs/comparison/ca-vs-gd-final.md`); } main().catch(err => { console.error(err); process.exit(1); });