415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
#!/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;
|