UNPKG

automagik-genie

Version:

Universal AI development companion that can be initialized in any codebase

553 lines (488 loc) 16.8 kB
const fs = require('fs').promises; const path = require('path'); /** * MergeEngine - Handles smart merging of template updates with user customizations * Provides section-based merging with conflict resolution */ class MergeEngine { constructor() { this.mergeMarkers = { conflictStart: '<<<<<<< CURRENT', conflictDivider: '=======', conflictEnd: '>>>>>>> TEMPLATE', sectionStart: '<!-- MERGE_SECTION_START:', sectionEnd: '<!-- MERGE_SECTION_END:', customStart: '<!-- USER_CUSTOM_START -->', customEnd: '<!-- USER_CUSTOM_END -->' }; } /** * Execute merge based on analysis and user choices * @param {string} filePath - Path to file being merged * @param {Object} analysis - Analysis from DiffEngine * @param {Object} userChoices - User's merge preferences * @param {Object} options - Merge options * @returns {Object} Merge results */ async executeMerge(filePath, analysis, userChoices, options = {}) { const { dryRun = false, createBackup = true } = options; const mergeResult = { filePath, strategy: analysis.mergeStrategy, success: false, changes: [], conflicts: [], warnings: [], backupPath: null }; try { // Create backup if requested if (createBackup && !dryRun) { mergeResult.backupPath = await this.createFileMergeBackup(filePath); } // Execute merge based on strategy let mergedContent; switch (analysis.mergeStrategy) { case 'safe': mergedContent = await this.executeSafeMerge(filePath, analysis); break; case 'merge': mergedContent = await this.executeSmartMerge(filePath, analysis, userChoices); break; case 'manual': mergedContent = await this.executeManualMerge(filePath, analysis, userChoices); break; case 'skip': mergeResult.success = true; mergeResult.changes.push({ type: 'skip', description: 'File skipped due to user customizations' }); return mergeResult; default: throw new Error(`Unknown merge strategy: ${analysis.mergeStrategy}`); } // Validate merged content const validation = await this.validateMergedContent(mergedContent, analysis); if (!validation.valid) { throw new Error(`Merge validation failed: ${validation.errors.join(', ')}`); } // Write merged content if not dry run if (!dryRun) { await fs.writeFile(filePath, mergedContent, 'utf-8'); } mergeResult.success = true; mergeResult.changes = this.generateChangeReport(analysis, userChoices); mergeResult.warnings = validation.warnings; return mergeResult; } catch (error) { mergeResult.success = false; mergeResult.error = error.message; // Restore from backup if merge failed and backup exists if (mergeResult.backupPath && !dryRun) { try { await this.restoreFromMergeBackup(filePath, mergeResult.backupPath); } catch (restoreError) { mergeResult.warnings.push(`Failed to restore from backup: ${restoreError.message}`); } } throw error; } } /** * Execute safe merge - only template updates, no user customizations * @param {string} filePath - File path * @param {Object} analysis - Merge analysis * @returns {string} Merged content */ async executeSafeMerge(filePath, analysis) { const templateSections = analysis.templateSections; const currentContent = await fs.readFile(filePath, 'utf-8'); // If no template changes, return current content if (templateSections.length === 0) { return currentContent; } // Replace template sections with updated versions let mergedContent = currentContent; for (const section of templateSections) { if (section.action === 'update') { mergedContent = this.replaceSectionContent( mergedContent, section.title, section.newContent.join('\n') ); } else if (section.action === 'add') { mergedContent = this.addNewSection( mergedContent, section.title, section.content.join('\n') ); } } return mergedContent; } /** * Execute smart merge - combine template updates with user customizations * @param {string} filePath - File path * @param {Object} analysis - Merge analysis * @param {Object} userChoices - User preferences * @returns {string} Merged content */ async executeSmartMerge(filePath, analysis, userChoices) { const currentContent = await fs.readFile(filePath, 'utf-8'); let mergedContent = currentContent; // Process template sections first for (const section of analysis.templateSections) { const userChoice = userChoices[section.title] || 'auto'; switch (userChoice) { case 'auto': case 'accept': if (section.action === 'update') { mergedContent = this.replaceSectionContent( mergedContent, section.title, section.newContent.join('\n') ); } else if (section.action === 'add') { mergedContent = this.addNewSection( mergedContent, section.title, section.content.join('\n') ); } break; case 'reject': // Keep current version - no action needed break; case 'merge': mergedContent = this.mergeSectionContent( mergedContent, section.title, section.oldContent.join('\n'), section.newContent.join('\n') ); break; } } // User sections are preserved automatically // (they weren't in templateSections, so they remain unchanged) return mergedContent; } /** * Execute manual merge - handle conflicts with user resolution * @param {string} filePath - File path * @param {Object} analysis - Merge analysis * @param {Object} userChoices - User conflict resolutions * @returns {string} Merged content */ async executeManualMerge(filePath, analysis, userChoices) { const currentContent = await fs.readFile(filePath, 'utf-8'); let mergedContent = currentContent; // Process conflicts first for (const conflict of analysis.conflicts) { const resolution = userChoices[conflict.title] || 'keep-current'; switch (resolution) { case 'keep-current': // No action needed - keep current version break; case 'use-template': mergedContent = this.replaceSectionContent( mergedContent, conflict.title, conflict.templateContent.join('\n') ); break; case 'manual-merge': mergedContent = this.createConflictMarkers( mergedContent, conflict.title, conflict.currentContent.join('\n'), conflict.templateContent.join('\n') ); break; } } // Process non-conflicting template sections for (const section of analysis.templateSections) { const userChoice = userChoices[section.title] || 'accept'; if (userChoice === 'accept') { if (section.action === 'update') { mergedContent = this.replaceSectionContent( mergedContent, section.title, section.newContent.join('\n') ); } else if (section.action === 'add') { mergedContent = this.addNewSection( mergedContent, section.title, section.content.join('\n') ); } } } return mergedContent; } /** * Replace section content in file * @param {string} content - File content * @param {string} sectionTitle - Section to replace * @param {string} newSectionContent - New content for section * @returns {string} Updated content */ replaceSectionContent(content, sectionTitle, newSectionContent) { const lines = content.split('\n'); const result = []; let inTargetSection = false; let sectionDepth = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const sectionMatch = line.match(/^(#{2,})\s+(.+)$/); if (sectionMatch) { const headerLevel = sectionMatch[1].length; const title = sectionMatch[2].trim(); if (title === sectionTitle && headerLevel === 2) { // Found target section start inTargetSection = true; sectionDepth = headerLevel; result.push(newSectionContent); continue; } else if (inTargetSection && headerLevel <= sectionDepth) { // Found end of target section inTargetSection = false; result.push(line); } else if (!inTargetSection) { result.push(line); } // Skip lines inside target section (they're being replaced) } else if (!inTargetSection) { result.push(line); } } return result.join('\n'); } /** * Add new section to file * @param {string} content - File content * @param {string} sectionTitle - New section title * @param {string} sectionContent - New section content * @returns {string} Updated content */ addNewSection(content, sectionTitle, sectionContent) { const lines = content.split('\n'); // Find appropriate insertion point (end of file or before last section) let insertIndex = lines.length; // Look for last section to insert before it for (let i = lines.length - 1; i >= 0; i--) { if (lines[i].match(/^##\s+/)) { insertIndex = i; break; } } // Insert new section const newSectionLines = sectionContent.split('\n'); const result = [ ...lines.slice(0, insertIndex), '', ...newSectionLines, '', ...lines.slice(insertIndex) ]; return result.join('\n'); } /** * Merge section content (three-way merge) * @param {string} content - File content * @param {string} sectionTitle - Section to merge * @param {string} oldContent - Original section content * @param {string} newContent - New template content * @returns {string} Updated content with merged section */ mergeSectionContent(content, sectionTitle, oldContent, newContent) { // For now, create conflict markers - later can implement smart merge return this.createConflictMarkers(content, sectionTitle, oldContent, newContent); } /** * Create conflict markers for manual resolution * @param {string} content - File content * @param {string} sectionTitle - Section with conflict * @param {string} currentContent - Current section content * @param {string} templateContent - Template section content * @returns {string} Content with conflict markers */ createConflictMarkers(content, sectionTitle, currentContent, templateContent) { const conflictSection = [ `${this.mergeMarkers.conflictStart}`, currentContent, `${this.mergeMarkers.conflictDivider}`, templateContent, `${this.mergeMarkers.conflictEnd}` ].join('\n'); return this.replaceSectionContent(content, sectionTitle, conflictSection); } /** * Validate merged content * @param {string} content - Merged content * @param {Object} analysis - Original analysis * @returns {Object} Validation results */ async validateMergedContent(content, analysis) { const validation = { valid: true, errors: [], warnings: [] }; // Check for unclosed conflict markers const conflictMarkers = [ this.mergeMarkers.conflictStart, this.mergeMarkers.conflictDivider, this.mergeMarkers.conflictEnd ]; for (const marker of conflictMarkers) { if (content.includes(marker)) { const count = (content.match(new RegExp(marker, 'g')) || []).length; if (count % 2 !== 0) { validation.errors.push(`Unclosed conflict marker: ${marker}`); validation.valid = false; } } } // Check for malformed sections const sectionHeaders = content.match(/^#{2,}\s+.+$/gm) || []; for (const header of sectionHeaders) { if (!header.match(/^#{2,}\s+\S/)) { validation.warnings.push(`Potentially malformed section header: ${header}`); } } // Validate that required sections still exist const requiredSections = ['GENIE PERSONALITY CORE', 'ROUTING DECISION MATRIX', 'DEVELOPMENT STANDARDS']; for (const required of requiredSections) { if (!content.includes(required)) { validation.warnings.push(`Required section "${required}" may be missing`); } } return validation; } /** * Generate change report * @param {Object} analysis - Merge analysis * @param {Object} userChoices - User choices * @returns {Array} Array of changes made */ generateChangeReport(analysis, userChoices) { const changes = []; // Template updates for (const section of analysis.templateSections) { const choice = userChoices[section.title] || 'auto'; if (choice !== 'reject') { changes.push({ type: 'template-update', section: section.title, action: section.action, description: `Template section "${section.title}" ${section.action}d` }); } } // User customizations preserved for (const section of analysis.userSections) { changes.push({ type: 'user-preservation', section: section.title, action: 'preserve', description: `User section "${section.title}" preserved` }); } // Conflicts resolved for (const conflict of analysis.conflicts) { const resolution = userChoices[conflict.title] || 'keep-current'; changes.push({ type: 'conflict-resolution', section: conflict.title, action: resolution, description: `Conflict in "${conflict.title}" resolved: ${resolution}` }); } return changes; } /** * Create temporary backup for merge operation * @param {string} filePath - File to backup * @returns {string} Backup file path */ async createFileMergeBackup(filePath) { const timestamp = Date.now(); const backupPath = `${filePath}.merge-backup.${timestamp}`; const content = await fs.readFile(filePath, 'utf-8'); await fs.writeFile(backupPath, content, 'utf-8'); return backupPath; } /** * Restore file from merge backup * @param {string} filePath - Original file path * @param {string} backupPath - Backup file path */ async restoreFromMergeBackup(filePath, backupPath) { const backupContent = await fs.readFile(backupPath, 'utf-8'); await fs.writeFile(filePath, backupContent, 'utf-8'); // Clean up backup file try { await fs.unlink(backupPath); } catch (error) { // Ignore cleanup errors } } /** * Preview merge operation without making changes * @param {string} filePath - File path * @param {Object} analysis - Merge analysis * @param {Object} userChoices - User choices * @returns {Object} Merge preview */ async previewMerge(filePath, analysis, userChoices) { const preview = { filePath, strategy: analysis.mergeStrategy, changes: this.generateChangeReport(analysis, userChoices), sections: { toUpdate: [], toAdd: [], toPreserve: [], conflicts: [] } }; // Categorize sections for preview for (const section of analysis.templateSections) { const choice = userChoices[section.title] || 'auto'; if (choice !== 'reject') { if (section.action === 'update') { preview.sections.toUpdate.push({ title: section.title, choice, hasChanges: section.changes ? true : false }); } else if (section.action === 'add') { preview.sections.toAdd.push({ title: section.title, choice }); } } } for (const section of analysis.userSections) { preview.sections.toPreserve.push({ title: section.title, type: section.type }); } for (const conflict of analysis.conflicts) { const resolution = userChoices[conflict.title] || 'keep-current'; preview.sections.conflicts.push({ title: conflict.title, resolution, reason: conflict.reason }); } return preview; } } module.exports = { MergeEngine };