real-prototypes-skill 0.1.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/.claude/skills/agent-browser-skill/SKILL.md +252 -0
- package/.claude/skills/real-prototypes-skill/.gitignore +188 -0
- package/.claude/skills/real-prototypes-skill/ACCESSIBILITY.md +668 -0
- package/.claude/skills/real-prototypes-skill/INSTALL.md +259 -0
- package/.claude/skills/real-prototypes-skill/LICENSE +21 -0
- package/.claude/skills/real-prototypes-skill/PUBLISH.md +310 -0
- package/.claude/skills/real-prototypes-skill/QUICKSTART.md +240 -0
- package/.claude/skills/real-prototypes-skill/README.md +442 -0
- package/.claude/skills/real-prototypes-skill/SKILL.md +375 -0
- package/.claude/skills/real-prototypes-skill/capture/capture-engine.js +1153 -0
- package/.claude/skills/real-prototypes-skill/capture/config.schema.json +170 -0
- package/.claude/skills/real-prototypes-skill/cli.js +596 -0
- package/.claude/skills/real-prototypes-skill/docs/TROUBLESHOOTING.md +278 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/capture-config.md +167 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/design-tokens.md +183 -0
- package/.claude/skills/real-prototypes-skill/docs/schemas/manifest.md +169 -0
- package/.claude/skills/real-prototypes-skill/examples/CLAUDE.md.example +73 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/CLAUDE.md +136 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/FEATURES.md +222 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/README.md +82 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/design-tokens.json +87 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/homepage-viewport.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-chatbot-final.png +0 -0
- package/.claude/skills/real-prototypes-skill/examples/amazon-chatbot/references/screenshots/prototype-fullpage-v2.png +0 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-fixes.md +298 -0
- package/.claude/skills/real-prototypes-skill/references/accessibility-report.json +253 -0
- package/.claude/skills/real-prototypes-skill/scripts/CAPTURE-ENHANCEMENTS.md +344 -0
- package/.claude/skills/real-prototypes-skill/scripts/IMPLEMENTATION-SUMMARY.md +517 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICK-START.md +229 -0
- package/.claude/skills/real-prototypes-skill/scripts/QUICKSTART-layout-analysis.md +148 -0
- package/.claude/skills/real-prototypes-skill/scripts/README-analyze-layout.md +407 -0
- package/.claude/skills/real-prototypes-skill/scripts/analyze-layout.js +880 -0
- package/.claude/skills/real-prototypes-skill/scripts/capture-platform.js +203 -0
- package/.claude/skills/real-prototypes-skill/scripts/comprehensive-capture.js +597 -0
- package/.claude/skills/real-prototypes-skill/scripts/create-manifest.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/enterprise-pipeline.js +428 -0
- package/.claude/skills/real-prototypes-skill/scripts/extract-tokens.js +468 -0
- package/.claude/skills/real-prototypes-skill/scripts/full-site-capture.js +738 -0
- package/.claude/skills/real-prototypes-skill/scripts/generate-tailwind-config.js +296 -0
- package/.claude/skills/real-prototypes-skill/scripts/integrate-accessibility.sh +161 -0
- package/.claude/skills/real-prototypes-skill/scripts/manifest-schema.json +302 -0
- package/.claude/skills/real-prototypes-skill/scripts/setup-prototype.sh +167 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-analyze-layout.js +338 -0
- package/.claude/skills/real-prototypes-skill/scripts/test-validation.js +307 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-accessibility.js +598 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-manifest.js +499 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-output.js +361 -0
- package/.claude/skills/real-prototypes-skill/scripts/validate-prerequisites.js +319 -0
- package/.claude/skills/real-prototypes-skill/scripts/verify-layout-analysis.sh +77 -0
- package/.claude/skills/real-prototypes-skill/templates/dashboard-widget.tsx.template +91 -0
- package/.claude/skills/real-prototypes-skill/templates/data-table.tsx.template +193 -0
- package/.claude/skills/real-prototypes-skill/templates/form-section.tsx.template +250 -0
- package/.claude/skills/real-prototypes-skill/templates/modal-dialog.tsx.template +239 -0
- package/.claude/skills/real-prototypes-skill/templates/nav-item.tsx.template +265 -0
- package/.claude/skills/real-prototypes-skill/validation/validation-engine.js +559 -0
- package/.env.example +74 -0
- package/LICENSE +21 -0
- package/README.md +444 -0
- package/bin/cli.js +319 -0
- package/package.json +59 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* POST-GENERATION VALIDATION
|
|
4
|
+
*
|
|
5
|
+
* This script validates that the generated prototype:
|
|
6
|
+
* 1. Uses ONLY colors from the extracted design tokens
|
|
7
|
+
* 2. Uses the correct font families
|
|
8
|
+
* 3. Doesn't contain any "made up" colors
|
|
9
|
+
*
|
|
10
|
+
* Exit codes:
|
|
11
|
+
* 0 = All validations passed
|
|
12
|
+
* 1 = Validation failed - colors don't match
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const REFERENCES_DIR = 'references';
|
|
19
|
+
const PROTOTYPE_DIR = 'prototype';
|
|
20
|
+
|
|
21
|
+
// Colors that are always allowed (CSS basics)
|
|
22
|
+
const ALLOWED_BASE_COLORS = [
|
|
23
|
+
'#fff', '#ffffff', '#000', '#000000',
|
|
24
|
+
'transparent', 'inherit', 'currentColor'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Tailwind color classes that might sneak through
|
|
28
|
+
const FORBIDDEN_TAILWIND_PATTERNS = [
|
|
29
|
+
/\b(teal|cyan|emerald|violet|purple|pink|rose|fuchsia|indigo)-\d{2,3}\b/,
|
|
30
|
+
/\bbg-(teal|cyan|emerald|violet|purple|pink|rose|fuchsia|indigo)/,
|
|
31
|
+
/\btext-(teal|cyan|emerald|violet|purple|pink|rose|fuchsia|indigo)/,
|
|
32
|
+
/\bborder-(teal|cyan|emerald|violet|purple|pink|rose|fuchsia|indigo)/
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function log(type, message) {
|
|
36
|
+
const icons = {
|
|
37
|
+
error: '❌',
|
|
38
|
+
warning: '⚠️',
|
|
39
|
+
success: '✅',
|
|
40
|
+
info: 'ℹ️'
|
|
41
|
+
};
|
|
42
|
+
console.log(`${icons[type] || '•'} ${message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function loadDesignTokens() {
|
|
46
|
+
const tokensPath = path.join(REFERENCES_DIR, 'design-tokens.json');
|
|
47
|
+
if (!fs.existsSync(tokensPath)) {
|
|
48
|
+
throw new Error('design-tokens.json not found. Run validation-prerequisites.js first.');
|
|
49
|
+
}
|
|
50
|
+
return JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getAllowedColors(tokens) {
|
|
54
|
+
const allowed = new Set(ALLOWED_BASE_COLORS);
|
|
55
|
+
|
|
56
|
+
// Add all colors from rawColors
|
|
57
|
+
if (tokens.rawColors) {
|
|
58
|
+
tokens.rawColors.forEach(([color]) => {
|
|
59
|
+
allowed.add(color.toLowerCase());
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Add all categorized colors
|
|
64
|
+
function extractColors(obj) {
|
|
65
|
+
if (!obj) return;
|
|
66
|
+
if (typeof obj === 'string' && obj.startsWith('#')) {
|
|
67
|
+
allowed.add(obj.toLowerCase());
|
|
68
|
+
} else if (typeof obj === 'object') {
|
|
69
|
+
Object.values(obj).forEach(extractColors);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
extractColors(tokens.colors);
|
|
74
|
+
|
|
75
|
+
return allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extractColorsFromCode(code) {
|
|
79
|
+
const colors = new Set();
|
|
80
|
+
|
|
81
|
+
// Extract hex colors
|
|
82
|
+
const hexMatches = code.match(/#[0-9a-fA-F]{3,8}/g) || [];
|
|
83
|
+
hexMatches.forEach(color => colors.add(color.toLowerCase()));
|
|
84
|
+
|
|
85
|
+
// Extract rgb/rgba colors
|
|
86
|
+
const rgbMatches = code.match(/rgba?\([^)]+\)/g) || [];
|
|
87
|
+
rgbMatches.forEach(color => colors.add(color.toLowerCase()));
|
|
88
|
+
|
|
89
|
+
return colors;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function findForbiddenTailwindClasses(code) {
|
|
93
|
+
const forbidden = [];
|
|
94
|
+
|
|
95
|
+
FORBIDDEN_TAILWIND_PATTERNS.forEach(pattern => {
|
|
96
|
+
const matches = code.match(pattern);
|
|
97
|
+
if (matches) {
|
|
98
|
+
forbidden.push(...matches);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return [...new Set(forbidden)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateGeneratedFile(filePath, allowedColors, tokens) {
|
|
106
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
107
|
+
const result = {
|
|
108
|
+
file: filePath,
|
|
109
|
+
passed: true,
|
|
110
|
+
invalidColors: [],
|
|
111
|
+
forbiddenClasses: [],
|
|
112
|
+
warnings: []
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
// Check for colors not in design tokens
|
|
116
|
+
const usedColors = extractColorsFromCode(code);
|
|
117
|
+
usedColors.forEach(color => {
|
|
118
|
+
// Normalize 3-digit hex to 6-digit
|
|
119
|
+
let normalizedColor = color;
|
|
120
|
+
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
121
|
+
normalizedColor = `#${color[1]}${color[1]}${color[2]}${color[2]}${color[3]}${color[3]}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!allowedColors.has(normalizedColor) && !allowedColors.has(color)) {
|
|
125
|
+
result.invalidColors.push(color);
|
|
126
|
+
result.passed = false;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Check for forbidden Tailwind classes
|
|
131
|
+
const forbiddenClasses = findForbiddenTailwindClasses(code);
|
|
132
|
+
if (forbiddenClasses.length > 0) {
|
|
133
|
+
result.forbiddenClasses = forbiddenClasses;
|
|
134
|
+
result.passed = false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if primary color is being used
|
|
138
|
+
const primaryColor = tokens.colors?.primary;
|
|
139
|
+
if (primaryColor && !code.includes(primaryColor)) {
|
|
140
|
+
result.warnings.push(`Primary color ${primaryColor} not found in file - buttons/links may have wrong color`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for font family usage
|
|
144
|
+
const fontFamily = tokens.fonts?.primary;
|
|
145
|
+
if (fontFamily && !code.toLowerCase().includes(fontFamily.toLowerCase().split(',')[0])) {
|
|
146
|
+
result.warnings.push(`Primary font "${fontFamily}" not found in file`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function findPrototypeFiles(dir, files = []) {
|
|
153
|
+
const items = fs.readdirSync(dir);
|
|
154
|
+
|
|
155
|
+
items.forEach(item => {
|
|
156
|
+
const fullPath = path.join(dir, item);
|
|
157
|
+
const stat = fs.statSync(fullPath);
|
|
158
|
+
|
|
159
|
+
if (stat.isDirectory()) {
|
|
160
|
+
// Skip node_modules and .next
|
|
161
|
+
if (item !== 'node_modules' && item !== '.next') {
|
|
162
|
+
findPrototypeFiles(fullPath, files);
|
|
163
|
+
}
|
|
164
|
+
} else if (
|
|
165
|
+
item.endsWith('.tsx') ||
|
|
166
|
+
item.endsWith('.jsx') ||
|
|
167
|
+
item.endsWith('.ts') ||
|
|
168
|
+
item.endsWith('.css')
|
|
169
|
+
) {
|
|
170
|
+
files.push(fullPath);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return files;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generateReport(results, tokens) {
|
|
178
|
+
const report = {
|
|
179
|
+
timestamp: new Date().toISOString(),
|
|
180
|
+
passed: results.every(r => r.passed),
|
|
181
|
+
summary: {
|
|
182
|
+
filesChecked: results.length,
|
|
183
|
+
filesPassed: results.filter(r => r.passed).length,
|
|
184
|
+
filesFailed: results.filter(r => !r.passed).length,
|
|
185
|
+
totalInvalidColors: results.reduce((sum, r) => sum + r.invalidColors.length, 0),
|
|
186
|
+
totalForbiddenClasses: results.reduce((sum, r) => sum + r.forbiddenClasses.length, 0)
|
|
187
|
+
},
|
|
188
|
+
allowedPrimaryColor: tokens.colors?.primary,
|
|
189
|
+
allowedFontFamily: tokens.fonts?.primary,
|
|
190
|
+
results: results
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const reportPath = path.join(REFERENCES_DIR, 'output-validation-report.json');
|
|
194
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
195
|
+
|
|
196
|
+
return report;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function suggestFixes(results, tokens) {
|
|
200
|
+
const allInvalidColors = [...new Set(results.flatMap(r => r.invalidColors))];
|
|
201
|
+
const allForbiddenClasses = [...new Set(results.flatMap(r => r.forbiddenClasses))];
|
|
202
|
+
|
|
203
|
+
if (allInvalidColors.length === 0 && allForbiddenClasses.length === 0) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
console.log('\n📝 SUGGESTED FIXES:\n');
|
|
208
|
+
|
|
209
|
+
if (allInvalidColors.length > 0) {
|
|
210
|
+
console.log('Invalid colors found - replace with these design token colors:');
|
|
211
|
+
console.log('');
|
|
212
|
+
|
|
213
|
+
allInvalidColors.forEach(invalidColor => {
|
|
214
|
+
const suggestion = findClosestColor(invalidColor, tokens);
|
|
215
|
+
console.log(` ${invalidColor} → ${suggestion.color} (${suggestion.category})`);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (allForbiddenClasses.length > 0) {
|
|
220
|
+
console.log('\nForbidden Tailwind classes - use inline styles with hex values:');
|
|
221
|
+
console.log('');
|
|
222
|
+
|
|
223
|
+
allForbiddenClasses.forEach(cls => {
|
|
224
|
+
console.log(` "${cls}" → style={{ color: "${tokens.colors?.primary || '#1c64f2'}" }}`);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { invalidColors: allInvalidColors, forbiddenClasses: allForbiddenClasses };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function findClosestColor(targetColor, tokens) {
|
|
232
|
+
// Simple suggestion based on color characteristics
|
|
233
|
+
const hex = targetColor.startsWith('#') ? targetColor : null;
|
|
234
|
+
if (!hex) return { color: tokens.colors?.primary || '#1c64f2', category: 'primary' };
|
|
235
|
+
|
|
236
|
+
const rgb = hexToRgb(hex);
|
|
237
|
+
if (!rgb) return { color: tokens.colors?.primary || '#1c64f2', category: 'primary' };
|
|
238
|
+
|
|
239
|
+
const brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
|
|
240
|
+
|
|
241
|
+
// Suggest based on brightness
|
|
242
|
+
if (brightness > 240) {
|
|
243
|
+
return { color: tokens.colors?.background?.white || '#ffffff', category: 'background.white' };
|
|
244
|
+
} else if (brightness > 200) {
|
|
245
|
+
return { color: tokens.colors?.background?.light || '#f6f6f5', category: 'background.light' };
|
|
246
|
+
} else if (brightness > 150) {
|
|
247
|
+
return { color: tokens.colors?.border?.default || '#e7e7e6', category: 'border' };
|
|
248
|
+
} else if (brightness > 100) {
|
|
249
|
+
return { color: tokens.colors?.text?.secondary || '#6b7280', category: 'text.secondary' };
|
|
250
|
+
} else if (brightness > 50) {
|
|
251
|
+
return { color: tokens.colors?.text?.primary || '#191918', category: 'text.primary' };
|
|
252
|
+
} else {
|
|
253
|
+
return { color: tokens.colors?.sidebar?.dark || '#0e2933', category: 'sidebar' };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function hexToRgb(hex) {
|
|
258
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
259
|
+
return result ? {
|
|
260
|
+
r: parseInt(result[1], 16),
|
|
261
|
+
g: parseInt(result[2], 16),
|
|
262
|
+
b: parseInt(result[3], 16)
|
|
263
|
+
} : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function runValidation(options = {}) {
|
|
267
|
+
console.log('\n' + '='.repeat(60));
|
|
268
|
+
console.log('🔍 POST-GENERATION VALIDATION');
|
|
269
|
+
console.log('='.repeat(60) + '\n');
|
|
270
|
+
|
|
271
|
+
// Load design tokens
|
|
272
|
+
let tokens;
|
|
273
|
+
try {
|
|
274
|
+
tokens = loadDesignTokens();
|
|
275
|
+
log('success', `Loaded design tokens (${tokens.totalColorsFound || 'unknown'} colors)`);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
log('error', error.message);
|
|
278
|
+
return { passed: false, errors: [error.message] };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Get allowed colors
|
|
282
|
+
const allowedColors = getAllowedColors(tokens);
|
|
283
|
+
log('info', `${allowedColors.size} colors in allowed palette`);
|
|
284
|
+
|
|
285
|
+
// Find all prototype files
|
|
286
|
+
const prototypeDir = options.prototypeDir || PROTOTYPE_DIR;
|
|
287
|
+
if (!fs.existsSync(prototypeDir)) {
|
|
288
|
+
log('error', `Prototype directory not found: ${prototypeDir}`);
|
|
289
|
+
return { passed: false, errors: ['Prototype directory not found'] };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const files = findPrototypeFiles(path.join(prototypeDir, 'src'));
|
|
293
|
+
log('info', `Found ${files.length} source files to validate`);
|
|
294
|
+
|
|
295
|
+
// Validate each file
|
|
296
|
+
const results = [];
|
|
297
|
+
files.forEach(file => {
|
|
298
|
+
const result = validateGeneratedFile(file, allowedColors, tokens);
|
|
299
|
+
results.push(result);
|
|
300
|
+
|
|
301
|
+
if (!result.passed) {
|
|
302
|
+
log('error', `${path.relative(prototypeDir, file)}`);
|
|
303
|
+
if (result.invalidColors.length > 0) {
|
|
304
|
+
console.log(` Invalid colors: ${result.invalidColors.join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
if (result.forbiddenClasses.length > 0) {
|
|
307
|
+
console.log(` Forbidden classes: ${result.forbiddenClasses.join(', ')}`);
|
|
308
|
+
}
|
|
309
|
+
} else if (result.warnings.length > 0) {
|
|
310
|
+
log('warning', `${path.relative(prototypeDir, file)}`);
|
|
311
|
+
result.warnings.forEach(w => console.log(` ${w}`));
|
|
312
|
+
} else {
|
|
313
|
+
log('success', `${path.relative(prototypeDir, file)}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Generate report
|
|
318
|
+
const report = generateReport(results, tokens);
|
|
319
|
+
|
|
320
|
+
// Show fixes if needed
|
|
321
|
+
if (!report.passed) {
|
|
322
|
+
suggestFixes(results, tokens);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Final summary
|
|
326
|
+
console.log('\n' + '='.repeat(60));
|
|
327
|
+
if (report.passed) {
|
|
328
|
+
console.log('✅ OUTPUT VALIDATION PASSED');
|
|
329
|
+
console.log(' All colors match the captured design tokens');
|
|
330
|
+
} else {
|
|
331
|
+
console.log('❌ OUTPUT VALIDATION FAILED');
|
|
332
|
+
console.log(` ${report.summary.totalInvalidColors} invalid colors found`);
|
|
333
|
+
console.log(` ${report.summary.totalForbiddenClasses} forbidden Tailwind classes found`);
|
|
334
|
+
console.log('\n Fix the issues above and run validation again.');
|
|
335
|
+
}
|
|
336
|
+
console.log('='.repeat(60) + '\n');
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
passed: report.passed,
|
|
340
|
+
report: report
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// CLI interface
|
|
345
|
+
if (require.main === module) {
|
|
346
|
+
const args = process.argv.slice(2);
|
|
347
|
+
const options = {};
|
|
348
|
+
|
|
349
|
+
for (let i = 0; i < args.length; i++) {
|
|
350
|
+
if (args[i] === '--dir' && args[i + 1]) {
|
|
351
|
+
options.prototypeDir = args[i + 1];
|
|
352
|
+
i++;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
runValidation(options).then(result => {
|
|
357
|
+
process.exit(result.passed ? 0 : 1);
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = { runValidation };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* PRE-GENERATION VALIDATION
|
|
4
|
+
*
|
|
5
|
+
* This script MUST pass before any prototype generation can begin.
|
|
6
|
+
* It validates that all required reference materials exist and are complete.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes:
|
|
9
|
+
* 0 = All validations passed, safe to proceed
|
|
10
|
+
* 1 = Validation failed, DO NOT proceed with generation
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const REFERENCES_DIR = 'references';
|
|
17
|
+
|
|
18
|
+
// Minimum requirements for generation
|
|
19
|
+
const REQUIREMENTS = {
|
|
20
|
+
minColors: 10,
|
|
21
|
+
minFonts: 1,
|
|
22
|
+
requiredColorCategories: ['primary', 'text', 'background', 'border'],
|
|
23
|
+
requiredFiles: [
|
|
24
|
+
'design-tokens.json',
|
|
25
|
+
'manifest.json'
|
|
26
|
+
]
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class ValidationError extends Error {
|
|
30
|
+
constructor(message, category) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.category = category;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function log(type, message) {
|
|
37
|
+
const icons = {
|
|
38
|
+
error: '❌',
|
|
39
|
+
warning: '⚠️',
|
|
40
|
+
success: '✅',
|
|
41
|
+
info: 'ℹ️'
|
|
42
|
+
};
|
|
43
|
+
console.log(`${icons[type] || '•'} ${message}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateFileExists(filePath, description) {
|
|
47
|
+
const fullPath = path.join(REFERENCES_DIR, filePath);
|
|
48
|
+
if (!fs.existsSync(fullPath)) {
|
|
49
|
+
throw new ValidationError(
|
|
50
|
+
`Missing required file: ${filePath}\n ${description}`,
|
|
51
|
+
'file'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return fullPath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function validateDesignTokens() {
|
|
58
|
+
log('info', 'Validating design tokens...');
|
|
59
|
+
|
|
60
|
+
const tokensPath = validateFileExists(
|
|
61
|
+
'design-tokens.json',
|
|
62
|
+
'Run comprehensive-capture.js first to extract design tokens'
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const tokens = JSON.parse(fs.readFileSync(tokensPath, 'utf8'));
|
|
66
|
+
const errors = [];
|
|
67
|
+
|
|
68
|
+
// Check total colors
|
|
69
|
+
const totalColors = tokens.totalColorsFound || Object.keys(tokens.rawColors || {}).length;
|
|
70
|
+
if (totalColors < REQUIREMENTS.minColors) {
|
|
71
|
+
errors.push(`Insufficient colors: found ${totalColors}, need at least ${REQUIREMENTS.minColors}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check required color categories
|
|
75
|
+
REQUIREMENTS.requiredColorCategories.forEach(category => {
|
|
76
|
+
if (!tokens.colors || !tokens.colors[category]) {
|
|
77
|
+
errors.push(`Missing color category: ${category}`);
|
|
78
|
+
} else if (category === 'primary' && !tokens.colors.primary) {
|
|
79
|
+
errors.push('Primary color not identified');
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Check fonts
|
|
84
|
+
if (!tokens.fonts || !tokens.fonts.families || tokens.fonts.families.length < REQUIREMENTS.minFonts) {
|
|
85
|
+
errors.push('No font families extracted');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for specific critical colors
|
|
89
|
+
if (!tokens.colors?.primary) {
|
|
90
|
+
errors.push('Primary/accent color not identified - buttons and links will use wrong color');
|
|
91
|
+
}
|
|
92
|
+
if (!tokens.colors?.sidebar?.dark && !tokens.colors?.sidebar?.bg) {
|
|
93
|
+
// Not an error, just a warning
|
|
94
|
+
log('warning', 'Sidebar color not identified - may need manual verification');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (errors.length > 0) {
|
|
98
|
+
throw new ValidationError(
|
|
99
|
+
`Design tokens incomplete:\n ${errors.join('\n ')}`,
|
|
100
|
+
'tokens'
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
log('success', `Design tokens valid (${totalColors} colors, ${tokens.fonts.families.length} fonts)`);
|
|
105
|
+
return tokens;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateManifest() {
|
|
109
|
+
log('info', 'Validating capture manifest...');
|
|
110
|
+
|
|
111
|
+
const manifestPath = validateFileExists(
|
|
112
|
+
'manifest.json',
|
|
113
|
+
'Run comprehensive-capture.js first to create manifest'
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
117
|
+
const errors = [];
|
|
118
|
+
|
|
119
|
+
if (!manifest.platform?.baseUrl) {
|
|
120
|
+
errors.push('Missing platform baseUrl in manifest');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!manifest.pages || manifest.pages.length === 0) {
|
|
124
|
+
errors.push('No pages captured in manifest');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Validate each page has screenshots
|
|
128
|
+
manifest.pages?.forEach(page => {
|
|
129
|
+
if (!page.captures || page.captures.length === 0) {
|
|
130
|
+
errors.push(`Page "${page.name}" has no captures`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (errors.length > 0) {
|
|
135
|
+
throw new ValidationError(
|
|
136
|
+
`Manifest incomplete:\n ${errors.join('\n ')}`,
|
|
137
|
+
'manifest'
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
log('success', `Manifest valid (${manifest.pages.length} pages, ${manifest.totalScreenshots || 'unknown'} screenshots)`);
|
|
142
|
+
return manifest;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function validateScreenshots(pageName = null) {
|
|
146
|
+
log('info', 'Validating screenshots...');
|
|
147
|
+
|
|
148
|
+
const screenshotDir = path.join(REFERENCES_DIR, 'screenshots');
|
|
149
|
+
if (!fs.existsSync(screenshotDir)) {
|
|
150
|
+
throw new ValidationError(
|
|
151
|
+
'Screenshots directory missing',
|
|
152
|
+
'screenshots'
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const screenshots = fs.readdirSync(screenshotDir).filter(f => f.endsWith('.png'));
|
|
157
|
+
|
|
158
|
+
if (screenshots.length === 0) {
|
|
159
|
+
throw new ValidationError(
|
|
160
|
+
'No screenshots found in references/screenshots/',
|
|
161
|
+
'screenshots'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// If specific page requested, check it exists
|
|
166
|
+
if (pageName) {
|
|
167
|
+
const pageScreenshot = screenshots.find(s =>
|
|
168
|
+
s.startsWith(pageName) || s.includes(pageName)
|
|
169
|
+
);
|
|
170
|
+
if (!pageScreenshot) {
|
|
171
|
+
throw new ValidationError(
|
|
172
|
+
`No screenshot found for page: ${pageName}\n Available: ${screenshots.join(', ')}`,
|
|
173
|
+
'screenshots'
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Check screenshot file sizes (should be > 10KB for valid captures)
|
|
179
|
+
const smallScreenshots = screenshots.filter(s => {
|
|
180
|
+
const stats = fs.statSync(path.join(screenshotDir, s));
|
|
181
|
+
return stats.size < 10000; // 10KB minimum
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
if (smallScreenshots.length > 0) {
|
|
185
|
+
log('warning', `Small screenshots detected (may be incomplete): ${smallScreenshots.join(', ')}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
log('success', `Screenshots valid (${screenshots.length} files)`);
|
|
189
|
+
return screenshots;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function validateComponentStyles() {
|
|
193
|
+
log('info', 'Validating component styles...');
|
|
194
|
+
|
|
195
|
+
const stylesPath = path.join(REFERENCES_DIR, 'component-styles.json');
|
|
196
|
+
|
|
197
|
+
if (!fs.existsSync(stylesPath)) {
|
|
198
|
+
log('warning', 'component-styles.json not found - component matching may be less accurate');
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const styles = JSON.parse(fs.readFileSync(stylesPath, 'utf8'));
|
|
203
|
+
log('success', 'Component styles found');
|
|
204
|
+
return styles;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function generateValidationReport(results) {
|
|
208
|
+
const report = {
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
passed: results.passed,
|
|
211
|
+
errors: results.errors,
|
|
212
|
+
warnings: results.warnings,
|
|
213
|
+
summary: {
|
|
214
|
+
totalColors: results.tokens?.totalColorsFound || 0,
|
|
215
|
+
primaryColor: results.tokens?.colors?.primary || 'NOT FOUND',
|
|
216
|
+
fontFamily: results.tokens?.fonts?.primary || 'NOT FOUND',
|
|
217
|
+
pagesCaptures: results.manifest?.pages?.length || 0,
|
|
218
|
+
totalScreenshots: results.screenshots?.length || 0
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const reportPath = path.join(REFERENCES_DIR, 'validation-report.json');
|
|
223
|
+
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
|
224
|
+
|
|
225
|
+
return report;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function runValidation(options = {}) {
|
|
229
|
+
console.log('\n' + '='.repeat(60));
|
|
230
|
+
console.log('🔍 PRE-GENERATION VALIDATION');
|
|
231
|
+
console.log('='.repeat(60) + '\n');
|
|
232
|
+
|
|
233
|
+
const results = {
|
|
234
|
+
passed: true,
|
|
235
|
+
errors: [],
|
|
236
|
+
warnings: [],
|
|
237
|
+
tokens: null,
|
|
238
|
+
manifest: null,
|
|
239
|
+
screenshots: null
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
try {
|
|
243
|
+
// 1. Validate design tokens (CRITICAL)
|
|
244
|
+
results.tokens = validateDesignTokens();
|
|
245
|
+
} catch (error) {
|
|
246
|
+
results.passed = false;
|
|
247
|
+
results.errors.push(error.message);
|
|
248
|
+
log('error', error.message);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
// 2. Validate manifest (CRITICAL)
|
|
253
|
+
results.manifest = validateManifest();
|
|
254
|
+
} catch (error) {
|
|
255
|
+
results.passed = false;
|
|
256
|
+
results.errors.push(error.message);
|
|
257
|
+
log('error', error.message);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// 3. Validate screenshots (CRITICAL)
|
|
262
|
+
results.screenshots = validateScreenshots(options.pageName);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
results.passed = false;
|
|
265
|
+
results.errors.push(error.message);
|
|
266
|
+
log('error', error.message);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
// 4. Validate component styles (OPTIONAL but recommended)
|
|
271
|
+
results.componentStyles = validateComponentStyles();
|
|
272
|
+
} catch (error) {
|
|
273
|
+
results.warnings.push(error.message);
|
|
274
|
+
log('warning', error.message);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Generate report
|
|
278
|
+
const report = generateValidationReport(results);
|
|
279
|
+
|
|
280
|
+
// Final summary
|
|
281
|
+
console.log('\n' + '='.repeat(60));
|
|
282
|
+
if (results.passed) {
|
|
283
|
+
console.log('✅ VALIDATION PASSED - Safe to proceed with generation');
|
|
284
|
+
console.log('='.repeat(60));
|
|
285
|
+
console.log('\nKey values for generation:');
|
|
286
|
+
console.log(` Primary color: ${report.summary.primaryColor}`);
|
|
287
|
+
console.log(` Font family: ${report.summary.fontFamily}`);
|
|
288
|
+
console.log(` Screenshots: ${report.summary.totalScreenshots}`);
|
|
289
|
+
} else {
|
|
290
|
+
console.log('❌ VALIDATION FAILED - DO NOT proceed with generation');
|
|
291
|
+
console.log('='.repeat(60));
|
|
292
|
+
console.log('\nErrors that must be fixed:');
|
|
293
|
+
results.errors.forEach((e, i) => console.log(` ${i + 1}. ${e}`));
|
|
294
|
+
console.log('\nRun comprehensive-capture.js to fix these issues.');
|
|
295
|
+
}
|
|
296
|
+
console.log('');
|
|
297
|
+
|
|
298
|
+
return results;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// CLI interface
|
|
302
|
+
if (require.main === module) {
|
|
303
|
+
const args = process.argv.slice(2);
|
|
304
|
+
const options = {};
|
|
305
|
+
|
|
306
|
+
// Parse arguments
|
|
307
|
+
for (let i = 0; i < args.length; i++) {
|
|
308
|
+
if (args[i] === '--page' && args[i + 1]) {
|
|
309
|
+
options.pageName = args[i + 1];
|
|
310
|
+
i++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
runValidation(options).then(results => {
|
|
315
|
+
process.exit(results.passed ? 0 : 1);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
module.exports = { runValidation };
|