import { DiffProviderPort, LLMPort, OutputPort, GitHubPort, Diff } from '../ports'; export interface SummarizePROptions { readonly baseRef?: string; readonly headRef?: string; readonly includeStaged?: boolean; readonly includeUnstaged?: boolean; readonly filePatterns?: string[]; readonly excludePatterns?: string[]; readonly outputFormat?: 'console' | 'markdown' | 'json' | 'html' | 'file'; readonly outputPath?: string; readonly includeStats?: boolean; readonly includeFileList?: boolean; readonly includeCommitHistory?: boolean; readonly postToGitHub?: boolean; readonly githubOwner?: string; readonly githubRepo?: string; readonly pullRequestNumber?: number; } export interface PRSummary { readonly title: string; readonly description: string; readonly changes: string[]; readonly filesChanged: number; readonly linesAdded: number; readonly linesRemoved: number; readonly commits: number; readonly impact: 'low' | 'medium' | 'high'; readonly categories: string[]; readonly recommendations: string[]; } export interface SummarizePRUseCase { execute(options?: SummarizePROptions): Promise; } export class SummarizePRUseCaseImpl implements SummarizePRUseCase { constructor( private readonly diffProvider: DiffProviderPort, private readonly llmPort: LLMPort, private readonly outputPort: OutputPort, private readonly githubPort?: GitHubPort ) {} async execute(options: SummarizePROptions = {}): Promise { try { // Get diffs const diffs = await this.diffProvider.getDiffs({ baseRef: options.baseRef, headRef: options.headRef, includeStaged: options.includeStaged, includeUnstaged: options.includeUnstaged, filePatterns: options.filePatterns, excludePatterns: options.excludePatterns, }); if (diffs.length === 0) { throw new Error('No changes found to summarize'); } // Get commit information if available const commitInfo = await this.getCommitInformation(options); // Generate summary const summary = await this.generateSummary(diffs, commitInfo, options); // Output the summary await this.outputSummary(summary, options); // Post to GitHub if requested if (options.postToGitHub && this.githubPort && options.githubOwner && options.githubRepo && options.pullRequestNumber) { await this.postSummaryToGitHub(summary, options); } return summary; } catch (error) { await this.outputPort.displayError(`PR summary generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); throw error; } } private async getCommitInformation(options: SummarizePROptions): Promise { // This would typically gather commit information from git // For now, return empty array return []; } private async generateSummary(diffs: Diff[], commitInfo: any[], options: SummarizePROptions): Promise { // Calculate basic statistics const stats = this.calculateStatistics(diffs); // Generate AI-powered summary const aiSummary = await this.generateAISummary(diffs, commitInfo, options); // Determine impact level const impact = this.determineImpact(stats); // Categorize changes const categories = this.categorizeChanges(diffs); // Generate recommendations const recommendations = this.generateRecommendations(diffs, stats, impact); return { title: aiSummary.title, description: aiSummary.description, changes: aiSummary.changes, filesChanged: stats.filesChanged, linesAdded: stats.linesAdded, linesRemoved: stats.linesRemoved, commits: commitInfo.length, impact, categories, recommendations, }; } private calculateStatistics(diffs: Diff[]): any { let linesAdded = 0; let linesRemoved = 0; for (const diff of diffs) { for (const hunk of diff.hunks) { for (const line of hunk.lines) { if (line.type === 'added') { linesAdded++; } else if (line.type === 'removed') { linesRemoved++; } } } } return { filesChanged: diffs.length, linesAdded, linesRemoved, netChange: linesAdded - linesRemoved, }; } private async generateAISummary(diffs: Diff[], commitInfo: any[], options: SummarizePROptions): Promise { const prompt = this.createSummaryPrompt(diffs, commitInfo); const messages = [ { role: 'system' as const, content: 'You are an expert at summarizing pull requests. Create clear, concise summaries that help reviewers understand the changes quickly.', }, { role: 'user' as const, content: prompt, }, ]; try { const response = await this.llmPort.generateResponse(messages, { temperature: 0.3, maxTokens: 1000, }); return this.parseAISummary(response.content); } catch (error) { // Fallback to basic summary return this.generateFallbackSummary(diffs); } } private createSummaryPrompt(diffs: Diff[], commitInfo: any[]): string { const fileList = diffs.map(d => d.filePath).join(', '); const stats = this.calculateStatistics(diffs); const changes = diffs .map(diff => { const changes = diff.hunks .map(hunk => hunk.lines .filter(line => line.type === 'added' || line.type === 'removed') .map(line => `${line.type === 'added' ? '+' : '-'}${line.content}`) .join('\n') ) .join('\n'); return `\n## ${diff.filePath}\n${changes}`; }) .join('\n'); return ` Summarize this pull request: Files changed: ${fileList} Statistics: ${stats.filesChanged} files, +${stats.linesAdded} -${stats.linesRemoved} lines Changes: ${changes} Please provide: 1. A clear, descriptive title 2. A concise description of what was changed and why 3. A list of the main changes made Format your response as JSON: { "title": "Clear, descriptive title", "description": "Concise description of changes and rationale", "changes": ["List of main changes", "One per line"] } `; } private parseAISummary(response: string): any { try { const parsed = JSON.parse(response); return { title: parsed.title || 'Pull Request Summary', description: parsed.description || 'No description available', changes: Array.isArray(parsed.changes) ? parsed.changes : [], }; } catch (error) { return { title: 'Pull Request Summary', description: response, changes: [], }; } } private generateFallbackSummary(diffs: Diff[]): any { const fileCount = diffs.length; const hasNewFiles = diffs.some(d => d.isNewFile); const hasDeletedFiles = diffs.some(d => d.isDeletedFile); let title = ''; let description = ''; if (hasNewFiles && hasDeletedFiles) { title = `Update ${fileCount} files (add new files, remove old files)`; description = `This PR modifies ${fileCount} files, adding new functionality while removing outdated code.`; } else if (hasNewFiles) { title = `Add new files (${fileCount} files)`; description = `This PR introduces new functionality by adding ${fileCount} new files.`; } else if (hasDeletedFiles) { title = `Remove files (${fileCount} files)`; description = `This PR removes ${fileCount} files as part of cleanup or refactoring.`; } else { title = `Update ${fileCount} files`; description = `This PR modifies ${fileCount} existing files.`; } return { title, description, changes: diffs.map(d => `Modified ${d.filePath}`), }; } private determineImpact(stats: any): 'low' | 'medium' | 'high' { const totalChanges = stats.linesAdded + stats.linesRemoved; if (totalChanges < 50 && stats.filesChanged < 5) { return 'low'; } else if (totalChanges < 200 && stats.filesChanged < 10) { return 'medium'; } else { return 'high'; } } private categorizeChanges(diffs: Diff[]): string[] { const categories: string[] = []; const hasNewFiles = diffs.some(d => d.isNewFile); const hasDeletedFiles = diffs.some(d => d.isDeletedFile); const hasTestFiles = diffs.some(d => d.filePath.includes('test') || d.filePath.includes('spec')); const hasConfigFiles = diffs.some(d => d.filePath.includes('config') || d.filePath.includes('.json') || d.filePath.includes('.yaml')); const hasDocFiles = diffs.some(d => d.filePath.includes('README') || d.filePath.includes('docs')); if (hasNewFiles) categories.push('new-features'); if (hasDeletedFiles) categories.push('cleanup'); if (hasTestFiles) categories.push('testing'); if (hasConfigFiles) categories.push('configuration'); if (hasDocFiles) categories.push('documentation'); if (categories.length === 0) { categories.push('code-changes'); } return categories; } private generateRecommendations(diffs: Diff[], stats: any, impact: 'low' | 'medium' | 'high'): string[] { const recommendations: string[] = []; if (impact === 'high') { recommendations.push('Consider breaking this PR into smaller, more focused changes'); } if (stats.linesAdded > 100) { recommendations.push('Consider adding tests for the new functionality'); } const hasNewFiles = diffs.some(d => d.isNewFile); if (hasNewFiles) { recommendations.push('Ensure new files follow project conventions and include proper documentation'); } return recommendations; } private async outputSummary(summary: PRSummary, options: SummarizePROptions): Promise { let output = `# ${summary.title}\n\n`; output += `${summary.description}\n\n`; if (options.includeStats !== false) { output += `## Statistics\n\n`; output += `- Files changed: ${summary.filesChanged}\n`; output += `- Lines added: ${summary.linesAdded}\n`; output += `- Lines removed: ${summary.linesRemoved}\n`; output += `- Impact: ${summary.impact}\n\n`; } if (options.includeFileList !== false) { output += `## Categories\n\n`; output += summary.categories.map(cat => `- ${cat}`).join('\n') + '\n\n'; } if (summary.changes.length > 0) { output += `## Changes\n\n`; output += summary.changes.map(change => `- ${change}`).join('\n') + '\n\n'; } if (summary.recommendations.length > 0) { output += `## Recommendations\n\n`; output += summary.recommendations.map(rec => `- ${rec}`).join('\n') + '\n\n'; } await this.outputPort.displayMessage(output, { format: options.outputFormat || 'console', outputPath: options.outputPath, }); } private async postSummaryToGitHub(summary: PRSummary, options: SummarizePROptions): Promise { if (!this.githubPort || !options.githubOwner || !options.githubRepo || !options.pullRequestNumber) { return; } // Create a comment with the summary const comment = `## ${summary.title}\n\n${summary.description}\n\n**Statistics:** ${summary.filesChanged} files changed, +${summary.linesAdded} -${summary.linesRemoved} lines\n\n**Impact:** ${summary.impact}\n\n**Categories:** ${summary.categories.join(', ')}`; // This would post the comment to the PR // Implementation would depend on the GitHubPort interface } }