UNPKG

blackflag_weekly

Version:

Generate weekly progress slides from git history using OpenAI Agents and Slidev

512 lines (437 loc) 15.2 kB
import { tool } from '@openai/agents'; import { z } from 'zod'; import simpleGit from 'simple-git'; import fs from 'fs/promises'; import path from 'path'; // Initialize git instance const git = simpleGit(); // Tool to analyze git diff for a specific commit export const analyzeGitDiffTool = tool({ name: 'analyze_git_diff', description: 'Analyze the actual code changes in a git commit by examining the diff', parameters: z.object({ commitHash: z.string(), maxLines: z.number().optional().default(500) }), async execute({ commitHash, maxLines }) { try { // Get the detailed diff for this commit const diffResult = await git.show([ commitHash, '--format=fuller', '--stat', '--name-status', `--max-count=${maxLines}` ]); // Parse the diff to understand the changes const analysis = parseDiffContent(diffResult, commitHash); return JSON.stringify(analysis, null, 2); } catch (error) { return `Error analyzing commit ${commitHash}: ${error.message}`; } } }); // Tool to detect architectural patterns in code changes export const detectArchitecturalPatternsTool = tool({ name: 'detect_architectural_patterns', description: 'Detect architectural patterns and refactoring in code changes', parameters: z.object({ fileChanges: z.array(z.object({ file: z.string(), status: z.string(), insertions: z.number().nullable(), deletions: z.number().nullable() })), commitMessage: z.string() }), async execute({ fileChanges, commitMessage }) { const patterns = analyzeArchitecturalPatterns(fileChanges, commitMessage); return JSON.stringify(patterns, null, 2); } }); // Tool to assess business impact of changes export const assessBusinessImpactTool = tool({ name: 'assess_business_impact', description: 'Assess the business impact of code changes based on affected files and systems', parameters: z.object({ fileChanges: z.array(z.object({ file: z.string(), status: z.string() })), commitHash: z.string() }), async execute({ fileChanges, commitHash }) { const impact = assessBusinessImpact(fileChanges); return JSON.stringify(impact, null, 2); } }); // Tool to analyze code complexity changes export const analyzeComplexityChangesTool = tool({ name: 'analyze_complexity_changes', description: 'Analyze changes in code complexity and technical debt', parameters: z.object({ commitHash: z.string(), fileChanges: z.array(z.object({ file: z.string(), insertions: z.number().nullable(), deletions: z.number().nullable() })) }), async execute({ commitHash, fileChanges }) { const complexity = analyzeComplexityChanges(fileChanges, commitHash); return JSON.stringify(complexity, null, 2); } }); // Helper function to parse git diff content function parseDiffContent(diffOutput, commitHash) { const lines = diffOutput.split('\n'); const analysis = { commitHash, fileAnalysis: [], overallImpact: { architecturalChanges: false, breakingChanges: false, performanceImpact: 'unknown', securityImpact: 'none', testingChanges: false }, codePatterns: [] }; let currentFile = null; let additions = 0; let deletions = 0; for (const line of lines) { // Parse file headers if (line.startsWith('diff --git')) { const match = line.match(/diff --git a\/(.+) b\/(.+)/); if (match) { currentFile = { file: match[1], type: getFileCategory(match[1]), changes: { additions: 0, deletions: 0, modifications: [] }, impact: 'low' }; analysis.fileAnalysis.push(currentFile); } } // Parse additions/deletions if (line.startsWith('+') && !line.startsWith('+++') && currentFile) { currentFile.changes.additions++; additions++; // Analyze the actual code being added const codeAnalysis = analyzeCodeLine(line.substring(1)); if (codeAnalysis.significance > 0) { currentFile.changes.modifications.push({ type: 'addition', content: line.substring(1).trim(), significance: codeAnalysis.significance, category: codeAnalysis.category }); } } if (line.startsWith('-') && !line.startsWith('---') && currentFile) { currentFile.changes.deletions++; deletions++; // Analyze the actual code being removed const codeAnalysis = analyzeCodeLine(line.substring(1)); if (codeAnalysis.significance > 0) { currentFile.changes.modifications.push({ type: 'deletion', content: line.substring(1).trim(), significance: codeAnalysis.significance, category: codeAnalysis.category }); } } } // Determine overall impact based on actual changes analysis.overallImpact = determineOverallImpact(analysis.fileAnalysis); return analysis; } // Analyze architectural patterns in file changes function analyzeArchitecturalPatterns(fileChanges, commitMessage) { const patterns = { refactoring: false, newFeature: false, bugfix: false, performance: false, security: false, testing: false, documentation: false, configuration: false, architectural: false, evidence: [] }; // Analyze by file types and changes const fileTypes = fileChanges.reduce((acc, change) => { const type = getFileCategory(change.file); acc[type] = (acc[type] || 0) + 1; return acc; }, {}); // Detect refactoring patterns if (hasRefactoringPattern(fileChanges)) { patterns.refactoring = true; patterns.evidence.push('Multiple files modified with balanced additions/deletions suggesting code restructuring'); } // Detect architectural changes if (hasArchitecturalPattern(fileChanges)) { patterns.architectural = true; patterns.evidence.push('Changes to core system files, configuration, or multiple modules'); } // Detect new features if (hasNewFeaturePattern(fileChanges)) { patterns.newFeature = true; patterns.evidence.push('New files created or significant additions to existing functionality'); } // Detect performance work if (hasPerformancePattern(fileChanges, commitMessage)) { patterns.performance = true; patterns.evidence.push('Changes to algorithms, database queries, or optimization-related files'); } // Detect security improvements if (hasSecurityPattern(fileChanges, commitMessage)) { patterns.security = true; patterns.evidence.push('Changes to authentication, authorization, validation, or security-related files'); } return patterns; } // Assess business impact based on file changes function assessBusinessImpact(fileChanges) { const impact = { level: 'low', categories: [], userFacing: false, systemCritical: false, dataImpact: false, apiChanges: false, description: '' }; const criticalPaths = [ 'api/', 'routes/', 'controllers/', 'models/', 'database/', 'migrations/', 'auth/', 'payment/', 'user/', 'admin/', 'security/', 'config/' ]; const userFacingPaths = [ 'ui/', 'components/', 'pages/', 'views/', 'frontend/', 'client/', 'public/', 'assets/' ]; for (const change of fileChanges) { const filePath = change.file.toLowerCase(); // Check for critical system files if (criticalPaths.some(path => filePath.includes(path))) { impact.systemCritical = true; impact.level = 'high'; impact.categories.push('system-critical'); } // Check for user-facing changes if (userFacingPaths.some(path => filePath.includes(path))) { impact.userFacing = true; impact.categories.push('user-experience'); } // Check for API changes if (filePath.includes('api') || filePath.includes('endpoint') || filePath.includes('route')) { impact.apiChanges = true; impact.categories.push('api-changes'); } // Check for data impact if (filePath.includes('model') || filePath.includes('schema') || filePath.includes('migration')) { impact.dataImpact = true; impact.categories.push('data-model'); } } // Generate description if (impact.categories.length > 0) { impact.description = `Changes affect: ${impact.categories.join(', ')}`; } else { impact.description = 'Internal implementation changes with minimal external impact'; } return impact; } // Analyze complexity changes function analyzeComplexityChanges(fileChanges, commitHash) { const complexity = { overallChange: 'neutral', technicalDebt: 'maintained', codeQuality: 'maintained', maintainability: 'maintained', evidence: [] }; let totalAdditions = 0; let totalDeletions = 0; let testFiles = 0; let codeFiles = 0; for (const change of fileChanges) { totalAdditions += change.insertions ?? 0; totalDeletions += change.deletions ?? 0; if (change.file.includes('test') || change.file.includes('spec')) { testFiles++; } else if (getFileCategory(change.file) === 'code') { codeFiles++; } } // Analyze net change const netChange = totalAdditions - totalDeletions; if (netChange > 100) { complexity.overallChange = 'increased'; complexity.evidence.push('Significant code additions'); } else if (netChange < -100) { complexity.overallChange = 'decreased'; complexity.evidence.push('Significant code removal/cleanup'); } // Test coverage analysis if (testFiles > 0 && codeFiles > 0) { complexity.codeQuality = 'improved'; complexity.evidence.push('Tests added/modified alongside code changes'); } return complexity; } // Helper functions for pattern detection function hasRefactoringPattern(fileChanges) { // Multiple files with balanced add/delete ratios often indicate refactoring const multipleFiles = fileChanges.length > 2; const balancedChanges = fileChanges.some(change => (change.insertions ?? 0) > 0 && (change.deletions ?? 0) > 0 ); return multipleFiles && balancedChanges; } function hasArchitecturalPattern(fileChanges) { // Changes to multiple modules or core configuration files const coreFiles = ['config', 'package.json', 'docker', 'makefile', 'webpack']; const moduleChanges = new Set(fileChanges.map(change => change.file.split('/')[0] )).size > 2; const coreChanges = fileChanges.some(change => coreFiles.some(core => change.file.toLowerCase().includes(core)) ); return moduleChanges || coreChanges; } function hasNewFeaturePattern(fileChanges) { // New files created or significant additions const newFiles = fileChanges.some(change => change.status === 'A'); const significantAdditions = fileChanges.some(change => (change.insertions ?? 0) > (change.deletions ?? 0) * 2 ); return newFiles || significantAdditions; } function hasPerformancePattern(fileChanges, commitMessage) { const performanceKeywords = ['optimize', 'performance', 'speed', 'cache', 'query', 'index']; const messageHasKeywords = performanceKeywords.some(keyword => commitMessage.toLowerCase().includes(keyword) ); const performanceFiles = fileChanges.some(change => ['database', 'query', 'cache', 'index'].some(keyword => change.file.toLowerCase().includes(keyword) ) ); return messageHasKeywords || performanceFiles; } function hasSecurityPattern(fileChanges, commitMessage) { const securityKeywords = ['security', 'auth', 'permission', 'validation', 'sanitize']; const messageHasKeywords = securityKeywords.some(keyword => commitMessage.toLowerCase().includes(keyword) ); const securityFiles = fileChanges.some(change => ['auth', 'security', 'permission', 'validation'].some(keyword => change.file.toLowerCase().includes(keyword) ) ); return messageHasKeywords || securityFiles; } // Helper function to categorize files function getFileCategory(filepath) { const ext = path.extname(filepath).toLowerCase(); const filename = path.basename(filepath).toLowerCase(); // Documentation if (ext === '.md' || ext === '.txt' || ext === '.rst') return 'documentation'; // Configuration if (ext === '.json' || ext === '.yaml' || ext === '.yml' || ext === '.toml' || filename.includes('config') || filename.includes('package')) return 'configuration'; // Testing if (filename.includes('test') || filename.includes('spec') || filepath.includes('__tests__')) return 'testing'; // Code files if (['.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.go', '.java', '.c', '.cpp', '.cs'].includes(ext)) return 'code'; return 'other'; } // Analyze individual code lines for significance function analyzeCodeLine(codeLine) { const line = codeLine.trim(); let significance = 0; let category = 'general'; // Function/method definitions if (line.match(/^(function|const|let|var|def|class|interface|type)/)) { significance = 3; category = 'definition'; } // API endpoints or routes if (line.match(/(app\.|router\.|@|endpoint|route)/)) { significance = 4; category = 'api'; } // Database or model changes if (line.match(/(model|schema|migration|database|query)/i)) { significance = 4; category = 'data'; } // Security-related changes if (line.match(/(auth|permission|security|validate|sanitize)/i)) { significance = 4; category = 'security'; } // Configuration changes if (line.match(/(config|setting|environment|env)/i)) { significance = 3; category = 'configuration'; } // Comments and documentation if (line.startsWith('//') || line.startsWith('*') || line.startsWith('#')) { significance = 1; category = 'documentation'; } return { significance, category }; } // Determine overall impact from file analysis function determineOverallImpact(fileAnalysis) { const impact = { architecturalChanges: false, breakingChanges: false, performanceImpact: 'minimal', securityImpact: 'none', testingChanges: false }; for (const file of fileAnalysis) { // Check for architectural changes based on file types and modification patterns if (file.type === 'configuration' || file.changes.modifications.length > 10) { impact.architecturalChanges = true; } // Check for potential breaking changes if (file.changes.modifications.some(mod => mod.category === 'api' && mod.type === 'deletion' )) { impact.breakingChanges = true; } // Assess performance impact if (file.changes.modifications.some(mod => mod.category === 'data')) { impact.performanceImpact = 'moderate'; } // Assess security impact if (file.changes.modifications.some(mod => mod.category === 'security')) { impact.securityImpact = 'positive'; } // Check for testing changes if (file.type === 'testing') { impact.testingChanges = true; } } return impact; } // Export all tools as an array for easy use export const gitAnalysisTools = [ analyzeGitDiffTool, detectArchitecturalPatternsTool, assessBusinessImpactTool, analyzeComplexityChangesTool ];