// cli/utils/shared.utils.ts // Shared utilities for CLI commands to eliminate duplication import { AgentRegistry } from '../../src/agents/agent-registry'; import { BusinessAnalystAgent } from '../../src/agents/implementations/business-analyst-agent'; import { SDETAgent } from '../../src/agents/implementations/sdet-agent'; import { DeveloperAuthorAgent } from '../../src/agents/implementations/developer-author-agent'; import { SeniorArchitectAgent } from '../../src/agents/implementations/senior-architect-agent'; import { DeveloperReviewerAgent } from '../../src/agents/implementations/developer-reviewer-agent'; import { AppConfig } from '../../src/config/config.interface'; import { generateEnhancedHtmlReport } from '../../src/formatters/html-report-formatter-enhanced'; import { generateConversationTranscript } from '../../src/formatters/conversation-transcript-formatter'; import { MetricsCalculationService } from '../../src/services/metrics-calculation.service'; import { AgentResult } from '../../src/agents/agent.interface'; import { TokenSnapshot, EvaluationHistoryEntry } from '../../src/types/output.types'; import fs from 'fs/promises'; import * as fsSync from 'fs'; import path from 'path'; import chalk from 'chalk'; import { AuthorStatsAggregatorService } from '../../src/services/author-stats-aggregator.service'; /** * Generate timestamp in yyyyMMddHHmmss format */ export function generateTimestamp(): string { const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); return `${year}${month}${day}${hours}${minutes}${seconds}`; } /** * Create and register agents based on config.agents.enabled list * Agent IDs: 'business-analyst', 'sdet', 'developer-author', 'senior-architect', 'developer-reviewer' */ export function createAgentRegistry(config: AppConfig): AgentRegistry { const agentRegistry = new AgentRegistry(); const enabledAgents = config.agents.enabled || [ 'business-analyst', 'sdet', 'developer-author', 'senior-architect', 'developer-reviewer', ]; // Validate that at least one agent is enabled if (!enabledAgents || enabledAgents.length === 0) { console.warn('āš ļø No agents enabled in config. Enabling all agents by default.'); enabledAgents.push( 'business-analyst', 'sdet', 'developer-author', 'senior-architect', 'developer-reviewer' ); } // Register agents based on enabled list const agentMap: Record any> = { 'business-analyst': () => new BusinessAnalystAgent(config), sdet: () => new SDETAgent(config), 'developer-author': () => new DeveloperAuthorAgent(config), 'senior-architect': () => new SeniorArchitectAgent(config), 'developer-reviewer': () => new DeveloperReviewerAgent(config), }; for (const agentId of enabledAgents) { if (agentMap[agentId]) { agentRegistry.register(agentMap[agentId]()); } else { console.warn(`āš ļø Unknown agent: ${agentId}. Skipping.`); } } return agentRegistry; } /** * Metadata for a commit evaluation */ export interface EvaluationMetadata { commitHash?: string; commitAuthor?: string; commitDate?: string; commitMessage?: string; timestamp?: string; source?: string; // 'commit', 'staged', 'current', 'file' developerOverview?: string; commitStats?: { filesChanged: number; insertions: number; deletions: number; }; } /** * Options for saving evaluation reports */ export interface SaveReportsOptions { outputDir: string; diff: string; agentResults: AgentResult[]; metadata?: EvaluationMetadata; developerOverview?: string; } /** * Save all evaluation reports to a directory * Creates a consistent structure: * - report-enhanced.html * - conversation.md * - results.json * - summary.txt * - commit.diff * - history.json (tracks re-evaluations) */ export async function saveEvaluationReports(options: SaveReportsOptions): Promise { const { outputDir, diff, agentResults, metadata = {}, developerOverview } = options; // Ensure output directory exists await fs.mkdir(outputDir, { recursive: true }); // Track evaluation history with metrics and tokens await trackEvaluationHistory(outputDir, metadata, agentResults); // 1. Save results.json const resultsJson = { timestamp: metadata.timestamp || new Date().toISOString(), metadata: { commitHash: metadata.commitHash, commitAuthor: metadata.commitAuthor, commitDate: metadata.commitDate, commitMessage: metadata.commitMessage, source: metadata.source, }, developerOverview: developerOverview || null, agents: agentResults, }; await fs.writeFile(path.join(outputDir, 'results.json'), JSON.stringify(resultsJson, null, 2)); // 2. Generate HTML report generateEnhancedHtmlReport(agentResults, path.join(outputDir, 'report-enhanced.html'), { commitHash: metadata.commitHash, commitAuthor: metadata.commitAuthor, commitMessage: metadata.commitMessage, commitDate: metadata.commitDate, timestamp: metadata.timestamp || new Date().toISOString(), developerOverview, filesChanged: metadata.commitStats?.filesChanged, insertions: metadata.commitStats?.insertions, deletions: metadata.commitStats?.deletions, }); // 3. Generate conversation transcript generateConversationTranscript(agentResults, path.join(outputDir, 'conversation.md'), { commitHash: metadata.commitHash, timestamp: metadata.timestamp || new Date().toISOString(), }); // 4. Save commit diff await fs.writeFile(path.join(outputDir, 'commit.diff'), diff); // 5. Generate summary text const summary = generateSummaryText(agentResults, metadata); await fs.writeFile(path.join(outputDir, 'summary.txt'), summary); // 6. Update evaluation index try { await updateEvaluationIndex(outputDir, metadata); } catch (indexError) { console.error( 'Failed to update evaluation index:', indexError instanceof Error ? indexError.message : String(indexError) ); throw indexError; // Re-throw to propagate the error } } /** * Extract metrics from final round agent results (last 5 agents) * Extracts individual agent scores and applies weighted averaging based on agent expertise * commitScore is calculated statically from the resulting weighted metrics */ function extractMetricsSnapshot(agentResults: AgentResult[]): any { // Use the centralized metrics calculation service for consistency const metrics = MetricsCalculationService.calculateWeightedMetrics(agentResults); return { functionalImpact: metrics.functionalImpact || 0, idealTimeHours: metrics.idealTimeHours || 0, testCoverage: metrics.testCoverage || 0, codeQuality: metrics.codeQuality || 0, codeComplexity: metrics.codeComplexity || 0, actualTimeHours: metrics.actualTimeHours || 0, technicalDebtHours: metrics.technicalDebtHours || 0, debtReductionHours: metrics.debtReductionHours || 0, commitScore: metrics.commitScore || 0, }; } /** * Extract token usage from all agent results */ function extractTokenSnapshot(agentResults: AgentResult[]): TokenSnapshot { let totalInputTokens = 0; let totalOutputTokens = 0; let totalCost = 0; agentResults.forEach((agent) => { if (agent.tokenUsage) { totalInputTokens += agent.tokenUsage.inputTokens || 0; totalOutputTokens += agent.tokenUsage.outputTokens || 0; } }); // Calculate cost based on provider and model const inputPrice = 3.0; // Anthropic Claude 3.5 Sonnet: $3/1M const outputPrice = 15.0; // $15/1M totalCost = (totalInputTokens / 1000000) * inputPrice + (totalOutputTokens / 1000000) * outputPrice; return { inputTokens: totalInputTokens, outputTokens: totalOutputTokens, totalTokens: totalInputTokens + totalOutputTokens, totalCost: Number(totalCost.toFixed(4)), }; } /** * Calculate convergence score from agent results */ function calculateConvergenceScore(agentResults: AgentResult[]): number { if (agentResults.length < 5) return 0; // Get final round agents const finalAgents = agentResults.slice(-5); // Simple convergence check: measure variance in code quality scores const qualityScores = finalAgents .filter((a) => a.metrics && a.metrics.codeQuality) .map((a) => a.metrics!.codeQuality); if (qualityScores.length < 2) return 0; const mean = qualityScores.reduce((a, b) => a + b, 0) / qualityScores.length; const variance = qualityScores.reduce((sum, score) => sum + Math.pow(score - mean, 2), 0) / qualityScores.length; const stdDev = Math.sqrt(variance); // Convergence score: 0 if stdDev > 2, 1 if stdDev == 0, linear in between const convergence = Math.max(0, 1 - stdDev / 2); return Number(convergence.toFixed(2)); } /** * Track evaluation history for re-evaluations * Maintains a history.json file with full metrics snapshots for each evaluation */ async function trackEvaluationHistory( outputDir: string, metadata: EvaluationMetadata, agentResults?: AgentResult[] ): Promise { const historyPath = path.join(outputDir, 'history.json'); let history: EvaluationHistoryEntry[] = []; // Read existing history try { const content = await fs.readFile(historyPath, 'utf-8'); history = JSON.parse(content); } catch { // No history yet history = []; } // Extract metrics and tokens if agent results provided const newEntry: EvaluationHistoryEntry = { timestamp: metadata.timestamp || new Date().toISOString(), source: metadata.source || 'unknown', evaluationNumber: history.length + 1, metrics: agentResults ? extractMetricsSnapshot(agentResults) : { functionalImpact: 0, idealTimeHours: 0, testCoverage: 0, codeQuality: 0, codeComplexity: 0, actualTimeHours: 0, technicalDebtHours: 0, debtReductionHours: 0, commitScore: 0, }, tokens: agentResults ? extractTokenSnapshot(agentResults) : { inputTokens: 0, outputTokens: 0, totalTokens: 0, totalCost: 0, }, convergenceScore: agentResults ? calculateConvergenceScore(agentResults) : 0, }; history.push(newEntry); // Write back await fs.writeFile(historyPath, JSON.stringify(history, null, 2)); } /** * Generate summary text from agent results */ function generateSummaryText(agentResults: AgentResult[], metadata: EvaluationMetadata): string { const lines: string[] = []; lines.push('='.repeat(80)); lines.push('COMMIT EVALUATION SUMMARY'); lines.push('='.repeat(80)); lines.push(''); if (metadata.commitHash) { lines.push(`Commit: ${metadata.commitHash}`); } if (metadata.commitAuthor) { lines.push(`Author: ${metadata.commitAuthor}`); } if (metadata.commitDate) { lines.push(`Date: ${metadata.commitDate}`); } if (metadata.commitMessage) { lines.push(`Message: ${metadata.commitMessage}`); } if (metadata.source) { lines.push(`Source: ${metadata.source}`); } if (metadata.timestamp) { lines.push(`Evaluated: ${metadata.timestamp}`); } lines.push(''); lines.push('-'.repeat(80)); lines.push('AGENT EVALUATIONS'); lines.push('-'.repeat(80)); lines.push(''); for (const result of agentResults) { lines.push(`Agent: ${result.agentName || 'Unknown'}`); if (result.metrics) { lines.push('Metrics:'); for (const [key, value] of Object.entries(result.metrics)) { lines.push(` ${key}: ${value}`); } } if (result.summary) { lines.push('Summary:'); lines.push(` ${result.summary}`); } lines.push(''); } return lines.join('\n'); } /** * Get the root evaluation directory * Returns: .evaluated-commits/ */ export function getEvaluationRoot(baseDir: string = '.'): string { return path.join(baseDir, '.evaluated-commits'); } /** * Create evaluation directory for a commit * Format: .evaluated-commits/{commitHash}/ * Works for both single and batch evaluations - always updates the same folder */ export async function createEvaluationDirectory( commitHash: string, baseDir: string = '.' ): Promise { const evaluationsRoot = getEvaluationRoot(baseDir); const commitDir = path.join(evaluationsRoot, commitHash); await fs.mkdir(commitDir, { recursive: true }); return commitDir; } /** * Calculate averaged metrics from agent results using weighted averaging (matching report calculations) */ async function calculateAveragedMetrics(evaluationDir: string): Promise { return MetricsCalculationService.loadMetricsFromDirectory(evaluationDir); } /** * Get or create an evaluation index * Maintains index.json and generates index.html showing all evaluations */ export async function updateEvaluationIndex( evaluationDir: string, metadata: EvaluationMetadata ): Promise { const evaluationsRoot = path.dirname(evaluationDir); const indexJsonPath = path.join(evaluationsRoot, 'index.json'); const indexHtmlPath = path.join(evaluationsRoot, 'index.html'); let index: any[] = []; // Read existing index try { const content = await fs.readFile(indexJsonPath, 'utf-8'); index = JSON.parse(content); } catch { // Index doesn't exist yet, start fresh index = []; } // Calculate averaged metrics const metrics = await calculateAveragedMetrics(evaluationDir); // Find existing entry or create new one const dirName = path.basename(evaluationDir); const existingIndex = index.findIndex((item) => item.directory === dirName); const entry = { directory: dirName, commitHash: metadata.commitHash || dirName, commitAuthor: metadata.commitAuthor, commitMessage: metadata.commitMessage, commitDate: metadata.commitDate, source: metadata.source, lastEvaluated: metadata.timestamp || new Date().toISOString(), evaluationCount: 1, metrics: metrics, // Add averaged metrics }; if (existingIndex >= 0) { // Update existing entry entry.evaluationCount = (index[existingIndex].evaluationCount || 0) + 1; index[existingIndex] = entry; } else { // Add new entry index.push(entry); } // Sort by commit date (newest first), then by last evaluated as tiebreaker index.sort( (a, b) => new Date(b.commitDate).getTime() - new Date(a.commitDate).getTime() || new Date(b.lastEvaluated).getTime() - new Date(a.lastEvaluated).getTime() ); // Write JSON index await fs.writeFile(indexJsonPath, JSON.stringify(index, null, 2)); // Generate HTML index await generateIndexHtml(indexHtmlPath, index); } /** * Generate HTML index showing all evaluations */ async function generateIndexHtml(indexPath: string, index: any[]): Promise { // Group commits by author const byAuthor = new Map(); index.forEach((item) => { const author = item.commitAuthor || 'Unknown'; if (!byAuthor.has(author)) { byAuthor.set(author, []); } byAuthor.get(author)!.push(item); }); // Calculate overall metrics for display // Note: item.metrics already contains weighted consensus values from MetricsCalculationService // This is just aggregating those pre-calculated values for the summary stats const overallMetrics = { avgQuality: 0, avgComplexity: 0, avgFunctionalImpact: 0, avgTestCoverage: 0, avgActualTime: 0, avgCommitScore: 0, totalTechDebt: 0, count: 0, }; index.forEach((item) => { if (item.metrics) { overallMetrics.avgQuality += item.metrics.codeQuality || 0; overallMetrics.avgComplexity += item.metrics.codeComplexity || 0; overallMetrics.avgFunctionalImpact += item.metrics.functionalImpact || 0; overallMetrics.avgTestCoverage += item.metrics.testCoverage || 0; overallMetrics.avgActualTime += item.metrics.actualTimeHours || 0; overallMetrics.avgCommitScore += item.metrics.commitScore || 0; // Calculate NET debt (debt introduced - debt reduction) const netDebt = (item.metrics.technicalDebtHours || 0) - (item.metrics.debtReductionHours || 0); overallMetrics.totalTechDebt += netDebt; overallMetrics.count++; } }); if (overallMetrics.count > 0) { overallMetrics.avgQuality = Number( (overallMetrics.avgQuality / overallMetrics.count).toFixed(1) ); overallMetrics.avgComplexity = Number( (overallMetrics.avgComplexity / overallMetrics.count).toFixed(1) ); overallMetrics.avgFunctionalImpact = Number( (overallMetrics.avgFunctionalImpact / overallMetrics.count).toFixed(1) ); overallMetrics.avgTestCoverage = Number( (overallMetrics.avgTestCoverage / overallMetrics.count).toFixed(1) ); overallMetrics.avgActualTime = Number( (overallMetrics.avgActualTime / overallMetrics.count).toFixed(2) ); overallMetrics.avgCommitScore = Number( (overallMetrics.avgCommitScore / overallMetrics.count).toFixed(1) ); overallMetrics.totalTechDebt = Number(overallMetrics.totalTechDebt.toFixed(2)); } // Calculate team metrics using aggregated author data for consistency const evalRoot = path.dirname(indexPath); const allAuthorData = await AuthorStatsAggregatorService.aggregateAuthorStats(evalRoot); const allMetrics: any[] = []; // Convert all authors' evaluations to metrics format for (const [authorName, authorEvaluations] of allAuthorData.entries()) { const authorMetrics = authorEvaluations.map((evaluation) => { // Calculate averaged metrics from agent results const averagedMetrics = evaluation.averagedMetrics || MetricsCalculationService.calculateWeightedMetrics(evaluation.agents); return { createdBy: authorName, commitScore: averagedMetrics?.commitScore || 0, testingQuality: averagedMetrics?.testCoverage || 0, technicalDebtRate: 0, deliveryRate: 0, functionalImpact: averagedMetrics?.functionalImpact || 0, codeQuality: averagedMetrics?.codeQuality || 0, codeComplexity: averagedMetrics?.codeComplexity || 0, actualTimeHours: averagedMetrics?.actualTimeHours || 0, idealTimeHours: averagedMetrics?.idealTimeHours || 0, technicalDebtHours: averagedMetrics?.technicalDebtHours || 0, debtReductionHours: averagedMetrics?.debtReductionHours || 0, }; }); allMetrics.push(...authorMetrics); } // Get comprehensive team summary (includes enhanced stats, BACI scores, and rankings) const teamSummary = MetricsCalculationService.calculateTeamSummary(allMetrics); // Calculate average BACI score from team summary const teamStats = Object.values(teamSummary.teamStats); const avgBaciScore = teamStats.length > 0 ? Number( ( teamStats.reduce((sum, stat) => sum + (stat.baciScore || 0), 0) / teamStats.length ).toFixed(1) ) : 0; // Generate author pages for each author (after team summary for BACI score consistency) for (const [author, commits] of byAuthor.entries()) { await generateAuthorPage(evalRoot, author, commits, teamSummary); } const html = ` Commit Evaluations Index

šŸ“Š Commit Evaluations

All evaluated commits in one place

${index.length}
Total Commits
${index.reduce((sum, item) => sum + (item.evaluationCount || 1), 0)}
Total Evaluations
${byAuthor.size}
Authors
${ overallMetrics.count > 0 ? `
${avgBaciScore}
Avg Score
out of 10
${overallMetrics.avgQuality}
Avg Quality
out of 10
${overallMetrics.avgComplexity}
Avg Complexity
out of 10
${overallMetrics.avgTestCoverage}
Avg Test Coverage
out of 10
${overallMetrics.avgFunctionalImpact}
Avg Impact
out of 10
${overallMetrics.avgCommitScore}
Avg Commit Score
out of 10
${overallMetrics.avgActualTime}h
Avg Time
per commit
${overallMetrics.totalTechDebt > 0 ? '+' : ''}${overallMetrics.totalTechDebt}h
Total Tech Debt
${overallMetrics.totalTechDebt > 0 ? 'added' : 'reduced'}
` : '' }

šŸ‘„ Authors Summary

${Array.from(byAuthor.entries()) .map(([author, commits]) => { // Calculate average commit score once let totalScore = 0; let count = 0; commits.forEach((c) => { if (c.metrics && typeof c.metrics.commitScore === 'number') { totalScore += c.metrics.commitScore; count++; } }); const avgCommitScore = count > 0 ? totalScore / count : 0; return { author, commits, avgCommitScore }; }) .sort( (a, b) => (teamSummary.teamStats[b.author]?.baciScore || 0) - (teamSummary.teamStats[a.author]?.baciScore || 0) ) // Sort by BACI score .map(({ author, commits }) => { // Sort commits by commit date (newest first) commits.sort((a, b) => new Date(b.commitDate).getTime() - new Date(a.commitDate).getTime()); const authorMetrics = { quality: 0, complexity: 0, testCoverage: 0, functionalImpact: 0, commitScore: 0, actualTime: 0, techDebt: 0, count: 0, }; commits.forEach((c) => { if (c.metrics) { authorMetrics.quality += c.metrics.codeQuality || 0; authorMetrics.complexity += c.metrics.codeComplexity || 0; authorMetrics.testCoverage += c.metrics.testCoverage || 0; authorMetrics.functionalImpact += c.metrics.functionalImpact || 0; authorMetrics.commitScore += c.metrics.commitScore || 0; authorMetrics.actualTime += c.metrics.actualTimeHours || 0; // Calculate NET debt (debt introduced - debt reduction) const netDebt = (c.metrics.technicalDebtHours || 0) - (c.metrics.debtReductionHours || 0); authorMetrics.techDebt += netDebt; authorMetrics.count++; } }); const avgQuality = authorMetrics.count > 0 ? (authorMetrics.quality / authorMetrics.count).toFixed(1) : 'N/A'; const avgComplexity = authorMetrics.count > 0 ? (authorMetrics.complexity / authorMetrics.count).toFixed(1) : 'N/A'; const avgTestCoverage = authorMetrics.count > 0 ? (authorMetrics.testCoverage / authorMetrics.count).toFixed(1) : 'N/A'; const avgFunctionalImpact = authorMetrics.count > 0 ? (authorMetrics.functionalImpact / authorMetrics.count).toFixed(1) : 'N/A'; const displayAvgCommitScore = authorMetrics.count > 0 ? (authorMetrics.commitScore / authorMetrics.count).toFixed(1) : 'N/A'; const avgActualTime = authorMetrics.count > 0 ? (authorMetrics.actualTime / authorMetrics.count).toFixed(2) : 'N/A'; const totalTechDebt = authorMetrics.count > 0 ? authorMetrics.techDebt.toFixed(2) : 'N/A'; const authorSlug = author.toLowerCase().replace(/[^a-z0-9]/g, '_'); const authorPageUrl = `author-${authorSlug}.html`; return ` `; }) .join('')}
Author Commits Score Avg Quality Avg Complexity Avg Tests Avg Impact Avg Commit Score Avg Time Total Tech Debt Action
šŸ‘¤ ${author} ${commits.length} ${(() => { const baciScore = teamSummary.teamStats[author]?.baciScore || 0; return baciScore > 0 ? `${baciScore.toFixed(1)}/10` : 'N/A'; })()} ${avgQuality !== 'N/A' ? `${avgQuality}/10` : 'N/A'} ${avgComplexity !== 'N/A' ? `${avgComplexity}/10` : 'N/A'} ${avgTestCoverage !== 'N/A' ? `${avgTestCoverage}/10` : 'N/A'} ${avgFunctionalImpact !== 'N/A' ? `${avgFunctionalImpact}/10` : 'N/A'} ${displayAvgCommitScore !== 'N/A' ? `${displayAvgCommitScore}/10` : 'N/A'} ${avgActualTime !== 'N/A' ? `${avgActualTime}h` : 'N/A'} ${totalTechDebt !== 'N/A' ? `${parseFloat(totalTechDebt) > 0 ? '+' : ''}${totalTechDebt}h` : 'N/A'} View Dashboard

šŸ“ All Commits

${index .map((item) => { const metrics = item.metrics || {}; const qualityColor = metrics.codeQuality >= 7 ? 'good' : metrics.codeQuality >= 4 ? 'medium' : 'bad'; const complexityColor = metrics.codeComplexity <= 3 ? 'good' : metrics.codeComplexity <= 6 ? 'medium' : 'bad'; const testsColor = metrics.testCoverage >= 7 ? 'good' : metrics.testCoverage >= 4 ? 'medium' : 'bad'; const impactColor = metrics.functionalImpact >= 7 ? 'good' : metrics.functionalImpact >= 4 ? 'medium' : 'bad'; const commitScoreColor = metrics.commitScore >= 7 ? 'good' : metrics.commitScore >= 4 ? 'medium' : 'bad'; const netDebt = (metrics.technicalDebtHours || 0) - (metrics.debtReductionHours || 0); const debtColor = netDebt > 0 ? 'bad' : netDebt < 0 ? 'good' : 'medium'; return ` `; }) .join('')}
Hash Author Message Date Last Evaluated Source Quality Complexity Tests Impact Commit Score Time Tech Debt Action
${item.commitHash?.substring(0, 8) || item.directory} ${item.evaluationCount > 1 ? `
${item.evaluationCount}x` : ''}
šŸ‘¤ ${item.commitAuthor || 'Unknown'} ${item.commitMessage ? item.commitMessage.split('\\n')[0] : ''} ${item.commitDate ? new Date(item.commitDate).toLocaleDateString() : ''} ${item.lastEvaluated ? new Date(item.lastEvaluated).toLocaleDateString() : ''} ${item.source || 'unknown'} ${item.metrics ? `${metrics.codeQuality}/10` : 'N/A'} ${item.metrics ? `${metrics.codeComplexity}/10` : 'N/A'} ${item.metrics ? `${metrics.testCoverage}/10` : 'N/A'} ${item.metrics ? `${metrics.functionalImpact}/10` : 'N/A'} ${item.metrics && typeof metrics.commitScore === 'number' ? `${metrics.commitScore}/10` : 'N/A'} ${item.metrics ? `${metrics.actualTimeHours}h` : 'N/A'} ${item.metrics ? `${netDebt > 0 ? '+' : ''}${netDebt.toFixed(1)}h` : 'N/A'} View
`; await fs.writeFile(indexPath, html); } /** * Generate batch identifier from options * Examples: * --count 10 → "last-10" * --since 2024-01-01 --until 2024-01-31 → "2024-01-01_to_2024-01-31" * --since 2024-01-01 → "since-2024-01-01" * --branch develop --count 5 → "branch-develop-last-5" */ export function generateBatchIdentifier(options: { since?: string; until?: string; count?: number; branch?: string; }): string { const parts: string[] = []; if (options.branch && options.branch !== 'HEAD') { parts.push(`branch-${options.branch.replace(/[^a-zA-Z0-9-]/g, '_')}`); } if (options.since && options.until) { parts.push(`${options.since}_to_${options.until}`); } else if (options.since) { parts.push(`since-${options.since}`); } else if (options.until) { parts.push(`until-${options.until}`); } else if (options.count) { parts.push(`last-${options.count}`); } return parts.length > 0 ? parts.join('-') : 'batch'; } /** * Generate author-specific page with their commits and dashboard */ export async function generateAuthorPage( evaluationsRoot: string, author: string, commits: any[], teamSummary: any ): Promise { const authorSlug = author.toLowerCase().replace(/[^a-z0-9]/g, '_'); const authorPagePath = path.join(evaluationsRoot, `author-${authorSlug}.html`); // Use centralized metrics calculation service for consistency with OKR generation const authorData = await AuthorStatsAggregatorService.aggregateAuthorStats(evaluationsRoot, { targetAuthor: author, }); // Use deduplicated evaluations (latest per commit) for both metrics AND display const evaluations = authorData.get(author) || []; const analysis = evaluations && evaluations.length > 0 ? AuthorStatsAggregatorService.analyzeAuthor(evaluations) : null; // Sort deduplicated commits by commit date (newest first) for display const deduplicatedCommits = evaluations.sort( (a: any, b: any) => new Date(b.metadata?.commitDate || 0).getTime() - new Date(a.metadata?.commitDate || 0).getTime() ); // Use centralized stats or fallback to empty const avgQuality = analysis ? analysis.stats.quality.toFixed(1) : 'N/A'; const avgComplexity = analysis ? analysis.stats.complexity.toFixed(1) : 'N/A'; const avgTestCoverage = analysis ? analysis.stats.tests.toFixed(1) : 'N/A'; const avgFunctionalImpact = analysis ? analysis.stats.impact.toFixed(1) : 'N/A'; const avgActualTime = analysis ? analysis.stats.time.toFixed(2) : 'N/A'; const totalTechDebt = analysis ? analysis.stats.techDebt.toFixed(2) : 'N/A'; const avgCommitScore = analysis ? analysis.stats.commitScore.toFixed(1) : 'N/A'; const commitCount = deduplicatedCommits.length; // Use deduplicated count // Get BACI score from the provided team summary const avgBaciScore = teamSummary?.teamStats?.[author]?.baciScore?.toFixed(1) || 'N/A'; // Check for OKR files and load latest OKR data const okrsDir = path.join(evaluationsRoot, '.okrs', authorSlug); let okrContentHtml = ''; if (fsSync.existsSync(okrsDir)) { const files = fsSync.readdirSync(okrsDir); const okrJsonFiles = files .filter((f: string) => f.startsWith('okr_') && f.endsWith('.json')) .sort() .reverse(); // Newest first if (okrJsonFiles.length > 0) { // Load the latest OKR data const latestJsonPath = path.join(okrsDir, okrJsonFiles[0]); try { const okrData = JSON.parse(fsSync.readFileSync(latestJsonPath, 'utf-8')); const generatedDate = new Date(okrData.generatedAt).toLocaleDateString(); // Build grid-based OKR section okrContentHtml = `

āœ… Latest OKR Profile

Generated: ${generatedDate}
šŸ“„ View Full Report
${ okrData.progressReport ? ` ` : '' }
šŸ’Ŗ Strong Points
    ${okrData.strongPoints.map((point: string) => `
  • ${point}
  • `).join('')}
āš ļø Growth Areas
    ${okrData.weakPoints.map((point: string) => `
  • ${point}
  • `).join('')}
🧩 Knowledge Gaps
    ${okrData.knowledgeGaps.map((gap: string) => `
  • ${gap}
  • `).join('')}
šŸŽÆ 3-Month Objective
${okrData.okr3Month.objective}
${okrData.okr3Month.keyResults .map( (kr: any, i: number) => `
KR ${i + 1}
${kr.kr}
Why: ${kr.why}
${ kr.actionSteps && kr.actionSteps.length > 0 ? `
āœ“ Action Steps:
    ${kr.actionSteps.map((step: string) => `
  1. ${step}
  2. `).join('')}
` : '' }
` ) .join('')}
${ okrData.okr6Month ? `

${okrData.okr6Month.keyResults .map( (kr: any, i: number) => `
KR ${i + 1}: ${kr.kr}
` ) .join('')}
${ okrData.okr12Month ? `

${okrData.okr12Month.keyResults .map( (kr: any, i: number) => `
KR ${i + 1}: ${kr.kr}
` ) .join('')}
` : '' }
` : '' } ${ okrData.actionPlan && okrData.actionPlan.length > 0 ? `
šŸš€ Action Plan
${okrData.actionPlan .map( (item: any) => ` ` ) .join('')}
Area Action Timeline Success Criteria
${item.area} ${item.action} ${item.timeline} ${item.success}
` : '' } `; } catch (e) { okrContentHtml = `
āš ļø Error loading OKR data

Unable to read OKR profile. The file may be corrupted.

`; } } else { okrContentHtml = `
šŸ“‹ No OKR profiles yet

OKRs are generated periodically to track developer growth and set quarterly objectives.

Run: codewave generate-okr to generate OKR profiles

`; } } else { okrContentHtml = `
šŸ“‹ No OKR profiles yet

OKRs are generated periodically to track developer growth and set quarterly objectives.

Run: codewave generate-okr to generate OKR profiles

`; } const html = ` ${author} - Commit Evaluations
← Back to All Commits

šŸ‘¤ ${author}

Developer Dashboard

${commits.length}
Total Commits
${avgBaciScore}
Avg Score
out of 10
${avgQuality}
Avg Quality
out of 10
${avgComplexity}
Avg Complexity
out of 10
${avgTestCoverage}
Avg Test Coverage
out of 10
${avgFunctionalImpact}
Avg Impact
out of 10
${avgCommitScore}
Avg Commit Score
out of 10
${avgActualTime}h
Avg Time
per commit
${totalTechDebt !== 'N/A' && parseFloat(totalTechDebt) > 0 ? '+' : ''}${totalTechDebt}h
Total Tech Debt
${totalTechDebt !== 'N/A' && parseFloat(totalTechDebt) > 0 ? 'added' : 'reduced'}

šŸŽÆ OKR & Growth Profile

${okrContentHtml}

šŸ“ Commits by ${author}

${commits .map((item) => { const metrics = item.metrics || {}; // Calculate NET debt (debt introduced - debt reduction) const netDebt = (metrics.technicalDebtHours || 0) - (metrics.debtReductionHours || 0); const qualityColor = metrics.codeQuality >= 7 ? 'good' : metrics.codeQuality >= 4 ? 'medium' : 'bad'; const complexityColor = metrics.codeComplexity <= 3 ? 'good' : metrics.codeComplexity <= 6 ? 'medium' : 'bad'; const testsColor = metrics.testCoverage >= 7 ? 'good' : metrics.testCoverage >= 4 ? 'medium' : 'bad'; const impactColor = metrics.functionalImpact >= 7 ? 'good' : metrics.functionalImpact >= 4 ? 'medium' : 'bad'; const commitScoreColor = metrics.commitScore >= 7 ? 'good' : metrics.commitScore >= 4 ? 'medium' : 'bad'; const debtColor = netDebt > 0 ? 'bad' : netDebt < 0 ? 'good' : 'medium'; return ` `; }) .join('')}
Hash Message Date Last Evaluated Source Quality Complexity Tests Impact Commit Score Time Tech Debt Action
${item.commitHash?.substring(0, 8) || item.directory} ${item.evaluationCount > 1 ? `
${item.evaluationCount}x` : ''}
${item.commitMessage ? item.commitMessage.split('\\n')[0] : ''} ${item.commitDate ? new Date(item.commitDate).toLocaleDateString() : ''} ${item.lastEvaluated ? new Date(item.lastEvaluated).toLocaleDateString() : ''} ${item.source || 'unknown'} ${item.metrics ? `${metrics.codeQuality}/10` : 'N/A'} ${item.metrics ? `${metrics.codeComplexity}/10` : 'N/A'} ${item.metrics ? `${metrics.testCoverage}/10` : 'N/A'} ${item.metrics ? `${metrics.functionalImpact}/10` : 'N/A'} ${item.metrics ? `${metrics.commitScore}/10` : 'N/A'} ${item.metrics ? `${metrics.actualTimeHours}h` : 'N/A'} ${item.metrics ? `${netDebt > 0 ? '+' : ''}${netDebt.toFixed(1)}h` : 'N/A'} View
`; await fs.writeFile(authorPagePath, html); } /** * Build the index URL for cross-platform access */ export function buildIndexUrl(): string { const indexPath = path.join(process.cwd(), '.evaluated-commits', 'index.html'); const normalizedPath = indexPath.replace(/\\/g, '/'); return process.platform === 'win32' ? `file:///${normalizedPath}` : `file://${normalizedPath}`; } /** * Print completion message for evaluate command (single commit) */ export function printEvaluateCompletionMessage(outputDir: string): void { try { console.log(chalk.green(`\nāœ… Evaluation complete!`)); console.log(chalk.cyan(`šŸ“ Output directory: ${chalk.bold(outputDir)}`)); console.log( chalk.white(` šŸ“„ report-enhanced.html - 🌟 Conversation Timeline (Interactive)`) ); console.log(chalk.white(` šŸ“ conversation.md - 🌟 Markdown Transcript`)); console.log(chalk.gray(` šŸ“„ report.html - Standard HTML report`)); console.log(chalk.gray(` šŸ“‹ results.json - Full JSON results`)); console.log(chalk.gray(` šŸ“ commit.diff - Original diff`)); console.log(chalk.gray(` šŸ“Š summary.txt - Quick summary`)); console.log( chalk.yellow( `\nšŸ’” Open ${chalk.bold('report-enhanced.html')} for interactive view or ${chalk.bold('conversation.md')} for transcript!\n` ) ); const indexUrl = buildIndexUrl(); console.log(chalk.cyan(`\n🌐 View all evaluations: ${indexUrl}\n`)); } catch (e) { // Fallback without chalk if it's not available console.log(`\nāœ… Evaluation complete!`); console.log(`šŸ“ Output directory: ${outputDir}`); console.log(` šŸ“„ report-enhanced.html - 🌟 Conversation Timeline (Interactive)`); console.log(` šŸ“ conversation.md - 🌟 Markdown Transcript`); console.log(` šŸ“„ report.html - Standard HTML report`); console.log(` šŸ“‹ results.json - Full JSON results`); console.log(` šŸ“ commit.diff - Original diff`); console.log(` šŸ“Š summary.txt - Quick summary`); console.log( `\nšŸ’” Open report-enhanced.html for interactive view or conversation.md for transcript!\n` ); const indexUrl = buildIndexUrl(); console.log(`\n🌐 View all evaluations: ${indexUrl}\n`); } } /** * Print completion message for batch command (multiple commits) */ export function printBatchCompletionMessage(summary: { total: number; complete: number; failed: number; }): void { console.log(`${'='.repeat(80)}`); console.log('āœ… CodeWave analysis complete!'); console.log(`${'='.repeat(80)}\n`); console.log(`šŸ“Š Summary:`); console.log(` Total commits: ${summary.total}`); console.log(` Successful: ${summary.complete}`); console.log(` Failed: ${summary.failed}`); console.log(`\nšŸ“ All evaluations saved to: .evaluated-commits/`); console.log(` 🌐 index.html - Master index of all evaluations`); console.log(` šŸ“‚ [commit-hash]/ - Individual commit evaluations`); const indexUrl = buildIndexUrl(); console.log(`\nšŸ’” Open index: ${indexUrl}\n`); }