416 lines
13 KiB
TypeScript
416 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;
|