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>
471 lines
16 KiB
JavaScript
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();
|