diff --git a/CLAUDE.md b/CLAUDE.md
index e7c03e8..7952451 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -339,7 +339,7 @@ Voir `API.md` pour documentation complète avec exemples.
**Assets**: `public/` (11 web interfaces), `configs/` (saved configs/pipelines), `tools/` (logViewer, bundler, audit), `tests/` (comprehensive test suite), `.env` (credentials)
## Dependencies & Workflow Sources
-**Deps**: googleapis, axios, dotenv, express, nodemailer
+**Deps**: google-spreadsheet, google-auth-library, axios, dotenv, express, nodemailer
**Sources**: production (Google Sheets), test_random_personality, node_server
## Git Push Configuration
diff --git a/TODO.md b/TODO.md
index b1a9fb2..3956dc4 100644
--- a/TODO.md
+++ b/TODO.md
@@ -62,7 +62,67 @@ async function enhanceWithPersonalityRecovery(content, personality, attempt = 1)
---
-## 📋 PRIORITÉ 3 - AUTRES AMÉLIORATIONS
+## 📋 PRIORITÉ 3 - INTÉGRATION LITELLM POUR TRACKING COÛTS
+
+### PROBLÈME ACTUEL
+- Impossible de récupérer les crédits restants via les APIs des providers (OpenAI, Anthropic, etc.)
+- OpenAI a supprimé l'endpoint `/v1/dashboard/billing/credit_grants` pour les soldes USD
+- Anthropic n'a aucune API pour la balance (feature request ouverte depuis longtemps)
+- Pas de visibilité centralisée sur les coûts multi-providers
+
+### SOLUTION REQUISE
+**Intégrer LiteLLM comme proxy pour tracking automatique des coûts**
+
+#### Pourquoi LiteLLM :
+- ✅ **Standard de l'industrie** : Utilisé par la majorité des projets multi-LLM
+- ✅ **Support 100+ LLMs** : OpenAI, Anthropic, Google, Deepseek, Moonshot, Mistral, etc.
+- ✅ **Tracking automatique** : Intercepte tous les appels et calcule les coûts
+- ✅ **Dashboard unifié** : Vue centralisée par user/team/API key
+- ✅ **API de métriques** : Récupération programmatique des stats
+
+#### Implémentation suggérée :
+```bash
+# Installation
+pip install litellm[proxy]
+
+# Démarrage proxy
+litellm --config litellm_config.yaml
+```
+
+```yaml
+# litellm_config.yaml
+model_list:
+ - model_name: gpt-5
+ litellm_params:
+ model: openai/gpt-5
+ api_key: ${OPENAI_API_KEY}
+ - model_name: claude-sonnet-4-5
+ litellm_params:
+ model: anthropic/claude-sonnet-4-5-20250929
+ api_key: ${ANTHROPIC_API_KEY}
+ # ... autres models
+```
+
+#### Changements dans notre code :
+1. **LLMManager.js** : Router tous les appels via LiteLLM proxy (localhost:8000)
+2. **LLM Monitoring** : Récupérer les stats via l'API LiteLLM
+3. **Dashboard** : Afficher "Dépensé ce mois" au lieu de "Crédits restants"
+
+#### Alternatives évaluées :
+- **Langfuse** : Bien mais moins de models supportés
+- **Portkey** : Commercial, pas open source
+- **Helicone** : Plus basique
+- **Tracking maison** : Trop de maintenance, risque d'erreurs de calcul
+
+#### Avantages supplémentaires :
+- 🔄 **Load balancing** : Rotation automatique entre plusieurs clés API
+- 📊 **Analytics** : Métriques détaillées par endpoint/user/model
+- 🚨 **Alertes** : Notifications quand budget dépassé
+- 💾 **Caching** : Cache intelligent pour réduire les coûts
+
+---
+
+## 📋 PRIORITÉ 4 - AUTRES AMÉLIORATIONS
### A. Monitoring des échecs IA
- **Logging détaillé** : Quel LLM échoue, quand, pourquoi
diff --git a/lib/BrainConfig.js b/lib/BrainConfig.js
index ad6328b..93ae382 100644
--- a/lib/BrainConfig.js
+++ b/lib/BrainConfig.js
@@ -87,32 +87,36 @@ async function getBrainConfig(data) {
async function readInstructionsData(rowNumber = 2) {
try {
logSh(`📊 Lecture Google Sheet ligne ${rowNumber}...`, 'INFO');
-
- // NOUVEAU : Lecture directe depuis Google Sheets
- const { google } = require('googleapis');
-
- // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
+
+ // ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
+ const { GoogleSpreadsheet } = require('google-spreadsheet');
+ const { JWT } = require('google-auth-library');
+
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
- const auth = new google.auth.GoogleAuth({
+ const serviceAccountAuth = new JWT({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
- logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth', 'INFO');
-
- const sheets = google.sheets({ version: 'v4', auth });
+
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
-
- // Récupérer la ligne spécifique (A à I au minimum)
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_ID,
- range: `Instructions!A${rowNumber}:I${rowNumber}` // Ligne spécifique A-I
- });
-
- if (!response.data.values || response.data.values.length === 0) {
+ const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
+
+ await doc.loadInfo();
+ const sheet = doc.sheetsByTitle['instructions'];
+
+ if (!sheet) {
+ throw new Error('Onglet "instructions" non trouvé dans Google Sheet');
+ }
+
+ const rows = await sheet.getRows();
+ const targetRow = rows[rowNumber - 2]; // -2 car index 0 = ligne 2 du sheet
+
+ if (!targetRow) {
throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`);
}
-
- const row = response.data.values[0];
+
+ // ✅ Même format que googleapis : tableau de valeurs
+ const row = targetRow._rawData;
logSh(`✅ Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO');
const xmlTemplateValue = row[8] || '';
@@ -166,33 +170,38 @@ async function readInstructionsData(rowNumber = 2) {
async function getPersonalities() {
try {
logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO');
-
- // Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
- const { google } = require('googleapis');
+
+ // ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
+ const { GoogleSpreadsheet } = require('google-spreadsheet');
+ const { JWT } = require('google-auth-library');
+
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
- const auth = new google.auth.GoogleAuth({
+ const serviceAccountAuth = new JWT({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
- logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth (personnalités)', 'INFO');
-
- const sheets = google.sheets({ version: 'v4', auth });
+
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
-
- // Récupérer toutes les personnalités (après la ligne d'en-tête)
- const response = await sheets.spreadsheets.values.get({
- spreadsheetId: SHEET_ID,
- range: 'Personnalites!A2:O' // Colonnes A à O pour inclure les nouvelles colonnes IA
- });
-
- if (!response.data.values || response.data.values.length === 0) {
+ const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
+
+ await doc.loadInfo();
+ const sheet = doc.sheetsByTitle['Personnalites'];
+
+ if (!sheet) {
+ throw new Error('Onglet "Personnalites" non trouvé dans Google Sheet');
+ }
+
+ const rows = await sheet.getRows();
+
+ if (!rows || rows.length === 0) {
throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites');
}
-
+
const personalities = [];
-
- // Traiter chaque ligne de personnalité
- response.data.values.forEach((row, index) => {
+
+ // Traiter chaque ligne de personnalité (✅ même logique qu'avant)
+ rows.forEach((rowObj, index) => {
+ const row = rowObj._rawData; // ✅ Même format tableau que googleapis
if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A)
const personality = {
nom: row[0]?.toString().trim() || '',
diff --git a/lib/ElementExtraction.js b/lib/ElementExtraction.js
index 87a1721..6c331cf 100644
--- a/lib/ElementExtraction.js
+++ b/lib/ElementExtraction.js
@@ -5,6 +5,7 @@
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
+const { logElementsList } = require('./selective-enhancement/SelectiveUtils');
// ============= EXTRACTION PRINCIPALE =============
@@ -140,8 +141,12 @@ async function extractElements(xmlTemplate, csvData) {
await logSh(`Tag séparé: ${pureTag} → "${resolvedContent}"`, 'DEBUG');
}
-
+
await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
+
+ // 📊 DÉTAIL DES ÉLÉMENTS EXTRAITS
+ logElementsList(elements, 'ÉLÉMENTS EXTRAITS (depuis XML + Google Sheets)');
+
return elements;
} catch (error) {
@@ -228,15 +233,12 @@ async function generateAllContent(elements, csvData, xmlTemplate) {
try {
await logSh(`Élément ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
-
+
const prompt = createPromptForElement(element, csvData);
- await logSh(`Prompt créé: ${prompt}`, 'DEBUG');
-
- // 🔄 NODE.JS : Import callOpenAI depuis LLM manager
+
+ // 🔄 NODE.JS : Import callOpenAI depuis LLM manager (le prompt/réponse seront loggés par LLMManager)
const { callLLM } = require('./LLMManager');
- const content = await callLLM('openai', prompt, {}, csvData.personality);
-
- await logSh(`Contenu reçu: ${content}`, 'DEBUG');
+ const content = await callLLM('gpt-4o-mini', prompt, {}, csvData.personality);
generatedContent[element.originalTag] = content;
@@ -277,12 +279,21 @@ function parseElementStructure(element) {
// ============= HIÉRARCHIE INTELLIGENTE - ADAPTÉE =============
async function buildSmartHierarchy(elements) {
+ await logSh(`🏗️ CONSTRUCTION HIÉRARCHIE - Début avec ${elements.length} éléments`, 'INFO');
+
const hierarchy = {};
-
- elements.forEach(element => {
+
+ elements.forEach((element, index) => {
const structure = parseElementStructure(element);
const path = structure.hierarchyPath;
-
+
+ // 📊 LOG: Détailler chaque élément traité
+ logSh(` [${index + 1}/${elements.length}] ${element.name}`, 'DEBUG');
+ logSh(` 📍 Path: ${path}`, 'DEBUG');
+ logSh(` 📝 Type: ${structure.type}`, 'DEBUG');
+ logSh(` 📄 ResolvedContent: "${element.resolvedContent}"`, 'DEBUG');
+ logSh(` 📜 Instructions: "${element.instructions ? element.instructions.substring(0, 80) : 'AUCUNE'}"`, 'DEBUG');
+
if (!hierarchy[path]) {
hierarchy[path] = {
title: null,
@@ -291,26 +302,44 @@ async function buildSmartHierarchy(elements) {
children: {}
};
}
-
+
// Associer intelligemment
if (structure.type === 'Titre') {
hierarchy[path].title = structure; // Tout l'objet avec variables + instructions
+ logSh(` ✅ Assigné comme TITRE dans hiérarchie[${path}].title`, 'DEBUG');
} else if (structure.type === 'Txt') {
hierarchy[path].text = structure;
+ logSh(` ✅ Assigné comme TEXTE dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Intro') {
- hierarchy[path].text = structure;
+ hierarchy[path].text = structure;
+ logSh(` ✅ Assigné comme INTRO dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Faq') {
hierarchy[path].questions.push(structure);
+ logSh(` ✅ Ajouté comme FAQ dans hiérarchie[${path}].questions`, 'DEBUG');
}
});
-
- // ← LIGNE COMPILÉE
+
+ // 📊 LOG: Résumé de la hiérarchie construite
const mappingSummary = Object.keys(hierarchy).map(path => {
const section = hierarchy[path];
return `${path}:[T:${section.title ? '✓' : '✗'} Txt:${section.text ? '✓' : '✗'} FAQ:${section.questions.length}]`;
}).join(' | ');
-
- await logSh('Correspondances: ' + mappingSummary, 'DEBUG');
+
+ await logSh(`📊 HIÉRARCHIE CONSTRUITE: ${Object.keys(hierarchy).length} sections`, 'INFO');
+ await logSh(` ${mappingSummary}`, 'INFO');
+
+ // 📊 LOG: Détail complet d'une section exemple
+ const firstPath = Object.keys(hierarchy)[0];
+ if (firstPath) {
+ const firstSection = hierarchy[firstPath];
+ await logSh(`📋 EXEMPLE SECTION [${firstPath}]:`, 'DEBUG');
+ if (firstSection.title) {
+ await logSh(` 📌 Title.instructions: "${firstSection.title.instructions ? firstSection.title.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
+ }
+ if (firstSection.text) {
+ await logSh(` 📌 Text.instructions: "${firstSection.text.instructions ? firstSection.text.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
+ }
+ }
return hierarchy;
}
diff --git a/lib/ErrorReporting.js b/lib/ErrorReporting.js
index 641f998..367da30 100644
--- a/lib/ErrorReporting.js
+++ b/lib/ErrorReporting.js
@@ -25,47 +25,48 @@ const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
+// File destination with dated filename - JSON format
+const fileDest = pino.destination({
+ dest: logFile,
+ mkdir: true,
+ sync: false,
+ minLength: 0
+});
+
+// Console destination - Pretty format
const prettyStream = pretty({
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname',
+ destination: 1 // stdout
});
-const tee = new PassThrough();
-// Lazy loading des pipes console (évite blocage à l'import)
-let consolePipeInitialized = false;
-
-// File destination with dated filename - FORCE DEBUG LEVEL
-const fileDest = pino.destination({
- dest: logFile,
- mkdir: true,
- sync: false,
- minLength: 0 // Force immediate write even for small logs
-});
-tee.pipe(fileDest);
-
-// Custom levels for Pino to include TRACE, PROMPT, and LLM
+// Custom levels for Pino
const customLevels = {
- trace: 5, // Below debug (10)
+ trace: 5,
debug: 10,
info: 20,
- prompt: 25, // New level for prompts (between info and warn)
- llm: 26, // New level for LLM interactions (between prompt and warn)
+ prompt: 25,
+ llm: 26,
warn: 30,
error: 40,
fatal: 50
};
-// Pino logger instance with enhanced configuration and custom levels
+// ✅ Multistream: pretty sur console + JSON dans fichier (pas de duplication)
const logger = pino(
- {
- level: 'debug', // FORCE DEBUG LEVEL for file logging
+ {
+ level: 'debug',
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
customLevels: customLevels,
- useOnlyCustomLevels: true
+ useOnlyCustomLevels: true,
+ browser: { disabled: true }
},
- tee
+ pino.multistream([
+ { level: 'debug', stream: prettyStream }, // Console: pretty format
+ { level: 'debug', stream: fileDest } // Fichier: JSON format
+ ])
);
// Initialize WebSocket server (only when explicitly requested)
@@ -155,13 +156,7 @@ async function logSh(message, level = 'INFO') {
if (!wsServer) {
initWebSocketServer();
}
-
- // Initialize console pipe if needed (lazy loading)
- if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') {
- tee.pipe(prettyStream).pipe(process.stdout);
- consolePipeInitialized = true;
- }
-
+
// Convert level to lowercase for Pino
const pinoLevel = level.toLowerCase();
diff --git a/lib/LLMManager.js b/lib/LLMManager.js
index 55c99d2..6f3baba 100644
--- a/lib/LLMManager.js
+++ b/lib/LLMManager.js
@@ -11,25 +11,82 @@ const { logSh } = require('./ErrorReporting');
require('dotenv').config();
// ============= CONFIGURATION CENTRALISÉE =============
+// IDs basés sur les MODÈLES (pas les providers) pour garantir la reproductibilité
const LLM_CONFIG = {
- openai: {
+ // OpenAI Models - GPT-5 Series (August 2025)
+ 'gpt-5': {
+ provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
- model: 'gpt-4o-mini',
+ model: 'gpt-5',
+ displayName: 'GPT-5',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
- timeout: 300000, // 5 minutes
+ maxTokens: 16000, // GPT-5 utilise reasoning tokens (reasoning_effort=minimal forcé)
+ timeout: 300000,
retries: 3
},
- claude: {
+ 'gpt-5-mini': {
+ provider: 'openai',
+ apiKey: process.env.OPENAI_API_KEY,
+ endpoint: 'https://api.openai.com/v1/chat/completions',
+ model: 'gpt-5-mini',
+ displayName: 'GPT-5 Mini',
+ headers: {
+ 'Authorization': 'Bearer {API_KEY}',
+ 'Content-Type': 'application/json'
+ },
+ temperature: 0.7,
+ maxTokens: 8000, // GPT-5-mini utilise reasoning tokens (reasoning_effort=minimal forcé)
+ timeout: 300000,
+ retries: 3
+ },
+
+ 'gpt-5-nano': {
+ provider: 'openai',
+ apiKey: process.env.OPENAI_API_KEY,
+ endpoint: 'https://api.openai.com/v1/chat/completions',
+ model: 'gpt-5-nano',
+ displayName: 'GPT-5 Nano',
+ headers: {
+ 'Authorization': 'Bearer {API_KEY}',
+ 'Content-Type': 'application/json'
+ },
+ temperature: 0.7,
+ maxTokens: 4000, // GPT-5-nano utilise reasoning tokens (reasoning_effort=minimal forcé)
+ timeout: 300000,
+ retries: 3
+ },
+
+ // OpenAI Models - GPT-4o Series
+ 'gpt-4o-mini': {
+ provider: 'openai',
+ apiKey: process.env.OPENAI_API_KEY,
+ endpoint: 'https://api.openai.com/v1/chat/completions',
+ model: 'gpt-4o-mini',
+ displayName: 'GPT-4o Mini',
+ headers: {
+ 'Authorization': 'Bearer {API_KEY}',
+ 'Content-Type': 'application/json'
+ },
+ temperature: 0.7,
+ maxTokens: 6000, // Augmenté pour contraintes de longueur
+ timeout: 300000,
+ retries: 3
+ },
+
+ // Claude Models
+ 'claude-sonnet-4-5': {
+ provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY,
endpoint: 'https://api.anthropic.com/v1/messages',
- model: 'claude-sonnet-4-20250514',
+ model: 'claude-sonnet-4-5-20250929',
+ displayName: 'Claude Sonnet 4.5',
headers: {
'x-api-key': '{API_KEY}',
'Content-Type': 'application/json',
@@ -37,54 +94,78 @@ const LLM_CONFIG = {
},
temperature: 0.7,
maxTokens: 6000,
- timeout: 300000, // 5 minutes
+ timeout: 300000,
retries: 6
},
- deepseek: {
+ // Google Models
+ 'gemini-pro': {
+ provider: 'google',
+ apiKey: process.env.GOOGLE_API_KEY,
+ endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
+ model: 'gemini-pro',
+ displayName: 'Google Gemini Pro',
+ headers: {
+ 'Content-Type': 'application/json'
+ },
+ temperature: 0.7,
+ maxTokens: 6000, // Augmenté pour contraintes de longueur
+ timeout: 300000,
+ retries: 3
+ },
+
+ // Deepseek Models
+ 'deepseek-chat': {
+ provider: 'deepseek',
apiKey: process.env.DEEPSEEK_API_KEY,
endpoint: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
+ displayName: 'Deepseek Chat',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
- timeout: 300000, // 5 minutes
+ maxTokens: 6000, // Augmenté pour contraintes de longueur
+ timeout: 300000,
retries: 3
},
-
- moonshot: {
+
+ // Moonshot Models
+ 'moonshot-v1-32k': {
+ provider: 'moonshot',
apiKey: process.env.MOONSHOT_API_KEY,
endpoint: 'https://api.moonshot.ai/v1/chat/completions',
model: 'moonshot-v1-32k',
+ displayName: 'Moonshot v1 32K',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
- timeout: 300000, // 5 minutes
+ maxTokens: 6000, // Augmenté pour contraintes de longueur
+ timeout: 300000,
retries: 3
},
-
- mistral: {
+
+ // Mistral Models
+ 'mistral-small': {
+ provider: 'mistral',
apiKey: process.env.MISTRAL_API_KEY,
endpoint: 'https://api.mistral.ai/v1/chat/completions',
model: 'mistral-small-latest',
+ displayName: 'Mistral Small',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
- max_tokens: 5000,
temperature: 0.7,
- timeout: 300000, // 5 minutes
+ maxTokens: 5000,
+ timeout: 300000,
retries: 3
}
};
-// Alias pour compatibilité avec le code existant
-LLM_CONFIG.gpt4 = LLM_CONFIG.openai;
-
// ============= HELPER FUNCTIONS =============
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
@@ -114,29 +195,23 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
throw new Error(`Clé API manquante pour ${llmProvider}`);
}
- logSh(`🤖 Appel LLM: ${llmProvider.toUpperCase()} (${config.model}) | Personnalité: ${personality?.nom || 'aucune'}`, 'DEBUG');
-
- // 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
- logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
+ // 📤 LOG PROMPT (une seule fois)
+ logSh(`\n📤 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, 'PROMPT');
-
- // 📤 LOG LLM REQUEST COMPLET
- logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
- logSh(prompt, 'LLM');
-
+
// Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality);
-
+
// Effectuer l'appel avec retry logic
const response = await callWithRetry(llmProvider, requestData, config);
-
+
// Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response);
-
- // 📥 LOG LLM RESPONSE COMPLET
- logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
+
+ // 📥 LOG RESPONSE
+ logSh(`\n📥 ===== RÉPONSE REÇUE DE ${llmProvider.toUpperCase()} (${config.model}) | Durée: ${Date.now() - startTime}ms =====`, 'LLM');
logSh(content, 'LLM');
-
+
const duration = Date.now() - startTime;
logSh(`✅ ${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
@@ -158,34 +233,59 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// ============= CONSTRUCTION DES REQUÊTES =============
-function buildRequestData(provider, prompt, options, personality) {
- const config = LLM_CONFIG[provider];
+function buildRequestData(modelId, prompt, options, personality) {
+ const config = LLM_CONFIG[modelId];
const temperature = options.temperature || config.temperature;
- const maxTokens = options.maxTokens || config.maxTokens;
-
+ let maxTokens = options.maxTokens || config.maxTokens;
+
+ // GPT-5: Force minimum tokens (reasoning tokens + content tokens)
+ if (modelId.startsWith('gpt-5')) {
+ const MIN_GPT5_TOKENS = 1500; // Minimum pour reasoning + contenu
+ if (maxTokens < MIN_GPT5_TOKENS) {
+ logSh(` ⚠️ GPT-5: maxTokens augmenté de ${maxTokens} à ${MIN_GPT5_TOKENS} (minimum pour reasoning)`, 'WARNING');
+ maxTokens = MIN_GPT5_TOKENS;
+ }
+ }
+
// Construire le système prompt si personnalité fournie
- const systemPrompt = personality ?
- `Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
+ const systemPrompt = personality ?
+ `Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
'Tu es un assistant expert.';
-
- switch (provider) {
+
+ // Switch sur le PROVIDER (pas le modelId)
+ switch (config.provider) {
case 'openai':
- case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
- return {
+ // GPT-5 models use max_completion_tokens instead of max_tokens
+ const tokenField = modelId.startsWith('gpt-5') ? 'max_completion_tokens' : 'max_tokens';
+
+ // GPT-5 models only support temperature: 1 (default)
+ const requestBody = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
- max_tokens: maxTokens,
- temperature: temperature,
+ [tokenField]: maxTokens,
stream: false
};
-
- case 'claude':
+
+ // Only add temperature if NOT GPT-5 (GPT-5 only supports default temperature=1)
+ if (!modelId.startsWith('gpt-5')) {
+ requestBody.temperature = temperature;
+ }
+
+ // GPT-5: Force minimal reasoning effort to reduce reasoning tokens
+ if (modelId.startsWith('gpt-5')) {
+ requestBody.reasoning_effort = 'minimal';
+ logSh(` 🧠 GPT-5: reasoning_effort=minimal, max_completion_tokens=${maxTokens}`, 'DEBUG');
+ }
+
+ return requestBody;
+
+ case 'anthropic':
return {
model: config.model,
max_tokens: maxTokens,
@@ -195,10 +295,21 @@ function buildRequestData(provider, prompt, options, personality) {
{ role: 'user', content: prompt }
]
};
-
-
+
+ case 'google':
+ // Format spécifique Gemini
+ return {
+ contents: [{
+ parts: [{ text: systemPrompt + '\n\n' + prompt }]
+ }],
+ generationConfig: {
+ temperature: temperature,
+ maxOutputTokens: maxTokens
+ }
+ };
+
default:
- throw new Error(`Format de requête non supporté pour ${provider}`);
+ throw new Error(`Format de requête non supporté pour provider ${config.provider}`);
}
}
@@ -258,26 +369,30 @@ async function callWithRetry(provider, requestData, config) {
// ============= PARSING DES RÉPONSES =============
-function parseResponse(provider, responseData) {
+function parseResponse(modelId, responseData) {
+ const config = LLM_CONFIG[modelId];
+
try {
- switch (provider) {
+ switch (config.provider) {
case 'openai':
- case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
return responseData.choices[0].message.content.trim();
-
- case 'claude':
+
+ case 'anthropic':
return responseData.content[0].text.trim();
-
+
+ case 'google':
+ return responseData.candidates[0].content.parts[0].text.trim();
+
default:
- throw new Error(`Parser non supporté pour ${provider}`);
+ throw new Error(`Parser non supporté pour provider ${config.provider}`);
}
} catch (error) {
- logSh(`❌ Erreur parsing ${provider}: ${error.toString()}`, 'ERROR');
+ logSh(`❌ Erreur parsing ${modelId} (${config.provider}): ${error.toString()}`, 'ERROR');
logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
- throw new Error(`Impossible de parser la réponse ${provider}: ${error.toString()}`);
+ throw new Error(`Impossible de parser la réponse ${modelId}: ${error.toString()}`);
}
}
@@ -285,6 +400,12 @@ function parseResponse(provider, responseData) {
async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
try {
+ // Vérifier que le provider existe dans la config
+ if (!LLM_CONFIG[provider]) {
+ logSh(`⚠ Stats: Provider inconnu "${provider}", skip stats`, 'DEBUG');
+ return;
+ }
+
// TODO: Adapter selon votre système de stockage Node.js
// Peut être une base de données, un fichier, MongoDB, etc.
const statsData = {
@@ -296,12 +417,12 @@ async function recordUsageStats(provider, promptTokens, responseTokens, duration
duration: duration,
error: error || ''
};
-
+
// Exemple: log vers console ou fichier
logSh(`📊 Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
-
+
// TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
-
+
} catch (statsError) {
// Ne pas faire planter le workflow si les stats échouent
logSh(`⚠ Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
@@ -351,17 +472,37 @@ async function testAllLLMs() {
*/
function getAvailableProviders() {
const available = [];
-
+
Object.keys(LLM_CONFIG).forEach(provider => {
const config = LLM_CONFIG[provider];
if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
available.push(provider);
}
});
-
+
return available;
}
+/**
+ * Obtenir la liste complète des providers pour UI/validation
+ * @returns {Array} Liste des providers avec id, name, model
+ */
+function getLLMProvidersList() {
+ const providers = [];
+
+ Object.entries(LLM_CONFIG).forEach(([id, config]) => {
+ providers.push({
+ id: id,
+ name: config.displayName,
+ model: config.model,
+ provider: config.provider,
+ default: id === 'claude-sonnet-4-5' // Claude Sonnet 4.5 par défaut
+ });
+ });
+
+ return providers;
+}
+
/**
* Obtenir des statistiques d'usage par provider
*/
@@ -383,7 +524,7 @@ async function getUsageStats() {
* Maintient la même signature pour ne pas casser votre code existant
*/
async function callOpenAI(prompt, personality) {
- return await callLLM('openai', prompt, {}, personality);
+ return await callLLM('gpt-4o-mini', prompt, {}, personality);
}
// ============= EXPORTS POUR TESTS =============
@@ -420,7 +561,7 @@ async function testLLMManager() {
// Test spécifique OpenAI (compatibilité avec ancien code)
try {
logSh('🎯 Test spécifique OpenAI (compatibilité)...', 'DEBUG');
- const response = await callLLM('openai', 'Dis juste "Test OK"');
+ const response = await callLLM('gpt-4o-mini', 'Dis juste "Test OK"');
logSh('✅ Test OpenAI compatibilité: ' + response, 'INFO');
} catch (error) {
logSh('❌ Test OpenAI compatibilité échoué: ' + error.toString(), 'ERROR');
@@ -537,6 +678,7 @@ module.exports = {
callOpenAI,
testAllLLMs,
getAvailableProviders,
+ getLLMProvidersList,
getUsageStats,
testLLMManager,
testLLMManagerComplete,
diff --git a/lib/Main.js b/lib/Main.js
index 64f3cd1..7b7df7b 100644
--- a/lib/Main.js
+++ b/lib/Main.js
@@ -810,7 +810,7 @@ async function handleModularWorkflow(config = {}) {
* BENCHMARK COMPARATIF STACKS
*/
async function benchmarkStacks(rowNumber = 2) {
- console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n');
+ logSh('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n', 'INFO');
const stacks = getAvailableStacks();
const adversarialModes = ['none', 'light', 'standard'];
@@ -1005,10 +1005,14 @@ module.exports = {
logSh(`🎨 Détection pipeline flexible: ${data.pipelineConfig.name}`, 'INFO');
const executor = new PipelineExecutor();
+
const result = await executor.execute(
data.pipelineConfig,
data.rowNumber || 2,
- { stopOnError: data.stopOnError }
+ {
+ stopOnError: data.stopOnError,
+ saveIntermediateSteps: data.saveIntermediateSteps || false // ✅ Passer saveIntermediateSteps
+ }
);
// Formater résultat pour compatibilité
@@ -1021,7 +1025,8 @@ module.exports = {
personality: result.metadata.personality,
pipelineName: result.metadata.pipelineName,
totalSteps: result.metadata.totalSteps,
- successfulSteps: result.metadata.successfulSteps
+ successfulSteps: result.metadata.successfulSteps,
+ versionHistory: result.versionHistory // ✅ Inclure versionHistory
}
};
}
diff --git a/lib/StepExecutor.js b/lib/StepExecutor.js
index 177a335..82e3a89 100644
--- a/lib/StepExecutor.js
+++ b/lib/StepExecutor.js
@@ -4,6 +4,7 @@
// ========================================
const { logSh } = require('./ErrorReporting');
+const { validateElement, hasUnresolvedPlaceholders } = require('./ValidationGuards');
/**
* EXECUTEUR D'ÉTAPES MODULAIRES
@@ -96,25 +97,88 @@ class StepExecutor {
* Construire la structure de contenu depuis la hiérarchie réelle
*/
buildContentStructureFromHierarchy(inputData, hierarchy) {
+ logSh(`🏗️ BUILD CONTENT STRUCTURE - Début`, 'INFO');
+ logSh(` 📊 Input: mc0="${inputData.mc0}"`, 'DEBUG');
+ logSh(` 📊 Hiérarchie: ${hierarchy ? Object.keys(hierarchy).length : 0} sections`, 'DEBUG');
+
const contentStructure = {};
// Si hiérarchie disponible, l'utiliser
if (hierarchy && Object.keys(hierarchy).length > 0) {
- logSh(`🔍 Hiérarchie debug: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
+ logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'INFO');
logSh(`🔍 Première section sample: ${JSON.stringify(Object.values(hierarchy)[0]).substring(0, 200)}`, 'DEBUG');
- Object.entries(hierarchy).forEach(([path, section]) => {
+ let validationErrors = 0;
+ let elementCount = 0;
+
+ Object.entries(hierarchy).forEach(([path, section], sectionIndex) => {
+ logSh(`📂 SECTION [${sectionIndex + 1}/${Object.keys(hierarchy).length}] path="${path}"`, 'DEBUG');
+
// Générer pour le titre si présent
if (section.title && section.title.originalElement) {
+ elementCount++;
+
+ // ✅ SOLUTION D: Validation guard avant utilisation
+ try {
+ validateElement(section.title.originalElement, {
+ strict: true,
+ checkInstructions: true,
+ context: `StepExecutor buildContent - path: ${path} (title)`
+ });
+ } catch (validationError) {
+ validationErrors++;
+ logSh(`⚠️ Validation échouée pour titre [${section.title.originalElement.name}]: ${validationError.message}`, 'WARNING');
+ // Ne pas bloquer, utiliser fallback
+ }
+
const tag = section.title.originalElement.name;
const instruction = section.title.instructions || section.title.originalElement.instructions || `Rédige un titre pour ${inputData.mc0}`;
+
+ // 📊 LOG: Détailler l'instruction extraite
+ logSh(` 📌 TITRE [${elementCount}] tag="${tag}"`, 'DEBUG');
+ logSh(` 🔹 section.title.instructions: "${section.title.instructions ? section.title.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
+ logSh(` 🔹 section.title.originalElement.instructions: "${section.title.originalElement.instructions ? section.title.originalElement.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
+ logSh(` ➡️ INSTRUCTION FINALE: "${instruction.substring(0, 100)}"`, 'INFO');
+
+ // ✅ Double-vérification des instructions avant ajout
+ const instructionCheck = hasUnresolvedPlaceholders(instruction);
+ if (instructionCheck.hasIssues) {
+ logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
+ }
+
contentStructure[tag] = instruction;
}
// Générer pour le texte si présent
if (section.text && section.text.originalElement) {
+ elementCount++;
+
+ // ✅ SOLUTION D: Validation guard
+ try {
+ validateElement(section.text.originalElement, {
+ strict: true,
+ checkInstructions: true,
+ context: `StepExecutor buildContent - path: ${path} (text)`
+ });
+ } catch (validationError) {
+ validationErrors++;
+ logSh(`⚠️ Validation échouée pour texte [${section.text.originalElement.name}]: ${validationError.message}`, 'WARNING');
+ }
+
const tag = section.text.originalElement.name;
const instruction = section.text.instructions || section.text.originalElement.instructions || `Rédige du contenu sur ${inputData.mc0}`;
+
+ // 📊 LOG: Détailler l'instruction extraite
+ logSh(` 📌 TEXTE [${elementCount}] tag="${tag}"`, 'DEBUG');
+ logSh(` 🔹 section.text.instructions: "${section.text.instructions ? section.text.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
+ logSh(` 🔹 section.text.originalElement.instructions: "${section.text.originalElement.instructions ? section.text.originalElement.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
+ logSh(` ➡️ INSTRUCTION FINALE: "${instruction.substring(0, 100)}"`, 'INFO');
+
+ const instructionCheck = hasUnresolvedPlaceholders(instruction);
+ if (instructionCheck.hasIssues) {
+ logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
+ }
+
contentStructure[tag] = instruction;
}
@@ -122,15 +186,45 @@ class StepExecutor {
if (section.questions && section.questions.length > 0) {
section.questions.forEach(q => {
if (q.originalElement) {
+ // ✅ SOLUTION D: Validation guard
+ try {
+ validateElement(q.originalElement, {
+ strict: true,
+ checkInstructions: true,
+ context: `StepExecutor buildContent - path: ${path} (question)`
+ });
+ } catch (validationError) {
+ validationErrors++;
+ logSh(`⚠️ Validation échouée pour question [${q.originalElement.name}]: ${validationError.message}`, 'WARNING');
+ }
+
const tag = q.originalElement.name;
const instruction = q.instructions || q.originalElement.instructions || `Rédige une question/réponse FAQ sur ${inputData.mc0}`;
+
+ const instructionCheck = hasUnresolvedPlaceholders(instruction);
+ if (instructionCheck.hasIssues) {
+ logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
+ }
+
contentStructure[tag] = instruction;
}
});
}
});
- logSh(`🏗️ Structure depuis hiérarchie: ${Object.keys(contentStructure).length} éléments`, 'DEBUG');
+ if (validationErrors > 0) {
+ logSh(`⚠️ ${validationErrors} erreurs de validation détectées lors de la construction de la structure`, 'WARNING');
+ logSh(`💡 Cela indique que MissingKeywords.js n'a pas correctement synchronisé resolvedContent et instructions`, 'WARNING');
+ }
+
+ logSh(`✅ STRUCTURE CONSTRUITE: ${Object.keys(contentStructure).length} éléments prêts pour génération`, 'INFO');
+ logSh(`📊 RÉSUMÉ INSTRUCTIONS:`, 'INFO');
+
+ // 📊 LOG: Afficher toutes les instructions finales
+ Object.entries(contentStructure).forEach(([tag, instruction], idx) => {
+ const shortInstr = instruction.length > 80 ? instruction.substring(0, 80) + '...' : instruction;
+ logSh(` [${idx + 1}] ${tag}: "${shortInstr}"`, 'INFO');
+ });
} else {
// Fallback: structure générique si pas de hiérarchie
logSh(`⚠️ Pas de hiérarchie, utilisation structure générique`, 'WARNING');
diff --git a/lib/adversarial-generation/AdversarialCore.js b/lib/adversarial-generation/AdversarialCore.js
index 2d845ef..0ade63c 100644
--- a/lib/adversarial-generation/AdversarialCore.js
+++ b/lib/adversarial-generation/AdversarialCore.js
@@ -24,7 +24,8 @@ async function applyAdversarialLayer(existingContent, config = {}) {
method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid'
preserveStructure = true,
csvData = null,
- context = {}
+ context = {},
+ llmProvider = 'gemini-pro' // ✅ AJOUTÉ: Extraction llmProvider avec fallback
} = config;
await tracer.annotate({
@@ -32,29 +33,31 @@ async function applyAdversarialLayer(existingContent, config = {}) {
detectorTarget,
intensity,
method,
+ llmProvider,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO');
- logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO');
+ logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity} | LLM: ${llmProvider}`, 'INFO');
try {
// Initialiser stratégie détecteur
const strategy = DetectorStrategyFactory.createStrategy(detectorTarget);
- // Appliquer méthode adversariale choisie
+ // Appliquer méthode adversariale choisie avec LLM spécifié
let adversarialContent = {};
-
+ const methodConfig = { ...config, llmProvider }; // ✅ Assurer propagation llmProvider
+
switch (method) {
case 'regeneration':
- adversarialContent = await applyRegenerationMethod(existingContent, config, strategy);
+ adversarialContent = await applyRegenerationMethod(existingContent, methodConfig, strategy);
break;
case 'enhancement':
- adversarialContent = await applyEnhancementMethod(existingContent, config, strategy);
+ adversarialContent = await applyEnhancementMethod(existingContent, methodConfig, strategy);
break;
case 'hybrid':
- adversarialContent = await applyHybridMethod(existingContent, config, strategy);
+ adversarialContent = await applyHybridMethod(existingContent, methodConfig, strategy);
break;
default:
throw new Error(`Méthode adversariale inconnue: ${method}`);
@@ -77,6 +80,7 @@ async function applyAdversarialLayer(existingContent, config = {}) {
return {
content: adversarialContent,
stats,
+ modifications: stats.elementsModified, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
original: existingContent,
config
};
@@ -102,22 +106,23 @@ async function applyAdversarialLayer(existingContent, config = {}) {
* MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux
*/
async function applyRegenerationMethod(existingContent, config, strategy) {
- logSh(`🔄 Méthode régénération adversariale`, 'DEBUG');
-
+ const llmToUse = config.llmProvider || 'gemini-pro';
+ logSh(`🔄 Méthode régénération adversariale (LLM: ${llmToUse})`, 'DEBUG');
+
const results = {};
const contentEntries = Object.entries(existingContent);
-
+
// Traiter en chunks pour éviter timeouts
const chunks = chunkArray(contentEntries, 4);
-
+
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
-
+
try {
const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy);
-
- const response = await callLLM(config.llmProvider || 'gemini', regenerationPrompt, {
+
+ const response = await callLLM(llmToUse, regenerationPrompt, {
temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité
maxTokens: 2000 * chunk.length
}, config.csvData?.personality);
@@ -149,22 +154,23 @@ async function applyRegenerationMethod(existingContent, config, strategy) {
* MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement
*/
async function applyEnhancementMethod(existingContent, config, strategy) {
- logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG');
-
+ const llmToUse = config.llmProvider || 'gemini-pro';
+ logSh(`🔧 Méthode enhancement adversarial (LLM: ${llmToUse})`, 'DEBUG');
+
const results = { ...existingContent }; // Base: contenu original
const elementsToEnhance = selectElementsForEnhancement(existingContent, config);
-
+
if (elementsToEnhance.length === 0) {
logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG');
return results;
}
-
+
logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG');
-
+
const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy);
-
+
try {
- const response = await callLLM(config.llmProvider || 'gemini', enhancementPrompt, {
+ const response = await callLLM(llmToUse, enhancementPrompt, {
temperature: 0.5 + (config.intensity * 0.3),
maxTokens: 3000
}, config.csvData?.personality);
diff --git a/lib/adversarial-generation/AdversarialLayers.js b/lib/adversarial-generation/AdversarialLayers.js
index e8435df..b2ff5cd 100644
--- a/lib/adversarial-generation/AdversarialLayers.js
+++ b/lib/adversarial-generation/AdversarialLayers.js
@@ -159,6 +159,7 @@ async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
return {
content: currentContent,
stats: pipelineStats,
+ modifications: pipelineStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
original: content
};
diff --git a/lib/human-simulation/HumanSimulationCore.js b/lib/human-simulation/HumanSimulationCore.js
index 9fda112..b4d5be8 100644
--- a/lib/human-simulation/HumanSimulationCore.js
+++ b/lib/human-simulation/HumanSimulationCore.js
@@ -167,6 +167,7 @@ async function applyHumanSimulationLayer(content, options = {}) {
return {
content: simulatedContent,
stats: simulationStats,
+ modifications: simulationStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
fallback: simulationStats.fallbackUsed,
qualityScore: simulationStats.qualityScore,
duration
diff --git a/lib/human-simulation/HumanSimulationLayers.js b/lib/human-simulation/HumanSimulationLayers.js
index d358156..8497c84 100644
--- a/lib/human-simulation/HumanSimulationLayers.js
+++ b/lib/human-simulation/HumanSimulationLayers.js
@@ -249,6 +249,7 @@ async function applyPredefinedSimulation(content, stackName, options = {}) {
return {
content,
stats: { fallbackUsed: true, error: error.message },
+ modifications: 0, // ✅ AJOUTÉ: Mapping pour PipelineExecutor (fallback = 0 modifs)
fallback: true,
stackInfo: { name: stack.name, error: error.message }
};
diff --git a/lib/modes/ManualServer.js b/lib/modes/ManualServer.js
index 7402c0c..75727d5 100644
--- a/lib/modes/ManualServer.js
+++ b/lib/modes/ManualServer.js
@@ -4,15 +4,43 @@
// FONCTIONNALITÉS: Dashboard, tests modulaires, API complète
// ========================================
-const express = require('express');
-const cors = require('cors');
-const path = require('path');
-const WebSocket = require('ws');
+// ⏱️ Timing chargement modules (logs avant/après chaque require)
+const _t0 = Date.now();
+console.log(`[${new Date().toISOString()}] ⏱️ [require] Début chargement ManualServer modules...`);
+const _t1 = Date.now();
+const express = require('express');
+console.log(`[${new Date().toISOString()}] ✓ [require] express en ${Date.now() - _t1}ms`);
+
+const _t2 = Date.now();
+const cors = require('cors');
+console.log(`[${new Date().toISOString()}] ✓ [require] cors en ${Date.now() - _t2}ms`);
+
+const _t3 = Date.now();
+const path = require('path');
+console.log(`[${new Date().toISOString()}] ✓ [require] path en ${Date.now() - _t3}ms`);
+
+const _t4 = Date.now();
+const WebSocket = require('ws');
+console.log(`[${new Date().toISOString()}] ✓ [require] ws (WebSocket) en ${Date.now() - _t4}ms`);
+
+const _t5 = Date.now();
const { logSh } = require('../ErrorReporting');
+console.log(`[${new Date().toISOString()}] ✓ [require] ErrorReporting en ${Date.now() - _t5}ms`);
+
+const _t6 = Date.now();
const { handleModularWorkflow, benchmarkStacks } = require('../Main');
+console.log(`[${new Date().toISOString()}] ✓ [require] Main en ${Date.now() - _t6}ms`);
+
+const _t7 = Date.now();
const { APIController } = require('../APIController');
+console.log(`[${new Date().toISOString()}] ✓ [require] APIController en ${Date.now() - _t7}ms`);
+
+const _t8 = Date.now();
const { BatchController } = require('../batch/BatchController');
+console.log(`[${new Date().toISOString()}] ✓ [require] BatchController en ${Date.now() - _t8}ms`);
+
+console.log(`[${new Date().toISOString()}] ✅ [require] TOTAL ManualServer modules chargés en ${Date.now() - _t0}ms`);
/**
* SERVEUR MODE MANUAL
@@ -43,6 +71,10 @@ class ManualServer {
this.isRunning = false;
this.apiController = new APIController();
this.batchController = new BatchController();
+
+ // Cache pour status LLMs (évite d'appeler trop souvent)
+ this.llmStatusCache = null;
+ this.llmStatusCacheTime = null;
}
// ========================================
@@ -57,32 +89,57 @@ class ManualServer {
logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING');
return;
}
-
+
+ const startTime = Date.now();
logSh('🎯 Démarrage ManualServer...', 'INFO');
-
+
try {
// 1. Configuration Express
+ logSh('⏱️ [1/7] Configuration Express...', 'INFO');
+ const t1 = Date.now();
await this.setupExpressApp();
-
+ logSh(`✓ Express configuré en ${Date.now() - t1}ms`, 'INFO');
+
// 2. Routes API
+ logSh('⏱️ [2/7] Configuration routes API...', 'INFO');
+ const t2 = Date.now();
this.setupAPIRoutes();
-
+ logSh(`✓ Routes API configurées en ${Date.now() - t2}ms`, 'INFO');
+
// 3. Interface Web
+ logSh('⏱️ [3/7] Configuration interface web...', 'INFO');
+ const t3 = Date.now();
this.setupWebInterface();
-
+ logSh(`✓ Interface web configurée en ${Date.now() - t3}ms`, 'INFO');
+
// 4. WebSocket pour logs temps réel
+ logSh('⏱️ [4/7] Démarrage WebSocket serveur...', 'INFO');
+ const t4 = Date.now();
await this.setupWebSocketServer();
-
+ logSh(`✓ WebSocket démarré en ${Date.now() - t4}ms`, 'INFO');
+
// 5. Démarrage serveur HTTP
+ logSh('⏱️ [5/7] Démarrage serveur HTTP...', 'INFO');
+ const t5 = Date.now();
await this.startHTTPServer();
-
+ logSh(`✓ Serveur HTTP démarré en ${Date.now() - t5}ms`, 'INFO');
+
// 6. Monitoring
+ logSh('⏱️ [6/7] Démarrage monitoring...', 'INFO');
+ const t6 = Date.now();
this.startMonitoring();
-
+ logSh(`✓ Monitoring démarré en ${Date.now() - t6}ms`, 'INFO');
+
+ // 7. Initialisation status LLMs au démarrage (en background)
+ logSh('⏱️ [7/7] Initialisation LLM status (background)...', 'INFO');
+ const t7 = Date.now();
+ this.initializeLLMStatus();
+ logSh(`✓ LLM init lancé en ${Date.now() - t7}ms`, 'INFO');
+
this.isRunning = true;
this.stats.startTime = Date.now();
-
- logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO');
+
+ logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port} (total: ${Date.now() - startTime}ms)`, 'INFO');
logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO');
} catch (error) {
@@ -97,19 +154,31 @@ class ManualServer {
*/
async stop() {
if (!this.isRunning) return;
-
+
logSh('🛑 Arrêt ManualServer...', 'INFO');
-
+
try {
+ // Arrêter le monitoring
+ if (this.monitorInterval) {
+ clearInterval(this.monitorInterval);
+ this.monitorInterval = null;
+ }
+
+ // Arrêter le refresh status LLMs
+ if (this.llmStatusInterval) {
+ clearInterval(this.llmStatusInterval);
+ this.llmStatusInterval = null;
+ }
+
// Déconnecter tous les clients WebSocket
this.disconnectAllClients();
-
+
// Arrêter WebSocket server
if (this.wsServer) {
this.wsServer.close();
this.wsServer = null;
}
-
+
// Arrêter serveur HTTP
if (this.server) {
await new Promise((resolve) => {
@@ -117,11 +186,11 @@ class ManualServer {
});
this.server = null;
}
-
+
this.isRunning = false;
-
+
logSh('✅ ManualServer arrêté', 'INFO');
-
+
} catch (error) {
logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING');
}
@@ -823,6 +892,11 @@ class ManualServer {
await this.apiController.getMetrics(req, res);
});
+ // === LLM MONITORING API ===
+ this.app.get('/api/llm/status', async (req, res) => {
+ await this.handleLLMStatus(req, res);
+ });
+
// === PROMPT ENGINE API ===
this.app.post('/api/generate-prompt', async (req, res) => {
await this.apiController.generatePrompt(req, res);
@@ -1483,6 +1557,187 @@ class ManualServer {
}
}
+ /**
+ * Initialise le status LLMs au démarrage (en background)
+ * ⚡ OPTIMISÉ: Mode rapide au démarrage, complet après 30s
+ */
+ async initializeLLMStatus() {
+ logSh('🚀 Initialisation status LLMs (mode rapide)...', 'DEBUG');
+
+ // ⚡ Phase 1: Vérification RAPIDE des clés API (immédiat, sans réseau)
+ setImmediate(async () => {
+ try {
+ await this.refreshLLMStatus(true); // quickMode = true
+ logSh('✅ Status LLMs (rapide) initialisé - vérification clés API OK', 'INFO');
+
+ // ⚡ Phase 2: Test COMPLET avec appels réseau après 30 secondes
+ setTimeout(async () => {
+ try {
+ logSh('🔄 Démarrage vérification complète status LLMs (avec tests réseau)...', 'DEBUG');
+ await this.refreshLLMStatus(false); // quickMode = false
+ logSh('✅ Status LLMs (complet) mis à jour avec tests réseau', 'INFO');
+ } catch (error) {
+ logSh(`⚠️ Erreur vérification complète LLMs: ${error.message}`, 'WARNING');
+ }
+ }, 30000); // Attendre 30 secondes avant le test complet
+
+ } catch (error) {
+ logSh(`⚠️ Erreur initialisation rapide status LLMs: ${error.message}`, 'WARNING');
+ }
+ });
+
+ // Rafraîchir toutes les 30 minutes (1800000ms) en mode complet
+ this.llmStatusInterval = setInterval(async () => {
+ try {
+ await this.refreshLLMStatus(false); // Mode complet pour les refreshs périodiques
+ } catch (error) {
+ logSh(`⚠️ Erreur refresh status LLMs: ${error.message}`, 'WARNING');
+ }
+ }, 1800000);
+ }
+
+ /**
+ * Rafraîchit le cache du status LLMs
+ * ⚡ OPTIMISÉ: Test rapide sans appels LLM réels au démarrage
+ */
+ async refreshLLMStatus(quickMode = false) {
+ const { getLLMProvidersList } = require('../LLMManager');
+
+ logSh(`📊 Récupération status LLMs${quickMode ? ' (mode rapide)' : ''}...`, 'DEBUG');
+
+ const providers = getLLMProvidersList();
+ const providersWithStatus = [];
+
+ if (quickMode) {
+ // ⚡ MODE RAPIDE: Vérifier juste les clés API sans appels réseau
+ for (const provider of providers) {
+ const hasApiKey = this.checkProviderApiKey(provider.id);
+
+ providersWithStatus.push({
+ ...provider,
+ status: hasApiKey ? 'unknown' : 'no_key',
+ latency: null,
+ lastTest: null,
+ credits: 'unlimited',
+ calls: 0,
+ successRate: hasApiKey ? null : 0,
+ quickMode: true
+ });
+ }
+
+ logSh('⚡ Status rapide LLMs (sans appels réseau) - vérification clés API uniquement', 'DEBUG');
+ } else {
+ // MODE COMPLET: Test réseau réel
+ for (const provider of providers) {
+ const startTime = Date.now();
+ let status = 'offline';
+ let latency = null;
+ let lastTest = null;
+
+ try {
+ const { callLLM } = require('../LLMManager');
+ await callLLM(provider.id, 'Test ping', { maxTokens: 10 });
+
+ latency = Date.now() - startTime;
+ status = 'online';
+ lastTest = new Date().toLocaleTimeString('fr-FR');
+ } catch (error) {
+ logSh(`⚠️ Provider ${provider.id} offline: ${error.message}`, 'DEBUG');
+ status = 'offline';
+ }
+
+ providersWithStatus.push({
+ ...provider,
+ status,
+ latency,
+ lastTest,
+ credits: 'unlimited',
+ calls: 0,
+ successRate: status === 'online' ? 100 : 0
+ });
+ }
+ }
+
+ // Mettre à jour le cache
+ this.llmStatusCache = {
+ success: true,
+ providers: providersWithStatus,
+ summary: {
+ total: providersWithStatus.length,
+ online: providersWithStatus.filter(p => p.status === 'online').length,
+ offline: providersWithStatus.filter(p => p.status === 'offline').length,
+ unknown: providersWithStatus.filter(p => p.status === 'unknown').length,
+ no_key: providersWithStatus.filter(p => p.status === 'no_key').length
+ },
+ quickMode: quickMode,
+ timestamp: new Date().toISOString()
+ };
+ this.llmStatusCacheTime = Date.now();
+
+ if (quickMode) {
+ logSh(`⚡ Status LLMs (rapide): ${this.llmStatusCache.summary.unknown} providers avec clés, ${this.llmStatusCache.summary.no_key} sans clés`, 'INFO');
+ } else {
+ logSh(`✅ Status LLMs (complet): ${this.llmStatusCache.summary.online}/${this.llmStatusCache.summary.total} online`, 'INFO');
+ }
+ }
+
+ /**
+ * Vérifie si un provider a une clé API configurée
+ */
+ checkProviderApiKey(providerId) {
+ const keyMap = {
+ 'claude-sonnet-4-5': 'ANTHROPIC_API_KEY',
+ 'claude-3-5-sonnet-20241022': 'ANTHROPIC_API_KEY',
+ 'gpt-4o': 'OPENAI_API_KEY',
+ 'gpt-4o-mini': 'OPENAI_API_KEY',
+ 'gemini-2-0-flash-exp': 'GOOGLE_API_KEY',
+ 'gemini-pro': 'GOOGLE_API_KEY',
+ 'deepseek-chat': 'DEEPSEEK_API_KEY',
+ 'moonshot-v1-8k': 'MOONSHOT_API_KEY',
+ 'mistral-small-latest': 'MISTRAL_API_KEY'
+ };
+
+ const envKey = keyMap[providerId];
+ if (!envKey) return false;
+
+ const apiKey = process.env[envKey];
+ return apiKey && apiKey.length > 10;
+ }
+
+ /**
+ * Handler pour status et monitoring des LLMs
+ */
+ async handleLLMStatus(req, res) {
+ try {
+ // Si on a un cache, le retourner directement
+ if (this.llmStatusCache) {
+ res.json(this.llmStatusCache);
+ } else {
+ // Pas encore de cache, retourner une réponse vide
+ res.json({
+ success: true,
+ providers: [],
+ summary: {
+ total: 0,
+ online: 0,
+ offline: 0
+ },
+ timestamp: new Date().toISOString(),
+ message: 'Status LLMs en cours de chargement...'
+ });
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur status LLMs: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération status LLMs',
+ message: error.message,
+ timestamp: new Date().toISOString()
+ });
+ }
+ }
+
/**
* 🆕 Handler pour génération simple d'article avec mot-clé
*/
@@ -1700,6 +1955,7 @@ class ManualServer {
📊 Monitoring & API
Endpoints disponibles en mode MANUAL.
+
🤖 LLM Monitoring
📊 Status API
📈 Statistiques
diff --git a/lib/modes/ModeManager.js b/lib/modes/ModeManager.js
index a22922c..f34a357 100644
--- a/lib/modes/ModeManager.js
+++ b/lib/modes/ModeManager.js
@@ -47,25 +47,36 @@ class ModeManager {
* @param {string} initialMode - Mode initial (manual|auto|detect)
*/
static async initialize(initialMode = 'detect') {
+ const startTime = Date.now();
logSh('🎛️ Initialisation ModeManager...', 'INFO');
-
+
try {
// Détecter mode selon arguments ou config
+ logSh('⏱️ Détection mode...', 'INFO');
+ const t1 = Date.now();
const detectedMode = this.detectIntendedMode(initialMode);
-
- logSh(`🎯 Mode détecté: ${detectedMode.toUpperCase()}`, 'INFO');
-
+ logSh(`✓ Mode détecté: ${detectedMode.toUpperCase()} en ${Date.now() - t1}ms`, 'INFO');
+
// Nettoyer état précédent si nécessaire
+ logSh('⏱️ Nettoyage état précédent...', 'INFO');
+ const t2 = Date.now();
await this.cleanupPreviousState();
-
+ logSh(`✓ Nettoyage terminé en ${Date.now() - t2}ms`, 'INFO');
+
// Basculer vers le mode détecté
+ logSh(`⏱️ Basculement vers mode ${detectedMode.toUpperCase()}...`, 'INFO');
+ const t3 = Date.now();
await this.switchToMode(detectedMode);
-
+ logSh(`✓ Basculement terminé en ${Date.now() - t3}ms`, 'INFO');
+
// Sauvegarder état
+ logSh('⏱️ Sauvegarde état...', 'INFO');
+ const t4 = Date.now();
this.saveModeState();
-
- logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()}`, 'INFO');
-
+ logSh(`✓ État sauvegardé en ${Date.now() - t4}ms`, 'INFO');
+
+ logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()} (total: ${Date.now() - startTime}ms)`, 'INFO');
+
return this.currentMode;
} catch (error) {
@@ -246,14 +257,22 @@ class ModeManager {
* Démarre le mode MANUAL
*/
static async startManualMode() {
+ const t1 = Date.now();
+ logSh('⏱️ Chargement module ManualServer...', 'INFO');
const { ManualServer } = require('./ManualServer');
-
- logSh('🎯 Démarrage ManualServer...', 'DEBUG');
-
+ logSh(`✓ ManualServer chargé en ${Date.now() - t1}ms`, 'INFO');
+
+ const t2 = Date.now();
+ logSh('⏱️ Instanciation ManualServer...', 'INFO');
this.activeServices.manualServer = new ManualServer();
+ logSh(`✓ ManualServer instancié en ${Date.now() - t2}ms`, 'INFO');
+
+ const t3 = Date.now();
+ logSh('⏱️ Démarrage ManualServer.start()...', 'INFO');
await this.activeServices.manualServer.start();
-
- logSh('✅ Mode MANUAL démarré', 'DEBUG');
+ logSh(`✓ ManualServer.start() terminé en ${Date.now() - t3}ms`, 'INFO');
+
+ logSh('✅ Mode MANUAL démarré', 'INFO');
}
/**
diff --git a/lib/pattern-breaking/PatternBreakingCore.js b/lib/pattern-breaking/PatternBreakingCore.js
index ddb36c6..0cf4ad6 100644
--- a/lib/pattern-breaking/PatternBreakingCore.js
+++ b/lib/pattern-breaking/PatternBreakingCore.js
@@ -277,6 +277,7 @@ async function applyPatternBreakingLayer(content, options = {}) {
return {
content: processedContent,
stats: patternStats,
+ modifications: patternStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
fallback: patternStats.fallbackUsed,
duration
};
diff --git a/lib/pattern-breaking/PatternBreakingLayers.js b/lib/pattern-breaking/PatternBreakingLayers.js
index 3c4540b..57bee14 100644
--- a/lib/pattern-breaking/PatternBreakingLayers.js
+++ b/lib/pattern-breaking/PatternBreakingLayers.js
@@ -178,6 +178,7 @@ async function applyPatternBreakingStack(stackName, content, overrides = {}) {
stackDescription: stack.description,
expectedReduction: stack.expectedReduction
},
+ modifications: result.modifications || result.stats?.totalModifications || 0, // ✅ AJOUTÉ: Propagation modifications
fallback: result.fallback,
stackUsed: stackName
};
diff --git a/lib/pipeline/PipelineDefinition.js b/lib/pipeline/PipelineDefinition.js
index 92b6b6a..30901c9 100644
--- a/lib/pipeline/PipelineDefinition.js
+++ b/lib/pipeline/PipelineDefinition.js
@@ -6,18 +6,12 @@
*/
const { logSh } = require('../ErrorReporting');
+const { getLLMProvidersList } = require('../LLMManager');
/**
- * Providers LLM disponibles
+ * Providers LLM disponibles (source unique depuis LLMManager)
*/
-const AVAILABLE_LLM_PROVIDERS = [
- { id: 'claude', name: 'Claude (Anthropic)', default: true },
- { id: 'openai', name: 'OpenAI GPT-4' },
- { id: 'gemini', name: 'Google Gemini' },
- { id: 'deepseek', name: 'Deepseek' },
- { id: 'moonshot', name: 'Moonshot' },
- { id: 'mistral', name: 'Mistral AI' }
-];
+const AVAILABLE_LLM_PROVIDERS = getLLMProvidersList();
/**
* Modules disponibles dans le pipeline
@@ -28,9 +22,9 @@ const AVAILABLE_MODULES = {
description: 'Génération initiale du contenu',
modes: ['simple'],
defaultIntensity: 1.0,
- defaultLLM: 'claude',
+ defaultLLM: 'claude-sonnet-4-5',
parameters: {
- llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude' }
+ llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude-sonnet-4-5' }
}
},
selective: {
@@ -45,10 +39,10 @@ const AVAILABLE_MODULES = {
'adaptive'
],
defaultIntensity: 1.0,
- defaultLLM: 'openai',
+ defaultLLM: 'gpt-4o-mini',
parameters: {
layers: { type: 'array', description: 'Couches spécifiques à appliquer' },
- llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'openai' }
+ llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' }
}
},
adversarial: {
@@ -56,11 +50,11 @@ const AVAILABLE_MODULES = {
description: 'Techniques anti-détection',
modes: ['none', 'light', 'standard', 'heavy', 'adaptive'],
defaultIntensity: 1.0,
- defaultLLM: 'gemini',
+ defaultLLM: 'gemini-pro',
parameters: {
detector: { type: 'string', enum: ['general', 'gptZero', 'originality'], default: 'general' },
method: { type: 'string', enum: ['enhancement', 'regeneration', 'hybrid'], default: 'regeneration' },
- llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini' }
+ llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini-pro' }
}
},
human: {
@@ -76,11 +70,11 @@ const AVAILABLE_MODULES = {
'temporalFocus'
],
defaultIntensity: 1.0,
- defaultLLM: 'mistral',
+ defaultLLM: 'mistral-small',
parameters: {
fatigueLevel: { type: 'number', min: 0, max: 1, default: 0.5 },
errorRate: { type: 'number', min: 0, max: 1, default: 0.3 },
- llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral' }
+ llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral-small' }
}
},
pattern: {
@@ -96,10 +90,10 @@ const AVAILABLE_MODULES = {
'connectorsFocus'
],
defaultIntensity: 1.0,
- defaultLLM: 'deepseek',
+ defaultLLM: 'deepseek-chat',
parameters: {
focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' },
- llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek' }
+ llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek-chat' }
}
}
};
diff --git a/lib/selective-enhancement/StyleLayer.js b/lib/selective-enhancement/StyleLayer.js
index 9ac1821..54055a5 100644
--- a/lib/selective-enhancement/StyleLayer.js
+++ b/lib/selective-enhancement/StyleLayer.js
@@ -332,8 +332,13 @@ class StyleLayer {
*/
analyzePersonalityAlignment(text, personality) {
if (!personality.vocabulairePref) return 1;
-
- const preferredWords = personality.vocabulairePref.toLowerCase().split(',');
+
+ // Convertir en string si ce n'est pas déjà le cas
+ const vocabPref = typeof personality.vocabulairePref === 'string'
+ ? personality.vocabulairePref
+ : String(personality.vocabulairePref);
+
+ const preferredWords = vocabPref.toLowerCase().split(',');
const contentLower = text.toLowerCase();
let alignmentScore = 0;
diff --git a/lib/selective-enhancement/TechnicalLayer.js b/lib/selective-enhancement/TechnicalLayer.js
index 386cf79..e84aaf5 100644
--- a/lib/selective-enhancement/TechnicalLayer.js
+++ b/lib/selective-enhancement/TechnicalLayer.js
@@ -15,7 +15,7 @@ const { chunkArray, sleep } = require('./SelectiveUtils');
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
- this.defaultLLM = 'openai';
+ this.defaultLLM = 'gpt-4o-mini';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}
diff --git a/public/index.html b/public/index.html
index 2662683..1e3433d 100644
--- a/public/index.html
+++ b/public/index.html
@@ -217,33 +217,7 @@
-
-
-
🔧
-
Éditeur de Configuration
-
⚠️ ANCIEN SYSTÈME - Désactivé
-
- - 4 couches modulaires configurables
- - Save/Load des configurations
- - Test en direct avec logs temps réel
- - Preview JSON de la configuration
-
-
-
-
-
-
🚀
-
Runner de Production
-
⚠️ ANCIEN SYSTÈME - Désactivé
-
- - Load configuration sauvegardée
- - Sélection ligne Google Sheets
- - Logs temps réel pendant l'exécution
- - Résultats et lien direct vers GSheets
-
-
-
-
+
🎨
Pipeline Builder
@@ -256,7 +230,7 @@
-
+
⚡
Pipeline Runner
@@ -268,6 +242,19 @@
Logs d'exécution complets
+
+
+
+
🤖
+
LLM Monitoring
+
Surveiller la santé et les performances de vos modèles LLM
+
+ - Status en temps réel (9 LLMs)
+ - Latences moyennes et barres de progression
+ - Crédits restants par plateforme
+ - Auto-refresh toutes les 30 secondes
+
+
diff --git a/public/pipeline-builder.html b/public/pipeline-builder.html
index ae9744e..3400e06 100644
--- a/public/pipeline-builder.html
+++ b/public/pipeline-builder.html
@@ -399,10 +399,38 @@
white-space: pre-wrap;
}
- @media (max-width: 1400px) {
+ /* Responsive Layouts */
+ @media (max-width: 1400px) and (min-width: 900px) {
+ .builder-layout {
+ grid-template-columns: 280px 1fr;
+ grid-template-rows: auto;
+ }
+
+ .modules-palette {
+ grid-row: 1 / 3;
+ height: calc(100vh - 180px);
+ }
+
+ .side-panel {
+ height: auto;
+ max-height: none;
+ }
+ }
+
+ @media (max-width: 899px) {
.builder-layout {
grid-template-columns: 1fr;
}
+
+ .modules-palette,
+ .side-panel {
+ height: auto;
+ max-height: 400px;
+ }
+
+ .panel {
+ padding: 15px;
+ }
}
diff --git a/public/pipeline-builder.js b/public/pipeline-builder.js
index 4f82284..f2dbf9e 100644
--- a/public/pipeline-builder.js
+++ b/public/pipeline-builder.js
@@ -74,14 +74,14 @@ async function loadLLMProviders() {
}
} catch (error) {
console.error('Erreur chargement LLM providers:', error);
- // Fallback providers si l'API échoue
+ // Fallback providers si l'API échoue (synchronisé avec LLMManager)
state.llmProviders = [
- { id: 'claude', name: 'Claude (Anthropic)', default: true },
- { id: 'openai', name: 'OpenAI GPT-4' },
- { id: 'gemini', name: 'Google Gemini' },
- { id: 'deepseek', name: 'Deepseek' },
- { id: 'moonshot', name: 'Moonshot' },
- { id: 'mistral', name: 'Mistral AI' }
+ { id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', default: true },
+ { id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
+ { id: 'gemini-pro', name: 'Google Gemini Pro' },
+ { id: 'deepseek-chat', name: 'Deepseek Chat' },
+ { id: 'moonshot-v1-32k', name: 'Moonshot v1 32K' },
+ { id: 'mistral-small', name: 'Mistral Small' }
];
}
}
diff --git a/public/pipeline-runner.html b/public/pipeline-runner.html
index ec6e8cd..4136034 100644
--- a/public/pipeline-runner.html
+++ b/public/pipeline-runner.html
@@ -345,6 +345,23 @@
+ 📄 Contenu Final Généré
+
+
Le contenu apparaîtra ici après l'exécution
+
+
+ 📦 Versions Sauvegardées
+
+
Aucune version disponible
+
+
+
+
📝 Log d'Exécution
diff --git a/public/pipeline-runner.js b/public/pipeline-runner.js
index bcc412f..d300680 100644
--- a/public/pipeline-runner.js
+++ b/public/pipeline-runner.js
@@ -182,6 +182,14 @@ function displayResults(result) {
const resultsSection = document.getElementById('resultsSection');
resultsSection.style.display = 'block';
+ // Log complet des résultats dans la console
+ console.log('=== RÉSULTAT PIPELINE ===');
+ console.log('Contenu final:', result.finalContent || result.content);
+ console.log('Stats:', result.stats);
+ console.log('Version history:', result.versionHistory);
+ console.log('Résultat complet:', result);
+ console.log('========================');
+
// Stats
document.getElementById('statDuration').textContent =
`${result.stats.totalDuration}ms`;
@@ -192,6 +200,130 @@ function displayResults(result) {
document.getElementById('statPersonality').textContent =
result.stats.personality || 'N/A';
+ // Final Content Display
+ const finalContentContainer = document.getElementById('finalContentContainer');
+ let rawContent = result.finalContent || result.content || result.organicContent;
+
+ // Extraire le texte si c'est un objet
+ let finalContent;
+ let isStructuredContent = false;
+
+ if (typeof rawContent === 'string') {
+ finalContent = rawContent;
+ } else if (rawContent && typeof rawContent === 'object') {
+ // Vérifier si c'est un contenu structuré (H2_1, H3_2, etc.)
+ const keys = Object.keys(rawContent);
+ if (keys.some(k => k.match(/^(H2|H3|P)_\d+$/))) {
+ isStructuredContent = true;
+ // Formater le contenu structuré
+ finalContent = keys
+ .sort((a, b) => {
+ // Trier par type (H2, H3, P) puis par numéro
+ const aMatch = a.match(/^([A-Z]+)_(\d+)$/);
+ const bMatch = b.match(/^([A-Z]+)_(\d+)$/);
+ if (!aMatch || !bMatch) return 0;
+ if (aMatch[1] !== bMatch[1]) return aMatch[1].localeCompare(bMatch[1]);
+ return parseInt(aMatch[2]) - parseInt(bMatch[2]);
+ })
+ .map(key => {
+ const match = key.match(/^([A-Z0-9]+)_(\d+)$/);
+ if (match) {
+ const tag = match[1];
+ return `[${tag}]\n${rawContent[key]}\n`;
+ }
+ return `${key}: ${rawContent[key]}\n`;
+ })
+ .join('\n');
+ } else {
+ // Si c'est un objet, essayer d'extraire le texte
+ finalContent = rawContent.text || rawContent.content || rawContent.organicContent || JSON.stringify(rawContent, null, 2);
+ }
+ }
+
+ if (finalContent) {
+ finalContentContainer.innerHTML = '';
+
+ // Warning si contenu incomplet
+ const elementCount = Object.keys(rawContent || {}).length;
+ if (isStructuredContent && elementCount < 30) {
+ const warningDiv = document.createElement('div');
+ warningDiv.style.cssText = 'padding: 10px; margin-bottom: 15px; background: #fed7d7; border: 1px solid #f56565; border-radius: 6px; color: #822727;';
+ warningDiv.innerHTML = `⚠️ Génération incomplète: ${elementCount} éléments générés (attendu ~33). Vérifiez les logs pour plus de détails.`;
+ finalContentContainer.appendChild(warningDiv);
+ }
+
+ // Créer un élément pre pour préserver le formatage
+ const contentDiv = document.createElement('div');
+ contentDiv.style.cssText = 'white-space: pre-wrap; line-height: 1.6; color: var(--text-dark); font-size: 14px;';
+ contentDiv.textContent = finalContent;
+
+ // Ajouter un bouton pour copier
+ const copyBtn = document.createElement('button');
+ copyBtn.textContent = '📋 Copier le contenu';
+ copyBtn.style.cssText = 'margin-bottom: 15px; padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;';
+ copyBtn.onclick = () => {
+ navigator.clipboard.writeText(finalContent);
+ copyBtn.textContent = '✓ Copié!';
+ setTimeout(() => { copyBtn.textContent = '📋 Copier le contenu'; }, 2000);
+ };
+
+ finalContentContainer.appendChild(copyBtn);
+ finalContentContainer.appendChild(contentDiv);
+
+ // Ajouter les métadonnées si disponibles
+ if (result.stats) {
+ const metaDiv = document.createElement('div');
+ metaDiv.style.cssText = 'margin-top: 15px; padding: 10px; background: white; border-radius: 6px; font-size: 12px; color: var(--text-light);';
+ const contentLength = finalContent.length;
+ const wordCount = finalContent.split(/\s+/).length;
+ metaDiv.innerHTML = `Métadonnées: ${contentLength} caractères, ~${wordCount} mots`;
+ finalContentContainer.appendChild(metaDiv);
+ }
+ } else {
+ finalContentContainer.innerHTML = '⚠️ Aucun contenu final disponible dans le résultat
';
+ }
+
+ // Version History
+ const versionHistoryContainer = document.getElementById('versionHistory');
+ versionHistoryContainer.innerHTML = '';
+
+ if (result.versionHistory && result.versionHistory.length > 0) {
+ result.versionHistory.forEach(version => {
+ const div = document.createElement('div');
+ div.style.cssText = 'padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border-left: 4px solid var(--success);';
+
+ const versionLabel = document.createElement('strong');
+ versionLabel.textContent = `Version ${version.version}`;
+ versionLabel.style.color = 'var(--primary)';
+
+ const articleId = document.createElement('span');
+ articleId.textContent = ` - Article ID: ${version.articleId}`;
+ articleId.style.color = 'var(--text-dark)';
+
+ const modifications = document.createElement('span');
+ modifications.textContent = ` - ${version.modifications || 0} modifications`;
+ modifications.style.color = 'var(--text-light)';
+ modifications.style.fontSize = '13px';
+ modifications.style.marginLeft = '10px';
+
+ div.appendChild(versionLabel);
+ div.appendChild(articleId);
+ div.appendChild(modifications);
+
+ versionHistoryContainer.appendChild(div);
+ });
+ } else {
+ versionHistoryContainer.innerHTML = 'Aucune version sauvegardée
';
+ }
+
+ // Google Sheets Link
+ if (result.gsheetsLink) {
+ document.getElementById('gsheetsLinkContainer').style.display = 'block';
+ document.getElementById('gsheetsLink').href = result.gsheetsLink;
+ } else {
+ document.getElementById('gsheetsLinkContainer').style.display = 'none';
+ }
+
// Execution log
const logContainer = document.getElementById('executionLog');
logContainer.innerHTML = '';
diff --git a/server.js b/server.js
index 0bc39f5..6eb7d7c 100644
--- a/server.js
+++ b/server.js
@@ -4,10 +4,27 @@
// MODES: MANUAL (interface client) | AUTO (batch GSheets)
// ========================================
+const startupTime = Date.now();
+console.log(`[${Date.now() - startupTime}ms] Chargement dotenv...`);
require('dotenv').config();
+console.log(`[${Date.now() - startupTime}ms] Chargement ErrorReporting...`);
const { logSh } = require('./lib/ErrorReporting');
-const { ModeManager } = require('./lib/modes/ModeManager');
+
+console.log(`[${Date.now() - startupTime}ms] Chargement ModeManager...`);
+
+// ⚡ LAZY LOADING: Charger ModeManager seulement quand nécessaire
+let ModeManager = null;
+function getModeManager() {
+ if (!ModeManager) {
+ const loadStart = Date.now();
+ logSh('⚡ Chargement ModeManager (lazy)...', 'DEBUG');
+ ModeManager = require('./lib/modes/ModeManager').ModeManager;
+ console.log(`[${Date.now() - startupTime}ms] ModeManager chargé !`);
+ logSh(`⚡ ModeManager chargé en ${Date.now() - loadStart}ms`, 'DEBUG');
+ }
+ return ModeManager;
+}
/**
* SERVEUR SEO GENERATOR - MODES EXCLUSIFS
@@ -30,9 +47,10 @@ class SEOGeneratorServer {
// Gestion signaux système
this.setupSignalHandlers();
-
+
// Initialisation du gestionnaire de modes
- const mode = await ModeManager.initialize();
+ const MM = getModeManager();
+ const mode = await MM.initialize();
logSh(`🎯 Serveur démarré en mode ${mode.toUpperCase()}`, 'INFO');
logSh(`⏱️ Temps de démarrage: ${Date.now() - this.startTime}ms`, 'DEBUG');
@@ -57,7 +75,8 @@ class SEOGeneratorServer {
const version = require('./package.json').version || '1.0.0';
const nodeVersion = process.version;
const platform = process.platform;
-
+
+ // Bannière visuelle en console.log (pas de logging structuré)
console.log(`
╔════════════════════════════════════════════════════════════╗
║ SEO GENERATOR SERVER ║
@@ -75,7 +94,7 @@ class SEOGeneratorServer {
║ SERVER_MODE=auto npm start ║
╚════════════════════════════════════════════════════════════╝
`);
-
+
logSh('🚀 === SEO GENERATOR SERVER - DÉMARRAGE ===', 'INFO');
logSh(`📦 Version: ${version} | Node: ${nodeVersion}`, 'INFO');
}
@@ -84,17 +103,22 @@ class SEOGeneratorServer {
* Configure la gestion des signaux système
*/
setupSignalHandlers() {
- // Arrêt propre sur SIGTERM/SIGINT
+ // SIGINT (Ctrl+C) : Kill immédiat sans graceful shutdown
+ process.on('SIGINT', () => {
+ console.log('\n🛑 SIGINT reçu - Arrêt immédiat (hard kill)');
+ process.exit(0);
+ });
+
+ // Arrêt propre sur SIGTERM
process.on('SIGTERM', () => this.handleShutdownSignal('SIGTERM'));
- process.on('SIGINT', () => this.handleShutdownSignal('SIGINT'));
-
+
// Gestion erreurs non capturées
process.on('uncaughtException', (error) => {
logSh(`❌ ERREUR NON CAPTURÉE: ${error.message}`, 'ERROR');
logSh(`Stack: ${error.stack}`, 'ERROR');
this.gracefulShutdown(1);
});
-
+
process.on('unhandledRejection', (reason, promise) => {
logSh(`❌ PROMESSE REJETÉE: ${reason}`, 'ERROR');
logSh(`Promise: ${promise}`, 'DEBUG');
@@ -131,7 +155,8 @@ class SEOGeneratorServer {
}
// Arrêter le mode actuel via ModeManager
- await ModeManager.stopCurrentMode();
+ const MM = getModeManager();
+ await MM.stopCurrentMode();
// Nettoyage final
await this.finalCleanup();
@@ -160,7 +185,8 @@ class SEOGeneratorServer {
async finalCleanup() {
try {
// Sauvegarder l'état final
- ModeManager.saveModeState();
+ const MM = getModeManager();
+ MM.saveModeState();
// Autres nettoyages si nécessaire
@@ -190,7 +216,8 @@ class SEOGeneratorServer {
* Vérifie la santé du système
*/
performHealthCheck() {
- const status = ModeManager.getStatus();
+ const MM = getModeManager();
+ const status = MM.getStatus();
const memUsage = process.memoryUsage();
const uptime = process.uptime();
@@ -212,9 +239,10 @@ class SEOGeneratorServer {
*/
async switchMode(newMode, force = false) {
logSh(`🔄 Demande changement mode vers: ${newMode}`, 'INFO');
-
+
try {
- await ModeManager.switchToMode(newMode, force);
+ const MM = getModeManager();
+ await MM.switchToMode(newMode, force);
logSh(`✅ Changement mode réussi vers: ${newMode}`, 'INFO');
return true;
@@ -237,7 +265,7 @@ class SEOGeneratorServer {
platform: process.platform,
pid: process.pid
},
- mode: ModeManager.getStatus(),
+ mode: getModeManager().getStatus(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
@@ -253,7 +281,7 @@ if (require.main === module) {
const server = new SEOGeneratorServer();
server.start().catch((error) => {
- console.error('💥 ERREUR CRITIQUE DÉMARRAGE:', error.message);
+ logSh(`💥 ERREUR CRITIQUE DÉMARRAGE: ${error.message}`, 'ERROR');
process.exit(1);
});
}
diff --git a/tests/validators/AntiDetectionValidator.js b/tests/validators/AntiDetectionValidator.js
index 570a587..b4b3146 100644
--- a/tests/validators/AntiDetectionValidator.js
+++ b/tests/validators/AntiDetectionValidator.js
@@ -181,7 +181,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (0=clairement IA, 100=clairement humain)`;
- const response = await LLMManager.callLLM('openai', prompt, {
+ const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 500
});
diff --git a/tests/validators/PersonalityValidator.js b/tests/validators/PersonalityValidator.js
index a7208c4..fd136b3 100644
--- a/tests/validators/PersonalityValidator.js
+++ b/tests/validators/PersonalityValidator.js
@@ -257,7 +257,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (0=pas du tout cette personnalité, 100=parfaitement aligné)`;
- const response = await LLMManager.callLLM('openai', prompt, {
+ const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 400
});
diff --git a/tests/validators/QualityMetrics.js b/tests/validators/QualityMetrics.js
index 9c84a89..3b24d13 100644
--- a/tests/validators/QualityMetrics.js
+++ b/tests/validators/QualityMetrics.js
@@ -127,7 +127,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (qualité globale perçue par un lecteur)`;
- const response = await LLMManager.callLLM('openai', prompt, {
+ const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 300
});
diff --git a/tools/analyze-skipped-exports.js b/tools/analyze-skipped-exports.js
new file mode 100644
index 0000000..73577ae
--- /dev/null
+++ b/tools/analyze-skipped-exports.js
@@ -0,0 +1,212 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+
+const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
+
+/**
+ * Parse un fichier de session pour extraire les tool uses
+ */
+function parseSessionFile(filePath) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const tools = [];
+
+ // Chercher tous les blocs JSON qui contiennent des tool uses
+ const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
+ const matches = content.match(jsonBlockRegex);
+
+ if (!matches) return tools;
+
+ for (const match of matches) {
+ try {
+ const parsed = JSON.parse(match);
+ for (const item of parsed) {
+ if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
+ tools.push({
+ name: item.name,
+ input: item.input
+ });
+ }
+ }
+ } catch (e) {
+ // Skip invalid JSON
+ }
+ }
+
+ return tools;
+}
+
+/**
+ * Analyse pourquoi un Edit a été skippé
+ */
+function analyzeSkippedEdit(filePath, oldString) {
+ if (!fs.existsSync(filePath)) {
+ return { reason: 'FILE_NOT_EXIST', details: 'Fichier n\'existe pas' };
+ }
+
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ if (!content.includes(oldString)) {
+ // Vérifier si une partie de old_string existe
+ const oldLines = oldString.split('\n').filter(l => l.trim());
+ const matchingLines = oldLines.filter(line => content.includes(line.trim()));
+
+ if (matchingLines.length > 0) {
+ return {
+ reason: 'PARTIAL_MATCH',
+ details: `${matchingLines.length}/${oldLines.length} lignes trouvées - code probablement modifié`
+ };
+ } else {
+ return {
+ reason: 'NO_MATCH',
+ details: 'Code complètement différent - changement déjà appliqué ou code refactorisé'
+ };
+ }
+ }
+
+ return { reason: 'OK', details: 'Devrait fonctionner' };
+}
+
+/**
+ * Main
+ */
+function main() {
+ console.log('🔍 Analyse des exports Claude skippés...\n');
+
+ const sessionFiles = fs.readdirSync(EXPORTS_DIR)
+ .filter(f => f.endsWith('-session.md'))
+ .sort((a, b) => {
+ const numA = parseInt(a.split('-')[0]);
+ const numB = parseInt(b.split('-')[0]);
+ return numB - numA;
+ });
+
+ const skippedAnalysis = {
+ FILE_NOT_EXIST: [],
+ PARTIAL_MATCH: [],
+ NO_MATCH: [],
+ FILE_EXISTS: [] // Pour les Write
+ };
+
+ let totalSkipped = 0;
+
+ for (const sessionFile of sessionFiles) {
+ const filePath = path.join(EXPORTS_DIR, sessionFile);
+ const tools = parseSessionFile(filePath);
+
+ for (const tool of tools) {
+ if (tool.name === 'Edit') {
+ const { file_path, old_string } = tool.input;
+
+ if (!fs.existsSync(file_path)) {
+ skippedAnalysis.FILE_NOT_EXIST.push({
+ session: sessionFile,
+ file: file_path,
+ preview: old_string.substring(0, 80)
+ });
+ totalSkipped++;
+ } else {
+ const content = fs.readFileSync(file_path, 'utf-8');
+ if (!content.includes(old_string)) {
+ const analysis = analyzeSkippedEdit(file_path, old_string);
+
+ if (analysis.reason === 'PARTIAL_MATCH') {
+ skippedAnalysis.PARTIAL_MATCH.push({
+ session: sessionFile,
+ file: file_path,
+ details: analysis.details,
+ preview: old_string.substring(0, 80)
+ });
+ } else {
+ skippedAnalysis.NO_MATCH.push({
+ session: sessionFile,
+ file: file_path,
+ preview: old_string.substring(0, 80)
+ });
+ }
+ totalSkipped++;
+ }
+ }
+ } else if (tool.name === 'Write') {
+ const { file_path } = tool.input;
+ if (fs.existsSync(file_path)) {
+ skippedAnalysis.FILE_EXISTS.push({
+ session: sessionFile,
+ file: file_path
+ });
+ totalSkipped++;
+ }
+ }
+ }
+ }
+
+ console.log(`📊 Total skippés: ${totalSkipped}\n`);
+
+ console.log('═══════════════════════════════════════════════════════');
+ console.log('🚫 FICHIERS N\'EXISTANT PAS (Edit)');
+ console.log(` ${skippedAnalysis.FILE_NOT_EXIST.length} cas\n`);
+ const fileNotExistByFile = {};
+ for (const item of skippedAnalysis.FILE_NOT_EXIST) {
+ if (!fileNotExistByFile[item.file]) {
+ fileNotExistByFile[item.file] = 0;
+ }
+ fileNotExistByFile[item.file]++;
+ }
+ Object.entries(fileNotExistByFile)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .forEach(([file, count]) => {
+ console.log(` ${count}x - ${file}`);
+ });
+
+ console.log('\n═══════════════════════════════════════════════════════');
+ console.log('⚠️ CORRESPONDANCE PARTIELLE (Edit - code probablement modifié)');
+ console.log(` ${skippedAnalysis.PARTIAL_MATCH.length} cas\n`);
+ const partialByFile = {};
+ for (const item of skippedAnalysis.PARTIAL_MATCH) {
+ if (!partialByFile[item.file]) {
+ partialByFile[item.file] = 0;
+ }
+ partialByFile[item.file]++;
+ }
+ Object.entries(partialByFile)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .forEach(([file, count]) => {
+ console.log(` ${count}x - ${file}`);
+ });
+
+ console.log('\n═══════════════════════════════════════════════════════');
+ console.log('❌ AUCUNE CORRESPONDANCE (Edit - changement déjà appliqué)');
+ console.log(` ${skippedAnalysis.NO_MATCH.length} cas\n`);
+ const noMatchByFile = {};
+ for (const item of skippedAnalysis.NO_MATCH) {
+ if (!noMatchByFile[item.file]) {
+ noMatchByFile[item.file] = 0;
+ }
+ noMatchByFile[item.file]++;
+ }
+ Object.entries(noMatchByFile)
+ .sort((a, b) => b[1] - a[1])
+ .slice(0, 10)
+ .forEach(([file, count]) => {
+ console.log(` ${count}x - ${file}`);
+ });
+
+ console.log('\n═══════════════════════════════════════════════════════');
+ console.log('✅ FICHIERS DÉJÀ EXISTANTS (Write - comportement normal)');
+ console.log(` ${skippedAnalysis.FILE_EXISTS.length} cas\n`);
+ skippedAnalysis.FILE_EXISTS.forEach(item => {
+ console.log(` ${item.session} → ${item.file}`);
+ });
+
+ console.log('\n═══════════════════════════════════════════════════════');
+ console.log('💡 CONCLUSION:\n');
+ console.log(` ✅ Write skippés: ${skippedAnalysis.FILE_EXISTS.length} (NORMAL - ne pas écraser)`);
+ console.log(` ❌ Edit skippés (NO_MATCH): ${skippedAnalysis.NO_MATCH.length} (changements déjà appliqués)`);
+ console.log(` ⚠️ Edit skippés (PARTIAL_MATCH): ${skippedAnalysis.PARTIAL_MATCH.length} (code modifié depuis)`);
+ console.log(` 🚫 Edit skippés (FILE_NOT_EXIST): ${skippedAnalysis.FILE_NOT_EXIST.length} (fichiers supprimés?)\n`);
+}
+
+main();
diff --git a/tools/apply-claude-exports-fuzzy.js b/tools/apply-claude-exports-fuzzy.js
new file mode 100644
index 0000000..97bb09d
--- /dev/null
+++ b/tools/apply-claude-exports-fuzzy.js
@@ -0,0 +1,470 @@
+#!/usr/bin/env node
+
+/**
+ * apply-claude-exports-fuzzy.js
+ *
+ * Applique les exports Claude avec fuzzy matching amélioré
+ *
+ * AMÉLIORATIONS:
+ * - Normalisation des line endings (\r\n, \r, \n → \n unifié)
+ * - Ignore les différences d'espacement (espaces multiples, tabs)
+ * - Score de similarité abaissé à 85% pour plus de flexibilité
+ * - Matching robuste qui ne casse pas sur les variations d'espaces
+ *
+ * Usage:
+ * node tools/apply-claude-exports-fuzzy.js # Apply changes
+ * node tools/apply-claude-exports-fuzzy.js --dry-run # Preview only
+ */
+
+const fs = require('fs');
+const path = require('path');
+
+const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
+const LOGS_DIR = path.join(__dirname, '../logs');
+
+// Créer dossier logs si nécessaire
+if (!fs.existsSync(LOGS_DIR)) {
+ fs.mkdirSync(LOGS_DIR, { recursive: true });
+}
+
+// Fichier de log avec timestamp
+const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
+const LOG_FILE = path.join(LOGS_DIR, `apply-exports-fuzzy-${timestamp}.log`);
+
+// Configuration fuzzy matching
+const FUZZY_CONFIG = {
+ minSimilarity: 0.85, // Minimum 85% de similarité pour accepter le match (abaissé de 95% pour plus de flexibilité)
+ contextLines: 3, // Lignes de contexte avant/après
+ ignoreWhitespace: true, // Ignorer les différences d'espacement (espaces multiples, tabs)
+ ignoreComments: false, // Ignorer les différences dans les commentaires
+ normalizeLineEndings: true // Unifier \r\n, \r, \n en \n (activé par défaut)
+};
+
+/**
+ * Logger dans console ET fichier
+ */
+function log(message, onlyFile = false) {
+ const line = `${message}\n`;
+
+ // Écrire dans le fichier
+ fs.appendFileSync(LOG_FILE, line, 'utf-8');
+
+ // Écrire aussi dans la console (sauf si onlyFile)
+ if (!onlyFile) {
+ process.stdout.write(line);
+ }
+}
+
+/**
+ * Parse un fichier de session pour extraire les tool uses
+ */
+function parseSessionFile(filePath) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const tools = [];
+
+ const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
+ const matches = content.match(jsonBlockRegex);
+
+ if (!matches) return tools;
+
+ for (const match of matches) {
+ try {
+ const parsed = JSON.parse(match);
+ for (const item of parsed) {
+ if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
+ tools.push({
+ name: item.name,
+ input: item.input
+ });
+ }
+ }
+ } catch (e) {
+ // Skip invalid JSON
+ }
+ }
+
+ return tools;
+}
+
+/**
+ * Normaliser un texte complet pour la comparaison
+ * - Unifie les line endings (\r\n, \r, \n → \n)
+ * - Ignore les différences d'espacement selon config
+ */
+function normalizeText(text) {
+ let normalized = text;
+
+ // Unifier tous les retours à la ligne si configuré
+ if (FUZZY_CONFIG.normalizeLineEndings) {
+ normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+ }
+
+ if (FUZZY_CONFIG.ignoreWhitespace) {
+ // Réduire les espaces/tabs multiples en un seul espace
+ normalized = normalized.replace(/[ \t]+/g, ' ');
+ // Nettoyer les espaces en début/fin de chaque ligne
+ normalized = normalized.split('\n').map(line => line.trim()).join('\n');
+ }
+
+ return normalized;
+}
+
+/**
+ * Normaliser une ligne pour la comparaison
+ */
+function normalizeLine(line) {
+ if (FUZZY_CONFIG.ignoreWhitespace) {
+ // Trim + réduire les espaces multiples à un seul
+ return line.trim().replace(/\s+/g, ' ');
+ }
+ return line;
+}
+
+/**
+ * Calculer la similarité entre deux chaînes (Levenshtein simplifié)
+ */
+function calculateSimilarity(str1, str2) {
+ const len1 = str1.length;
+ const len2 = str2.length;
+
+ if (len1 === 0) return len2 === 0 ? 1.0 : 0.0;
+ if (len2 === 0) return 0.0;
+
+ // Matrice de distance
+ const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
+
+ for (let i = 0; i <= len1; i++) matrix[i][0] = i;
+ for (let j = 0; j <= len2; j++) matrix[0][j] = j;
+
+ for (let i = 1; i <= len1; i++) {
+ for (let j = 1; j <= len2; j++) {
+ const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
+ matrix[i][j] = Math.min(
+ matrix[i - 1][j] + 1, // deletion
+ matrix[i][j - 1] + 1, // insertion
+ matrix[i - 1][j - 1] + cost // substitution
+ );
+ }
+ }
+
+ const distance = matrix[len1][len2];
+ const maxLen = Math.max(len1, len2);
+ return 1 - (distance / maxLen);
+}
+
+/**
+ * Créer un diff détaillé ligne par ligne
+ */
+function createDiff(oldLines, newLines) {
+ const diff = [];
+ const maxLen = Math.max(oldLines.length, newLines.length);
+
+ for (let i = 0; i < maxLen; i++) {
+ const oldLine = oldLines[i];
+ const newLine = newLines[i];
+
+ if (oldLine === undefined) {
+ // Ligne ajoutée
+ diff.push({ type: 'add', line: newLine, lineNum: i });
+ } else if (newLine === undefined) {
+ // Ligne supprimée
+ diff.push({ type: 'del', line: oldLine, lineNum: i });
+ } else if (oldLine !== newLine) {
+ // Ligne modifiée
+ diff.push({ type: 'mod', oldLine, newLine, lineNum: i });
+ } else {
+ // Ligne identique
+ diff.push({ type: 'same', line: oldLine, lineNum: i });
+ }
+ }
+
+ return diff;
+}
+
+/**
+ * Trouver la meilleure position pour un fuzzy match
+ */
+function findFuzzyMatch(content, oldString) {
+ // 🔥 CRITICAL FIX: Unifier SEULEMENT line endings (comme dans applyEdit)
+ // pour que les positions correspondent au même format de texte
+ // On normalise les espaces SEULEMENT pour la COMPARAISON (ligne par ligne)
+ const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+ const oldStringWithUnifiedLineEndings = oldString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+
+ const contentLines = contentWithUnifiedLineEndings.split('\n');
+ const oldLines = oldStringWithUnifiedLineEndings.split('\n');
+
+ if (oldLines.length === 0) return null;
+
+ let bestMatch = null;
+ let bestScore = 0;
+
+ // Chercher dans tout le contenu
+ for (let startLine = 0; startLine <= contentLines.length - oldLines.length; startLine++) {
+ const candidateLines = contentLines.slice(startLine, startLine + oldLines.length);
+
+ // Calculer le score de similarité ligne par ligne
+ let totalScore = 0;
+ let matchedLines = 0;
+
+ for (let i = 0; i < oldLines.length; i++) {
+ const oldNorm = normalizeLine(oldLines[i]);
+ const candidateNorm = normalizeLine(candidateLines[i]);
+
+ const similarity = calculateSimilarity(oldNorm, candidateNorm);
+ totalScore += similarity;
+ if (similarity > 0.8) matchedLines++;
+ }
+
+ const avgScore = totalScore / oldLines.length;
+ const matchRatio = matchedLines / oldLines.length;
+
+ // Score combiné: moyenne de similarité + ratio de lignes matchées
+ const combinedScore = (avgScore * 0.7) + (matchRatio * 0.3);
+
+ if (combinedScore > bestScore && combinedScore >= FUZZY_CONFIG.minSimilarity) {
+ bestScore = combinedScore;
+ bestMatch = {
+ startLine,
+ endLine: startLine + oldLines.length,
+ score: combinedScore,
+ matchedLines,
+ totalLines: oldLines.length
+ };
+ }
+ }
+
+ return bestMatch;
+}
+
+/**
+ * Appliquer un Edit avec fuzzy matching
+ */
+function applyEdit(filePath, oldString, newString, dryRun = false) {
+ try {
+ if (!fs.existsSync(filePath)) {
+ log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
+ return { success: false, reason: 'FILE_NOT_EXIST' };
+ }
+
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ // 🔥 Essayer d'abord un match exact SANS normalisation (le plus rapide et sûr)
+ if (content.includes(oldString)) {
+ const newContent = content.replace(oldString, newString);
+
+ if (!dryRun) {
+ fs.writeFileSync(filePath, newContent, 'utf-8');
+ }
+
+ log(`✅ EDIT EXACT appliqué: ${filePath}`);
+ return { success: true, reason: 'EXACT_MATCH', method: 'exact' };
+ }
+
+ // Si pas de match exact, essayer le fuzzy matching (avec normalisation)
+ const fuzzyMatch = findFuzzyMatch(content, oldString);
+
+ if (fuzzyMatch) {
+ // 🔥 IMPORTANT: fuzzyMatch a trouvé les positions avec normalisation
+ // Mais on applique le remplacement sur les versions ORIGINALES (espaces préservés)
+ // On unifie SEULEMENT les line endings (\r\n → \n) pour que les positions correspondent
+
+ // Unifier line endings UNIQUEMENT (garder espaces originaux)
+ const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+ const newStringWithUnifiedLineEndings = newString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
+
+ const contentLines = contentWithUnifiedLineEndings.split('\n');
+ const newLines = newStringWithUnifiedLineEndings.split('\n');
+
+ // Capturer les lignes matchées ORIGINALES (AVANT remplacement)
+ const matchedLines = contentLines.slice(fuzzyMatch.startLine, fuzzyMatch.endLine);
+
+ // Remplacer la zone identifiée avec le patch ORIGINAL
+ const before = contentLines.slice(0, fuzzyMatch.startLine);
+ const after = contentLines.slice(fuzzyMatch.endLine);
+ const newContent = [...before, ...newLines, ...after].join('\n');
+
+ if (!dryRun) {
+ fs.writeFileSync(filePath, newContent, 'utf-8');
+ }
+
+ log(`🎯 EDIT FUZZY appliqué: ${filePath} (score: ${(fuzzyMatch.score * 100).toFixed(1)}%, lignes ${fuzzyMatch.startLine}-${fuzzyMatch.endLine})`);
+
+ // Créer un diff détaillé
+ const diff = createDiff(matchedLines, newLines);
+
+ log(`┌─ 📝 DIFF DÉTAILLÉ ────────────────────────────────────────────────`);
+ diff.forEach((item, idx) => {
+ const lineNum = String(fuzzyMatch.startLine + idx + 1).padStart(4, ' ');
+
+ if (item.type === 'same') {
+ // Ligne identique
+ log(`│ ${lineNum} │ ${item.line}`);
+ } else if (item.type === 'add') {
+ // Ligne ajoutée
+ log(`│ ${lineNum} │ + ${item.line}`);
+ } else if (item.type === 'del') {
+ // Ligne supprimée
+ log(`│ ${lineNum} │ - ${item.line}`);
+ } else if (item.type === 'mod') {
+ // Ligne modifiée - afficher les deux
+ log(`│ ${lineNum} │ - ${item.oldLine}`);
+ log(`│ ${lineNum} │ + ${item.newLine}`);
+ }
+ });
+ log(`└────────────────────────────────────────────────────────────────────`);
+ log('');
+
+ return {
+ success: true,
+ reason: 'FUZZY_MATCH',
+ method: 'fuzzy',
+ score: fuzzyMatch.score,
+ lines: `${fuzzyMatch.startLine}-${fuzzyMatch.endLine}`
+ };
+ }
+
+ log(`⏭️ SKIP Edit - Aucun match trouvé: ${filePath}`);
+ return { success: false, reason: 'NO_MATCH' };
+
+ } catch (e) {
+ log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
+ return { success: false, reason: 'ERROR', error: e.message };
+ }
+}
+
+/**
+ * Appliquer un Write sur un fichier
+ */
+function applyWrite(filePath, content, dryRun = false) {
+ try {
+ if (fs.existsSync(filePath)) {
+ log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
+ return { success: false, reason: 'FILE_EXISTS' };
+ }
+
+ const dir = path.dirname(filePath);
+ if (!fs.existsSync(dir)) {
+ if (!dryRun) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+ }
+
+ if (!dryRun) {
+ fs.writeFileSync(filePath, content, 'utf-8');
+ }
+
+ log(`✅ WRITE appliqué: ${filePath}`);
+ return { success: true, reason: 'CREATED' };
+
+ } catch (e) {
+ log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
+ return { success: false, reason: 'ERROR', error: e.message };
+ }
+}
+
+/**
+ * Main
+ */
+function main() {
+ // Check for dry-run mode
+ const dryRun = process.argv.includes('--dry-run');
+
+ log(`📝 Logs sauvegardés dans: ${LOG_FILE}`);
+ log('');
+
+ if (dryRun) {
+ log('🔍 MODE DRY-RUN: Aucun fichier ne sera modifié');
+ log('');
+ }
+
+ log('🔄 Application des exports Claude avec FUZZY MATCHING...');
+ log(`⚙️ Config: minSimilarity=${FUZZY_CONFIG.minSimilarity * 100}%, ignoreWhitespace=${FUZZY_CONFIG.ignoreWhitespace}`);
+ log('');
+
+ // Lire tous les fichiers de session
+ const sessionFiles = fs.readdirSync(EXPORTS_DIR)
+ .filter(f => f.endsWith('-session.md'))
+ .sort((a, b) => {
+ const numA = parseInt(a.split('-')[0]);
+ const numB = parseInt(b.split('-')[0]);
+ return numB - numA; // Ordre inverse: 15 -> 1
+ });
+
+ log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
+ log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}`);
+ log('');
+
+ const stats = {
+ totalEdits: 0,
+ totalWrites: 0,
+ exactMatches: 0,
+ fuzzyMatches: 0,
+ successWrites: 0,
+ skipped: 0,
+ errors: 0
+ };
+
+ for (const sessionFile of sessionFiles) {
+ const filePath = path.join(EXPORTS_DIR, sessionFile);
+ log('');
+ log(`📄 Traitement de: ${sessionFile}`);
+
+ const tools = parseSessionFile(filePath);
+ log(` ${tools.length} tool use(s) trouvé(s)`);
+
+ for (const tool of tools) {
+ if (tool.name === 'Edit') {
+ stats.totalEdits++;
+ const { file_path, old_string, new_string } = tool.input;
+ const result = applyEdit(file_path, old_string, new_string, dryRun);
+
+ if (result.success) {
+ if (result.method === 'exact') {
+ stats.exactMatches++;
+ } else if (result.method === 'fuzzy') {
+ stats.fuzzyMatches++;
+ }
+ } else {
+ if (result.reason === 'ERROR') {
+ stats.errors++;
+ } else {
+ stats.skipped++;
+ }
+ }
+ } else if (tool.name === 'Write') {
+ stats.totalWrites++;
+ const { file_path, content } = tool.input;
+ const result = applyWrite(file_path, content, dryRun);
+
+ if (result.success) {
+ stats.successWrites++;
+ } else {
+ stats.skipped++;
+ }
+ }
+ }
+ }
+
+ log('');
+ log('');
+ log('📊 RÉSUMÉ:');
+ log(` Edit Exact: ${stats.exactMatches}/${stats.totalEdits} appliqués`);
+ log(` Edit Fuzzy: ${stats.fuzzyMatches}/${stats.totalEdits} appliqués`);
+ log(` Write: ${stats.successWrites}/${stats.totalWrites} appliqués`);
+ log(` Skippés: ${stats.skipped}`);
+ log(` Erreurs: ${stats.errors}`);
+ log(` Total: ${stats.exactMatches + stats.fuzzyMatches + stats.successWrites}/${stats.totalEdits + stats.totalWrites} opérations réussies`);
+ log('');
+
+ if (dryRun) {
+ log('💡 Pour appliquer réellement, relancez sans --dry-run');
+ } else {
+ log('✨ Terminé!');
+ }
+
+ log('');
+ log(`📝 Logs complets: ${LOG_FILE}`);
+}
+
+main();
diff --git a/tools/apply-claude-exports.js b/tools/apply-claude-exports.js
new file mode 100644
index 0000000..2b01fe4
--- /dev/null
+++ b/tools/apply-claude-exports.js
@@ -0,0 +1,146 @@
+#!/usr/bin/env node
+
+const fs = require('fs');
+const path = require('path');
+
+const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
+
+/**
+ * Parse un fichier de session pour extraire les tool uses
+ */
+function parseSessionFile(filePath) {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ const tools = [];
+
+ // Chercher tous les blocs JSON qui contiennent des tool uses
+ const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
+ const matches = content.match(jsonBlockRegex);
+
+ if (!matches) return tools;
+
+ for (const match of matches) {
+ try {
+ const parsed = JSON.parse(match);
+ for (const item of parsed) {
+ if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
+ tools.push({
+ name: item.name,
+ input: item.input
+ });
+ }
+ }
+ } catch (e) {
+ // Skip invalid JSON
+ }
+ }
+
+ return tools;
+}
+
+/**
+ * Applique un Edit sur un fichier
+ */
+function applyEdit(filePath, oldString, newString) {
+ try {
+ if (!fs.existsSync(filePath)) {
+ console.log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
+ return false;
+ }
+
+ const content = fs.readFileSync(filePath, 'utf-8');
+
+ if (!content.includes(oldString)) {
+ console.log(`⏭️ SKIP Edit - old_string non trouvée dans: ${filePath}`);
+ return false;
+ }
+
+ const newContent = content.replace(oldString, newString);
+ fs.writeFileSync(filePath, newContent, 'utf-8');
+ console.log(`✅ EDIT appliqué: ${filePath}`);
+ return true;
+ } catch (e) {
+ console.log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
+ return false;
+ }
+}
+
+/**
+ * Applique un Write sur un fichier
+ */
+function applyWrite(filePath, content) {
+ try {
+ if (fs.existsSync(filePath)) {
+ console.log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
+ return false;
+ }
+
+ // Créer les dossiers parents si nécessaire
+ const dir = path.dirname(filePath);
+ if (!fs.existsSync(dir)) {
+ fs.mkdirSync(dir, { recursive: true });
+ }
+
+ fs.writeFileSync(filePath, content, 'utf-8');
+ console.log(`✅ WRITE appliqué: ${filePath}`);
+ return true;
+ } catch (e) {
+ console.log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
+ return false;
+ }
+}
+
+/**
+ * Main
+ */
+function main() {
+ console.log('🔄 Application des exports Claude...\n');
+
+ // Lire tous les fichiers de session
+ const sessionFiles = fs.readdirSync(EXPORTS_DIR)
+ .filter(f => f.endsWith('-session.md'))
+ .sort((a, b) => {
+ const numA = parseInt(a.split('-')[0]);
+ const numB = parseInt(b.split('-')[0]);
+ return numB - numA; // Ordre inverse: 15 -> 1
+ });
+
+ console.log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
+ console.log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}\n`);
+
+ let totalEdits = 0;
+ let totalWrites = 0;
+ let successEdits = 0;
+ let successWrites = 0;
+
+ for (const sessionFile of sessionFiles) {
+ const filePath = path.join(EXPORTS_DIR, sessionFile);
+ console.log(`\n📄 Traitement de: ${sessionFile}`);
+
+ const tools = parseSessionFile(filePath);
+ console.log(` ${tools.length} tool use(s) trouvé(s)`);
+
+ for (const tool of tools) {
+ if (tool.name === 'Edit') {
+ totalEdits++;
+ const { file_path, old_string, new_string } = tool.input;
+ if (applyEdit(file_path, old_string, new_string)) {
+ successEdits++;
+ }
+ } else if (tool.name === 'Write') {
+ totalWrites++;
+ const { file_path, content } = tool.input;
+ if (applyWrite(file_path, content)) {
+ successWrites++;
+ }
+ }
+ }
+ }
+
+ console.log('\n\n📊 RÉSUMÉ:');
+ console.log(` Edit: ${successEdits}/${totalEdits} appliqués`);
+ console.log(` Write: ${successWrites}/${totalWrites} appliqués`);
+ console.log(` Total: ${successEdits + successWrites}/${totalEdits + totalWrites} opérations réussies`);
+ console.log('\n✨ Terminé!');
+}
+
+main();
diff --git a/tools/audit-unused.cjs b/tools/audit-unused.cjs
index b557a6c..18f0642 100644
--- a/tools/audit-unused.cjs
+++ b/tools/audit-unused.cjs
@@ -23,6 +23,14 @@ const EXCLUSION_PATTERNS = [
/\.spec\./, // Spec files
/^scripts\//, // Build/deploy scripts
/^docs?\//, // Documentation
+ /^public\//, // Static files served by Express
+ /^reports\//, // Generated reports
+ /^cache\//, // Cache directory
+ /^configs\//, // Configuration files
+ /^logs\//, // Log files
+ /code\.js$/, // Generated bundle
+ /\.original\./, // Backup/original files
+ /\.refactored\./, // Refactored versions kept for reference
];
function getEntrypoints() {