#!/usr/bin/env node /** * Script pour générer les données de visualisation équipe/technologies * Génère un fichier JSON pour les visualisations Cytoscape.js et ECharts */ const fs = require('fs'); const path = require('path'); // Extraire une valeur YAML function extractYaml(yamlContent, key) { const regex = new RegExp(`^${key}:\\s*["']?([^"'\n]+)["']?`, 'm'); const match = yamlContent.match(regex); return match ? match[1].trim() : null; } // Extraire les compétences depuis le YAML (avec details) function extractSkills(yamlContent) { const skills = []; const lines = yamlContent.split('\n'); let inSkillsSection = false; let currentSkill = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.match(/^skills:\s*$/)) { inSkillsSection = true; continue; } if (inSkillsSection && line.match(/^\w+:/) && !line.match(/^\s+/)) { if (currentSkill) skills.push(currentSkill); inSkillsSection = false; continue; } if (inSkillsSection) { const nameMatch = line.match(/^\s+-\s+name:\s*["']?([^"'\n]+)["']?/); if (nameMatch) { if (currentSkill) skills.push(currentSkill); currentSkill = { name: nameMatch[1], level: 'beginner', years: 0 }; } const levelMatch = line.match(/^\s+level:\s*["']?([^"'\n]+)["']?/); if (levelMatch && currentSkill) currentSkill.level = levelMatch[1]; const yearsMatch = line.match(/^\s+years:\s*(\d+)/); if (yearsMatch && currentSkill) currentSkill.years = parseInt(yearsMatch[1]); } } if (currentSkill) skills.push(currentSkill); return skills; } // Extraire une liste YAML simple (ex: interests, softSkills, projects) function extractYamlList(yamlContent, key) { const items = []; const lines = yamlContent.split('\n'); let inSection = false; for (let i = 0; i < lines.length; i++) { const line = lines[i]; // Debut de section if (line.match(new RegExp(`^${key}:\\s*$`))) { inSection = true; continue; } // Fin de section (nouvelle cle au niveau racine) if (inSection && line.match(/^\w+:/) && !line.match(/^\s+/)) { inSection = false; continue; } // Extraction des items if (inSection) { const itemMatch = line.match(/^\s+-\s*["']?([^"'\n]+)["']?/); if (itemMatch) { items.push(itemMatch[1].trim()); } } } return items; } // Extraire la bio (contenu apres le front matter YAML) function extractBio(content) { const parts = content.split(/^---\n[\s\S]*?\n---/m); if (parts.length > 1) { return parts[1].trim().replace(/\n/g, ' ').substring(0, 500); } return ''; } // Charger les technologies depuis les blips function loadTechnologies() { const techDir = path.join(__dirname, '../radar-business/2025-01-15'); const technologies = []; if (!fs.existsSync(techDir)) { console.warn(`⚠️ Dossier ${techDir} introuvable`); return []; } const files = fs.readdirSync(techDir).filter(f => f.endsWith('.md')); for (const file of files) { const filePath = path.join(techDir, file); const content = fs.readFileSync(filePath, 'utf-8'); const yamlMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!yamlMatch) continue; const yaml = yamlMatch[1]; // Extraire les membres depuis la section "Compétences" let members = []; const compSection = content.match(/## Compétences\s*\n([\s\S]*?)(?=\n## |$)/); if (compSection) { const membersMatch = compSection[1].match(/Membres de l'équipe\s*:\s*([^\n]+)/); if (membersMatch) { members = membersMatch[1].split(',').map(m => m.trim()).filter(m => m && m !== 'Aucun'); } } const tech = { id: file.replace('.md', ''), title: extractYaml(yaml, 'title') || file.replace('.md', ''), ring: extractYaml(yaml, 'ring') || 'support', quadrant: extractYaml(yaml, 'quadrant') || 'technologies-commodite', businessImpact: extractYaml(yaml, 'businessImpact') || 'medium', teamCoverage: parseInt(extractYaml(yaml, 'teamCoverage') || '0'), skillGap: extractYaml(yaml, 'skillGap') || 'medium', competencyLevel: extractYaml(yaml, 'competencyLevel') || 'beginner', members: members }; technologies.push(tech); } return technologies; } // Charger les membres de l'équipe (avec profils complets) function loadTeamMembers() { const teamDir = path.join(__dirname, '../data/team'); const members = []; if (!fs.existsSync(teamDir)) { console.warn(`⚠️ Dossier ${teamDir} introuvable`); return []; } const files = fs.readdirSync(teamDir).filter(f => f.endsWith('.md')); for (const file of files) { const filePath = path.join(teamDir, file); const content = fs.readFileSync(filePath, 'utf-8'); const yamlMatch = content.match(/^---\n([\s\S]*?)\n---/); if (!yamlMatch) continue; const yaml = yamlMatch[1]; const skillsData = extractSkills(yaml); const member = { id: extractYaml(yaml, 'name') || file.replace('.md', ''), fullName: extractYaml(yaml, 'fullName') || extractYaml(yaml, 'name'), role: extractYaml(yaml, 'role') || '', availability: parseInt(extractYaml(yaml, 'availability') || '0'), seniorityLevel: extractYaml(yaml, 'seniorityLevel') || 'beginner', yearsExperience: parseInt(extractYaml(yaml, 'yearsExperience') || '0'), joinDate: extractYaml(yaml, 'joinDate') || '', skills: skillsData.map(s => typeof s === 'string' ? s : s.name), skillsDetailed: skillsData, interests: extractYamlList(yaml, 'interests'), softSkills: extractYamlList(yaml, 'softSkills'), projects: extractYamlList(yaml, 'projects'), bio: extractBio(content) }; members.push(member); } return members; } // Générer les données pour le graphe réseau (Cytoscape.js) function generateNetworkData(technologies, members) { const nodes = []; const edges = []; // Nœuds technologies technologies.forEach(tech => { const ringColor = { 'core': '#ff4444', 'strategic': '#ff8800', 'support': '#4488ff', 'legacy': '#888888' }[tech.ring] || '#999999'; nodes.push({ data: { id: `tech-${tech.id}`, label: tech.title, type: 'technology', ring: tech.ring, quadrant: tech.quadrant, coverage: tech.teamCoverage, businessImpact: tech.businessImpact, skillGap: tech.skillGap, color: ringColor } }); }); // Nœuds membres members.forEach(member => { nodes.push({ data: { id: `member-${member.id}`, label: member.fullName || member.id, type: 'member', availability: member.availability, seniority: member.seniorityLevel, role: member.role } }); }); // Liens technologies ↔ membres technologies.forEach(tech => { tech.members.forEach(memberName => { const member = members.find(m => { const mId = m.id.toLowerCase(); const mFullName = (m.fullName || '').toLowerCase(); const memberNameLower = memberName.toLowerCase(); return mId === memberNameLower || mFullName === memberNameLower; }); if (member) { // Vérifier si le membre a vraiment cette compétence const hasSkill = member.skills.some(skill => { const skillLower = skill.toLowerCase(); const techTitleLower = tech.title.toLowerCase(); return skillLower.includes(techTitleLower) || techTitleLower.includes(skillLower) || tech.title.toLowerCase().split(/[\s\/-]/).some(word => skillLower.includes(word) || word.length > 3 && skillLower.includes(word) ); }); if (hasSkill || tech.members.includes(member.id) || tech.members.includes(member.fullName)) { edges.push({ data: { id: `edge-${tech.id}-${member.id}`, source: `tech-${tech.id}`, target: `member-${member.id}`, weight: hasSkill ? 1 : 0.5 } }); } } }); }); return { nodes, edges }; } // Générer la matrice de congestion function generateCongestionMatrix(technologies, members) { // Filtrer technologies adopt uniquement (anciennement "core") const coreTechs = technologies.filter(t => t.ring === 'adopt').sort((a, b) => { // Trier par businessImpact puis par coverage const impactOrder = { 'high': 3, 'medium': 2, 'low': 1 }; const impactDiff = (impactOrder[b.businessImpact] || 0) - (impactOrder[a.businessImpact] || 0); if (impactDiff !== 0) return impactDiff; return a.teamCoverage - b.teamCoverage; // Moins de couverture = plus critique }); // Filtrer membres avec disponibilité >= 50% const availableMembers = members.filter(m => m.availability >= 50) .sort((a, b) => b.availability - a.availability); const matrix = []; coreTechs.forEach(tech => { const row = { technology: tech.title, technologyId: tech.id, businessImpact: tech.businessImpact, coverage: tech.teamCoverage, skillGap: tech.skillGap, members: [] }; availableMembers.forEach(member => { const hasSkill = tech.members.some(m => { const mId = m.toLowerCase(); return mId === member.id.toLowerCase() || mId === (member.fullName || '').toLowerCase(); }); row.members.push({ member: member.id, fullName: member.fullName, hasSkill: hasSkill, availability: member.availability, seniority: member.seniorityLevel }); }); matrix.push(row); }); return matrix; } // Générer l'équipe de genèse MVP function generateGenesisTeam(technologies, members) { // Technologies adopt (anciennement "core") - technologies fondamentales en production const coreTechs = technologies.filter(t => t.ring === 'adopt'); const availableMembers = members.filter(m => m.availability >= 50); const genesisTeam = []; const coveredTechs = new Set(); const memberTechMap = new Map(); // Pour chaque membre, lister les technos core qu'il maîtrise availableMembers.forEach(member => { const memberTechs = coreTechs.filter(tech => { return tech.members.some(m => { const mId = m.toLowerCase(); return mId === member.id.toLowerCase() || mId === (member.fullName || '').toLowerCase(); }); }); if (memberTechs.length > 0) { memberTechMap.set(member.id, { member: member, technologies: memberTechs, coverage: memberTechs.length }); memberTechs.forEach(t => coveredTechs.add(t.id)); } }); // Sélectionner les membres qui couvrent le plus de technos const sortedMembers = Array.from(memberTechMap.entries()) .sort((a, b) => b[1].coverage - a[1].coverage); sortedMembers.forEach(([memberId, data]) => { genesisTeam.push({ member: data.member.id, fullName: data.member.fullName, role: data.member.role, availability: data.member.availability, seniority: data.member.seniorityLevel, technologies: data.technologies.map(t => ({ id: t.id, title: t.title, businessImpact: t.businessImpact })), coverage: data.coverage }); }); // Identifier les technos non couvertes const uncoveredTechs = coreTechs.filter(t => !coveredTechs.has(t.id)); return { team: genesisTeam, totalMembers: genesisTeam.length, coveredTechnologies: coveredTechs.size, totalCoreTechnologies: coreTechs.length, uncoveredTechnologies: uncoveredTechs.map(t => ({ id: t.id, title: t.title, businessImpact: t.businessImpact, skillGap: t.skillGap, teamCoverage: t.teamCoverage })), totalCapacity: genesisTeam.reduce((sum, m) => sum + m.availability, 0), averageAvailability: genesisTeam.length > 0 ? Math.round(genesisTeam.reduce((sum, m) => sum + m.availability, 0) / genesisTeam.length) : 0 }; } // Main function main() { console.log('📊 Génération des données de visualisation équipe/technologies...\n'); const technologies = loadTechnologies(); const members = loadTeamMembers(); console.log(`✅ ${technologies.length} technologies chargées`); console.log(`✅ ${members.length} membres chargés`); // Creer un index des profils membres pour acces rapide const memberProfiles = {}; members.forEach(m => { memberProfiles[m.id] = { id: m.id, fullName: m.fullName, role: m.role, availability: m.availability, seniorityLevel: m.seniorityLevel, yearsExperience: m.yearsExperience, joinDate: m.joinDate, skillsDetailed: m.skillsDetailed, interests: m.interests, softSkills: m.softSkills, projects: m.projects, bio: m.bio }; }); const data = { network: generateNetworkData(technologies, members), congestionMatrix: generateCongestionMatrix(technologies, members), genesisTeam: generateGenesisTeam(technologies, members), memberProfiles: memberProfiles, technologies: technologies, members: members, generatedAt: new Date().toISOString() }; const outputPath = path.join(__dirname, '../public/team-visualization-data.json'); fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf-8'); console.log(`\n✅ Données générées: ${outputPath}`); console.log(`📊 Graphe réseau: ${data.network.nodes.length} nœuds, ${data.network.edges.length} liens`); console.log(`🔥 Matrice congestion: ${data.congestionMatrix.length} technologies core`); console.log(`👥 Équipe de genèse: ${data.genesisTeam.totalMembers} membres`); console.log(` - Capacité totale: ${data.genesisTeam.totalCapacity}%`); console.log(` - Disponibilité moyenne: ${data.genesisTeam.averageAvailability}%`); console.log(` - Technologies couvertes: ${data.genesisTeam.coveredTechnologies}/${data.genesisTeam.totalCoreTechnologies}`); if (data.genesisTeam.uncoveredTechnologies.length > 0) { console.log(`⚠️ Technologies non couvertes: ${data.genesisTeam.uncoveredTechnologies.length}`); data.genesisTeam.uncoveredTechnologies.forEach(t => { console.log(` - ${t.title} (${t.businessImpact} impact, ${t.skillGap} gap)`); }); } } if (require.main === module) { main(); } module.exports = { loadTechnologies, loadTeamMembers, generateNetworkData, generateCongestionMatrix, generateGenesisTeam };