/** * Clone Architect — Batch DESIGN.md Regeneration * * Regenerates DESIGN.md for all extractions that have both raw-css.json + tokens.json. * Defensive: skips extractions with missing files (never process.exit on partial failure). * Reports a final summary with scores. * * Usage: npx tsx scripts/regen-catalog.ts [--dry-run] [--domain ] * --dry-run Show what would be regenerated without writing files * --domain Regenerate a single domain only */ import { readdir, readFile, stat } from 'fs/promises'; import { existsSync } from 'fs'; import { join } from 'path'; import { spawnSync } from 'child_process'; const ROOT = process.cwd(); const EXTRACTIONS_DIR = join(ROOT, 'extractions'); const GENERATE_SCRIPT = join(ROOT, 'scripts', 'generate-design-md.ts'); const isDryRun = process.argv.includes('--dry-run'); const domainFilter = (() => { const idx = process.argv.indexOf('--domain'); return idx >= 0 ? process.argv[idx + 1] : null; })(); interface RegenResult { domain: string; status: 'ok' | 'skipped' | 'error'; reason?: string; completeness?: number; lines?: number; } async function getCompleteness(designMdPath: string): Promise { try { const content = await readFile(designMdPath, 'utf-8'); const m = content.match(/^completeness:\s*(\d+)/m); return m ? parseInt(m[1]) : null; } catch { return null; } } async function getLineCount(designMdPath: string): Promise { try { const content = await readFile(designMdPath, 'utf-8'); return content.split('\n').length; } catch { return 0; } } async function main() { console.log(`\nšŸ”„ Clone Architect — Batch DESIGN.md Regeneration`); if (isDryRun) console.log(` DRY RUN — no files will be written`); if (domainFilter) console.log(` Filter: ${domainFilter} only`); console.log(''); // Discover all extraction directories let allDomains: string[] = []; try { const entries = await readdir(EXTRACTIONS_DIR); for (const entry of entries) { const fullPath = join(EXTRACTIONS_DIR, entry); try { const s = await stat(fullPath); if (s.isDirectory()) allDomains.push(entry); } catch { /* skip */ } } } catch (err) { console.error('Cannot read extractions directory:', err); process.exit(1); } if (domainFilter) { allDomains = allDomains.filter(d => d === domainFilter); if (allDomains.length === 0) { console.error(`Domain "${domainFilter}" not found in extractions/`); process.exit(1); } } allDomains.sort(); console.log(`Found ${allDomains.length} extraction(s) to process...\n`); const results: RegenResult[] = []; let okCount = 0; let skipCount = 0; let errorCount = 0; for (const domain of allDomains) { const extractionDir = join(EXTRACTIONS_DIR, domain); const rawCssPath = join(extractionDir, 'raw-css.json'); const tokensPath = join(extractionDir, 'tokens.json'); const designMdPath = join(extractionDir, 'DESIGN.md'); // Skip if missing required input files if (!existsSync(rawCssPath)) { results.push({ domain, status: 'skipped', reason: 'no raw-css.json' }); skipCount++; console.log(` ā­ļø ${domain} — skipped (no raw-css.json)`); continue; } if (!existsSync(tokensPath)) { results.push({ domain, status: 'skipped', reason: 'no tokens.json' }); skipCount++; console.log(` ā­ļø ${domain} — skipped (no tokens.json)`); continue; } if (isDryRun) { console.log(` šŸ”² ${domain} — would regenerate`); results.push({ domain, status: 'ok', reason: 'dry-run' }); okCount++; continue; } // Resolve tsx binary const localTsx = join(ROOT, 'node_modules', '.bin', 'tsx'); const tsxCmd = existsSync(localTsx) ? localTsx : 'npx'; const tsxArgs = existsSync(localTsx) ? [GENERATE_SCRIPT, domain] : ['tsx', GENERATE_SCRIPT, domain]; process.stdout.write(` āš™ļø ${domain}... `); const result = spawnSync(tsxCmd, tsxArgs, { cwd: ROOT, encoding: 'utf-8', timeout: 30000, // 30s max per domain }); if (result.error || result.status !== 0) { const errMsg = result.stderr?.split('\n')[0] || result.error?.message || 'unknown error'; console.log(`āŒ ${errMsg.slice(0, 80)}`); results.push({ domain, status: 'error', reason: errMsg }); errorCount++; continue; } // Read back completeness score const completeness = await getCompleteness(designMdPath); const lines = await getLineCount(designMdPath); const scoreDisplay = completeness !== null ? `${completeness}/100` : 'no score'; console.log(`āœ… ${lines}L | ${scoreDisplay}`); results.push({ domain, status: 'ok', completeness: completeness ?? undefined, lines }); okCount++; } // ── Summary ── console.log('\n' + '═'.repeat(60)); console.log(`šŸ“Š Regeneration Summary`); console.log(` āœ… OK: ${okCount} | ā­ļø Skipped: ${skipCount} | āŒ Errors: ${errorCount}`); if (!isDryRun) { const withScores = results.filter(r => r.completeness !== undefined); if (withScores.length > 0) { const avgScore = Math.round(withScores.reduce((sum, r) => sum + (r.completeness!), 0) / withScores.length); const maxScore = Math.max(...withScores.map(r => r.completeness!)); const minScore = Math.min(...withScores.map(r => r.completeness!)); console.log(` Completeness: avg ${avgScore}/100 | best ${maxScore} | worst ${minScore}`); // Top 5 and bottom 5 const sorted = [...withScores].sort((a, b) => b.completeness! - a.completeness!); console.log(`\n Top 5:`); for (const r of sorted.slice(0, 5)) { console.log(` ${r.domain}: ${r.completeness}/100`); } if (sorted.length > 5) { console.log(`\n Needs improvement:`); for (const r of sorted.slice(-5).reverse()) { console.log(` ${r.domain}: ${r.completeness}/100`); } } } // Run catalog enrichment to update catalog/index.json with new scores console.log('\nšŸ”§ Updating catalog/index.json with new scores...'); const localTsx3 = join(ROOT, 'node_modules', '.bin', 'tsx'); const tsxCmd3 = existsSync(localTsx3) ? localTsx3 : 'npx'; const enrichScript = join(ROOT, 'scripts', 'enrich-catalog.ts'); if (existsSync(enrichScript)) { const enrichArgs = existsSync(localTsx3) ? [enrichScript] : ['tsx', enrichScript]; const enrichResult = spawnSync(tsxCmd3, enrichArgs, { stdio: 'inherit', cwd: ROOT }); if (enrichResult.status !== 0) { console.warn('āš ļø catalog enrichment failed — run manually: npx tsx scripts/enrich-catalog.ts'); } } } console.log(''); process.exit(errorCount > 0 ? 1 : 0); } main().catch(err => { console.error('Fatal error in regen-catalog:', err); process.exit(1); });