UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

415 lines 16.8 kB
/** * Release Readiness Detector for Smart Git Push * * Analyzes TODO.md, project state, and commit patterns to determine * if a release is ready, preventing endless development cycles * * Supports both TODO.md (standard) and todo.md (legacy) for compatibility */ import { readFileSync, existsSync } from 'fs'; import { join } from 'path'; /** * Main release readiness analysis function */ export async function analyzeReleaseReadiness(options) { const { projectPath, todoPath = join(projectPath, 'TODO.md'), minCompletionRate = 0.8, maxCriticalTodos = 0, includeAnalysis: _includeAnalysis = true, releaseType = 'minor' } = options; const result = { isReady: false, score: 0, confidence: 0, blockers: [], recommendations: [], milestones: [], summary: '' }; try { // 1. Analyze TODO.md if it exists if (existsSync(todoPath)) { const todoAnalysis = await analyzeTodoFile(todoPath, minCompletionRate); result.milestones = todoAnalysis.milestones; result.blockers.push(...todoAnalysis.blockers); } // 2. Analyze project state const projectAnalysis = await analyzeProjectState(projectPath); result.blockers.push(...projectAnalysis.blockers); // 3. Analyze commit patterns const commitAnalysis = await analyzeCommitPatterns(projectPath); result.blockers.push(...commitAnalysis.blockers); // 4. Calculate readiness score const scoringResult = calculateReadinessScore(result.milestones, result.blockers, { minCompletionRate, maxCriticalTodos, releaseType }); result.score = scoringResult.score; result.confidence = scoringResult.confidence; result.isReady = scoringResult.isReady; // 5. Generate recommendations result.recommendations = generateReleaseRecommendations(result.blockers, result.milestones, result.score, releaseType); // 6. Generate summary result.summary = generateSummary(result); return result; } catch (error) { // Silently handle release readiness analysis errors result.blockers.push({ type: 'unstable-code', severity: 'error', message: `Analysis failed: ${error instanceof Error ? error.message : String(error)}`, suggestedActions: ['Fix analysis errors before proceeding'] }); return result; } } /** * Analyze TODO.md file for completion status */ async function analyzeTodoFile(todoPath, minCompletionRate = 0.7) { const content = readFileSync(todoPath, 'utf8'); const milestones = []; const blockers = []; // Parse TODO.md structure const sections = parseTodoSections(content); for (const section of sections) { const milestone = analyzeTodoSection(section); milestones.push(milestone); // Check for release blockers if (milestone.criticalTodos > 0) { blockers.push({ type: 'critical-todos', severity: 'error', message: `${milestone.name} has ${milestone.criticalTodos} critical TODO items`, todoItems: milestone.blockers, suggestedActions: [ 'Complete critical TODO items before release', 'Reassess priority of TODO items', 'Consider moving non-critical items to next release' ] }); } if (milestone.completionRate < minCompletionRate) { blockers.push({ type: 'incomplete-features', severity: milestone.completionRate < 0.5 ? 'error' : 'warning', message: `${milestone.name} is only ${(milestone.completionRate * 100).toFixed(1)}% complete`, suggestedActions: [ 'Complete remaining tasks in this milestone', 'Consider deferring incomplete features to next release', 'Review milestone scope and priorities' ] }); } } return { milestones, blockers }; } /** * Parse TODO.md into sections/milestones */ function parseTodoSections(content) { const sections = []; const lines = content.split('\n'); let currentSection = null; for (const line of lines) { const trimmed = line.trim(); // Detect section headers (## Header) if (trimmed.startsWith('## ')) { if (currentSection) { sections.push(currentSection); } currentSection = { name: trimmed.substring(3).trim(), todos: [] }; } // Detect TODO items else if (trimmed.startsWith('- [') && currentSection) { const isCompleted = trimmed.startsWith('- [x]') || trimmed.startsWith('- [X]'); const text = trimmed.substring(isCompleted ? 6 : 5).trim(); const priority = extractPriority(text); const isCritical = priority === 'high' || text.toLowerCase().includes('critical') || text.toLowerCase().includes('blocker'); currentSection.todos.push({ text, completed: isCompleted, priority, isCritical }); } } if (currentSection) { sections.push(currentSection); } return sections; } /** * Extract priority from TODO text */ function extractPriority(text) { const lower = text.toLowerCase(); if (lower.includes('high') || lower.includes('urgent') || lower.includes('critical')) { return 'high'; } if (lower.includes('medium') || lower.includes('moderate')) { return 'medium'; } return 'low'; } /** * Analyze a TODO section for completion status */ function analyzeTodoSection(section) { const total = section.todos.length; const completed = section.todos.filter(t => t.completed).length; const criticalTodos = section.todos.filter(t => t.isCritical && !t.completed).length; const blockers = section.todos .filter(t => t.isCritical && !t.completed) .map(t => t.text); return { name: section.name, completed, total, completionRate: total > 0 ? completed / total : 1, criticalTodos, blockers }; } /** * Analyze project state for release readiness */ async function analyzeProjectState(projectPath) { const blockers = []; try { // Check if tests are configured (look for test scripts) const packageJsonPath = join(projectPath, 'package.json'); if (existsSync(packageJsonPath)) { try { const packageJsonContent = readFileSync(packageJsonPath, 'utf8'); const packageJson = JSON.parse(packageJsonContent); if (packageJson.scripts && packageJson.scripts.test) { // In a real implementation, we'd run the tests // For now, we'll just note that tests should be run blockers.push({ type: 'test-failures', severity: 'warning', message: 'Tests should be run before release', suggestedActions: [ 'Run test suite: npm test', 'Ensure all tests pass', 'Review test coverage' ] }); } } catch (parseError) { // Invalid package.json, skip test analysis } } // Check for common unstable indicators const unstablePatterns = [ 'TODO.md', 'FIXME.md', 'debug_*.js', 'temp_*.js', 'test_*.js' ]; // This would be expanded to check for actual unstable files if (unstablePatterns.length > 0) { // Use the patterns for analysis } } catch (error) { blockers.push({ type: 'unstable-code', severity: 'error', message: `Project analysis failed: ${error instanceof Error ? error.message : String(error)}`, suggestedActions: ['Fix project structure issues'] }); } return { blockers }; } /** * Analyze commit patterns for release readiness */ async function analyzeCommitPatterns(projectPath) { const blockers = []; try { // Check recent commit activity const { execSync } = await import('child_process'); // Get recent commits const recentCommits = execSync('git log --oneline -n 10', { cwd: projectPath, encoding: 'utf8' }).trim(); // Look for concerning patterns const commitLines = recentCommits.split('\n'); const concerningPatterns = [ /debug/i, /temp/i, /test/i, /wip/i, /work in progress/i, /fixme/i ]; const concerningCommits = commitLines.filter((line) => concerningPatterns.some(pattern => pattern.test(line))); if (concerningCommits.length > 0) { blockers.push({ type: 'unstable-code', severity: 'warning', message: `Recent commits contain concerning patterns: ${concerningCommits.length} commits`, suggestedActions: [ 'Review recent commits for temporary/debug code', 'Clean up any experimental or debug commits', 'Ensure commits represent stable, production-ready code' ] }); } // Check for uncommitted changes const status = execSync('git status --porcelain', { cwd: projectPath, encoding: 'utf8' }).trim(); if (status) { blockers.push({ type: 'unstable-code', severity: 'warning', message: 'Uncommitted changes detected', suggestedActions: [ 'Commit or stash uncommitted changes', 'Ensure working directory is clean before release' ] }); } } catch (error) { // Git commands failed - likely not a git repository blockers.push({ type: 'unstable-code', severity: 'info', message: 'Could not analyze git history', suggestedActions: ['Ensure project is under version control'] }); } return { blockers }; } /** * Calculate overall readiness score */ function calculateReadinessScore(milestones, blockers, options) { let score = 1.0; let confidence = 1.0; // Factor in milestone completion if (milestones.length > 0) { const avgCompletion = milestones.reduce((sum, m) => sum + m.completionRate, 0) / milestones.length; score *= avgCompletion; // Reduce confidence if completion rates vary widely const completionVariance = milestones.reduce((sum, m) => sum + Math.pow(m.completionRate - avgCompletion, 2), 0) / milestones.length; confidence *= Math.max(0.5, 1 - completionVariance); } // Factor in blockers (excluding test-failures warnings for base scoring) const errorCount = blockers.filter(b => b.severity === 'error').length; const warningCount = blockers.filter(b => b.severity === 'warning' && b.type !== 'test-failures').length; score *= Math.max(0, 1 - (errorCount * 0.3 + warningCount * 0.1)); // Critical todos are show-stoppers const criticalTodos = milestones.reduce((sum, m) => sum + m.criticalTodos, 0); if (criticalTodos > options.maxCriticalTodos) { score *= 0.3; // Heavily penalize critical todos } // Determine readiness (ignore test-failures warnings for readiness check) const criticalBlockers = blockers.filter(b => b.severity === 'error' || (b.severity === 'warning' && b.type !== 'test-failures')); const isReady = score >= options.minCompletionRate && criticalBlockers.length === 0 && criticalTodos <= options.maxCriticalTodos; return { score, confidence, isReady }; } /** * Generate release recommendations */ function generateReleaseRecommendations(blockers, milestones, score, _releaseType) { const recommendations = []; if (score >= 0.9) { recommendations.push('✅ Project appears ready for release'); recommendations.push('🚀 Consider creating a release candidate'); recommendations.push('📋 Run final pre-release checklist'); } else if (score >= 0.7) { recommendations.push('⚠️ Project is mostly ready with minor issues'); recommendations.push('🔧 Address remaining blockers before release'); recommendations.push('📊 Review milestone completion status'); } else if (score >= 0.5) { recommendations.push('🚧 Project needs significant work before release'); recommendations.push('📝 Focus on completing critical TODO items'); recommendations.push('🔍 Consider reducing scope for this release'); } else { recommendations.push('❌ Project is not ready for release'); recommendations.push('🛠️ Substantial development work required'); recommendations.push('📋 Review and prioritize TODO items'); } // Add specific recommendations based on blockers const criticalBlockers = blockers.filter(b => b.severity === 'error'); if (criticalBlockers.length > 0) { recommendations.push('🚨 Critical blockers must be resolved:'); criticalBlockers.forEach(blocker => { recommendations.push(` - ${blocker.message}`); // Add specific TODO items if available if (blocker.todoItems && blocker.todoItems.length > 0) { blocker.todoItems.forEach(todo => { recommendations.push(` • ${todo}`); }); } }); } // Add milestone-specific recommendations const incompleteMilestones = milestones.filter(m => m.completionRate < 0.8); if (incompleteMilestones.length > 0) { recommendations.push('📈 Focus on completing these milestones:'); incompleteMilestones.forEach(milestone => { recommendations.push(` - ${milestone.name}: ${(milestone.completionRate * 100).toFixed(1)}% complete`); }); } return recommendations; } /** * Generate summary of release readiness */ function generateSummary(result) { const scorePercentage = (result.score * 100).toFixed(1); const confidencePercentage = (result.confidence * 100).toFixed(1); let summary = `## Release Readiness: ${result.isReady ? '✅ READY' : '❌ NOT READY'}\n\n`; summary += `**Score**: ${scorePercentage}% (Confidence: ${confidencePercentage}%)\n\n`; if (result.milestones.length > 0) { summary += `### Milestone Status\n`; result.milestones.forEach(milestone => { const status = milestone.completionRate >= 0.8 ? '✅' : milestone.completionRate >= 0.5 ? '⚠️' : '❌'; summary += `- ${status} **${milestone.name}**: ${milestone.completed}/${milestone.total} (${(milestone.completionRate * 100).toFixed(1)}%)\n`; }); summary += '\n'; } if (result.blockers.length > 0) { summary += `### Release Blockers (${result.blockers.length})\n`; result.blockers.forEach(blocker => { const icon = blocker.severity === 'error' ? '🚨' : blocker.severity === 'warning' ? '⚠️' : '💡'; summary += `- ${icon} **${blocker.type}**: ${blocker.message}\n`; }); summary += '\n'; } return summary; } /** * Integration with existing MCP tools */ export async function integrateWithMcpTools(projectPath, options = {}) { try { // Use existing manage_todo tool if available // const { manageTodo } = await import('../tools/manage-todo-tool.js'); // Get current TODO status // const todoStatus = await manageTodo({ // action: 'analyze_progress', // projectPath // }); // Enhance analysis with TODO tool data const enhancedOptions = { projectPath, includeAnalysis: true, ...options }; return await analyzeReleaseReadiness(enhancedOptions); } catch (error) { // Silently handle MCP tool integration errors // Fallback to standalone analysis return await analyzeReleaseReadiness({ projectPath, includeAnalysis: true, ...options }); } } //# sourceMappingURL=release-readiness-detector.js.map