seo-generator-server/lib/selective-enhancement/SelectiveLayers.js
StillHammer 4f60de68d6 Fix BatchProcessor initialization and add comprehensive test suite
- Fix BatchProcessor constructor to avoid server blocking during startup
- Add comprehensive integration tests for all modular combinations
- Enhance CLAUDE.md documentation with new test commands
- Update SelectiveLayers configuration for better LLM allocation
- Add AutoReporter system for test automation
- Include production workflow validation tests

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 14:17:49 +08:00

544 lines
17 KiB
JavaScript

// ========================================
// SELECTIVE LAYERS - COUCHES COMPOSABLES
// Responsabilité: Stacks prédéfinis et couches adaptatives pour selective enhancement
// Architecture: Composable layers avec orchestration intelligente
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const { applySelectiveLayer } = require('./SelectiveCore');
/**
* STACKS PRÉDÉFINIS SELECTIVE ENHANCEMENT
*/
const PREDEFINED_STACKS = {
// Stack léger - Amélioration technique uniquement
lightEnhancement: {
name: 'lightEnhancement',
description: 'Amélioration technique légère avec OpenAI',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.7 }
],
layersCount: 1
},
// Stack standard - Technique + Transitions
standardEnhancement: {
name: 'standardEnhancement',
description: 'Amélioration technique et style (OpenAI + Mistral)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 0.9 },
{ type: 'style', llm: 'mistral', intensity: 0.8 }
],
layersCount: 2
},
// Stack complet - Toutes couches séquentielles
fullEnhancement: {
name: 'fullEnhancement',
description: 'Enhancement complet multi-LLM (OpenAI + Mistral)',
layers: [
{ type: 'technical', llm: 'openai', intensity: 1.0 },
{ type: 'style', llm: 'mistral', intensity: 0.8 }
],
layersCount: 2
},
// Stack personnalité - Style prioritaire
personalityFocus: {
name: 'personalityFocus',
description: 'Focus personnalité et style avec Mistral + technique légère',
layers: [
{ type: 'style', llm: 'mistral', intensity: 1.2 },
{ type: 'technical', llm: 'openai', intensity: 0.6 }
],
layersCount: 2
},
// Stack fluidité - Style prioritaire
fluidityFocus: {
name: 'fluidityFocus',
description: 'Focus style et technique avec Mistral + OpenAI',
layers: [
{ type: 'style', llm: 'mistral', intensity: 1.1 },
{ type: 'technical', llm: 'openai', intensity: 0.7 }
],
layersCount: 2
}
};
/**
* APPLIQUER STACK PRÉDÉFINI
*/
async function applyPredefinedStack(content, stackName, config = {}) {
return await tracer.run('SelectiveLayers.applyPredefinedStack()', async () => {
const stack = PREDEFINED_STACKS[stackName];
if (!stack) {
throw new Error(`Stack selective prédéfini inconnu: ${stackName}. Disponibles: ${Object.keys(PREDEFINED_STACKS).join(', ')}`);
}
await tracer.annotate({
selectivePredefinedStack: true,
stackName,
layersCount: stack.layersCount,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`📦 APPLICATION STACK SELECTIVE: ${stack.name} (${stack.layersCount} couches)`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Description: ${stack.description}`, 'INFO');
try {
let currentContent = content;
const stackStats = {
stackName,
layers: [],
totalModifications: 0,
totalDuration: 0,
success: true
};
// Appliquer chaque couche séquentiellement
for (let i = 0; i < stack.layers.length; i++) {
const layer = stack.layers[i];
try {
logSh(` 🔧 Couche ${i + 1}/${stack.layersCount}: ${layer.type} (${layer.llm})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
stackStats.totalModifications += layerResult.stats.elementsEnhanced;
stackStats.totalDuration += layerResult.stats.duration;
logSh(` ✅ Couche ${layer.type}: ${layerResult.stats.elementsEnhanced} améliorations`, 'DEBUG');
} catch (layerError) {
logSh(` ❌ Couche ${layer.type} échouée: ${layerError.message}`, 'ERROR');
stackStats.layers.push({
order: i + 1,
type: layer.type,
llm: layer.llm,
error: layerError.message,
duration: 0,
success: false
});
// Continuer avec les autres couches
}
}
const duration = Date.now() - startTime;
const successfulLayers = stackStats.layers.filter(l => l.success).length;
logSh(`✅ STACK SELECTIVE ${stackName}: ${successfulLayers}/${stack.layersCount} couches | ${stackStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Stack selective appliqué', { ...stackStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...stackStats, totalDuration: duration },
original: content,
stackApplied: stackName
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ STACK SELECTIVE ${stackName} ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { stackName, error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), stackName, config });
}
/**
* APPLIQUER COUCHES ADAPTATIVES
*/
async function applyAdaptiveLayers(content, config = {}) {
return await tracer.run('SelectiveLayers.applyAdaptiveLayers()', async () => {
const {
maxIntensity = 1.0,
analysisThreshold = 0.4,
csvData = null
} = config;
await tracer.annotate({
selectiveAdaptiveLayers: true,
maxIntensity,
analysisThreshold,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🧠 APPLICATION COUCHES ADAPTATIVES SELECTIVE`, 'INFO');
logSh(` 📊 ${Object.keys(content).length} éléments | Seuil: ${analysisThreshold}`, 'INFO');
try {
// 1. Analyser besoins de chaque type de couche
const needsAnalysis = await analyzeSelectiveNeeds(content, csvData);
logSh(` 📋 Analyse besoins: Tech=${needsAnalysis.technical.score.toFixed(2)} | Trans=${needsAnalysis.transitions.score.toFixed(2)} | Style=${needsAnalysis.style.score.toFixed(2)}`, 'DEBUG');
// 2. Déterminer couches à appliquer selon scores
const layersToApply = [];
if (needsAnalysis.technical.needed && needsAnalysis.technical.score > analysisThreshold) {
layersToApply.push({
type: 'technical',
llm: 'openai',
intensity: Math.min(maxIntensity, needsAnalysis.technical.score * 1.2),
priority: 1
});
}
// Transitions layer removed - Gemini disabled
if (needsAnalysis.style.needed && needsAnalysis.style.score > analysisThreshold) {
layersToApply.push({
type: 'style',
llm: 'mistral',
intensity: Math.min(maxIntensity, needsAnalysis.style.score),
priority: 3
});
}
if (layersToApply.length === 0) {
logSh(`✅ COUCHES ADAPTATIVES: Aucune amélioration nécessaire`, 'INFO');
return {
content,
stats: {
adaptive: true,
layersApplied: 0,
analysisOnly: true,
duration: Date.now() - startTime
}
};
}
// 3. Appliquer couches par ordre de priorité
layersToApply.sort((a, b) => a.priority - b.priority);
logSh(` 🎯 Couches sélectionnées: ${layersToApply.map(l => `${l.type}(${l.intensity.toFixed(1)})`).join(' → ')}`, 'INFO');
let currentContent = content;
const adaptiveStats = {
layersAnalyzed: 3,
layersApplied: layersToApply.length,
layers: [],
totalModifications: 0,
adaptive: true
};
for (const layer of layersToApply) {
try {
logSh(` 🔧 Couche adaptative: ${layer.type} (intensité: ${layer.intensity.toFixed(1)})`, 'DEBUG');
const layerResult = await applySelectiveLayer(currentContent, {
...config,
layerType: layer.type,
llmProvider: layer.llm,
intensity: layer.intensity,
analysisMode: true
});
currentContent = layerResult.content;
adaptiveStats.layers.push({
type: layer.type,
llm: layer.llm,
intensity: layer.intensity,
elementsEnhanced: layerResult.stats.elementsEnhanced,
duration: layerResult.stats.duration,
success: !layerResult.stats.fallback
});
adaptiveStats.totalModifications += layerResult.stats.elementsEnhanced;
} catch (layerError) {
logSh(` ❌ Couche adaptative ${layer.type} échouée: ${layerError.message}`, 'ERROR');
adaptiveStats.layers.push({
type: layer.type,
error: layerError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulLayers = adaptiveStats.layers.filter(l => l.success).length;
logSh(`✅ COUCHES ADAPTATIVES: ${successfulLayers}/${layersToApply.length} appliquées | ${adaptiveStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Couches adaptatives appliquées', { ...adaptiveStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...adaptiveStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ COUCHES ADAPTATIVES ÉCHOUÉES après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { adaptive: true, error: error.message, duration },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), config });
}
/**
* PIPELINE COUCHES PERSONNALISÉ
*/
async function applyLayerPipeline(content, layerSequence, config = {}) {
return await tracer.run('SelectiveLayers.applyLayerPipeline()', async () => {
if (!Array.isArray(layerSequence) || layerSequence.length === 0) {
throw new Error('Séquence de couches invalide ou vide');
}
await tracer.annotate({
selectiveLayerPipeline: true,
pipelineLength: layerSequence.length,
elementsCount: Object.keys(content).length
});
const startTime = Date.now();
logSh(`🔄 PIPELINE COUCHES SELECTIVE PERSONNALISÉ: ${layerSequence.length} étapes`, 'INFO');
try {
let currentContent = content;
const pipelineStats = {
pipelineLength: layerSequence.length,
steps: [],
totalModifications: 0,
success: true
};
for (let i = 0; i < layerSequence.length; i++) {
const step = layerSequence[i];
try {
logSh(` 📍 Étape ${i + 1}/${layerSequence.length}: ${step.type} (${step.llm || 'auto'})`, 'DEBUG');
const stepResult = await applySelectiveLayer(currentContent, {
...config,
...step
});
currentContent = stepResult.content;
pipelineStats.steps.push({
order: i + 1,
...step,
elementsEnhanced: stepResult.stats.elementsEnhanced,
duration: stepResult.stats.duration,
success: !stepResult.stats.fallback
});
pipelineStats.totalModifications += stepResult.stats.elementsEnhanced;
} catch (stepError) {
logSh(` ❌ Étape ${i + 1} échouée: ${stepError.message}`, 'ERROR');
pipelineStats.steps.push({
order: i + 1,
...step,
error: stepError.message,
success: false
});
}
}
const duration = Date.now() - startTime;
const successfulSteps = pipelineStats.steps.filter(s => s.success).length;
logSh(`✅ PIPELINE SELECTIVE: ${successfulSteps}/${layerSequence.length} étapes | ${pipelineStats.totalModifications} modifications (${duration}ms)`, 'INFO');
await tracer.event('Pipeline selective appliqué', { ...pipelineStats, totalDuration: duration });
return {
content: currentContent,
stats: { ...pipelineStats, totalDuration: duration },
original: content
};
} catch (error) {
const duration = Date.now() - startTime;
logSh(`❌ PIPELINE SELECTIVE ÉCHOUÉ après ${duration}ms: ${error.message}`, 'ERROR');
return {
content,
stats: { error: error.message, duration, success: false },
original: content,
fallback: true
};
}
}, { content: Object.keys(content), layerSequence, config });
}
// ============= HELPER FUNCTIONS =============
/**
* Analyser besoins selective enhancement
*/
async function analyzeSelectiveNeeds(content, csvData) {
const analysis = {
technical: { needed: false, score: 0, elements: [] },
transitions: { needed: false, score: 0, elements: [] },
style: { needed: false, score: 0, elements: [] }
};
// Analyser chaque élément pour tous types de besoins
Object.entries(content).forEach(([tag, text]) => {
// Analyse technique (import depuis SelectiveCore logic)
const technicalNeed = assessTechnicalNeed(text, csvData);
if (technicalNeed.score > 0.3) {
analysis.technical.needed = true;
analysis.technical.score += technicalNeed.score;
analysis.technical.elements.push({ tag, score: technicalNeed.score });
}
// Analyse transitions
const transitionNeed = assessTransitionNeed(text);
if (transitionNeed.score > 0.3) {
analysis.transitions.needed = true;
analysis.transitions.score += transitionNeed.score;
analysis.transitions.elements.push({ tag, score: transitionNeed.score });
}
// Analyse style
const styleNeed = assessStyleNeed(text, csvData?.personality);
if (styleNeed.score > 0.3) {
analysis.style.needed = true;
analysis.style.score += styleNeed.score;
analysis.style.elements.push({ tag, score: styleNeed.score });
}
});
// Normaliser scores
const elementCount = Object.keys(content).length;
analysis.technical.score = analysis.technical.score / elementCount;
analysis.transitions.score = analysis.transitions.score / elementCount;
analysis.style.score = analysis.style.score / elementCount;
return analysis;
}
/**
* Évaluer besoin technique (simplifié de SelectiveCore)
*/
function assessTechnicalNeed(content, csvData) {
let score = 0;
// Manque de termes techniques spécifiques
if (csvData?.mc0) {
const technicalTerms = ['dibond', 'pmma', 'aluminium', 'fraisage', 'impression', 'gravure'];
const foundTerms = technicalTerms.filter(term => content.toLowerCase().includes(term));
if (foundTerms.length === 0 && content.length > 100) {
score += 0.4;
}
}
// Vocabulaire générique
const genericWords = ['produit', 'solution', 'service', 'qualité'];
const genericCount = genericWords.filter(word => content.toLowerCase().includes(word)).length;
if (genericCount > 2) score += 0.3;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin transitions (simplifié)
*/
function assessTransitionNeed(content) {
const sentences = content.split(/[.!?]+/).filter(s => s.trim().length > 10);
if (sentences.length < 2) return { score: 0 };
let score = 0;
// Connecteurs répétitifs
const connectors = ['par ailleurs', 'en effet', 'de plus'];
let repetitions = 0;
connectors.forEach(connector => {
const matches = (content.match(new RegExp(connector, 'gi')) || []);
if (matches.length > 1) repetitions++;
});
if (repetitions > 1) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Évaluer besoin style (simplifié)
*/
function assessStyleNeed(content, personality) {
let score = 0;
if (!personality) {
score += 0.2;
return { score };
}
// Style générique
const personalityWords = (personality.vocabulairePref || '').toLowerCase().split(',');
const personalityFound = personalityWords.some(word =>
word.trim() && content.toLowerCase().includes(word.trim())
);
if (!personalityFound && content.length > 50) score += 0.4;
return { score: Math.min(1, score) };
}
/**
* Obtenir stacks disponibles
*/
function getAvailableStacks() {
return Object.values(PREDEFINED_STACKS);
}
module.exports = {
// Main functions
applyPredefinedStack,
applyAdaptiveLayers,
applyLayerPipeline,
// Utils
getAvailableStacks,
analyzeSelectiveNeeds,
// Constants
PREDEFINED_STACKS
};