fix: double skills cleaning and script

This commit is contained in:
Julien Froidefond
2025-08-25 22:46:36 +02:00
parent e9232938ce
commit e12816a9c2
9 changed files with 754 additions and 136 deletions

412
scripts/clean-duplicates.js Normal file
View File

@@ -0,0 +1,412 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
/**
* Script pour nettoyer automatiquement les doublons dans les fichiers de skills
* Usage: node scripts/clean-duplicates.js
*/
// Configuration
const SKILLS_DIR = path.join(__dirname, '..', 'data', 'skills');
const BACKUP_DIR = path.join(__dirname, '..', 'backups', 'skills');
// 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}`);
}
/**
* Crée une sauvegarde des fichiers avant modification
*/
function createBackup() {
try {
if (!fs.existsSync(BACKUP_DIR)) {
fs.mkdirSync(BACKUP_DIR, { recursive: true });
}
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = path.join(BACKUP_DIR, `skills-backup-${timestamp}`);
if (!fs.existsSync(backupPath)) {
fs.mkdirSync(backupPath, { recursive: true });
}
const files = fs.readdirSync(SKILLS_DIR);
files.forEach(file => {
if (file.endsWith('.json')) {
const sourcePath = path.join(SKILLS_DIR, file);
const destPath = path.join(backupPath, file);
fs.copyFileSync(sourcePath, destPath);
}
});
log(`💾 Sauvegarde créée dans: ${backupPath}`, 'green');
return backupPath;
} catch (error) {
log(`❌ Erreur lors de la création de la sauvegarde: ${error.message}`, 'red');
return null;
}
}
/**
* Lit et parse un fichier JSON de skills
*/
function parseSkillsFile(filePath) {
try {
const content = fs.readFileSync(filePath, 'utf8');
return JSON.parse(content);
} catch (error) {
log(`❌ Erreur lors de la lecture de ${filePath}: ${error.message}`, 'red');
return null;
}
}
/**
* Sauvegarde un fichier JSON de skills
*/
function saveSkillsFile(filePath, data) {
try {
const content = JSON.stringify(data, null, 2);
fs.writeFileSync(filePath, content, 'utf8');
return true;
} catch (error) {
log(`❌ Erreur lors de la sauvegarde de ${filePath}: ${error.message}`, 'red');
return false;
}
}
/**
* 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 = {};
files.forEach(filePath => {
const fileName = path.basename(filePath, '.json');
const data = parseSkillsFile(filePath);
if (data) {
const category = data.category;
const skillsCount = data.skills.length;
categoryStats[category] = {
filePath,
fileName,
count: skillsCount,
data
};
data.skills.forEach(skill => {
allSkills.push({
id: skill.id,
name: skill.name,
category: category,
filePath,
fileName,
skill
});
});
}
});
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étermine la catégorie la plus appropriée pour un skill
*/
function determineBestCategory(occurrences) {
// Priorité des catégories (plus spécifique en premier)
const categoryPriority = {
'Testing': 1,
'Security': 2,
'Mobile': 3,
'Design': 4,
'Data': 5,
'Frontend': 6,
'Backend': 7,
'DevOps': 8,
'Culture': 9
};
let bestOccurrence = occurrences[0];
let bestPriority = categoryPriority[bestOccurrence.category] || 999;
occurrences.forEach(occurrence => {
const priority = categoryPriority[occurrence.category] || 999;
if (priority < bestPriority) {
bestPriority = priority;
bestOccurrence = occurrence;
}
});
return bestOccurrence;
}
/**
* Nettoie les doublons en gardant une version par catégorie
*/
function cleanDuplicates(duplicateIds, categoryStats) {
const cleanedFiles = new Set();
const removedSkills = [];
duplicateIds.forEach(duplicate => {
const { id, occurrences } = duplicate;
// Détermine la meilleure catégorie
const bestOccurrence = determineBestCategory(occurrences);
log(`🔧 Traitement du doublon "${id}" (${occurrences.length} occurrences)`, 'yellow');
log(` ✅ Gardé dans: ${bestOccurrence.category} (${bestOccurrence.fileName}.json)`, 'green');
// Supprime les doublons des autres catégories
occurrences.forEach(occurrence => {
if (occurrence.filePath !== bestOccurrence.filePath) {
const categoryData = categoryStats[occurrence.category];
if (categoryData) {
// Supprime le skill dupliqué
const skillIndex = categoryData.data.skills.findIndex(s => s.id === id);
if (skillIndex !== -1) {
const removedSkill = categoryData.data.skills.splice(skillIndex, 1)[0];
removedSkills.push({
id: removedSkill.id,
name: removedSkill.name,
from: occurrence.category,
file: occurrence.fileName
});
log(` 🗑️ Supprimé de: ${occurrence.category} (${occurrence.fileName}.json)`, 'red');
cleanedFiles.add(occurrence.filePath);
}
}
}
});
});
return { cleanedFiles, removedSkills };
}
/**
* Sauvegarde les fichiers modifiés
*/
function saveModifiedFiles(cleanedFiles, categoryStats) {
let savedCount = 0;
let errorCount = 0;
cleanedFiles.forEach(filePath => {
const fileName = path.basename(filePath, '.json');
const categoryData = Object.values(categoryStats).find(stat => stat.fileName === fileName);
if (categoryData && saveSkillsFile(filePath, categoryData.data)) {
savedCount++;
log(`💾 Fichier sauvegardé: ${fileName}.json`, 'green');
} else {
errorCount++;
log(`❌ Erreur lors de la sauvegarde: ${fileName}.json`, 'red');
}
});
return { savedCount, errorCount };
}
/**
* Génère un rapport de nettoyage
*/
function generateCleaningReport(duplicateIds, removedSkills, savedCount, errorCount) {
const report = [];
report.push('='.repeat(80));
report.push('RAPPORT DE NETTOYAGE DES DOUBLONS - SKILLS');
report.push('='.repeat(80));
report.push(`Généré le: ${new Date().toLocaleString('fr-FR')}`);
report.push('');
// Résumé des actions
report.push('📊 RÉSUMÉ DES ACTIONS');
report.push('-'.repeat(30));
report.push(`Doublons traités: ${duplicateIds.length}`);
report.push(`Skills supprimés: ${removedSkills.length}`);
report.push(`Fichiers modifiés: ${savedCount}`);
report.push(`Erreurs: ${errorCount}`);
report.push('');
// Détails des doublons traités
if (duplicateIds.length > 0) {
report.push('🔧 DOUBLONS TRAITÉS');
report.push('-'.repeat(30));
duplicateIds.forEach(duplicate => {
const bestOccurrence = determineBestCategory(duplicate.occurrences);
report.push(`ID: "${duplicate.id}"`);
report.push(` ✅ Gardé dans: ${bestOccurrence.category}`);
duplicate.occurrences.forEach(occurrence => {
if (occurrence.filePath !== bestOccurrence.filePath) {
report.push(` 🗑️ Supprimé de: ${occurrence.category}`);
}
});
report.push('');
});
}
// Détails des skills supprimés
if (removedSkills.length > 0) {
report.push('🗑️ SKILLS SUPPRIMÉS');
report.push('-'.repeat(30));
removedSkills.forEach(skill => {
report.push(`${skill.name} (ID: ${skill.id}) - Supprimé de ${skill.from} (${skill.file}.json)`);
});
report.push('');
}
return report.join('\n');
}
/**
* Fonction principale
*/
function main() {
try {
logHeader('NETTOYAGE AUTOMATIQUE DES DOUBLONS');
// Création de la sauvegarde
logSubHeader('CRÉATION DE LA SAUVEGARDE');
const backupPath = createBackup();
if (!backupPath) {
log('❌ Impossible de continuer sans sauvegarde', 'red');
return;
}
// Analyse des fichiers
logSubHeader('ANALYSE DES FICHIERS');
const { allSkills, categoryStats } = analyzeSkills();
if (allSkills.length === 0) {
log('❌ Aucun skill trouvé. Vérifiez le répertoire.', 'red');
return;
}
log(`📁 Fichiers analysés: ${Object.keys(categoryStats).length}`);
log(`🔍 Total de skills: ${allSkills.length}`);
// Détection des doublons
logSubHeader('DÉTECTION DES DOUBLONS');
const duplicateIds = findDuplicateIds(allSkills);
if (duplicateIds.length === 0) {
log('🎉 Aucun doublon détecté ! Aucune action requise.', 'green');
return;
}
log(`🚨 Doublons détectés: ${duplicateIds.length}`, 'red');
// Nettoyage des doublons
logSubHeader('NETTOYAGE DES DOUBLONS');
const { cleanedFiles, removedSkills } = cleanDuplicates(duplicateIds, categoryStats);
// Sauvegarde des fichiers modifiés
logSubHeader('SAUVEGARDE DES FICHIERS MODIFIÉS');
const { savedCount, errorCount } = saveModifiedFiles(cleanedFiles, categoryStats);
// Génération du rapport
const report = generateCleaningReport(duplicateIds, removedSkills, savedCount, errorCount);
// Sauvegarde du rapport
const reportPath = path.join(__dirname, '..', 'cleaning-report.txt');
fs.writeFileSync(reportPath, report, 'utf8');
// Résumé final
logHeader('RÉSUMÉ FINAL');
if (errorCount === 0) {
log('🎉 Nettoyage terminé avec succès !', 'green');
log(`📊 ${duplicateIds.length} doublons traités, ${removedSkills.length} skills supprimés`, 'green');
log(`💾 Rapport sauvegardé dans: ${reportPath}`, 'green');
} else {
log(`⚠️ Nettoyage terminé avec ${errorCount} erreur(s)`, 'yellow');
log('Consultez le rapport 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 = {
createBackup,
analyzeSkills,
findDuplicateIds,
cleanDuplicates,
generateCleaningReport
};