/** * Clone Architect vs GetDesign β€” Comparison Framework * * Pour chaque brand qui existe Γ  la fois dans extractions/ (CA) et compare-gd/ (GD), * compute des mΓ©triques side-by-side et output un JSON structurΓ© + Markdown report. * * Usage: npx tsx scripts/compare-vs-gd.ts [brands...] * npx tsx scripts/compare-vs-gd.ts --all */ import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs'; import { join } from 'path'; interface BrandMetrics { domain: string; hasCA: boolean; hasGD: boolean; ca?: { designMdLines: number; sectionsCount: number; colorsCount: number; fontFamiliesCount: number; hasScreenshots: boolean; hasTokensJson: boolean; completenessScore: number; sizeKb: number; dark: boolean; extractedAt: string; }; gd?: { designMdLines: number; sectionsCount: number; colorsNamed: number; fontFamiliesCount: number; hasFrontmatter: boolean; sizeKb: number; }; winner?: { volume: 'CA' | 'GD' | 'tie'; colorRichness: 'CA' | 'GD' | 'tie'; verifiability: 'CA' | 'GD'; narrative: 'CA' | 'GD'; }; } const ROOT = process.cwd(); const GD_DOMAIN_MAP: Record = { 'airbnb': 'airbnb.com', 'cal': 'cal.com', 'clickhouse': 'clickhouse.com', 'cursor': 'cursor.com', 'figma': 'figma.com', 'framer': 'framer.com', 'github': 'github.com', 'linear': 'linear.app', 'loom': 'loom.com', 'lovable': 'lovable.dev', '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', 'posthog': 'posthog.com', 'revolut': 'revolut.com', 'starbucks': 'starbucks.com', 'stripe': 'stripe.com', 'theverge': 'theverge.com', 'together.ai': 'together.ai', 'vercel': 'vercel.com', 'vodafone': 'vodafone.com', }; const DOMAIN_TO_GD: Record = Object.fromEntries( Object.entries(GD_DOMAIN_MAP).map(([gd, dom]) => [dom, gd]) ); function countSections(content: string): number { // Count lines starting with "## " (H2 markdown headers) const lines = content.split('\n'); return lines.filter(l => l.match(/^##\s+\d?\.?\s*[A-Z]/)).length; } function countColorsInDesignMd(content: string): number { // Count hex codes referenced const hexes = content.match(/#[0-9a-fA-F]{6}\b/g) || []; const rgbs = content.match(/rgb\([^)]+\)/g) || []; return new Set([...hexes, ...rgbs]).size; } function countFontFamilies(content: string): number { const matches = content.match(/font[-_]family[^:\n]*:[^\n]+/gi) || []; return matches.length; } function analyzeCABrand(domain: string): BrandMetrics['ca'] | undefined { const extractDir = join(ROOT, 'extractions', domain); const designMdPath = join(extractDir, 'DESIGN.md'); const tokensPath = join(extractDir, 'tokens.json'); const screenshotsPath = join(extractDir, 'screenshots'); const summaryPath = join(extractDir, 'extraction-summary.json'); if (!existsSync(designMdPath)) return undefined; const content = readFileSync(designMdPath, 'utf-8'); const lines = content.split('\n').length; const sectionsCount = countSections(content); const colorsCount = countColorsInDesignMd(content); const fontFamiliesCount = countFontFamilies(content); const sizeKb = Math.round(Buffer.byteLength(content, 'utf-8') / 1024); // Read catalog for completeness score const catalogPath = join(ROOT, 'catalog', 'index.json'); const catalog = JSON.parse(readFileSync(catalogPath, 'utf-8')); const brandEntry = catalog.brands.find((b: any) => b.domain === domain); return { designMdLines: lines, sectionsCount, colorsCount, fontFamiliesCount, hasScreenshots: existsSync(screenshotsPath), hasTokensJson: existsSync(tokensPath), completenessScore: brandEntry?.completeness || 0, sizeKb, dark: brandEntry?.dark || false, extractedAt: brandEntry?.extractedAt || '?', }; } function analyzeGDBrand(domain: string): BrandMetrics['gd'] | undefined { const gdSlug = DOMAIN_TO_GD[domain]; if (!gdSlug) return undefined; const gdPath = join(ROOT, 'compare-gd', gdSlug, 'GD-DESIGN.md'); if (!existsSync(gdPath)) return undefined; const content = readFileSync(gdPath, 'utf-8'); const lines = content.split('\n').length; const sectionsCount = countSections(content); const colorsNamed = countColorsInDesignMd(content); const fontFamiliesCount = countFontFamilies(content); const hasFrontmatter = content.startsWith('---\n') || content.startsWith('---\r\n'); const sizeKb = Math.round(Buffer.byteLength(content, 'utf-8') / 1024); return { designMdLines: lines, sectionsCount, colorsNamed, fontFamiliesCount, hasFrontmatter, sizeKb }; } function determineWinner(ca: BrandMetrics['ca'], gd: BrandMetrics['gd']): BrandMetrics['winner'] { if (!ca || !gd) return undefined; return { volume: ca.designMdLines > gd.designMdLines * 1.1 ? 'CA' : gd.designMdLines > ca.designMdLines * 1.1 ? 'GD' : 'tie', colorRichness: ca.colorsCount > gd.colorsNamed * 1.1 ? 'CA' : gd.colorsNamed > ca.colorsCount * 1.1 ? 'GD' : 'tie', verifiability: ca.hasScreenshots && ca.hasTokensJson ? 'CA' : 'GD', narrative: gd.hasFrontmatter ? 'GD' : 'CA', // GD has YAML frontmatter narrative }; } async function main() { const args = process.argv.slice(2); const allMode = args.includes('--all'); // Build the list of domains to compare let domains: string[]; if (allMode) { domains = Object.values(GD_DOMAIN_MAP); } else if (args.length > 0) { domains = args.filter(a => !a.startsWith('--')); } else { domains = Object.values(GD_DOMAIN_MAP); } console.log(`πŸ”¬ Clone Architect vs GetDesign β€” comparison of ${domains.length} brands\n`); const results: BrandMetrics[] = []; let caWins = { volume: 0, color: 0, verif: 0, narrative: 0 }; let gdWins = { volume: 0, color: 0, verif: 0, narrative: 0 }; for (const domain of domains) { const ca = analyzeCABrand(domain); const gd = analyzeGDBrand(domain); const metrics: BrandMetrics = { domain, hasCA: !!ca, hasGD: !!gd, ca, gd, winner: ca && gd ? determineWinner(ca, gd) : undefined, }; if (metrics.winner) { if (metrics.winner.volume === 'CA') caWins.volume++; else if (metrics.winner.volume === 'GD') gdWins.volume++; if (metrics.winner.colorRichness === 'CA') caWins.color++; else if (metrics.winner.colorRichness === 'GD') gdWins.color++; if (metrics.winner.verifiability === 'CA') caWins.verif++; else gdWins.verif++; if (metrics.winner.narrative === 'CA') caWins.narrative++; else gdWins.narrative++; } results.push(metrics); const statusCA = ca ? `βœ… ${ca.designMdLines}L ${ca.colorsCount}c ${ca.completenessScore}/100` : '❌'; const statusGD = gd ? `βœ… ${gd.designMdLines}L ${gd.colorsNamed}c` : '❌'; console.log(`${domain.padEnd(20)} CA: ${statusCA.padEnd(30)} GD: ${statusGD}`); } // Aggregate stats const compared = results.filter(r => r.hasCA && r.hasGD); const avgCALines = compared.reduce((s, r) => s + (r.ca?.designMdLines || 0), 0) / compared.length; const avgGDLines = compared.reduce((s, r) => s + (r.gd?.designMdLines || 0), 0) / compared.length; const avgCAColors = compared.reduce((s, r) => s + (r.ca?.colorsCount || 0), 0) / compared.length; const avgGDColors = compared.reduce((s, r) => s + (r.gd?.colorsNamed || 0), 0) / compared.length; const avgCAScore = compared.reduce((s, r) => s + (r.ca?.completenessScore || 0), 0) / compared.length; console.log('\n══════════════════════════════════════════════════'); console.log('πŸ“Š AGGREGATE METRICS'); console.log('══════════════════════════════════════════════════'); console.log(`Compared brands : ${compared.length} / ${domains.length}`); console.log(`Avg lines : CA ${avgCALines.toFixed(0)} vs GD ${avgGDLines.toFixed(0)} (CA ${(avgCALines/avgGDLines).toFixed(1)}x)`); console.log(`Avg colors : CA ${avgCAColors.toFixed(1)} vs GD ${avgGDColors.toFixed(1)} (GD ${(avgGDColors/avgCAColors).toFixed(1)}x)`); console.log(`Avg completeness CA : ${avgCAScore.toFixed(0)}/100`); console.log(''); console.log('πŸ† WINS PER DIMENSION'); console.log(`Volume : CA ${caWins.volume} | GD ${gdWins.volume}`); console.log(`Color richness : CA ${caWins.color} | GD ${gdWins.color}`); console.log(`Verifiability : CA ${caWins.verif} | GD ${gdWins.verif}`); console.log(`Narrative : CA ${caWins.narrative} | GD ${gdWins.narrative}`); // Save outputs const outDir = join(ROOT, 'docs', 'comparison'); mkdirSync(outDir, { recursive: true }); const jsonPath = join(outDir, 'ca-vs-gd-metrics.json'); writeFileSync(jsonPath, JSON.stringify({ generatedAt: new Date().toISOString(), totalBrands: domains.length, comparedCount: compared.length, avg: { caLines: avgCALines, gdLines: avgGDLines, caColors: avgCAColors, gdColors: avgGDColors, caScore: avgCAScore }, wins: { ca: caWins, gd: gdWins }, brands: results, }, null, 2)); console.log(`\nπŸ’Ύ JSON output β†’ ${jsonPath}`); // Markdown report const mdPath = join(outDir, 'ca-vs-gd-report.md'); let md = `# Clone Architect vs getdesign.md β€” Comparison Report\n\n`; md += `Generated: ${new Date().toISOString()}\n\n`; md += `## Aggregate Metrics\n\n`; md += `| Metric | Clone Architect | getdesign.md | Ratio |\n`; md += `|---|---|---|---|\n`; md += `| Brands compared | ${compared.length} / ${domains.length} | β€” | β€” |\n`; md += `| Avg DESIGN.md lines | ${avgCALines.toFixed(0)} | ${avgGDLines.toFixed(0)} | CA ${(avgCALines/avgGDLines).toFixed(1)}Γ— |\n`; md += `| Avg colors referenced | ${avgCAColors.toFixed(1)} | ${avgGDColors.toFixed(1)} | GD ${(avgGDColors/avgCAColors).toFixed(1)}Γ— |\n`; md += `| Avg CA completeness | ${avgCAScore.toFixed(0)}/100 | β€” | β€” |\n`; md += `\n## Wins per Dimension\n\n`; md += `| Dimension | CA wins | GD wins |\n`; md += `|---|---|---|\n`; md += `| Volume (lines) | ${caWins.volume} | ${gdWins.volume} |\n`; md += `| Color richness | ${caWins.color} | ${gdWins.color} |\n`; md += `| Verifiability (screenshots+tokens) | ${caWins.verif} | ${gdWins.verif} |\n`; md += `| Narrative editorial | ${caWins.narrative} | ${gdWins.narrative} |\n`; md += `\n## Per-Brand Details\n\n`; md += `| Brand | CA lines | GD lines | CA colors | GD colors | CA score | Winner volume | Winner color |\n`; md += `|---|---|---|---|---|---|---|---|\n`; for (const r of results) { if (!r.hasCA || !r.hasGD) { md += `| ${r.domain} | ${r.hasCA ? 'βœ…' : '❌'} | ${r.hasGD ? 'βœ…' : '❌'} | β€” | β€” | β€” | β€” | β€” |\n`; } else { md += `| ${r.domain} | ${r.ca?.designMdLines} | ${r.gd?.designMdLines} | ${r.ca?.colorsCount} | ${r.gd?.colorsNamed} | ${r.ca?.completenessScore} | ${r.winner?.volume} | ${r.winner?.colorRichness} |\n`; } } writeFileSync(mdPath, md); console.log(`πŸ“„ Markdown report β†’ ${mdPath}`); } main().catch(err => { console.error(err); process.exit(1); });