#!/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 function extractSkills(yamlContent) { const skills = []; const lines = yamlContent.split('\n'); let inSkillsSection = false; 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+/)) { inSkillsSection = false; continue; } if (inSkillsSection) { const nameMatch = line.match(/^\s+-\s+name:\s*["']?([^"'\n]+)["']?/); if (nameMatch) { skills.push(nameMatch[1]); } } } return skills; } // 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 function loadTeamMembers() { const teamDir = path.join(__dirname, '../docs/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 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'), skills: extractSkills(yaml) }; 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`); const data = { network: generateNetworkData(technologies, members), congestionMatrix: generateCongestionMatrix(technologies, members), genesisTeam: generateGenesisTeam(technologies, members), 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 };