/** * ValidatorCore.js * * Orchestrateur principal du Pipeline Validator * Coordonne l'exécution du pipeline, l'échantillonnage et l'évaluation */ const { logSh } = require('../ErrorReporting'); const { tracer } = require('../trace'); const { PipelineExecutor } = require('../pipeline/PipelineExecutor'); const { SamplingEngine } = require('./SamplingEngine'); const { CriteriaEvaluator } = require('./CriteriaEvaluator'); const { v4: uuidv4 } = require('uuid'); const fs = require('fs').promises; const path = require('path'); /** * Presets de validation configurables */ const VALIDATION_PRESETS = { 'ultra-rapid': { name: 'Ultra-rapide', description: 'Validation minimaliste (v1.0 + v2.0, 2 critères, 3 échantillons)', versions: ['v1.0', 'v2.0'], criteria: ['qualite', 'naturalite'], maxSamples: 3, estimatedCost: 0.05, estimatedDuration: '30s' }, 'economical': { name: 'Économique', description: 'Validation légère (v1.0 + v2.0, 3 critères, 5 échantillons)', versions: ['v1.0', 'v2.0'], criteria: ['qualite', 'naturalite', 'seo'], maxSamples: 5, estimatedCost: 0.12, estimatedDuration: '1min' }, 'standard': { name: 'Standard', description: 'Validation équilibrée (3 versions, tous critères, 5 échantillons)', versions: ['v1.0', 'v1.2', 'v2.0'], criteria: ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'], maxSamples: 5, estimatedCost: 0.30, estimatedDuration: '2min' }, 'complete': { name: 'Complet', description: 'Validation exhaustive (toutes versions, tous critères, tous échantillons)', versions: null, // null = toutes les versions criteria: ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'], maxSamples: null, // null = tous les échantillons estimatedCost: 1.00, estimatedDuration: '5min' } }; /** * Classe ValidatorCore */ class ValidatorCore { constructor(options = {}) { this.validationId = null; this.validationDir = null; this.executor = new PipelineExecutor(); this.samplingEngine = new SamplingEngine(); this.criteriaEvaluator = new CriteriaEvaluator(); this.csvData = null; this.status = 'idle'; this.progress = { phase: null, message: '', percentage: 0 }; this.validationConfig = null; // ✅ NOUVEAU: Config validation active // ✅ PHASE 3: WebSocket callback pour broadcast temps réel this.broadcastCallback = options.broadcastCallback || null; } /** * ✅ NOUVEAU: Obtient les presets disponibles */ static getPresets() { return VALIDATION_PRESETS; } /** * ✅ NOUVEAU: Applique un preset ou une config custom */ applyConfig(config) { // Si config est un string, utiliser le preset correspondant if (typeof config === 'string') { if (!VALIDATION_PRESETS[config]) { throw new Error(`Preset inconnu: ${config}`); } this.validationConfig = { preset: config, ...VALIDATION_PRESETS[config] }; } else if (config && typeof config === 'object') { // Config custom this.validationConfig = { preset: 'custom', name: config.name || 'Custom', description: config.description || 'Configuration personnalisée', versions: config.versions || null, criteria: config.criteria || ['qualite', 'verbosite', 'seo', 'repetitions', 'naturalite'], maxSamples: config.maxSamples || null }; } else { // Par défaut: mode complet this.validationConfig = { preset: 'complete', ...VALIDATION_PRESETS.complete }; } logSh(`⚙️ Configuration validation: ${this.validationConfig.name}`, 'INFO'); logSh(` Versions: ${this.validationConfig.versions ? this.validationConfig.versions.join(', ') : 'toutes'}`, 'DEBUG'); logSh(` Critères: ${this.validationConfig.criteria.join(', ')}`, 'DEBUG'); logSh(` Max échantillons: ${this.validationConfig.maxSamples || 'tous'}`, 'DEBUG'); } /** * Exécute une validation complète * @param {Object|String} config - Configuration validation (preset name ou config object) * @param {Object} pipelineConfig - Configuration pipeline * @param {number} rowNumber - Numéro de ligne Google Sheets * @returns {Object} - Résultats validation */ async runValidation(config, pipelineConfig, rowNumber) { return tracer.run('ValidatorCore.runValidation', async () => { try { this.status = 'running'; this.validationId = uuidv4(); // ✅ NOUVEAU: Appliquer configuration this.applyConfig(config); logSh(`🚀 Démarrage validation: ${this.validationId}`, 'INFO'); logSh(` Pipeline: ${pipelineConfig.name} | Row: ${rowNumber}`, 'INFO'); logSh(` Mode: ${this.validationConfig.name}`, 'INFO'); // ======================================== // PHASE 1: SETUP // ======================================== this.updateProgress('setup', 'Création structure dossiers...', 5); await this.setupValidationStructure(); // ======================================== // PHASE 2: EXÉCUTION PIPELINE // ======================================== this.updateProgress('pipeline', 'Exécution pipeline avec sauvegarde versions...', 10); const pipelineResult = await this.runPipeline(pipelineConfig, rowNumber); if (!pipelineResult.success) { throw new Error('Échec exécution pipeline'); } this.updateProgress('pipeline', 'Pipeline terminé avec succès', 40); // ======================================== // PHASE 3: ÉCHANTILLONNAGE // ======================================== this.updateProgress('sampling', 'Extraction échantillons représentatifs...', 50); const samplesResult = await this.runSampling(pipelineResult.versionPaths); this.updateProgress('sampling', `Échantillonnage terminé: ${samplesResult.summary.totalSamples} échantillons`, 60); // ======================================== // PHASE 4: ÉVALUATION LLM (✅ NOUVEAU) // ======================================== this.updateProgress('evaluation', 'Évaluation LLM des échantillons...', 65); const evaluationsResult = await this.runEvaluations(samplesResult.samples, this.csvData); this.updateProgress('evaluation', `Évaluations terminées: ${Object.keys(evaluationsResult).length} échantillons évalués`, 85); // ======================================== // PHASE 5: SAUVEGARDE CONFIGURATION ET RÉSULTATS // ======================================== this.updateProgress('saving', 'Sauvegarde configuration et métadonnées...', 88); await this.saveConfiguration(pipelineConfig, rowNumber, pipelineResult); await this.saveSamplesData(samplesResult); await this.saveEvaluationsData(evaluationsResult); // ======================================== // PHASE 6: GÉNÉRATION RAPPORT // ======================================== this.updateProgress('report', 'Génération rapport validation...', 95); const report = await this.generateReport(pipelineResult, samplesResult, evaluationsResult); this.updateProgress('completed', 'Validation terminée avec succès', 100); this.status = 'completed'; logSh(`✅ Validation ${this.validationId} terminée avec succès`, 'INFO'); return { success: true, validationId: this.validationId, validationDir: this.validationDir, report, versionPaths: pipelineResult.versionPaths, samples: samplesResult }; } catch (error) { this.status = 'error'; this.updateProgress('error', `Erreur: ${error.message}`, 0); logSh(`❌ Validation ${this.validationId} échouée: ${error.message}`, 'ERROR'); return { success: false, validationId: this.validationId, error: error.message, status: this.status }; } }, { validationId: this.validationId }); } /** * Crée la structure de dossiers pour la validation */ async setupValidationStructure() { this.validationDir = path.join(process.cwd(), 'validations', this.validationId); const dirs = [ this.validationDir, path.join(this.validationDir, 'versions'), path.join(this.validationDir, 'samples'), path.join(this.validationDir, 'results') ]; for (const dir of dirs) { await fs.mkdir(dir, { recursive: true }); } logSh(`📁 Structure validation créée: ${this.validationDir}`, 'DEBUG'); } /** * Exécute le pipeline avec sauvegarde toutes versions */ async runPipeline(pipelineConfig, rowNumber) { logSh(`▶ Exécution pipeline: ${pipelineConfig.name}`, 'INFO'); const versionsDir = path.join(this.validationDir, 'versions'); const result = await this.executor.execute(pipelineConfig, rowNumber, { saveAllVersions: true, outputDir: versionsDir, stopOnError: true }); // ✅ Stocker csvData pour contexte évaluations this.csvData = this.executor.csvData; logSh(`✓ Pipeline exécuté: ${result.versionPaths.length} versions sauvegardées`, 'INFO'); return result; } /** * Exécute l'échantillonnage * ✅ MODIFIÉ: Filtre versions et limite échantillons selon config */ async runSampling(versionPaths) { logSh(`▶ Échantillonnage: ${versionPaths.length} versions`, 'INFO'); // ✅ Filtrer versions si config spécifie une liste let filteredPaths = versionPaths; if (this.validationConfig.versions) { filteredPaths = versionPaths.filter(vp => { const versionName = path.basename(vp, '.json'); return this.validationConfig.versions.includes(versionName); }); logSh(` Versions filtrées: ${filteredPaths.map(vp => path.basename(vp, '.json')).join(', ')}`, 'DEBUG'); } const samplesResult = await this.samplingEngine.extractSamples(filteredPaths); // ✅ Limiter nombre échantillons si config le spécifie if (this.validationConfig.maxSamples && this.validationConfig.maxSamples < Object.keys(samplesResult.samples).length) { const allTags = Object.keys(samplesResult.samples); const selectedTags = allTags.slice(0, this.validationConfig.maxSamples); const limitedSamples = {}; selectedTags.forEach(tag => { limitedSamples[tag] = samplesResult.samples[tag]; }); samplesResult.samples = limitedSamples; samplesResult.summary.totalSamples = selectedTags.length; logSh(` Échantillons limités à ${this.validationConfig.maxSamples}`, 'DEBUG'); } // Sauvegarder les échantillons const samplesPath = path.join(this.validationDir, 'samples', 'all-samples.json'); await this.samplingEngine.saveSamples(samplesResult, samplesPath); logSh(`✓ Échantillons extraits et sauvegardés`, 'INFO'); return samplesResult; } /** * ✅ MODIFIÉ: Exécute les évaluations LLM avec filtrage critères */ async runEvaluations(samples, csvData) { logSh(`▶ Évaluation LLM: ${Object.keys(samples).length} échantillons`, 'INFO'); logSh(` Critères actifs: ${this.validationConfig.criteria.join(', ')}`, 'DEBUG'); // Préparer contexte pour évaluations const context = { mc0: csvData?.mc0 || '', t0: csvData?.t0 || '', personality: csvData?.personality || {} }; // ✅ Passer la liste des critères à évaluer const evaluations = await this.criteriaEvaluator.evaluateBatch( samples, context, 3, // maxConcurrent this.validationConfig.criteria // ✅ NOUVEAU: critères filtrés ); // Calculer scores agrégés const aggregated = this.criteriaEvaluator.aggregateScores(evaluations); logSh(`✓ Évaluations terminées: ${Object.keys(evaluations).length} échantillons`, 'INFO'); logSh(` Score global moyen: ${aggregated.overall.avgScore}/10`, 'INFO'); return { evaluations, aggregated }; } /** * Sauvegarde la configuration utilisée */ async saveConfiguration(pipelineConfig, rowNumber, pipelineResult) { const configPath = path.join(this.validationDir, 'config.json'); const configData = { validationId: this.validationId, timestamp: new Date().toISOString(), pipeline: pipelineConfig, rowNumber, personality: pipelineResult.metadata.personality, executionLog: pipelineResult.executionLog }; await fs.writeFile(configPath, JSON.stringify(configData, null, 2), 'utf8'); logSh(`💾 Configuration sauvegardée: ${configPath}`, 'DEBUG'); } /** * Sauvegarde les données d'échantillons */ async saveSamplesData(samplesResult) { const samplesPath = path.join(this.validationDir, 'samples', 'summary.json'); await fs.writeFile(samplesPath, JSON.stringify(samplesResult.summary, null, 2), 'utf8'); logSh(`💾 Résumé échantillons sauvegardé: ${samplesPath}`, 'DEBUG'); } /** * ✅ NOUVEAU: Sauvegarde les données d'évaluations */ async saveEvaluationsData(evaluationsResult) { const evaluationsPath = path.join(this.validationDir, 'results', 'evaluations.json'); await fs.writeFile(evaluationsPath, JSON.stringify(evaluationsResult, null, 2), 'utf8'); logSh(`💾 Évaluations sauvegardées: ${evaluationsPath}`, 'DEBUG'); } /** * ✅ MODIFIÉ: Génère le rapport de validation (avec évaluations LLM) */ async generateReport(pipelineResult, samplesResult, evaluationsResult = null) { const reportPath = path.join(this.validationDir, 'report.json'); const report = { validationId: this.validationId, timestamp: new Date().toISOString(), status: 'completed', pipeline: { name: pipelineResult.metadata.pipelineName, totalSteps: pipelineResult.metadata.totalSteps, successfulSteps: pipelineResult.metadata.successfulSteps, totalDuration: pipelineResult.metadata.totalDuration }, versions: { count: pipelineResult.versionPaths.length, paths: pipelineResult.versionPaths }, samples: { total: samplesResult.summary.totalSamples, titles: samplesResult.summary.titles, content: samplesResult.summary.content, faqs: samplesResult.summary.faqs }, evaluations: evaluationsResult ? { totalEvaluations: evaluationsResult.aggregated.overall.totalEvaluations, overallScore: evaluationsResult.aggregated.overall.avgScore, byVersion: evaluationsResult.aggregated.byVersion, byCriteria: evaluationsResult.aggregated.byCriteria, details: `Voir ${path.join('results', 'evaluations.json')} pour détails complets` } : null }; await fs.writeFile(reportPath, JSON.stringify(report, null, 2), 'utf8'); logSh(`📊 Rapport validation généré: ${reportPath}`, 'INFO'); if (evaluationsResult) { logSh(` 🎯 Score global: ${evaluationsResult.aggregated.overall.avgScore}/10`, 'INFO'); } return report; } /** * Met à jour le statut de progression * ✅ PHASE 3: Avec broadcast WebSocket */ updateProgress(phase, message, percentage) { this.progress = { phase, message, percentage }; logSh(`📈 [${percentage}%] ${phase}: ${message}`, 'INFO'); // ✅ PHASE 3: Broadcast via WebSocket si disponible if (this.broadcastCallback) { try { this.broadcastCallback({ type: 'validation_progress', validationId: this.validationId, status: this.status, progress: { phase, message, percentage }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`⚠️ Erreur broadcast WebSocket: ${error.message}`, 'WARN'); } } } /** * Obtient le statut actuel */ getStatus() { return { validationId: this.validationId, status: this.status, progress: this.progress }; } /** * Reset l'état */ reset() { this.validationId = null; this.validationDir = null; this.csvData = null; // ✅ NOUVEAU: Reset csvData this.executor.reset(); this.samplingEngine.reset(); this.criteriaEvaluator.resetCache(); // ✅ NOUVEAU: Reset cache évaluations this.status = 'idle'; this.progress = { phase: null, message: '', percentage: 0 }; } } module.exports = { ValidatorCore, VALIDATION_PRESETS };