Skip to main content
Glama
decomposition-summary-generator.ts23.4 kB
/** * Decomposition Summary Generator * * Generates comprehensive session-specific summary files for decomposition sessions * including detailed analysis, task breakdown, and visual representations. */ import { DecompositionSession } from './decomposition-service.js'; import { AtomicTask } from '../types/task.js'; import { getVibeTaskManagerOutputDir } from '../utils/config-loader.js'; import { FileUtils } from '../utils/file-utils.js'; import logger from '../../../logger.js'; import * as path from 'path'; import * as fs from 'fs-extra'; /** * Summary generation configuration */ export interface SummaryConfig { /** Include detailed task breakdown */ includeTaskBreakdown: boolean; /** Include dependency analysis */ includeDependencyAnalysis: boolean; /** Include performance metrics */ includePerformanceMetrics: boolean; /** Include visual diagrams */ includeVisualDiagrams: boolean; /** Include JSON exports */ includeJsonExports: boolean; /** Custom output directory */ customOutputDir?: string; } /** * Summary generation result */ export interface SummaryGenerationResult { success: boolean; outputDirectory: string; generatedFiles: string[]; error?: string; metadata: { sessionId: string; projectId: string; totalTasks: number; totalHours: number; generationTime: number; timestamp: Date; }; } /** * Task analysis summary */ export interface TaskAnalysisSummary { totalTasks: number; totalHours: number; averageHours: number; tasksByType: Record<string, number>; tasksByPriority: Record<string, number>; complexityDistribution: { simple: number; medium: number; complex: number; }; estimatedDuration: { minimum: number; maximum: number; average: number; }; } /** * Default summary configuration */ const DEFAULT_SUMMARY_CONFIG: SummaryConfig = { includeTaskBreakdown: true, includeDependencyAnalysis: true, includePerformanceMetrics: true, includeVisualDiagrams: true, includeJsonExports: true }; /** * Decomposition Summary Generator Service */ export class DecompositionSummaryGenerator { private config: SummaryConfig; constructor(config: Partial<SummaryConfig> = {}) { this.config = { ...DEFAULT_SUMMARY_CONFIG, ...config }; } /** * Generate comprehensive summary for a decomposition session */ async generateSessionSummary(session: DecompositionSession): Promise<SummaryGenerationResult> { const startTime = Date.now(); try { logger.info({ sessionId: session.id, projectId: session.projectId, status: session.status }, 'Starting decomposition summary generation'); // Create session-specific output directory const outputDirectory = await this.createSessionDirectory(session); const generatedFiles: string[] = []; // Generate task analysis const taskAnalysis = this.analyzeSessionTasks(session); // Generate main summary markdown if (this.config.includeTaskBreakdown) { const summaryFile = await this.generateMainSummary(session, taskAnalysis, outputDirectory); generatedFiles.push(summaryFile); } // Generate detailed task breakdown const taskBreakdownFile = await this.generateTaskBreakdown(session, outputDirectory); generatedFiles.push(taskBreakdownFile); // Generate performance metrics if (this.config.includePerformanceMetrics) { const metricsFile = await this.generatePerformanceMetrics(session, outputDirectory); generatedFiles.push(metricsFile); } // Generate dependency analysis if (this.config.includeDependencyAnalysis && session.persistedTasks) { const dependencyFile = await this.generateDependencyAnalysis(session, outputDirectory); generatedFiles.push(dependencyFile); } // Generate visual diagrams if (this.config.includeVisualDiagrams) { const diagramFiles = await this.generateVisualDiagrams(session, outputDirectory); generatedFiles.push(...diagramFiles); } // Generate JSON exports if (this.config.includeJsonExports) { const jsonFiles = await this.generateJsonExports(session, taskAnalysis, outputDirectory); generatedFiles.push(...jsonFiles); } const generationTime = Date.now() - startTime; logger.info({ sessionId: session.id, projectId: session.projectId, outputDirectory, filesGenerated: generatedFiles.length, generationTime }, 'Decomposition summary generation completed successfully'); return { success: true, outputDirectory, generatedFiles, metadata: { sessionId: session.id, projectId: session.projectId, totalTasks: session.persistedTasks?.length || 0, totalHours: taskAnalysis.totalHours, generationTime, timestamp: new Date() } }; } catch (error) { const generationTime = Date.now() - startTime; logger.error({ err: error, sessionId: session.id, projectId: session.projectId, generationTime }, 'Failed to generate decomposition summary'); return { success: false, outputDirectory: '', generatedFiles: [], error: error instanceof Error ? error.message : String(error), metadata: { sessionId: session.id, projectId: session.projectId, totalTasks: 0, totalHours: 0, generationTime, timestamp: new Date() } }; } } /** * Create session-specific output directory */ private async createSessionDirectory(session: DecompositionSession): Promise<string> { const baseOutputDir = this.config.customOutputDir || getVibeTaskManagerOutputDir(); const sessionDir = path.join( baseOutputDir, 'decomposition-sessions', `${session.projectId}-${session.id}` ); await fs.ensureDir(sessionDir); return sessionDir; } /** * Analyze session tasks for summary statistics */ private analyzeSessionTasks(session: DecompositionSession): TaskAnalysisSummary { const tasks = session.persistedTasks || []; if (tasks.length === 0) { return { totalTasks: 0, totalHours: 0, averageHours: 0, tasksByType: {}, tasksByPriority: {}, complexityDistribution: { simple: 0, medium: 0, complex: 0 }, estimatedDuration: { minimum: 0, maximum: 0, average: 0 } }; } const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); const averageHours = totalHours / tasks.length; // Group by type const tasksByType: Record<string, number> = {}; tasks.forEach(task => { tasksByType[task.type] = (tasksByType[task.type] || 0) + 1; }); // Group by priority const tasksByPriority: Record<string, number> = {}; tasks.forEach(task => { tasksByPriority[task.priority] = (tasksByPriority[task.priority] || 0) + 1; }); // Complexity distribution based on estimated hours const complexityDistribution = { simple: tasks.filter(t => (t.estimatedHours || 0) <= 2).length, medium: tasks.filter(t => (t.estimatedHours || 0) > 2 && (t.estimatedHours || 0) <= 8).length, complex: tasks.filter(t => (t.estimatedHours || 0) > 8).length }; // Duration statistics const hours = tasks.map(t => t.estimatedHours || 0); const estimatedDuration = { minimum: Math.min(...hours), maximum: Math.max(...hours), average: averageHours }; return { totalTasks: tasks.length, totalHours, averageHours, tasksByType, tasksByPriority, complexityDistribution, estimatedDuration }; } /** * Generate main summary markdown file */ private async generateMainSummary( session: DecompositionSession, analysis: TaskAnalysisSummary, outputDir: string ): Promise<string> { const timestamp = new Date().toISOString(); const duration = session.endTime ? session.endTime.getTime() - session.startTime.getTime() : Date.now() - session.startTime.getTime(); let content = `# Decomposition Session Summary\n\n`; content += `**Session ID:** ${session.id}\n`; content += `**Project ID:** ${session.projectId}\n`; content += `**Status:** ${session.status}\n`; content += `**Generated:** ${timestamp}\n\n`; content += `## Session Overview\n\n`; content += `- **Start Time:** ${session.startTime.toISOString()}\n`; content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`; content += `- **Duration:** ${Math.round(duration / 1000)}s\n`; content += `- **Progress:** ${session.progress}%\n`; content += `- **Max Depth:** ${session.maxDepth}\n`; content += `- **Current Depth:** ${session.currentDepth}\n\n`; content += `## Task Analysis\n\n`; content += `- **Total Tasks Generated:** ${analysis.totalTasks}\n`; content += `- **Total Estimated Hours:** ${analysis.totalHours.toFixed(1)}h\n`; content += `- **Average Hours per Task:** ${analysis.averageHours.toFixed(1)}h\n\n`; content += `### Task Distribution by Type\n\n`; Object.entries(analysis.tasksByType).forEach(([type, count]) => { content += `- **${type}:** ${count} tasks\n`; }); content += `\n### Task Distribution by Priority\n\n`; Object.entries(analysis.tasksByPriority).forEach(([priority, count]) => { content += `- **${priority}:** ${count} tasks\n`; }); content += `\n### Complexity Distribution\n\n`; content += `- **Simple (≤2h):** ${analysis.complexityDistribution.simple} tasks\n`; content += `- **Medium (2-8h):** ${analysis.complexityDistribution.medium} tasks\n`; content += `- **Complex (>8h):** ${analysis.complexityDistribution.complex} tasks\n\n`; if (session.error) { content += `## Error Information\n\n`; content += `**Error:** ${session.error}\n\n`; } content += `---\n`; content += `*Generated by Vibe Task Manager Decomposition Summary Generator*\n`; const filePath = path.join(outputDir, 'session-summary.md'); await FileUtils.writeFile(filePath, content); return filePath; } /** * Generate detailed task breakdown file */ private async generateTaskBreakdown(session: DecompositionSession, outputDir: string): Promise<string> { const tasks = session.persistedTasks || []; let content = `# Detailed Task Breakdown\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n`; content += `**Total Tasks:** ${tasks.length}\n\n`; if (tasks.length === 0) { content += `No tasks were generated in this session.\n`; } else { tasks.forEach((task, index) => { content += `## Task ${index + 1}: ${task.title}\n\n`; content += `- **ID:** ${task.id}\n`; content += `- **Type:** ${task.type}\n`; content += `- **Priority:** ${task.priority}\n`; content += `- **Status:** ${task.status}\n`; content += `- **Estimated Hours:** ${task.estimatedHours || 0}h\n`; content += `- **Epic ID:** ${task.epicId || 'N/A'}\n\n`; content += `**Description:**\n${task.description}\n\n`; if (task.acceptanceCriteria.length > 0) { content += `**Acceptance Criteria:**\n`; task.acceptanceCriteria.forEach((criteria, i) => { content += `${i + 1}. ${criteria}\n`; }); content += `\n`; } if (task.filePaths.length > 0) { content += `**File Paths:**\n`; task.filePaths.forEach(filePath => { content += `- ${filePath}\n`; }); content += `\n`; } if (task.dependencies.length > 0) { content += `**Dependencies:**\n`; task.dependencies.forEach(dep => { content += `- ${dep}\n`; }); content += `\n`; } if (task.tags.length > 0) { content += `**Tags:** ${task.tags.join(', ')}\n\n`; } content += `---\n\n`; }); } const filePath = path.join(outputDir, 'task-breakdown.md'); await FileUtils.writeFile(filePath, content); return filePath; } /** * Generate performance metrics file */ private async generatePerformanceMetrics(session: DecompositionSession, outputDir: string): Promise<string> { const duration = session.endTime ? session.endTime.getTime() - session.startTime.getTime() : Date.now() - session.startTime.getTime(); const tasks = session.persistedTasks || []; const totalHours = tasks.reduce((sum, task) => sum + (task.estimatedHours || 0), 0); let content = `# Performance Metrics\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `## Timing Metrics\n\n`; content += `- **Total Duration:** ${Math.round(duration / 1000)}s (${(duration / 60000).toFixed(2)} minutes)\n`; content += `- **Start Time:** ${session.startTime.toISOString()}\n`; content += `- **End Time:** ${session.endTime?.toISOString() || 'In Progress'}\n`; content += `- **Progress:** ${session.progress}%\n\n`; content += `## Decomposition Metrics\n\n`; content += `- **Max Depth:** ${session.maxDepth}\n`; content += `- **Current Depth:** ${session.currentDepth}\n`; content += `- **Total Tasks Processed:** ${session.processedTasks}\n`; content += `- **Tasks Generated:** ${tasks.length}\n`; content += `- **Total Estimated Work:** ${totalHours.toFixed(1)} hours\n\n`; content += `## Efficiency Metrics\n\n`; if (duration > 0) { const tasksPerSecond = tasks.length / (duration / 1000); const hoursPerSecond = totalHours / (duration / 1000); content += `- **Tasks Generated per Second:** ${tasksPerSecond.toFixed(3)}\n`; content += `- **Work Hours Planned per Second:** ${hoursPerSecond.toFixed(3)}\n`; content += `- **Average Task Generation Time:** ${(duration / tasks.length / 1000).toFixed(2)}s per task\n\n`; } if (session.results.length > 0) { content += `## Decomposition Results\n\n`; session.results.forEach((result, index) => { content += `### Result ${index + 1}\n`; content += `- **Success:** ${result.success}\n`; content += `- **Is Atomic:** ${result.isAtomic}\n`; content += `- **Depth:** ${result.depth}\n`; content += `- **Sub-tasks:** ${result.subTasks.length}\n`; if (result.error) { content += `- **Error:** ${result.error}\n`; } content += `\n`; }); } const filePath = path.join(outputDir, 'performance-metrics.md'); await FileUtils.writeFile(filePath, content); return filePath; } /** * Generate dependency analysis file */ private async generateDependencyAnalysis(session: DecompositionSession, outputDir: string): Promise<string> { const tasks = session.persistedTasks || []; let content = `# Dependency Analysis\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; // Analyze dependencies const dependencyMap = new Map<string, string[]>(); const dependentMap = new Map<string, string[]>(); tasks.forEach(task => { dependencyMap.set(task.id, task.dependencies); task.dependencies.forEach(depId => { if (!dependentMap.has(depId)) { dependentMap.set(depId, []); } dependentMap.get(depId)!.push(task.id); }); }); const totalDependencies = Array.from(dependencyMap.values()).flat().length; const tasksWithDependencies = Array.from(dependencyMap.values()).filter(deps => deps.length > 0).length; const orphanedTasks = tasks.filter(task => task.dependencies.length === 0 && (!dependentMap.has(task.id) || dependentMap.get(task.id)!.length === 0) ); content += `## Overview\n\n`; content += `- **Total Tasks:** ${tasks.length}\n`; content += `- **Total Dependencies:** ${totalDependencies}\n`; content += `- **Tasks with Dependencies:** ${tasksWithDependencies}\n`; content += `- **Orphaned Tasks:** ${orphanedTasks.length}\n\n`; if (orphanedTasks.length > 0) { content += `## Orphaned Tasks (No Dependencies)\n\n`; orphanedTasks.forEach(task => { content += `- **${task.title}** (${task.id})\n`; }); content += `\n`; } content += `## Task Dependencies\n\n`; tasks.forEach(task => { if (task.dependencies.length > 0) { content += `### ${task.title} (${task.id})\n`; content += `**Depends on:**\n`; task.dependencies.forEach(depId => { const depTask = tasks.find(t => t.id === depId); content += `- ${depTask?.title || depId} (${depId})\n`; }); content += `\n`; } }); const filePath = path.join(outputDir, 'dependency-analysis.md'); await FileUtils.writeFile(filePath, content); return filePath; } /** * Generate visual diagrams (Mermaid) */ private async generateVisualDiagrams(session: DecompositionSession, outputDir: string): Promise<string[]> { const tasks = session.persistedTasks || []; const files: string[] = []; // Generate task flow diagram const taskFlowDiagram = this.generateTaskFlowDiagram(tasks, session); const taskFlowFile = path.join(outputDir, 'task-flow-diagram.md'); await FileUtils.writeFile(taskFlowFile, taskFlowDiagram); files.push(taskFlowFile); // Generate dependency diagram if (tasks.some(task => task.dependencies.length > 0)) { const dependencyDiagram = this.generateDependencyDiagram(tasks, session); const dependencyFile = path.join(outputDir, 'dependency-diagram.md'); await FileUtils.writeFile(dependencyFile, dependencyDiagram); files.push(dependencyFile); } return files; } /** * Generate JSON exports */ private async generateJsonExports( session: DecompositionSession, analysis: TaskAnalysisSummary, outputDir: string ): Promise<string[]> { const files: string[] = []; // Export session data const sessionData = { session: { id: session.id, projectId: session.projectId, status: session.status, startTime: session.startTime, endTime: session.endTime, progress: session.progress, maxDepth: session.maxDepth, currentDepth: session.currentDepth, totalTasks: session.totalTasks, processedTasks: session.processedTasks, error: session.error }, analysis, tasks: session.persistedTasks || [], results: session.results, richResults: session.richResults }; const sessionFile = path.join(outputDir, 'session-data.json'); await FileUtils.writeFile(sessionFile, JSON.stringify(sessionData, null, 2)); files.push(sessionFile); // Export tasks only if (session.persistedTasks && session.persistedTasks.length > 0) { const tasksFile = path.join(outputDir, 'tasks.json'); await FileUtils.writeFile(tasksFile, JSON.stringify(session.persistedTasks, null, 2)); files.push(tasksFile); } // Export analysis summary const analysisFile = path.join(outputDir, 'analysis-summary.json'); await FileUtils.writeFile(analysisFile, JSON.stringify(analysis, null, 2)); files.push(analysisFile); return files; } /** * Generate task flow Mermaid diagram */ private generateTaskFlowDiagram(tasks: AtomicTask[], session: DecompositionSession): string { let content = `# Task Flow Diagram\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `\`\`\`mermaid\n`; content += `graph TD\n`; content += ` Start([Decomposition Started])\n`; if (tasks.length === 0) { content += ` Start --> NoTasks[No Tasks Generated]\n`; } else { // Group tasks by type for better visualization const tasksByType = tasks.reduce((acc, task) => { if (!acc[task.type]) acc[task.type] = []; acc[task.type].push(task); return acc; }, {} as Record<string, AtomicTask[]>); content += ` Start --> Decomp[Task Decomposition]\n`; Object.entries(tasksByType).forEach(([type, typeTasks]) => { const typeNode = `Type_${type.replace(/[^a-zA-Z0-9]/g, '_')}`; content += ` Decomp --> ${typeNode}[${type} Tasks: ${typeTasks.length}]\n`; typeTasks.slice(0, 5).forEach((task) => { // Limit to 5 tasks per type for readability const taskNode = `Task_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; const taskTitle = task.title.length > 30 ? task.title.substring(0, 30) + '...' : task.title; content += ` ${typeNode} --> ${taskNode}["${taskTitle}<br/>${task.estimatedHours}h"]\n`; }); if (typeTasks.length > 5) { content += ` ${typeNode} --> More_${typeNode}[... ${typeTasks.length - 5} more tasks]\n`; } }); content += ` Decomp --> Complete([Decomposition Complete])\n`; } content += `\`\`\`\n\n`; content += `## Legend\n\n`; content += `- **Rectangles**: Task groups by type\n`; content += `- **Rounded rectangles**: Individual tasks with estimated hours\n`; content += `- **Circles**: Process start/end points\n`; return content; } /** * Generate dependency Mermaid diagram */ private generateDependencyDiagram(tasks: AtomicTask[], session: DecompositionSession): string { let content = `# Dependency Diagram\n\n`; content += `**Session:** ${session.id}\n`; content += `**Project:** ${session.projectId}\n\n`; content += `\`\`\`mermaid\n`; content += `graph LR\n`; // Create nodes for all tasks tasks.forEach(task => { const nodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; const taskTitle = task.title.length > 20 ? task.title.substring(0, 20) + '...' : task.title; // Priority color not used in this implementation content += ` ${nodeId}["${taskTitle}<br/>${task.estimatedHours}h"]:::${task.priority}\n`; }); // Add dependency relationships tasks.forEach(task => { if (task.dependencies.length > 0) { const taskNodeId = `T_${task.id.replace(/[^a-zA-Z0-9]/g, '_')}`; task.dependencies.forEach(depId => { const depNodeId = `T_${depId.replace(/[^a-zA-Z0-9]/g, '_')}`; content += ` ${depNodeId} --> ${taskNodeId}\n`; }); } }); // Add styling content += ` classDef high fill:#ffcccc,stroke:#ff0000,stroke-width:2px\n`; content += ` classDef medium fill:#ffffcc,stroke:#ffaa00,stroke-width:2px\n`; content += ` classDef low fill:#ccffcc,stroke:#00aa00,stroke-width:2px\n`; content += `\`\`\`\n\n`; content += `## Legend\n\n`; content += `- **Red**: High priority tasks\n`; content += `- **Yellow**: Medium priority tasks\n`; content += `- **Green**: Low priority tasks\n`; content += `- **Arrows**: Dependency relationships (from dependency to dependent)\n`; return content; } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/freshtechbro/vibe-coder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server