veza/tools/sync_docs.ts

416 lines
13 KiB
TypeScript
Raw Normal View History

#!/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<void> {
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;