@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
1,593 lines (1,374 loc) • 62.1 kB
JavaScript
/**
* Consolidated Utility Functions
*
* Provides comprehensive utility functions for AI changelog generation:
* - Data manipulation and conversion utilities
* - Format and presentation utilities
* - File analysis and categorization
* - Text processing and analysis
* - Commit analysis and changelog generation
*
* For advanced JSON operations with error detection, use JsonUtils from './json-utils.js'
* For specialized error handling, use error classes from './error-classes.js'
*/
import fs from 'fs';
import { execSync } from 'child_process';
import colors from '../constants/colors.js';
import JsonUtils from './json-utils.js';
import { GitError, ValidationError, ConfigError } from './error-classes.js';
// ========================================
// ERROR HANDLING UTILITIES
// ========================================
/**
* Base error class for domain-specific errors
*/
export class AIChangelogError extends Error {
constructor(message, type, context = {}, originalError = null) {
super(message);
this.name = this.constructor.name;
this.type = type;
this.context = context;
this.originalError = originalError;
this.timestamp = new Date().toISOString();
}
toJSON() {
return {
name: this.name,
message: this.message,
type: this.type,
context: this.context,
timestamp: this.timestamp,
stack: this.stack
};
}
}
/**
* Abstract method error for base classes
*/
export class AbstractMethodError extends AIChangelogError {
constructor(message, className, methodName, context = {}) {
super(message, 'abstract', { className, methodName, ...context });
this.className = className;
this.methodName = methodName;
}
}
/**
* Provider-specific error
*/
export class ProviderError extends AIChangelogError {
constructor(message, providerName, context = {}, originalError = null) {
super(message, 'provider', { providerName, ...context }, originalError);
this.providerName = providerName;
}
}
// ========================================
// DATA MANIPULATION UTILITIES
// ========================================
/**
* Convert Sets to Arrays for JSON serialization
*/
export function convertSetsToArrays(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Set) {
return Array.from(obj);
}
if (Array.isArray(obj)) {
return obj.map(convertSetsToArrays);
}
const result = {};
for (const [key, value] of Object.entries(obj)) {
result[key] = convertSetsToArrays(value);
}
return result;
}
/**
* Enhanced conventional commit parsing
* Based on git-conventional-commits patterns with breaking change detection
*/
export function extractCommitScope(message) {
// Enhanced regex to match conventional commits format: type(scope)!: description
const conventionalMatch = message.match(/^(?<type>\w+)(?:\((?<scope>[^()]+)\))?(?<breaking>!)?:\s*(?<description>.+)/);
if (conventionalMatch) {
const { type, scope, breaking, description } = conventionalMatch.groups;
return {
type: type,
scope: scope || null,
description: description.trim(),
breaking: !!breaking,
isConventional: true
};
}
// Fallback for non-conventional commits
return {
type: null,
scope: null,
description: message.trim(),
breaking: false,
isConventional: false
};
}
/**
* Parse conventional commit message with full body analysis
*/
export function parseConventionalCommit(subject, body = '') {
const parsed = extractCommitScope(subject);
const fullMessage = `${subject}\n\n${body}`.trim();
// Enhanced breaking change detection from body
const breakingChanges = [];
// Check for BREAKING CHANGE: footer
const breakingMatch = fullMessage.match(/BREAKING CHANGE:\s*(.*?)(?:\n\n|\n[A-Z]|\n*$)/s);
if (breakingMatch) {
breakingChanges.push(breakingMatch[1].trim());
parsed.breaking = true;
}
// Check for BREAKING-CHANGE: footer (alternative format)
const breakingAltMatch = fullMessage.match(/BREAKING-CHANGE:\s*(.*?)(?:\n\n|\n[A-Z]|\n*$)/s);
if (breakingAltMatch) {
breakingChanges.push(breakingAltMatch[1].trim());
parsed.breaking = true;
}
// Extract issue references (GitHub/GitLab format)
const issueRefs = [];
const issueMatches = fullMessage.match(/#[0-9]+/g);
if (issueMatches) {
issueRefs.push(...issueMatches);
}
// Extract closes references
const closesMatches = fullMessage.match(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?):?\s*#?([0-9]+)/gi);
const closesRefs = [];
if (closesMatches) {
closesMatches.forEach(match => {
const num = match.match(/([0-9]+)/);
if (num) closesRefs.push(`#${num[1]}`);
});
}
return {
...parsed,
breakingChanges,
issueReferences: [...new Set(issueRefs)],
closesReferences: [...new Set(closesRefs)],
body: body.trim(),
revert: subject.toLowerCase().startsWith('revert')
};
}
/**
* Generate markdown link for commit hash
*/
export function markdownCommitLink(commitHash, commitUrl, shortHash = true) {
if (!commitUrl) {
return shortHash ? commitHash.substring(0, 7) : commitHash;
}
const url = commitUrl.replace('%commit%', commitHash);
const displayHash = shortHash ? commitHash.substring(0, 7) : commitHash;
return `[${displayHash}](${url})`;
}
/**
* Generate markdown link for commit range
*/
export function markdownCommitRangeLink(fromCommit, toCommit, commitRangeUrl) {
if (!commitRangeUrl) {
return `${fromCommit.substring(0, 7)}...${toCommit.substring(0, 7)}`;
}
const url = commitRangeUrl
.replace('%from%', fromCommit)
.replace('%to%', toCommit);
return `[${fromCommit.substring(0, 7)}...${toCommit.substring(0, 7)}](${url})`;
}
/**
* Generate markdown link for issue reference
*/
export function markdownIssueLink(issueId, issueUrl) {
if (!issueUrl) {
return issueId;
}
const cleanIssueId = issueId.replace('#', '');
const url = issueUrl.replace('%issue%', cleanIssueId);
return `[${issueId}](${url})`;
}
/**
* Process issue references in text and convert to markdown links
*/
export function processIssueReferences(text, issueUrl, issueRegex) {
if (!issueUrl || !text) return text;
return text.replace(issueRegex, (match) => {
return markdownIssueLink(match, issueUrl);
});
}
/**
* Deep merge objects
*/
export function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
result[key] = deepMerge(result[key] || {}, source[key]);
} else {
result[key] = source[key];
}
}
return result;
}
// ========================================
// FORMAT UTILITIES
// ========================================
/**
* Format duration in human-readable format
*/
export function formatDuration(ms) {
if (ms < 1000) return `${ms}ms`;
if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`;
return `${(ms / 3600000).toFixed(1)}h`;
}
/**
* Interactive configuration prompt (simplified)
*/
export function promptForConfig(message = 'Configure settings', defaultValue = '') {
console.log(colors.infoMessage(message));
return Promise.resolve(defaultValue);
}
/**
* Get health status color
*/
export function getHealthColor(status) {
const colorMap = {
'excellent': colors.successMessage,
'good': colors.successMessage,
'fair': colors.warningMessage,
'poor': colors.errorMessage,
'critical': colors.errorMessage
};
return colorMap[status] || colors.infoMessage;
}
/**
* Format file size
*/
export function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
/**
* Format percentage
*/
export function formatPercentage(value, total) {
if (total === 0) return '0%';
return `${((value / total) * 100).toFixed(1)}%`;
}
// ========================================
// FILE UTILITIES
// ========================================
/**
* Categorize file by path and extension
*/
export function categorizeFile(filePath) {
if (!filePath || typeof filePath !== 'string') {
return 'other';
}
const path = filePath.toLowerCase();
const ext = path.split('.').pop();
// Configuration files
if (path.includes('package.json') || path.includes('yarn.lock') ||
path.includes('pnpm-lock') || path.includes('.gitignore') ||
ext === 'toml' || ext === 'yaml' || ext === 'yml' ||
path.includes('dockerfile') || path.includes('.env')) {
return 'configuration';
}
// Documentation
if (ext === 'md' || ext === 'txt' || ext === 'rst' ||
path.includes('readme') || path.includes('changelog') ||
path.includes('/docs/') || path.includes('/doc/')) {
return 'documentation';
}
// Test files
if (path.includes('/test/') || path.includes('/tests/') ||
path.includes('__tests__') || path.includes('.test.') ||
path.includes('.spec.') || ext === 'test' || ext === 'spec') {
return 'tests';
}
// Source code
const sourceExts = ['js', 'ts', 'jsx', 'tsx', 'py', 'java', 'cpp', 'c', 'cs', 'go', 'rs', 'php'];
if (sourceExts.includes(ext)) {
if (path.includes('/src/') || path.includes('/lib/')) {
return 'source';
}
return 'source';
}
// Frontend/UI
const frontendExts = ['html', 'css', 'scss', 'sass', 'less', 'vue', 'svelte'];
if (frontendExts.includes(ext)) {
return 'frontend';
}
// Assets
const assetExts = ['png', 'jpg', 'jpeg', 'gif', 'svg', 'ico', 'webp'];
if (assetExts.includes(ext)) {
return 'assets';
}
// Build/tooling
if (path.includes('webpack') || path.includes('rollup') ||
path.includes('vite') || path.includes('babel') ||
path.includes('eslint') || path.includes('prettier') ||
path.includes('/build/') || path.includes('/dist/')) {
return 'build';
}
return 'other';
}
/**
* Detect programming language from file extension
*/
export function detectLanguage(filePath) {
if (!filePath || typeof filePath !== 'string') {
return 'Unknown';
}
const ext = filePath.split('.').pop()?.toLowerCase();
const langMap = {
js: 'JavaScript',
jsx: 'JavaScript',
ts: 'TypeScript',
tsx: 'TypeScript',
py: 'Python',
java: 'Java',
cpp: 'C++',
c: 'C',
cs: 'C#',
go: 'Go',
rs: 'Rust',
php: 'PHP',
rb: 'Ruby',
swift: 'Swift',
kt: 'Kotlin',
scala: 'Scala',
html: 'HTML',
css: 'CSS',
scss: 'SCSS',
sass: 'Sass',
vue: 'Vue',
svelte: 'Svelte',
json: 'JSON',
xml: 'XML',
yaml: 'YAML',
yml: 'YAML',
toml: 'TOML',
md: 'Markdown',
sql: 'SQL'
};
return langMap[ext] || 'Unknown';
}
/**
* Assess file importance based on path and type
*/
export function assessFileImportance(filePath, status) {
if (!filePath || typeof filePath !== 'string') {
return 'medium';
}
const path = filePath.toLowerCase();
// Critical files
if (path.includes('package.json') || path.includes('pom.xml') ||
path.includes('cargo.toml') || path.includes('requirements.txt') ||
path.includes('dockerfile') || path.includes('docker-compose')) {
return 'critical';
}
// Core source files
if (path.includes('/src/') || path.includes('/lib/')) {
if (path.includes('index.') || path.includes('main.') ||
path.includes('app.') || path.includes('server.')) {
return 'critical';
}
return 'high';
}
// Configuration
if (categorizeFile(filePath) === 'configuration') {
return 'high';
}
// Tests
if (categorizeFile(filePath) === 'tests') {
return 'medium';
}
// Documentation
if (categorizeFile(filePath) === 'documentation') {
return 'low';
}
// File deletion is always important
if (status === 'D') {
return 'high';
}
return 'medium';
}
// ========================================
// TEXT PROCESSING UTILITIES
// ========================================
/**
* Assess overall complexity of changes
*/
export function assessOverallComplexity(diffContent, fileCount) {
const lines = diffContent.split('\n');
const addedLines = lines.filter(line => line.startsWith('+')).length;
const deletedLines = lines.filter(line => line.startsWith('-')).length;
const totalChanges = addedLines + deletedLines;
// Complexity factors
let complexity = 'low';
if (fileCount > 20 || totalChanges > 500) {
complexity = 'high';
} else if (fileCount > 5 || totalChanges > 100) {
complexity = 'medium';
}
// Check for complex patterns
const complexPatterns = [
/class\s+\w+/g,
/function\s+\w+/g,
/async\s+function/g,
/try\s*{/g,
/catch\s*\(/g,
/interface\s+\w+/g,
/type\s+\w+/g
];
const patternMatches = complexPatterns.reduce((count, pattern) => {
return count + (diffContent.match(pattern) || []).length;
}, 0);
if (patternMatches > 10) {
complexity = complexity === 'low' ? 'medium' : 'high';
}
return complexity;
}
/**
* Assess risk level of changes
*/
export function assessRisk(diffContent, fileCount, commitMessage) {
let risk = 'low';
// High-risk keywords in commit message
const highRiskKeywords = ['breaking', 'remove', 'delete', 'deprecated', 'migration'];
const mediumRiskKeywords = ['refactor', 'restructure', 'change', 'modify', 'update'];
if (!commitMessage || typeof commitMessage !== 'string') {
return risk; // Return early with default risk level
}
const message = commitMessage.toLowerCase();
if (highRiskKeywords.some(keyword => message.includes(keyword))) {
risk = 'high';
} else if (mediumRiskKeywords.some(keyword => message.includes(keyword))) {
risk = 'medium';
}
// High-risk file patterns
const highRiskPatterns = [
/package\.json/,
/package-lock\.json/,
/yarn\.lock/,
/Dockerfile/,
/docker-compose/,
/database/,
/migration/,
/config/,
/env/
];
const diffLines = diffContent.split('\n');
const hasHighRiskFiles = diffLines.some(line =>
highRiskPatterns.some(pattern => pattern.test(line))
);
if (hasHighRiskFiles) {
risk = risk === 'low' ? 'medium' : 'high';
}
// Large changes are risky
if (fileCount > 15 || diffContent.length > 10000) {
risk = risk === 'low' ? 'medium' : 'high';
}
return risk;
}
/**
* Check if changes are breaking
*/
export function isBreakingChange(commitMessage, diffContent) {
if (!commitMessage || typeof commitMessage !== 'string') {
return false;
}
const message = commitMessage.toLowerCase();
// Check commit message for breaking indicators
if (message.includes('breaking') || message.includes('!:')) {
return true;
}
// Check diff for breaking patterns
const breakingPatterns = [
/function\s+\w+\s*\([^)]*\)\s*{[\s\S]*?}[\s\S]*?-/g, // Function signature changes
/class\s+\w+[\s\S]*?-/g, // Class changes
/interface\s+\w+[\s\S]*?-/g, // Interface changes
/export\s+[\s\S]*?-/g // Export changes
];
return breakingPatterns.some(pattern => pattern.test(diffContent));
}
/**
* Assess business relevance of changes
*/
export function assessBusinessRelevance(commitMessage, filePaths) {
if (!commitMessage || typeof commitMessage !== 'string') {
return 'low';
}
const message = commitMessage.toLowerCase();
// Business-relevant keywords
const businessKeywords = [
'feature', 'user', 'customer', 'client', 'business', 'revenue',
'payment', 'billing', 'subscription', 'auth', 'login', 'security'
];
const hasBusinessKeywords = businessKeywords.some(keyword =>
message.includes(keyword)
);
// Business-relevant file paths
const businessPaths = [
'/api/', '/service/', '/controller/', '/model/',
'/auth/', '/payment/', '/billing/', '/user/'
];
const hasBusinessFiles = filePaths.some(path =>
path && typeof path === 'string' && businessPaths.some(businessPath => path.includes(businessPath))
);
if (hasBusinessKeywords && hasBusinessFiles) return 'high';
if (hasBusinessKeywords || hasBusinessFiles) return 'medium';
return 'low';
}
// ========================================
// JSON UTILITIES
// ========================================
/**
* Safe JSON parse with fallback
*/
export function safeJsonParse(jsonString, fallback = null) {
// Use the advanced JsonUtils for better error handling
return JsonUtils.safeParse(jsonString, fallback);
}
/**
* Safe JSON stringify with formatting
*/
export function safeJsonStringify(obj, indent = 2) {
try {
return JSON.stringify(convertSetsToArrays(obj), null, indent);
} catch (error) {
console.warn(colors.warningMessage(`JSON stringify error: ${error.message}`));
return '{}';
}
}
// ========================================
// UTILITY HELPERS
// ========================================
/**
* Sleep utility for rate limiting
*/
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Retry utility with exponential backoff
*/
export async function retry(fn, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (i < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, i);
await sleep(delay);
}
}
}
throw lastError;
}
/**
* Debounce utility
*/
export function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Throttle utility
*/
export function throttle(func, limit) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// ========================================
// ANALYSIS UTILITIES
// ========================================
/**
* Analyze semantic changes in code diffs
*/
export function analyzeSemanticChanges(diff, filePath) {
const analysis = {
changeType: 'modification',
patterns: new Set(),
frameworks: new Set(),
keywords: new Set(),
codeElements: new Set(),
apiChanges: [],
dataChanges: []
};
if (!diff || diff === 'Binary file or diff unavailable') {
return convertSetsToArrays(analysis);
}
const addedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = diff.split('\n').filter(line => line.startsWith('-') && !line.startsWith('---'));
// Framework detection (only if filePath is valid)
if (filePath && typeof filePath === 'string' && (filePath.includes('database/') || filePath.includes('sql/') || filePath.includes('migrations/'))) {
analysis.frameworks.add('Database');
if (diff.includes('CREATE TABLE') || diff.includes('ALTER TABLE')) {
analysis.changeType = 'schema_change';
analysis.patterns.add('database_schema');
}
if (diff.includes('CREATE POLICY') || diff.includes('ALTER POLICY')) {
analysis.patterns.add('security_policy');
}
}
if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) {
analysis.frameworks.add('React');
if (diff.includes('useState') || diff.includes('useEffect')) {
analysis.patterns.add('react_hooks');
}
if (diff.includes('useCallback') || diff.includes('useMemo')) {
analysis.patterns.add('performance_optimization');
}
}
if (filePath && typeof filePath === 'string' && (filePath.includes('/api/') || filePath.includes('route.'))) {
analysis.frameworks.add('API');
analysis.changeType = 'api_change';
['GET', 'POST', 'PUT', 'DELETE', 'PATCH'].forEach(method => {
if (diff.includes(`export async function ${method}`) ||
diff.includes(`app.${method.toLowerCase()}`)) {
analysis.apiChanges.push(`${method} endpoint`);
analysis.patterns.add('api_endpoint');
}
});
}
// Code element detection
const codePatterns = {
'function_definition': /(?:export\s+)?(?:async\s+)?function\s+(\w+)/g,
'component_definition': /(?:export\s+)?(?:const|function)\s+(\w+Component|\w+Page|\w+Layout)/g,
'hook_definition': /(?:export\s+)?(?:const|function)\s+(use\w+)/g,
'type_definition': /(?:export\s+)?(?:type|interface)\s+(\w+)/g,
'constant_definition': /(?:export\s+)?const\s+(\w+)/g
};
Object.entries(codePatterns).forEach(([pattern, regex]) => {
const matches = [...diff.matchAll(regex)];
if (matches.length > 0) {
analysis.patterns.add(pattern);
matches.forEach(match => analysis.codeElements.add(match[1]));
}
});
// Advanced pattern detection
const advancedPatterns = {
'error_handling': [/try\s*{/, /catch\s*\(/, /throw\s+/, /Error\(/],
'async_operations': [/async\s+/, /await\s+/, /Promise\./, /\.then\(/],
'data_validation': [/validate/, /schema/, /validation/, /validator/i],
'authentication': [/auth/, /login/, /logout/, /token/, /jwt/, /session/i],
'authorization': [/permission/, /role/, /access/, /policy/, /guard/i],
'caching': [/cache/, /memo/, /useMemo/, /useCallback/i],
'testing': [/test/, /spec/, /mock/, /describe/, /it\(/],
'styling': [/className/, /css/, /styled/, /style/],
'state_management': [/useState/, /useReducer/, /store/, /state/i],
'routing': [/router/, /navigate/, /redirect/, /route/, /Link/],
'data_fetching': [/fetch/, /axios/, /useQuery/, /useMutation/, /api/i]
};
Object.entries(advancedPatterns).forEach(([pattern, regexes]) => {
if (regexes.some(regex => regex.test(diff))) {
analysis.patterns.add(pattern);
}
});
return convertSetsToArrays(analysis);
}
/**
* Analyze functional impact of code changes
*/
export function analyzeFunctionalImpact(diff, filePath, status) {
const impact = {
scope: 'local',
severity: 'low',
affectedSystems: new Set(),
businessImpact: 'minimal',
technicalDebt: 'none',
migrationRequired: false,
backwardCompatible: true
};
if (!diff || diff === 'Binary file or diff unavailable') {
return convertSetsToArrays(impact);
}
// File deletion has high impact
if (status === 'D') {
impact.severity = 'high';
impact.scope = 'global';
impact.backwardCompatible = false;
return convertSetsToArrays(impact);
}
const addedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = diff.split('\n').filter(line => line.startsWith('-') && !line.startsWith('---'));
// Assess scope based on file type and changes (only if filePath is valid)
if (filePath && typeof filePath === 'string') {
if (filePath.includes('/api/') || filePath.includes('/server/') || filePath.includes('/backend/')) {
impact.scope = 'system';
impact.affectedSystems.add('backend');
if (diff.includes('export') || diff.includes('endpoint') || diff.includes('route')) {
impact.scope = 'global';
impact.severity = 'medium';
}
}
if (filePath.includes('package.json') || filePath.includes('package-lock.json')) {
impact.scope = 'global';
impact.severity = 'high';
impact.affectedSystems.add('dependencies');
impact.migrationRequired = true;
}
if (filePath.includes('database/') || filePath.includes('migrations/')) {
impact.scope = 'global';
impact.severity = 'high';
impact.affectedSystems.add('database');
impact.migrationRequired = true;
impact.backwardCompatible = false;
}
}
// Breaking change detection
const breakingPatterns = [
/export\s+(?:async\s+)?function\s+\w+\s*\([^)]*\)\s*{[\s\S]*?}-/g,
/export\s+(?:const|let|var)\s+\w+\s*=[\s\S]*?-/g,
/export\s+(?:class|interface|type)\s+\w+[\s\S]*?-/g
];
if (breakingPatterns.some(pattern => pattern.test(diff))) {
impact.backwardCompatible = false;
impact.severity = 'high';
impact.businessImpact = 'high';
}
// Performance impact
if (diff.includes('async') || diff.includes('await') || diff.includes('Promise')) {
impact.affectedSystems.add('performance');
}
// Security impact
if (diff.includes('auth') || diff.includes('permission') || diff.includes('security')) {
impact.affectedSystems.add('security');
impact.severity = 'high';
impact.businessImpact = 'high';
}
return convertSetsToArrays(impact);
}
/**
* Generate analysis summary from semantic and functional analysis
*/
export function generateAnalysisSummary(semanticAnalysis, functionalImpact) {
const summary = {
primaryChanges: [],
impactLevel: functionalImpact.severity || 'low',
technicalScope: functionalImpact.scope || 'local',
businessRelevance: functionalImpact.businessImpact || 'minimal',
frameworks: Array.from(semanticAnalysis.frameworks || []),
patterns: Array.from(semanticAnalysis.patterns || []),
recommendations: [],
riskFactors: []
};
// Generate primary changes based on patterns
if (semanticAnalysis.patterns && semanticAnalysis.patterns.includes('api_endpoint')) {
summary.primaryChanges.push('API endpoint modifications');
}
if (semanticAnalysis.patterns && semanticAnalysis.patterns.includes('database_schema')) {
summary.primaryChanges.push('Database schema changes');
}
if (semanticAnalysis.patterns && semanticAnalysis.patterns.includes('react_hooks')) {
summary.primaryChanges.push('React component updates');
}
if (semanticAnalysis.patterns && semanticAnalysis.patterns.includes('function_definition')) {
summary.primaryChanges.push('Function implementations');
}
// Generate recommendations
if (functionalImpact.migrationRequired) {
summary.recommendations.push('Migration script required');
}
if (!functionalImpact.backwardCompatible) {
summary.recommendations.push('Breaking change - update dependent code');
}
if (functionalImpact.affectedSystems && functionalImpact.affectedSystems.includes('security')) {
summary.recommendations.push('Security review recommended');
}
if (functionalImpact.affectedSystems && functionalImpact.affectedSystems.includes('performance')) {
summary.recommendations.push('Performance testing advised');
}
// Identify risk factors
if (functionalImpact.severity === 'high') {
summary.riskFactors.push('High impact changes');
}
if (functionalImpact.scope === 'global') {
summary.riskFactors.push('System-wide effects');
}
if (semanticAnalysis.frameworks && semanticAnalysis.frameworks.length > 2) {
summary.riskFactors.push('Multiple framework dependencies');
}
return summary;
}
/**
* Perform semantic analysis on files and commit message
*/
export function performSemanticAnalysis(files, subject, body) {
const conventionalCommit = parseConventionalCommit(subject, body);
const analysis = {
commitType: conventionalCommit.type || 'unknown',
scope: conventionalCommit.scope,
description: conventionalCommit.description,
isConventional: conventionalCommit.isConventional,
breaking: conventionalCommit.breaking,
breakingChanges: conventionalCommit.breakingChanges,
issueReferences: conventionalCommit.issueReferences,
closesReferences: conventionalCommit.closesReferences,
affectedDomains: new Set(),
technicalElements: new Set(),
businessElements: new Set(),
complexity: 'low'
};
// Analyze files
files.forEach(file => {
const filePath = file.filePath || file.path || '';
const category = categorizeFile(filePath);
const language = detectLanguage(filePath);
analysis.affectedDomains.add(category);
analysis.technicalElements.add(language);
// Business domain detection (only if filePath exists)
if (filePath && typeof filePath === 'string') {
if (filePath.includes('/user/') || filePath.includes('/customer/')) {
analysis.businessElements.add('user_management');
}
if (filePath.includes('/payment/') || filePath.includes('/billing/')) {
analysis.businessElements.add('financial_operations');
}
if (filePath.includes('/auth/') || filePath.includes('/security/')) {
analysis.businessElements.add('security_access');
}
if (filePath.includes('/analytics/') || filePath.includes('/metrics/')) {
analysis.businessElements.add('business_intelligence');
}
}
});
// Complexity assessment
if (files.length > 10 || analysis.affectedDomains.size > 3) {
analysis.complexity = 'high';
} else if (files.length > 3 || analysis.affectedDomains.size > 1) {
analysis.complexity = 'medium';
}
return convertSetsToArrays(analysis);
}
// ========================================
// AI UTILITIES
// ========================================
/**
* Build enhanced prompt for AI analysis
*/
export function buildEnhancedPrompt(commitAnalysis, analysisMode = 'standard') {
const { subject, files = [], semanticAnalysis = {}, diffStats = {}, complexity = {}, riskAssessment = {} } = commitAnalysis;
// Build comprehensive context - adjust based on commit size
const fileCount = files.length;
let fileLimit, diffLimit;
if (fileCount <= 10) {
// Small commits: show all files with full diffs
fileLimit = fileCount;
diffLimit = 2000;
} else if (fileCount <= 30) {
// Medium commits: show most files with substantial diffs
fileLimit = Math.min(20, fileCount);
diffLimit = 1500;
} else {
// Large commits: prioritize modified files with meaningful diffs
fileLimit = 20;
diffLimit = 800; // Increased from 300 to actually see code changes
}
// Sort files to prioritize those with actual code changes
const sortedFiles = [...files].sort((a, b) => {
// Prioritize modified files over added files
const statusPriority = { 'M': 0, 'R': 1, 'A': 2, 'D': 3 };
const aScore = statusPriority[a.status] || 4;
const bScore = statusPriority[b.status] || 4;
if (aScore !== bScore) return aScore - bScore;
// Secondary: prioritize source code files
const isSourceFile = (path) => path.includes('/src/') || path.endsWith('.js') || path.endsWith('.ts');
const aIsSource = isSourceFile(a.filePath) ? 0 : 1;
const bIsSource = isSourceFile(b.filePath) ? 0 : 1;
return aIsSource - bIsSource;
});
const filesContext = sortedFiles.slice(0, fileLimit).map(file => ({
path: file.filePath,
status: file.status,
category: file.category || categorizeFile(file.filePath),
language: file.language || detectLanguage(file.filePath),
complexity: file.complexity,
semanticChanges: file.semanticChanges,
functionalImpact: file.functionalImpact,
businessRelevance: file.businessRelevance,
diff: file.diff || 'No diff available' // Include actual diff content so AI can see what changed
})).slice(0, fileLimit);
// Enhanced analysis requirements based on mode
const getAnalysisRequirements = () => {
const baseRequirements = [
"**Primary Impact**: What does this change do for end users?",
"**Technical Scope**: How does this affect the codebase architecture?",
"**Business Value**: What problem does this solve or feature does this enable?",
"**Risk Assessment**: What are the potential impacts of this change?"
];
if (analysisMode === 'detailed' || analysisMode === 'enterprise') {
baseRequirements.push(
"**Migration Needs**: Are there any breaking changes or upgrade requirements?",
"**Performance Impact**: How might this affect system performance?",
"**Security Implications**: Are there any security considerations?",
"**Testing Strategy**: What areas should be prioritized for testing?",
"**Rollback Plan**: What would be needed if this change needs to be reverted?"
);
}
if (analysisMode === 'enterprise') {
baseRequirements.push(
"**Operational Impact**: How does this affect deployment, monitoring, or operations?",
"**Compliance**: Are there regulatory or compliance considerations?",
"**Cross-team Impact**: What teams or systems might be affected?",
"**Timeline Implications**: Are there dependencies or staging requirements?"
);
}
return baseRequirements;
};
// Enhanced task description based on analysis mode
const getTaskDescription = () => {
switch (analysisMode) {
case 'enterprise':
return `Perform comprehensive enterprise-grade analysis of this git commit for changelog generation. Provide deep technical insights, thorough risk assessment, and detailed operational considerations suitable for enterprise deployment and stakeholder communication.`;
case 'detailed':
return `Perform detailed technical analysis of this git commit for changelog generation. Provide comprehensive technical insights, thorough impact assessment, and detailed migration considerations for development teams.`;
default:
return `Analyze this git commit for changelog generation using your reasoning capabilities.`;
}
};
const insertions = diffStats.insertions || 0;
const deletions = diffStats.deletions || 0;
const prompt = `Analyze this git commit for changelog generation.
**COMMIT:** ${subject}
**FILES:** ${files.length} files, ${insertions + deletions} lines changed
**TARGET AUDIENCE:** End users and project stakeholders who need to understand what changed and why it matters.
**YOUR ROLE:** You are a release manager translating technical changes into clear, user-focused release notes.
**ANALYSIS APPROACH:**
1. **First, categorize correctly** using the rules below
2. **Then, focus on user impact** - what can they do now that they couldn't before?
3. **Keep technical details minimal** - only what's necessary for understanding
**ACTUAL DIFFS** (top ${fileLimit} files):
${filesContext.map(file => {
const diffPreview = file.diff ? file.diff.substring(0, diffLimit) + (file.diff.length > diffLimit ? '...' : '') : 'No diff available';
return `
**${file.path}** (${file.status}):
${diffPreview}
`;
}).join('')}
**CATEGORIZATION RULES (STRICTLY ENFORCED):**
- **fix**: ONLY actual bug fixes - broken functionality now works correctly
- **feature**: New capabilities, tools, or major functionality additions
- **refactor**: Code restructuring without changing what users can do
- **perf**: Performance improvements users will notice
- **docs**: Documentation updates only
- **build/chore**: Build system, dependencies, maintenance
**CRITICAL VALIDATION:**
- Large additions (>10 files OR >1000 lines) = "feature" or "refactor", NEVER "fix"
- New modules/classes/tools = "feature"
- Only actual bug repairs = "fix"
Provide a JSON response with ONLY these fields:
{
"summary": "${subject}",
"impact": "critical|high|medium|low|minimal",
"category": "feature|fix|security|breaking|docs|style|refactor|perf|test|chore",
"description": "One clear sentence describing what users can now do (for features) or what now works correctly (for fixes)",
"technicalDetails": "Maximum 2 sentences about key technical changes - focus on the most important functions/modules/APIs added or modified",
"businessValue": "Brief user benefit in 1 sentence",
"riskFactors": ["minimal", "list"],
"recommendations": ["minimal", "list"],
"breakingChanges": false,
"migrationRequired": false
}`;
return prompt;
}
/**
* Validate and correct AI categorization and impact assessment based on commit characteristics
*/
export function validateCommitCategory(category, commitAnalysis) {
const { files = [], diffStats = {}, subject = '' } = commitAnalysis;
const fileCount = files.length;
const addedFiles = files.filter(f => f.status === 'A' || f.status === '??').length;
const { insertions = 0, deletions = 0 } = diffStats;
// Rule 1: Large additions cannot be bug fixes
if (category === 'fix' && (fileCount > 10 || addedFiles > 5 || insertions > 1000)) {
// Check if it's likely a refactor vs new feature
if (deletions > insertions * 0.5) {
return 'refactor'; // Significant deletions suggest refactoring
}
return 'feature'; // More additions suggest new functionality
}
// Rule 2: New module/class additions are features
if (category === 'fix' && addedFiles > 0) {
const hasNewModules = files.some(f =>
(f.status === 'A' || f.status === '??') &&
(f.filePath.includes('.js') || f.filePath.includes('.ts') || f.filePath.includes('.py'))
);
if (hasNewModules) {
return 'feature';
}
}
// Rule 3: Documentation-only changes
if (category !== 'docs' && files.every(f =>
f.filePath.endsWith('.md') ||
f.filePath.endsWith('.txt') ||
f.filePath.includes('README') ||
f.filePath.includes('CHANGELOG')
)) {
return 'docs';
}
// Rule 4: Test-only changes
if (category !== 'test' && files.every(f =>
f.filePath.includes('test') ||
f.filePath.includes('spec') ||
f.filePath.endsWith('.test.js') ||
f.filePath.endsWith('.spec.js')
)) {
return 'test';
}
return category; // No correction needed
}
/**
* Validate and correct impact assessment based on actual change magnitude
*/
export function validateImpactAssessment(impact, commitAnalysis) {
const { files = [], diffStats = {}, subject = '' } = commitAnalysis;
const fileCount = files.length;
const addedFiles = files.filter(f => f.status === 'A' || f.status === '??').length;
const { insertions = 0, deletions = 0 } = diffStats;
const totalChanges = insertions + deletions;
// Rule 1: Large architectural changes cannot be "minimal" or "low"
if ((impact === 'minimal' || impact === 'low') && (fileCount > 50 || totalChanges > 5000)) {
return 'high';
}
// Rule 2: Major refactors or large features should be at least "medium"
if (impact === 'minimal' && (fileCount > 20 || totalChanges > 2000 || addedFiles > 10)) {
return 'medium';
}
// Rule 3: Small changes cannot be "critical" or "high" unless breaking
if ((impact === 'critical' || impact === 'high') && fileCount <= 3 && totalChanges <= 100) {
const hasBreakingIndicators = subject.includes('!') || subject.toLowerCase().includes('breaking');
if (!hasBreakingIndicators) {
return 'medium';
}
}
// Rule 4: Documentation-only changes should be low impact
if (files.every(f =>
f.filePath.endsWith('.md') ||
f.filePath.endsWith('.txt') ||
f.filePath.includes('README') ||
f.filePath.includes('CHANGELOG')
) && (impact === 'critical' || impact === 'high')) {
return 'low';
}
return impact; // No correction needed
}
/**
* Parse AI response content
*/
export function parseAIResponse(content, originalCommit = {}) {
if (!content) {
return {
summary: originalCommit.subject || 'Unknown change',
impact: 'low',
category: 'chore',
description: 'AI analysis unavailable - using pattern-based description',
technicalDetails: 'Analysis performed using file pattern recognition',
businessValue: 'Impact assessment based on file changes only',
riskFactors: [],
recommendations: ['Consider configuring AI provider for detailed analysis'],
breakingChanges: false,
migrationRequired: false
};
}
try {
// Try to extract JSON from the response
const jsonMatch = content.match(/\{[\s\S]*\}/);
if (jsonMatch) {
const parsed = safeJsonParse(jsonMatch[0]);
if (parsed) {
// Validate and correct the category and impact
const validatedCategory = validateCommitCategory(parsed.category || 'chore', originalCommit);
const validatedImpact = validateImpactAssessment(parsed.impact || 'low', originalCommit);
return {
summary: parsed.summary || originalCommit.subject || 'Unknown change',
impact: validatedImpact,
category: validatedCategory,
description: parsed.description || '',
technicalDetails: parsed.technicalDetails || '',
businessValue: parsed.businessValue || '',
riskFactors: Array.isArray(parsed.riskFactors) ? parsed.riskFactors : [],
recommendations: Array.isArray(parsed.recommendations) ? parsed.recommendations : [],
breakingChanges: Boolean(parsed.breakingChanges),
migrationRequired: Boolean(parsed.migrationRequired)
};
}
}
// Fallback to text parsing if JSON extraction fails
const safeContent = content && typeof content === 'string' ? content : '';
const lowerContent = safeContent.toLowerCase();
return {
summary: originalCommit.subject || 'Unknown change',
impact: lowerContent.includes('critical') ? 'critical' :
lowerContent.includes('high') ? 'high' :
lowerContent.includes('medium') ? 'medium' : 'low',
category: extractCategoryFromText(safeContent),
description: safeContent.substring(0, 200) + (safeContent.length > 200 ? '...' : ''),
technicalDetails: safeContent,
businessValue: '',
riskFactors: [],
recommendations: [],
breakingChanges: lowerContent.includes('breaking'),
migrationRequired: lowerContent.includes('migration')
};
} catch (error) {
console.warn(colors.warningMessage(`AI response parsing error: ${error.message}`));
return {
summary: originalCommit.subject || 'Unknown change',
impact: 'low',
category: 'chore',
description: 'AI analysis failed',
technicalDetails: '',
businessValue: '',
riskFactors: [],
recommendations: [],
breakingChanges: false,
migrationRequired: false
};
}
}
/**
* Extract category from text content
* Helper for parseAIResponse
*/
function extractCategoryFromText(content) {
if (!content || typeof content !== 'string') {
return 'chore';
}
const text = content.toLowerCase();
if (text.includes('feature') || text.includes('feat')) return 'feature';
if (text.includes('fix') || text.includes('bug')) return 'fix';
if (text.includes('security')) return 'security';
if (text.includes('breaking')) return 'breaking';
if (text.includes('doc')) return 'docs';
if (text.includes('style')) return 'style';
if (text.includes('refactor')) return 'refactor';
if (text.includes('perf') || text.includes('performance')) return 'perf';
if (text.includes('test')) return 'test';
return 'chore';
}
// ========================================
// CHANGELOG UTILITIES
// ========================================
/**
* Process working directory changes for changelog generation
*/
/**
* Get raw working directory changes from git status
*/
export function getWorkingDirectoryChanges() {
try {
const result = execSync('git status --porcelain', { encoding: 'utf8' });
if (!result.trim()) {
return [];
}
const lines = result.split('\n').filter(line => line.trim());
const changes = lines.map(line => {
const status = line.substring(0, 2).trim() || '??';
const filePath = line.substring(3);
return {
status: status,
filePath: filePath,
path: filePath, // alias for compatibility
diff: '', // Will be populated later if needed
additions: 0,
deletions: 0
};
});
return changes;
} catch (error) {
console.warn('Could not get git status:', error.message);
return [];
}
}
export function processWorkingDirectoryChanges(workingDirChanges, gitManager = null) {
// If no changes provided, get them from git status
if (!workingDirChanges) {
workingDirChanges = getWorkingDirectoryChanges();
}
if (!workingDirChanges || workingDirChanges.length === 0) {
return [];
}
const categorizedChanges = {
added: [],
modified: [],
deleted: [],
renamed: [],
unknown: []
};
let totalFiles = 0;
workingDirChanges.forEach(change => {
const category = categorizeFile(change.filePath);
const language = detectLanguage(change.filePath);
const importance = assessFileImportance(change.filePath, change.status);
const processedChange = {
filePath: change.filePath,
status: change.status,
category,
language,
importance,
diff: change.diff || '',
additions: change.additions || 0,
deletions: change.deletions || 0
};
switch (change.status) {
case 'A':
categorizedChanges.added.push(processedChange);
break;
case 'M':
categorizedChanges.modified.push(processedChange);
break;
case 'D':
categorizedChanges.deleted.push(processedChange);
break;
case 'R':
categorizedChanges.renamed.push(processedChange);
break;
default:
categorizedChanges.unknown.push(processedChange);
}
totalFiles++;
});
// Generate summary
const summary = generateWorkspaceChangesSummary(categorizedChanges);
// Assess complexity
const complexity = assessWorkspaceComplexity(categorizedChanges, totalFiles);
return {
categorizedChanges,
summary,
totalFiles,
complexity
};
}
/**
* Summarize file changes for changelog
*/
export function summarizeFileChanges(changes) {
if (!changes || changes.length === 0) {
return {
summary: 'No file changes detected',
categories: {},
stats: { added: 0, modified: 0, deleted: 0, renamed: 0 },
languages: {}
};
}
const stats = {
added: 0,
modified: 0,
deleted: 0,
renamed: 0
};
const categories = {};
const languages = new Set();
changes.forEach(change => {
// Handle status mapping for git status format
let status = change.status;
if (status === '??') status = 'A'; // Untracked files are "added"
if (status.length > 1) status = status[0]; // Take first character for multiple status codes
switch (status) {
case 'A': stats.added++; break;
case 'M': stats.modified++; break;
case 'D': stats.deleted++; break;
case 'R': stats.renamed++; break;
}
const filePath = change.filePath || change.path;
const category = categorizeFile(filePath);
const language = detectLanguage(filePath);
// Group files by category
if (!categories[category]) {
categories[category] = [];
}
categories[category].push({
status: status,
path: filePath,
language: language
});
languages.add(language);
});
const parts = [];
if (stats.added > 0) parts.push(`${stats.added} added`);
if (stats.modified > 0) parts.push(`${stats.modified} modified`);
if (stats.deleted > 0) parts.push(`${stats.deleted} deleted`);
if (stats.renamed > 0) parts.push(`${stats.renamed} renamed`);
let summary = `${changes.length} files changed: ${parts.join(', ')}`;
if (Object.keys(categories).length > 0) {
summary += `. Affected areas: ${Object.keys(categories).join(', ')}`;
}
return {
summary,
categories,
stats,
languages: Array.from(languages)
};
}
/**
* Build commit changelog from analyzed commits
*/
export function buildCommitChangelog(analyzedCommits, releaseInsights, version, options = {}) {
const { metrics, includeAttribution = true } = options;
const currentDate = new Date().toISOString().split('T')[0];
const versionHeader = version || 'Unreleased';
let changelog = `# Changelog\n\n## [${versionHeader}] - ${currentDate}\n\n`;
// Add release summary with business impact
if (releaseInsights && releaseInsights.summary) {
changelog += `### 📋 Release Summary\n${releaseInsights.summary}\n\n`;
changelog += `**Business Impact**: ${releaseInsights.businessImpact}\n`;
changelog += `**Complexity**: ${releaseInsights.complexity}\n`;
if (releaseInsights.deploymentRequirements && releaseInsights.deploymentRequirements.length > 0) {
changelog += `**Deployment Requirements**: ${releaseInsights.deploymentRequirements.join(', ')}\n`;
}
changelog += '\n';
}
// Use unified format with impact-based ordering instead of categorized sections
changelog += `## Changes\n\n`;
// Sort commits by business impact (breaking/critical first, then by impact level)
const sortedCommits = analyzedCommits.sort((a, b) => {
// Breaking changes always come first
if (a.breaking && !b.breaking) return -1;
if (!a.breaking && b.breaking) return 1;
// Then sort by impact level
const impactOrder = { critical: 0, high: 1, medium: 2, low: 3, minimal: 4 };
const aImpact = impactOrder[a.aiSummary?.impact] ?? 5;
const bImpact = impactOrder[b.aiSummary?.impact] ?? 5;
return aImpact - bImpact;
});
sortedCommits.forEach(commit => {
const type = commit.breaking ? 'breaking' : (commit.type || 'chore');
const summary = commit.aiSummary?.summary || commit.subject;
const confidence = commit.aiSummary?.confidence ? `${Math.round(commit.aiSummary.confidence * 100)}%` : '85%';
// Format: - (tag) Brief description - Detailed explanation (hash) (confidence%)
let line = `- (${type}) ${summary}`;
// Add breaking change indicator
if (commit.breaking || commit.aiSummary?.breaking) {
line += ' ⚠️ BREAKING CHANGE';
}
// Add high/critical impact indicator
if (commit.aiSummary?.impact === 'critical' || commit.aiSummary?.impact === 'high') {
line += ' 🔥';
}
// Add detailed technical explanation
if (commit.aiSummary?.technicalDetails) {
line += ` - ${commit.aiSummary.technicalDetails}`;
} else if (commit.aiSummary?.description && commit.aiSummary.description !== commit.aiSummary.summary) {
line += ` - ${commit.aiSummary.description}`;