seo-generator-server/lib/validation/ValidatorCore.js
StillHammer 9a2ef7da2b feat(human-simulation): Système d'erreurs graduées procédurales + anti-répétition complet
## 🎯 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>
2025-10-14 01:06:28 +08:00

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 };