UNPKG

@entro314labs/ai-changelog-generator

Version:

AI-powered changelog generator with MCP server support - works with most providers, online and local models

2,425 lines โ€ข 117 kB
import path from 'node:path';
import process from 'node:process';
import colors from '../../shared/constants/colors.js';
import { EnhancedConsole } from '../../shared/utils/cli-ui.js';
import { DiffProcessor } from '../../shared/utils/diff-processor.js';
import { assessFileImportance, categorizeFile, detectLanguage, getWorkingDirectoryChanges, handleUnifiedOutput, markdownCommitLink, markdownCommitRangeLink, markdownIssueLink, parseConventionalCommit, processIssueReferences, renderChangelogTemplate, sleep, summarizeFileChanges, } from '../../shared/utils/utils.js';
export class ChangelogService {
    constructor(gitService, aiAnalysisService, analysisEngine = null, configManager = null) {
        this.gitService = gitService;
        this.aiAnalysisService = aiAnalysisService;
        this.analysisEngine = analysisEngine;
        this.configManager = configManager;
    }
    getRepositoryCwd() {
        return this.gitService?.gitManager?.options?.cwd;
    }
    /**
     * Filter out commits whose message matches the configured ignore pattern
     * (defaults to WIP commits). Operates on `{hash, message}` objects returned
     * by the git service.
     * @param {Array<{hash: string, message: string}>} commits - Raw commit list
     * @returns {Array<{hash: string, message: string}>} Commits to keep
     */
    filterIgnoredCommits(commits) {
        if (!(commits && Array.isArray(commits)) || commits.length === 0) {
            return commits || [];
        }
        const ignoreRegex = this.configManager?.getCommitIgnoreRegex?.();
        if (!ignoreRegex) {
            return commits;
        }
        return commits.filter((commit) => {
            const message = commit?.message || commit?.subject || '';
            return !ignoreRegex.test(message);
        });
    }
    classifyCommit(commitAnalysis, aiSummary = null) {
        const fallbackType = commitAnalysis?.type || commitAnalysis?.semanticAnalysis?.category || 'other';
        const fallbackBreaking = Boolean(commitAnalysis?.breaking ||
            commitAnalysis?.semanticAnalysis?.breaking ||
            (commitAnalysis?.breakingChanges?.length ?? 0) > 0);
        if (aiSummary) {
            return {
                type: aiSummary.category || fallbackType,
                breaking: Boolean(aiSummary.breaking ?? fallbackBreaking),
            };
        }
        return {
            type: fallbackType,
            breaking: fallbackBreaking,
        };
    }
    async enrichCommitWithAISummary(commitAnalysis, options = {}) {
        const classification = this.classifyCommit(commitAnalysis);
        // Keep commit data with rule-based classification when AI is unavailable OR explicitly
        // disabled (includeAIAnalysis:false), so a --no-ai run still retains and classifies commits.
        if (options.includeAIAnalysis === false ||
            !this.aiAnalysisService?.generateAISummary ||
            !this.aiAnalysisService?.selectOptimalModel) {
            return {
                ...commitAnalysis,
                ...classification,
            };
        }
        try {
            const selectedModel = await this.aiAnalysisService.selectOptimalModel(commitAnalysis);
            const aiSummary = await this.aiAnalysisService.generateAISummary(commitAnalysis, selectedModel);
            const enrichedClassification = this.classifyCommit(commitAnalysis, aiSummary || null);
            return {
                ...commitAnalysis,
                ...(aiSummary ? { aiSummary } : {}),
                ...enrichedClassification,
            };
        }
        catch {
            // Preserve commit analysis even when AI summarization fails.
            return {
                ...commitAnalysis,
                ...classification,
            };
        }
    }
    async enrichCommitAnalysesWithAI(commitAnalyses, concurrency = 3, options = {}) {
        const enriched = [];
        for (let i = 0; i < commitAnalyses.length; i += concurrency) {
            const chunk = commitAnalyses.slice(i, i + concurrency);
            const chunkResults = await Promise.all(chunk.map((commit) => this.enrichCommitWithAISummary(commit, options)));
            enriched.push(...chunkResults.filter(Boolean));
        }
        return enriched;
    }
    /**
     * 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
     * @param {Object} options - Additional options (dryRun, etc.)
     * @returns {Promise<string>} Complete changelog content
     */
    async generateChangelog(version = null, since = null, options = {}) {
        const includeWorkingDirectoryChanges = options.includeWorkingDirectoryChanges === true;
        const silent = options.silent === true || process.env.MCP_SERVER_MODE === 'true';
        if (!silent) {
            console.log(colors.processingMessage('๐Ÿค– Analyzing changes with AI...'));
        }
        // Get committed changes with optional filters
        const rawCommits = await this.gitService.getCommitsSince(since, {
            author: options.author,
            until: options.until,
        });
        // Drop commits that match the configured ignore pattern (e.g. WIP commits)
        // before any analysis happens so they never reach the changelog.
        const commits = this.filterIgnoredCommits(rawCommits);
        let workingDirAnalysis = null;
        if (includeWorkingDirectoryChanges && this.analysisEngine) {
            workingDirAnalysis = await this.analysisEngine.analyzeCurrentChanges();
        }
        if (commits.length === 0 && (!workingDirAnalysis || workingDirAnalysis.changes.length === 0)) {
            if (!silent) {
                console.log(colors.infoMessage('No commits or working directory changes found.'));
            }
            return;
        }
        const workingChangesCount = workingDirAnalysis ? workingDirAnalysis.changes.length : 0;
        if (!silent) {
            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);
        if (!silent) {
            console.log(colors.processingMessage(`Analyzing ${colors.number(commitHashes.length)} commits...`));
        }
        // Use batch processing for large commit sets
        let analyzedCommits;
        if (commitHashes.length > 20) {
            if (!silent) {
                console.log(colors.infoMessage('Using batch processing for large commit set...'));
            }
            analyzedCommits = await this.generateChangelogBatch(commitHashes, options);
        }
        else {
            analyzedCommits = await this.processCommitsSequentially(commitHashes, options);
        }
        if (analyzedCommits.length === 0) {
            if (!silent) {
                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.
        // `options` is threaded so template selection and attribution are honored.
        const changelog = await this.buildChangelog(analyzedCommits, insights, version, workingDirAnalysis, options);
        // Route through the unified writer when an output file or a non-default
        // format is requested so --format/--output/--dry-run behave consistently.
        if (options.outputFile || (options.format && options.format !== 'markdown')) {
            handleUnifiedOutput(changelog, {
                format: options.format || 'markdown',
                outputFile: options.outputFile,
                silent,
                dryRun: options.dryRun,
                version,
            });
        }
        return {
            changelog,
            insights,
            analyzedCommits,
            workingDirAnalysis,
        };
    }
    /**
     * Progress chatter must stay off when the caller asked for silence (library
     * embedding, VS Code output channel) or when running as an MCP server.
     */
    isSilent(options = {}) {
        return options.silent === true || process.env.MCP_SERVER_MODE === 'true';
    }
    async processCommitsSequentially(commitHashes, options = {}) {
        // Optimize by using concurrent processing with controlled concurrency
        const concurrency = 3; // Limit concurrent API calls to avoid rate limits
        const analyzedCommits = [];
        // Honor an explicit AI opt-out (--no-ai / includeAIAnalysis:false): skip the per-commit
        // AI round-trip and rely on rule-based classifyCommit, while still retaining the commit.
        const useAI = options.includeAIAnalysis !== false;
        const silent = this.isSilent(options);
        if (!silent) {
            console.log(colors.processingMessage(`Processing ${commitHashes.length} commits with ${concurrency} concurrent operations...`));
        }
        // Process in chunks to control concurrency
        for (let i = 0; i < commitHashes.length; i += concurrency) {
            const chunk = commitHashes.slice(i, i + concurrency);
            // Process chunk concurrently
            const chunkPromises = chunk.map(async (commitHash, index) => {
                const globalIndex = i + index;
                // Reading the commit from git is the only step that can legitimately drop
                // it. AI enrichment is optional, so its failure must never remove a commit
                // that git read successfully โ€” otherwise a provider outage silently
                // empties a small release while a large one (which goes through the batch
                // path) survives the same outage.
                let commitAnalysis;
                try {
                    commitAnalysis = await this.gitService.getCommitAnalysis(commitHash);
                }
                catch (error) {
                    console.warn(colors.warningMessage(`Failed to read commit ${commitHash.substring(0, 7)}: ${error.message}`));
                    return null;
                }
                if (!commitAnalysis) {
                    return null;
                }
                if (!silent) {
                    console.log(colors.processingMessage(`Processing commit ${colors.highlight(`${globalIndex + 1}/${commitHashes.length}`)}: ${colors.hash(commitHash.substring(0, 7))}`));
                }
                if (!useAI) {
                    return {
                        ...commitAnalysis,
                        ...this.classifyCommit(commitAnalysis),
                    };
                }
                try {
                    const aiSummary = await this.aiAnalysisService.generateAISummary(commitAnalysis, await this.aiAnalysisService.selectOptimalModel(commitAnalysis));
                    return {
                        ...commitAnalysis,
                        ...(aiSummary ? { aiSummary } : {}),
                        ...this.classifyCommit(commitAnalysis, aiSummary || null),
                    };
                }
                catch (error) {
                    console.warn(colors.warningMessage(`AI summary unavailable for ${commitHash.substring(0, 7)} (${error.message}); using rule-based analysis.`));
                    return {
                        ...commitAnalysis,
                        ...this.classifyCommit(commitAnalysis),
                    };
                }
            });
            // Wait for chunk to complete and add successful results
            const chunkResults = await Promise.all(chunkPromises);
            analyzedCommits.push(...chunkResults.filter((result) => result !== null));
            // Brief delay between chunks to be respectful to APIs
            if (i + concurrency < commitHashes.length) {
                await new Promise((resolve) => setTimeout(resolve, 100));
            }
        }
        return analyzedCommits;
    }
    async generateChangelogBatch(commitHashes, options = {}) {
        const batchSize = 10;
        const results = [];
        const silent = this.isSilent(options);
        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);
            if (!silent) {
                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.flatMap((result) => result.status === 'fulfilled' && result.value ? [result.value] : []);
            const enrichedResults = await this.enrichCommitAnalysesWithAI(successfulResults, 3, options);
            results.push(...enrichedResults);
            // 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: [],
            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) => {
                if (typeof file.category === 'string' && file.category.length > 0) {
                    insights.affectedAreas.push(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(new Set(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')
                    : 'Working directory changes detected',
            };
            // Add working directory files to affected areas
            workingDirAnalysis.changes.forEach((change) => {
                if (typeof change.category === 'string' && change.category.length > 0) {
                    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]) => Number(b) - Number(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, options = {}) {
        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();
        const includeAttribution = options.includeAttribution ?? this.configManager?.getIncludeAttribution?.() ?? true;
        // Group commits by conventional type up front so both the templated and the
        // default layouts share the same grouping/breaking-change computation.
        const groupedCommits = this.groupCommitsByType(analyzedCommits);
        const breakingCommits = analyzedCommits.filter((commit) => commit.breaking || (commit.breakingChanges && commit.breakingChanges.length > 0));
        // Optional template rendering: when a template is requested, build a
        // TemplateData object from the grouped changes and delegate to the shared
        // template engine instead of emitting the hardcoded layout below.
        if (options.template) {
            const templateData = this.buildTemplateData(groupedCommits, breakingCommits, version, timestamp, { commitUrl, issueUrl, issueRegex });
            let templated = renderChangelogTemplate(options.template, templateData);
            // Working directory changes are not part of the structured commit data, so
            // append them after the templated body to preserve the feature.
            if (workingDirAnalysis?.changes && workingDirAnalysis.changes.length > 0) {
                templated += `\n${await this.buildWorkingDirectorySection(workingDirAnalysis)}`;
            }
            return templated;
        }
        // Keep a Changelog standard header
        let changelog = '# Changelog\n\n';
        changelog += 'All notable changes to this project will be documented in this file.\n\n';
        changelog +=
            'The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\n';
        changelog +=
            'and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n';
        // Version header with commit range link
        const firstCommit = analyzedCommits[0];
        const lastCommit = analyzedCommits.at(-1);
        let versionHeader = `## [${version || 'Unreleased'}] - ${timestamp}`;
        if (firstCommit && lastCommit && commitRangeUrl) {
            const rangeLink = markdownCommitRangeLink(lastCommit.fullHash, firstCommit.fullHash, commitRangeUrl);
            versionHeader += ` (${rangeLink})`;
        }
        changelog += `${versionHeader}\n\n`;
        // Define preferred order for changelog sections
        const preferredOrder = [
            'feat',
            'fix',
            'perf',
            'refactor',
            'docs',
            'test',
            'build',
            'ci',
            'chore',
            'style',
            'revert',
            'other',
        ];
        const sortedTypes = preferredOrder
            .filter((type) => groupedCommits[type] && groupedCommits[type].length > 0)
            .concat(Object.keys(groupedCommits).filter((type) => !preferredOrder.includes(type) && groupedCommits[type].length > 0));
        // Generate sections for each commit type
        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 (if any breaking changes exist)
        if (breakingCommits.length > 0) {
            changelog += '### ๐Ÿšจ BREAKING CHANGES\n\n';
            for (const commit of breakingCommits) {
                changelog += this.buildBreakingChangeEntry(commit, commitUrl, issueUrl, issueRegex);
            }
            changelog += '\n';
        }
        // Append working directory changes if present
        if (workingDirAnalysis?.changes && workingDirAnalysis.changes.length > 0) {
            const workingDirSection = await this.buildWorkingDirectorySection(workingDirAnalysis);
            changelog += workingDirSection;
        }
        // Attribution footer (suppressed when attribution is disabled)
        if (includeAttribution) {
            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;
    }
    /**
     * Build a structured {@link TemplateData} object from grouped commits and
     * breaking changes for rendering via the shared template engine.
     * @param {Record<string, any[]>} groupedCommits - Commits grouped by type
     * @param {any[]} breakingCommits - Commits flagged as breaking
     * @param {string|null} version - Release version
     * @param {string} timestamp - ISO date (YYYY-MM-DD)
     * @param {Object} linkConfig - { commitUrl, issueUrl, issueRegex, includeAttribution }
     * @returns {TemplateData} Structured changelog data
     */
    buildTemplateData(groupedCommits, breakingCommits, version, timestamp, linkConfig = {}) {
        const { commitUrl, issueUrl, issueRegex } = linkConfig;
        const changes = {};
        for (const [type, commits] of Object.entries(groupedCommits)) {
            if (!(commits && commits.length > 0)) {
                continue;
            }
            changes[type] = commits.map((commit) => {
                let description = commit.aiSummary?.summary || commit.description || commit.subject || 'No description';
                description = description.replace(/^(feat|fix|refactor|docs|chore|test|style|perf|build|ci):\s*/i, '');
                if (commit.scope) {
                    description = `**${commit.scope}**: ${description}`;
                }
                if (issueUrl && issueRegex) {
                    description = processIssueReferences(description, issueUrl, issueRegex);
                }
                const hash = commit.fullHash || commit.hash;
                const entry = { description };
                if (hash) {
                    entry.hash = hash;
                    // The template engine appends the hash itself; it needs the raw target
                    // URL (built from the configured %commit% template), not a markdown link.
                    if (commitUrl) {
                        entry.commitUrl = commitUrl.replace('%commit%', hash);
                    }
                }
                if (commit.author || commit.authorName) {
                    entry.author = commit.author || commit.authorName;
                }
                const details = commit.aiSummary?.technicalDetails;
                if (details && details !== description) {
                    entry.details = details;
                }
                return entry;
            });
        }
        const breaking = breakingCommits.map((commit) => {
            let description = '';
            if (commit.aiSummary?.breakingChangeDescription) {
                description = commit.aiSummary.breakingChangeDescription;
            }
            else if (commit.breakingChanges && commit.breakingChanges.length > 0) {
                description = commit.breakingChanges[0];
            }
            else {
                description =
                    commit.aiSummary?.summary || commit.description || commit.subject || 'Breaking change';
            }
            description = description.replace(/^(feat|fix|refactor|docs|chore|test|style|perf|build|ci):\s*/i, '');
            const entry = { description };
            if (commit.aiSummary?.migrationNotes) {
                entry.migration = commit.aiSummary.migrationNotes;
            }
            return entry;
        });
        const totalCommits = Object.values(changes).reduce((sum, entries) => sum + entries.length, 0);
        return {
            title: 'Changelog',
            version: version || 'Unreleased',
            date: timestamp,
            changes,
            breaking,
            metadata: {
                totalCommits,
                includeCommitHash: Boolean(commitUrl),
                includeAuthor: false,
            },
            aiProvider: this.aiAnalysisService?.aiProvider?.name,
        };
    }
    buildCommitEntry(commit, commitUrl, issueUrl, issueRegex) {
        // Use AI-generated summary if available, fallback to commit message
        let title = commit.aiSummary?.summary || commit.description || commit.subject || 'No description';
        // Clean up title - remove conventional commit prefix if it exists
        title = title.replace(/^(feat|fix|refactor|docs|chore|test|style|perf|build|ci):\s*/i, '');
        // Process issue references in title
        if (issueUrl && issueRegex) {
            title = processIssueReferences(title, issueUrl, issueRegex);
        }
        // Build clean entry
        let entry = '- ';
        // Add scope if available
        if (commit.scope) {
            entry += `**${commit.scope}**: `;
        }
        // Add the main title/description
        entry += title;
        // Add commit link if available
        if (commitUrl && (commit.fullHash || commit.hash)) {
            const commitLink = markdownCommitLink(commit.fullHash || commit.hash, commitUrl);
            entry += ` (${commitLink})`;
        }
        // Add additional technical details if available and different from title
        const techDetails = commit.aiSummary?.technicalDetails;
        if (techDetails && techDetails !== title && techDetails.length > title.length) {
            entry += `\n  - ${techDetails}`;
        }
        // Add migration notes if available
        if (commit.aiSummary?.migrationNotes) {
            entry += `\n  - **Migration**: ${commit.aiSummary.migrationNotes}`;
        }
        // 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 += `\n  - 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 += `\n  - Closes: ${closesLinks}`;
        }
        return `${entry}\n`;
    }
    buildBreakingChangeEntry(commit, commitUrl, _issueUrl, _issueRegex) {
        let entry = '- ';
        // Add scope if available
        if (commit.scope) {
            entry += `**${commit.scope}**: `;
        }
        // Use AI-generated breaking change description if available
        let breakingDescription = '';
        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';
        }
        // Clean up breaking description
        breakingDescription = breakingDescription.replace(/^(feat|fix|refactor|docs|chore|test|style|perf|build|ci):\s*/i, '');
        entry += breakingDescription;
        // Add commit link if available
        if (commitUrl && (commit.fullHash || commit.hash)) {
            const commitLink = markdownCommitLink(commit.fullHash || commit.hash, commitUrl);
            entry += ` (${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 different from description
        const techDetails = commit.aiSummary?.technicalDetails;
        if (techDetails &&
            techDetails !== breakingDescription &&
            techDetails.length > breakingDescription.length) {
            entry += `\n  - ${techDetails}`;
        }
        return `${entry}\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,
                });
            }
        });
        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
     * @param {Object} options - Working-dir options: { analysisMode, includeAttribution, format, outputFile, dryRun, silent }
     * @returns {Promise<string>} Changelog content for working directory changes
     */
    async generateChangelogFromChanges(version = null, options = {}) {
        const silent = options.silent === true || process.env.MCP_SERVER_MODE === 'true';
        // Honor the caller's analysis mode, falling back to the configured default
        // and finally "standard" instead of the previous hardcoded "detailed".
        const analysisMode = options.analysisMode ?? this.configManager?.getDefaultAnalysisMode?.() ?? 'standard';
        const includeAttribution = options.includeAttribution ?? this.configManager?.getIncludeAttribution?.() ?? true;
        // Use the proper WorkspaceChangelogService with optimized single AI call approach
        const result = await this.generateWorkspaceChangelog(version, {
            analysisMode,
            includeAttribution,
        });
        if (!result) {
            if (!silent) {
                console.log(colors.infoMessage('No changes detected in working directory.'));
            }
            return null;
        }
        // Route working-dir output through the unified writer so --format/--output/
        // --dry-run behave the same as the core committed-changes path.
        if (options.outputFile || (options.format && options.format !== 'markdown')) {
            handleUnifiedOutput(result.changelog, {
                format: options.format || 'markdown',
                outputFile: options.outputFile,
                silent,
                dryRun: options.dryRun,
                version,
            });
        }
        return {
            changelog: result.changelog,
            analysis: result.summary,
            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.generateCommitStyleWorkingDirectoryEntries({
            analysisMode: 'detailed',
            workingDirAnalysis,
        });
        if (!workspaceResult?.entries || workspaceResult.entries.length === 0) {
            return '';
        }
        let section = '### ๐Ÿ”ง Unreleased Changes\n\n';
        // Add each entry in clean format
        workspaceResult.entries.forEach((entry) => {
            // Clean up the AI-generated entries to match our new format
            let cleanEntry = entry.replace(/^- \([\w]+\)\s*/, '- '); // Remove type prefixes like "(feature)"
            cleanEntry = cleanEntry.replace(/^- (.+?) - (.+)$/, '- $2'); // Extract just the description
            section += `${cleanEntry}\n`;
        });
        section += '\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?.hasAI) {
                const aiSummary = await this.aiAnalysisService.generateAISummary({
                    subject: `Working directory change: ${path}`,
                    files: [change],
                    diffStats: { files: 1, insertions: 0, deletions: 0 },
                });
                if (aiSummary?.technicalDetails) {
                    description = aiSummary.technicalDetails;
                    confidence = 85;
                }
                else {
                    description = this.analyzeDiffContentForDescription(change);
                    confidence = 75;
                }
            }
            else {
                description = this.analyzeDiffContentForDescription(change);
                confidence = 75;
            }
        }
        catch {
            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?.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?.[1]) {
            return null;
        }
        const content = previewMatch[1];
        const filename = path ? path.split('/').pop() : 'file';
        // Analyze based on file type and content
        if (path?.endsWith('.md')) {
            return this.analyzeMarkdownContent(content, filename);
        }
        if (path && (path.endsWith('.js') || path.endsWith('.ts'))) {
            return this.analyzeJavaScriptContent(content, filename);
        }
        if (path?.endsWith('.json')) {
            return this.analyzeJsonContent(content, filename);
        }
        if (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 = '';
        const 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
        const 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?.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)`;
            }
            if (uniqueChanges.length === 2) {
                return `${uniqueChanges.join(' and ')} (${fileCount} files)`;
            }
            // 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)`;
                }
                return `${primary} (${fileCount} files)`;
            }
            // 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)`;
            }
            // 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)`;
        }
        if (techList.length > 0) {
            return `${changeType} changes to ${techList.slice(0, 3).join(', ')} (${fileCount} files)`;
        }
        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?.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?.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 &&
                removedContent.includes('test') &&
                addedContent.includes('validate')) {
                functionalChanges.push('replaced test functions with validation system');
            }
            if ((path.includes('provider') || path.includes('ai-')) &&
                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?.includes('package.json')) {
            const configChanges = this.analyzePackageJsonChanges(addedContent, removedContent);
            if (configChanges.length > 0) {
                changes.push(...configChanges);
            }
        }
        // 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?.patterns && change.semanticChanges.patterns.length > 0) {
            confidence += 10;
        }
        if (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?.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 = pattern.exec(content);
            while (match !== null) {
                if (match[1] && !functions.includes(match[1])) {
                    functions.push(match[1]);
                }
                match = pattern.exec(content);
            }
        });
        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);
        }
        if (language === 'markdown') {
            return this.analyzeDeletedMarkdownFile(beforeContent, filePath);
        }
        if (language === 'json') {
            return this.analyzeDeletedJsonFile(beforeContent, filePath);
        }
        // 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`;
        }
        if (category === 'documentation') {
            return `Removed ${fileName} documentation file containing ${lines} lines`;
        }
        if (category === 'configuration') {
            return `Removed ${fileName} configuration file`;
        }
        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(', ')}`;
        }
        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`;
        }
        if (fileName.toLowerCase().includes('changelog')) {
            return `Removed ${fileName} changelog documentation`;
        }
        if (sections > 0) {
            return `Removed ${fileName} documentation containing ${sections} sections`;
        }
        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)`;
            }
            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';
    }
    // Missing methods expected by tests
    generateMarkdownChangelog(data) {
        return `# Changelog\n\n## Version ${data.version || 'Unreleased'}\n\n${data.content || 'No changes documented.'}`;
    }
    generateJSONChangelog(data) {
        return JSON.stringify({
            version: data.version || 'Unreleased',
            changes: data.changes || [],
            metadata: data.metadata || {},
        }, null, 2);
    }
    generatePlainTextChangelog(data) {
        return `Changelog - Version ${data.version || 'Unreleased'}\n\n${data.content || 'No changes documented.'}`;
    }
    // Workspace changelog methods (formerly in WorkspaceChangelogService)
    async generateWorkspaceChangelog(version = null, options = {}) {
        const result = await this.generateComprehensiveWorkspaceChangelog(options);
        if (!result) {
            return null;
        }
        let changelog = result.changelog;
        // Add version information if provided
        if (version) {
            changelog = changelog.replace(/# Working Directory Changelog/, `# Working Directory Changelog - Version ${version}`);
        }
        // Add context information for detailed modes
        if (options.analysisMode === 'detailed' || options.analysisMode === 'enterprise') {
            changelog += this.generateContextSection(result.summary);
        }
        return {
            ...result,
            changelog,
            version,
        };
    }
    async generateComprehensiveWorkspaceChangelog(options = {}) {
        try {
            // Get working directory changes as raw array
            const rawChanges = getWorkingDirectoryChanges(this.getRepositoryCwd());
            if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
                EnhancedConsole.info('No changes detected in working directory.');
                return null;
            }
            // Enhanced analysis of changes with diff content for AI analysis
            const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
            const changesSummary = summarizeFileChanges(enhancedChanges);
            // Use DiffProcessor for intelligent processing
            const analysisMode = options.analysisMode || 'standard';
            const diffProcessor = new DiffProcessor({
                analysisMode,
                enableFiltering: true,
                enablePatternDetection: true,
            });
            const processedResult = diffProcessor.processFiles(enhancedChanges);
            // Generate changelog content with processed files. `options` is threaded so
            // an explicit AI opt-out is honored here too โ€” previously the detailed and
            // enterprise modes called the provider regardless of includeAIAnalysis.
            const changelog = await this.generateWorkspaceChangelogContent(processedResult.processedFiles, changesSummary, processedResult, analysisMode, options);
            return {
                changelog,
                changes: enhancedChanges,
                processedFiles: processedResult.processedFiles,
                patterns: processedResult.patterns,
                summary: changesSummary,
                filesProcessed: processedResult.filesProcessed,
                filesSkipped: processedResult.filesSkipped,
            };
        }
        catch (error) {
            console.error(colors.errorMessage('Workspace changelog generation failed:'), error.message);
            throw error;
        }
    }
    async generateCommitStyleWorkingDirectoryEntries(options = {}) {
        // Use provided working directory analysis or get current changes
        let rawChanges;
        if (options.workingDirAnalysis?.changes) {
            rawChanges = options.workingDirAnalysis.changes;
        }
        else {
            rawChanges = getWorkingDirectoryChanges(this.getRepositoryCwd());
        }
        try {
            if (!(rawChanges && Array.isArray(rawChanges)) || rawChanges.length === 0) {
                return { entries: [] };
            }
            // Enhanced analysis of changes with diff content for AI analysis
            const enhancedChanges = await this.enhanceChangesWithDiff(rawChanges);
            const changesSummary = summarizeFileChanges(enhancedChanges);
            // Use DiffProcessor for intelligent diff processing
            const analysisMode = this.aiAnalysisService?.analysisMode || options.analysisMode || 'standard';
            const diffProcessor = new DiffProcessor({
                analysisMode,
                enableFiltering: true,
                enablePatternDetection: true,
            });
            const processedResult = diffProcessor.processFiles(enhancedChanges);
            const { processedFiles, patterns } = processedResult;
            // Build pattern summary if patterns were detected
            const patternSummary = Object.keys(patterns).length > 0
                ? `\n\n**BULK PATTERNS DETECTED:**\n${Object.values(patterns)
                    .map((p) => `- ${p.description}`)
                    .join('\n')}`
                : '';
            // Build files section with processed diffs
            const filesSection = processedFiles
                .map((file) => {
                if (file.isSummary) {
                    return `\n**[REMAINING FILES]:** ${file.diff}`;
                }
                const compressionInfo = file.compressionApplied
                    ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
                    : '';
                const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : '';
                return `\n**${file.filePath || file.path}** (${file.status})${compressionInfo}${patternInfo}:\n${file.diff}`;
            })
                .join('\n');
            // Build prompt for commit-style entries
            const prompt = `Generate working directory change entries in the SAME FORMAT as git commits:

**Analysis Mode**: ${analysisMode}
**Total Files**: ${changesSummary.totalFiles} (${processedResult.filesProcessed} analyzed, ${processedResult.filesSkipped} summarized)
**Categories**: ${Object.keys(changesSummary.categories).join(', ')}${patternSummary}

**PROCESSED FILES:**${filesSection}

STRICT FORMATTING REQUIREMENTS:
Generate working directory change entries based ONLY on visible diff content:
- (type) Detailed but focused description - Include key functional changes, method/function names, and important technical details without overwhelming verbosity

Where:
- type = feature, fix, refactor, docs, chore, etc. based on the actual changes
- Detailed description = specific functions/methods affected, key technical changes, and functional purpose
- Include exact method names, variable names, and technical specifics from the diffs

EXAMPLES of CORRECT DETAILED FORMAT:
- (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude Sonnet 5 and Llama 4 models with streaming capabilities.

- (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-sonnet-4-6' to 'claude-opus-5'. Added claude-opus-5 model entry with 1M context window and updated pricing tier.

- (fix) Updated configuration.manager.js - Added null check in getProviderConfig() method to prevent crashes when .env.local file is missing. Modified loadConfig() to gracefully handle missing environment files.

FORBIDDEN - DO NOT MAKE ASSUMPTIONS:
โŒ Do not mention "integration" unless you see actual integration code
โŒ Do not mention "provider selection logic" unless you see that specific code
โŒ Do not assume files work together unless explicitly shown in diffs

Generate one entry per file or logical change group. Only describe what you can literally see.`;
            // Make AI call
            const messages = [
                {
                    role: 'system',
                    content: 'You are an expert at analyzing code changes and generating detailed but focused commit-style changelog entries. You MUST only describe changes that are visible in the provided diff content. Include specific function/method names, key technical details, and the functional purpose of changes. Be precise and factual - only describe what you can literally see in the diffs. Provide enough detail to understand what changed technically, but avoid overwhelming verbosity.',
                },
                {
                    role: 'user',
                    content: prompt,
                },
            ];
            // Set token limits based on analysis mode and number of changes
            let maxTokens = 1200; // Default
            if (analysisMode === 'enterprise') {
                maxTokens = 3000;
            }
            else if (analysisMode === 'detailed') {
                maxTokens = 2500;
            }
            // Increase token limit for large numbers of working directory changes
            if (enhancedChanges.length > 50) {
                maxTokens = Math.min(maxTokens + 1500, 6000);
            }
            const aiOptions = {
                max_tokens: maxTokens,
                temperature: 0.3,
            };
            const response = await this.aiAnalysisService.generateCompletion(messages, aiOptions);
            const content = response.content || response.text;
            // Check if content is valid before processing
            if (!content || typeof content !== 'string') {
                console.warn(colors.warningMessage('โš ๏ธ  AI response was empty or invalid'));
                console.warn(colors.infoMessage('๐Ÿ’ก Using basic file change detection instead'));
                // Fallback to basic entries from the changes we were given
                const fallbackChanges = rawChanges || getWorkingDirectoryChanges(this.getRepositoryCwd());
                const basicEntries = fallbackChanges.map((change) => {
                    const filePath = change.filePath || change.path || 'unknown file';
                    const status = change.status || 'M';
                    const changeType = status === 'M'
                        ? 'update'
                        : status === 'A'
                            ? 'feature'
                            : status === 'D'
                                ? 'remove'
                                : 'chore';
                    const changeDesc = status === 'M'
                        ? 'updated'
                        : status === 'A'
                            ? 'added'
                            : status === 'D'
                                ? 'deleted'
                                : 'changed';
                    return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`;
                });
                return { entries: basicEntries };
            }
            // Parse entries from response
            const entries = content
                .split('\n')
                .filter((line) => {
                const trimmed = line.trim();
                // Accept lines starting with '- (' or directly with '(' for changelog entries
                return trimmed.startsWith('- (') || trimmed.startsWith('(');
            })
                .map((line) => {
                const trimmed = line.trim();
                // Ensure all entries start with '- ' for consistent formatting
                return trimmed.startsWith('- ') ? trimmed : `- ${trimmed}`;
            });
            return {
                entries,
                changes: enhancedChanges,
                summary: changesSummary,
            };
        }
        catch (error) {
            // Provide specific guidance based on error type
            if (error.message.includes('fetch failed') || error.message.includes('connection')) {
                console.warn(colors.warningMessage('โš ๏ธ  AI provider connection failed'));
                console.warn(colors.infoMessage('๐Ÿ’ก Check your internet connection and provider configuration'));
            }
            else if (error.message.includes('API key') || error.message.includes('401')) {
                console.warn(colors.warningMessage('โš ๏ธ  Authentication failed'));
                console.warn(colors.infoMessage('๐Ÿ’ก Run `ai-changelog init` to configure your API key'));
            }
            else {
                console.warn(colors.warningMessage(`โš ๏ธ  AI analysis failed: ${error.message}`));
                console.warn(colors.infoMessage('๐Ÿ’ก Using basic file change detection instead'));
            }
            // Return basic entries from the changes we were given instead of getting fresh ones
            const fallbackChanges = rawChanges || getWorkingDirectoryChanges(this.getRepositoryCwd());
            const basicEntries = fallbackChanges.map((change) => {
                const filePath = change.filePath || change.path || 'unknown file';
                const status = change.status || 'M';
                const changeType = status === 'M'
                    ? 'update'
                    : status === 'A'
                        ? 'feature'
                        : status === 'D'
                            ? 'remove'
                            : 'chore';
                const changeDesc = status === 'M'
                    ? 'updated'
                    : status === 'A'
                        ? 'added'
                        : status === 'D'
                            ? 'deleted'
                            : 'changed';
                return `- (${changeType}) Modified ${filePath} - File ${changeDesc} (pattern-based analysis)`;
            });
            return { entries: basicEntries };
        }
    }
    async enhanceChangesWithDiff(changes) {
        const enhancedChanges = [];
        for (const change of changes) {
            const enhancedChange = {
                ...change,
                category: categorizeFile(change.path || change.filePath),
                language: detectLanguage(change.path || change.filePath),
                importance: assessFileImportance(change.path || change.filePath, change.status),
                enhanced: true,
            };
            // Get diff content if git service is available
            if (this.gitService) {
                try {
                    const diffAnalysis = await this.gitService.analyzeWorkingDirectoryFileChange(change.status, change.path || change.filePath);
                    if (diffAnalysis) {
                        enhancedChange.diff = diffAnalysis.diff;
                        enhancedChange.beforeContent = diffAnalysis.beforeContent;
                        enhancedChange.afterContent = diffAnalysis.afterContent;
                        enhancedChange.semanticChanges = diffAnalysis.semanticChanges;
                        enhancedChange.functionalImpact = diffAnalysis.functionalImpact;
                        enhancedChange.complexity = diffAnalysis.complexity;
                    }
                }
                catch (error) {
                    console.warn(`Failed to get diff for ${change.path || change.filePath}:`, error.message);
                }
            }
            enhancedChanges.push(enhancedChange);
        }
        return enhancedChanges;
    }
    async generateWorkspaceChangelogContent(changes, summary, _context, analysisMode, options = {}) {
        // Standard mode is rule-based by design; the richer modes use AI only when the
        // caller has not opted out. `includeAIAnalysis:false` must win over the mode.
        const useAI = options.includeAIAnalysis !== false &&
            (analysisMode === 'detailed' || analysisMode === 'enterprise');
        if (useAI) {
            return await this.generateAIChangelogContentFromChanges(changes, summary, analysisMode);
        }
        return this.generateBasicChangelogContentFromChanges(changes, summary);
    }
    async generateAIChangelogContentFromChanges(changes, changesSummary, analysisMode = 'standard') {
        if (!this.aiAnalysisService.hasAI) {
            console.log(colors.infoMessage('AI not available, using rule-based analysis...'));
            return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
        }
        try {
            // Use DiffProcessor for intelligent diff processing
            const diffProcessor = new DiffProcessor({
                analysisMode,
                enableFiltering: true,
                enablePatternDetection: true,
            });
            const processedResult = diffProcessor.processFiles(changes);
            const { processedFiles, patterns } = processedResult;
            // Build pattern summary if patterns were detected
            const patternSummary = Object.keys(patterns).length > 0
                ? `\n\n**BULK PATTERNS DETECTED:**\n${Object.values(patterns)
                    .map((p) => `- ${p.description}`)
                    .join('\n')}`
                : '';
            // Build files section with processed diffs
            const filesSection = processedFiles
                .map((file) => {
                if (file.isSummary) {
                    return `\n**[REMAINING FILES]:** ${file.diff}`;
                }
                const compressionInfo = file.compressionApplied
                    ? ` [compressed from ${file.originalSize || 'unknown'} chars]`
                    : '';
                const patternInfo = file.bulkPattern ? ` [${file.bulkPattern}]` : '';
                return `\n**${file.filePath || file.path}** (${file.status})${compressionInfo}${patternInfo}:\n${file.diff}`;
            })
                .join('\n');
            // Build comprehensive prompt with processed changes
            const prompt = `Generate a comprehensive AI changelog for the following working directory changes:

**Analysis Mode**: ${analysisMode}
**Total Files**: ${changesSummary.totalFiles} (${processedResult.filesProcessed} analyzed, ${processedResult.filesSkipped} summarized)
**Categories**: ${Object.keys(changesSummary.categories).join(', ')}${patternSummary}

**PROCESSED FILES:**${filesSection}

CRITICAL INSTRUCTIONS FOR ANALYSIS:
1. **ONLY DESCRIBE CHANGES VISIBLE IN THE DIFF CONTENT** - Do not invent or assume changes
2. **BE FACTUAL AND PRECISE** - Only mention specific lines, functions, imports that you can see
3. **NO ASSUMPTIONS OR SPECULATION** - If you can't see it in the diff, don't mention it
4. **STICK TO OBSERVABLE FACTS** - Describe what was added, removed, or modified line by line
5. **DO NOT MAKE UP INTEGRATION DETAILS** - Don't assume files work together unless explicitly shown

STRICT FORMATTING REQUIREMENTS:
Generate working directory change entries based ONLY on visible diff content:
- (type) Detailed but focused description - Include key functional changes, method/function names, and important technical details without overwhelming verbosity

EXAMPLES of CORRECT DETAILED FORMAT:
โœ… (feature) Created new bedrock.js file - Added BedrockProvider class with generateCompletion(), initializeClient(), and getAvailableModels() methods. Imported AWS SDK BedrockRuntimeClient and added support for Claude Sonnet 5 and Llama 4 models with streaming capabilities.

โœ… (refactor) Updated model list in anthropic.js - Changed getDefaultModel() return value from 'claude-sonnet-4-6' to 'claude-opus-5'. Added claude-opus-5 model entry with 1M context and updated pricing tier.

โœ… (fix) Updated configuration.manager.js - Added null check in getProviderConfig() method to prevent crashes when .env.local file is missing. Modified loadConfig() to gracefully handle missing environment files.

EXAMPLES of FORBIDDEN ASSUMPTIONS:
โŒ "Updated other providers to recognize bedrock" (not visible in diff)
โŒ "Refactored provider selection logic" (not shown in actual changes)
โŒ "Improved integration across the system" (speculation)
โŒ "Enhanced error handling throughout" (assumption)

ONLY describe what you can literally see in the diff content. Do not invent connections or integrations.`;
            // Make AI call with all the context
            const messages = [
                {
                    role: 'system',
                    content: 'You are an expert at analyzing code changes and generating detailed but focused changelog entries. You MUST only describe changes that are visible in the provided diff content. Include specific function/method names, key technical details, and the functional purpose of changes. Be precise and factual - only describe what you can literally see in the diffs. Provide enough detail to understand what changed technically, but avoid overwhelming verbosity.',
                },
                {
                    role: 'user',
                    content: prompt,
                },
            ];
            const options = {
                max_tokens: analysisMode === 'enterprise' ? 2500 : analysisMode === 'detailed' ? 2000 : 1200,
                temperature: 0.2,
            };
            const response = await this.aiAnalysisService.generateCompletion(messages, options);
            let changelog = response.content || response.text;
            // Check if changelog content is valid
            if (!changelog || typeof changelog !== 'string') {
                console.warn(colors.warningMessage('โš ๏ธ  AI response was empty or invalid'));
                console.warn(colors.infoMessage('๐Ÿ’ก Using basic file change detection instead'));
                // Generate basic changelog from file changes
                const timestamp = new Date().toISOString().split('T')[0];
                const fallbackChanges = changes;
                const basicEntries = fallbackChanges.map((change) => {
                    const filePath = change.filePath || change.path || 'unknown file';
                    const status = change.status || 'M';
                    const changeType = status === 'M'
                        ? 'Modified'
                        : status === 'A'
                            ? 'Added'
                            : status === 'D'
                                ? 'Deleted'
                                : 'Changed';
                    return `- ${changeType} ${filePath}`;
                });
                changelog = `# Working Directory Changelog - ${timestamp}\n\n## Changes\n\n${basicEntries.join('\n')}`;
            }
            // Add metadata
            const timestamp = new Date().toISOString().split('T')[0];
            // Ensure proper changelog format with Keep a Changelog header
            if (!changelog.includes('# ')) {
                changelog = `# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),\nand this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased] - ${timestamp}\n\n${changelog}`;
            }
            // Add generation metadata
            changelog += `\n\n---\n\n*Generated from ${changesSummary.totalFiles} working directory changes*\n`;
            return changelog;
        }
        catch (error) {
            // Specific error guidance for AI failures
            if (error.message.includes('fetch failed') || error.message.includes('ECONNREFUSED')) {
                console.warn(colors.warningMessage('โš ๏ธ  Cannot connect to AI provider'));
                console.warn(colors.infoMessage('๐Ÿ’ก Check internet connection and provider service status'));
            }
            else if (error.message.includes('API key') || error.message.includes('401')) {
                console.warn(colors.warningMessage('โš ๏ธ  API authentication failed'));
                console.warn(colors.infoMessage('๐Ÿ’ก Run: ai-changelog init'));
            }
            else if (error.message.includes('rate limit')) {
                console.warn(colors.warningMessage('โš ๏ธ  Rate limit exceeded'));
                console.warn(colors.infoMessage('๐Ÿ’ก Wait a moment before retrying'));
            }
            else {
                console.warn(colors.warningMessage(`โš ๏ธ  AI analysis failed: ${error.message}`));
            }
            console.warn(colors.infoMessage('๐Ÿ”„ Falling back to pattern-based analysis'));
            return this.generateBasicChangelogContentFromChanges(changes, changesSummary);
        }
    }
    generateBasicChangelogContentFromChanges(changes, changesSummary) {
        const timestamp = new Date().toISOString().split('T')[0];
        let changelog = `# Working Directory Changes - ${timestamp}\n\n`;
        // Basic summary
        changelog += '## Summary\n';
        changelog += `${changes.length} files modified across ${Object.keys(changesSummary.categories).length} categories.\n\n`;
        // Changes by category
        changelog += this.buildChangesByCategory(changes, changesSummary);
        // Basic recommendations
        changelog += '## Recommendations\n';
        changelog += '- Review changes before committing\n';
        changelog += '- Consider adding tests for new functionality\n';
        changelog += '- Update documentation if needed\n\n';
        return changelog;
    }
    buildChangesByCategory(_changes, changesSummary) {
        let content = '## Changes by Category\n\n';
        Object.entries(changesSummary.categories).forEach(([category, files]) => {
            const categoryIcon = this.getCategoryIcon(category);
            const categoryFiles = Array.isArray(files) ? files : [];
            content += `### ${categoryIcon} ${category.charAt(0).toUpperCase() + category.slice(1)} (${categoryFiles.length} files)\n\n`;
            categoryFiles.forEach((file) => {
                const statusIcon = this.getStatusIcon(file.status);
                content += `- ${statusIcon} ${file.path}\n`;
            });
            content += '\n';
        });
        return content;
    }
    getCategoryIcon(category) {
        const icons = {
            source: '๐Ÿ’ป',
            tests: '๐Ÿงช',
            documentation: '๐Ÿ“š',
            configuration: 'โš™๏ธ',
            frontend: '๐ŸŽจ',
            assets: '๐Ÿ–ผ๏ธ',
            build: '๐Ÿ”ง',
            other: '๐Ÿ“„',
        };
        return icons[category] || '๐Ÿ“„';
    }
    getStatusIcon(status) {
        const icons = {
            A: 'โž•', // Added
            M: 'โœ๏ธ', // Modified
            D: 'โŒ', // Deleted
            R: '๐Ÿ“', // Renamed
            C: '๐Ÿ“‹', // Copied
        };
        return icons[status] || '๐Ÿ“„';
    }
    generateContextSection(context) {
        if (!context) {
            return '';
        }
        let section = '## Context Analysis\n\n';
        // Handle totalFiles from either structure
        if (context.totalFiles !== undefined) {
            section += `- **Total Files:** ${context.totalFiles}\n`;
        }
        // Handle categories from summarizeFileChanges structure
        if (context.categories && typeof context.categories === 'object') {
            const categoryNames = Object.keys(context.categories);
            if (categoryNames.length > 0) {
                section += `- **Affected Areas:** ${categoryNames.join(', ')}\n`;
            }
        }
        // Handle stats if available
        if (context.stats) {
            const statParts = [];
            if (context.stats.added > 0)
                statParts.push(`${context.stats.added} added`);
            if (context.stats.modified > 0)
                statParts.push(`${context.stats.modified} modified`);
            if (context.stats.deleted > 0)
                statParts.push(`${context.stats.deleted} deleted`);
            if (statParts.length > 0) {
                section += `- **Changes:** ${statParts.join(', ')}\n`;
            }
        }
        // Handle languages if available
        if (context.languages && Array.isArray(context.languages) && context.languages.length > 0) {
            section += `- **Languages:** ${context.languages.join(', ')}\n`;
        }
        if (context.primaryCategory) {
            section += `- **Primary Category:** ${context.primaryCategory}\n`;
        }
        if (context.riskLevel) {
            section += `- **Risk Level:** ${context.riskLevel}\n`;
        }
        if (context.complexity) {
            section += `- **Complexity:** ${context.complexity}\n`;
        }
        section += '\n';
        if (context.recommendations &&
            Array.isArray(context.recommendations) &&
            context.recommendations.length > 0) {
            section += '### Recommendations\n\n';
            context.recommendations.forEach((rec) => {
                section += `- ${rec}\n`;
            });
            section += '\n';
        }
        return section;
    }
}