docs/ est entièrement ignoré par .gitignore, mais docs/data/team/*.md est nécessaire au build (generate-team-visualization-data.js). Déplacement vers data/ à la racine pour que ces fichiers soient versionnés et disponibles lors du déploiement depuis le dépôt. - Nouveau dossier data/ versionné (profils équipe, technologies) - docs/ entièrement ignoré (documentation humaine uniquement) - Mise à jour des 4 références dans les scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
393 lines
13 KiB
JavaScript
Executable File
393 lines
13 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Script pour analyser les métriques business du radar
|
|
* et identifier des patterns stratégiques
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Parser un blip
|
|
function parseBlip(filePath) {
|
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
const frontMatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
|
|
if (!frontMatterMatch) return null;
|
|
|
|
const frontMatter = frontMatterMatch[1];
|
|
const body = frontMatterMatch[2];
|
|
|
|
const metadata = {};
|
|
for (const line of frontMatter.split('\n')) {
|
|
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
if (match) {
|
|
const key = match[1];
|
|
let value = match[2].trim();
|
|
|
|
// Parser les valeurs
|
|
if (value === 'true') value = true;
|
|
else if (value === 'false') value = false;
|
|
else if (!isNaN(value) && value !== '') value = Number(value);
|
|
else if (value.startsWith('[')) {
|
|
value = value.slice(1, -1).split(',').map(v => v.trim().replace(/['"]/g, ''));
|
|
}
|
|
|
|
metadata[key] = value;
|
|
}
|
|
}
|
|
|
|
return { metadata, body };
|
|
}
|
|
|
|
// Analyser tous les blips
|
|
function analyzeRadar(radarDir) {
|
|
const files = fs.readdirSync(radarDir).filter(f => f.endsWith('.md'));
|
|
const blips = [];
|
|
|
|
for (const file of files) {
|
|
const blip = parseBlip(path.join(radarDir, file));
|
|
if (blip) {
|
|
blips.push({
|
|
file,
|
|
...blip
|
|
});
|
|
}
|
|
}
|
|
|
|
return blips;
|
|
}
|
|
|
|
// Calculer les métriques
|
|
function calculateMetrics(blips) {
|
|
const metrics = {
|
|
total: blips.length,
|
|
byQuadrant: {},
|
|
byRing: {},
|
|
totalCostToReplace: 0,
|
|
totalMaintenanceCost: 0,
|
|
riskDistribution: { high: 0, medium: 0, low: 0 },
|
|
competencyDistribution: { expert: 0, intermediate: 0, beginner: 0 },
|
|
skillGapDistribution: { high: 0, medium: 0, low: 0 },
|
|
businessImpactDistribution: { high: 0, medium: 0, low: 0 },
|
|
differentiationDistribution: { high: 0, medium: 0, low: 0 },
|
|
totalTeamCoverage: 0,
|
|
technologiesByRisk: {
|
|
high: [],
|
|
medium: [],
|
|
low: []
|
|
},
|
|
technologiesBySkillGap: {
|
|
high: [],
|
|
medium: [],
|
|
low: []
|
|
},
|
|
criticalTechnologies: [],
|
|
emergingTechnologies: []
|
|
};
|
|
|
|
for (const blip of blips) {
|
|
const m = blip.metadata;
|
|
|
|
// Par quadrant
|
|
if (!metrics.byQuadrant[m.quadrant]) {
|
|
metrics.byQuadrant[m.quadrant] = 0;
|
|
}
|
|
metrics.byQuadrant[m.quadrant]++;
|
|
|
|
// Par ring
|
|
if (!metrics.byRing[m.ring]) {
|
|
metrics.byRing[m.ring] = 0;
|
|
}
|
|
metrics.byRing[m.ring]++;
|
|
|
|
// Coûts
|
|
metrics.totalCostToReplace += m.costToReplace || 0;
|
|
metrics.totalMaintenanceCost += m.maintenanceCost || 0;
|
|
|
|
// Distributions
|
|
if (m.riskLevel) metrics.riskDistribution[m.riskLevel]++;
|
|
if (m.competencyLevel) metrics.competencyDistribution[m.competencyLevel]++;
|
|
if (m.skillGap) metrics.skillGapDistribution[m.skillGap]++;
|
|
if (m.businessImpact) metrics.businessImpactDistribution[m.businessImpact]++;
|
|
if (m.differentiation) metrics.differentiationDistribution[m.differentiation]++;
|
|
|
|
// Coverage
|
|
metrics.totalTeamCoverage += m.teamCoverage || 0;
|
|
|
|
// Technologies à risque
|
|
if (m.riskLevel === 'high') {
|
|
metrics.technologiesByRisk.high.push({
|
|
name: m.title,
|
|
quadrant: m.quadrant,
|
|
ring: m.ring,
|
|
riskLevel: m.riskLevel
|
|
});
|
|
}
|
|
|
|
// Technologies avec gap de compétences
|
|
if (m.skillGap === 'high') {
|
|
metrics.technologiesBySkillGap.high.push({
|
|
name: m.title,
|
|
quadrant: m.quadrant,
|
|
ring: m.ring,
|
|
teamCoverage: m.teamCoverage,
|
|
competencyLevel: m.competencyLevel
|
|
});
|
|
}
|
|
|
|
// Technologies critiques (adopt + high impact) - anciennement "core"
|
|
if (m.ring === 'adopt' && m.businessImpact === 'high') {
|
|
metrics.criticalTechnologies.push({
|
|
name: m.title,
|
|
quadrant: m.quadrant,
|
|
riskLevel: m.riskLevel,
|
|
skillGap: m.skillGap,
|
|
teamCoverage: m.teamCoverage
|
|
});
|
|
}
|
|
|
|
// Technologies émergentes
|
|
if (m.quadrant === 'technologies-emergentes') {
|
|
metrics.emergingTechnologies.push({
|
|
name: m.title,
|
|
ring: m.ring,
|
|
businessImpact: m.businessImpact,
|
|
differentiation: m.differentiation
|
|
});
|
|
}
|
|
}
|
|
|
|
return metrics;
|
|
}
|
|
|
|
// Identifier les patterns
|
|
function identifyPatterns(blips, metrics) {
|
|
const patterns = {
|
|
criticalNonDifferentiating: [],
|
|
singleVendorDependencies: [],
|
|
obsoleteTechnologies: [],
|
|
innovationOpportunities: [],
|
|
skillGaps: []
|
|
};
|
|
|
|
for (const blip of blips) {
|
|
const m = blip.metadata;
|
|
|
|
// Technologies critiques non différenciantes (commodité critique) - anciennement "core"
|
|
if (m.ring === 'adopt' && m.differentiation === 'low') {
|
|
patterns.criticalNonDifferentiating.push({
|
|
name: m.title,
|
|
quadrant: m.quadrant,
|
|
costToReplace: m.costToReplace,
|
|
maintenanceCost: m.maintenanceCost
|
|
});
|
|
}
|
|
|
|
// Technologies obsolètes (legacy)
|
|
if (m.ring === 'legacy') {
|
|
patterns.obsoleteTechnologies.push({
|
|
name: m.title,
|
|
riskLevel: m.riskLevel,
|
|
costToReplace: m.costToReplace
|
|
});
|
|
}
|
|
|
|
// Opportunités d'innovation (émergentes + high differentiation)
|
|
if (m.quadrant === 'technologies-emergentes' && m.differentiation === 'high') {
|
|
patterns.innovationOpportunities.push({
|
|
name: m.title,
|
|
ring: m.ring,
|
|
businessImpact: m.businessImpact
|
|
});
|
|
}
|
|
|
|
// Gaps de compétences critiques - anciennement "core", maintenant "adopt"
|
|
if (m.skillGap === 'high' && (m.ring === 'adopt' || m.businessImpact === 'high')) {
|
|
patterns.skillGaps.push({
|
|
name: m.title,
|
|
ring: m.ring,
|
|
businessImpact: m.businessImpact,
|
|
teamCoverage: m.teamCoverage,
|
|
competencyLevel: m.competencyLevel
|
|
});
|
|
}
|
|
}
|
|
|
|
return patterns;
|
|
}
|
|
|
|
// Générer le rapport
|
|
function generateReport(metrics, patterns) {
|
|
const report = `# Analyse Stratégique - Radar Business Duniter/Ğ1
|
|
|
|
Date: ${new Date().toLocaleDateString('fr-FR')}
|
|
|
|
## Vue d'ensemble
|
|
|
|
- **Total de technologies analysées** : ${metrics.total}
|
|
- **Coût total de remplacement** : ${metrics.totalCostToReplace.toLocaleString('fr-FR')}€
|
|
- **Coût total de maintenance annuel** : ${metrics.totalMaintenanceCost.toLocaleString('fr-FR')}€
|
|
- **Couverture moyenne de l'équipe** : ${(metrics.totalTeamCoverage / metrics.total).toFixed(1)} personnes par technologie
|
|
|
|
## Répartition par Quadrant
|
|
|
|
${Object.entries(metrics.byQuadrant).map(([q, count]) => `- **${q}** : ${count} technologies`).join('\n')}
|
|
|
|
## Répartition par Ring
|
|
|
|
${Object.entries(metrics.byRing).map(([r, count]) => `- **${r}** : ${count} technologies`).join('\n')}
|
|
|
|
## Distribution des Risques
|
|
|
|
- **Risque élevé** : ${metrics.riskDistribution.high} technologies
|
|
- **Risque modéré** : ${metrics.riskDistribution.medium} technologies
|
|
- **Risque faible** : ${metrics.riskDistribution.low} technologies
|
|
|
|
## Distribution des Compétences
|
|
|
|
- **Expert** : ${metrics.competencyDistribution.expert} technologies
|
|
- **Intermédiaire** : ${metrics.competencyDistribution.intermediate} technologies
|
|
- **Débutant** : ${metrics.competencyDistribution.beginner} technologies
|
|
|
|
## Distribution des Gaps de Compétences
|
|
|
|
- **Gap élevé** : ${metrics.skillGapDistribution.high} technologies
|
|
- **Gap modéré** : ${metrics.skillGapDistribution.medium} technologies
|
|
- **Gap faible** : ${metrics.skillGapDistribution.low} technologies
|
|
|
|
## Technologies à Risque Élevé
|
|
|
|
${metrics.technologiesByRisk.high.length > 0
|
|
? metrics.technologiesByRisk.high.map(t => `- **${t.name}** (${t.quadrant}, ${t.ring})`).join('\n')
|
|
: 'Aucune technologie à risque élevé identifiée.'}
|
|
|
|
## Technologies avec Gap de Compétences Élevé
|
|
|
|
${metrics.technologiesBySkillGap.high.length > 0
|
|
? metrics.technologiesBySkillGap.high.map(t => `- **${t.name}** (${t.teamCoverage} personne(s), niveau: ${t.competencyLevel})`).join('\n')
|
|
: 'Aucun gap de compétences élevé identifié.'}
|
|
|
|
## Technologies Critiques
|
|
|
|
${metrics.criticalTechnologies.length > 0
|
|
? metrics.criticalTechnologies.map(t => `- **${t.name}** (risque: ${t.riskLevel}, gap: ${t.skillGap}, couverture: ${t.teamCoverage})`).join('\n')
|
|
: 'Aucune technologie critique identifiée.'}
|
|
|
|
## Technologies Émergentes
|
|
|
|
${metrics.emergingTechnologies.length > 0
|
|
? metrics.emergingTechnologies.map(t => `- **${t.name}** (impact: ${t.businessImpact}, différenciation: ${t.differentiation})`).join('\n')
|
|
: 'Aucune technologie émergente identifiée.'}
|
|
|
|
## Patterns Identifiés
|
|
|
|
### Technologies Critiques Non Différenciantes
|
|
|
|
${patterns.criticalNonDifferentiating.length > 0
|
|
? patterns.criticalNonDifferentiating.map(t => `- **${t.name}** (coût remplacement: ${t.costToReplace}€, maintenance: ${t.maintenanceCost}€/an)`).join('\n')
|
|
: 'Aucune technologie critique non différenciante identifiée.'}
|
|
|
|
### Technologies Obsolètes
|
|
|
|
${patterns.obsoleteTechnologies.length > 0
|
|
? patterns.obsoleteTechnologies.map(t => `- **${t.name}** (risque: ${t.riskLevel}, coût remplacement: ${t.costToReplace}€)`).join('\n')
|
|
: 'Aucune technologie obsolète identifiée.'}
|
|
|
|
### Opportunités d'Innovation
|
|
|
|
${patterns.innovationOpportunities.length > 0
|
|
? patterns.innovationOpportunities.map(t => `- **${t.name}** (ring: ${t.ring}, impact: ${t.businessImpact})`).join('\n')
|
|
: 'Aucune opportunité d\'innovation identifiée.'}
|
|
|
|
### Gaps de Compétences Critiques
|
|
|
|
${patterns.skillGaps.length > 0
|
|
? patterns.skillGaps.map(t => `- **${t.name}** (ring: ${t.ring}, impact: ${t.businessImpact}, couverture: ${t.teamCoverage}, niveau: ${t.competencyLevel})`).join('\n')
|
|
: 'Aucun gap de compétences critique identifié.'}
|
|
|
|
## Recommandations Stratégiques
|
|
|
|
### Priorité 1 : Gérer les Risques Critiques
|
|
|
|
${patterns.skillGaps.length > 0
|
|
? `- **Formation et recrutement** : Investir dans la formation ou le recrutement pour les technologies suivantes :
|
|
${patterns.skillGaps.map(t => ` - ${t.name} (${t.teamCoverage} personne(s), niveau ${t.competencyLevel})`).join('\n')}`
|
|
: '- Aucune action urgente requise.'}
|
|
|
|
### Priorité 2 : Optimiser les Coûts
|
|
|
|
${patterns.criticalNonDifferentiating.length > 0
|
|
? `- **Optimisation des commodités** : Réduire les coûts de maintenance pour :
|
|
${patterns.criticalNonDifferentiating.map(t => ` - ${t.name} (${t.maintenanceCost}€/an)`).join('\n')}`
|
|
: '- Aucune optimisation majeure identifiée.'}
|
|
|
|
### Priorité 3 : Planifier les Migrations
|
|
|
|
${patterns.obsoleteTechnologies.length > 0
|
|
? `- **Plan de migration** : Planifier le remplacement de :
|
|
${patterns.obsoleteTechnologies.map(t => ` - ${t.name} (coût estimé: ${t.costToReplace}€)`).join('\n')}`
|
|
: '- Aucune migration urgente requise.'}
|
|
|
|
### Priorité 4 : Investir dans l'Innovation
|
|
|
|
${patterns.innovationOpportunities.length > 0
|
|
? `- **Technologies émergentes** : Évaluer l\'adoption de :
|
|
${patterns.innovationOpportunities.map(t => ` - ${t.name} (ring: ${t.ring})`).join('\n')}`
|
|
: '- Aucune opportunité d\'innovation identifiée.'}
|
|
|
|
## Matrice Risques/Opportunités
|
|
|
|
### Zone Critique (Risque élevé + Impact élevé)
|
|
|
|
${metrics.criticalTechnologies.filter(t => t.riskLevel === 'high').length > 0
|
|
? metrics.criticalTechnologies.filter(t => t.riskLevel === 'high').map(t => `- **${t.name}** : Action immédiate requise`).join('\n')
|
|
: 'Aucune technologie en zone critique.'}
|
|
|
|
### Zone d'Opportunité (Faible risque + Différenciation élevée)
|
|
|
|
${metrics.emergingTechnologies.filter(t => t.differentiation === 'high').length > 0
|
|
? metrics.emergingTechnologies.filter(t => t.differentiation === 'high').map(t => `- **${t.name}** : Opportunité d'investissement`).join('\n')
|
|
: 'Aucune opportunité majeure identifiée.'}
|
|
|
|
`;
|
|
|
|
return report;
|
|
}
|
|
|
|
// Main
|
|
function main() {
|
|
const radarDir = path.join(__dirname, '../radar-business/2025-01-15');
|
|
const outputFile = path.join(__dirname, '../data/analyse-strategique.md');
|
|
|
|
if (!fs.existsSync(radarDir)) {
|
|
console.error(`Répertoire non trouvé: ${radarDir}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('Analyse du radar business...');
|
|
const blips = analyzeRadar(radarDir);
|
|
console.log(`${blips.length} blips analysés`);
|
|
|
|
const metrics = calculateMetrics(blips);
|
|
const patterns = identifyPatterns(blips, metrics);
|
|
|
|
const report = generateReport(metrics, patterns);
|
|
fs.writeFileSync(outputFile, report, 'utf-8');
|
|
|
|
console.log(`Rapport généré: ${outputFile}`);
|
|
}
|
|
|
|
if (require.main === module) {
|
|
// Vérifier si js-yaml est disponible (optionnel)
|
|
try {
|
|
require('js-yaml');
|
|
} catch (e) {
|
|
// Pas grave, on n'en a pas besoin pour ce script
|
|
}
|
|
main();
|
|
}
|
|
|
|
module.exports = { analyzeRadar, calculateMetrics, identifyPatterns, generateReport };
|
|
|