/** * Final comparison PDF — Clone Architect vs getdesign.md (71 brands) * * Generates a comprehensive A4 PDF with: * - Cover (KPIs, composite scores) * - Aggregate metrics + dimension wins * - Per-brand table sorted by CA composite desc * - CA_WEAK roadmap (brands to improve) * - Comparison radar charts (Chart.js inline) * - Palette swatches CA vs GD side-by-side for top 20 brands * * Output: docs/audit-vs-gd-71brands-{TIMESTAMP}.pdf */ import { chromium } from 'playwright'; import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; const ROOT = process.cwd(); async function main() { const dataPath = join(ROOT, 'docs', 'comparison', 'ca-vs-gd-final.json'); if (!existsSync(dataPath)) { console.error(`❌ Run compare-vs-gd-final.ts first to produce: ${dataPath}`); process.exit(1); } const data = JSON.parse(readFileSync(dataPath, 'utf-8')); const today = new Date().toISOString().slice(0, 10); const outHtml = join(ROOT, 'docs', `audit-vs-gd-71brands-${today}.html`); const outPdf = join(ROOT, 'docs', `audit-vs-gd-71brands-${today}.pdf`); const sortedBrands = [...data.brands] .filter((b: any) => b.hasCA && b.hasGD) .sort((a: any, b: any) => b.compositeCA - a.compositeCA); const caWeakList = sortedBrands.filter((b: any) => b.verdict.overall === 'CA_WEAK'); const noCaList = data.brands.filter((b: any) => !b.hasCA); function paletteSwatches(palette: string[], max = 8): string { return palette.slice(0, max).map(c => `
`).join(''); } function verdictBadge(v: string): string { const cls = v === 'CA_WINS' ? 'ca' : v === 'GD_WINS' ? 'gd' : v === 'TIE' ? 'tie' : v === 'CA_WEAK' ? 'weak' : 'none'; return `${v.replace('_', ' ')}`; } const brandRows = sortedBrands.map((b: any) => ` ${b.domain} ${b.compositeCA} ${b.compositeGD} ${b.ca?.designMdLines || '—'} ${b.gd?.designMdLines || '—'} ${b.ca?.colorsCount || '—'} ${b.gd?.colorsCount || '—'}
${paletteSwatches(b.ca?.palette || [], 5)}${paletteSwatches(b.gd?.palette || [], 5)}
${verdictBadge(b.verdict.overall)} `).join(''); const html = ` CA vs getdesign.md — Final 71 brands
AUDIT FINAL · OPTION A · WIPE & REBUILD

Clone Architect vs getdesign.md

71 brands GD coverage · Composite score /100 · Per-brand verdict
${today} · npm v2.4.0

1. Score composite global

${data.avg.caComposite}CA avg /100
${data.avg.gdComposite}GD avg /100
${data.avg.delta >= 0 ? '+' : ''}${data.avg.delta}CA − GD delta
${data.comparedCount}/${data.totalGDBrands}brands comparées
Verdict global : ${data.avg.delta > 5 ? '🏆 Clone Architect domine GD globalement' : data.avg.delta < -5 ? '⚠️ GD domine globalement — roadmap nécessaire' : '🤝 Match technique serré entre CA et GD'} · ${data.wins.ca} CA_WINS · ${data.wins.gd} GD_WINS · ${data.wins.tie} TIE · ${data.wins.caWeak} CA_WEAK

2. Verdicts par dimension

DimensionCA winsGD winsTieVainqueur
Volume (lines)${data.dimWins.volume.ca}${data.dimWins.volume.gd}${data.dimWins.volume.tie}${data.dimWins.volume.ca > data.dimWins.volume.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}
Couleurs détectées${data.dimWins.color.ca}${data.dimWins.color.gd}${data.dimWins.color.tie}${data.dimWins.color.ca > data.dimWins.color.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}
Vérifiabilité (screenshots+tokens)${data.dimWins.verif.ca}${data.dimWins.verif.gd}${data.dimWins.verif.ca > data.dimWins.verif.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}
Narrative (frontmatter)${data.dimWins.narrative.ca}${data.dimWins.narrative.gd}${data.dimWins.narrative.tie}${data.dimWins.narrative.ca > data.dimWins.narrative.gd ? verdictBadge('CA_WINS') : verdictBadge('GD_WINS')}

3. Per-brand scoreboard (71 brands)

${brandRows}
Brand CA /100 GD /100 CA L GD L CA C GD C Palette CA │ GD Verdict
${caWeakList.length > 0 ? `

4. CA_WEAK Roadmap — brands à améliorer

${caWeakList.length} brands où CA < GD ET CA < 60/100 — Roadmap prioritaire pour Sprint 5.
${caWeakList.map((b: any) => ` `).join('')}
BrandCAGDDeltaHypothèse cause
${b.domain} ${b.compositeCA} ${b.compositeGD} +${b.compositeGD - b.compositeCA} ${b.ca?.completenessScore < 70 ? 'Extraction incomplète (SPA/auth/géoblock)' : b.ca?.designMdLines < 800 ? 'Volume insuffisant' : 'Couleurs/sections faibles'}
` : ''} ${noCaList.length > 0 ? `

5. Brands non-extraites par CA

${noCaList.length} brands GD non-extraites par CA — extraction échouée (Cloudflare/anti-bot) ou pas encore tentée.
` : ''}

6. Méthodologie

Composite score formula

Score = 0.25·Volume(log) + 0.20·Color + 0.20·Verif + 0.15·Narrative + 0.10·Completeness + 0.10·SectionCoverage

Verdict thresholds

Reproductibilité

# 1. Re-extract toutes les brands GD
bash scripts/mass-extract.sh

# 2. Tokenize + DESIGN.md
for d in extractions/*/; do npx tsx scripts/tokenize.ts $(basename $d); done
for d in extractions/*/; do npx tsx scripts/generate-design-md.ts $(basename $d); done

# 3. Compute comparison
npx tsx scripts/compare-vs-gd-final.ts

# 4. Generate PDF
npx tsx scripts/generate-final-pdf.ts
`; writeFileSync(outHtml, html); console.log(`📄 HTML → ${outHtml}`); const browser = await chromium.launch({ headless: true }); const page = await browser.newPage(); await page.goto(`file://${outHtml}`, { waitUntil: 'networkidle' }); await page.pdf({ path: outPdf, format: 'A4', landscape: true, margin: { top: '12mm', right: '10mm', bottom: '12mm', left: '10mm' }, printBackground: true, }); await browser.close(); const { statSync } = await import('fs'); console.log(`✅ PDF → ${outPdf} (${(statSync(outPdf).size / 1024).toFixed(0)} KB)`); } main().catch(err => { console.error(err); process.exit(1); });