prism-design 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +292 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/bin/clone-architect.mjs +476 -0
- package/bin/prism.mjs +467 -0
- package/catalog/index.json +1155 -0
- package/extractions/airbnb.com/DESIGN.md +1068 -0
- package/extractions/airbnb.com/tokens.json +507 -0
- package/extractions/attio.com/DESIGN.md +1295 -0
- package/extractions/attio.com/tokens.json +438 -0
- package/extractions/auroxdashboard.com/DESIGN.md +724 -0
- package/extractions/auroxdashboard.com/tokens.json +195 -0
- package/extractions/careerexplorer.com/DESIGN.md +1178 -0
- package/extractions/careerexplorer.com/tokens.json +141 -0
- package/extractions/chance.co/DESIGN.md +1209 -0
- package/extractions/chance.co/tokens.json +160 -0
- package/extractions/choisis-ton-avenir.com/DESIGN.md +1265 -0
- package/extractions/choisis-ton-avenir.com/tokens.json +227 -0
- package/extractions/example.com/DESIGN.md +436 -0
- package/extractions/example.com/tokens.json +91 -0
- package/extractions/getdesign.md/DESIGN.md +1009 -0
- package/extractions/getdesign.md/tokens.json +219 -0
- package/extractions/github.com/DESIGN.md +1130 -0
- package/extractions/github.com/tokens.json +2092 -0
- package/extractions/hello-charly.com/DESIGN.md +1146 -0
- package/extractions/hello-charly.com/tokens.json +322 -0
- package/extractions/hyperliquid.xyz/DESIGN.md +779 -0
- package/extractions/hyperliquid.xyz/tokens.json +598 -0
- package/extractions/instagram.com/DESIGN.md +996 -0
- package/extractions/instagram.com/tokens.json +1240 -0
- package/extractions/jobirl.com/DESIGN.md +1160 -0
- package/extractions/jobirl.com/tokens.json +139 -0
- package/extractions/life360.com/DESIGN.md +1133 -0
- package/extractions/life360.com/tokens.json +491 -0
- package/extractions/lifesum.com/DESIGN.md +965 -0
- package/extractions/lifesum.com/tokens.json +170 -0
- package/extractions/linear.app/DESIGN.md +1301 -0
- package/extractions/linear.app/tokens.json +732 -0
- package/extractions/mavoie.org/DESIGN.md +1148 -0
- package/extractions/mavoie.org/tokens.json +128 -0
- package/extractions/miro.com/DESIGN.md +1237 -0
- package/extractions/miro.com/tokens.json +401 -0
- package/extractions/notion.so/DESIGN.md +1319 -0
- package/extractions/notion.so/tokens.json +906 -0
- package/extractions/onetonline.org/DESIGN.md +909 -0
- package/extractions/onetonline.org/tokens.json +280 -0
- package/extractions/posthog.com/DESIGN.md +1024 -0
- package/extractions/posthog.com/tokens.json +197 -0
- package/extractions/revolut.com/DESIGN.md +1080 -0
- package/extractions/revolut.com/tokens.json +401 -0
- package/extractions/stripe.com/DESIGN.md +1272 -0
- package/extractions/stripe.com/tokens.json +794 -0
- package/extractions/switchcollective.com/DESIGN.md +1040 -0
- package/extractions/switchcollective.com/tokens.json +98 -0
- package/extractions/truity.com/DESIGN.md +970 -0
- package/extractions/truity.com/tokens.json +166 -0
- package/extractions/uniquekicks.be/DESIGN.md +1171 -0
- package/extractions/uniquekicks.be/tokens.json +237 -0
- package/package.json +122 -0
- package/scripts/analyze.ts +281 -0
- package/scripts/bank-register.ts +379 -0
- package/scripts/bank.ts +374 -0
- package/scripts/browser-stealth.ts +189 -0
- package/scripts/clone.ts +198 -0
- package/scripts/compare-vs-gd-final.ts +273 -0
- package/scripts/compare-vs-gd.ts +269 -0
- package/scripts/compare.ts +405 -0
- package/scripts/deploy-site.ts +181 -0
- package/scripts/diff-snapshots.ts +340 -0
- package/scripts/enrich-catalog.ts +212 -0
- package/scripts/extract.ts +2038 -0
- package/scripts/extractors/advanced.ts +524 -0
- package/scripts/extractors/widgets.ts +711 -0
- package/scripts/generate-design-md.ts +5775 -0
- package/scripts/generate-final-pdf.ts +274 -0
- package/scripts/generate-og-image.ts +87 -0
- package/scripts/generate-showcase.ts +1588 -0
- package/scripts/generate-site.ts +847 -0
- package/scripts/mass-extract.sh +91 -0
- package/scripts/post-process-all.sh +55 -0
- package/scripts/regen-catalog.ts +203 -0
- package/scripts/shared/cache.ts +149 -0
- package/scripts/shared/css-helpers.ts +263 -0
- package/scripts/shared/logger.ts +57 -0
- package/scripts/shared/named-colors.ts +355 -0
- package/scripts/shared/types.ts +220 -0
- package/scripts/sync-catalog.ts +105 -0
- package/scripts/tokenize.ts +988 -0
- package/templates/layout-template.md +52 -0
- package/templates/tokens-template.json +34 -0
package/scripts/clone.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism — Orchestrateur principal
|
|
3
|
+
*
|
|
4
|
+
* Pipeline complet : URL → Extract → Analyze → Tokenize
|
|
5
|
+
* Usage: npm run clone -- <URL>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'child_process';
|
|
9
|
+
import { existsSync } from 'fs';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { shouldSkipExtraction, writeFingerprint } from './shared/cache.js';
|
|
12
|
+
|
|
13
|
+
// Phase 2.3 — Parse CLI flags: clone <url> [--force] [--enrich]
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const forceFlag = args.includes('--force');
|
|
16
|
+
const enrichFlag = args.includes('--enrich');
|
|
17
|
+
const fidelityFlag = args.includes('--fidelity');
|
|
18
|
+
const url = args.find(a => !a.startsWith('--'));
|
|
19
|
+
|
|
20
|
+
if (!url) {
|
|
21
|
+
console.error('Usage: npm run clone -- <URL> [--force]');
|
|
22
|
+
console.error('Example: npm run clone -- https://linear.app');
|
|
23
|
+
console.error(' npm run clone -- https://linear.app --force (bypass cache)');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
new URL(url);
|
|
29
|
+
} catch {
|
|
30
|
+
console.error(`Invalid URL: ${url}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const domain = new URL(url).hostname.replace('www.', '');
|
|
35
|
+
const baseDir = join(process.cwd(), 'extractions', domain);
|
|
36
|
+
|
|
37
|
+
const startTime = Date.now();
|
|
38
|
+
|
|
39
|
+
console.log('╔══════════════════════════════════════════════════╗');
|
|
40
|
+
console.log('║ CLONE ARCHITECT — Full Pipeline ║');
|
|
41
|
+
console.log('╚══════════════════════════════════════════════════╝');
|
|
42
|
+
console.log(`\n🎯 Target: ${url}\n`);
|
|
43
|
+
|
|
44
|
+
// Phase 2.3 — Cache check before expensive Playwright extraction
|
|
45
|
+
const cacheResult = await shouldSkipExtraction(url, domain, { force: forceFlag });
|
|
46
|
+
if (cacheResult.skip) {
|
|
47
|
+
console.log(`✨ Cache hit: ${cacheResult.reason} — skipping extraction (saved ~140s)`);
|
|
48
|
+
console.log(` Use --force to bypass cache and re-extract`);
|
|
49
|
+
console.log(`\n📁 Existing extraction at: ${baseDir}\n`);
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
console.log(`🔍 Cache: ${cacheResult.reason}`);
|
|
53
|
+
|
|
54
|
+
// Step 1 — Extract
|
|
55
|
+
console.log('━━━ STEP 1/4 — EXTRACTION ━━━');
|
|
56
|
+
try {
|
|
57
|
+
execSync(`npx tsx scripts/extract.ts "${url}"`, {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
cwd: process.cwd(),
|
|
60
|
+
timeout: 120000,
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error('❌ Extraction failed');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Phase 5.4.1 — Write fingerprint ONLY after extraction validation
|
|
68
|
+
// Previously (bug G4): fingerprint was written even if raw-css.json was empty/cassée.
|
|
69
|
+
// Result: future runs hit cache and skip a broken extraction forever.
|
|
70
|
+
// Now: validate raw-css.json has at least 5 CSS vars AND a body element before writing fingerprint.
|
|
71
|
+
if (cacheResult.fingerprint) {
|
|
72
|
+
try {
|
|
73
|
+
const rawCssPath = join(baseDir, 'raw-css.json');
|
|
74
|
+
if (existsSync(rawCssPath)) {
|
|
75
|
+
const rawData = JSON.parse(require('fs').readFileSync(rawCssPath, 'utf-8'));
|
|
76
|
+
const cssVarCount = Object.keys(rawData?.desktop?.cssCustomProperties || {}).length;
|
|
77
|
+
const hasBody = !!rawData?.desktop?.elements?.body;
|
|
78
|
+
const hasColors = (rawData?.desktop?.allColors || []).length >= 3;
|
|
79
|
+
// Threshold: at least 3 colors + a body element. Extractions that fail anti-bot have ~0 vars + no body.
|
|
80
|
+
if (hasBody && hasColors) {
|
|
81
|
+
await writeFingerprint(domain, cacheResult.fingerprint);
|
|
82
|
+
} else {
|
|
83
|
+
console.warn(` ⚠️ Skipping cache fingerprint (cssVars=${cssVarCount}, hasBody=${hasBody}, colors=${(rawData?.desktop?.allColors || []).length}) — looks like an aborted extraction`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn(` ⚠️ Failed to validate raw-css.json before fingerprint: ${(e as Error).message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check extraction output
|
|
92
|
+
if (!existsSync(join(baseDir, 'raw-css.json'))) {
|
|
93
|
+
console.error('❌ raw-css.json not found after extraction');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Step 1b (v2.10-C) — download key assets locally so the extraction is self-contained (best-effort)
|
|
98
|
+
try {
|
|
99
|
+
const { downloadKeyAssets } = await import('./asset-downloader.js');
|
|
100
|
+
const a = await downloadKeyAssets(baseDir, domain);
|
|
101
|
+
console.log(` 📥 Assets: ${a.downloaded} downloaded, ${a.failed} failed → extractions/${domain}/assets/`);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
console.warn(` ⚠️ Asset download skipped (non-blocking): ${(err as Error).message}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Step 2 — Analyze
|
|
107
|
+
console.log('\n━━━ STEP 2/4 — LAYOUT ANALYSIS ━━━');
|
|
108
|
+
try {
|
|
109
|
+
execSync(`npx tsx scripts/analyze.ts "${domain}"`, {
|
|
110
|
+
stdio: 'inherit',
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
timeout: 30000,
|
|
113
|
+
});
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.error('❌ Analysis failed');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Step 3 — Tokenize
|
|
120
|
+
console.log('\n━━━ STEP 3/4 — TOKENIZATION ━━━');
|
|
121
|
+
try {
|
|
122
|
+
execSync(`npx tsx scripts/tokenize.ts "${domain}"`, {
|
|
123
|
+
stdio: 'inherit',
|
|
124
|
+
cwd: process.cwd(),
|
|
125
|
+
timeout: 30000,
|
|
126
|
+
});
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.error('❌ Tokenization failed');
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Step 4 — Generate DESIGN.md
|
|
133
|
+
console.log('\n━━━ STEP 4/5 — DESIGN.MD GENERATION ━━━\n');
|
|
134
|
+
try {
|
|
135
|
+
execSync(`npx tsx scripts/generate-design-md.ts "${domain}"`, {
|
|
136
|
+
stdio: 'inherit',
|
|
137
|
+
cwd: process.cwd(),
|
|
138
|
+
timeout: 30000,
|
|
139
|
+
});
|
|
140
|
+
} catch (err) {
|
|
141
|
+
console.error('⚠️ DESIGN.md generation failed (non-blocking)');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Step 5 — Generate showcase HTML
|
|
145
|
+
console.log('\n━━━ STEP 5/5 — SHOWCASE ━━━\n');
|
|
146
|
+
try {
|
|
147
|
+
execSync(`npx tsx scripts/generate-showcase.ts "${domain}"`, {
|
|
148
|
+
stdio: 'inherit',
|
|
149
|
+
cwd: process.cwd(),
|
|
150
|
+
timeout: 30000,
|
|
151
|
+
});
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error('⚠️ Showcase generation failed (non-blocking)');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Step 5b (optional) — Fidelity loop: rebuild a token-faithful page + pixelmatch vs original.
|
|
157
|
+
// Writes extractions/<domain>/comparison/score.json (above-fold, 2×-matched). HONEST: token-level
|
|
158
|
+
// reconstruction, not a pixel clone — the score is a floor. Run with --fidelity.
|
|
159
|
+
if (fidelityFlag) {
|
|
160
|
+
console.log('\n━━━ FIDELITY — token rebuild + pixelmatch (above-fold) ━━━\n');
|
|
161
|
+
try {
|
|
162
|
+
execSync(`npx tsx scripts/rebuild.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 30000 });
|
|
163
|
+
execSync(`npx tsx scripts/compare.ts "${domain}"`, { stdio: 'inherit', cwd: process.cwd(), timeout: 120000 });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
console.warn(`⚠️ Fidelity step failed (non-blocking): ${(err as Error).message}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Step 6 (optional) — Narrative enrichment via Claude API
|
|
170
|
+
if (enrichFlag) {
|
|
171
|
+
console.log('\n━━━ STEP 6/6 — NARRATIVE ENRICHMENT (LLM pass) ━━━\n');
|
|
172
|
+
try {
|
|
173
|
+
const { enrichDesignMd } = await import('./narrative-enricher.js');
|
|
174
|
+
await enrichDesignMd(domain, { apiKey: process.env.ANTHROPIC_API_KEY });
|
|
175
|
+
console.log(' DESIGN.enriched.md written ✅');
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.warn(`⚠️ Narrative enrichment failed (non-blocking): ${(err as Error).message}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
182
|
+
|
|
183
|
+
console.log('\n╔══════════════════════════════════════════════════╗');
|
|
184
|
+
console.log('║ PIPELINE COMPLETE ║');
|
|
185
|
+
console.log('╚══════════════════════════════════════════════════╝');
|
|
186
|
+
console.log(`\n⏱️ Duration: ${duration}s`);
|
|
187
|
+
console.log(`📁 Output: extractions/${domain}/`);
|
|
188
|
+
console.log(' ├── screenshots/ — Playwright captures');
|
|
189
|
+
console.log(' ├── raw-css.json — Computed styles brut');
|
|
190
|
+
console.log(' ├── extraction-summary.json');
|
|
191
|
+
console.log(' ├── layout-analysis.md — Structure analysée');
|
|
192
|
+
console.log(' ├── tokens.json — Design tokens normalisés');
|
|
193
|
+
console.log(' ├── DESIGN.md — Narratif LLM-optimisé (format Google DESIGN.md, 24 sections)');
|
|
194
|
+
console.log(' └── showcase/index.html — Page de présentation visuelle');
|
|
195
|
+
console.log('\n📋 Next steps:');
|
|
196
|
+
console.log(' 1. Review tokens.json and layout-analysis.md');
|
|
197
|
+
console.log(' 2. Use tokens to generate faithful code (R16 pipeline)');
|
|
198
|
+
console.log(' 3. Run: npm run compare -- ' + domain);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prism vs getdesign.md — HONEST Comparison Framework
|
|
3
|
+
*
|
|
4
|
+
* v2.12 REWRITE — the previous scorer measured VOLUME, not quality:
|
|
5
|
+
* - it rewarded raw line count (`normalizeLog(designMdLines)`),
|
|
6
|
+
* - it gave Prism a free +20 "verifiability" that getdesign could NEVER earn
|
|
7
|
+
* (screenshots+tokens are CA-only), then declared CA the winner.
|
|
8
|
+
* A ground-truthed audit (docs/comparison/auditmax-vs-gd) showed the opposite:
|
|
9
|
+
* getdesign wins on editorial/rebuild fidelity. So the old composite was self-deception.
|
|
10
|
+
*
|
|
11
|
+
* This version:
|
|
12
|
+
* - Scores COVERAGE + CONCRETENESS with an IDENTICAL formula for CA and GD (symmetric, fair).
|
|
13
|
+
* - Caps every sub-score at 100 so breadth beyond the canonical 9 sections can't inflate.
|
|
14
|
+
* - Removes line-count/volume entirely.
|
|
15
|
+
* - Reports Prism's structural advantages (screenshots, raw CSS, re-extractability) SEPARATELY,
|
|
16
|
+
* never folded into the head-to-head number.
|
|
17
|
+
* - States explicitly what it does NOT measure (editorial quality, rebuild fidelity).
|
|
18
|
+
*
|
|
19
|
+
* Output: docs/comparison/ca-vs-gd-final.{json,md}
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
23
|
+
import { join } from 'path';
|
|
24
|
+
|
|
25
|
+
const ROOT = process.cwd();
|
|
26
|
+
|
|
27
|
+
// ── GD slug → CA domain mapping ──────────────────────────────────────────
|
|
28
|
+
const GD_SLUG_MAP: Record<string, string> = {
|
|
29
|
+
'bmw-m': 'bmwm.com', 'hp': 'hp.com', 'airbnb': 'airbnb.com', 'airtable': 'airtable.com',
|
|
30
|
+
'apple': 'apple.com', 'binance': 'binance.com', 'bmw': 'bmw.com', 'bugatti': 'bugatti.com',
|
|
31
|
+
'cal': 'cal.com', 'claude': 'claude.com', 'clay': 'clay.com', 'clickhouse': 'clickhouse.com',
|
|
32
|
+
'cohere': 'cohere.com', 'coinbase': 'coinbase.com', 'composio': 'composio.dev',
|
|
33
|
+
'cursor': 'cursor.com', 'elevenlabs': 'elevenlabs.io', 'expo': 'expo.dev',
|
|
34
|
+
'ferrari': 'ferrari.com', 'figma': 'figma.com', 'framer': 'framer.com',
|
|
35
|
+
'hashicorp': 'hashicorp.com', 'ibm': 'ibm.com', 'intercom': 'intercom.com',
|
|
36
|
+
'kraken': 'kraken.com', 'lamborghini': 'lamborghini.com', 'linear': 'linear.app',
|
|
37
|
+
'lovable': 'lovable.dev', 'mastercard': 'mastercard.com', 'meta': 'meta.com',
|
|
38
|
+
'minimax': 'minimax.io', 'mintlify': 'mintlify.com', 'miro': 'miro.com',
|
|
39
|
+
'mistral.ai': 'mistral.ai', 'mongodb': 'mongodb.com', 'nike': 'nike.com',
|
|
40
|
+
'notion': 'notion.so', 'nvidia': 'nvidia.com', 'ollama': 'ollama.com',
|
|
41
|
+
'opencode.ai': 'opencode.ai', 'pinterest': 'pinterest.com', 'playstation': 'playstation.com',
|
|
42
|
+
'posthog': 'posthog.com', 'raycast': 'raycast.com', 'renault': 'renault.com',
|
|
43
|
+
'replicate': 'replicate.com', 'resend': 'resend.com', 'revolut': 'revolut.com',
|
|
44
|
+
'runwayml': 'runwayml.com', 'sanity': 'sanity.io', 'sentry': 'sentry.io',
|
|
45
|
+
'shopify': 'shopify.com', 'spacex': 'spacex.com', 'spotify': 'spotify.com',
|
|
46
|
+
'starbucks': 'starbucks.com', 'stripe': 'stripe.com', 'supabase': 'supabase.com',
|
|
47
|
+
'superhuman': 'superhuman.com', 'tesla': 'tesla.com', 'theverge': 'theverge.com',
|
|
48
|
+
'together.ai': 'together.ai', 'uber': 'uber.com', 'vercel': 'vercel.com',
|
|
49
|
+
'vodafone': 'vodafone.com', 'voltagent': 'voltagent.dev', 'warp': 'warp.dev',
|
|
50
|
+
'webflow': 'webflow.com', 'wired': 'wired.com', 'wise': 'wise.com',
|
|
51
|
+
'x.ai': 'x.ai', 'zapier': 'zapier.com'
|
|
52
|
+
};
|
|
53
|
+
const DOMAIN_TO_SLUG: Record<string, string> = Object.fromEntries(
|
|
54
|
+
Object.entries(GD_SLUG_MAP).map(([s, d]) => [d, s])
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// ── Helpers ──────────────────────────────────────────────────────────────
|
|
58
|
+
function countSections(content: string): number {
|
|
59
|
+
// count distinct top-level numbered/titled sections (## 1. X, ## Foo)
|
|
60
|
+
return content.split('\n').filter(l => /^##\s+\d?\.?\s*[A-Z]/.test(l)).length;
|
|
61
|
+
}
|
|
62
|
+
function extractHexColors(content: string): string[] {
|
|
63
|
+
const hexes = content.match(/#[0-9a-fA-F]{6}\b/g) || [];
|
|
64
|
+
return [...new Set(hexes.map(h => h.toLowerCase()))];
|
|
65
|
+
}
|
|
66
|
+
function extractFontFamilies(content: string): string[] {
|
|
67
|
+
// Format-agnostic: getdesign writes typography in PROSE ("Primary typeface: Inter"),
|
|
68
|
+
// Prism in CSS-ish form ("font-family: Inter"). Penalizing GD for prose = dishonest.
|
|
69
|
+
// Count typography presence across both conventions.
|
|
70
|
+
const matches = content.match(/font[-_ ]?family\s*:|typeface|font stack|primary font|monospace font/gi) || [];
|
|
71
|
+
return matches;
|
|
72
|
+
}
|
|
73
|
+
function hasFrontmatter(content: string): boolean {
|
|
74
|
+
return content.startsWith('---\n') || content.startsWith('---\r\n');
|
|
75
|
+
}
|
|
76
|
+
function rgbDistance(hex1: string, hex2: string): number {
|
|
77
|
+
const h1 = hex1.replace('#', ''), h2 = hex2.replace('#', '');
|
|
78
|
+
if (h1.length !== 6 || h2.length !== 6) return 999;
|
|
79
|
+
const r1 = parseInt(h1.slice(0, 2), 16), g1 = parseInt(h1.slice(2, 4), 16), b1 = parseInt(h1.slice(4, 6), 16);
|
|
80
|
+
const r2 = parseInt(h2.slice(0, 2), 16), g2 = parseInt(h2.slice(2, 4), 16), b2 = parseInt(h2.slice(4, 6), 16);
|
|
81
|
+
return Math.sqrt((r1 - r2) ** 2 + (g1 - g2) ** 2 + (b1 - b2) ** 2);
|
|
82
|
+
}
|
|
83
|
+
function avgPaletteDelta(paletteA: string[], paletteB: string[], k = 5): number {
|
|
84
|
+
const topA = paletteA.slice(0, k), topB = paletteB.slice(0, k);
|
|
85
|
+
if (topA.length === 0 || topB.length === 0) return 999;
|
|
86
|
+
let sum = 0, count = 0;
|
|
87
|
+
for (const ca of topA) { sum += Math.min(...topB.map(cb => rgbDistance(ca, cb))); count++; }
|
|
88
|
+
return count > 0 ? sum / count : 999;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Brand metrics ────────────────────────────────────────────────────────
|
|
92
|
+
interface DocMetrics {
|
|
93
|
+
designMdLines: number; sectionsCount: number; colorsCount: number;
|
|
94
|
+
palette: string[]; fontFamiliesCount: number; sizeKb: number; hasFrontmatter: boolean;
|
|
95
|
+
}
|
|
96
|
+
interface CABrand extends DocMetrics {
|
|
97
|
+
hasScreenshots: boolean; hasTokensJson: boolean; hasRawCss: boolean;
|
|
98
|
+
completenessScore: number; dark: boolean; fidelityScore: number | null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function baseMetrics(content: string): DocMetrics {
|
|
102
|
+
return {
|
|
103
|
+
designMdLines: content.split('\n').length,
|
|
104
|
+
sectionsCount: countSections(content),
|
|
105
|
+
colorsCount: extractHexColors(content).length,
|
|
106
|
+
palette: extractHexColors(content).slice(0, 10),
|
|
107
|
+
fontFamiliesCount: extractFontFamilies(content).length,
|
|
108
|
+
sizeKb: Math.round(Buffer.byteLength(content, 'utf-8') / 1024),
|
|
109
|
+
hasFrontmatter: hasFrontmatter(content),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function analyzeCA(domain: string, catalog: any): CABrand | null {
|
|
114
|
+
const dir = join(ROOT, 'extractions', domain);
|
|
115
|
+
const md = join(dir, 'DESIGN.md');
|
|
116
|
+
if (!existsSync(md)) return null;
|
|
117
|
+
const content = readFileSync(md, 'utf-8');
|
|
118
|
+
const entry = catalog.brands?.find((b: any) => b.domain === domain);
|
|
119
|
+
// optional fidelity score from compare.ts output (honest, may be absent)
|
|
120
|
+
let fidelityScore: number | null = null;
|
|
121
|
+
const cmpPath = join(dir, 'comparison', 'score.json');
|
|
122
|
+
if (existsSync(cmpPath)) {
|
|
123
|
+
try { fidelityScore = JSON.parse(readFileSync(cmpPath, 'utf-8')).score ?? null; } catch { /* noop */ }
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...baseMetrics(content),
|
|
127
|
+
hasScreenshots: existsSync(join(dir, 'screenshots')),
|
|
128
|
+
hasTokensJson: existsSync(join(dir, 'tokens.json')),
|
|
129
|
+
hasRawCss: existsSync(join(dir, 'raw-css.json')),
|
|
130
|
+
completenessScore: entry?.completeness || 0,
|
|
131
|
+
dark: entry?.dark || false,
|
|
132
|
+
fidelityScore,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function analyzeGD(slug: string): DocMetrics | null {
|
|
137
|
+
// GD reference DESIGN.md files live under compare-gd/<slug>/GD-DESIGN.md
|
|
138
|
+
const candidates = [
|
|
139
|
+
join(ROOT, 'compare-gd', slug, 'GD-DESIGN.md'),
|
|
140
|
+
join(ROOT, 'audit-getdesign', `${slug}-DESIGN.md`),
|
|
141
|
+
];
|
|
142
|
+
const path = candidates.find(p => existsSync(p));
|
|
143
|
+
if (!path) return null;
|
|
144
|
+
return baseMetrics(readFileSync(path, 'utf-8'));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── HONEST scoring — identical formula for CA and GD ───────────────────────
|
|
148
|
+
const CANONICAL_SECTIONS = 9; // the dimensions getdesign.md's own format covers
|
|
149
|
+
|
|
150
|
+
function coverageScore(sectionsCount: number): number {
|
|
151
|
+
return Math.round(Math.min(100, (sectionsCount / CANONICAL_SECTIONS) * 100));
|
|
152
|
+
}
|
|
153
|
+
function concretenessScore(colorsCount: number, fontFamiliesCount: number): number {
|
|
154
|
+
// Does the doc specify ENOUGH concrete values to act on? Saturates FAST — a curated 8-color doc
|
|
155
|
+
// is as "concrete" as a 50-color dump. This deliberately does NOT reward listing more colors
|
|
156
|
+
// (that was the old volume game in disguise, and it rewarded palette pollution). Both a tight
|
|
157
|
+
// hand-curated catalog and a broad auto-extraction reach 100 if they specify the essentials.
|
|
158
|
+
const colorPart = colorsCount >= 6 ? 55 : Math.round(colorsCount * 9);
|
|
159
|
+
const fontPart = fontFamiliesCount >= 1 ? 45 : 0;
|
|
160
|
+
return Math.min(100, colorPart + fontPart);
|
|
161
|
+
}
|
|
162
|
+
/** Documentation score: coverage + concreteness, identical for both sides. No volume, no verif bonus. */
|
|
163
|
+
function docScore(d: DocMetrics | null): number {
|
|
164
|
+
if (!d) return 0;
|
|
165
|
+
return Math.round(0.5 * coverageScore(d.sectionsCount) + 0.5 * concretenessScore(d.colorsCount, d.fontFamiliesCount));
|
|
166
|
+
}
|
|
167
|
+
/** Prism-only structural advantages — reported SEPARATELY, never added to the doc score (R26). */
|
|
168
|
+
function structuralAdvantages(ca: CABrand | null): string[] {
|
|
169
|
+
if (!ca) return [];
|
|
170
|
+
const a: string[] = [];
|
|
171
|
+
if (ca.hasScreenshots) a.push('desktop+mobile screenshots');
|
|
172
|
+
if (ca.hasRawCss) a.push('raw-css.json audit trail');
|
|
173
|
+
if (ca.hasTokensJson) a.push('tokens.json');
|
|
174
|
+
a.push('re-extractable / dated');
|
|
175
|
+
return a;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function verdict(ca: CABrand | null, gd: DocMetrics | null): { doc: 'CA' | 'GD' | 'tie'; reason: string } {
|
|
179
|
+
if (!ca && !gd) return { doc: 'tie', reason: 'no data' };
|
|
180
|
+
if (!gd) return { doc: 'CA', reason: 'no GD reference for this brand' };
|
|
181
|
+
if (!ca) return { doc: 'GD', reason: 'no Prism extraction for this brand' };
|
|
182
|
+
const c = docScore(ca), g = docScore(gd);
|
|
183
|
+
const doc = c > g + 8 ? 'CA' : g > c + 8 ? 'GD' : 'tie';
|
|
184
|
+
return { doc, reason: `coverage+concreteness ${c} vs ${g}` };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Main ─────────────────────────────────────────────────────────────────
|
|
188
|
+
async function main() {
|
|
189
|
+
console.log('🔬 Prism vs getdesign.md — HONEST comparison (coverage + concreteness, symmetric)\n');
|
|
190
|
+
|
|
191
|
+
const catalog = JSON.parse(readFileSync(join(ROOT, 'catalog', 'index.json'), 'utf-8'));
|
|
192
|
+
const gdDomains = Object.values(GD_SLUG_MAP);
|
|
193
|
+
|
|
194
|
+
const results: any[] = [];
|
|
195
|
+
const docWins = { ca: 0, gd: 0, tie: 0 };
|
|
196
|
+
let sumCA = 0, sumGD = 0, compared = 0, caStructuralCount = 0;
|
|
197
|
+
|
|
198
|
+
for (const domain of gdDomains) {
|
|
199
|
+
const slug = DOMAIN_TO_SLUG[domain];
|
|
200
|
+
const ca = analyzeCA(domain, catalog);
|
|
201
|
+
const gd = analyzeGD(slug);
|
|
202
|
+
if (!ca || !gd) continue; // only score brands where BOTH docs exist (honest head-to-head)
|
|
203
|
+
|
|
204
|
+
const sCA = docScore(ca), sGD = docScore(gd);
|
|
205
|
+
const v = verdict(ca, gd);
|
|
206
|
+
const adv = structuralAdvantages(ca);
|
|
207
|
+
const paletteDelta = Math.round(avgPaletteDelta(ca.palette, gd.palette, 5));
|
|
208
|
+
|
|
209
|
+
compared++; sumCA += sCA; sumGD += sGD;
|
|
210
|
+
docWins[v.doc === 'CA' ? 'ca' : v.doc === 'GD' ? 'gd' : 'tie']++;
|
|
211
|
+
if (adv.length >= 3) caStructuralCount++;
|
|
212
|
+
|
|
213
|
+
results.push({
|
|
214
|
+
domain, slug,
|
|
215
|
+
caDoc: sCA, gdDoc: sGD,
|
|
216
|
+
caCoverage: coverageScore(ca.sectionsCount), gdCoverage: coverageScore(gd.sectionsCount),
|
|
217
|
+
caConcreteness: concretenessScore(ca.colorsCount, ca.fontFamiliesCount),
|
|
218
|
+
gdConcreteness: concretenessScore(gd.colorsCount, gd.fontFamiliesCount),
|
|
219
|
+
caFidelity: ca.fidelityScore, caStructural: adv, paletteDelta, verdict: v,
|
|
220
|
+
});
|
|
221
|
+
console.log(` ${domain.padEnd(22)} doc CA ${String(sCA).padStart(3)} vs GD ${String(sGD).padStart(3)} → ${v.doc.toUpperCase()} ${ca.fidelityScore != null ? `(fidelity ${ca.fidelityScore}%)` : ''}`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const avgCA = compared ? Math.round(sumCA / compared) : 0;
|
|
225
|
+
const avgGD = compared ? Math.round(sumGD / compared) : 0;
|
|
226
|
+
|
|
227
|
+
console.log('\n══════════════════════════════════════════════════');
|
|
228
|
+
console.log('📊 HONEST DOC SCORE (coverage + concreteness, same formula both sides)');
|
|
229
|
+
console.log('══════════════════════════════════════════════════');
|
|
230
|
+
console.log(`Brands with BOTH docs : ${compared}`);
|
|
231
|
+
console.log(`Avg doc score : CA ${avgCA} vs GD ${avgGD} (Δ ${avgCA - avgGD})`);
|
|
232
|
+
console.log(`Doc-coverage wins : CA ${docWins.ca} | GD ${docWins.gd} | tie ${docWins.tie}`);
|
|
233
|
+
console.log(`CA structural extras : ${caStructuralCount}/${compared} brands ship screenshots+rawCSS+tokens`);
|
|
234
|
+
console.log('\n⚠️ NOT measured here: editorial-narrative quality (human-judged; a ground-truth audit favored getdesign)');
|
|
235
|
+
console.log('⚠️ NOT measured here: rebuild fidelity (run scripts/compare.ts per brand — pixelmatch vs original)');
|
|
236
|
+
|
|
237
|
+
// ── Save outputs ────────────────────────────────────────────────────
|
|
238
|
+
const outDir = join(ROOT, 'docs', 'comparison');
|
|
239
|
+
mkdirSync(outDir, { recursive: true });
|
|
240
|
+
|
|
241
|
+
const jsonOut = {
|
|
242
|
+
generatedAt: new Date().toISOString(),
|
|
243
|
+
method: 'symmetric coverage+concreteness; volume removed; verifiability reported separately',
|
|
244
|
+
comparedCount: compared,
|
|
245
|
+
avg: { caDoc: avgCA, gdDoc: avgGD, delta: avgCA - avgGD },
|
|
246
|
+
docWins,
|
|
247
|
+
caStructuralCount,
|
|
248
|
+
notMeasured: ['editorial narrative quality (human-judged, audit favored GD)', 'rebuild fidelity (see compare.ts)'],
|
|
249
|
+
brands: results,
|
|
250
|
+
};
|
|
251
|
+
writeFileSync(join(outDir, 'ca-vs-gd-final.json'), JSON.stringify(jsonOut, null, 2));
|
|
252
|
+
|
|
253
|
+
let md = `# Prism vs getdesign.md — Honest Comparison\n\n`;
|
|
254
|
+
md += `Generated: ${jsonOut.generatedAt}\n\n`;
|
|
255
|
+
md += `**Method (v2.12, honest):** documentation score = 50% coverage (vs the 9 canonical sections) + 50% concreteness (concrete hex/font values, capped). **Identical formula for both sides.** Volume (line count) removed — it rewarded bloat. Prism's structural artifacts (screenshots, raw CSS, tokens) are listed **separately**, never added to the score.\n\n`;
|
|
256
|
+
md += `> **What this does NOT measure:** editorial-narrative quality (human-judged — a ground-truth audit favored getdesign) and rebuild fidelity (pixelmatch, see \`compare.ts\`). This tool measures documentation **coverage + concreteness only.**\n\n`;
|
|
257
|
+
md += `## Aggregate\n\n`;
|
|
258
|
+
md += `| Metric | Prism | getdesign.md | Δ |\n|---|---|---|---|\n`;
|
|
259
|
+
md += `| Brands compared (both docs present) | ${compared} | ${compared} | — |\n`;
|
|
260
|
+
md += `| Avg doc score /100 | ${avgCA} | ${avgGD} | ${avgCA - avgGD >= 0 ? '+' : ''}${avgCA - avgGD} |\n`;
|
|
261
|
+
md += `| Doc-coverage wins | ${docWins.ca} | ${docWins.gd} | (tie ${docWins.tie}) |\n`;
|
|
262
|
+
md += `| Ships screenshots + rawCSS + tokens | ${caStructuralCount}/${compared} | 0/${compared} | structural |\n\n`;
|
|
263
|
+
md += `## Per-brand\n\n`;
|
|
264
|
+
md += `| Brand | CA doc | GD doc | CA cover | GD cover | CA concr | GD concr | ΔE palette | Verdict |\n|---|---|---|---|---|---|---|---|---|\n`;
|
|
265
|
+
for (const r of [...results].sort((a, b) => b.caDoc - a.caDoc)) {
|
|
266
|
+
md += `| ${r.domain} | ${r.caDoc} | ${r.gdDoc} | ${r.caCoverage} | ${r.gdCoverage} | ${r.caConcreteness} | ${r.gdConcreteness} | ${r.paletteDelta} | ${r.verdict.doc.toUpperCase()} |\n`;
|
|
267
|
+
}
|
|
268
|
+
md += `\n*Prism's edge is automation + verifiable artifacts + re-extractability + breadth — not a claim of better editorial fidelity. Where the doc scores tie, Prism's separately-listed structural artifacts are the real differentiator.*\n`;
|
|
269
|
+
writeFileSync(join(outDir, 'ca-vs-gd-final.md'), md);
|
|
270
|
+
console.log(`\n💾 docs/comparison/ca-vs-gd-final.{json,md}`);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
main().catch(err => { console.error(err); process.exit(1); });
|