seo-generator-server/lib/pattern-breaking/MicroEnhancements.js
StillHammer 2fc31c12aa feat(pattern-breaking): Correctifs 1-7 user feedback + protection binômes avancée
## Correctifs Majeurs

### Correctifs 1-4 (Session 1)
- Réduction insertions temporelles: 0.8 → 0.05 (-94%)
- Protection 18 binômes basiques (esthétique+praticité, etc.)
- Retrait "Ajoutons que" des connecteurs de découpage
- Validation expressions fixes (En effet, Plus la, etc.)

### Correctifs 5-6 (Session 2)
- Protection compléments de nom: +14 binômes + 2 patterns regex dynamiques
- Tracking connecteurs répétitifs: limite 2× par connecteur (21 surveillés)
- Comptage automatique usage existant dans texte
- Diversification automatique alternatives

### Bonus
- Élimination "du coup" de tous contextes (trop familier B2B)
- Total 32 binômes protégés (vs 18 avant)

## Fichiers Modifiés

**Pattern Breaking Core:**
- lib/pattern-breaking/PatternBreakingCore.js (DEFAULT_CONFIG optimisé)
- lib/pattern-breaking/PatternBreakingLayers.js (mode professionnel)
- lib/pattern-breaking/MicroEnhancements.js (NOUVEAU + binômes + regex)
- lib/pattern-breaking/SyntaxVariations.js (binômes + regex + validation)
- lib/pattern-breaking/NaturalConnectors.js (tracking répétition)

**Documentation:**
- CHANGELOG_USER_FEEDBACK_FIX.md (correctifs 1-4)
- CHANGELOG_CORRECTIFS_5_6.md (correctifs 5-6)
- CHANGELOG_PROFESSIONAL_MODE.md (mode pro)
- CHANGELOG_GLOBAL_IMPROVEMENTS.md (améliorations globales)
- HANDOFF_NOTES.md (notes passation complètes)
- docs/PATTERN_BREAKING_PROFESSIONAL_MODE.md
- docs/MICRO_ENHANCEMENTS.md

## Résultats Tests

- Tests user feedback: 7/7 (100%) 
- Tests full text: 3/3 intensités (100%) 
- Suite complète: 20/21 stacks (95%) 
- Pipeline 4 phases: PASS 
- **Total: 97% tests réussis**

## Métriques Amélioration

| Métrique | Avant | Après | Gain |
|----------|-------|-------|------|
| Qualité globale | 92% | 96% | +4pp |
| Insertions inappropriées | 5-8/texte | 0-1/texte | -87% |
| Binômes préservés | 60% | 100% | +67% |
| Connecteurs répétés 3×+ | 60% | 5% | -92% |
| "du coup" en B2B | 15% | 0% | -100% |

## Breaking Changes

Aucun - Rétrocompatibilité 100%

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-15 00:39:29 +08:00

468 lines
14 KiB
JavaScript

// ========================================
// FICHIER: MicroEnhancements.js
// RESPONSABILITÉ: Micro-améliorations subtiles (phrases courtes, ponctuation)
// Variations très légères pour plus de naturel
// ========================================
const { logSh } = require('../ErrorReporting');
/**
* MICRO-PHRASES D'INSERTION (2-3 mots)
* Petites incises naturelles qui cassent la monotonie
*/
const MICRO_INSERTIONS = {
// Incises temporelles
temporal: [
'aujourd\'hui',
'actuellement',
'de nos jours',
'désormais',
'dorénavant'
],
// Incises de renforcement (début de phrase)
reinforcement: [
'En effet',
'Effectivement',
'Bien sûr',
'Naturellement',
'Évidemment'
],
// Incises de nuance
nuance: [
'sans doute',
'bien entendu',
'en général',
'le plus souvent',
'dans l\'ensemble',
'sans aucun doute', // ✅ Nouveau
'il faut dire', // ✅ Nouveau
'à noter' // ✅ Nouveau
],
// Transitions courtes
transition: [
'par exemple',
'notamment',
'entre autres',
'en particulier',
'qui plus est', // ✅ Nouveau
'point important', // ✅ Nouveau
'à souligner' // ✅ Nouveau
]
};
/**
* VARIATIONS DE PONCTUATION
* Remplacement point par point-virgule ou deux-points dans certains cas
*/
const PUNCTUATION_PATTERNS = [
// Point → Point-virgule (pour lier deux phrases courtes apparentées)
{
pattern: /\. ([A-ZÉÈÊ][a-zéèêàùô]{2,20}) (est|sont|permet|offre|assure|garantit|reste|propose)/,
replacement: ' ; $1 $2',
probability: 0.25,
description: 'Liaison phrases courtes apparentées'
},
// Point → Deux-points (avant explication/liste)
{
pattern: /\. (Ces|Cette|Ce|Votre|Notre|Les) ([a-zéèêàùô\s]{5,40}) (sont|est|offre|offrent|permet|garantit|assure)/,
replacement: ' : $1 $2 $3',
probability: 0.2,
description: 'Introduction explication'
}
];
/**
* APPLICATION MICRO-INSERTIONS
* @param {string} text - Texte à enrichir
* @param {object} options - { intensity, maxInsertions }
* @returns {object} - { content, insertions }
*/
function applyMicroInsertions(text, options = {}) {
const config = {
intensity: 0.3,
maxInsertions: 2,
...options
};
if (!text || text.trim().length === 0) {
return { content: text, insertions: 0 };
}
let modified = text;
let insertions = 0;
try {
const sentences = modified.split(/\. (?=[A-Z])/);
if (sentences.length < 3) {
return { content: text, insertions: 0 }; // Texte trop court
}
// Insertion en début de phrase (après le premier tiers)
if (Math.random() < config.intensity && insertions < config.maxInsertions) {
const targetIndex = Math.floor(sentences.length / 3);
const reinforcements = MICRO_INSERTIONS.reinforcement;
const chosen = reinforcements[Math.floor(Math.random() * reinforcements.length)];
// Vérifier que la phrase ne commence pas déjà par une incise
if (!sentences[targetIndex].match(/^(En effet|Effectivement|Bien sûr|Naturellement)/)) {
sentences[targetIndex] = chosen + ', ' + sentences[targetIndex].toLowerCase();
insertions++;
logSh(` ✨ Micro-insertion: "${chosen}"`, 'DEBUG');
}
}
// Insertion temporelle (milieu du texte) - ✅ DRASTIQUEMENT RÉDUIT
// Probabilité réduite de 0.8 → 0.05 (-94%) car souvent inapproprié
if (Math.random() < config.intensity * 0.05 && insertions < config.maxInsertions) {
const targetIndex = Math.floor(sentences.length / 2);
const temporals = MICRO_INSERTIONS.temporal;
const chosen = temporals[Math.floor(Math.random() * temporals.length)];
// Insérer après le premier mot de la phrase
const words = sentences[targetIndex].split(' ');
// ✅ VALIDATION: Ne pas insérer dans expressions fixes ou comparatifs
const firstWords = words.slice(0, 3).join(' ').toLowerCase();
const forbiddenPatterns = ['plus la', 'plus le', 'en effet', 'leur ', 'c\'est'];
const isForbidden = forbiddenPatterns.some(pattern => firstWords.includes(pattern));
if (words.length > 5 && !isForbidden) { // ✅ Augmenté de 3→5 mots minimum
words.splice(1, 0, chosen + ',');
sentences[targetIndex] = words.join(' ');
insertions++;
logSh(` 🕐 Insertion temporelle: "${chosen}"`, 'DEBUG');
}
}
// Insertion nuance (dernier tiers)
if (Math.random() < config.intensity * 0.6 && insertions < config.maxInsertions) {
const targetIndex = Math.floor(sentences.length * 2 / 3);
const nuances = MICRO_INSERTIONS.nuance;
const chosen = nuances[Math.floor(Math.random() * nuances.length)];
// Insérer après une virgule existante
if (sentences[targetIndex].includes(',')) {
sentences[targetIndex] = sentences[targetIndex].replace(',', `, ${chosen},`);
insertions++;
logSh(` 💭 Insertion nuance: "${chosen}"`, 'DEBUG');
}
}
modified = sentences.join('. ');
} catch (error) {
logSh(`⚠️ Erreur micro-insertions: ${error.message}`, 'WARNING');
return { content: text, insertions: 0 };
}
return {
content: modified,
insertions
};
}
/**
* APPLICATION VARIATIONS PONCTUATION
* @param {string} text - Texte à ponctuer
* @param {object} options - { intensity, maxVariations }
* @returns {object} - { content, variations }
*/
function applyPunctuationVariations(text, options = {}) {
const config = {
intensity: 0.2,
maxVariations: 2,
...options
};
if (!text || text.trim().length === 0) {
return { content: text, variations: 0 };
}
let modified = text;
let variations = 0;
try {
PUNCTUATION_PATTERNS.forEach(punctPattern => {
if (variations >= config.maxVariations) return;
const matches = modified.match(punctPattern.pattern);
if (matches && Math.random() < (config.intensity * punctPattern.probability)) {
// Appliquer UNE SEULE fois (première occurrence)
modified = modified.replace(punctPattern.pattern, punctPattern.replacement);
variations++;
logSh(` 📍 Ponctuation: ${punctPattern.description}`, 'DEBUG');
}
});
} catch (error) {
logSh(`⚠️ Erreur variations ponctuation: ${error.message}`, 'WARNING');
return { content: text, variations: 0 };
}
return {
content: modified,
variations
};
}
/**
* BINÔMES COURANTS À PRÉSERVER
* Paires de mots qui doivent rester ensemble
*/
const COMMON_BINOMES = [
// Binômes avec "et"
'esthétique et praticité',
'esthétique et pratique',
'style et durabilité',
'design et fonctionnalité',
'élégance et performance',
'qualité et prix',
'rapidité et efficacité',
'simplicité et efficacité',
'confort et sécurité',
'robustesse et légèreté',
'durabilité et résistance',
'performance et fiabilité',
'innovation et tradition',
'modernité et authenticité',
'sur mesure et fiable',
'faciles à manipuler et à installer',
// ✅ NOUVEAU: Compléments de nom
'son éclat et sa lisibilité',
'son éclat et sa',
'sa lisibilité et son',
'votre adresse et votre',
'leur durabilité et leur',
'notre gamme et nos',
// ✅ NOUVEAU: Couples nom + complément descriptif
'personnalisation et élégance',
'qualité et performance',
'résistance et esthétique',
'praticité et design',
'fonctionnalité et style',
'efficacité et confort',
'solidité et légèreté',
'authenticité et modernité'
];
/**
* PATTERNS REGEX POUR DÉTECTER COMPLÉMENTS DE NOM
*/
const COMPLEMENT_PATTERNS = [
// Possessifs + nom + et + possessif + nom
/\b(son|sa|ses|votre|vos|leur|leurs|notre|nos)\s+\w+\s+et\s+(son|sa|ses|votre|vos|leur|leurs|notre|nos)\s+\w+\b/gi,
// Nom abstrait + et + nom abstrait
/\b(personnalisation|durabilité|résistance|esthétique|élégance|qualité|performance|praticité|fonctionnalité|efficacité|solidité|authenticité|modernité)\s+et\s+(personnalisation|durabilité|résistance|esthétique|élégance|qualité|performance|praticité|fonctionnalité|efficacité|solidité|authenticité|modernité)\b/gi
];
/**
* VALIDATION BINÔMES
* Vérifie si une partie de texte contient un binôme à préserver (liste + regex)
*/
function containsBinome(text) {
const lowerText = text.toLowerCase();
// 1. Vérifier liste statique
const hasStaticBinome = COMMON_BINOMES.some(binome =>
lowerText.includes(binome.toLowerCase())
);
if (hasStaticBinome) {
return true;
}
// 2. Vérifier patterns regex dynamiques
const hasDynamicPattern = COMPLEMENT_PATTERNS.some(pattern => {
pattern.lastIndex = 0;
return pattern.test(text);
});
return hasDynamicPattern;
}
/**
* RESTRUCTURATION LÉGÈRE
* Découpage/fusion très occasionnel (probabilité faible)
* @param {string} text - Texte à restructurer
* @param {object} options - { intensity, maxRestructures }
* @returns {object} - { content, restructures }
*/
function applyLightRestructuring(text, options = {}) {
const config = {
intensity: 0.2,
maxRestructures: 1, // Maximum 1 restructuration
...options
};
if (!text || text.trim().length === 0) {
return { content: text, restructures: 0 };
}
let modified = text;
let restructures = 0;
try {
const sentences = modified.split('. ');
// DÉCOUPAGE : Si une phrase très longue existe (>150 chars)
if (Math.random() < config.intensity * 0.5 && restructures < config.maxRestructures) {
for (let i = 0; i < sentences.length; i++) {
if (sentences[i].length > 150) {
// Chercher un point de découpe naturel
const cutPoints = [
{ pattern: /, car (.+)/, replacement: '. En effet, $1', connector: 'car' },
{ pattern: /, donc (.+)/, replacement: '. Ainsi, $1', connector: 'donc' },
{ pattern: / et (.{30,})/, replacement: '. Également, $1', connector: 'et long' }
];
for (const cutPoint of cutPoints) {
if (sentences[i].match(cutPoint.pattern)) {
sentences[i] = sentences[i].replace(cutPoint.pattern, cutPoint.replacement);
restructures++;
logSh(` ✂️ Découpage léger: "${cutPoint.connector}"`, 'DEBUG');
break;
}
}
break; // Une seule restructuration
}
}
}
// FUSION : Si deux phrases très courtes consécutives (<40 chars chacune)
if (Math.random() < config.intensity * 0.4 && restructures < config.maxRestructures) {
for (let i = 0; i < sentences.length - 1; i++) {
const current = sentences[i];
const next = sentences[i + 1];
if (current.length < 40 && next && next.length < 50) {
// ✅ VALIDATION: Ne pas fusionner si binôme détecté
const combined = current + ' ' + next;
if (containsBinome(combined)) {
logSh(` ⚠️ Fusion évitée: binôme détecté`, 'DEBUG');
continue;
}
// Fusion avec connecteur neutre + originaux
const connectors = [', et', ', puis', ' ;', ', tout en', ', sans oublier', ', qui plus est,'];
const connector = connectors[Math.floor(Math.random() * connectors.length)];
sentences[i] = current + connector + ' ' + next.toLowerCase();
sentences.splice(i + 1, 1);
restructures++;
logSh(` 🔗 Fusion légère: "${connector}"`, 'DEBUG');
break; // Une seule restructuration
}
}
}
modified = sentences.join('. ');
} catch (error) {
logSh(`⚠️ Erreur restructuration légère: ${error.message}`, 'WARNING');
return { content: text, restructures: 0 };
}
return {
content: modified,
restructures
};
}
/**
* APPLICATION COMPLÈTE MICRO-ENHANCEMENTS
* Combine insertions + ponctuation + restructuration légère
* @param {string} text - Texte à améliorer
* @param {object} options - Options globales
* @returns {object} - { content, stats }
*/
function applyMicroEnhancements(text, options = {}) {
const config = {
intensity: 0.3,
enableInsertions: true,
enablePunctuation: true,
enableRestructuring: true,
...options
};
if (!text || text.trim().length === 0) {
return { content: text, stats: { insertions: 0, punctuations: 0, restructures: 0 } };
}
let modified = text;
const stats = {
insertions: 0,
punctuations: 0,
restructures: 0,
total: 0
};
try {
// 1. Micro-insertions (si activé)
if (config.enableInsertions) {
const insertResult = applyMicroInsertions(modified, {
intensity: config.intensity,
maxInsertions: 2
});
modified = insertResult.content;
stats.insertions = insertResult.insertions;
}
// 2. Variations ponctuation (si activé) - AVANT restructuration pour préserver patterns
if (config.enablePunctuation) {
const punctResult = applyPunctuationVariations(modified, {
intensity: config.intensity * 1.5, // ✅ Augmenté 0.8 → 1.5 pour plus de chances
maxVariations: 1
});
modified = punctResult.content;
stats.punctuations = punctResult.variations;
}
// 3. Restructuration légère (si activé)
if (config.enableRestructuring) {
const restructResult = applyLightRestructuring(modified, {
intensity: config.intensity * 0.6,
maxRestructures: 1
});
modified = restructResult.content;
stats.restructures = restructResult.restructures;
}
stats.total = stats.insertions + stats.punctuations + stats.restructures;
// ✅ NETTOYAGE FINAL : Corriger espaces parasites avant ponctuation
modified = modified
.replace(/\s+\./g, '.') // Espace avant point
.replace(/\s+,/g, ',') // Espace avant virgule
.replace(/\s+;/g, ';') // Espace avant point-virgule
.replace(/\s+:/g, ':') // Espace avant deux-points
.replace(/\.\s+\./g, '. ') // Double points
.replace(/\s+/g, ' ') // Multiples espaces
.trim();
} catch (error) {
logSh(`❌ Erreur micro-enhancements: ${error.message}`, 'WARNING');
return { content: text, stats };
}
return {
content: modified,
stats
};
}
// ============= EXPORTS =============
module.exports = {
applyMicroEnhancements,
applyMicroInsertions,
applyPunctuationVariations,
applyLightRestructuring,
MICRO_INSERTIONS,
PUNCTUATION_PATTERNS
};