seo-generator-server/tools/apply-claude-exports-fuzzy.js
StillHammer 64fb319e65 refactor: Synchronisation complète du codebase - Application de tous les patches
Application systématique et méthodique de tous les patches historiques.

##  FICHIERS SYNCHRONISÉS (19 fichiers)

### Core & Infrastructure:
- server.js (14 patches) - Lazy loading ModeManager, SIGINT hard kill, timing logs
- ModeManager.js (4 patches) - Instrumentation complète avec timing détaillé

### Pipeline System:
- PipelineDefinition.js (6 patches) - Source unique getLLMProvidersList()
- pipeline-builder.js (8 patches) - Standardisation LLM providers
- pipeline-runner.js (6 patches) - Affichage résultats structurés + debug console
- pipeline-builder.html (2 patches) - Fallback providers synchronisés
- pipeline-runner.html (3 patches) - UI améliorée résultats

### Enhancement Layers:
- TechnicalLayer.js (1 patch) - defaultLLM: 'gpt-4o-mini'
- StyleLayer.js (1 patch) - Type safety vocabulairePref
- PatternBreakingCore.js (1 patch) - Mapping modifications
- PatternBreakingLayers.js (1 patch) - LLM standardisé

### Validators & Tests:
- QualityMetrics.js (1 patch) - callLLM('gpt-4o-mini')
- PersonalityValidator.js (1 patch) - Provider gpt-4o-mini
- AntiDetectionValidator.js - Synchronisé

### Documentation:
- TODO.md (1 patch) - Section LiteLLM pour tracking coûts
- CLAUDE.md - Documentation à jour

### Tools:
- tools/analyze-skipped-exports.js (nouveau)
- tools/apply-claude-exports.js (nouveau)
- tools/apply-claude-exports-fuzzy.js (nouveau)

## 🎯 Changements principaux:
-  Standardisation LLM providers (openai → gpt-4o-mini, claude → claude-sonnet-4-5)
-  Lazy loading optimisé (ModeManager chargé à la demande)
-  SIGINT immediate exit (pas de graceful shutdown)
-  Type safety renforcé (conversions string explicites)
-  Instrumentation timing complète
-  Debug logging amélioré (console.log résultats pipeline)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 20:36:17 +08:00

471 lines
16 KiB
JavaScript

#!/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();