UNPKG

mcp-adr-analysis-server

Version:

MCP server for analyzing Architectural Decision Records and project architecture

362 lines 13.1 kB
/** * TODO JSON ↔ Markdown Converter * * Converts between structured JSON format and human-readable markdown, * preserving all metadata and enabling seamless bidirectional sync. */ import { ProjectHealthScoring } from './project-health-scoring.js'; import { loadConfig } from './config.js'; import crypto from 'crypto'; /** * Convert JSON TODO data to markdown format */ export async function generateTodoMarkdown(data) { const config = loadConfig(); const healthScoring = new ProjectHealthScoring(config.projectPath); // Generate health dashboard header const healthDashboard = await healthScoring.generateScoreDisplay(); // Calculate progress metrics const metrics = calculateProgressMetrics(data); let markdown = healthDashboard; // Add TODO overview markdown += `\n## 📋 TODO Overview\n\n`; markdown += `**Progress**: ${metrics.completedTasks}/${metrics.totalTasks} tasks completed (${metrics.completionPercentage.toFixed(1)}%)\n`; markdown += `**Priority Score**: ${metrics.priorityWeightedScore.toFixed(1)}% (weighted by priority)\n`; markdown += `**Critical Remaining**: ${metrics.criticalRemaining} critical tasks\n`; markdown += `**Blocked**: ${metrics.blockedTasks} tasks blocked\n\n`; // Add velocity metrics if available if (metrics.velocity.tasksCompletedLastWeek > 0) { markdown += `**Velocity**: ${metrics.velocity.tasksCompletedLastWeek} tasks/week, avg ${metrics.velocity.averageCompletionTime.toFixed(1)}h completion time\n\n`; } // Add sections const sortedSections = data.sections.sort((a, b) => a.order - b.order); for (const section of sortedSections) { if (section.tasks.length === 0) continue; markdown += `## ${getEmojiForSection(section.id)} ${section.title}\n\n`; if (section.description) { markdown += `${section.description}\n\n`; } // Group tasks by priority for better organization const sectionTasks = section.tasks .map(taskId => data.tasks[taskId]) .filter(Boolean) .sort((a, b) => { const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 }; return priorityOrder[b.priority] - priorityOrder[a.priority]; }); for (const task of sectionTasks) { if (task) { markdown += formatTaskAsMarkdown(task, data.tasks); } } markdown += '\n'; } // Add metadata footer markdown += `---\n\n`; markdown += `*Last updated: ${new Date(data.metadata.lastUpdated).toLocaleString()}*\n`; markdown += `*Auto-sync: ${data.metadata.autoSyncEnabled ? 'enabled' : 'disabled'}*\n`; markdown += `*Knowledge Graph: ${data.knowledgeGraphSync.linkedIntents.length} linked intents*\n`; return markdown; } /** * Format a single task as markdown */ function formatTaskAsMarkdown(task, allTasks) { const checkbox = task.status === 'completed' ? '[x]' : '[ ]'; const priorityEmoji = getPriorityEmoji(task.priority); const statusEmoji = getStatusEmoji(task.status); let line = `- ${checkbox} ${priorityEmoji} ${statusEmoji} **${task.title}**`; // Add assignee if present if (task.assignee) { line += ` (@${task.assignee})`; } // Add due date if present if (task.dueDate) { const dueDate = new Date(task.dueDate); const isOverdue = dueDate < new Date() && task.status !== 'completed'; const dueDateStr = dueDate.toLocaleDateString(); line += ` ${isOverdue ? '🔴' : '📅'} ${dueDateStr}`; } // Add progress for in-progress tasks if (task.status === 'in_progress' && task.progressPercentage > 0) { line += ` (${task.progressPercentage}%)`; } line += '\n'; // Add description if present if (task.description) { line += ` ${task.description}\n`; } // Add subtasks if (task.subtasks.length > 0) { for (const subtaskId of task.subtasks) { const subtask = allTasks[subtaskId]; if (subtask) { line += ` - ${subtask.status === 'completed' ? '[x]' : '[ ]'} ${subtask.title}\n`; } } } // Add dependencies if (task.dependencies.length > 0) { const depTasks = task.dependencies .map(depId => allTasks[depId]) .filter(Boolean); if (depTasks.length > 0) { line += ` *Depends on: ${depTasks.map(t => t.title).join(', ')}*\n`; } } // Add blocking information if (task.blockedBy.length > 0) { const blockers = task.blockedBy .map(blockerId => allTasks[blockerId]) .filter(Boolean); if (blockers.length > 0) { line += ` *Blocked by: ${blockers.map(t => t.title).join(', ')}*\n`; } } // Add linked ADRs if (task.linkedAdrs.length > 0) { line += ` *ADRs: ${task.linkedAdrs.join(', ')}*\n`; } // Add tags if (task.tags.length > 0) { line += ` *Tags: ${task.tags.map(tag => `#${tag}`).join(' ')}*\n`; } // Add notes if (task.notes) { line += ` *Notes: ${task.notes}*\n`; } return line; } /** * Parse markdown content back to JSON format */ export async function parseMarkdownToJson(markdown) { const lines = markdown.split('\n'); const now = new Date().toISOString(); const data = { version: '1.0.0', metadata: { lastUpdated: now, totalTasks: 0, completedTasks: 0, autoSyncEnabled: true }, tasks: {}, sections: [], scoringSync: { lastScoreUpdate: now, taskCompletionScore: 0, priorityWeightedScore: 0, criticalTasksRemaining: 0, scoreHistory: [] }, knowledgeGraphSync: { lastSync: now, linkedIntents: [], pendingUpdates: [] }, automationRules: [] }; let currentSection = null; let sectionOrder = 1; for (let i = 0; i < lines.length; i++) { const line = lines[i]?.trim() || ''; // Parse section headers if (line.startsWith('## ') && !line.includes('📊') && !line.includes('🎯')) { const sectionTitle = line.replace(/^## /, '').replace(/^[^\s]+ /, ''); // Remove emoji const sectionId = sectionTitle.toLowerCase().replace(/\s+/g, '_'); currentSection = { id: sectionId, title: sectionTitle, order: sectionOrder++, collapsed: false, tasks: [] }; data.sections.push(currentSection); continue; } // Parse task lines if (line.match(/^- \[(x| )\]/)) { if (!currentSection) { // Create default section if none exists currentSection = { id: 'default', title: 'Tasks', order: sectionOrder++, collapsed: false, tasks: [] }; data.sections.push(currentSection); } const task = parseTaskLine(line, lines, i); if (task && currentSection) { data.tasks[task.id] = task; currentSection.tasks.push(task.id); } } } // Update metadata data.metadata.totalTasks = Object.keys(data.tasks).length; data.metadata.completedTasks = Object.values(data.tasks).filter(t => t.status === 'completed').length; return data; } /** * Parse a task line and extract task information */ function parseTaskLine(line, _allLines, _lineIndex) { const taskMatch = line.match(/^- \[(x| )\] (.+)$/); if (!taskMatch) return null; const isCompleted = taskMatch[1] === 'x'; const content = taskMatch[2]; if (!content) return null; // Extract priority from emoji const priority = extractPriorityFromEmoji(content); // Extract title (remove emojis and metadata) const title = content .replace(/^[^\s]+ [^\s]+ \*\*(.+?)\*\*.*/, '$1') .replace(/^[^\s]+ [^\s]+ (.+?) \(.*/, '$1') .replace(/^[^\s]+ [^\s]+ (.+)$/, '$1'); // Extract assignee const assigneeMatch = content.match(/@(\w+)/); const assignee = assigneeMatch ? assigneeMatch[1] : undefined; // Extract due date const dueDateMatch = content.match(/📅 (\d{1,2}\/\d{1,2}\/\d{4})/); const dueDate = dueDateMatch?.[1] ? new Date(dueDateMatch[1]).toISOString() : undefined; // Extract progress const progressMatch = content.match(/\((\d+)%\)/); const progressPercentage = progressMatch?.[1] ? parseInt(progressMatch[1]) : 0; const taskId = crypto.randomUUID(); const now = new Date().toISOString(); return { id: taskId, title, description: '', // Will be extracted from following lines if present status: isCompleted ? 'completed' : 'pending', priority, assignee, createdAt: now, updatedAt: now, completedAt: isCompleted ? now : undefined, dueDate, parentTaskId: undefined, subtasks: [], dependencies: [], blockedBy: [], linkedAdrs: [], adrGeneratedTask: false, intentId: undefined, toolExecutions: [], scoreWeight: 1, scoreCategory: 'task_completion', progressPercentage, tags: [], notes: undefined, lastModifiedBy: 'tool', autoComplete: false, version: 1, changeLog: [{ timestamp: now, action: 'created', details: `Task imported from markdown: ${title}`, modifiedBy: 'tool' }] }; } /** * Extract priority from emoji */ function extractPriorityFromEmoji(content) { if (content.includes('🔴')) return 'critical'; if (content.includes('🟠')) return 'high'; if (content.includes('🟡')) return 'medium'; return 'low'; } /** * Get emoji for task priority */ function getPriorityEmoji(priority) { switch (priority) { case 'critical': return '🔴'; case 'high': return '🟠'; case 'medium': return '🟡'; case 'low': return '🟢'; default: return '⚪'; } } /** * Get emoji for task status */ function getStatusEmoji(status) { switch (status) { case 'completed': return '✅'; case 'in_progress': return '🔄'; case 'blocked': return '🚫'; case 'cancelled': return '❌'; default: return '⏳'; } } /** * Get emoji for section */ function getEmojiForSection(sectionId) { switch (sectionId) { case 'pending': return '📋'; case 'in_progress': return '🔄'; case 'completed': return '✅'; case 'blocked': return '🚫'; case 'cancelled': return '❌'; default: return '📁'; } } /** * Calculate progress metrics from JSON data */ function calculateProgressMetrics(data) { const tasks = Object.values(data.tasks); const totalTasks = tasks.length; const completedTasks = tasks.filter(t => t.status === 'completed').length; const completionPercentage = totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 100; // Priority-weighted scoring const priorityWeights = { low: 1, medium: 2, high: 3, critical: 4 }; const totalWeight = tasks.reduce((sum, t) => sum + priorityWeights[t.priority], 0); const completedWeight = tasks .filter(t => t.status === 'completed') .reduce((sum, t) => sum + priorityWeights[t.priority], 0); const priorityWeightedScore = totalWeight > 0 ? (completedWeight / totalWeight) * 100 : 100; // Other metrics const criticalRemaining = tasks.filter(t => t.priority === 'critical' && t.status !== 'completed').length; const blockedTasks = tasks.filter(t => t.status === 'blocked').length; // Velocity metrics const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); const tasksCompletedLastWeek = tasks.filter(t => t.status === 'completed' && t.completedAt && new Date(t.completedAt) > weekAgo).length; const completedTasksWithDuration = tasks.filter(t => t.status === 'completed' && t.completedAt); const averageCompletionTime = completedTasksWithDuration.length > 0 ? completedTasksWithDuration.reduce((sum, t) => { const duration = (new Date(t.completedAt).getTime() - new Date(t.createdAt).getTime()) / (1000 * 60 * 60); return sum + duration; }, 0) / completedTasksWithDuration.length : 0; return { totalTasks, completedTasks, completionPercentage, priorityWeightedScore, criticalRemaining, blockedTasks, velocity: { tasksCompletedLastWeek, averageCompletionTime } }; } //# sourceMappingURL=todo-markdown-converter.js.map