#!/usr/bin/env node const fs = require('fs'); const path = require('path'); /** * Script pour détecter les doublons dans les fichiers de skills * Usage: node scripts/find-duplicates.js */ // Configuration const SKILLS_DIR = path.join(__dirname, '..', 'data', 'skills'); const OUTPUT_FILE = path.join(__dirname, '..', 'duplicates-report.txt'); // Couleurs pour la console const colors = { red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', reset: '\x1b[0m', bold: '\x1b[1m' }; function log(message, color = 'reset') { console.log(`${colors[color]}${message}${colors.reset}`); } function logHeader(message) { log(`\n${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}`); log(`${colors.bold}${colors.blue}${message}${colors.reset}`); log(`${colors.bold}${colors.blue}${'='.repeat(60)}${colors.reset}`); } function logSubHeader(message) { log(`\n${colors.bold}${colors.cyan}${message}${colors.reset}`); log(`${colors.cyan}${'-'.repeat(message.length)}${colors.reset}`); } /** * Lit et parse un fichier JSON de skills */ function parseSkillsFile(filePath) { try { const content = fs.readFileSync(filePath, 'utf8'); const data = JSON.parse(content); return { category: data.category, skills: data.skills || [] }; } catch (error) { log(`❌ Erreur lors de la lecture de ${filePath}: ${error.message}`, 'red'); return null; } } /** * Trouve tous les fichiers de skills */ function findSkillsFiles() { try { const files = fs.readdirSync(SKILLS_DIR); return files .filter(file => file.endsWith('.json')) .map(file => path.join(SKILLS_DIR, file)); } catch (error) { log(`❌ Erreur lors de la lecture du répertoire ${SKILLS_DIR}: ${error.message}`, 'red'); return []; } } /** * Analyse tous les fichiers et collecte les informations */ function analyzeSkills() { const files = findSkillsFiles(); const allSkills = []; const categoryStats = {}; logHeader('ANALYSE DES FICHIERS DE SKILLS'); log(`📁 Répertoire: ${SKILLS_DIR}`); log(`🔍 Fichiers trouvés: ${files.length}`); files.forEach(filePath => { const fileName = path.basename(filePath, '.json'); const data = parseSkillsFile(filePath); if (data) { const category = data.category; const skillsCount = data.skills.length; // Statistiques par catégorie categoryStats[category] = { file: fileName, count: skillsCount, skills: data.skills }; // Collecte de tous les skills avec métadonnées data.skills.forEach(skill => { allSkills.push({ id: skill.id, name: skill.name, category: category, file: fileName, description: skill.description?.substring(0, 100) + '...' || 'Pas de description' }); }); log(`✅ ${category}: ${skillsCount} skills (${fileName}.json)`, 'green'); } }); return { allSkills, categoryStats }; } /** * Détecte les doublons par ID */ function findDuplicateIds(allSkills) { const idCounts = {}; const duplicates = []; allSkills.forEach(skill => { if (!idCounts[skill.id]) { idCounts[skill.id] = []; } idCounts[skill.id].push(skill); }); Object.entries(idCounts).forEach(([id, skills]) => { if (skills.length > 1) { duplicates.push({ id, count: skills.length, occurrences: skills }); } }); return duplicates; } /** * Détecte les doublons par nom (avec tolérance pour les variations) */ function findDuplicateNames(allSkills) { const nameCounts = {}; const duplicates = []; allSkills.forEach(skill => { const normalizedName = skill.name.toLowerCase().trim(); if (!nameCounts[normalizedName]) { nameCounts[normalizedName] = []; } nameCounts[normalizedName].push(skill); }); Object.entries(nameCounts).forEach(([name, skills]) => { if (skills.length > 1) { duplicates.push({ name, count: skills.length, occurrences: skills }); } }); return duplicates; } /** * Génère le rapport */ function generateReport(allSkills, categoryStats, duplicateIds, duplicateNames) { const report = []; report.push('='.repeat(80)); report.push('RAPPORT DE DÉTECTION DE DOUBLONS - SKILLS'); report.push('='.repeat(80)); report.push(`Généré le: ${new Date().toLocaleString('fr-FR')}`); report.push(''); // Statistiques générales report.push('📊 STATISTIQUES GÉNÉRALES'); report.push('-'.repeat(30)); report.push(`Total de skills: ${allSkills.length}`); report.push(`Total de catégories: ${Object.keys(categoryStats).length}`); report.push(''); // Statistiques par catégorie report.push('📁 STATISTIQUES PAR CATÉGORIE'); report.push('-'.repeat(30)); Object.entries(categoryStats).forEach(([category, stats]) => { report.push(`${category}: ${stats.count} skills (${stats.file}.json)`); }); report.push(''); // Doublons par ID if (duplicateIds.length > 0) { report.push('🚨 DOUBLONS PAR ID'); report.push('-'.repeat(30)); duplicateIds.forEach(duplicate => { report.push(`ID: "${duplicate.id}" (${duplicate.count} occurrences)`); duplicate.occurrences.forEach(occurrence => { report.push(` - ${occurrence.category} (${occurrence.file}.json): ${occurrence.name}`); }); report.push(''); }); } else { report.push('✅ AUCUN DOUBLON PAR ID DÉTECTÉ'); report.push(''); } // Doublons par nom if (duplicateNames.length > 0) { report.push('⚠️ DOUBLONS PAR NOM (POTENTIELS)'); report.push('-'.repeat(40)); duplicateNames.forEach(duplicate => { report.push(`Nom: "${duplicate.name}" (${duplicate.count} occurrences)`); duplicate.occurrences.forEach(occurrence => { report.push(` - ${occurrence.category} (${occurrence.file}.json): ID="${occurrence.id}"`); }); report.push(''); }); } else { report.push('✅ AUCUN DOUBLON PAR NOM DÉTECTÉ'); report.push(''); } // Recommandations report.push('💡 RECOMMANDATIONS'); report.push('-'.repeat(20)); if (duplicateIds.length > 0) { report.push('• Supprimer les doublons par ID (priorité haute)'); report.push('• Garder la version dans la catégorie la plus appropriée'); } if (duplicateNames.length > 0) { report.push('• Vérifier les doublons par nom (peuvent être légitimes)'); report.push('• S\'assurer que les IDs sont uniques'); } if (duplicateIds.length === 0 && duplicateNames.length === 0) { report.push('• Aucune action requise - tous les skills sont uniques !'); } return report.join('\n'); } /** * Affiche le rapport dans la console */ function displayReport(report) { logHeader('RAPPORT COMPLET'); console.log(report); } /** * Sauvegarde le rapport dans un fichier */ function saveReport(report) { try { fs.writeFileSync(OUTPUT_FILE, report, 'utf8'); log(`\n💾 Rapport sauvegardé dans: ${OUTPUT_FILE}`, 'green'); } catch (error) { log(`❌ Erreur lors de la sauvegarde du rapport: ${error.message}`, 'red'); } } /** * Fonction principale */ function main() { try { logHeader('DÉTECTION DE DOUBLONS DANS LES SKILLS'); // Analyse des fichiers const { allSkills, categoryStats } = analyzeSkills(); if (allSkills.length === 0) { log('❌ Aucun skill trouvé. Vérifiez le répertoire.', 'red'); return; } // Détection des doublons logSubHeader('DÉTECTION DES DOUBLONS'); const duplicateIds = findDuplicateIds(allSkills); const duplicateNames = findDuplicateNames(allSkills); log(`🔍 Doublons par ID: ${duplicateIds.length}`, duplicateIds.length > 0 ? 'red' : 'green'); log(`🔍 Doublons par nom: ${duplicateNames.length}`, duplicateNames.length > 0 ? 'yellow' : 'green'); // Génération et affichage du rapport const report = generateReport(allSkills, categoryStats, duplicateIds, duplicateNames); displayReport(report); // Sauvegarde du rapport saveReport(report); // Résumé final logHeader('RÉSUMÉ'); if (duplicateIds.length === 0 && duplicateNames.length === 0) { log('🎉 Aucun doublon détecté ! Tous les skills sont uniques.', 'green'); } else { log(`⚠️ ${duplicateIds.length} doublons par ID et ${duplicateNames.length} doublons par nom détectés.`, 'yellow'); log('Consultez le rapport ci-dessus pour plus de détails.', 'yellow'); } } catch (error) { log(`❌ Erreur fatale: ${error.message}`, 'red'); process.exit(1); } } // Exécution du script if (require.main === module) { main(); } module.exports = { analyzeSkills, findDuplicateIds, findDuplicateNames, generateReport };