## 🎯 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>
675 lines
24 KiB
JavaScript
675 lines
24 KiB
JavaScript
/**
|
|
* PipelineExecutor.js
|
|
*
|
|
* Moteur d'exécution des pipelines modulaires flexibles.
|
|
* Orchestre l'exécution séquentielle des modules avec gestion d'état.
|
|
*/
|
|
|
|
const { logSh } = require('../ErrorReporting');
|
|
const { tracer } = require('../trace');
|
|
const { PipelineDefinition } = require('./PipelineDefinition');
|
|
const { getPersonalities, readInstructionsData, selectPersonalityWithAI } = require('../BrainConfig');
|
|
const { extractElements, buildSmartHierarchy } = require('../ElementExtraction');
|
|
const { generateMissingKeywords, generateMissingSheetVariables } = require('../MissingKeywords');
|
|
const { injectGeneratedContent } = require('../ContentAssembly');
|
|
const { saveGeneratedArticleOrganic } = require('../ArticleStorage');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
// Modules d'exécution
|
|
const { generateSimple } = require('../selective-enhancement/SelectiveUtils');
|
|
const { applySelectiveLayer } = require('../selective-enhancement/SelectiveCore');
|
|
const { applyPredefinedStack: applySelectiveStack } = require('../selective-enhancement/SelectiveLayers');
|
|
const { SmartTouchCore } = require('../selective-smart-touch/SmartTouchCore'); // ✅ NOUVEAU: SelectiveSmartTouch
|
|
const { applyAdversarialLayer } = require('../adversarial-generation/AdversarialCore');
|
|
const { applyPredefinedStack: applyAdversarialStack } = require('../adversarial-generation/AdversarialLayers');
|
|
const { applyHumanSimulationLayer } = require('../human-simulation/HumanSimulationCore');
|
|
const { applyPredefinedSimulation } = require('../human-simulation/HumanSimulationLayers');
|
|
const { applyPatternBreakingLayer } = require('../pattern-breaking/PatternBreakingCore');
|
|
const { applyPatternBreakingStack } = require('../pattern-breaking/PatternBreakingLayers');
|
|
|
|
/**
|
|
* Classe PipelineExecutor
|
|
*/
|
|
class PipelineExecutor {
|
|
constructor() {
|
|
this.currentContent = null;
|
|
this.executionLog = [];
|
|
this.checkpoints = [];
|
|
this.versionHistory = []; // ✅ Historique des versions sauvegardées
|
|
this.parentArticleId = null; // ✅ ID parent pour versioning
|
|
this.csvData = null; // ✅ Données CSV pour sauvegarde
|
|
this.finalElements = null; // ✅ Éléments extraits pour assemblage
|
|
this.versionPaths = []; // ✅ NOUVEAU: Chemins des versions JSON locales
|
|
this.metadata = {
|
|
startTime: null,
|
|
endTime: null,
|
|
totalDuration: 0,
|
|
personality: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Exécute un pipeline complet
|
|
*/
|
|
async execute(pipelineConfig, rowNumber, options = {}) {
|
|
return tracer.run('PipelineExecutor.execute', async () => {
|
|
logSh(`🚀 Démarrage pipeline "${pipelineConfig.name}" (${pipelineConfig.pipeline.length} étapes)`, 'INFO');
|
|
|
|
// Validation
|
|
const validation = PipelineDefinition.validate(pipelineConfig);
|
|
if (!validation.valid) {
|
|
throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`);
|
|
}
|
|
|
|
this.metadata.startTime = Date.now();
|
|
this.executionLog = [];
|
|
this.checkpoints = [];
|
|
this.versionHistory = []; // ✅ Reset version history
|
|
this.parentArticleId = null; // ✅ Reset parent ID
|
|
this.versionPaths = []; // ✅ Reset version paths
|
|
|
|
// ✅ NOUVEAU: Créer outputDir si saveAllVersions activé
|
|
if (options.saveAllVersions && options.outputDir) {
|
|
await fs.mkdir(options.outputDir, { recursive: true });
|
|
logSh(`📁 Dossier versions créé: ${options.outputDir}`, 'DEBUG');
|
|
}
|
|
|
|
// Charger les données
|
|
const csvData = await this.loadData(rowNumber);
|
|
this.csvData = csvData; // ✅ Stocker pour sauvegarde
|
|
|
|
// Exécuter les étapes
|
|
const enabledSteps = pipelineConfig.pipeline.filter(s => s.enabled !== false);
|
|
|
|
for (let i = 0; i < enabledSteps.length; i++) {
|
|
const step = enabledSteps[i];
|
|
|
|
try {
|
|
logSh(`▶ Étape ${step.step}/${pipelineConfig.pipeline.length}: ${step.module} (${step.mode})`, 'INFO');
|
|
|
|
const stepStartTime = Date.now();
|
|
const result = await this.executeStep(step, csvData, options);
|
|
const stepDuration = Date.now() - stepStartTime;
|
|
|
|
// Log l'étape
|
|
this.executionLog.push({
|
|
step: step.step,
|
|
module: step.module,
|
|
mode: step.mode,
|
|
intensity: step.intensity,
|
|
duration: stepDuration,
|
|
modifications: result.modifications || 0,
|
|
success: true,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Mise à jour du contenu
|
|
if (result.content) {
|
|
this.currentContent = result.content;
|
|
}
|
|
|
|
// Checkpoint si demandé
|
|
if (step.saveCheckpoint) {
|
|
this.checkpoints.push({
|
|
step: step.step,
|
|
content: this.currentContent,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
logSh(`💾 Checkpoint sauvegardé (étape ${step.step})`, 'DEBUG');
|
|
}
|
|
|
|
// ✅ Sauvegarde Google Sheets si activée
|
|
if (options.saveIntermediateSteps && this.currentContent) {
|
|
await this.saveStepVersion(step, result.modifications || 0, pipelineConfig.name);
|
|
}
|
|
|
|
// ✅ NOUVEAU: Sauvegarde JSON locale si saveAllVersions activé
|
|
if (options.saveAllVersions && options.outputDir && this.currentContent) {
|
|
await this.saveVersionJSON(step, i, options.outputDir);
|
|
}
|
|
|
|
logSh(`✔ Étape ${step.step} terminée (${stepDuration}ms, ${result.modifications || 0} modifs)`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`✖ Erreur étape ${step.step}: ${error.message}`, 'ERROR');
|
|
|
|
this.executionLog.push({
|
|
step: step.step,
|
|
module: step.module,
|
|
mode: step.mode,
|
|
success: false,
|
|
error: error.message,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
// Propager l'erreur ou continuer selon options
|
|
if (options.stopOnError !== false) {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
this.metadata.endTime = Date.now();
|
|
this.metadata.totalDuration = this.metadata.endTime - this.metadata.startTime;
|
|
|
|
// ✅ NOUVEAU: Sauvegarder version finale v2.0 si saveAllVersions activé
|
|
if (options.saveAllVersions && options.outputDir && this.currentContent) {
|
|
const finalVersionPath = path.join(options.outputDir, 'v2.0.json');
|
|
await fs.writeFile(finalVersionPath, JSON.stringify(this.currentContent, null, 2), 'utf8');
|
|
this.versionPaths.push(finalVersionPath);
|
|
logSh(`💾 Version finale v2.0 sauvegardée: ${finalVersionPath}`, 'DEBUG');
|
|
}
|
|
|
|
logSh(`✅ Pipeline terminé: ${this.metadata.totalDuration}ms`, 'INFO');
|
|
|
|
return {
|
|
success: true,
|
|
finalContent: this.currentContent,
|
|
executionLog: this.executionLog,
|
|
checkpoints: this.checkpoints,
|
|
versionHistory: this.versionHistory, // ✅ Inclure version history
|
|
versionPaths: this.versionPaths, // ✅ NOUVEAU: Chemins des versions JSON
|
|
metadata: {
|
|
...this.metadata,
|
|
pipelineName: pipelineConfig.name,
|
|
totalSteps: enabledSteps.length,
|
|
successfulSteps: this.executionLog.filter(l => l.success).length
|
|
}
|
|
};
|
|
}, { pipelineName: pipelineConfig.name, rowNumber });
|
|
}
|
|
|
|
/**
|
|
* Charge les données depuis Google Sheets
|
|
*/
|
|
async loadData(rowNumber) {
|
|
return tracer.run('PipelineExecutor.loadData', async () => {
|
|
const csvData = await readInstructionsData(rowNumber);
|
|
|
|
// Charger personnalité si besoin
|
|
const personalities = await getPersonalities();
|
|
const personality = await selectPersonalityWithAI(
|
|
csvData.mc0,
|
|
csvData.t0,
|
|
personalities
|
|
);
|
|
|
|
csvData.personality = personality;
|
|
this.metadata.personality = personality.nom;
|
|
|
|
logSh(`📊 Données chargées: ${csvData.mc0}, personnalité: ${personality.nom}`, 'DEBUG');
|
|
|
|
return csvData;
|
|
}, { rowNumber });
|
|
}
|
|
|
|
/**
|
|
* Exécute une étape individuelle
|
|
*/
|
|
async executeStep(step, csvData, options) {
|
|
return tracer.run(`PipelineExecutor.executeStep.${step.module}`, async () => {
|
|
|
|
switch (step.module) {
|
|
case 'generation':
|
|
return await this.runGeneration(step, csvData);
|
|
|
|
case 'selective':
|
|
return await this.runSelective(step, csvData);
|
|
|
|
case 'smarttouch': // ✅ NOUVEAU: SelectiveSmartTouch
|
|
return await this.runSmartTouch(step, csvData);
|
|
|
|
case 'adversarial':
|
|
return await this.runAdversarial(step, csvData);
|
|
|
|
case 'human':
|
|
return await this.runHumanSimulation(step, csvData);
|
|
|
|
case 'pattern':
|
|
return await this.runPatternBreaking(step, csvData);
|
|
|
|
default:
|
|
throw new Error(`Module inconnu: ${step.module}`);
|
|
}
|
|
|
|
}, { step: step.step, module: step.module, mode: step.mode });
|
|
}
|
|
|
|
/**
|
|
* Exécute la génération initiale
|
|
*/
|
|
async runGeneration(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runGeneration', async () => {
|
|
|
|
if (this.currentContent) {
|
|
logSh('⚠️ Contenu déjà généré, génération ignorée', 'WARN');
|
|
return { content: this.currentContent, modifications: 0 };
|
|
}
|
|
|
|
// 🆕 Étape 0: Générer les variables Google Sheets manquantes (MC+1_5, T+1_6, etc.)
|
|
logSh('🔄 Vérification variables Google Sheets...', 'DEBUG');
|
|
const updatedCsvData = await generateMissingSheetVariables(csvData.xmlTemplate, csvData);
|
|
// Mettre à jour csvData pour les étapes suivantes
|
|
Object.assign(csvData, updatedCsvData);
|
|
|
|
// Étape 1: Extraire les éléments depuis le template XML (avec csvData complet)
|
|
const elements = await extractElements(csvData.xmlTemplate, csvData);
|
|
logSh(`✓ Extraction: ${elements.length} éléments extraits`, 'DEBUG');
|
|
|
|
// Étape 2: Générer les mots-clés manquants (titres, textes, FAQ)
|
|
const finalElements = await generateMissingKeywords(elements, csvData);
|
|
this.finalElements = finalElements; // ✅ Stocker pour sauvegarde
|
|
|
|
// Étape 3: Construire la hiérarchie
|
|
const elementsArray = Array.isArray(finalElements) ? finalElements :
|
|
(finalElements && typeof finalElements === 'object') ? Object.values(finalElements) : [];
|
|
const hierarchy = await buildSmartHierarchy(elementsArray);
|
|
logSh(`✓ Hiérarchie: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
|
|
|
|
// Étape 4: Génération simple avec LLM configurable
|
|
const llmProvider = step.parameters?.llmProvider || 'claude-sonnet-4-5';
|
|
const result = await generateSimple(hierarchy, csvData, { llmProvider });
|
|
|
|
logSh(`✓ Génération: ${Object.keys(result.content || {}).length} éléments créés avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content,
|
|
modifications: Object.keys(result.content || {}).length
|
|
};
|
|
|
|
}, { mode: step.mode });
|
|
}
|
|
|
|
/**
|
|
* Exécute l'enhancement sélectif
|
|
*/
|
|
async runSelective(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runSelective', async () => {
|
|
|
|
if (!this.currentContent) {
|
|
throw new Error('Aucun contenu à améliorer. Génération requise avant selective enhancement');
|
|
}
|
|
|
|
// Configuration de la couche
|
|
const llmProvider = step.parameters?.llmProvider || 'gpt-4o-mini';
|
|
const config = {
|
|
csvData,
|
|
personality: csvData.personality,
|
|
intensity: step.intensity || 1.0,
|
|
llmProvider: llmProvider,
|
|
...step.parameters
|
|
};
|
|
|
|
let result;
|
|
|
|
// Utiliser le stack si c'est un mode prédéfini
|
|
const predefinedStacks = ['lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus', 'adaptive'];
|
|
|
|
if (predefinedStacks.includes(step.mode)) {
|
|
result = await applySelectiveStack(this.currentContent, step.mode, config);
|
|
} else {
|
|
// Sinon utiliser la couche directe
|
|
result = await applySelectiveLayer(this.currentContent, config);
|
|
}
|
|
|
|
logSh(`✓ Selective: modifications appliquées avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content || result,
|
|
modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount
|
|
};
|
|
|
|
}, { mode: step.mode, intensity: step.intensity });
|
|
}
|
|
|
|
/**
|
|
* ✅ NOUVEAU: Exécute SelectiveSmartTouch (Analyse→Améliorations ciblées)
|
|
*/
|
|
async runSmartTouch(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runSmartTouch', async () => {
|
|
|
|
if (!this.currentContent) {
|
|
throw new Error('Aucun contenu à améliorer. Génération requise avant SmartTouch');
|
|
}
|
|
|
|
// ✅ Extraire llmProvider depuis parameters (comme les autres modules)
|
|
const llmProvider = step.parameters?.llmProvider || 'gpt-4o-mini'; // Default gpt-4o-mini pour analyse objective
|
|
|
|
logSh(`🧠 SMART TOUCH: Mode ${step.mode}, LLM: ${llmProvider}`, 'INFO');
|
|
|
|
// ✅ NOUVEAU: Charger personnalité spécifique si demandée
|
|
let effectiveCsvData = { ...csvData };
|
|
if (step.parameters?.personalityName) {
|
|
const personalities = await getPersonalities();
|
|
const requestedPersonality = personalities.find(p => p.nom === step.parameters.personalityName);
|
|
|
|
if (requestedPersonality) {
|
|
effectiveCsvData.personality = requestedPersonality;
|
|
logSh(`🎭 Personnalité override: ${requestedPersonality.nom} (au lieu de ${csvData.personality?.nom})`, 'INFO');
|
|
} else {
|
|
logSh(`⚠️ Personnalité "${step.parameters.personalityName}" non trouvée, utilisation de ${csvData.personality?.nom}`, 'WARN');
|
|
}
|
|
}
|
|
|
|
// Instancier SmartTouchCore
|
|
const smartTouch = new SmartTouchCore();
|
|
|
|
// Configuration
|
|
const config = {
|
|
mode: step.mode || 'full', // full, analysis_only, technical_only, style_only, readability_only
|
|
intensity: step.intensity || 1.0,
|
|
csvData: effectiveCsvData, // ✅ Utiliser csvData avec personnalité potentiellement overridée
|
|
llmProvider: llmProvider, // ✅ Passer le LLM choisi dans pipeline
|
|
skipAnalysis: step.parameters?.skipAnalysis || false,
|
|
layersOrder: step.parameters?.layersOrder || ['technical', 'style', 'readability'],
|
|
charsPerExpression: step.parameters?.charsPerExpression || 4000 // ✅ NOUVEAU: Budget dynamique
|
|
};
|
|
|
|
// Exécuter SmartTouch
|
|
const result = await smartTouch.apply(this.currentContent, config);
|
|
|
|
logSh(`✓ SmartTouch: ${result.modifications || 0} modifications appliquées avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content || result,
|
|
modifications: result.modifications || 0,
|
|
analysisResults: result.analysisResults // Inclure analyse pour debugging
|
|
};
|
|
|
|
}, { mode: step.mode, intensity: step.intensity });
|
|
}
|
|
|
|
/**
|
|
* Exécute l'adversarial generation
|
|
*/
|
|
async runAdversarial(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runAdversarial', async () => {
|
|
|
|
if (!this.currentContent) {
|
|
throw new Error('Aucun contenu à traiter. Génération requise avant adversarial');
|
|
}
|
|
|
|
if (step.mode === 'none') {
|
|
logSh('Adversarial mode = none, ignoré', 'DEBUG');
|
|
return { content: this.currentContent, modifications: 0 };
|
|
}
|
|
|
|
const llmProvider = step.parameters?.llmProvider || 'gemini-pro';
|
|
const config = {
|
|
csvData,
|
|
detectorTarget: step.parameters?.detector || 'general',
|
|
method: step.parameters?.method || 'regeneration',
|
|
intensity: step.intensity || 1.0,
|
|
llmProvider: llmProvider
|
|
};
|
|
|
|
let result;
|
|
|
|
// Mapper les noms user-friendly vers les vrais noms de stacks
|
|
const stackMapping = {
|
|
'light': 'lightDefense',
|
|
'standard': 'standardDefense',
|
|
'heavy': 'heavyDefense',
|
|
'adaptive': 'adaptive'
|
|
};
|
|
|
|
// Utiliser le stack si c'est un mode prédéfini
|
|
if (stackMapping[step.mode]) {
|
|
const stackName = stackMapping[step.mode];
|
|
|
|
if (stackName === 'adaptive') {
|
|
// Mode adaptatif utilise la couche directe
|
|
result = await applyAdversarialLayer(this.currentContent, config);
|
|
} else {
|
|
result = await applyAdversarialStack(this.currentContent, stackName, config);
|
|
}
|
|
} else {
|
|
// Sinon utiliser la couche directe
|
|
result = await applyAdversarialLayer(this.currentContent, config);
|
|
}
|
|
|
|
logSh(`✓ Adversarial: modifications appliquées avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content || result,
|
|
modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount
|
|
};
|
|
|
|
}, { mode: step.mode, detector: step.parameters?.detector });
|
|
}
|
|
|
|
/**
|
|
* Exécute la simulation humaine
|
|
*/
|
|
async runHumanSimulation(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runHumanSimulation', async () => {
|
|
|
|
if (!this.currentContent) {
|
|
throw new Error('Aucun contenu à traiter. Génération requise avant human simulation');
|
|
}
|
|
|
|
if (step.mode === 'none') {
|
|
logSh('Human simulation mode = none, ignoré', 'DEBUG');
|
|
return { content: this.currentContent, modifications: 0 };
|
|
}
|
|
|
|
const llmProvider = step.parameters?.llmProvider || 'mistral-small';
|
|
const config = {
|
|
csvData,
|
|
personality: csvData.personality,
|
|
intensity: step.intensity || 1.0,
|
|
fatigueLevel: step.parameters?.fatigueLevel || 0.5,
|
|
errorRate: step.parameters?.errorRate || 0.3,
|
|
llmProvider: llmProvider
|
|
};
|
|
|
|
let result;
|
|
|
|
// Utiliser le stack si c'est un mode prédéfini
|
|
const predefinedModes = ['lightSimulation', 'standardSimulation', 'heavySimulation', 'adaptiveSimulation', 'personalityFocus', 'temporalFocus'];
|
|
|
|
if (predefinedModes.includes(step.mode)) {
|
|
result = await applyPredefinedSimulation(this.currentContent, step.mode, config);
|
|
} else {
|
|
// Sinon utiliser la couche directe
|
|
result = await applyHumanSimulationLayer(this.currentContent, config);
|
|
}
|
|
|
|
logSh(`✓ Human Simulation: modifications appliquées avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content || result,
|
|
modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount
|
|
};
|
|
|
|
}, { mode: step.mode, intensity: step.intensity });
|
|
}
|
|
|
|
/**
|
|
* Exécute le pattern breaking
|
|
*/
|
|
async runPatternBreaking(step, csvData) {
|
|
return tracer.run('PipelineExecutor.runPatternBreaking', async () => {
|
|
|
|
if (!this.currentContent) {
|
|
throw new Error('Aucun contenu à traiter. Génération requise avant pattern breaking');
|
|
}
|
|
|
|
if (step.mode === 'none') {
|
|
logSh('Pattern breaking mode = none, ignoré', 'DEBUG');
|
|
return { content: this.currentContent, modifications: 0 };
|
|
}
|
|
|
|
const llmProvider = step.parameters?.llmProvider || 'deepseek-chat';
|
|
const config = {
|
|
csvData,
|
|
personality: csvData.personality,
|
|
intensity: step.intensity || 1.0,
|
|
focus: step.parameters?.focus || 'both',
|
|
llmProvider: llmProvider
|
|
};
|
|
|
|
let result;
|
|
|
|
// Utiliser le stack si c'est un mode prédéfini
|
|
const predefinedModes = ['lightPatternBreaking', 'standardPatternBreaking', 'heavyPatternBreaking', 'adaptivePatternBreaking', 'syntaxFocus', 'connectorsFocus'];
|
|
|
|
if (predefinedModes.includes(step.mode)) {
|
|
result = await applyPatternBreakingStack(step.mode, this.currentContent, config);
|
|
} else {
|
|
// Sinon utiliser la couche directe
|
|
result = await applyPatternBreakingLayer(this.currentContent, config);
|
|
}
|
|
|
|
logSh(`✓ Pattern Breaking: modifications appliquées avec ${llmProvider}`, 'DEBUG');
|
|
|
|
return {
|
|
content: result.content || result,
|
|
modifications: result.modifications || 0 // ✅ CORRIGÉ: modifications au lieu de modificationsCount
|
|
};
|
|
|
|
}, { mode: step.mode, intensity: step.intensity });
|
|
}
|
|
|
|
/**
|
|
* Obtient le contenu actuel
|
|
*/
|
|
getCurrentContent() {
|
|
return this.currentContent;
|
|
}
|
|
|
|
/**
|
|
* Obtient le log d'exécution
|
|
*/
|
|
getExecutionLog() {
|
|
return this.executionLog;
|
|
}
|
|
|
|
/**
|
|
* Obtient les checkpoints sauvegardés
|
|
*/
|
|
getCheckpoints() {
|
|
return this.checkpoints;
|
|
}
|
|
|
|
/**
|
|
* Obtient les métadonnées d'exécution
|
|
*/
|
|
getMetadata() {
|
|
return this.metadata;
|
|
}
|
|
|
|
/**
|
|
* Reset l'état de l'executor
|
|
*/
|
|
reset() {
|
|
this.currentContent = null;
|
|
this.executionLog = [];
|
|
this.checkpoints = [];
|
|
this.versionHistory = [];
|
|
this.parentArticleId = null;
|
|
this.csvData = null;
|
|
this.finalElements = null;
|
|
this.versionPaths = []; // ✅ NOUVEAU: Reset version paths
|
|
this.metadata = {
|
|
startTime: null,
|
|
endTime: null,
|
|
totalDuration: 0,
|
|
personality: null
|
|
};
|
|
}
|
|
|
|
/**
|
|
* ✅ NOUVEAU: Sauvegarde une version JSON locale pour Pipeline Validator
|
|
*/
|
|
async saveVersionJSON(step, stepIndex, outputDir) {
|
|
try {
|
|
// Déterminer le nom de la version
|
|
let versionName;
|
|
if (step.module === 'generation') {
|
|
versionName = 'v1.0'; // Version initiale après génération
|
|
} else {
|
|
versionName = `v1.${stepIndex + 1}`; // v1.1, v1.2, v1.3...
|
|
}
|
|
|
|
const versionPath = path.join(outputDir, `${versionName}.json`);
|
|
|
|
// Sauvegarder le contenu actuel en JSON
|
|
await fs.writeFile(versionPath, JSON.stringify(this.currentContent, null, 2), 'utf8');
|
|
|
|
// Ajouter au tableau des versions
|
|
this.versionPaths.push(versionPath);
|
|
|
|
logSh(`💾 Version ${versionName} sauvegardée: ${versionPath}`, 'DEBUG');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur sauvegarde version JSON: ${error.message}`, 'ERROR');
|
|
// Ne pas propager l'erreur pour ne pas bloquer l'exécution
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ Sauvegarde une version intermédiaire dans Google Sheets
|
|
*/
|
|
async saveStepVersion(step, modifications, pipelineName) {
|
|
try {
|
|
if (!this.csvData || !this.finalElements) {
|
|
logSh('⚠️ Données manquantes pour sauvegarde, ignorée', 'WARN');
|
|
return;
|
|
}
|
|
|
|
// Déterminer la version basée sur le module et le nombre d'étapes
|
|
const versionNumber = `v1.${step.step}`;
|
|
const stageName = `${step.module}_${step.mode}`;
|
|
|
|
logSh(`💾 Sauvegarde ${versionNumber}: ${stageName}`, 'INFO');
|
|
|
|
// Assemblage du contenu
|
|
const xmlString = this.csvData.xmlTemplate.startsWith('<?xml')
|
|
? this.csvData.xmlTemplate
|
|
: Buffer.from(this.csvData.xmlTemplate, 'base64').toString('utf8');
|
|
|
|
await injectGeneratedContent(xmlString, this.currentContent, this.finalElements);
|
|
|
|
// Sauvegarde dans Google Sheets
|
|
const storage = await saveGeneratedArticleOrganic(
|
|
{ generatedTexts: this.currentContent },
|
|
this.csvData,
|
|
{
|
|
version: versionNumber,
|
|
stage: stageName,
|
|
source: `pipeline_${pipelineName}`,
|
|
adversarialMode: step.mode === 'adversarial' ? step.mode : 'none',
|
|
stageDescription: `${step.module} (${step.mode}) - ${modifications} modifications`,
|
|
parentArticleId: this.parentArticleId,
|
|
useVersionedSheet: true // ✅ Sauvegarder dans Generated_Articles_Versioned
|
|
}
|
|
);
|
|
|
|
// Stocker l'ID parent si c'est la première version
|
|
if (!this.parentArticleId) {
|
|
this.parentArticleId = storage.articleId;
|
|
}
|
|
|
|
// Ajouter à l'historique
|
|
this.versionHistory.push({
|
|
version: versionNumber,
|
|
stage: stageName,
|
|
articleId: storage.articleId,
|
|
length: storage.textLength,
|
|
wordCount: storage.wordCount,
|
|
modifications: modifications
|
|
});
|
|
|
|
logSh(` ✅ Sauvé ${versionNumber} - ID: ${storage.articleId}`, 'INFO');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur sauvegarde version: ${error.message}`, 'ERROR');
|
|
// Ne pas propager l'erreur pour ne pas bloquer l'exécution
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { PipelineExecutor };
|