@hanzo/persona 1.0.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.
@@ -0,0 +1,384 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ // Configuration
7
+ const SOURCE_DIR = path.join(__dirname, '..', 'personalities');
8
+ const TARGET_DIR = path.join(__dirname, '..', 'personalities_v2');
9
+ const SCHEMA_PATH = path.join(__dirname, '..', 'schemas', 'personality.schema.json');
10
+
11
+ // Category mappings
12
+ const CATEGORY_DIRS = {
13
+ programmer: 'programmers',
14
+ philosopher: 'philosophers',
15
+ scientist: 'scientists',
16
+ religious: 'religious',
17
+ revolutionary: 'revolutionaries',
18
+ writer: 'writers',
19
+ artist: 'artists',
20
+ musician: 'artists',
21
+ filmmaker: 'artists',
22
+ comedian: 'artists',
23
+ architect: 'architects',
24
+ athlete: 'athletes',
25
+ explorer: 'explorers',
26
+ activist: 'activists',
27
+ tech_leader: 'leaders',
28
+ leader: 'leaders',
29
+ statesman: 'leaders',
30
+ pioneer: 'pioneers',
31
+ mathematician: 'scientists',
32
+ special: 'special',
33
+ systems: 'programmers',
34
+ master: 'programmers',
35
+ 'language-creator': 'programmers',
36
+ historian: 'writers',
37
+ gaming: 'programmers',
38
+ blockchain: 'programmers',
39
+ media: 'leaders',
40
+ poet: 'writers'
41
+ };
42
+
43
+ // Initialize directories
44
+ function initDirectories() {
45
+ // Create main directories
46
+ const dirs = [
47
+ TARGET_DIR,
48
+ path.join(TARGET_DIR, 'index'),
49
+ path.join(TARGET_DIR, 'schemas'),
50
+ path.join(TARGET_DIR, 'scripts'),
51
+ path.join(TARGET_DIR, 'dist'),
52
+ path.join(TARGET_DIR, 'dist', 'by-category'),
53
+ path.join(TARGET_DIR, 'dist', 'by-tag')
54
+ ];
55
+
56
+ // Create category directories
57
+ const uniqueDirs = [...new Set(Object.values(CATEGORY_DIRS))];
58
+ uniqueDirs.forEach(dir => {
59
+ dirs.push(path.join(TARGET_DIR, dir));
60
+ });
61
+
62
+ dirs.forEach(dir => {
63
+ if (!fs.existsSync(dir)) {
64
+ fs.mkdirSync(dir, { recursive: true });
65
+ console.log(`Created directory: ${dir}`);
66
+ }
67
+ });
68
+
69
+ // Copy schema
70
+ const schemaTarget = path.join(TARGET_DIR, 'schemas', 'personality.schema.json');
71
+ if (fs.existsSync(SCHEMA_PATH) && !fs.existsSync(schemaTarget)) {
72
+ fs.copyFileSync(SCHEMA_PATH, schemaTarget);
73
+ console.log('Copied schema file');
74
+ }
75
+ }
76
+
77
+ // Convert OCEAN scores to 0-100 scale if needed
78
+ function normalizeOcean(ocean) {
79
+ const normalized = {};
80
+ for (const [key, value] of Object.entries(ocean)) {
81
+ // If any value is greater than 100, assume it's on a different scale
82
+ normalized[key] = value > 100 ? Math.round(value / 10) : value;
83
+ }
84
+ return normalized;
85
+ }
86
+
87
+ // Generate ID from name
88
+ function generateId(name) {
89
+ return name
90
+ .toLowerCase()
91
+ .replace(/[^a-z0-9\s-]/g, '')
92
+ .replace(/\s+/g, '_')
93
+ .replace(/-+/g, '_')
94
+ .substring(0, 50);
95
+ }
96
+
97
+ // Convert old format to new format
98
+ function convertPersonality(oldData, source) {
99
+ const id = oldData.id || generateId(oldData.name);
100
+ const category = oldData.category || 'special';
101
+
102
+ // Build new format
103
+ const newFormat = {
104
+ "$schema": "../schemas/personality.schema.json",
105
+ id: id,
106
+ name: oldData.name || oldData.display_name,
107
+ category: category
108
+ };
109
+
110
+ // Add full name if available
111
+ if (oldData.fullName || oldData.programmer) {
112
+ newFormat.fullName = oldData.fullName || oldData.programmer;
113
+ }
114
+
115
+ // Add subcategory if makes sense
116
+ if (oldData.subcategory) {
117
+ newFormat.subcategory = oldData.subcategory;
118
+ }
119
+
120
+ // Generate tags
121
+ const tags = [];
122
+ if (oldData.tags) {
123
+ tags.push(...oldData.tags);
124
+ } else {
125
+ // Auto-generate some tags
126
+ if (category) tags.push(category);
127
+ if (oldData.primary_tech) tags.push(...oldData.primary_tech.map(t => t.toLowerCase()));
128
+ if (oldData.role) tags.push(...oldData.role.toLowerCase().split(/[&,\s]+/).filter(t => t.length > 2));
129
+ }
130
+ if (tags.length > 0) {
131
+ newFormat.tags = [...new Set(tags)];
132
+ }
133
+
134
+ // Add metadata
135
+ const metadata = {};
136
+ if (oldData.born || oldData.lived) metadata.born = oldData.born || oldData.lived;
137
+ if (oldData.died) metadata.died = oldData.died;
138
+ if (oldData.nationality) metadata.nationality = oldData.nationality;
139
+ if (oldData.company) metadata.company = oldData.company;
140
+ if (oldData.achievements) metadata.achievements = oldData.achievements;
141
+ if (oldData.active !== undefined) metadata.active = oldData.active;
142
+
143
+ if (Object.keys(metadata).length > 0) {
144
+ newFormat.metadata = metadata;
145
+ }
146
+
147
+ // OCEAN scores (normalize to 0-100)
148
+ if (oldData.ocean) {
149
+ newFormat.ocean = normalizeOcean(oldData.ocean);
150
+ } else if (oldData.personality && typeof oldData.personality === 'object' && oldData.personality.openness) {
151
+ newFormat.ocean = normalizeOcean(oldData.personality);
152
+ } else {
153
+ // Default OCEAN scores
154
+ newFormat.ocean = {
155
+ openness: 70,
156
+ conscientiousness: 70,
157
+ extraversion: 50,
158
+ agreeableness: 60,
159
+ neuroticism: 40
160
+ };
161
+ }
162
+
163
+ // Personality info
164
+ const personality = {
165
+ summary: oldData.description || oldData.summary || `${category} personality`,
166
+ philosophy: oldData.philosophy || oldData.quote || "No philosophy recorded"
167
+ };
168
+
169
+ if (oldData.approach) personality.approach = oldData.approach;
170
+ if (oldData.communication) personality.communication = oldData.communication;
171
+ if (oldData.style?.communication) personality.communication = oldData.style.communication;
172
+ if (oldData.values) personality.values = oldData.values;
173
+ if (oldData.principles) personality.values = oldData.principles;
174
+
175
+ newFormat.personality = personality;
176
+
177
+ // Technical info (for programmers)
178
+ if (category === 'programmer' || category === 'tech_leader' || oldData.primary_tech) {
179
+ const technical = {};
180
+
181
+ if (oldData.languages || oldData.primary_tech) {
182
+ technical.languages = oldData.languages || oldData.primary_tech;
183
+ }
184
+
185
+ if (oldData.domains || oldData.tools?.domains) {
186
+ technical.domains = oldData.domains || oldData.tools.domains;
187
+ }
188
+
189
+ if (oldData.tools) {
190
+ technical.tools = {};
191
+ if (oldData.tools.essential) technical.tools.essential = oldData.tools.essential;
192
+ if (oldData.tools.preferred) technical.tools.preferred = oldData.tools.preferred;
193
+ if (oldData.tools.created) technical.tools.created = oldData.tools.created;
194
+ if (oldData.style?.tools) technical.tools.preferred = oldData.style.tools;
195
+ }
196
+
197
+ if (Object.keys(technical).length > 0) {
198
+ newFormat.technical = technical;
199
+ }
200
+ }
201
+
202
+ // Quotes
203
+ if (oldData.quotes) {
204
+ newFormat.quotes = oldData.quotes;
205
+ } else if (oldData.quote) {
206
+ newFormat.quotes = [oldData.quote];
207
+ }
208
+
209
+ // Behavioral traits
210
+ if (oldData.behavioral || oldData.contribution_style || oldData.style) {
211
+ const behavioral = {};
212
+
213
+ if (oldData.behavioral?.codeStyle || oldData.style?.code) {
214
+ behavioral.codeStyle = oldData.behavioral?.codeStyle || oldData.style.code;
215
+ }
216
+ if (oldData.behavioral?.reviewStyle || oldData.contribution_style?.review) {
217
+ behavioral.reviewStyle = oldData.behavioral?.reviewStyle || oldData.contribution_style.review;
218
+ }
219
+ if (oldData.behavioral?.workStyle || oldData.style?.approach) {
220
+ behavioral.workStyle = oldData.behavioral?.workStyle || oldData.style.approach;
221
+ }
222
+ if (oldData.behavioral?.collaboration) {
223
+ behavioral.collaboration = oldData.behavioral.collaboration;
224
+ }
225
+
226
+ if (Object.keys(behavioral).length > 0) {
227
+ newFormat.behavioral = behavioral;
228
+ }
229
+ }
230
+
231
+ return newFormat;
232
+ }
233
+
234
+ // Process a single JSON file
235
+ function processFile(filePath) {
236
+ console.log(`\nProcessing: ${path.basename(filePath)}`);
237
+
238
+ try {
239
+ const content = fs.readFileSync(filePath, 'utf8');
240
+ const data = JSON.parse(content);
241
+
242
+ let personalities = [];
243
+
244
+ // Handle different formats
245
+ if (data.personalities && Array.isArray(data.personalities)) {
246
+ personalities = data.personalities;
247
+ } else if (data.personas && Array.isArray(data.personas)) {
248
+ personalities = data.personas;
249
+ } else if (data.name) {
250
+ // Single personality file
251
+ personalities = [data];
252
+ } else {
253
+ console.log(` Skipping - unknown format`);
254
+ return 0;
255
+ }
256
+
257
+ let count = 0;
258
+ personalities.forEach(person => {
259
+ try {
260
+ const converted = convertPersonality(person, path.basename(filePath));
261
+ const targetDir = CATEGORY_DIRS[converted.category] || 'special';
262
+ const targetPath = path.join(TARGET_DIR, targetDir, `${converted.id}.json`);
263
+
264
+ // Check for duplicates
265
+ if (fs.existsSync(targetPath)) {
266
+ console.log(` Skipping duplicate: ${converted.id}`);
267
+ return;
268
+ }
269
+
270
+ // Write the file
271
+ fs.writeFileSync(targetPath, JSON.stringify(converted, null, 2));
272
+ console.log(` Created: ${targetDir}/${converted.id}.json`);
273
+ count++;
274
+ } catch (err) {
275
+ console.error(` Error converting ${person.name || 'unknown'}: ${err.message}`);
276
+ }
277
+ });
278
+
279
+ return count;
280
+ } catch (err) {
281
+ console.error(` Error reading file: ${err.message}`);
282
+ return 0;
283
+ }
284
+ }
285
+
286
+ // Generate manifest
287
+ function generateManifest() {
288
+ console.log('\nGenerating manifest...');
289
+
290
+ const manifest = {
291
+ version: "2.0.0",
292
+ generated: new Date().toISOString(),
293
+ total: 0,
294
+ categories: {},
295
+ index: {}
296
+ };
297
+
298
+ // Scan all category directories
299
+ const categoryDirs = [...new Set(Object.values(CATEGORY_DIRS))];
300
+
301
+ categoryDirs.forEach(dir => {
302
+ const dirPath = path.join(TARGET_DIR, dir);
303
+ if (!fs.existsSync(dirPath)) return;
304
+
305
+ const files = fs.readdirSync(dirPath).filter(f => f.endsWith('.json'));
306
+ if (files.length === 0) return;
307
+
308
+ const categoryName = Object.keys(CATEGORY_DIRS).find(key => CATEGORY_DIRS[key] === dir) || dir;
309
+ const ids = [];
310
+
311
+ files.forEach(file => {
312
+ const filePath = path.join(dirPath, file);
313
+ try {
314
+ const content = JSON.parse(fs.readFileSync(filePath, 'utf8'));
315
+ ids.push(content.id);
316
+
317
+ manifest.index[content.id] = {
318
+ path: path.relative(TARGET_DIR, filePath),
319
+ name: content.name,
320
+ category: content.category,
321
+ tags: content.tags || []
322
+ };
323
+
324
+ manifest.total++;
325
+ } catch (err) {
326
+ console.error(`Error reading ${file}: ${err.message}`);
327
+ }
328
+ });
329
+
330
+ manifest.categories[categoryName] = {
331
+ count: ids.length,
332
+ path: dir,
333
+ ids: ids
334
+ };
335
+ });
336
+
337
+ // Write manifest
338
+ const manifestPath = path.join(TARGET_DIR, 'index', 'manifest.json');
339
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
340
+ console.log(`Generated manifest with ${manifest.total} personalities`);
341
+
342
+ return manifest;
343
+ }
344
+
345
+ // Main migration function
346
+ async function migrate() {
347
+ console.log('Starting migration...\n');
348
+
349
+ // Initialize directory structure
350
+ initDirectories();
351
+
352
+ // Process all JSON files
353
+ const files = fs.readdirSync(SOURCE_DIR).filter(f => f.endsWith('.json'));
354
+ let totalConverted = 0;
355
+
356
+ files.forEach(file => {
357
+ const filePath = path.join(SOURCE_DIR, file);
358
+ totalConverted += processFile(filePath);
359
+ });
360
+
361
+ console.log(`\nTotal personalities converted: ${totalConverted}`);
362
+
363
+ // Generate manifest
364
+ const manifest = generateManifest();
365
+
366
+ // Generate category summary
367
+ console.log('\nCategory Summary:');
368
+ Object.entries(manifest.categories).forEach(([cat, info]) => {
369
+ console.log(` ${cat}: ${info.count} personalities`);
370
+ });
371
+
372
+ console.log('\nMigration complete!');
373
+ console.log(`Output directory: ${TARGET_DIR}`);
374
+ }
375
+
376
+ // Run migration
377
+ if (require.main === module) {
378
+ migrate().catch(err => {
379
+ console.error('Migration failed:', err);
380
+ process.exit(1);
381
+ });
382
+ }
383
+
384
+ module.exports = { migrate, convertPersonality };
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import os from 'os';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ const PROFILES_DIR = path.join(__dirname, '../profiles');
12
+ const NUM_WORKERS = os.cpus().length;
13
+
14
+ // Validation checks
15
+ const ValidationChecks = {
16
+ // Structure checks
17
+ hasRequiredFields: (data) => {
18
+ const required = ['id', 'name', 'category', 'ocean'];
19
+ const missing = required.filter(f => !data[f]);
20
+ return missing.length === 0 ? null : `Missing fields: ${missing.join(', ')}`;
21
+ },
22
+
23
+ // OCEAN validation
24
+ oceanScoresValid: (data) => {
25
+ if (!data.ocean) return 'Missing OCEAN scores';
26
+
27
+ const fields = ['openness', 'conscientiousness', 'extraversion', 'agreeableness', 'neuroticism'];
28
+ const errors = [];
29
+
30
+ fields.forEach(field => {
31
+ const value = data.ocean[field];
32
+ if (value === undefined) {
33
+ errors.push(`Missing ${field}`);
34
+ } else if (typeof value !== 'number') {
35
+ errors.push(`${field} not a number`);
36
+ } else if (value < 0 || value > 100) {
37
+ errors.push(`${field}=${value} out of range [0-100]`);
38
+ }
39
+ });
40
+
41
+ const total = fields.reduce((sum, f) => sum + (data.ocean[f] || 0), 0);
42
+ if (total === 0) errors.push('All OCEAN scores are 0');
43
+ if (total === 500) errors.push('All OCEAN scores are 100');
44
+
45
+ return errors.length > 0 ? errors.join(', ') : null;
46
+ },
47
+
48
+ // ID format check
49
+ idFormat: (data) => {
50
+ if (!data.id) return 'Missing ID';
51
+ if (!/^[a-z0-9_-]+$/.test(data.id)) {
52
+ return `Invalid ID format: ${data.id} (must be lowercase alphanumeric with - or _)`;
53
+ }
54
+ return null;
55
+ },
56
+
57
+ // Category validation
58
+ categoryValid: (data) => {
59
+ const validCategories = [
60
+ 'philosopher', 'scientist', 'artist', 'musician', 'writer', 'poet',
61
+ 'programmer', 'architect', 'revolutionary', 'activist', 'religious',
62
+ 'explorer', 'filmmaker', 'comedian', 'athlete', 'tech_leader',
63
+ 'leader', 'statesman', 'mathematician', 'composer', 'historian',
64
+ 'pioneer', 'special'
65
+ ];
66
+
67
+ if (!data.category) return 'Missing category';
68
+ if (!validCategories.includes(data.category)) {
69
+ return `Invalid category: ${data.category}`;
70
+ }
71
+ return null;
72
+ },
73
+
74
+ // Enhanced fields for specific categories
75
+ categorySpecificFields: (data) => {
76
+ const warnings = [];
77
+
78
+ if (data.category === 'programmer' || data.category === 'tech_leader') {
79
+ if (!data.tools) warnings.push('Programmer without tools defined');
80
+ if (!data.programmer) warnings.push('Missing programmer username');
81
+ }
82
+
83
+ if (data.category === 'scientist') {
84
+ if (!data.contributions) warnings.push('Scientist without contributions');
85
+ if (!data.philosophy) warnings.push('Scientist without philosophy');
86
+ }
87
+
88
+ if (data.category === 'philosopher') {
89
+ if (!data.philosophy) warnings.push('Philosopher without philosophy');
90
+ }
91
+
92
+ return warnings.length > 0 ? warnings.join(', ') : null;
93
+ },
94
+
95
+ // Check for suspicious patterns
96
+ detectAnomalies: (data) => {
97
+ const anomalies = [];
98
+
99
+ // Check for extreme OCEAN patterns
100
+ if (data.ocean) {
101
+ const values = Object.values(data.ocean);
102
+ const avg = values.reduce((a, b) => a + b, 0) / values.length;
103
+ const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
104
+
105
+ if (variance < 25) {
106
+ anomalies.push('OCEAN scores too similar (low variance)');
107
+ }
108
+
109
+ // Check for contradictory scores
110
+ if (data.ocean.extraversion > 80 && data.ocean.agreeableness < 20) {
111
+ anomalies.push('High extraversion with very low agreeableness is unusual');
112
+ }
113
+
114
+ if (data.ocean.conscientiousness > 90 && data.ocean.openness < 10) {
115
+ anomalies.push('Very high conscientiousness with very low openness is unusual');
116
+ }
117
+ }
118
+
119
+ return anomalies.length > 0 ? anomalies.join(', ') : null;
120
+ },
121
+
122
+ // Filename consistency
123
+ filenameMatch: (data, filename) => {
124
+ const expectedFilename = `${data.id}.json`;
125
+ if (filename !== expectedFilename) {
126
+ return `Filename mismatch: expected ${expectedFilename}, got ${filename}`;
127
+ }
128
+ return null;
129
+ }
130
+ };
131
+
132
+ // Worker function to validate a batch of files
133
+ function validateBatch(files) {
134
+ const results = [];
135
+
136
+ files.forEach(filename => {
137
+ const filepath = path.join(PROFILES_DIR, filename);
138
+ const errors = [];
139
+ const warnings = [];
140
+
141
+ try {
142
+ const content = fs.readFileSync(filepath, 'utf8');
143
+ const data = JSON.parse(content);
144
+
145
+ // Run all validation checks
146
+ for (const [checkName, checkFn] of Object.entries(ValidationChecks)) {
147
+ const result = checkName === 'filenameMatch'
148
+ ? checkFn(data, filename)
149
+ : checkFn(data);
150
+
151
+ if (result) {
152
+ if (checkName === 'categorySpecificFields' || checkName === 'detectAnomalies') {
153
+ warnings.push(result);
154
+ } else {
155
+ errors.push(result);
156
+ }
157
+ }
158
+ }
159
+
160
+ results.push({
161
+ file: filename,
162
+ id: data.id,
163
+ name: data.name,
164
+ category: data.category,
165
+ errors,
166
+ warnings,
167
+ valid: errors.length === 0
168
+ });
169
+
170
+ } catch (err) {
171
+ results.push({
172
+ file: filename,
173
+ errors: [`Parse error: ${err.message}`],
174
+ warnings: [],
175
+ valid: false
176
+ });
177
+ }
178
+ });
179
+
180
+ return results;
181
+ }
182
+
183
+ // Main parallel validation
184
+ async function parallelValidate() {
185
+ console.log('šŸ” Running parallel validation on all personality profiles...\n');
186
+
187
+ // Get all JSON files
188
+ const files = fs.readdirSync(PROFILES_DIR)
189
+ .filter(f => f.endsWith('.json') && f !== 'index.json' && f !== 'categories.json');
190
+
191
+ console.log(`šŸ“ Found ${files.length} profiles to validate`);
192
+ console.log(`šŸ”§ Using ${NUM_WORKERS} parallel workers\n`);
193
+
194
+ // Split files into batches
195
+ const batchSize = Math.ceil(files.length / NUM_WORKERS);
196
+ const batches = [];
197
+ for (let i = 0; i < files.length; i += batchSize) {
198
+ batches.push(files.slice(i, i + batchSize));
199
+ }
200
+
201
+ // Process batches in parallel
202
+ const startTime = Date.now();
203
+ const allResults = [];
204
+
205
+ for (let i = 0; i < batches.length; i++) {
206
+ const results = validateBatch(batches[i]);
207
+ allResults.push(...results);
208
+ process.stdout.write(`āœ“ Batch ${i + 1}/${batches.length} complete\n`);
209
+ }
210
+
211
+ const duration = Date.now() - startTime;
212
+
213
+ // Analyze results
214
+ const validFiles = allResults.filter(r => r.valid);
215
+ const filesWithErrors = allResults.filter(r => !r.valid);
216
+ const filesWithWarnings = allResults.filter(r => r.warnings.length > 0);
217
+
218
+ // Group by category
219
+ const byCategory = {};
220
+ allResults.forEach(r => {
221
+ if (!byCategory[r.category]) {
222
+ byCategory[r.category] = { total: 0, valid: 0, errors: 0 };
223
+ }
224
+ byCategory[r.category].total++;
225
+ if (r.valid) byCategory[r.category].valid++;
226
+ else byCategory[r.category].errors++;
227
+ });
228
+
229
+ // Check for duplicate IDs
230
+ const idCounts = {};
231
+ allResults.forEach(r => {
232
+ if (r.id) {
233
+ idCounts[r.id] = (idCounts[r.id] || 0) + 1;
234
+ }
235
+ });
236
+ const duplicates = Object.entries(idCounts).filter(([_, count]) => count > 1);
237
+
238
+ // Print results
239
+ console.log('\n' + '='.repeat(60));
240
+ console.log('šŸ“Š VALIDATION RESULTS');
241
+ console.log('='.repeat(60));
242
+
243
+ console.log(`\nā±ļø Completed in ${duration}ms`);
244
+ console.log(`āœ… Valid files: ${validFiles.length}/${files.length} (${(validFiles.length/files.length*100).toFixed(1)}%)`);
245
+ console.log(`āŒ Files with errors: ${filesWithErrors.length}`);
246
+ console.log(`āš ļø Files with warnings: ${filesWithWarnings.length}`);
247
+
248
+ if (duplicates.length > 0) {
249
+ console.log(`\nšŸ”“ DUPLICATE IDS FOUND:`);
250
+ duplicates.forEach(([id, count]) => {
251
+ console.log(` - "${id}" appears ${count} times`);
252
+ });
253
+ }
254
+
255
+ console.log(`\nšŸ“ By Category:`);
256
+ Object.entries(byCategory)
257
+ .sort(([a], [b]) => a.localeCompare(b))
258
+ .forEach(([cat, stats]) => {
259
+ const pct = (stats.valid/stats.total*100).toFixed(0);
260
+ const status = stats.errors === 0 ? 'āœ…' : 'āš ļø';
261
+ console.log(` ${status} ${cat.padEnd(15)} ${stats.valid}/${stats.total} (${pct}%)`);
262
+ });
263
+
264
+ if (filesWithErrors.length > 0) {
265
+ console.log(`\nāŒ FILES WITH ERRORS:`);
266
+ filesWithErrors.slice(0, 10).forEach(r => {
267
+ console.log(`\n ${r.file}:`);
268
+ r.errors.forEach(err => console.log(` - ${err}`));
269
+ });
270
+ if (filesWithErrors.length > 10) {
271
+ console.log(`\n ... and ${filesWithErrors.length - 10} more files with errors`);
272
+ }
273
+ }
274
+
275
+ if (filesWithWarnings.length > 0 && process.argv.includes('--warnings')) {
276
+ console.log(`\nāš ļø FILES WITH WARNINGS:`);
277
+ filesWithWarnings.slice(0, 10).forEach(r => {
278
+ console.log(`\n ${r.file}:`);
279
+ r.warnings.forEach(warn => console.log(` - ${warn}`));
280
+ });
281
+ }
282
+
283
+ // Write detailed report
284
+ const report = {
285
+ timestamp: new Date().toISOString(),
286
+ duration: `${duration}ms`,
287
+ summary: {
288
+ total: files.length,
289
+ valid: validFiles.length,
290
+ errors: filesWithErrors.length,
291
+ warnings: filesWithWarnings.length,
292
+ duplicates: duplicates.length
293
+ },
294
+ byCategory,
295
+ errors: filesWithErrors.map(r => ({ file: r.file, errors: r.errors })),
296
+ warnings: filesWithWarnings.map(r => ({ file: r.file, warnings: r.warnings })),
297
+ duplicates
298
+ };
299
+
300
+ fs.writeFileSync(
301
+ path.join(__dirname, '../validation-report.json'),
302
+ JSON.stringify(report, null, 2)
303
+ );
304
+
305
+ console.log(`\nšŸ“„ Detailed report saved to validation-report.json`);
306
+
307
+ // Exit with error code if validation failed
308
+ if (filesWithErrors.length > 0 || duplicates.length > 0) {
309
+ console.log('\nāŒ Validation failed!');
310
+ process.exit(1);
311
+ } else {
312
+ console.log('\nāœ… All validations passed!');
313
+ }
314
+ }
315
+
316
+ // Run validation
317
+ parallelValidate().catch(err => {
318
+ console.error('Validation failed:', err);
319
+ process.exit(1);
320
+ });
321
+
322
+ export { parallelValidate, ValidationChecks };