UNPKG

qraft

Version:

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

613 lines 24.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ChangeAnalysis = void 0; class ChangeAnalysis { analyzeChanges(comparison, diffSummary) { const fileAnalyses = this.analyzeFiles(comparison.files, diffSummary.files); const manifestAnalysis = this.analyzeManifestChanges(comparison.manifest); const impacts = this.calculateImpacts(fileAnalyses, manifestAnalysis); const overall = this.calculateOverallRisk(fileAnalyses, comparison, manifestAnalysis); const summary = this.generateSummary(comparison, manifestAnalysis); const recommendations = this.generateRecommendations(fileAnalyses, overall, manifestAnalysis); const result = { overall, summary, impacts, fileAnalyses, recommendations }; if (manifestAnalysis) { result.manifestAnalysis = manifestAnalysis; } return result; } analyzeFiles(comparisons, diffs) { const analyses = []; for (const comparison of comparisons) { if (comparison.status === 'unchanged') continue; const diff = diffs.find(d => d.path === comparison.path); const analysis = this.analyzeFile(comparison, diff); analyses.push(analysis); } return analyses; } analyzeManifestChanges(manifestComparison) { if (!manifestComparison || !manifestComparison.hasLocalManifest || !manifestComparison.hasRemoteManifest) { return undefined; } const hasChanges = manifestComparison.manifestComparison && !manifestComparison.manifestComparison.isIdentical; if (!hasChanges) { return { hasChanges: false, changeType: 'none', impact: { level: 'low', description: 'No manifest changes detected', affectedFiles: [], recommendations: [] }, riskFactors: [], changes: {}, recommendations: [] }; } const manifestComp = manifestComparison.manifestComparison; const changeType = this.determineManifestChangeType(manifestComp); const riskFactors = this.identifyManifestRiskFactors(manifestComp); const impact = this.calculateManifestImpact(changeType, riskFactors, manifestComp); const changes = this.analyzeManifestChanges_Details(manifestComp); const recommendations = this.generateManifestRecommendations(changeType, riskFactors, changes); return { hasChanges: true, changeType, impact, riskFactors, changes, recommendations }; } analyzeFile(comparison, diff) { const riskFactors = this.identifyRiskFactors(comparison, diff); const impact = this.calculateFileImpact(comparison, riskFactors); const size = this.calculateSizeChange(comparison); const content = this.analyzeContentChanges(comparison, diff); return { path: comparison.path, changeType: this.mapChangeType(comparison.status), impact, riskFactors, size, content }; } identifyRiskFactors(comparison, diff) { const factors = []; // File type risks if (this.isCriticalFile(comparison.path)) { factors.push('Critical system file'); } if (this.isConfigurationFile(comparison.path)) { factors.push('Configuration file'); } if (this.isExecutableFile(comparison.path)) { factors.push('Executable file'); } // Change size risks if (comparison.changes?.sizeChange && Math.abs(comparison.changes.sizeChange) > 10000) { factors.push('Large size change'); } // Content risks if (comparison.similarity !== undefined && comparison.similarity < 0.5) { factors.push('Major content changes'); } if (comparison.changes?.extensionChanged) { factors.push('File extension changed'); } // Binary file risks if (diff?.isBinary) { factors.push('Binary file'); } // Deletion risks if (comparison.status === 'deleted') { factors.push('File deletion'); } return factors; } isCriticalFile(path) { const criticalPatterns = [ /package\.json$/, /package-lock\.json$/, /yarn\.lock$/, /Dockerfile$/, /docker-compose\.ya?ml$/, /\.env$/, /\.env\./, /requirements\.txt$/, /Pipfile$/, /Cargo\.toml$/, /go\.mod$/, /pom\.xml$/, /build\.gradle$/ ]; return criticalPatterns.some(pattern => pattern.test(path)); } isConfigurationFile(path) { const configPatterns = [ /\.config\./, /\.conf$/, /\.ini$/, /\.properties$/, /\.toml$/, /\.ya?ml$/, /\.json$/, /\.xml$/ ]; return configPatterns.some(pattern => pattern.test(path)); } isExecutableFile(path) { const executablePatterns = [ /\.sh$/, /\.bat$/, /\.cmd$/, /\.exe$/, /\.bin$/, /\.run$/ ]; return executablePatterns.some(pattern => pattern.test(path)); } calculateFileImpact(comparison, riskFactors) { let level = 'low'; const affectedFiles = [comparison.path]; const recommendations = []; // Determine impact level based on risk factors if (riskFactors.includes('Critical system file') || riskFactors.includes('File deletion') || riskFactors.includes('Major content changes')) { level = 'critical'; recommendations.push('Manual review required before applying changes'); recommendations.push('Create backup before proceeding'); } else if (riskFactors.includes('Configuration file') || riskFactors.includes('Large size change') || riskFactors.includes('File extension changed')) { level = 'high'; recommendations.push('Review changes carefully'); recommendations.push('Test after applying changes'); } else if (riskFactors.includes('Binary file') || riskFactors.includes('Executable file')) { level = 'medium'; recommendations.push('Verify binary file integrity'); } const description = this.generateImpactDescription(comparison, level); return { level, description, affectedFiles, recommendations }; } generateImpactDescription(comparison, level) { const action = comparison.status === 'added' ? 'Adding' : comparison.status === 'deleted' ? 'Deleting' : comparison.status === 'modified' ? 'Modifying' : 'Changing'; return `${action} ${comparison.path} (${level} impact)`; } calculateSizeChange(comparison) { const before = comparison.oldFile?.size || 0; const after = comparison.newFile?.size || 0; const change = after - before; const changePercent = before > 0 ? (change / before) * 100 : (after > 0 ? 100 : 0); return { before, after, change, changePercent }; } analyzeContentChanges(comparison, diff) { let linesAdded = 0; let linesDeleted = 0; if (diff) { for (const hunk of diff.hunks) { for (const line of hunk.lines) { if (line.type === 'added') linesAdded++; if (line.type === 'deleted') linesDeleted++; } } } const similarity = comparison.similarity || 1.0; const hasBreakingChanges = this.detectBreakingChanges(comparison, diff); return { linesAdded, linesDeleted, similarity, hasBreakingChanges }; } detectBreakingChanges(comparison, diff) { // Simple heuristics for breaking changes if (comparison.status === 'deleted') return true; if (comparison.changes?.extensionChanged) return true; if (comparison.similarity !== undefined && comparison.similarity < 0.3) return true; // Check for API-related changes in code files if (diff && this.isCodeFile(comparison.path)) { const content = diff.hunks.map(h => h.lines.map(l => l.content).join('\n')).join('\n'); const breakingPatterns = [ /export\s+.*\s+deleted/i, /function\s+\w+.*deleted/i, /class\s+\w+.*deleted/i, /interface\s+\w+.*deleted/i ]; return breakingPatterns.some(pattern => pattern.test(content)); } return false; } isCodeFile(path) { const codeExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cs', '.cpp', '.c', '.h']; return codeExtensions.some(ext => path.endsWith(ext)); } determineManifestChangeType(manifestComparison) { if (!manifestComparison || !manifestComparison.differences) { return 'none'; } const differences = manifestComparison.differences; // Check for version changes if (differences.some((diff) => diff.field === 'version')) { return 'version'; } // Check for metadata changes if (differences.some((diff) => ['name', 'description', 'author', 'tags'].includes(diff.field))) { return 'metadata'; } return 'metadata'; } identifyManifestRiskFactors(manifestComparison) { const factors = []; if (!manifestComparison || !manifestComparison.differences) { return factors; } const differences = manifestComparison.differences; // Version-related risk factors const versionDiff = differences.find((diff) => diff.field === 'version'); if (versionDiff) { factors.push('Version change detected'); // Check for major version changes const oldVersion = versionDiff.oldValue || '0.0.0'; const newVersion = versionDiff.newValue || '0.0.0'; if (this.isMajorVersionChange(oldVersion, newVersion)) { factors.push('Major version change'); } } // Metadata risk factors if (differences.some((diff) => diff.field === 'name')) { factors.push('Box name changed'); } if (differences.some((diff) => diff.field === 'author')) { factors.push('Author changed'); } if (differences.some((diff) => diff.field === 'defaultTarget')) { factors.push('Default target path changed'); } // Check for multiple changes if (differences.length > 3) { factors.push('Multiple manifest fields changed'); } return factors; } isMajorVersionChange(oldVersion, newVersion) { try { const oldMajor = parseInt(oldVersion.split('.')[0], 10); const newMajor = parseInt(newVersion.split('.')[0], 10); return newMajor !== oldMajor; } catch { return false; } } calculateManifestImpact(changeType, riskFactors, manifestComparison) { let level = 'low'; const recommendations = []; // Determine impact level based on change type and risk factors if (changeType === 'version') { if (riskFactors.includes('Major version change')) { level = 'high'; recommendations.push('Major version change detected - review compatibility'); recommendations.push('Test thoroughly before applying changes'); } else { level = 'medium'; recommendations.push('Version change detected - verify compatibility'); } } else if (changeType === 'metadata') { if (riskFactors.includes('Box name changed') || riskFactors.includes('Default target path changed')) { level = 'high'; recommendations.push('Critical metadata changed - review carefully'); } else if (riskFactors.includes('Multiple manifest fields changed')) { level = 'medium'; recommendations.push('Multiple metadata fields changed - review changes'); } else { level = 'low'; recommendations.push('Minor metadata changes detected'); } } const description = this.generateManifestImpactDescription(changeType, level, manifestComparison); return { level, description, affectedFiles: ['manifest.json'], recommendations }; } generateManifestImpactDescription(changeType, level, manifestComparison) { const changeCount = manifestComparison?.differences?.length || 0; switch (changeType) { case 'version': return `Manifest version change detected (${level} impact)`; case 'metadata': return `Manifest metadata changes detected: ${changeCount} field(s) (${level} impact)`; case 'missing': return `Missing manifest detected (${level} impact)`; case 'corrupted': return `Corrupted manifest detected (${level} impact)`; default: return `Manifest changes detected (${level} impact)`; } } analyzeManifestChanges_Details(manifestComparison) { const changes = {}; if (!manifestComparison || !manifestComparison.differences) { return changes; } const differences = manifestComparison.differences; // Analyze version changes const versionDiff = differences.find((diff) => diff.field === 'version'); if (versionDiff) { const oldVersion = versionDiff.oldValue || '0.0.0'; const newVersion = versionDiff.newValue || '0.0.0'; changes.versionChange = { from: oldVersion, to: newVersion, isUpgrade: this.isVersionUpgrade(oldVersion, newVersion), isMajorChange: this.isMajorVersionChange(oldVersion, newVersion) }; } // Analyze metadata changes const metadataFields = ['name', 'description', 'author', 'tags', 'defaultTarget']; const metadataChanges = differences.filter((diff) => metadataFields.includes(diff.field)); if (metadataChanges.length > 0) { changes.metadataChanges = metadataChanges.map((diff) => ({ field: diff.field, from: diff.oldValue, to: diff.newValue, impact: this.getMetadataChangeImpact(diff.field) })); } // Check for compatibility issues changes.compatibilityIssues = this.identifyCompatibilityIssues(differences); return changes; } isVersionUpgrade(oldVersion, newVersion) { try { const oldParts = oldVersion.split('.').map(n => parseInt(n, 10)); const newParts = newVersion.split('.').map(n => parseInt(n, 10)); for (let i = 0; i < Math.max(oldParts.length, newParts.length); i++) { const oldPart = oldParts[i] || 0; const newPart = newParts[i] || 0; if (newPart > oldPart) return true; if (newPart < oldPart) return false; } return false; } catch { return false; } } getMetadataChangeImpact(field) { switch (field) { case 'name': case 'defaultTarget': return 'high'; case 'author': case 'description': return 'medium'; case 'tags': return 'low'; default: return 'medium'; } } identifyCompatibilityIssues(differences) { const issues = []; // Check for breaking changes const nameDiff = differences.find(diff => diff.field === 'name'); if (nameDiff) { issues.push('Box name change may break existing references'); } const targetDiff = differences.find(diff => diff.field === 'defaultTarget'); if (targetDiff) { issues.push('Default target path change may affect deployment'); } return issues; } generateManifestRecommendations(changeType, riskFactors, changes) { const recommendations = []; if (changeType === 'version' && changes.versionChange) { if (changes.versionChange.isMajorChange) { recommendations.push('🚨 Major version change - review breaking changes'); recommendations.push('Update documentation and dependencies'); } else if (changes.versionChange.isUpgrade) { recommendations.push('✅ Version upgrade detected - verify new features'); } else { recommendations.push('⚠️ Version downgrade detected - check for compatibility issues'); } } if (riskFactors.includes('Box name changed')) { recommendations.push('📝 Update any scripts or references that use the old box name'); } if (riskFactors.includes('Default target path changed')) { recommendations.push('🎯 Verify the new target path is appropriate for your use case'); } if (changes.compatibilityIssues && changes.compatibilityIssues.length > 0) { recommendations.push('⚠️ Compatibility issues detected - review carefully'); } return recommendations; } mapChangeType(status) { switch (status) { case 'added': return 'addition'; case 'deleted': return 'deletion'; case 'modified': return 'modification'; default: return 'modification'; } } calculateImpacts(analyses, manifestAnalysis) { const impactMap = new Map(); // Add file impacts for (const analysis of analyses) { const key = `${analysis.impact.level}-${analysis.changeType}`; if (impactMap.has(key)) { const existing = impactMap.get(key); existing.affectedFiles.push(...analysis.impact.affectedFiles); } else { impactMap.set(key, { ...analysis.impact }); } } // Add manifest impact if present if (manifestAnalysis && manifestAnalysis.hasChanges) { const key = `${manifestAnalysis.impact.level}-manifest`; impactMap.set(key, { ...manifestAnalysis.impact }); } return Array.from(impactMap.values()); } calculateOverallRisk(analyses, comparison, manifestAnalysis) { const criticalCount = analyses.filter(a => a.impact.level === 'critical').length; const highCount = analyses.filter(a => a.impact.level === 'high').length; const mediumCount = analyses.filter(a => a.impact.level === 'medium').length; let riskLevel = 'low'; let confidence = 0.9; let requiresReview = false; let canAutoApply = true; // Factor in manifest changes let manifestRiskBoost = 0; if (manifestAnalysis && manifestAnalysis.hasChanges) { switch (manifestAnalysis.impact.level) { case 'critical': manifestRiskBoost = 4; break; case 'high': manifestRiskBoost = 3; break; case 'medium': manifestRiskBoost = 2; break; case 'low': manifestRiskBoost = 1; break; } } // Calculate risk with manifest considerations if (criticalCount > 0 || manifestRiskBoost >= 4) { riskLevel = 'critical'; confidence = 0.95; requiresReview = true; canAutoApply = false; } else if (highCount > 0 || comparison.summary.deleted > 0 || manifestRiskBoost >= 3) { riskLevel = 'high'; confidence = 0.85; requiresReview = true; canAutoApply = false; } else if (mediumCount > 2 || comparison.summary.modified > 5 || manifestRiskBoost >= 2) { riskLevel = 'medium'; confidence = 0.8; requiresReview = true; canAutoApply = false; } else if (comparison.summary.added > 10 || manifestRiskBoost >= 1) { riskLevel = 'medium'; confidence = 0.75; requiresReview = true; } return { riskLevel, confidence, requiresReview, canAutoApply }; } generateSummary(comparison, manifestAnalysis) { return { totalFiles: comparison.summary.added + comparison.summary.modified + comparison.summary.deleted, additions: comparison.summary.added, deletions: comparison.summary.deleted, modifications: comparison.summary.modified, renames: 0, // Not implemented yet manifestChanges: manifestAnalysis && manifestAnalysis.hasChanges ? 1 : 0 }; } generateRecommendations(analyses, overall, manifestAnalysis) { const recommendations = []; if (overall.riskLevel === 'critical') { recommendations.push('🚨 CRITICAL: Manual review required before proceeding'); recommendations.push('Create full backup of existing box'); recommendations.push('Test changes in isolated environment first'); } else if (overall.riskLevel === 'high') { recommendations.push('⚠️ HIGH RISK: Careful review recommended'); recommendations.push('Review all modified files individually'); recommendations.push('Consider incremental deployment'); } else if (overall.riskLevel === 'medium') { recommendations.push('📋 MEDIUM RISK: Review key changes'); recommendations.push('Verify configuration files'); } else { recommendations.push('✅ LOW RISK: Changes appear safe to apply'); } // Add manifest-specific recommendations if (manifestAnalysis && manifestAnalysis.hasChanges) { recommendations.push('📋 MANIFEST CHANGES DETECTED:'); recommendations.push(...manifestAnalysis.recommendations); } // Add specific recommendations based on file types const criticalFiles = analyses.filter(a => a.impact.level === 'critical'); if (criticalFiles.length > 0) { recommendations.push(`Review critical files: ${criticalFiles.map(f => f.path).join(', ')}`); } const binaryFiles = analyses.filter(a => a.riskFactors.includes('Binary file')); if (binaryFiles.length > 0) { recommendations.push('Verify binary file integrity after update'); } return recommendations; } // Get files requiring manual review getFilesRequiringReview(result) { return result.fileAnalyses.filter(analysis => analysis.impact.level === 'critical' || analysis.impact.level === 'high' || analysis.content.hasBreakingChanges); } // Get safe files that can be auto-applied getSafeFiles(result) { return result.fileAnalyses.filter(analysis => analysis.impact.level === 'low' && !analysis.content.hasBreakingChanges && !analysis.riskFactors.includes('Critical system file')); } } exports.ChangeAnalysis = ChangeAnalysis; //# sourceMappingURL=changeAnalysis.js.map