/**
* 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
| Dimension | CA wins | GD wins | Tie | Vainqueur |
| 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)
| Brand |
CA /100 |
GD /100 |
CA L |
GD L |
CA C |
GD C |
Palette CA │ GD |
Verdict |
${brandRows}
${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.
| Brand | CA | GD | Delta | Hypothèse cause |
${caWeakList.map((b: any) => `
| ${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'} |
`).join('')}
` : ''}
${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.
${noCaList.map((b: any) => `- ${b.domain} — GD: ${b.gd?.designMdLines || '?'}L, ${b.gd?.colorsCount || '?'} colors
`).join('')}
` : ''}
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
- Volume — Lines DESIGN.md, normalisé log (saturation à 2000 lines)
- Color — Couleurs hex distinctes détectées (saturation 25 colors)
- Verif — 50pts screenshots + 50pts tokens.json (CA seul peut atteindre 100)
- Narrative — 100pts frontmatter YAML, 0 sinon
- Completeness — Score 0-100 du catalog CA (heuristique 75 pour GD)
- Section coverage — sectionsCount / 11 canonical
Verdict thresholds
- CA_WINS : compositeCA > compositeGD + 5
- GD_WINS : compositeGD > compositeCA + 5 ET compositeCA ≥ 60
- TIE : |compositeCA − compositeGD| ≤ 5
- CA_WEAK : compositeGD > compositeCA + 5 ET compositeCA < 60 → roadmap prioritaire
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); });