## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
496 lines
16 KiB
JavaScript
496 lines
16 KiB
JavaScript
/**
|
|
* 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 };
|