@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.
- package/README.md +68 -0
- package/index.js +36 -0
- package/package.json +40 -0
- package/schemas/persona_schema.yaml +295 -0
- package/schemas/personality.schema.json +228 -0
- package/scripts/add-personality.js +220 -0
- package/scripts/build.js +345 -0
- package/scripts/enhance-scientists.js +215 -0
- package/scripts/enhance_all_personalities.py +926 -0
- package/scripts/migrate.js +384 -0
- package/scripts/parallel-validate.js +322 -0
- package/scripts/split-personalities.js +95 -0
- package/scripts/validate.js +306 -0
- package/tools/unix_tools.yaml +349 -0
|
@@ -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 };
|