@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
1,103 lines (1,102 loc) • 46.6 kB
JavaScript
import process from 'node:process';
import { EnhancedConsole } from '../../shared/utils/cli-ui.js';
import { assessBusinessRelevance, assessChangeComplexity, assessFileImportance, assessOverallComplexity, assessRisk, categorizeFile, detectLanguage, generateAnalysisSummary, getWorkingDirectoryChanges, performSemanticAnalysis, } from '../../shared/utils/utils.js';
export class AnalysisEngine {
constructor(gitService, aiAnalysisService, gitManager = null) {
this.gitService = gitService;
this.aiAnalysisService = aiAnalysisService;
this.gitManager = gitManager;
this.gitRepoAnalyzer = null;
}
async _ensureGitAnalyzer() {
if (!this.gitRepoAnalyzer && this.gitService?.gitManager) {
// Use the existing GitService instead of separate analyzer
this.gitRepoAnalyzer = this.gitService;
}
return this.gitRepoAnalyzer;
}
analyze(type, config = {}) {
switch (type) {
case 'changes':
return this.analyzeCurrentChanges(config);
case 'commits':
return this.analyzeRecentCommits(config.limit || 10, config);
case 'branches':
return this.analyzeBranches(config);
case 'comprehensive':
return this.analyzeComprehensive(config);
case 'health':
return this.assessRepositoryHealth(config);
default:
throw new Error(`Unknown analysis type: ${type}`);
}
}
async analyzeCurrentChanges(config = {}) {
let enhancedChanges = [];
const suppressOutput = process.env.MCP_SERVER_MODE === 'true';
// Honor the includeAIAnalysis toggle (defaults to true when undefined).
const includeAIAnalysis = config.includeAIAnalysis !== false;
try {
// Get cwd from git manager if available
const cwd = this.gitService?.gitManager?.options?.cwd;
if (!suppressOutput) {
console.log(`[AnalysisEngine] analyzeCurrentChanges called with cwd: ${cwd || 'undefined'}`);
}
const changes = getWorkingDirectoryChanges(cwd);
if (changes.length === 0) {
if (!suppressOutput) {
EnhancedConsole.info('No changes detected in working directory.');
}
return { changes: [], analysis: null };
}
if (!suppressOutput) {
EnhancedConsole.processing(`Analyzing ${changes.length} working directory changes...`);
}
// Enhanced file analysis with actual diff content
enhancedChanges = [];
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
try {
// Show progress every 5 files or for last file to avoid spam
if (!suppressOutput && (i % 5 === 0 || i === changes.length - 1)) {
process.stdout.write(`\r Progress: ${i + 1}/${changes.length} files analyzed`);
}
// Use git service to get detailed diff analysis
let detailedAnalysis = null;
if (this.gitService?.analyzeWorkingDirectoryFileChange) {
detailedAnalysis = await this.gitService.analyzeWorkingDirectoryFileChange(change.status, change.path);
}
if (detailedAnalysis) {
// Use the rich analysis from git service
enhancedChanges.push({
...change,
...detailedAnalysis,
// Preserve original fields for compatibility
filePath: detailedAnalysis.filePath || change.path,
path: change.path,
});
}
else {
// Fallback to basic analysis
enhancedChanges.push({
...change,
category: categorizeFile(change.path),
language: detectLanguage(change.path),
importance: assessFileImportance(change.path, change.status),
complexity: assessChangeComplexity(change.diff || ''),
diff: change.diff || '',
semanticChanges: { changeType: 'unknown', patterns: [], frameworks: [] },
functionalImpact: { scope: 'local', severity: 'low' },
});
}
}
catch (error) {
EnhancedConsole.error(`Error processing change ${i}:`, change, error.message);
enhancedChanges.push({
...change,
category: 'other',
language: 'Unknown',
importance: 'medium',
complexity: { score: 1 },
diff: 'Analysis failed',
semanticChanges: { changeType: 'unknown', patterns: [], frameworks: [] },
functionalImpact: { scope: 'local', severity: 'low' },
});
}
}
// Clear the progress line
if (!suppressOutput) {
process.stdout.write('\n');
}
// AI analysis if available and enabled. When includeAIAnalysis === false the
// caller explicitly opted out, so we skip the AI call and use the
// rule-based/pattern path only.
let aiAnalysis = null;
if (this.aiAnalysisService.hasAI && includeAIAnalysis) {
try {
aiAnalysis = await this.aiAnalysisService.analyzeChanges(enhancedChanges, 'working-directory');
}
catch (error) {
EnhancedConsole.error('Error in AI analysis:', error.message);
aiAnalysis = null;
}
}
else if (!suppressOutput && this.aiAnalysisService.hasAI && !includeAIAnalysis) {
EnhancedConsole.info('AI analysis disabled (includeAIAnalysis=false); using rule-based analysis.');
}
// Rule-based analysis
let ruleBasedAnalysis = null;
try {
ruleBasedAnalysis = this.analyzeChangesRuleBased(enhancedChanges);
}
catch (error) {
EnhancedConsole.error('Error in rule-based analysis:', error.message);
ruleBasedAnalysis = {
summary: 'Analysis failed',
category: 'other',
impact: 'medium',
userFacing: false,
};
}
// Combine analyses
let finalAnalysis = null;
try {
finalAnalysis = this.combineAnalyses(aiAnalysis, ruleBasedAnalysis);
}
catch (error) {
EnhancedConsole.error('Error combining analyses:', error.message);
finalAnalysis = {
summary: `${enhancedChanges.length} working directory changes detected`,
category: 'working-directory',
impact: 'medium',
userFacing: false,
};
}
return {
changes: enhancedChanges,
analysis: finalAnalysis,
summary: generateAnalysisSummary(enhancedChanges, finalAnalysis),
};
}
catch (error) {
EnhancedConsole.error('Error analyzing current changes:', error.message);
// Return enhanced changes if they were created before the error
return {
changes: enhancedChanges,
analysis: enhancedChanges.length > 0
? {
summary: `${enhancedChanges.length} working directory changes detected (analysis failed)`,
category: 'working-directory',
impact: 'medium',
userFacing: false,
}
: null,
error: error.message,
};
}
}
async analyzeRecentCommits(limit = 10, config = {}) {
try {
EnhancedConsole.processing(`Analyzing recent ${limit} commits...`);
// Apply the caller's filters at the git level, then take the limit from the
// already-filtered set. Loading unfiltered history and slicing it ignored
// --author/--since/--tag-range entirely, so the CLI reported analysis for
// the wrong commits and the VS Code release view suggested versions from
// commits that were already released.
const { since = null, author, tagRange } = config;
let commits;
if (tagRange) {
const [fromRef, toRef] = String(tagRange).split('..');
commits = await this.gitService.getCommitsBetween(fromRef, toRef || 'HEAD', { author });
}
else {
commits = await this.gitService.getCommitsSince(since, { author });
}
const recentCommits = commits.slice(0, limit);
if (recentCommits.length === 0) {
EnhancedConsole.info('No recent commits found.');
return { commits: [], analysis: null };
}
// Analyze each commit
const analyzedCommits = [];
for (const commit of recentCommits) {
const analysis = await this.gitService.getCommitAnalysis(commit.hash);
if (analysis) {
// Enhanced analysis
const enhancedAnalysis = await this.enhanceCommitAnalysis(analysis);
analyzedCommits.push(enhancedAnalysis);
}
}
// Generate overall analysis
const overallAnalysis = this.generateOverallCommitAnalysis(analyzedCommits);
return {
commits: analyzedCommits,
analysis: overallAnalysis,
summary: this.generateCommitsSummary(analyzedCommits, overallAnalysis),
};
}
catch (error) {
EnhancedConsole.error('Error analyzing recent commits:', error.message);
return { commits: [], analysis: null, error: error.message };
}
}
enhanceCommitAnalysis(commitAnalysis) {
try {
// Add complexity assessment
const complexity = assessOverallComplexity(commitAnalysis.files.map((f) => f.diff).join('\n'), commitAnalysis.files.length);
// Add risk assessment
const risk = assessRisk(commitAnalysis.files.map((f) => f.diff).join('\n'), commitAnalysis.files.length, commitAnalysis.subject);
// Add business relevance
const businessRelevance = assessBusinessRelevance(commitAnalysis.subject, commitAnalysis.files.map((f) => f.filePath));
// Enhanced semantic analysis
const semanticAnalysis = performSemanticAnalysis(commitAnalysis.files, commitAnalysis.subject, commitAnalysis.body);
return {
...commitAnalysis,
complexity,
risk,
businessRelevance,
semanticAnalysis: {
...commitAnalysis.semanticAnalysis,
...semanticAnalysis,
},
};
}
catch (error) {
EnhancedConsole.warn(`Failed to enhance analysis for commit ${commitAnalysis.hash}:`, error.message);
return commitAnalysis;
}
}
analyzeChangesRuleBased(changes) {
const categories = this.categorizeChanges(changes);
const complexity = this.assessOverallChangeComplexity(changes);
const risk = this.assessOverallChangeRisk(changes);
return {
categories,
complexity,
risk,
totalFiles: changes.length,
primaryCategory: Object.keys(categories)[0] || 'other',
impact: this.assessChangeImpact(changes),
userFacing: this.hasUserFacingChanges(changes),
};
}
categorizeChanges(changes) {
const categories = {};
changes.forEach((change) => {
const category = change.category || 'other';
if (!categories[category]) {
categories[category] = [];
}
categories[category].push(change);
});
return categories;
}
assessOverallChangeComplexity(changes) {
const totalComplexity = changes.reduce((sum, change) => {
return sum + (change.complexity?.score || 0);
}, 0);
const avgComplexity = totalComplexity / changes.length;
if (avgComplexity > 0.7) {
return 'high';
}
if (avgComplexity > 0.4) {
return 'medium';
}
return 'low';
}
assessOverallChangeRisk(changes) {
const riskFactors = changes.filter((change) => change.importance === 'critical' || change.category === 'core' || change.status === 'D' // Deletions are risky
);
if (riskFactors.length > changes.length * 0.3) {
return 'high';
}
if (riskFactors.length > 0) {
return 'medium';
}
return 'low';
}
assessChangeImpact(changes) {
if (changes.length > 20) {
return 'high';
}
if (changes.length > 5) {
return 'medium';
}
return 'low';
}
hasUserFacingChanges(changes) {
return changes.some((change) => change.category === 'ui' ||
change.category === 'frontend' ||
change.path?.includes('/component/') ||
change.path?.includes('/page/') ||
change.path?.includes('/ui/'));
}
generateOverallCommitAnalysis(commits) {
const types = {};
let totalFiles = 0;
let totalLines = 0;
let breakingChanges = 0;
let highRiskCommits = 0;
commits.forEach((commit) => {
const type = commit.categories?.[0] || 'other';
types[type] = (types[type] || 0) + 1;
totalFiles += commit.files?.length || 0;
totalLines += (commit.diffStats?.insertions || 0) + (commit.diffStats?.deletions || 0);
if (commit.breakingChanges?.length > 0) {
breakingChanges++;
}
if (commit.risk === 'high') {
highRiskCommits++;
}
});
return {
totalCommits: commits.length,
commitTypes: types,
totalFiles,
totalLines,
breakingChanges,
highRiskCommits,
riskLevel: this.calculateOverallRisk(commits),
complexity: this.calculateOverallComplexity(commits),
trends: this.analyzeTrends(commits),
};
}
calculateOverallRisk(commits) {
const highRisk = commits.filter((c) => c.risk === 'high').length;
const ratio = highRisk / commits.length;
if (ratio > 0.3) {
return 'high';
}
if (ratio > 0.1) {
return 'medium';
}
return 'low';
}
calculateOverallComplexity(commits) {
const avgFiles = commits.reduce((sum, c) => sum + (c.files?.length || 0), 0) / commits.length;
const avgLines = commits.reduce((sum, c) => sum + ((c.diffStats?.insertions || 0) + (c.diffStats?.deletions || 0)), 0) / commits.length;
if (avgFiles > 15 || avgLines > 200) {
return 'high';
}
if (avgFiles > 5 || avgLines > 50) {
return 'medium';
}
return 'low';
}
analyzeTrends(commits) {
// Simple trend analysis based on recent patterns
const recentCommits = commits.slice(0, Math.min(5, commits.length));
const categories = recentCommits.map((c) => c.categories?.[0] || 'other');
const categoryCount = {};
categories.forEach((cat) => {
categoryCount[cat] = (categoryCount[cat] || 0) + 1;
});
const dominantCategory = Object.entries(categoryCount).sort(([, a], [, b]) => b - a)[0];
return {
dominantCategory: dominantCategory?.[0] || 'mixed',
frequency: dominantCategory?.[1] || 0,
pattern: this.identifyPattern(categories),
};
}
identifyPattern(categories) {
if (categories.every((cat) => cat === categories[0])) {
return 'focused';
}
if (new Set(categories).size === categories.length) {
return 'diverse';
}
return 'mixed';
}
combineAnalyses(aiAnalysis, ruleBasedAnalysis) {
if (!aiAnalysis) {
// No AI insights (unavailable or disabled); label the output truthfully.
return {
...ruleBasedAnalysis,
confidence: 'medium',
source: 'rules',
};
}
return {
...ruleBasedAnalysis,
aiInsights: aiAnalysis,
confidence: 'high',
source: 'ai+rules',
};
}
generateCommitsSummary(_commits, analysis) {
const { totalCommits, commitTypes, riskLevel, complexity } = analysis;
let summary = `Analyzed ${totalCommits} recent commits. `;
if (Object.keys(commitTypes).length > 0) {
const dominantType = Object.entries(commitTypes).sort(([, a], [, b]) => Number(b) - Number(a))[0];
summary += `Primary activity: ${dominantType[1]} ${dominantType[0]} commits. `;
}
summary += `Risk level: ${riskLevel}. Complexity: ${complexity}.`;
if (analysis.breakingChanges > 0) {
summary += ` ⚠️ ${analysis.breakingChanges} commits contain breaking changes.`;
}
return summary;
}
// Delegate to GitRepositoryAnalyzer for specialized analysis
async analyzeBranches(config) {
const analyzer = await this._ensureGitAnalyzer();
if (analyzer) {
return await analyzer.analyzeBranches(config?.format || 'markdown');
}
return { branches: [], analysis: 'Git analyzer not available' };
}
async analyzeComprehensive(config) {
const analyzer = await this._ensureGitAnalyzer();
if (analyzer) {
return await analyzer.analyzeComprehensive(config?.format || 'markdown');
}
return { comprehensive: true, analysis: 'Git analyzer not available' };
}
async assessRepositoryHealth(config) {
const analyzer = await this._ensureGitAnalyzer();
if (analyzer) {
return await analyzer.assessRepositoryHealth(config || {});
}
return { health: 'unknown', analysis: 'Git analyzer not available' };
}
// Advanced analysis methods
analyzeCommitPatterns(commits) {
try {
if (!commits || commits.length === 0) {
return {
patterns: [],
compliance: 0,
suggestions: ['No commits provided for analysis'],
};
}
const patterns = [];
let conventionalCount = 0;
let semanticCount = 0;
const suggestions = [];
for (const commit of commits) {
const message = commit.subject || commit.message || '';
// Check for conventional commit format
if (/^(feat|fix|docs|style|refactor|test|chore|perf|build|ci)(\(.+\))?!?:/.test(message)) {
conventionalCount++;
patterns.push('conventional');
}
// Check for semantic patterns
if (/\b(add|remove|update|fix|improve|refactor)\b/i.test(message)) {
semanticCount++;
patterns.push('semantic');
}
}
const compliance = Math.round((conventionalCount / commits.length) * 100);
if (compliance < 50) {
suggestions.push('Consider adopting conventional commit format for better changelog generation');
}
if (conventionalCount === 0) {
suggestions.push('No conventional commits found. See https://www.conventionalcommits.org/');
}
return {
patterns: [...new Set(patterns)],
compliance,
conventionalCommits: conventionalCount,
semanticCommits: semanticCount,
totalCommits: commits.length,
suggestions,
};
}
catch (error) {
EnhancedConsole.warn(`Commit pattern analysis failed: ${error.message}`);
return {
patterns: [],
compliance: 0,
suggestions: ['Analysis failed - ensure valid commits provided'],
};
}
}
detectChangeTypes(changes) {
try {
if (!changes || changes.length === 0) {
return {
types: [],
confidence: 'low',
details: 'No changes provided',
};
}
const detectedTypes = new Set();
const typeIndicators = {
feat: ['feat', 'feature', 'add', 'new', 'implement', 'create'],
fix: ['fix', 'bug', 'issue', 'resolve', 'correct', 'repair'],
docs: ['doc', 'readme', 'comment', 'documentation', 'guide'],
test: ['test', 'spec', 'coverage', 'unit', 'integration'],
refactor: ['refactor', 'cleanup', 'restructure', 'reorganize'],
style: ['style', 'format', 'lint', 'prettier', 'formatting'],
perf: ['perf', 'performance', 'optimize', 'speed', 'efficient'],
build: ['build', 'compile', 'bundle', 'package', 'deploy'],
chore: ['chore', 'maintenance', 'update', 'upgrade', 'bump'],
};
// Analyze file paths and change descriptions
for (const change of changes) {
const searchText = [
change.filePath || change.path || '',
change.diff || '',
change.description || '',
]
.join(' ')
.toLowerCase();
for (const [type, indicators] of Object.entries(typeIndicators)) {
if (indicators.some((indicator) => searchText.includes(indicator))) {
detectedTypes.add(type);
}
}
// File-based type detection
const filePath = change.filePath || change.path || '';
if (filePath.includes('test') || filePath.includes('spec')) {
detectedTypes.add('test');
}
else if (filePath.includes('doc') || filePath.endsWith('.md')) {
detectedTypes.add('docs');
}
else if (filePath.includes('config') || filePath.includes('build')) {
detectedTypes.add('build');
}
}
const confidence = detectedTypes.size > 0 ? (detectedTypes.size === 1 ? 'high' : 'medium') : 'low';
return {
types: Array.from(detectedTypes),
confidence,
totalChanges: changes.length,
details: `Detected ${detectedTypes.size} change types from ${changes.length} changes`,
};
}
catch (error) {
EnhancedConsole.warn(`Change type detection failed: ${error.message}`);
return {
types: [],
confidence: 'low',
details: `Analysis failed: ${error.message}`,
};
}
}
assessCodeQuality(files) {
try {
if (!files || files.length === 0) {
return {
score: 0,
issues: ['No files provided for analysis'],
recommendations: [],
};
}
let score = 10.0;
const issues = [];
const recommendations = [];
// Analyze each file
for (const file of files) {
const filePath = file.filePath || file.path || '';
const diff = file.diff || '';
// Check for code quality indicators
if (diff.includes('TODO') || diff.includes('FIXME')) {
issues.push(`${filePath}: Contains TODO/FIXME comments`);
score -= 0.5;
}
if (diff.includes('console.log') && !filePath.includes('test')) {
issues.push(`${filePath}: Contains console.log statements`);
score -= 0.3;
}
if (diff.includes('debugger')) {
issues.push(`${filePath}: Contains debugger statements`);
score -= 0.5;
}
// Check for large functions (very basic heuristic)
const functionMatches = diff.match(/function\s+\w+|const\s+\w+\s*=/g);
if (functionMatches && functionMatches.length > 5) {
issues.push(`${filePath}: May contain large or complex functions`);
score -= 0.2;
}
// Check for proper error handling
if (diff.includes('try') && !diff.includes('catch')) {
issues.push(`${filePath}: Incomplete error handling detected`);
score -= 0.3;
}
// Check for documentation
if (!(diff.includes('/**') || diff.includes('//')) && filePath.endsWith('.js')) {
issues.push(`${filePath}: Lacks documentation comments`);
score -= 0.2;
}
}
// Generate recommendations based on issues
if (issues.some((issue) => issue.includes('TODO'))) {
recommendations.push('Address TODO comments before releasing');
}
if (issues.some((issue) => issue.includes('console.log'))) {
recommendations.push('Remove debug console.log statements');
}
if (issues.some((issue) => issue.includes('documentation'))) {
recommendations.push('Add documentation comments to improve maintainability');
}
if (score < 7) {
recommendations.push('Consider code review and refactoring for better quality');
}
return {
score: Math.max(0, Math.min(10, score)),
issues: issues.slice(0, 10), // Limit to top 10 issues
recommendations,
filesAnalyzed: files.length,
};
}
catch (error) {
EnhancedConsole.warn(`Code quality assessment failed: ${error.message}`);
return {
score: 0,
issues: [`Assessment failed: ${error.message}`],
recommendations: ['Ensure valid file data is provided'],
};
}
}
identifyDependencies(changes) {
try {
const dependencies = {
added: [],
removed: [],
updated: [],
details: [],
};
if (!changes || changes.length === 0) {
return dependencies;
}
// Look for package.json changes
const packageJsonChanges = changes.filter((change) => (change.filePath || change.path || '').includes('package.json'));
for (const change of packageJsonChanges) {
const diff = change.diff || '';
// Parse dependency changes from diff
const addedDeps = diff.match(/\+\s*"([^"]+)":\s*"([^"]+)"/g) || [];
const removedDeps = diff.match(/-\s*"([^"]+)":\s*"([^"]+)"/g) || [];
for (const match of addedDeps) {
const [, name, version] = match.match(/\+\s*"([^"]+)":\s*"([^"]+)"/) || [];
if (name && version) {
dependencies.added.push({ name, version, file: change.filePath });
}
}
for (const match of removedDeps) {
const [, name, version] = match.match(/-\s*"([^"]+)":\s*"([^"]+)"/) || [];
if (name && version) {
dependencies.removed.push({ name, version, file: change.filePath });
}
}
}
// Look for other dependency files
const depFiles = changes.filter((change) => {
const path = change.filePath || change.path || '';
return (path.includes('requirements.txt') ||
path.includes('Gemfile') ||
path.includes('go.mod') ||
path.includes('Cargo.toml'));
});
dependencies.details = [
`Found ${packageJsonChanges.length} package.json changes`,
`Found ${depFiles.length} other dependency file changes`,
`Total: ${dependencies.added.length} added, ${dependencies.removed.length} removed`,
];
return dependencies;
}
catch (error) {
EnhancedConsole.warn(`Dependency analysis failed: ${error.message}`);
return {
added: [],
removed: [],
updated: [],
details: [`Analysis failed: ${error.message}`],
};
}
}
evaluatePerformanceImpact(changes) {
try {
const impact = {
impact: 'low',
metrics: {},
concerns: [],
improvements: [],
};
if (!changes || changes.length === 0) {
return impact;
}
// Performance-related patterns
const performancePatterns = {
high: [
/database.*query/i,
/n\+1/i,
/memory.*leak/i,
/infinite.*loop/i,
/synchronous.*call/i,
],
medium: [/algorithm/i, /cache/i, /optimization/i, /performance/i, /async/i, /await/i],
improvements: [
/optimize/i,
/faster/i,
/efficient/i,
/reduce.*time/i,
/improve.*performance/i,
],
};
let riskScore = 0;
let improvementScore = 0;
for (const change of changes) {
const content = [
change.filePath || change.path || '',
change.diff || '',
change.description || '',
].join(' ');
// Check for performance risks
for (const pattern of performancePatterns.high) {
if (pattern.test(content)) {
riskScore += 3;
impact.concerns.push(`High-risk pattern detected in ${change.filePath}`);
}
}
for (const pattern of performancePatterns.medium) {
if (pattern.test(content)) {
riskScore += 1;
}
}
for (const pattern of performancePatterns.improvements) {
if (pattern.test(content)) {
improvementScore += 2;
impact.improvements.push(`Performance improvement in ${change.filePath}`);
}
}
}
// Determine overall impact
if (riskScore > 5) {
impact.impact = 'high';
}
else if (riskScore > 2 || improvementScore > 3) {
impact.impact = 'medium';
}
impact.metrics = {
riskScore,
improvementScore,
filesAnalyzed: changes.length,
concernsFound: impact.concerns.length,
improvementsFound: impact.improvements.length,
};
return impact;
}
catch (error) {
console.warn(`Performance impact evaluation failed: ${error.message}`);
return {
impact: 'unknown',
metrics: { error: error.message },
concerns: [],
improvements: [],
};
}
}
checkSecurityImplications(changes) {
try {
const security = {
issues: [],
score: 'safe',
warnings: [],
recommendations: [],
};
if (!changes || changes.length === 0) {
return security;
}
// Security-related patterns
const securityPatterns = {
critical: [
/password.*=.*['"][^'"]+['"]/i,
/api[_-]?key.*=.*['"][^'"]+['"]/i,
/secret.*=.*['"][^'"]+['"]/i,
/token.*=.*['"][^'"]+['"]/i,
/eval\s*\(/i,
/innerHTML\s*=/i,
/document\.write/i,
],
warning: [
/http:\/\//i,
/\.execute\(/i,
/shell_exec/i,
/system\(/i,
/exec\(/i,
/sudo/i,
/chmod\s+777/i,
],
auth: [/auth/i, /login/i, /permission/i, /role/i, /access/i],
};
let riskLevel = 0;
for (const change of changes) {
const content = [
change.filePath || change.path || '',
change.diff || '',
change.description || '',
].join(' ');
// Check for critical security issues
for (const pattern of securityPatterns.critical) {
if (pattern.test(content)) {
security.issues.push(`Critical: Potential security issue in ${change.filePath}`);
riskLevel += 10;
}
}
// Check for warnings
for (const pattern of securityPatterns.warning) {
if (pattern.test(content)) {
security.warnings.push(`Warning: Security concern in ${change.filePath}`);
riskLevel += 3;
}
}
// Check for authentication changes
for (const pattern of securityPatterns.auth) {
if (pattern.test(content)) {
security.warnings.push(`Authentication changes detected in ${change.filePath}`);
riskLevel += 1;
}
}
}
// Determine security score
if (riskLevel >= 10) {
security.score = 'critical';
security.recommendations.push('Immediate security review required');
}
else if (riskLevel >= 5) {
security.score = 'warning';
security.recommendations.push('Security review recommended');
}
else if (riskLevel > 0) {
security.score = 'caution';
security.recommendations.push('Monitor for security implications');
}
if (security.issues.length > 0) {
security.recommendations.push('Remove hardcoded credentials immediately');
}
return security;
}
catch (error) {
console.warn(`Security analysis failed: ${error.message}`);
return {
issues: [`Analysis failed: ${error.message}`],
score: 'unknown',
warnings: [],
recommendations: [],
};
}
}
analyzeDocumentationChanges(changes) {
try {
const documentation = {
coverage: 'unknown',
changes: [],
statistics: {},
recommendations: [],
};
if (!changes || changes.length === 0) {
return documentation;
}
let docFiles = 0;
let codeFiles = 0;
let docChanges = 0;
for (const change of changes) {
const filePath = change.filePath || change.path || '';
const diff = change.diff || '';
if (filePath.endsWith('.md') || filePath.includes('doc') || filePath.includes('readme')) {
docFiles++;
docChanges++;
documentation.changes.push({
file: filePath,
type: 'documentation',
status: change.status,
});
}
else if (filePath.endsWith('.js') ||
filePath.endsWith('.ts') ||
filePath.endsWith('.py')) {
codeFiles++;
// Check for comment changes
if (diff.includes('/**') || diff.includes('//') || diff.includes('#')) {
documentation.changes.push({
file: filePath,
type: 'code-comments',
status: change.status,
});
}
}
}
// Calculate coverage assessment
const totalFiles = docFiles + codeFiles;
if (totalFiles === 0) {
documentation.coverage = 'unknown';
}
else {
const docRatio = docFiles / totalFiles;
if (docRatio > 0.3) {
documentation.coverage = 'good';
}
else if (docRatio > 0.1) {
documentation.coverage = 'fair';
}
else {
documentation.coverage = 'poor';
}
}
documentation.statistics = {
documentationFiles: docFiles,
codeFiles,
totalChanges: documentation.changes.length,
documentationRatio: totalFiles > 0 ? Math.round((docFiles / totalFiles) * 100) : 0,
};
// Generate recommendations
if (documentation.coverage === 'poor') {
documentation.recommendations.push('Consider adding more documentation');
}
if (codeFiles > 0 && docChanges === 0) {
documentation.recommendations.push('Code changes detected without documentation updates');
}
return documentation;
}
catch (error) {
console.warn(`Documentation analysis failed: ${error.message}`);
return {
coverage: 'unknown',
changes: [],
statistics: { error: error.message },
recommendations: [],
};
}
}
assessTestCoverage(changes) {
try {
const coverage = {
coverage: 0,
missing: [],
statistics: {},
recommendations: [],
};
if (!changes || changes.length === 0) {
return coverage;
}
let testFiles = 0;
let codeFiles = 0;
const missingTests = [];
for (const change of changes) {
const filePath = change.filePath || change.path || '';
if (filePath.includes('test') ||
filePath.includes('spec') ||
filePath.endsWith('.test.js') ||
filePath.endsWith('.spec.js')) {
testFiles++;
}
else if (filePath.endsWith('.js') ||
filePath.endsWith('.ts') ||
filePath.endsWith('.py')) {
codeFiles++;
// Check if corresponding test file exists (simplified check)
const hasTest = changes.some((c) => {
const testPath = c.filePath || c.path || '';
return (testPath.includes(filePath.replace(/\.(js|ts|py)$/, '')) &&
(testPath.includes('test') || testPath.includes('spec')));
});
if (!hasTest) {
missingTests.push(filePath);
}
}
}
// Calculate coverage estimate
const totalFiles = testFiles + codeFiles;
if (totalFiles > 0) {
coverage.coverage = Math.round((testFiles / totalFiles) * 100);
}
coverage.missing = missingTests.slice(0, 10); // Limit to top 10
coverage.statistics = {
testFiles,
codeFiles,
totalFiles,
estimatedCoverage: coverage.coverage,
};
// Generate recommendations
if (coverage.coverage < 50) {
coverage.recommendations.push('Low test coverage detected - consider adding more tests');
}
if (missingTests.length > 0) {
coverage.recommendations.push(`${missingTests.length} code files may lack corresponding tests`);
}
return coverage;
}
catch (error) {
console.warn(`Test coverage assessment failed: ${error.message}`);
return {
coverage: 0,
missing: [],
statistics: { error: error.message },
recommendations: [],
};
}
}
evaluateArchitecturalChanges(changes) {
try {
const architecture = {
impact: 'minimal',
changes: [],
patterns: [],
recommendations: [],
};
if (!changes || changes.length === 0) {
return architecture;
}
// Architectural patterns to detect
const architecturalPatterns = {
'database-schema': /migration|schema|table|index/i,
'api-changes': /api|endpoint|route|controller/i,
'dependency-injection': /inject|provider|service|factory/i,
configuration: /config|setting|environment|env/i,
security: /auth|security|permission|role/i,
infrastructure: /docker|kubernetes|deploy|infra/i,
};
let impactScore = 0;
const detectedPatterns = new Set();
for (const change of changes) {
const content = [
change.filePath || change.path || '',
change.diff || '',
change.description || '',
].join(' ');
// Check for architectural patterns
for (const [pattern, regex] of Object.entries(architecturalPatterns)) {
if (regex.test(content)) {
detectedPatterns.add(pattern);
impactScore += 2;
architecture.changes.push({
file: change.filePath || change.path,
pattern,
type: change.status,
});
}
}
// Check for directory structure changes
const filePath = change.filePath || change.path || '';
if (filePath.includes('/') && (change.status === 'A' || change.status === 'D')) {
impactScore += 1;
architecture.changes.push({
file: filePath,
pattern: 'structure-change',
type: change.status,
});
}
}
// Determine impact level
if (impactScore > 10) {
architecture.impact = 'major';
}
else if (impactScore > 5) {
architecture.impact = 'moderate';
}
else if (impactScore > 0) {
architecture.impact = 'minor';
}
architecture.patterns = Array.from(detectedPatterns);
// Generate recommendations
if (detectedPatterns.has('database-schema')) {
architecture.recommendations.push('Database schema changes detected - ensure migration scripts are tested');
}
if (detectedPatterns.has('api-changes')) {
architecture.recommendations.push('API changes detected - update documentation and versioning');
}
if (architecture.impact === 'major') {
architecture.recommendations.push('Major architectural changes - consider architectural review');
}
return architecture;
}
catch (error) {
console.warn(`Architectural analysis failed: ${error.message}`);
return {
impact: 'unknown',
changes: [],
patterns: [],
recommendations: [`Analysis failed: ${error.message}`],
};
}
}
// Metrics method expected by tests
getMetrics() {
return {
analysisCount: 0,
averageTime: 0,
successRate: 100,
};
}
}