UNPKG

qraft

Version:

A powerful CLI tool to qraft structured project setups from GitHub template repositories

580 lines 23.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ConflictResolution = void 0; const manifestUtils_1 = require("../utils/manifestUtils"); const manifestManager_1 = require("./manifestManager"); class ConflictResolution { constructor(manifestManager) { this.manifestManager = manifestManager || new manifestManager_1.ManifestManager(); } createResolutionSession(analysis, comparison, diffSummary, options) { const session = { id: this.generateSessionId(), timestamp: new Date(), totalConflicts: 0, resolvedConflicts: 0, plans: [], autoResolved: [], requiresManualReview: [], manifestConflicts: [], manifestAutoResolved: [], manifestRequiresReview: [] }; // Create resolution plans for each file change for (const fileAnalysis of analysis.fileAnalyses) { const diff = diffSummary.files.find(d => d.path === fileAnalysis.path); const plan = this.createResolutionPlan(fileAnalysis, diff, options); session.plans.push(plan); session.totalConflicts++; // Categorize based on resolution approach if (this.canAutoResolve(plan, options)) { session.autoResolved.push(plan); session.resolvedConflicts++; } else { session.requiresManualReview.push(plan); } } // Process manifest conflicts if they exist if (comparison.manifest?.manifestConflicts) { for (const manifestConflict of comparison.manifest.manifestConflicts) { const manifestPlan = this.createManifestResolutionPlan(manifestConflict, comparison.manifest.manifestComparison, options); session.manifestConflicts.push(manifestPlan); session.totalConflicts++; // Categorize manifest conflicts if (this.canAutoResolveManifest(manifestPlan, options)) { session.manifestAutoResolved.push(manifestPlan); session.resolvedConflicts++; } else { session.manifestRequiresReview.push(manifestPlan); } } } return session; } createResolutionPlan(analysis, diff, options) { const choice = this.determineDefaultChoice(analysis, options); return { file: analysis.path, choice, analysis, diff }; } determineDefaultChoice(analysis, options) { // For new files (additions), default to use new if (analysis.changeType === 'addition') { return { action: 'use_new', reason: 'New file addition' }; } // For deletions, default to keep existing unless explicitly requested if (analysis.changeType === 'deletion') { return { action: 'keep_existing', reason: 'File deletion requires manual confirmation' }; } // For modifications, consider risk level and auto-resolve settings if (analysis.changeType === 'modification') { const autoLevel = options?.autoResolveLevel || 'safe'; // Critical files always require manual review if (analysis.impact.level === 'critical') { return { action: options?.createBackups ? 'backup_and_replace' : 'keep_existing', reason: 'Critical file requires manual review' }; } // High risk files if (analysis.impact.level === 'high') { if (autoLevel === 'aggressive') { return { action: 'backup_and_replace', reason: 'High risk but aggressive auto-resolve enabled' }; } return { action: 'keep_existing', reason: 'High risk file requires manual review' }; } // Medium risk files if (analysis.impact.level === 'medium') { if (autoLevel === 'moderate' || autoLevel === 'aggressive') { return { action: options?.createBackups ? 'backup_and_replace' : 'use_new', reason: 'Medium risk with moderate auto-resolve' }; } return { action: 'keep_existing', reason: 'Medium risk requires review in safe mode' }; } // Low risk files return { action: 'use_new', reason: 'Low risk modification' }; } // Default fallback return { action: 'keep_existing', reason: 'Unknown change type' }; } /** * Create a resolution plan for manifest conflicts * @param conflictInfo Manifest conflict information * @param manifestComparison Optional manifest comparison result * @param options Resolution options * @returns ManifestConflictResolutionPlan */ createManifestResolutionPlan(conflictInfo, manifestComparison, options) { const choice = this.determineManifestDefaultChoice(conflictInfo, options); const priority = this.getManifestConflictPriority(conflictInfo); const plan = { conflictInfo, choice, priority }; if (manifestComparison) { plan.manifestComparison = manifestComparison; } return plan; } /** * Get manifest manager instance for advanced operations * @returns ManifestManager instance */ getManifestManager() { return this.manifestManager; } /** * Determine default choice for manifest conflicts * @param conflictInfo Manifest conflict information * @param options Resolution options * @returns ManifestConflictResolutionChoice */ determineManifestDefaultChoice(conflictInfo, options) { // Handle different types of manifest conflicts switch (conflictInfo.type) { case 'manifest_version': // Version conflicts are critical - require careful handling if (conflictInfo.severity === 'high') { return { action: 'backup_and_replace', reason: 'Version conflict requires backup before update', mergeStrategy: 'version_priority' }; } return { action: 'use_new', reason: 'Minor version update' }; case 'manifest_metadata': // Metadata conflicts can often be auto-resolved if (conflictInfo.severity === 'low') { return { action: 'use_new', reason: 'Low-risk metadata update' }; } return { action: options?.createBackups ? 'backup_and_replace' : 'manual_merge', reason: 'Metadata conflict requires review', mergeStrategy: 'field_by_field' }; case 'manifest_missing': return { action: 'use_new', reason: 'Add missing manifest' }; case 'manifest_corrupted': return { action: 'backup_and_replace', reason: 'Replace corrupted manifest with backup' }; default: return { action: 'manual_merge', reason: 'Unknown manifest conflict type requires manual review', mergeStrategy: 'custom' }; } } /** * Get priority level for manifest conflicts * @param conflictInfo Manifest conflict information * @returns Priority level */ getManifestConflictPriority(conflictInfo) { // Version conflicts are always high priority if (conflictInfo.type === 'manifest_version') { return conflictInfo.severity === 'high' ? 'critical' : 'high'; } // Corruption is critical if (conflictInfo.type === 'manifest_corrupted') { return 'critical'; } // Map severity to priority switch (conflictInfo.severity) { case 'high': return 'high'; case 'medium': return 'medium'; case 'low': default: return 'low'; } } /** * Check if manifest conflict can be auto-resolved * @param plan Manifest resolution plan * @param options Resolution options * @returns boolean */ canAutoResolveManifest(plan, options) { // Never auto-resolve in interactive mode if (options.interactiveMode) { return false; } // Never auto-resolve critical priority conflicts if (plan.priority === 'critical') { return false; } // Check auto-resolve level switch (options.autoResolveLevel) { case 'none': return false; case 'safe': return plan.priority === 'low' && (plan.conflictInfo.type === 'manifest_missing' || plan.conflictInfo.type === 'manifest_metadata'); case 'moderate': return plan.priority === 'low' || (plan.priority === 'medium' && plan.conflictInfo.type !== 'manifest_version'); case 'aggressive': return plan.priority === 'low' || plan.priority === 'medium' || plan.priority === 'high'; default: return false; } } canAutoResolve(plan, options) { // Never auto-resolve in interactive mode if (options.interactiveMode) { return false; } // Never auto-resolve critical files if (plan.analysis.impact.level === 'critical') { return false; } // Check auto-resolve level switch (options.autoResolveLevel) { case 'none': return false; case 'safe': return plan.analysis.impact.level === 'low' && plan.analysis.changeType === 'addition'; case 'moderate': return plan.analysis.impact.level === 'low' || (plan.analysis.impact.level === 'medium' && plan.analysis.changeType !== 'deletion'); case 'aggressive': return plan.analysis.impact.level !== 'critical' && plan.analysis.changeType !== 'deletion'; default: return false; } } // Interactive resolution methods async resolveInteractively(session, onPrompt) { for (const plan of session.requiresManualReview) { try { const choice = await onPrompt(plan); plan.choice = choice; session.resolvedConflicts++; } catch (error) { // User cancelled or error occurred plan.choice = { action: 'skip', reason: 'User cancelled or error occurred' }; } } return session; } // Generate resolution summary including manifest conflicts generateResolutionSummary(session) { const actions = {}; const manifestActions = {}; let highRiskActions = 0; let criticalManifestConflicts = 0; // Count file actions for (const plan of session.plans) { const action = plan.choice.action; actions[action] = (actions[action] || 0) + 1; if (plan.analysis.impact.level === 'critical' || plan.analysis.impact.level === 'high') { if (action === 'use_new' || action === 'backup_and_replace') { highRiskActions++; } } } // Count manifest actions for (const plan of session.manifestConflicts) { const action = plan.choice.action; manifestActions[action] = (manifestActions[action] || 0) + 1; if (plan.priority === 'critical' || plan.priority === 'high') { criticalManifestConflicts++; } } const resolvedPercent = Math.round((session.resolvedConflicts / session.totalConflicts) * 100); let riskAssessment = 'Low risk'; if (criticalManifestConflicts > 0 || highRiskActions > 0) { riskAssessment = (criticalManifestConflicts > 0 || highRiskActions > 2) ? 'High risk' : 'Medium risk'; } const actionSummary = Object.entries(actions) .map(([action, count]) => `${count} ${action.replace(/_/g, ' ')}`) .join(', '); const manifestSummary = Object.entries(manifestActions).length > 0 ? Object.entries(manifestActions) .map(([action, count]) => `${count} manifest ${action.replace(/_/g, ' ')}`) .join(', ') : ''; let text = `Resolution Summary: ${resolvedPercent}% resolved (${session.resolvedConflicts}/${session.totalConflicts})\n`; if (actionSummary) { text += `File Actions: ${actionSummary}\n`; } if (manifestSummary) { text += `Manifest Actions: ${manifestSummary}\n`; } text += `Risk Assessment: ${riskAssessment}`; return { text, actions: { ...actions, ...manifestActions }, riskAssessment }; } // Validate resolution plan validateResolutionPlan(plan) { const warnings = []; const errors = []; // Check for risky actions on critical files if (plan.analysis.impact.level === 'critical') { if (plan.choice.action === 'use_new') { warnings.push('Replacing critical file without backup'); } if (plan.choice.action === 'skip') { warnings.push('Skipping critical file may cause issues'); } } // Check for deletions if (plan.analysis.changeType === 'deletion' && plan.choice.action === 'use_new') { warnings.push('File will be deleted'); } // Check for breaking changes if (plan.analysis.content.hasBreakingChanges && plan.choice.action === 'use_new') { warnings.push('Changes may break existing functionality'); } // Validate backup paths if (plan.choice.action === 'backup_and_replace' && !plan.choice.backupPath) { errors.push('Backup path required for backup_and_replace action'); } return { valid: errors.length === 0, warnings, errors }; } // Generate backup filename generateBackupPath(originalPath, backupDir) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const path = require('path'); const ext = path.extname(originalPath); const base = path.basename(originalPath, ext); const dir = path.dirname(originalPath); const backupName = `${base}.backup-${timestamp}${ext}`; return path.join(backupDir, dir, backupName); } // Apply resolution plan (dry run or actual) async applyResolutionPlan(plan, options) { if (options.dryRun) { return { success: true, message: `[DRY RUN] Would ${plan.choice.action} for ${plan.file}` }; } try { switch (plan.choice.action) { case 'keep_existing': return { success: true, message: `Kept existing file: ${plan.file}` }; case 'use_new': return { success: true, message: `Updated file: ${plan.file}` }; case 'backup_and_replace': const backupPath = plan.choice.backupPath || this.generateBackupPath(plan.file, options.backupDirectory); return { success: true, message: `Backed up and replaced: ${plan.file}`, backupCreated: backupPath }; case 'skip': return { success: true, message: `Skipped file: ${plan.file}` }; default: return { success: false, message: `Unknown action: ${plan.choice.action}` }; } } catch (error) { return { success: false, message: `Error applying resolution: ${error}` }; } } generateSessionId() { return `resolution-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } // Get statistics about the resolution session including manifest conflicts getSessionStats(session) { const byAction = {}; const byRisk = {}; const manifestByPriority = {}; for (const plan of session.plans) { const action = plan.choice.action; const risk = plan.analysis.impact.level; byAction[action] = (byAction[action] || 0) + 1; byRisk[risk] = (byRisk[risk] || 0) + 1; } for (const plan of session.manifestConflicts) { const priority = plan.priority; manifestByPriority[priority] = (manifestByPriority[priority] || 0) + 1; } return { total: session.totalConflicts, autoResolved: session.autoResolved.length, manualReview: session.requiresManualReview.length, byAction, byRisk, manifestStats: { total: session.manifestConflicts.length, autoResolved: session.manifestAutoResolved.length, manualReview: session.manifestRequiresReview.length, byPriority: manifestByPriority } }; } /** * Execute manifest conflict resolution * @param plan Manifest resolution plan * @param targetDirectory Target directory for manifest operations * @param options Execution options * @returns Promise<ExecutionResult> */ async executeManifestResolution(plan, targetDirectory) { try { switch (plan.choice.action) { case 'keep_existing': return { success: true, message: `Kept existing manifest for ${plan.conflictInfo.manifestField || plan.conflictInfo.type}`, action: 'keep_existing' }; case 'use_new': return { success: true, message: `Will use new manifest for ${plan.conflictInfo.manifestField || plan.conflictInfo.type}`, action: 'use_new' }; case 'backup_and_replace': { let backupPath = plan.choice.backupPath; if (!backupPath) { try { backupPath = await manifestUtils_1.ManifestUtils.backupQraftDirectory(targetDirectory); } catch (error) { backupPath = `${targetDirectory}/.qraft-backup-${Date.now()}`; } } return { success: true, message: `Backed up manifest and will replace ${plan.conflictInfo.manifestField || plan.conflictInfo.type}`, action: 'backup_and_replace', backupPath }; } case 'manual_merge': return { success: false, message: `Manual merge required for ${plan.conflictInfo.manifestField || plan.conflictInfo.type}`, action: 'manual_merge', requiresManualIntervention: true }; case 'skip': return { success: true, message: `Skipped manifest conflict for ${plan.conflictInfo.manifestField || plan.conflictInfo.type}`, action: 'skip' }; default: return { success: false, message: `Unknown manifest resolution action: ${plan.choice.action}`, action: plan.choice.action }; } } catch (error) { return { success: false, message: `Failed to execute manifest resolution: ${error instanceof Error ? error.message : String(error)}`, action: plan.choice.action }; } } /** * Validate manifest resolution plan * @param plan Manifest resolution plan * @returns Validation result */ validateManifestResolutionPlan(plan) { const warnings = []; const errors = []; // Check for risky actions on critical manifest conflicts if (plan.priority === 'critical') { if (plan.choice.action === 'use_new') { warnings.push('Replacing critical manifest field without backup'); } if (plan.choice.action === 'skip') { warnings.push('Skipping critical manifest conflict may cause issues'); } } // Check for version conflicts if (plan.conflictInfo.type === 'manifest_version' && plan.choice.action === 'use_new') { warnings.push('Version change may affect compatibility'); } // Check for manual merge strategy if (plan.choice.action === 'manual_merge' && !plan.choice.mergeStrategy) { warnings.push('Manual merge action should specify merge strategy'); } return { valid: errors.length === 0, warnings, errors }; } } exports.ConflictResolution = ConflictResolution; //# sourceMappingURL=conflictResolution.js.map