@entro314labs/ai-changelog-generator
Version:
AI-powered changelog generator with MCP server support - works with most providers, online and local models
1,761 lines โข 64.6 kB
JavaScript
import { buildCommitChangelog, summarizeFileChanges, processWorkingDirectoryChanges, formatDuration, sleep, handleUnifiedOutput, parseConventionalCommit, markdownCommitLink, markdownCommitRangeLink, processIssueReferences } from '../../shared/utils/utils.js';
import colors from '../../shared/constants/colors.js';
import path from 'path';
import { WorkspaceChangelogService } from './workspace-changelog.service.js';
export class ChangelogService {
constructor(gitService, aiAnalysisService, analysisEngine = null, configManager = null) {
this.gitService = gitService;
this.aiAnalysisService = aiAnalysisService;
this.analysisEngine = analysisEngine;
this.configManager = configManager;
this.workspaceChangelogService = new WorkspaceChangelogService(aiAnalysisService, gitService);
}
/**
* Generates a changelog from committed changes (git history)
* @param {string} version - Version number for the release
* @param {string} since - Generate changelog since this date/commit
* @returns {Promise<string>} Complete changelog content
*/
async generateChangelog(version = null, since = null) {
console.log(colors.processingMessage('๐ค Analyzing changes with AI...'));
// Get committed changes
const commits = await this.gitService.getCommitsSince(since);
// Get working directory changes using analysis engine
let workingDirAnalysis = null;
if (this.analysisEngine) {
workingDirAnalysis = await this.analysisEngine.analyzeCurrentChanges();
}
if (commits.length === 0 && (!workingDirAnalysis || workingDirAnalysis.changes.length === 0)) {
console.log(colors.infoMessage('No commits or working directory changes found.'));
return;
}
const workingChangesCount = workingDirAnalysis ? workingDirAnalysis.changes.length : 0;
console.log(colors.processingMessage(`Found ${colors.number(commits.length)} commits and ${colors.number(workingChangesCount)} working directory changes...`));
// Extract hash strings from commit objects
const commitHashes = commits.map(c => c.hash);
console.log(colors.processingMessage(`Analyzing ${colors.number(commitHashes.length)} commits...`));
// Use batch processing for large commit sets
let analyzedCommits;
if (commitHashes.length > 20) {
console.log(colors.infoMessage('Using batch processing for large commit set...'));
analyzedCommits = await this.generateChangelogBatch(commitHashes);
} else {
analyzedCommits = await this.processCommitsSequentially(commitHashes);
}
if (analyzedCommits.length === 0) {
console.log(colors.warningMessage('No valid commits to analyze.'));
return;
}
// Generate release insights including working directory changes
const insights = await this.generateReleaseInsights(analyzedCommits, version, workingDirAnalysis);
// Build final changelog including both committed and working changes
const changelog = await this.buildChangelog(analyzedCommits, insights, version, workingDirAnalysis);
// Write changelog to file using existing utility
const filename = version && version !== 'latest' ? `CHANGELOG-${version}.md` : 'AI_CHANGELOG.md';
const outputFile = path.join(process.cwd(), filename);
handleUnifiedOutput(changelog, { format: 'markdown', outputFile, silent: false });
return {
changelog,
insights,
analyzedCommits,
workingDirAnalysis
};
}
async processCommitsSequentially(commitHashes) {
const analyzedCommits = [];
for (let i = 0; i < commitHashes.length; i++) {
const commitHash = commitHashes[i];
const commitAnalysis = await this.gitService.getCommitAnalysis(commitHash);
if (commitAnalysis) {
const selectedModel = await this.aiAnalysisService.selectOptimalModel(commitAnalysis);
console.log(colors.processingMessage(`Processing commit ${colors.highlight(`${i + 1}/${commitHashes.length}`)}: ${colors.hash(commitHash.substring(0, 7))}`));
const aiSummary = await this.aiAnalysisService.generateAISummary(commitAnalysis, selectedModel);
if (aiSummary) {
analyzedCommits.push({
...commitAnalysis,
aiSummary,
type: aiSummary.category || 'other',
breaking: aiSummary.breaking || false
});
}
}
}
return analyzedCommits;
}
async generateChangelogBatch(commitHashes) {
const batchSize = 10;
const results = [];
for (let i = 0; i < commitHashes.length; i += batchSize) {
const batch = commitHashes.slice(i, i + batchSize);
const batchNum = Math.floor(i/batchSize) + 1;
const totalBatches = Math.ceil(commitHashes.length/batchSize);
console.log(colors.processingMessage(`Processing batch ${colors.highlight(`${batchNum}/${totalBatches}`)} (${colors.number(batch.length)} commits)`));
console.log(colors.progress(batchNum, totalBatches, 'batches processed'));
const batchPromises = batch.map(hash => this.gitService.getCommitAnalysis(hash));
const batchResults = await Promise.allSettled(batchPromises);
const successfulResults = batchResults
.filter(r => r.status === 'fulfilled' && r.value)
.map(r => r.value);
results.push(...successfulResults);
// Rate limiting between batches
if (i + batchSize < commitHashes.length) {
await sleep(1000);
}
}
return results.filter(Boolean);
}
async generateReleaseInsights(analyzedCommits, version, workingDirAnalysis = null) {
const insights = {
summary: '',
totalCommits: analyzedCommits.length,
commitTypes: {},
riskLevel: 'low',
affectedAreas: new Set(),
breaking: false,
complexity: 'low',
businessImpact: 'minor',
deploymentRequirements: []
};
// Count commit types and assess risk
analyzedCommits.forEach(commit => {
insights.commitTypes[commit.type] = (insights.commitTypes[commit.type] || 0) + 1;
if (commit.breaking) insights.breaking = true;
if (commit.semanticAnalysis?.riskLevel === 'high') insights.riskLevel = 'high';
commit.files.forEach(file => {
insights.affectedAreas.add(file.category);
});
// Check for deployment requirements
if (commit.semanticAnalysis?.hasDbChanges) {
insights.deploymentRequirements.push('Database migration required');
}
if (commit.breaking) {
insights.deploymentRequirements.push('Breaking changes - review migration notes above.');
}
});
insights.affectedAreas = Array.from(insights.affectedAreas);
// Include working directory changes in insights
if (workingDirAnalysis && workingDirAnalysis.changes.length > 0) {
insights.workingDirectoryChanges = {
count: workingDirAnalysis.changes.length,
summary: workingDirAnalysis.analysis ? workingDirAnalysis.analysis.summary : 'Working directory changes detected'
};
// Add working directory files to affected areas
workingDirAnalysis.changes.forEach(change => {
if (change.category) {
insights.affectedAreas.push(change.category);
}
});
// Remove duplicates
insights.affectedAreas = Array.from(new Set(insights.affectedAreas));
}
// Assess overall complexity
const totalFiles = analyzedCommits.reduce((sum, commit) => sum + commit.files.length, 0);
const avgFilesPerCommit = totalFiles / analyzedCommits.length;
if (avgFilesPerCommit > 20 || insights.breaking) {
insights.complexity = 'high';
} else if (avgFilesPerCommit > 10) {
insights.complexity = 'medium';
}
// Generate summary based on insights
insights.summary = this.generateInsightsSummary(insights, version);
return insights;
}
generateInsightsSummary(insights, version) {
const { totalCommits, commitTypes, breaking, complexity, affectedAreas } = insights;
let summary = `Release ${version || 'latest'} includes ${totalCommits} commits`;
if (breaking) {
summary += ' with breaking changes';
}
if (Object.keys(commitTypes).length > 0) {
const types = Object.entries(commitTypes)
.sort(([,a], [,b]) => b - a)
.map(([type, count]) => `${count} ${type}`)
.join(', ');
summary += ` (${types})`;
}
summary += `. Complexity: ${complexity}. Affected areas: ${affectedAreas.join(', ')}.`;
return summary;
}
async buildChangelog(analyzedCommits, insights, version, workingDirAnalysis = null) {
const timestamp = new Date().toISOString().split('T')[0];
const commitUrl = this.configManager?.getCommitUrl();
const commitRangeUrl = this.configManager?.getCommitRangeUrl();
const issueUrl = this.configManager?.getIssueUrl();
const issueRegex = this.configManager?.getIssueRegexPattern();
let changelog = `# Changelog\n\n`;
// Version header with commit range link
const firstCommit = analyzedCommits[0];
const lastCommit = analyzedCommits[analyzedCommits.length - 1];
let versionHeader = `## [${version || 'Unreleased'}] - ${timestamp}`;
if (firstCommit && lastCommit && commitRangeUrl) {
const rangeLink = markdownCommitRangeLink(lastCommit.fullHash, firstCommit.fullHash, commitRangeUrl);
versionHeader += ` (${rangeLink})`;
}
changelog += `${versionHeader}\n\n`;
// Release summary
changelog += `### ๐ Release Summary\n`;
changelog += `${insights.summary}\n\n`;
changelog += `**Business Impact**: ${insights.businessImpact}\n`;
changelog += `**Complexity**: ${insights.complexity}\n\n`;
// Group commits by conventional type
const groupedCommits = this.groupCommitsByType(analyzedCommits);
const sortedTypes = Object.keys(groupedCommits).sort();
// Generate sections for each commit type
changelog += `## Changes\n\n`;
for (const type of sortedTypes) {
const commits = groupedCommits[type];
if (commits.length === 0) continue;
changelog += `### ${this.getTypeHeader(type)}\n\n`;
for (const commit of commits) {
changelog += this.buildCommitEntry(commit, commitUrl, issueUrl, issueRegex);
}
changelog += '\n';
}
// Breaking changes section
const breakingCommits = analyzedCommits.filter(commit =>
commit.breaking || (commit.breakingChanges && commit.breakingChanges.length > 0)
);
if (breakingCommits.length > 0) {
const breakingHeader = this.configManager?.getHeadlines()?.breakingChange || '๐จ BREAKING CHANGES';
changelog += `### ${breakingHeader}\n\n`;
for (const commit of breakingCommits) {
changelog += this.buildBreakingChangeEntry(commit, commitUrl, issueUrl, issueRegex);
}
changelog += '\n';
}
// Append working directory changes if present
if (workingDirAnalysis && workingDirAnalysis.changes && workingDirAnalysis.changes.length > 0) {
const workingDirSection = await this.buildWorkingDirectorySection(workingDirAnalysis);
changelog += workingDirSection;
}
// Attribution footer
changelog += `---\n\n`;
changelog += `*Generated using [ai-changelog-generator](https://github.com/entro314-labs/AI-changelog-generator) - AI-powered changelog generation for Git repositories*\n`;
return changelog;
}
buildCommitEntry(commit, commitUrl, issueUrl, issueRegex) {
const commitLink = markdownCommitLink(commit.fullHash || commit.hash, commitUrl);
// Use AI-generated summary and description if available, fallback to commit message
let title = commit.aiSummary?.summary || commit.description || commit.subject || 'No description';
let description = commit.aiSummary?.technicalDetails || commit.aiSummary?.description;
// If AI description is same as title, don't repeat it
if (description === title) {
description = null;
}
// Process issue references in title
if (issueUrl && issueRegex) {
title = processIssueReferences(title, issueUrl, issueRegex);
}
// Build entry with scope and title
let entry = '- ';
if (commit.scope) {
entry += `**${commit.scope}**: `;
}
// Add impact indicators
if (commit.aiSummary?.impact === 'critical' || commit.aiSummary?.impact === 'high') {
entry += '๐ฅ ';
}
entry += `**${title}**`;
// Add detailed description if available
if (description && description !== title) {
entry += `: ${description}`;
}
entry += ` (${commitLink})`;
// Add issue references from commit body
if (commit.issueReferences && commit.issueReferences.length > 0) {
const issueLinks = commit.issueReferences.map(ref =>
issueUrl ? markdownIssueLink(ref, issueUrl) : ref
).join(', ');
entry += ` - References: ${issueLinks}`;
}
// Add closes references
if (commit.closesReferences && commit.closesReferences.length > 0) {
const closesLinks = commit.closesReferences.map(ref =>
issueUrl ? markdownIssueLink(ref, issueUrl) : ref
).join(', ');
entry += ` - Closes: ${closesLinks}`;
}
// Add migration notes if available
if (commit.aiSummary?.migrationNotes) {
entry += `\n - **Migration**: ${commit.aiSummary.migrationNotes}`;
}
// Add highlights as sub-bullets
if (commit.aiSummary?.highlights && commit.aiSummary.highlights.length > 1) {
commit.aiSummary.highlights.slice(1).forEach(highlight => {
entry += `\n - ${highlight}`;
});
}
return `${entry}\n`;
}
buildBreakingChangeEntry(commit, commitUrl, issueUrl, issueRegex) {
const commitLink = markdownCommitLink(commit.fullHash || commit.hash, commitUrl);
let entry = '- ';
if (commit.scope) {
entry += `**${commit.scope}**: `;
}
// Use AI-generated breaking change description if available
let breakingDescription = null;
if (commit.aiSummary?.breakingChangeDescription) {
breakingDescription = commit.aiSummary.breakingChangeDescription;
} else if (commit.breakingChanges && commit.breakingChanges.length > 0) {
breakingDescription = commit.breakingChanges[0];
} else {
breakingDescription = commit.aiSummary?.summary || commit.description || commit.subject || 'Breaking change';
}
entry += `**${breakingDescription}** (${commitLink})`;
// Add migration notes for breaking changes
if (commit.aiSummary?.migrationNotes) {
entry += `\n - **Migration**: ${commit.aiSummary.migrationNotes}`;
}
// Add additional breaking change details
if (commit.breakingChanges && commit.breakingChanges.length > 1) {
commit.breakingChanges.slice(1).forEach(change => {
entry += `\n - ${change}`;
});
}
// Add technical details for breaking changes
if (commit.aiSummary?.technicalDetails && commit.aiSummary.technicalDetails !== breakingDescription) {
entry += `\n - ${commit.aiSummary.technicalDetails}`;
}
return `${entry}\n\n`;
}
groupCommitsByType(commits) {
const configuredTypes = this.configManager?.getChangelogCommitTypes() || ['feat', 'fix', 'refactor', 'perf', 'docs', 'build', 'chore'];
const includeInvalid = this.configManager?.shouldIncludeInvalidCommits() ?? true;
const types = {};
commits.forEach(commit => {
// ALWAYS parse conventional commit first - this is the primary source of truth for types
const conventionalCommit = parseConventionalCommit(commit.subject || commit.message, commit.body);
let type = conventionalCommit.type;
// Only use AI analysis type as fallback if no conventional commit type found
if (!type || type === 'other') {
type = commit.aiSummary?.category || commit.type || (includeInvalid ? 'other' : null);
}
// Only include configured types or invalid commits if allowed
if (type && (configuredTypes.includes(type) || (type === 'other' && includeInvalid))) {
if (!types[type]) types[type] = [];
// Preserve AI analysis and enhance with conventional commit data
types[type].push({
...commit, // Preserves aiSummary and other AI analysis
...conventionalCommit, // Add conventional commit fields - this includes scope, breaking, etc.
type: type
});
}
});
return types;
}
getTypeHeader(type) {
const configuredHeadlines = this.configManager?.getHeadlines() || {};
// Use configured headlines from YAML config or defaults
const defaultHeaders = {
'feat': '๐ Features',
'fix': '๐ Bug Fixes',
'perf': 'โก Performance Improvements',
'refactor': 'โป๏ธ Refactoring',
'docs': '๐ Documentation',
'test': '๐งช Tests',
'build': '๐ง Build System',
'ci': 'โ๏ธ CI/CD',
'chore': '๐ง Maintenance',
'style': '๐ Code Style',
'revert': 'โช Reverts',
'merge': '๐ Merges',
'other': '๐ฆ Other Changes'
};
return configuredHeadlines[type] || defaultHeaders[type] || `๐ฆ ${type.charAt(0).toUpperCase() + type.slice(1)}`;
}
/**
* Generates a changelog from working directory changes (uncommitted files)
* @param {string} version - Version number for the release
* @returns {Promise<string>} Changelog content for working directory changes
*/
async generateChangelogFromChanges(version = null) {
// Use the proper WorkspaceChangelogService with optimized single AI call approach
const result = await this.workspaceChangelogService.generateWorkspaceChangelog(version, {
analysisMode: 'detailed' // Use detailed mode for better analysis
});
if (!result) {
console.log(colors.infoMessage('No changes detected in working directory.'));
return null;
}
return {
changelog: result.changelog,
analysis: result.context,
changes: result.changes
};
}
/**
* Builds the working directory changes section for the changelog
* @param {Object} workingDirAnalysis - Analysis results from analysis engine
* @returns {Promise<string>} Formatted working directory section
*/
async buildWorkingDirectorySection(workingDirAnalysis) {
// Use the optimized WorkspaceChangelogService for consistent formatting
const workspaceResult = await this.workspaceChangelogService.generateCommitStyleWorkingDirectoryEntries({
analysisMode: 'detailed',
workingDirAnalysis: workingDirAnalysis
});
if (!workspaceResult || !workspaceResult.entries || workspaceResult.entries.length === 0) {
return `## ๐ง Working Directory Changes\n\nNo working directory changes detected.\n\n`;
}
let section = `## ๐ง Working Directory Changes\n\n`;
// Add each entry in commit format
workspaceResult.entries.forEach(entry => {
section += `${entry}\n\n`;
});
return section;
}
async generateAIPoweredChangeEntry(change) {
const path = change.path || change.filePath || 'unknown';
const status = change.status || '??';
const category = change.category || 'other';
// Map status to change type
const statusTypeMap = {
'??': 'feature',
'A': 'feature',
'M': 'update',
'D': 'remove',
'R': 'refactor'
};
const changeType = statusTypeMap[status] || 'other';
// Use AI to analyze the actual diff content for intelligent description
let description = 'Working directory change';
let confidence = 85;
try {
// Use AI analysis for working directory files to get better descriptions
// especially important for architectural changes (deleted files)
if (this.aiAnalysisService && this.aiAnalysisService.hasAI) {
const aiSummary = await this.aiAnalysisService.generateAISummary({
subject: `Working directory change: ${path}`,
files: [change],
diffStats: { files: 1, insertions: 0, deletions: 0 }
});
if (aiSummary && aiSummary.technicalDetails) {
description = aiSummary.technicalDetails;
confidence = 85;
} else {
description = this.analyzeDiffContentForDescription(change);
confidence = 75;
}
} else {
description = this.analyzeDiffContentForDescription(change);
confidence = 75;
}
} catch (error) {
console.warn(`AI analysis failed for ${path}, using enhanced fallback`);
// Use enhanced analysis instead of generic
description = this.analyzeDiffContentForDescription(change);
confidence = 75;
}
// Format like commit entry
return `- (${changeType}) ${path} - ${description} (${confidence}%)`;
}
generateWorkingDirectoryChangeEntry(change) {
const path = change.path || change.filePath || 'unknown';
const status = change.status || '??';
const category = change.category || 'other';
const importance = change.importance || 'medium';
// Map status to change type
const statusTypeMap = {
'??': 'feature',
'A': 'feature',
'M': 'update',
'D': 'remove',
'R': 'refactor'
};
const changeType = statusTypeMap[status] || 'other';
// Generate AI-style description
const description = this.generateChangeDescription(change);
// Calculate confidence score based on change analysis
const confidence = this.calculateChangeConfidence(change);
// Format like commit entry
return `- (${changeType}) ${path} - ${description} (${confidence}%)`;
}
generateChangeDescription(change) {
const path = change.path || change.filePath;
const status = change.status;
const diff = change.diff || '';
const category = change.category || 'other';
if (status === 'D') {
return `Removed ${category} file from working directory`;
}
if (status === 'A' || status === '??') {
// Handle directories differently from files
if (path.endsWith('/')) {
return this.analyzeDirectoryAddition(path);
}
// For new files, analyze the actual content from the diff
const contentAnalysis = this.analyzeNewFileContent(diff, path);
if (contentAnalysis) {
return contentAnalysis;
}
return `Added new ${category} file to working directory`;
}
if (status === 'M') {
// Analyze what actually changed in the diff - prioritize functional analysis
const changeAnalysis = this.analyzeModifiedFileChanges(diff, path);
if (changeAnalysis) {
return `Modified ${category} file - ${changeAnalysis}`;
}
// Fallback to generic line count only if no functional analysis available
const lines = diff.split('\n');
const additions = lines.filter(line => line.startsWith('+') && !line.startsWith('+++')).length;
const deletions = lines.filter(line => line.startsWith('-') && !line.startsWith('---')).length;
return `Modified ${category} file with ${additions} additions and ${deletions} deletions`;
}
if (status === 'R') {
return `Renamed ${category} file in working directory`;
}
return `Updated ${category} file in working directory`;
}
analyzeNewFileContent(diff, path) {
if (!diff || !diff.includes('Content preview:')) {
return null;
}
// Extract the content preview from our diff
const previewMatch = diff.match(/Content preview:\n([\s\S]*?)(?:\n\.\.\. \(truncated\)|$)/);
if (!previewMatch || !previewMatch[1]) {
return null;
}
const content = previewMatch[1];
const filename = path ? path.split('/').pop() : 'file';
// Analyze based on file type and content
if (path && path.endsWith('.md')) {
return this.analyzeMarkdownContent(content, filename);
}
if (path && (path.endsWith('.js') || path.endsWith('.ts'))) {
return this.analyzeJavaScriptContent(content, filename);
}
if (path && path.endsWith('.json')) {
return this.analyzeJsonContent(content, filename);
}
if (path && path.startsWith('.')) {
return this.analyzeConfigContent(content, filename);
}
// Generic content analysis
const lines = content.split('\n').length;
return `Added new file containing ${lines} lines of content`;
}
analyzeMarkdownContent(content, filename) {
const lines = content.split('\n');
const headings = lines.filter(line => line.startsWith('#')).length;
const codeBlocks = (content.match(/```/g) || []).length / 2;
const links = (content.match(/\[.*?\]\(.*?\)/g) || []).length;
const tables = (content.match(/\|.*\|/g) || []).length;
// Extract actual content insights
const contentLower = content.toLowerCase();
let documentType = '';
let keyContent = [];
if (filename.includes('MAPPING') || filename.includes('mapping')) {
documentType = 'architectural mapping documentation';
if (contentLower.includes('class')) keyContent.push('class documentation');
if (contentLower.includes('function')) keyContent.push('function mapping');
if (contentLower.includes('service')) keyContent.push('service architecture');
} else if (filename.includes('ENVIRONMENT') || filename.includes('env')) {
documentType = 'environment variable configuration guide';
const envVars = (content.match(/[A-Z_]+=/g) || []).length;
if (envVars > 0) keyContent.push(`${envVars} environment variables`);
} else if (filename.includes('README') || filename.includes('readme')) {
documentType = 'project documentation';
if (contentLower.includes('install')) keyContent.push('installation guide');
if (contentLower.includes('setup')) keyContent.push('setup instructions');
} else if (filename.includes('CHANGELOG') || filename.includes('changelog')) {
documentType = 'changelog documentation';
const versions = (content.match(/##?\s+\[?\d+\.\d+/g) || []).length;
if (versions > 0) keyContent.push(`${versions} version entries`);
} else {
documentType = 'documentation';
}
let analysis = `Added ${filename} ${documentType}`;
// Add specific content details
const details = [];
if (keyContent.length > 0) details.push(...keyContent);
if (headings > 0) details.push(`${headings} sections`);
if (codeBlocks > 0) details.push(`${codeBlocks} code examples`);
if (tables > 0) details.push(`${tables} table rows`);
if (links > 0) details.push(`${links} links`);
if (details.length > 0) {
analysis += ` containing ${details.slice(0, 3).join(', ')}`;
}
return analysis;
}
analyzeJavaScriptContent(content, filename) {
const functions = (content.match(/function\s+\w+|const\s+\w+\s*=\s*\(|=>\s*{/g) || []).length;
const imports = (content.match(/import\s+.*?from|require\s*\(/g) || []).length;
const classes = (content.match(/class\s+\w+/g) || []).length;
const exports = (content.match(/export\s+/g) || []).length;
// Extract actual content insights
const lines = content.split('\n');
const comments = lines.filter(line => line.trim().startsWith('//') || line.trim().startsWith('*')).length;
const consoleStatements = (content.match(/console\.(log|error|warn|info)/g) || []).length;
// Look for specific patterns that indicate purpose
let purpose = '';
if (content.includes('verification') || content.includes('check')) {
purpose = 'verification script';
} else if (content.includes('test') || content.includes('spec')) {
purpose = 'test file';
} else if (content.includes('server') || content.includes('mcp')) {
purpose = 'MCP server entry point';
} else if (content.includes('setup') || content.includes('config')) {
purpose = 'setup/configuration script';
} else if (content.includes('import') && content.includes('update')) {
purpose = 'import update utility';
} else if (filename.includes('util')) {
purpose = 'utility module';
} else {
purpose = 'JavaScript module';
}
// Extract meaningful text content for analysis
const meaningfulContent = content
.split('\n')
.filter(line => line.trim() && !line.trim().startsWith('//') && !line.trim().startsWith('*'))
.join(' ')
.toLowerCase();
// Look for specific functionality indicators
let functionality = [];
if (meaningfulContent.includes('method') && meaningfulContent.includes('analysis')) {
functionality.push('analyzing methods');
}
if (meaningfulContent.includes('legacy') || meaningfulContent.includes('old')) {
functionality.push('legacy code handling');
}
if (meaningfulContent.includes('repository') || meaningfulContent.includes('repo')) {
functionality.push('repository analysis');
}
if (meaningfulContent.includes('investigation') || meaningfulContent.includes('check')) {
functionality.push('investigation checklist');
}
if (consoleStatements > 3) {
functionality.push('detailed logging');
}
let analysis = `Added ${filename.replace('.js', '')} ${purpose}`;
// Add functionality description
if (functionality.length > 0) {
analysis += ` for ${functionality.slice(0, 2).join(' and ')}`;
}
const features = [];
if (classes > 0) features.push(`${classes} class${classes > 1 ? 'es' : ''}`);
if (functions > 0) features.push(`${functions} function${functions > 1 ? 's' : ''}`);
if (imports > 0) features.push(`${imports} import${imports > 1 ? 's' : ''}`);
if (exports > 0) features.push(`${exports} export${exports > 1 ? 's' : ''}`);
if (features.length > 0) {
analysis += ` with ${features.slice(0, 3).join(', ')}`;
}
return analysis;
}
analyzeJsonContent(content, filename) {
try {
const parsed = JSON.parse(content);
if (filename === 'package.json') {
const deps = Object.keys(parsed.dependencies || {}).length;
const devDeps = Object.keys(parsed.devDependencies || {}).length;
const scripts = Object.keys(parsed.scripts || {}).length;
return `Added package.json with ${deps} dependencies, ${devDeps} dev dependencies, and ${scripts} scripts`;
}
if (filename === 'manifest.json') {
return `Added manifest.json configuration for ${parsed.name || 'application'} with ${Object.keys(parsed).length} properties`;
}
const keys = Object.keys(parsed).length;
return `Added ${filename} configuration with ${keys} settings`;
} catch {
return `Added ${filename} JSON configuration file`;
}
}
analyzeConfigContent(content, filename) {
const lines = content.split('\n').filter(line => line.trim()).length;
if (filename.includes('env')) {
const variables = content.split('\n').filter(line => line.includes('=')).length;
return `Added environment configuration with ${variables} variables`;
}
if (filename.includes('ignore')) {
const patterns = content.split('\n').filter(line => line.trim() && !line.startsWith('#')).length;
return `Added ignore file with ${patterns} patterns`;
}
return `Added ${filename} configuration with ${lines} lines`;
}
analyzeDirectoryAddition(path) {
const dirName = path.replace(/\/$/, '');
// Analyze based on directory name and purpose
if (dirName === '.claude') {
return 'Added Claude Code configuration directory for AI coding assistant integration';
}
if (dirName === '.context') {
return 'Added context directory for project context and documentation';
}
if (dirName === '.github') {
return 'Added GitHub configuration directory for workflows, templates, and repository settings';
}
if (dirName === 'docs') {
return 'Added documentation directory for project documentation';
}
if (dirName === 'test' || dirName === 'tests') {
return 'Added test directory for unit tests and test files';
}
if (dirName === 'src') {
return 'Added source code directory for main application code';
}
if (dirName === 'lib') {
return 'Added library directory for shared code and utilities';
}
if (dirName === 'bin') {
return 'Added binary directory for executable scripts';
}
if (dirName === 'config') {
return 'Added configuration directory for application settings';
}
if (dirName === 'server') {
return 'Added server directory for server-side code and configuration';
}
if (dirName === 'dxt') {
return 'Added DXT directory for development tooling and utilities';
}
if (dirName.includes('test')) {
return `Added ${dirName} directory for testing and test results`;
}
if (dirName.startsWith('.')) {
return `Added ${dirName} configuration directory`;
}
return `Added ${dirName} directory`;
}
groupWorkingDirectoryChangesByType(changes) {
const grouped = {
'architectural': [],
'feature': [],
'refactor': [],
'update': [],
'remove': [],
'config': [],
'docs': [],
'test': []
};
for (const change of changes) {
const path = change.path || change.filePath || '';
const status = change.status || '??';
const category = change.category || 'other';
// Determine the change type based on content and context
if (this.isArchitecturalChange(path, change)) {
grouped.architectural.push(change);
} else if (status === 'D') {
grouped.remove.push(change);
} else if (status === 'M') {
if (this.isConfigurationFile(path)) {
grouped.config.push(change);
} else {
grouped.update.push(change);
}
} else if (status === 'A' || status === '??') {
if (this.isTestFile(path)) {
grouped.test.push(change);
} else if (this.isDocumentationFile(path)) {
grouped.docs.push(change);
} else if (this.isConfigurationFile(path)) {
grouped.config.push(change);
} else {
grouped.feature.push(change);
}
}
}
// Remove empty groups
return Object.fromEntries(
Object.entries(grouped).filter(([key, value]) => value.length > 0)
);
}
async generateGroupedChangeEntry(changeType, files) {
const fileCount = files.length;
// Generate individual file descriptions using our existing perfect analysis
const fileDescriptions = [];
for (const file of files) {
const description = await this.generateAIPoweredChangeEntry(file);
if (description) {
// Extract just the description part (remove the "- (type) path - " prefix)
const match = description.match(/- \(\w+\) .+ - (.+) \(\d+%\)/);
if (match) {
fileDescriptions.push(match[1]);
}
}
}
// Intelligently summarize the individual descriptions
const summary = this.summarizeFileDescriptions(fileDescriptions, changeType, fileCount);
return `- (${changeType}) ${summary}`;
}
summarizeFileDescriptions(descriptions, changeType, fileCount) {
if (descriptions.length === 0) {
return `${changeType} changes affecting ${fileCount} files`;
}
// Extract specific, detailed functional changes and contexts
const specificChanges = [];
const keyFiles = [];
const technologies = new Set();
const purposes = new Set();
for (const desc of descriptions) {
// Use the existing individual file analysis instead of hardcoded patterns
if (desc && desc.trim() && desc !== 'No description available') {
specificChanges.push(desc);
}
}
// If we have specific changes, create detailed description
if (specificChanges.length > 0) {
const uniqueChanges = [...new Set(specificChanges)];
// Create a detailed, specific description
if (uniqueChanges.length === 1) {
const change = uniqueChanges[0];
const techList = [...technologies];
if (techList.length > 0) {
return `${change} affecting ${techList.join(', ')} (${fileCount} files)`;
}
return `${change} (${fileCount} files)`;
} else if (uniqueChanges.length === 2) {
return `${uniqueChanges.join(' and ')} (${fileCount} files)`;
} else {
// For architectural changes affecting many files, show more detail
if (changeType === 'architectural' && fileCount >= 10) {
// Show first 5 changes for major architectural changes
const primary = uniqueChanges.slice(0, 5).join(', ');
const remaining = uniqueChanges.length - 5;
if (remaining > 0) {
return `${primary} and ${remaining} additional change${remaining > 1 ? 's' : ''} (${fileCount} files)`;
} else {
return `${primary} (${fileCount} files)`;
}
} else {
// For smaller changes, show more details instead of hiding them
if (uniqueChanges.length <= 5) {
// Show all changes if 5 or fewer - user wants to see exactly what each feature is
return `${uniqueChanges.join(', ')} (${fileCount} files)`;
} else {
// Show first 4 and remaining count for 6+ changes
const primary = uniqueChanges.slice(0, 4).join(', ');
const remaining = uniqueChanges.length - 4;
return `${primary} and ${remaining} additional enhancement${remaining > 1 ? 's' : ''} (${fileCount} files)`;
}
}
}
}
// Enhanced fallback with more context
const techList = [...technologies];
const purposeList = [...purposes];
if (techList.length > 0 && purposeList.length > 0) {
return `${changeType} changes to ${techList.slice(0, 2).join(', ')} for ${purposeList.slice(0, 2).join(', ')} (${fileCount} files)`;
} else if (techList.length > 0) {
return `${changeType} changes to ${techList.slice(0, 3).join(', ')} (${fileCount} files)`;
} else if (purposeList.length > 0) {
return `${changeType} changes for ${purposeList.slice(0, 3).join(', ')} (${fileCount} files)`;
}
// Final fallback
return `${changeType} changes affecting ${fileCount} files`;
}
isArchitecturalChange(path, change) {
return path === 'src/' || path.includes('lib/') ||
path.includes('bin/') || path.includes('package.json');
}
isConfigurationFile(path) {
return path.startsWith('.') || path.includes('config') ||
path.endsWith('.json') || path.endsWith('.env') ||
path.includes('package.json');
}
isTestFile(path) {
return path.includes('test') || path.includes('spec') ||
path.startsWith('test-') || path.includes('/test/');
}
isDocumentationFile(path) {
return path.endsWith('.md') || path.includes('docs') ||
path.includes('README') || path.includes('CHANGELOG');
}
analyzeModifiedFileChanges(diff, path) {
const lines = diff.split('\n');
const addedLines = lines.filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = lines.filter(line => line.startsWith('-') && !line.startsWith('---'));
const addedContent = addedLines.map(line => line.substring(1)).join('\n');
const removedContent = removedLines.map(line => line.substring(1)).join('\n');
// Extract actual functionality changes instead of just counting
const functionalChanges = this.extractFunctionalityChanges(addedContent, removedContent, path);
if (functionalChanges && functionalChanges.length > 0) {
return functionalChanges.join(', ');
}
// Fallback to basic pattern analysis if no functional changes detected
const changes = [];
// Function changes
const addedFunctions = (addedContent.match(/function\s+\w+|const\s+\w+\s*=\s*\(/g) || []).length;
const removedFunctions = (removedContent.match(/function\s+\w+|const\s+\w+\s*=\s*\(/g) || []).length;
if (addedFunctions > 0) changes.push(`Added ${addedFunctions} function${addedFunctions > 1 ? 's' : ''}`);
if (removedFunctions > 0) changes.push(`removed ${removedFunctions} function${removedFunctions > 1 ? 's' : ''}`);
// Import/export changes
const addedImports = (addedContent.match(/import\s+.*?from|require\s*\(/g) || []).length;
const removedImports = (removedContent.match(/import\s+.*?from|require\s*\(/g) || []).length;
if (addedImports > 0) changes.push(`added ${addedImports} import${addedImports > 1 ? 's' : ''}`);
if (removedImports > 0) changes.push(`removed ${removedImports} import${removedImports > 1 ? 's' : ''}`);
// Class changes
const addedClasses = (addedContent.match(/class\s+\w+/g) || []).length;
const removedClasses = (removedContent.match(/class\s+\w+/g) || []).length;
if (addedClasses > 0) changes.push(`added ${addedClasses} class${addedClasses > 1 ? 'es' : ''}`);
if (removedClasses > 0) changes.push(`removed ${removedClasses} class${removedClasses > 1 ? 'es' : ''}`);
// Method changes within classes
const addedMethods = (addedContent.match(/\s+\w+\s*\([^)]*\)\s*{/g) || []).length;
const removedMethods = (removedContent.match(/\s+\w+\s*\([^)]*\)\s*{/g) || []).length;
if (addedMethods > 0) changes.push(`added ${addedMethods} method${addedMethods > 1 ? 's' : ''}`);
if (removedMethods > 0) changes.push(`removed ${removedMethods} method${removedMethods > 1 ? 's' : ''}`);
// Error handling changes
if (addedContent.includes('try') || addedContent.includes('catch')) {
changes.push('enhanced error handling');
}
// Configuration-specific changes
if (path && path.includes('package.json')) {
if (addedContent.includes('"dependencies"')) changes.push('updated dependencies');
if (addedContent.includes('"scripts"')) changes.push('modified scripts');
}
return changes.length > 0 ? changes.slice(0, 3).join(', ') : null;
}
inferFileContent(path, diff) {
if (!path) return 'content';
const ext = path.split('.').pop()?.toLowerCase();
if (['js', 'ts', 'jsx', 'tsx'].includes(ext)) {
if (diff.includes('function') || diff.includes('const ') || diff.includes('class ')) {
return 'JavaScript/TypeScript code including functions and classes';
}
return 'JavaScript/TypeScript code';
}
if (ext === 'json') {
if (path.includes('package.json')) {
return 'package configuration and dependencies';
}
return 'JSON configuration data';
}
if (['md', 'txt'].includes(ext)) {
return 'documentation and text content';
}
if (['png', 'jpg', 'jpeg', 'gif', 'svg'].includes(ext)) {
return 'image or asset data';
}
if (['css', 'scss', 'less'].includes(ext)) {
return 'styling definitions';
}
return 'content';
}
extractSpecificChanges(diff, path) {
if (!diff || diff.length < 50) return null;
const addedLines = diff.split('\n').filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = diff.split('\n').filter(line => line.startsWith('-') && !line.startsWith('---'));
const changes = [];
// Check for function changes
const addedFunctions = addedLines.filter(line =>
line.includes('function ') || line.includes('const ') && line.includes('= (') || line.includes('async ')
);
if (addedFunctions.length > 0) {
changes.push(`Added ${addedFunctions.length} new function${addedFunctions.length > 1 ? 's' : ''}`);
}
// Check for import changes
const addedImports = addedLines.filter(line => line.includes('import ') || line.includes('require('));
const removedImports = removedLines.filter(line => line.includes('import ') || line.includes('require('));
if (addedImports.length > 0) {
changes.push(`added ${addedImports.length} import${addedImports.length > 1 ? 's' : ''}`);
}
if (removedImports.length > 0) {
changes.push(`removed ${removedImports.length} import${removedImports.length > 1 ? 's' : ''}`);
}
// Check for package.json specific changes
if (path && path.includes('package.json')) {
if (diff.includes('"dependencies"') || diff.includes('"devDependencies"')) {
changes.push('updated project dependencies');
}
if (diff.includes('"scripts"')) {
changes.push('modified build scripts');
}
}
return changes.length > 0 ? changes.slice(0, 2).join(' and ') : null;
}
extractFunctionalityChanges(addedContent, removedContent, path) {
const functionalChanges = [];
// Analyze what functionality was actually added or removed
if (removedContent.length > 50) { // Only analyze substantial changes
// Connection/Testing functionality
if (removedContent.includes('testConnection') || removedContent.includes('test.success')) {
functionalChanges.push('removed connection testing functionality');
}
// Model availability checking
if (removedContent.includes('getAvailableModels') || removedContent.includes('getAvailable') || removedContent.includes('models.length')) {
functionalChanges.push('removed model availability checking');
}
// Authentication/Provider initialization
if (removedContent.includes('new AIProvider') || removedContent.includes('provider =') || removedContent.includes('Provider(')) {
functionalChanges.push('removed provider initialization');
}
// Error handling/validation
if (removedContent.includes('if (testResult.success)') || removedContent.includes('Connection test failed')) {
functionalChanges.push('removed error handling and validation');
}
// Configuration/Setup
if (removedContent.includes('require(') && addedContent.includes('skipping test')) {
functionalChanges.push('simplified configuration setup');
}
// Database/API operations
if (removedContent.includes('await ') && removedContent.includes('database') || removedContent.includes('api')) {
functionalChanges.push('removed database/API operations');
}
// UI/Display functionality
if (removedContent.includes('forEach') && removedContent.includes('console.log')) {
functionalChanges.push('removed dynamic model listing display');
}
// Authentication changes
if (removedContent.includes('auth') || removedContent.includes('token') || removedContent.includes('key')) {
functionalChanges.push('modified authentication handling');
}
}
// Analyze what was added
if (addedContent.length > 20) {
if (addedContent.includes('Configuration testing requires') || addedContent.includes('skipping test')) {
functionalChanges.push('added configuration migration notice');
}
if (addedContent.includes('--validate') || addedContent.includes('test your configuration')) {
functionalChanges.push('redirected testing to CLI --validate');
}
// New functionality patterns
if (addedContent.includes('async ') && addedContent.includes('function')) {
const newFunctions = (addedContent.match(/async\\s+function\\s+([\\w]+)/g) || []).map(f => f.replace('async function ', ''));
if (newFunctions.length > 0) {
functionalChanges.push(`added ${newFunctions.slice(0, 2).join(', ')} async functionality`);
}
}
}
// File-specific analysis
if (path) {
if (path.includes('setup') && functionalChanges.length === 0) {
if (removedContent.includes('test') && addedContent.includes('validate')) {
functionalChanges.push('replaced test functions with validation system');
}
}
if (path.includes('provider') || path.includes('ai-')) {
if (removedContent.includes('import') && addedContent.includes('moved to')) {
functionalChanges.push('refactored import paths for new architecture');
}
}
}
return functionalChanges.length > 0 ? functionalChanges : null;
}
extractFileDescriptionFromAI(aiSummary, change) {
// Extract meaningful description from AI analysis
if (!aiSummary || typeof aiSummary !== 'string') {
return this.analyzeDiffContentForDescription(change);
}
// Clean up AI response to be suitable for changelog entry
let description = aiSummary.trim();
// Remove generic prefixes
description = description.replace(/^(This change|The change|Change:|Summary:)\s*/i, '');
description = description.replace(/^(Analysis|Description):\s*/i, '');
// Take first sentence if it's a long response
const sentences = description.split(/[.!?]\s+/);
if (sentences.length > 1 && sentences[0].length > 20) {
description = sentences[0];
}
// Capitalize first letter
if (description.length > 0) {
description = description.charAt(0).toUpperCase() + description.slice(1);
}
// Fallback if AI response is too generic
if (description.length < 10 ||
description.toLowerCase().includes('file changed') ||
description.toLowerCase().includes('modified') ||
description.toLowerCase().includes('updated')) {
return this.analyzeDiffContentForDescription(change);
}
return description;
}
analyzeDiffContentForDescription(change) {
const path = change.path || change.filePath;
const status = change.status;
const diff = change.diff || '';
const category = change.category || 'other';
if (!diff || diff === 'Analysis failed') {
return this.generateChangeDescription(change);
}
// For modified files, use the enhanced functional analysis
if (status === 'M' && this.analyzeModifiedFileChanges) {
const functionalAnalysis = this.analyzeModifiedFileChanges(diff, path);
if (functionalAnalysis) {
return functionalAnalysis;
}
}
// For new files, use content analysis
if (status === 'A' || status === '??') {
const newFileAnalysis = this.analyzeNewFileContent(diff, path);
if (newFileAnalysis) {
return newFileAnalysis;
}
}
// For deleted files, analyze what was removed
if (status === 'D') {
const deletedFileAnalysis = this.analyzeDeletedFileContent(change);
if (deletedFileAnalysis) {
return deletedFileAnalysis;
}
}
// Fallback to old pattern-based analysis for other cases
const lines = diff.split('\n');
const addedLines = lines.filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = lines.filter(line => line.startsWith('-') && !line.startsWith('---'));
const addedContent = addedLines.map(line => line.substring(1).trim()).join(' ');
const removedContent = removedLines.map(line => line.substring(1).trim()).join(' ');
// Look for specific patterns in the actual changes
const changes = [];
// Function/method additions
const addedFunctions = this.extractFunctions(addedContent);
const removedFunctions = this.extractFunctions(removedContent);
if (addedFunctions.length > 0) {
changes.push(`Added ${addedFunctions.slice(0, 2).join(', ')} method${addedFunctions.length > 1 ? 's' : ''}`);
}
if (removedFunctions.length > 0) {
changes.push(`removed ${removedFunctions.slice(0, 2).join(', ')} method${removedFunctions.length > 1 ? 's' : ''}`);
}
// Import/export changes
if (addedContent.includes('import ') || addedContent.includes('require(')) {
const importCount = (addedContent.match(/import\s+/g) || []).length;
changes.push(`added ${importCount} import${importCount > 1 ? 's' : ''}`);
}
if (removedContent.includes('import ') || removedContent.includes('require(')) {
const importCount = (removedContent.match(/import\s+/g) || []).length;
changes.push(`removed ${importCount} import${importCount > 1 ? 's' : ''}`);
}
// Configuration changes
if (path && path.includes('package.json')) {
if (addedContent.includes('"dependencies"') || addedContent.includes('"devDependencies"')) {
changes.push('updated dependencies');
}
if (addedContent.includes('"scripts"')) {
changes.push('modified scripts');
}
}
// Error handling
if (addedContent.includes('try') || addedContent.includes('catch') || addedContent.includes('throw')) {
changes.push('enhanced error handling');
}
// Build meaningful description
if (changes.length > 0) {
const changeDesc = changes.slice(0, 3).join(', ');
return `Modified ${category} file with ${addedLines.length} additions and ${removedLines.length} deletions. ${changeDesc}`;
}
// Generic fallback with line counts
if (status === 'M' && (addedLines.length > 0 || removedLines.length > 0)) {
return `Modified ${category} file with ${addedLines.length} additions and ${removedLines.length} deletions`;
}
return this.generateChangeDescription(change);
}
calculateChangeConfidence(change) {
// Simple confidence calculation based on available data
let confidence = 70; // Base confidence
if (change.diff && change.diff.length > 50) confidence += 10;
if (change.semanticChanges && change.semanticChanges.patterns && change.semanticChanges.patterns.length > 0) confidence += 10;
if (change.complexity && change.complexity.score) confidence += 5;
if (change.functionalImpact) confidence += 5;
return Math.min(confidence, 95);
}
generateDiffSummary(change) {
if (!change.diff || change.diff === 'Analysis failed') {
return null;
}
const diff = change.diff;
const status = change.status;
const filePath = change.path || change.filePath;
// Handle different file statuses
if (status === 'A' || status === '??') {
if (diff.includes('New file created with')) {
const lineMatch = diff.match(/(\d+) lines/);
const lines = lineMatch ? lineMatch[1] : 'unknown';
return `New file with ${lines} lines`;
}
return 'New file created';
}
if (status === 'D') {
return 'File deleted';
}
if (status === 'R') {
return 'File renamed';
}
// For modified files, analyze actual diff content
if (status === 'M' || status.includes('M')) {
const analysis = this.analyzeDiffContent(diff, filePath);
return analysis;
}
return null;
}
analyzeDiffContent(diff, filePath) {
const lines = diff.split('\n');
const addedLines = lines.filter(line => line.startsWith('+') && !line.startsWith('+++'));
const removedLines = lines.filter(line => line.startsWith('-') && !line.startsWith('---'));
const changes = [];
// Analyze what was actually changed
const addedContent = addedLines.map(line => line.substring(1).trim()).join(' ');
const removedContent = removedLines.map(line => line.substring(1).trim()).join(' ');
// Function/method changes
const addedFunctions = this.extractFunctions(addedContent);
const removedFunctions = this.extractFunctions(removedContent);
if (addedFunctions.length > 0) {
changes.push(`added ${addedFunctions.slice(0, 2).join(', ')}`);
}
if (removedFunctions.length > 0) {
changes.push(`removed ${removedFunctions.slice(0, 2).join(', ')}`);
}
// Configuration changes
if (filePath && filePath.includes('package.json')) {
const configChanges = this.analyzePackageJsonChanges(addedContent, removedContent);
if (configChanges.length > 0) {
changes.push(...configChanges);
}
}
// Import/export changes
const importChanges = this.analyzeImportChanges(addedContent, removedContent);
if (importChanges.length > 0) {
changes.push(...importChanges);
}
// Variable/constant changes
const variableChanges = this.analyzeVariableChanges(addedContent, removedContent);
if (variableChanges.length > 0) {
changes.push(...variableChanges);
}
// Error handling changes
const errorChanges = this.analyzeErrorHandling(addedContent, removedContent);
if (errorChanges.length > 0) {
changes.push(...errorChanges);
}
// Build summary with line counts
let summary = `+${addedLines.length}, -${removedLines.length} lines`;
if (changes.length > 0) {
summary += `: ${changes.slice(0, 3).join(', ')}`;
}
return summary;
}
extractFunctions(content) {
const functions = [];
const patterns = [
/function\s+(\w+)/g,
/(\w+)\s*\(/g,
/const\s+(\w+)\s*=\s*\(/g,
/async\s+(\w+)/g
];
patterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
if (match[1] && !functions.includes(match[1])) {
functions.push(match[1]);
}
}
});
return functions.slice(0, 3);
}
analyzePackageJsonChanges(added, removed) {
const changes = [];
if (added.includes('"dependencies"') || added.includes('"devDependencies"')) {
changes.push('updated dependencies');
}
if (added.includes('"scripts"')) {
changes.push('updated scripts');
}
if (added.includes('"name"') || added.includes('"version"')) {
changes.push('updated metadata');
}
if (added.includes('"repository"') || added.includes('"author"')) {
changes.push('updated project info');
}
return changes;
}
analyzeImportChanges(added, removed) {
const changes = [];
if (added.includes('import') || added.includes('require(')) {
changes.push('added imports');
}
if (removed.includes('import') || removed.includes('require(')) {
changes.push('removed imports');
}
if (added.includes('export')) {
changes.push('added exports');
}
if (removed.includes('export')) {
changes.push('removed exports');
}
return changes;
}
analyzeVariableChanges(added, removed) {
const changes = [];
if (added.includes('const ') || added.includes('let ') || added.includes('var ')) {
changes.push('added variables');
}
if (added.includes('= {') || added.includes('= [')) {
changes.push('updated data structures');
}
return changes;
}
analyzeErrorHandling(added, removed) {
const changes = [];
if (added.includes('try') || added.includes('catch') || added.includes('throw')) {
changes.push('enhanced error handling');
}
if (added.includes('console.error') || added.includes('console.warn')) {
changes.push('added logging');
}
if (added.includes('if (') && added.includes('error')) {
changes.push('added error checks');
}
return changes;
}
buildChangelogFromAnalysis(analysis, changes, version) {
const timestamp = new Date().toISOString().split('T')[0];
let changelog = `# Working Directory Changes - ${timestamp}\n\n`;
if (version) {
changelog += `## Version ${version}\n\n`;
}
changelog += `### Summary\n`;
changelog += `${analysis.summary || 'Working directory changes detected'}\n\n`;
changelog += `### Changes (${changes.length} files)\n`;
changes.forEach(change => {
changelog += `- ${change.status} ${change.path}\n`;
});
return changelog;
}
analyzeDeletedFileContent(change) {
const filePath = change.path || change.filePath;
const beforeContent = change.beforeContent || '';
if (!beforeContent || beforeContent.trim() === '') {
return `Removed ${path.basename(filePath)} file from working directory`;
}
// Analyze what functionality was removed based on the file content
const language = change.language || this.detectLanguageFromPath(filePath);
const category = change.category || 'other';
if (language === 'javascript') {
return this.analyzeDeletedJavaScriptFile(beforeContent, filePath, category);
} else if (language === 'markdown') {
return this.analyzeDeletedMarkdownFile(beforeContent, filePath);
} else if (language === 'json') {
return this.analyzeDeletedJsonFile(beforeContent, filePath);
} else {
// Generic analysis for other file types
const lines = beforeContent.split('\n').length;
const fileName = path.basename(filePath);
const extension = path.extname(filePath);
if (category === 'source') {
return `Removed ${fileName} source file containing ${lines} lines of ${language || extension} code`;
} else if (category === 'documentation') {
return `Removed ${fileName} documentation file containing ${lines} lines`;
} else if (category === 'configuration') {
return `Removed ${fileName} configuration file`;
} else {
return `Removed ${fileName} file containing ${lines} lines`;
}
}
}
analyzeDeletedJavaScriptFile(content, filePath, category) {
const fileName = path.basename(filePath);
// Extract functions, classes, and main features
const functions = this.extractFunctions(content);
const classes = this.extractClasses(content);
const imports = this.extractImports(content);
const exports = this.extractExports(content);
const features = [];
if (classes.length > 0) {
features.push(`${classes.length} class${classes.length > 1 ? 'es' : ''} (${classes.slice(0, 2).join(', ')})`);
}
if (functions.length > 0) {
features.push(`${functions.length} function${functions.length > 1 ? 's' : ''} (${functions.slice(0, 3).join(', ')})`);
}
if (imports.length > 0) {
features.push(`${imports.length} import${imports.length > 1 ? 's' : ''}`);
}
if (exports.length > 0) {
features.push(`${exports.length} export${exports.length > 1 ? 's' : ''}`);
}
// Determine the purpose based on file name and content patterns
let purpose = '';
if (fileName.includes('generator')) {
purpose = 'generator implementation';
} else if (fileName.includes('provider')) {
purpose = 'provider implementation';
} else if (fileName.includes('config')) {
purpose = 'configuration management';
} else if (fileName.includes('manager') || fileName.includes('service')) {
purpose = 'service management';
} else if (fileName.includes('cli') || fileName.includes('bin')) {
purpose = 'CLI functionality';
} else if (content.includes('class ') && content.includes('constructor')) {
purpose = 'service class';
} else if (content.includes('module.exports') || content.includes('export')) {
purpose = 'module';
} else {
purpose = 'JavaScript module';
}
if (features.length > 0) {
return `Removed ${fileName} ${purpose} containing ${features.join(', ')}`;
} else {
const lines = content.split('\n').length;
return `Removed ${fileName} ${purpose} containing ${lines} lines of JavaScript code`;
}
}
analyzeDeletedMarkdownFile(content, filePath) {
const fileName = path.basename(filePath);
const lines = content.split('\n');
const sections = lines.filter(line => line.startsWith('#')).length;
if (fileName.toLowerCase().includes('readme')) {
return `Removed ${fileName} project documentation containing ${sections} sections and ${lines.length} lines`;
} else if (fileName.toLowerCase().includes('changelog')) {
return `Removed ${fileName} changelog documentation`;
} else if (sections > 0) {
return `Removed ${fileName} documentation containing ${sections} sections`;
} else {
return `Removed ${fileName} documentation containing ${lines.length} lines`;
}
}
analyzeDeletedJsonFile(content, filePath) {
const fileName = path.basename(filePath);
try {
const parsed = JSON.parse(content);
const keys = Object.keys(parsed).length;
if (fileName === 'package.json') {
return `Removed package.json configuration with ${keys} properties (dependencies, metadata, scripts)`;
} else {
return `Removed ${fileName} configuration file with ${keys} configuration properties`;
}
} catch {
return `Removed ${fileName} JSON configuration file`;
}
}
detectLanguageFromPath(filePath) {
const ext = path.extname(filePath).toLowerCase();
const languageMap = {
'.js': 'javascript',
'.ts': 'typescript',
'.json': 'json',
'.md': 'markdown',
'.py': 'python',
'.java': 'java',
'.css': 'css',
'.html': 'html',
'.yml': 'yaml',
'.yaml': 'yaml'
};
return languageMap[ext] || 'text';
}
}