seo-generator-server/lib/validation/SamplingEngine.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

176 lines
5.2 KiB
JavaScript

/**
* SamplingEngine.js
*
* Moteur d'échantillonnage pour Pipeline Validator
* Extrait automatiquement des échantillons représentatifs du contenu généré
*/
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const fs = require('fs').promises;
const path = require('path');
/**
* Classe SamplingEngine
*/
class SamplingEngine {
constructor() {
this.samples = {
titles: [],
content: [],
faqs: []
};
}
/**
* Extrait les échantillons depuis les versions sauvegardées
* @param {Array<string>} versionPaths - Chemins des fichiers JSON versions
* @returns {Object} - Échantillons avec leurs versions
*/
async extractSamples(versionPaths) {
return tracer.run('SamplingEngine.extractSamples', async () => {
logSh(`📊 Démarrage échantillonnage: ${versionPaths.length} versions`, 'INFO');
// Charger la version finale pour identifier les échantillons
const finalVersionPath = versionPaths.find(p => p.includes('v2.0.json'));
if (!finalVersionPath) {
throw new Error('Version finale v2.0.json introuvable');
}
const finalContent = await this.loadVersion(finalVersionPath);
const allTags = Object.keys(finalContent);
logSh(` 📋 ${allTags.length} balises trouvées dans version finale`, 'DEBUG');
// Catégoriser les balises automatiquement
const titleTags = allTags.filter(tag => tag.includes('T'));
const contentTags = allTags.filter(tag => tag.includes('MC') || tag.includes('L')).slice(0, 4);
const faqTags = allTags.filter(tag => tag.includes('FAQ')).slice(0, 4);
logSh(` ✓ Catégorisation: ${titleTags.length} titres, ${contentTags.length} contenus, ${faqTags.length} FAQ`, 'INFO');
// Extraire versions pour chaque échantillon
const samplesData = {};
// Titres
for (const tag of titleTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'title';
this.samples.titles.push(tag);
}
// Contenus
for (const tag of contentTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'content';
this.samples.content.push(tag);
}
// FAQ
for (const tag of faqTags) {
samplesData[tag] = await this.extractVersionsForTag(tag, versionPaths);
samplesData[tag].type = 'faq';
this.samples.faqs.push(tag);
}
const totalSamples = titleTags.length + contentTags.length + faqTags.length;
logSh(`✅ Échantillonnage terminé: ${totalSamples} échantillons extraits`, 'INFO');
return {
samples: samplesData,
summary: {
totalSamples,
titles: titleTags.length,
content: contentTags.length,
faqs: faqTags.length
}
};
}, { versionsCount: versionPaths.length });
}
/**
* Extrait les versions d'une balise à travers toutes les étapes
* @param {string} tag - Balise à extraire
* @param {Array<string>} versionPaths - Chemins des versions
* @returns {Object} - Versions de la balise
*/
async extractVersionsForTag(tag, versionPaths) {
const versions = {};
for (const versionPath of versionPaths) {
try {
const content = await this.loadVersion(versionPath);
const versionName = path.basename(versionPath, '.json');
// Stocker le contenu de cette balise pour cette version
versions[versionName] = content[tag] || "[Non disponible à cette étape]";
} catch (error) {
logSh(`⚠️ Erreur lecture version ${versionPath}: ${error.message}`, 'WARN');
versions[path.basename(versionPath, '.json')] = "[Erreur lecture]";
}
}
return {
tag,
versions
};
}
/**
* Charge un fichier version JSON
* @param {string} versionPath - Chemin du fichier
* @returns {Object} - Contenu JSON
*/
async loadVersion(versionPath) {
try {
const data = await fs.readFile(versionPath, 'utf8');
return JSON.parse(data);
} catch (error) {
logSh(`❌ Erreur chargement version ${versionPath}: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Sauvegarde les échantillons dans un fichier
* @param {Object} samplesData - Données échantillons
* @param {string} outputPath - Chemin de sauvegarde
*/
async saveSamples(samplesData, outputPath) {
try {
await fs.writeFile(outputPath, JSON.stringify(samplesData, null, 2), 'utf8');
logSh(`💾 Échantillons sauvegardés: ${outputPath}`, 'DEBUG');
} catch (error) {
logSh(`❌ Erreur sauvegarde échantillons: ${error.message}`, 'ERROR');
throw error;
}
}
/**
* Obtient le résumé des échantillons
*/
getSummary() {
return {
titles: this.samples.titles,
content: this.samples.content,
faqs: this.samples.faqs,
total: this.samples.titles.length + this.samples.content.length + this.samples.faqs.length
};
}
/**
* Reset l'état
*/
reset() {
this.samples = {
titles: [],
content: [],
faqs: []
};
}
}
module.exports = { SamplingEngine };