- 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>
544 lines
17 KiB
JavaScript
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
|
|
}; |