## 🎯 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>
313 lines
10 KiB
JavaScript
313 lines
10 KiB
JavaScript
// ========================================
|
||
// FICHIER: SpellingErrors.js
|
||
// RESPONSABILITÉ: Fautes d'orthographe et grammaire réalistes
|
||
// Probabilité MINUSCULE (1-3%) pour réalisme maximal
|
||
// ========================================
|
||
|
||
const { logSh } = require('../ErrorReporting');
|
||
|
||
/**
|
||
* FAUTES D'ORTHOGRAPHE COURANTES EN FRANÇAIS
|
||
* Basées sur erreurs humaines fréquentes
|
||
*/
|
||
const COMMON_SPELLING_ERRORS = [
|
||
// Doubles consonnes manquantes
|
||
{ correct: 'appeler', wrong: 'apeler' },
|
||
{ correct: 'apparaître', wrong: 'aparaître' },
|
||
{ correct: 'occurrence', wrong: 'occurence' },
|
||
{ correct: 'connexion', wrong: 'connection' },
|
||
{ correct: 'professionnel', wrong: 'profesionnel' },
|
||
{ correct: 'efficace', wrong: 'éficace' },
|
||
{ correct: 'différent', wrong: 'différant' },
|
||
{ correct: 'développement', wrong: 'dévelopement' },
|
||
|
||
// Accents oubliés/inversés
|
||
{ correct: 'élément', wrong: 'element' },
|
||
{ correct: 'système', wrong: 'systeme' },
|
||
{ correct: 'intéressant', wrong: 'interessant' },
|
||
{ correct: 'qualité', wrong: 'qualite' },
|
||
{ correct: 'créer', wrong: 'creer' },
|
||
{ correct: 'dépôt', wrong: 'depot' },
|
||
|
||
// Homophones
|
||
{ correct: 'et', wrong: 'est', context: 'coord' }, // et/est
|
||
{ correct: 'a', wrong: 'à', context: 'verb' }, // a/à
|
||
{ correct: 'ce', wrong: 'se', context: 'demo' }, // ce/se
|
||
{ correct: 'leur', wrong: 'leurs', context: 'sing' }, // leur/leurs
|
||
{ correct: 'son', wrong: 'sont', context: 'poss' }, // son/sont
|
||
|
||
// Terminaisons -é/-er
|
||
{ correct: 'utilisé', wrong: 'utiliser', context: 'past' },
|
||
{ correct: 'développé', wrong: 'développer', context: 'past' },
|
||
{ correct: 'créé', wrong: 'créer', context: 'past' },
|
||
|
||
// Pluriels oubliés
|
||
{ correct: 'les éléments', wrong: 'les élément' },
|
||
{ correct: 'des solutions', wrong: 'des solution' },
|
||
{ correct: 'nos services', wrong: 'nos service' }
|
||
];
|
||
|
||
/**
|
||
* ERREURS DE GRAMMAIRE COURANTES
|
||
*/
|
||
const COMMON_GRAMMAR_ERRORS = [
|
||
// Accord sujet-verbe oublié
|
||
{ correct: /nous (sommes|avons|faisons)/gi, wrong: (match) => match.replace(/sommes|avons|faisons/, m => m.slice(0, -1)) },
|
||
{ correct: /ils (sont|ont|font)/gi, wrong: (match) => match.replace(/sont|ont|font/, m => m === 'sont' ? 'est' : m === 'ont' ? 'a' : 'fait') },
|
||
|
||
// Participes passés non accordés
|
||
{ correct: /les solutions sont (\w+)ées/gi, wrong: (match) => match.replace(/ées/, 'é') },
|
||
{ correct: /qui sont (\w+)és/gi, wrong: (match) => match.replace(/és/, 'é') },
|
||
|
||
// Virgules manquantes
|
||
{ correct: /(Néanmoins|Cependant|Toutefois|Par ailleurs),/gi, wrong: (match) => match.replace(',', '') },
|
||
{ correct: /(Ainsi|Donc|En effet),/gi, wrong: (match) => match.replace(',', '') }
|
||
];
|
||
|
||
/**
|
||
* FAUTES DE FRAPPE RÉALISTES
|
||
* Touches proches sur clavier AZERTY
|
||
*/
|
||
const TYPO_ERRORS = [
|
||
{ correct: 'q', wrong: 'a' }, // Touches adjacentes
|
||
{ correct: 's', wrong: 'd' },
|
||
{ correct: 'e', wrong: 'r' },
|
||
{ correct: 'o', wrong: 'p' },
|
||
{ correct: 'i', wrong: 'u' },
|
||
{ correct: 'n', wrong: 'b' },
|
||
{ correct: 'm', wrong: 'n' }
|
||
];
|
||
|
||
/**
|
||
* INJECTION FAUTES D'ORTHOGRAPHE
|
||
* Probabilité TRÈS FAIBLE (1-2%) - MAXIMUM 1 FAUTE PAR TEXTE
|
||
* @param {string} content - Contenu à modifier
|
||
* @param {number} intensity - Intensité (0-1)
|
||
* @param {object} tracker - HumanSimulationTracker instance (optionnel)
|
||
* @returns {object} - { content, modifications }
|
||
*/
|
||
function injectSpellingErrors(content, intensity = 0.5, tracker = null) {
|
||
if (!content || typeof content !== 'string') {
|
||
return { content, modifications: 0 };
|
||
}
|
||
|
||
// ✅ LIMITE 1 FAUTE: Vérifier avec tracker
|
||
if (tracker && !tracker.canApplySpellingError()) {
|
||
logSh(`🚫 Faute spelling bloquée: déjà ${tracker.spellingErrorsApplied} faute(s) appliquée(s)`, 'DEBUG');
|
||
return { content, modifications: 0 };
|
||
}
|
||
|
||
let modified = content;
|
||
let count = 0;
|
||
|
||
// Probabilité MINUSCULE: 1-2% base × intensité
|
||
const spellingErrorChance = 0.01 * intensity; // 1% max
|
||
|
||
logSh(`🔤 Injection fautes orthographe (chance: ${(spellingErrorChance * 100).toFixed(1)}%)`, 'DEBUG');
|
||
|
||
// Parcourir les fautes courantes - STOPPER APRÈS PREMIÈRE FAUTE
|
||
for (const error of COMMON_SPELLING_ERRORS) {
|
||
if (count > 0) break; // ✅ STOPPER après 1 faute
|
||
|
||
if (Math.random() < spellingErrorChance) {
|
||
// Vérifier présence du mot correct
|
||
const regex = new RegExp(`\\b${error.correct}\\b`, 'gi');
|
||
if (modified.match(regex)) {
|
||
// Remplacer UNE SEULE occurrence (pas toutes)
|
||
modified = modified.replace(regex, error.wrong);
|
||
count++;
|
||
|
||
// ✅ Enregistrer dans tracker
|
||
if (tracker) {
|
||
tracker.trackSpellingError();
|
||
}
|
||
|
||
logSh(` 📝 Faute ortho: "${error.correct}" → "${error.wrong}"`, 'DEBUG');
|
||
}
|
||
}
|
||
}
|
||
|
||
return { content: modified, modifications: count };
|
||
}
|
||
|
||
/**
|
||
* INJECTION FAUTES DE GRAMMAIRE
|
||
* Probabilité TRÈS FAIBLE (0.5-1%)
|
||
* @param {string} content - Contenu à modifier
|
||
* @param {number} intensity - Intensité (0-1)
|
||
* @returns {object} - { content, modifications }
|
||
*/
|
||
function injectGrammarErrors(content, intensity = 0.5) {
|
||
if (!content || typeof content !== 'string') {
|
||
return { content, modifications: 0 };
|
||
}
|
||
|
||
let modified = content;
|
||
let count = 0;
|
||
|
||
// Probabilité MINUSCULE: 0.5% base × intensité
|
||
const grammarErrorChance = 0.005 * intensity; // 0.5% max
|
||
|
||
logSh(`📐 Injection fautes grammaire (chance: ${(grammarErrorChance * 100).toFixed(1)}%)`, 'DEBUG');
|
||
|
||
// Virgules manquantes (plus fréquent)
|
||
if (Math.random() < grammarErrorChance * 3) { // 1.5% max
|
||
const commaPattern = /(Néanmoins|Cependant|Toutefois|Par ailleurs|Ainsi|Donc),/gi;
|
||
if (modified.match(commaPattern)) {
|
||
modified = modified.replace(commaPattern, (match) => match.replace(',', ''));
|
||
count++;
|
||
logSh(` 📝 Virgule oubliée après connecteur`, 'DEBUG');
|
||
}
|
||
}
|
||
|
||
// Accords sujet-verbe (rare)
|
||
if (Math.random() < grammarErrorChance) {
|
||
const subjectVerbPattern = /nous (sommes|avons|faisons)/gi;
|
||
if (modified.match(subjectVerbPattern)) {
|
||
modified = modified.replace(subjectVerbPattern, (match) => {
|
||
return match.replace(/sommes|avons|faisons/, m => {
|
||
if (m === 'sommes') return 'est';
|
||
if (m === 'avons') return 'a';
|
||
return 'fait';
|
||
});
|
||
});
|
||
count++;
|
||
logSh(` 📝 Accord sujet-verbe incorrect`, 'DEBUG');
|
||
}
|
||
}
|
||
|
||
return { content: modified, modifications: count };
|
||
}
|
||
|
||
/**
|
||
* INJECTION FAUTES DE FRAPPE
|
||
* Probabilité ULTRA MINUSCULE (0.1-0.5%)
|
||
* @param {string} content - Contenu à modifier
|
||
* @param {number} intensity - Intensité (0-1)
|
||
* @returns {object} - { content, modifications }
|
||
*/
|
||
function injectTypoErrors(content, intensity = 0.5) {
|
||
if (!content || typeof content !== 'string') {
|
||
return { content, modifications: 0 };
|
||
}
|
||
|
||
let modified = content;
|
||
let count = 0;
|
||
|
||
// Probabilité ULTRA MINUSCULE: 0.1% base × intensité
|
||
const typoChance = 0.001 * intensity; // 0.1% max
|
||
|
||
if (Math.random() > typoChance) {
|
||
return { content: modified, modifications: count };
|
||
}
|
||
|
||
logSh(`⌨️ Injection faute de frappe (chance: ${(typoChance * 100).toFixed(2)}%)`, 'DEBUG');
|
||
|
||
// Sélectionner UN mot au hasard
|
||
const words = modified.split(/\s+/);
|
||
if (words.length === 0) return { content: modified, modifications: count };
|
||
|
||
const targetWordIndex = Math.floor(Math.random() * words.length);
|
||
let targetWord = words[targetWordIndex];
|
||
|
||
// Appliquer UNE faute de frappe (touche adjacente)
|
||
if (targetWord.length > 3) { // Seulement sur mots > 3 lettres
|
||
const charIndex = Math.floor(Math.random() * targetWord.length);
|
||
const char = targetWord[charIndex].toLowerCase();
|
||
|
||
// Trouver remplacement touche adjacente
|
||
const typoError = TYPO_ERRORS.find(t => t.correct === char);
|
||
if (typoError) {
|
||
const newWord = targetWord.substring(0, charIndex) + typoError.wrong + targetWord.substring(charIndex + 1);
|
||
words[targetWordIndex] = newWord;
|
||
modified = words.join(' ');
|
||
count++;
|
||
logSh(` ⌨️ Faute de frappe: "${targetWord}" → "${newWord}"`, 'DEBUG');
|
||
}
|
||
}
|
||
|
||
return { content: modified, modifications: count };
|
||
}
|
||
|
||
/**
|
||
* APPLICATION COMPLÈTE FAUTES
|
||
* Orchestre tous les types de fautes
|
||
* @param {string} content - Contenu à modifier
|
||
* @param {object} options - { intensity, spellingEnabled, grammarEnabled, typoEnabled, tracker }
|
||
* @returns {object} - { content, modifications }
|
||
*/
|
||
function applySpellingErrors(content, options = {}) {
|
||
if (!content || typeof content !== 'string') {
|
||
return { content, modifications: 0 };
|
||
}
|
||
|
||
const {
|
||
intensity = 0.5,
|
||
spellingEnabled = true,
|
||
grammarEnabled = true,
|
||
typoEnabled = false, // Désactivé par défaut (trop risqué)
|
||
tracker = null
|
||
} = options;
|
||
|
||
let modified = content;
|
||
let totalModifications = 0;
|
||
|
||
// 1. Fautes d'orthographe (1-2% chance) - MAX 1 FAUTE
|
||
if (spellingEnabled) {
|
||
const spellingResult = injectSpellingErrors(modified, intensity, tracker);
|
||
modified = spellingResult.content;
|
||
totalModifications += spellingResult.modifications;
|
||
}
|
||
|
||
// 2. Fautes de grammaire (0.5-1% chance)
|
||
if (grammarEnabled) {
|
||
const grammarResult = injectGrammarErrors(modified, intensity);
|
||
modified = grammarResult.content;
|
||
totalModifications += grammarResult.modifications;
|
||
}
|
||
|
||
// 3. Fautes de frappe (0.1% chance - ULTRA RARE)
|
||
if (typoEnabled) {
|
||
const typoResult = injectTypoErrors(modified, intensity);
|
||
modified = typoResult.content;
|
||
totalModifications += typoResult.modifications;
|
||
}
|
||
|
||
if (totalModifications > 0) {
|
||
logSh(`✅ Fautes injectées: ${totalModifications} modification(s)`, 'DEBUG');
|
||
}
|
||
|
||
return {
|
||
content: modified,
|
||
modifications: totalModifications
|
||
};
|
||
}
|
||
|
||
/**
|
||
* OBTENIR STATISTIQUES FAUTES
|
||
*/
|
||
function getSpellingErrorStats() {
|
||
return {
|
||
totalSpellingErrors: COMMON_SPELLING_ERRORS.length,
|
||
totalGrammarErrors: COMMON_GRAMMAR_ERRORS.length,
|
||
totalTypoErrors: TYPO_ERRORS.length,
|
||
defaultProbabilities: {
|
||
spelling: '1-2%',
|
||
grammar: '0.5-1%',
|
||
typo: '0.1%'
|
||
}
|
||
};
|
||
}
|
||
|
||
// ============= EXPORTS =============
|
||
module.exports = {
|
||
applySpellingErrors,
|
||
injectSpellingErrors,
|
||
injectGrammarErrors,
|
||
injectTypoErrors,
|
||
getSpellingErrorStats,
|
||
COMMON_SPELLING_ERRORS,
|
||
COMMON_GRAMMAR_ERRORS,
|
||
TYPO_ERRORS
|
||
};
|