#!/usr/bin/env tsx /** * Script de synchronisation de la documentation Docusaurus * Vérifie la cohérence entre Vision et Current * Génère des rapports de synchronisation */ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs'; import { join, relative } from 'path'; import { execSync } from 'child_process'; interface DocFile { path: string; content: string; lastModified: Date; size: number; } interface SyncReport { timestamp: Date; visionFiles: DocFile[]; currentFiles: DocFile[]; inconsistencies: Inconsistency[]; recommendations: string[]; } interface Inconsistency { type: 'missing' | 'outdated' | 'mismatch' | 'broken-link'; file: string; description: string; severity: 'low' | 'medium' | 'high' | 'critical'; suggestion: string; } class DocSyncManager { private visionPath = 'veza-docs/docs-vision'; private currentPath = 'veza-docs/docs-current'; private assetsPath = 'veza-docs/docs-assets'; private reportPath = 'REPORTS'; constructor() { console.log('🔄 Initialisation du gestionnaire de synchronisation de documentation'); } /** * Scanne un répertoire et retourne la liste des fichiers */ private scanDirectory(dirPath: string): DocFile[] { if (!existsSync(dirPath)) { console.warn(`⚠️ Répertoire non trouvé: ${dirPath}`); return []; } const files: DocFile[] = []; const scanDir = (currentPath: string) => { const entries = readdirSync(currentPath); for (const entry of entries) { const fullPath = join(currentPath, entry); const stat = statSync(fullPath); if (stat.isDirectory()) { scanDir(fullPath); } else if (entry.endsWith('.md')) { try { const content = readFileSync(fullPath, 'utf-8'); files.push({ path: relative(dirPath, fullPath), content, lastModified: stat.mtime, size: stat.size }); } catch (error) { console.error(`❌ Erreur lecture fichier ${fullPath}:`, error); } } } }; scanDir(dirPath); return files; } /** * Vérifie la cohérence entre Vision et Current */ private checkConsistency(visionFiles: DocFile[], currentFiles: DocFile[]): Inconsistency[] { const inconsistencies: Inconsistency[] = []; // Vérifier les fichiers manquants const visionPaths = new Set(visionFiles.map(f => f.path)); const currentPaths = new Set(currentFiles.map(f => f.path)); // Fichiers manquants dans Current for (const visionFile of visionFiles) { if (!currentPaths.has(visionFile.path)) { inconsistencies.push({ type: 'missing', file: visionFile.path, description: `Fichier Vision manquant dans Current: ${visionFile.path}`, severity: 'medium', suggestion: `Créer le fichier correspondant dans docs-current/` }); } } // Vérifier les liens brisés for (const file of [...visionFiles, ...currentFiles]) { const brokenLinks = this.findBrokenLinks(file.content, file.path); inconsistencies.push(...brokenLinks); } // Vérifier les badges de statut for (const file of [...visionFiles, ...currentFiles]) { const badgeIssues = this.checkStatusBadges(file); inconsistencies.push(...badgeIssues); } return inconsistencies; } /** * Trouve les liens brisés dans un fichier */ private findBrokenLinks(content: string, filePath: string): Inconsistency[] { const inconsistencies: Inconsistency[] = []; const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g; let match; while ((match = linkRegex.exec(content)) !== null) { const [, text, link] = match; // Vérifier les liens internes if (link.startsWith('./') || link.startsWith('../')) { const resolvedPath = this.resolveLink(link, filePath); if (!existsSync(resolvedPath)) { inconsistencies.push({ type: 'broken-link', file: filePath, description: `Lien brisé: ${link} (vers ${text})`, severity: 'medium', suggestion: `Vérifier le chemin: ${resolvedPath}` }); } } } return inconsistencies; } /** * Résout un lien relatif */ private resolveLink(link: string, fromFile: string): string { const baseDir = fromFile.includes('docs-vision') ? this.visionPath : this.currentPath; const fromDir = join(baseDir, fromFile.split('/').slice(0, -1).join('/')); return join(fromDir, link); } /** * Vérifie les badges de statut */ private checkStatusBadges(file: DocFile): Inconsistency[] { const inconsistencies: Inconsistency[] = []; const content = file.content; // Vérifier la présence de badges if (!content.includes(':::note') && !content.includes(':::tip') && !content.includes(':::warning')) { inconsistencies.push({ type: 'mismatch', file: file.path, description: 'Fichier sans badge de statut', severity: 'low', suggestion: 'Ajouter un badge de statut (NOTE, TIP, WARNING)' }); } // Vérifier la cohérence des badges if (file.path.includes('docs-vision') && !content.includes('CIBLE')) { inconsistencies.push({ type: 'mismatch', file: file.path, description: 'Fichier Vision sans badge CIBLE', severity: 'medium', suggestion: 'Ajouter le badge CIBLE pour les fichiers Vision' }); } if (file.path.includes('docs-current') && !content.includes('ÉTAT ACTUEL')) { inconsistencies.push({ type: 'mismatch', file: file.path, description: 'Fichier Current sans badge ÉTAT ACTUEL', severity: 'medium', suggestion: 'Ajouter le badge ÉTAT ACTUEL pour les fichiers Current' }); } return inconsistencies; } /** * Génère des recommandations */ private generateRecommendations(inconsistencies: Inconsistency[]): string[] { const recommendations: string[] = []; const criticalIssues = inconsistencies.filter(i => i.severity === 'critical'); const highIssues = inconsistencies.filter(i => i.severity === 'high'); const mediumIssues = inconsistencies.filter(i => i.severity === 'medium'); if (criticalIssues.length > 0) { recommendations.push(`🚨 ${criticalIssues.length} problème(s) critique(s) à résoudre immédiatement`); } if (highIssues.length > 0) { recommendations.push(`⚠️ ${highIssues.length} problème(s) de haute priorité à traiter cette semaine`); } if (mediumIssues.length > 0) { recommendations.push(`📝 ${mediumIssues.length} problème(s) de priorité moyenne à traiter prochainement`); } // Recommandations spécifiques const missingFiles = inconsistencies.filter(i => i.type === 'missing'); if (missingFiles.length > 0) { recommendations.push(`📄 ${missingFiles.length} fichier(s) manquant(s) dans Current - synchroniser avec Vision`); } const brokenLinks = inconsistencies.filter(i => i.type === 'broken-link'); if (brokenLinks.length > 0) { recommendations.push(`🔗 ${brokenLinks.length} lien(s) brisé(s) à corriger`); } const badgeIssues = inconsistencies.filter(i => i.type === 'mismatch'); if (badgeIssues.length > 0) { recommendations.push(`🏷️ ${badgeIssues.length} problème(s) de badges à corriger`); } return recommendations; } /** * Génère un rapport de synchronisation */ private generateReport(visionFiles: DocFile[], currentFiles: DocFile[], inconsistencies: Inconsistency[]): SyncReport { return { timestamp: new Date(), visionFiles, currentFiles, inconsistencies, recommendations: this.generateRecommendations(inconsistencies) }; } /** * Sauvegarde le rapport */ private saveReport(report: SyncReport): void { const timestamp = report.timestamp.toISOString().split('T')[0]; const reportFile = join(this.reportPath, `doc-sync-${timestamp}.md`); const reportContent = this.formatReport(report); writeFileSync(reportFile, reportContent, 'utf-8'); console.log(`📊 Rapport sauvegardé: ${reportFile}`); } /** * Formate le rapport en Markdown */ private formatReport(report: SyncReport): string { const { timestamp, visionFiles, currentFiles, inconsistencies, recommendations } = report; let content = `# 📚 Rapport de Synchronisation Documentation\n\n`; content += `**Date** : ${timestamp.toLocaleDateString('fr-FR')}\n`; content += `**Heure** : ${timestamp.toLocaleTimeString('fr-FR')}\n\n`; // Statistiques content += `## 📊 Statistiques\n\n`; content += `- **Fichiers Vision** : ${visionFiles.length}\n`; content += `- **Fichiers Current** : ${currentFiles.length}\n`; content += `- **Incohérences** : ${inconsistencies.length}\n`; content += `- **Recommandations** : ${recommendations.length}\n\n`; // Recommandations if (recommendations.length > 0) { content += `## 🎯 Recommandations\n\n`; recommendations.forEach(rec => { content += `- ${rec}\n`; }); content += `\n`; } // Incohérences par sévérité const critical = inconsistencies.filter(i => i.severity === 'critical'); const high = inconsistencies.filter(i => i.severity === 'high'); const medium = inconsistencies.filter(i => i.severity === 'medium'); const low = inconsistencies.filter(i => i.severity === 'low'); if (critical.length > 0) { content += `## 🚨 Problèmes Critiques\n\n`; critical.forEach(issue => { content += `### ${issue.file}\n`; content += `- **Type** : ${issue.type}\n`; content += `- **Description** : ${issue.description}\n`; content += `- **Suggestion** : ${issue.suggestion}\n\n`; }); } if (high.length > 0) { content += `## ⚠️ Problèmes de Haute Priorité\n\n`; high.forEach(issue => { content += `### ${issue.file}\n`; content += `- **Type** : ${issue.type}\n`; content += `- **Description** : ${issue.description}\n`; content += `- **Suggestion** : ${issue.suggestion}\n\n`; }); } if (medium.length > 0) { content += `## 📝 Problèmes de Priorité Moyenne\n\n`; medium.forEach(issue => { content += `### ${issue.file}\n`; content += `- **Type** : ${issue.type}\n`; content += `- **Description** : ${issue.description}\n`; content += `- **Suggestion** : ${issue.suggestion}\n\n`; }); } if (low.length > 0) { content += `## ℹ️ Problèmes de Priorité Faible\n\n`; low.forEach(issue => { content += `### ${issue.file}\n`; content += `- **Type** : ${issue.type}\n`; content += `- **Description** : ${issue.description}\n`; content += `- **Suggestion** : ${issue.suggestion}\n\n`; }); } // Fichiers récents content += `## 📄 Fichiers Récents\n\n`; const recentFiles = [...visionFiles, ...currentFiles] .sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime()) .slice(0, 10); recentFiles.forEach(file => { const type = file.path.includes('docs-vision') ? 'Vision' : 'Current'; content += `- **${type}** : ${file.path} (${file.lastModified.toLocaleDateString('fr-FR')})\n`; }); content += `\n---\n\n`; content += `*Rapport généré automatiquement par DocSyncManager*\n`; return content; } /** * Exécute la synchronisation complète */ public async sync(): Promise { console.log('🔄 Démarrage de la synchronisation de documentation...'); try { // Scanner les répertoires console.log('📁 Scan des fichiers Vision...'); const visionFiles = this.scanDirectory(this.visionPath); console.log('📁 Scan des fichiers Current...'); const currentFiles = this.scanDirectory(this.currentPath); // Vérifier la cohérence console.log('🔍 Vérification de la cohérence...'); const inconsistencies = this.checkConsistency(visionFiles, currentFiles); // Générer le rapport console.log('📊 Génération du rapport...'); const report = this.generateReport(visionFiles, currentFiles, inconsistencies); // Sauvegarder le rapport this.saveReport(report); // Afficher le résumé console.log('\n📋 Résumé de la synchronisation:'); console.log(` - Fichiers Vision: ${visionFiles.length}`); console.log(` - Fichiers Current: ${currentFiles.length}`); console.log(` - Incohérences: ${inconsistencies.length}`); console.log(` - Recommandations: ${report.recommendations.length}`); if (inconsistencies.length > 0) { console.log('\n⚠️ Incohérences détectées:'); inconsistencies.forEach(issue => { const emoji = issue.severity === 'critical' ? '🚨' : issue.severity === 'high' ? '⚠️' : issue.severity === 'medium' ? '📝' : 'ℹ️'; console.log(` ${emoji} ${issue.file}: ${issue.description}`); }); } else { console.log('\n✅ Aucune incohérence détectée!'); } } catch (error) { console.error('❌ Erreur lors de la synchronisation:', error); process.exit(1); } } } // Exécution du script if (require.main === module) { const syncManager = new DocSyncManager(); syncManager.sync().catch(console.error); } export default DocSyncManager;