UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,086 lines 44.7 kB
import { randomUUID } from 'crypto'; import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js'; import { renderADRTemplate } from './templates.js'; /** * Generate next ADR number */ async function getNextADRNumber(context) { const result = await context.db.get('SELECT MAX(CAST(SUBSTR(id, 5) AS INTEGER)) as max_num FROM adr_records WHERE project_id = ?', [context.projectId || 'default']); const nextNum = (result.data?.max_num || 0) + 1; return nextNum.toString().padStart(4, '0'); } /** * Create a new Architecture Decision Record */ const createADRTool = createTool({ name: 'create_adr', description: 'Create a new Architecture Decision Record', category: 'adr', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Title of the decision', minLength: 1, maxLength: 200 }, template: { type: 'string', enum: ['nygard', 'madr', 'y-statement'], default: 'nygard', description: 'ADR template to use' }, deciders: { type: 'array', items: { type: 'string', minLength: 1, maxLength: 100 }, description: 'People involved in the decision', minItems: 1, maxItems: 20 }, context: { type: 'string', description: 'Context and problem statement', minLength: 1, maxLength: 5000 }, decision: { type: 'string', description: 'The decision that was made', minLength: 1, maxLength: 5000 }, consequences: { type: 'string', description: 'Positive and negative consequences', minLength: 1, maxLength: 5000 }, tags: { type: 'array', items: { type: 'string', maxLength: 50 }, description: 'Optional tags for categorization', maxItems: 20 }, decisionDrivers: { type: 'array', items: { type: 'string' }, description: 'Key factors that influenced the decision (MADR template)', maxItems: 20 }, consideredOptions: { type: 'array', items: { type: 'object', properties: { title: { type: 'string', maxLength: 200 }, description: { type: 'string', maxLength: 1000 }, pros: { type: 'array', items: { type: 'string' } }, cons: { type: 'array', items: { type: 'string' } } }, required: ['title', 'description', 'pros', 'cons'] }, description: 'Alternative options that were considered (MADR template)', maxItems: 10 }, supersedes: { type: 'array', items: { type: 'string', pattern: '^ADR-\\d{4}$' }, description: 'ADR IDs that this decision supersedes', maxItems: 10 }, relatedTo: { type: 'array', items: { type: 'string', pattern: '^ADR-\\d{4}$' }, description: 'ADR IDs that this decision relates to', maxItems: 20 } }, required: ['title', 'deciders', 'context', 'decision', 'consequences'], additionalProperties: false }, async execute(input, context) { try { const adrNumber = await getNextADRNumber(context); const adrId = `ADR-${adrNumber}`; const now = Date.now(); // Validate superseded ADRs exist if (input.supersedes) { for (const supersededId of input.supersedes) { const existsResult = await context.db.get('SELECT id FROM adr_records WHERE id = ? AND project_id = ?', [supersededId, context.projectId || 'default']); if (!existsResult.success || !existsResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Referenced ADR ${supersededId} not found`, category: 'validation' }); } } } // Validate related ADRs exist if (input.relatedTo) { for (const relatedId of input.relatedTo) { const existsResult = await context.db.get('SELECT id FROM adr_records WHERE id = ? AND project_id = ?', [relatedId, context.projectId || 'default']); if (!existsResult.success || !existsResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Referenced ADR ${relatedId} not found`, category: 'validation' }); } } } // Create ADR record in database const result = await context.db.run(`INSERT INTO adr_records (id, title, status, date, deciders, template, context, decision, consequences, tags, decision_drivers, considered_options, pros_and_cons, supersedes, superseded_by, related_to, project_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ adrId, input.title, 'proposed', now, JSON.stringify(input.deciders), input.template || 'nygard', input.context, input.decision, input.consequences, JSON.stringify(input.tags || []), JSON.stringify(input.decisionDrivers || []), JSON.stringify(input.consideredOptions || []), JSON.stringify(input.prosAndCons || []), JSON.stringify(input.supersedes || []), null, JSON.stringify(input.relatedTo || []), context.projectId || 'default', now, now ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to create ADR', details: { error: result.error }, category: 'system' }); } // Update superseded ADRs if (input.supersedes) { for (const supersededId of input.supersedes) { await context.db.run('UPDATE adr_records SET status = ?, superseded_by = ?, updated_at = ? WHERE id = ? AND project_id = ?', ['superseded', adrId, now, supersededId, context.projectId || 'default']); } } // Create status history entry await context.db.run(`INSERT INTO adr_status_history (id, adr_id, from_status, to_status, changed_at, changed_by, reason, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [randomUUID(), adrId, null, 'proposed', now, context.userId || 'system', 'Initial creation', context.projectId || 'default']); // Build ADR object for template rendering const adr = { id: adrId, title: input.title, status: 'proposed', date: new Date(now), deciders: input.deciders, template: input.template || 'nygard', context: input.context, decision: input.decision, consequences: input.consequences, tags: input.tags, decisionDrivers: input.decisionDrivers, consideredOptions: input.consideredOptions, prosAndCons: input.prosAndCons, supersedes: input.supersedes, relatedTo: input.relatedTo, createdAt: new Date(now), updatedAt: new Date(now), statusHistory: [] }; const markdown = renderADRTemplate(adr); return createSuccessResult({ adr: { id: adrId, title: input.title, status: 'proposed', template: input.template || 'nygard', deciders: input.deciders, createdAt: new Date(now).toISOString() }, markdown, message: `ADR ${adrId} "${input.title}" created successfully`, nextSteps: [ 'Review the decision with stakeholders', 'Update status when decision is finalized', 'Link to related ADRs if needed' ] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to create ADR: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Update an existing ADR */ const updateADRTool = createTool({ name: 'update_adr', description: 'Update an existing Architecture Decision Record', category: 'adr', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'ADR ID (e.g., ADR-0001)', pattern: '^ADR-\\d{4}$' }, status: { type: 'string', enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'], description: 'New status' }, statusChangeReason: { type: 'string', description: 'Reason for status change', maxLength: 1000 }, statusChangedBy: { type: 'string', description: 'Person making the status change', maxLength: 100 }, title: { type: 'string', description: 'Updated title', maxLength: 200 }, consequences: { type: 'string', description: 'Updated consequences', maxLength: 5000 }, tags: { type: 'array', items: { type: 'string', maxLength: 50 }, description: 'Updated tags', maxItems: 20 } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { // Get existing ADR const adrResult = await context.db.get('SELECT * FROM adr_records WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!adrResult.success || !adrResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `ADR ${input.id} not found`, category: 'validation' }); } const existingADR = adrResult.data; const now = Date.now(); const updates = []; const values = []; // Build update query dynamically if (input.title !== undefined) { updates.push('title = ?'); values.push(input.title); } if (input.consequences !== undefined) { updates.push('consequences = ?'); values.push(input.consequences); } if (input.tags !== undefined) { updates.push('tags = ?'); values.push(JSON.stringify(input.tags)); } if (input.status !== undefined && input.status !== existingADR.status) { updates.push('status = ?'); values.push(input.status); // Record status change await context.db.run(`INSERT INTO adr_status_history (id, adr_id, from_status, to_status, changed_at, changed_by, reason, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ randomUUID(), input.id, existingADR.status, input.status, now, input.statusChangedBy || context.userId || 'unknown', input.statusChangeReason || null, context.projectId || 'default' ]); } if (updates.length > 0) { updates.push('updated_at = ?'); values.push(now); values.push(input.id); values.push(context.projectId || 'default'); const updateResult = await context.db.run(`UPDATE adr_records SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`, values); if (!updateResult.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to update ADR', details: { error: updateResult.error }, category: 'system' }); } } // Get updated ADR with status history const updatedResult = await context.db.get('SELECT * FROM adr_records WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); const historyResult = await context.db.query('SELECT * FROM adr_status_history WHERE adr_id = ? AND project_id = ? ORDER BY changed_at', [input.id, context.projectId || 'default']); const statusHistory = (historyResult.data || []).map((h) => ({ from: h.from_status, to: h.to_status, date: new Date(h.changed_at).toISOString(), changedBy: h.changed_by, reason: h.reason })); return createSuccessResult({ adr: { id: input.id, title: updatedResult.data?.title, status: updatedResult.data?.status, updatedAt: new Date(updatedResult.data?.updated_at || now).toISOString() }, statusHistory, message: `ADR ${input.id} updated successfully`, changesApplied: updates.length > 0 ? updates.map(u => u.split(' = ')[0]) : ['No changes'] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to update ADR: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get a specific ADR */ const getADRTool = createTool({ name: 'get_adr', description: 'Get details of a specific Architecture Decision Record', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'ADR ID (e.g., ADR-0001)', pattern: '^ADR-\\d{4}$' } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { // Get ADR details const adrResult = await context.db.get('SELECT * FROM adr_records WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!adrResult.success || !adrResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `ADR ${input.id} not found`, category: 'validation' }); } const adr = adrResult.data; // Get status history const historyResult = await context.db.query('SELECT * FROM adr_status_history WHERE adr_id = ? AND project_id = ? ORDER BY changed_at', [input.id, context.projectId || 'default']); // Build full ADR object for template rendering const adrObject = { id: adr.id, title: adr.title, status: adr.status, date: new Date(adr.date), deciders: JSON.parse(adr.deciders || '[]'), template: adr.template, context: adr.context, decision: adr.decision, consequences: adr.consequences, tags: JSON.parse(adr.tags || '[]'), decisionDrivers: JSON.parse(adr.decision_drivers || '[]'), consideredOptions: JSON.parse(adr.considered_options || '[]'), prosAndCons: JSON.parse(adr.pros_and_cons || '[]'), supersedes: JSON.parse(adr.supersedes || '[]'), supersededBy: adr.superseded_by, relatedTo: JSON.parse(adr.related_to || '[]'), createdAt: new Date(adr.created_at), updatedAt: new Date(adr.updated_at), statusHistory: (historyResult.data || []).map((h) => ({ from: h.from_status, to: h.to_status, date: new Date(h.changed_at), changedBy: h.changed_by, reason: h.reason })) }; const markdown = renderADRTemplate(adrObject); // Build relationships const relationships = []; if (adrObject.relatedTo) { for (const relatedId of adrObject.relatedTo) { relationships.push({ type: 'related-to', from: input.id, to: relatedId }); } } if (adrObject.supersedes) { for (const supersededId of adrObject.supersedes) { relationships.push({ type: 'supersedes', from: input.id, to: supersededId }); } } if (adrObject.supersededBy) { relationships.push({ type: 'superseded-by', from: input.id, to: adrObject.supersededBy }); } return createSuccessResult({ adr: adrObject, markdown, relationships, statusHistory: adrObject.statusHistory }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to get ADR: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * List all ADRs with optional filtering */ const listADRsTool = createTool({ name: 'list_adrs', description: 'List all Architecture Decision Records with optional status filter', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'], description: 'Filter by status' } }, required: [], additionalProperties: false }, async execute(input, context) { try { let query = 'SELECT id, title, status, date, deciders, tags, created_at FROM adr_records WHERE project_id = ?'; const params = [context.projectId || 'default']; if (input.status) { query += ' AND status = ?'; params.push(input.status); } query += ' ORDER BY date DESC'; const result = await context.db.query(query, params); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to list ADRs', details: { error: result.error }, category: 'system' }); } const adrs = (result.data || []).map((adr) => ({ id: adr.id, title: adr.title, status: adr.status, date: new Date(adr.date).toISOString(), deciders: JSON.parse(adr.deciders || '[]'), tags: JSON.parse(adr.tags || '[]'), createdAt: new Date(adr.created_at).toISOString() })); return createSuccessResult({ adrs, count: adrs.length, filter: input.status ? { status: input.status } : null }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to list ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Delete an ADR */ const deleteADRTool = createTool({ name: 'delete_adr', description: 'Delete an Architecture Decision Record', category: 'adr', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'ADR ID to delete', pattern: '^ADR-\\d{4}$' } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { // Check if ADR exists const existsResult = await context.db.get('SELECT id, title FROM adr_records WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!existsResult.success || !existsResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `ADR ${input.id} not found`, category: 'validation' }); } const adr = existsResult.data; // Delete in transaction to ensure consistency await context.db.transaction(async (tx) => { // Delete status history await tx.run('DELETE FROM adr_status_history WHERE adr_id = ? AND project_id = ?', [input.id, context.projectId || 'default']); // Delete the ADR await tx.run('DELETE FROM adr_records WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); // Update any ADRs that reference this one await tx.run('UPDATE adr_records SET superseded_by = NULL WHERE superseded_by = ? AND project_id = ?', [input.id, context.projectId || 'default']); }); return createSuccessResult({ message: `ADR ${input.id} "${adr.title}" deleted successfully`, deletedADR: { id: input.id, title: adr.title } }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to delete ADR: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get ADR metrics and statistics */ const getADRMetricsTool = createTool({ name: 'adr_metrics', description: 'Get metrics and statistics about Architecture Decision Records', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false }, async execute(input, context) { try { // Get total count and status breakdown const statusResult = await context.db.query('SELECT status, COUNT(*) as count FROM adr_records WHERE project_id = ? GROUP BY status', [context.projectId || 'default']); // Get template breakdown const templateResult = await context.db.query('SELECT template, COUNT(*) as count FROM adr_records WHERE project_id = ? GROUP BY template', [context.projectId || 'default']); // Get total count const totalResult = await context.db.get('SELECT COUNT(*) as total FROM adr_records WHERE project_id = ?', [context.projectId || 'default']); // Get most active deciders const decidersResult = await context.db.query('SELECT deciders FROM adr_records WHERE project_id = ?', [context.projectId || 'default']); // Process deciders data const deciderCounts = new Map(); (decidersResult.data || []).forEach((row) => { const deciders = JSON.parse(row.deciders || '[]'); deciders.forEach((decider) => { deciderCounts.set(decider, (deciderCounts.get(decider) || 0) + 1); }); }); const mostActiveDeciders = Array.from(deciderCounts.entries()) .map(([name, count]) => ({ name, count })) .sort((a, b) => b.count - a.count) .slice(0, 10); // Calculate average decision time for accepted ADRs const decisionTimeResult = await context.db.query(`SELECT adr.created_at, h.changed_at FROM adr_records adr JOIN adr_status_history h ON adr.id = h.adr_id WHERE adr.project_id = ? AND h.to_status = 'accepted'`, [context.projectId || 'default']); let averageDecisionTime = 0; if (decisionTimeResult.data && decisionTimeResult.data.length > 0) { const decisionTimes = decisionTimeResult.data.map((row) => { return (row.changed_at - row.created_at) / (1000 * 60 * 60 * 24); // Convert to days }); averageDecisionTime = decisionTimes.reduce((a, b) => a + b, 0) / decisionTimes.length; } // Build status breakdown const statusBreakdown = { proposed: 0, accepted: 0, rejected: 0, deprecated: 0, superseded: 0 }; (statusResult.data || []).forEach((row) => { statusBreakdown[row.status] = row.count; }); // Build template breakdown const templateBreakdown = { nygard: 0, madr: 0, 'y-statement': 0 }; (templateResult.data || []).forEach((row) => { templateBreakdown[row.template] = row.count; }); return createSuccessResult({ metrics: { total: totalResult.data?.total || 0, byStatus: statusBreakdown, byTemplate: templateBreakdown, averageDecisionTime: Math.round(averageDecisionTime * 100) / 100, mostActiveDeciders }, insights: [ `Total ${totalResult.data?.total || 0} ADRs in the system`, statusBreakdown.accepted > 0 ? `${statusBreakdown.accepted} decisions have been accepted` : 'No accepted decisions yet', averageDecisionTime > 0 ? `Average decision time: ${Math.round(averageDecisionTime * 100) / 100} days` : 'No decision time data available', mostActiveDeciders.length > 0 ? `Most active decider: ${mostActiveDeciders[0].name} (${mostActiveDeciders[0].count} decisions)` : 'No decision makers identified' ] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to get ADR metrics: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Create ADR interactively (placeholder - uses wizard) */ const createADRInteractiveTool = createTool({ name: 'create_adr_interactive', description: 'Start interactive ADR creation process', category: 'adr', inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false }, async execute(input, context) { return createSuccessResult({ message: 'Interactive ADR creation started', instructions: [ 'Use create_adr tool with required fields:', '- title: Brief title of the decision', '- deciders: Array of decision makers', '- context: Situation context', '- decision: The decision made', '- consequences: Expected outcomes' ], nextStep: 'call_create_adr' }); } }); /** * Link ADRs with relationships */ const linkADRsTool = createTool({ name: 'link_adrs', description: 'Create relationships between ADRs', category: 'adr', inputSchema: { type: 'object', properties: { sourceId: { type: 'string', description: 'Source ADR ID', pattern: '^[a-zA-Z0-9-]+$' }, targetId: { type: 'string', description: 'Target ADR ID', pattern: '^[a-zA-Z0-9-]+$' }, relationship: { type: 'string', enum: ['supersedes', 'relates_to', 'amends'], description: 'Type of relationship' } }, required: ['sourceId', 'targetId', 'relationship'], additionalProperties: false }, async execute(input, context) { try { // Verify both ADRs exist const sourceResult = await context.db.get('SELECT * FROM adr_records WHERE id = ? AND project_id = ?', [input.sourceId, context.projectId || 'default']); const targetResult = await context.db.get('SELECT * FROM adr_records WHERE id = ? AND project_id = ?', [input.targetId, context.projectId || 'default']); if (!sourceResult.success || !sourceResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Source ADR not found', details: { sourceId: input.sourceId }, category: 'validation' }); } if (!targetResult.success || !targetResult.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Target ADR not found', details: { targetId: input.targetId }, category: 'validation' }); } // Update relationships based on type const now = new Date().toISOString(); if (input.relationship === 'supersedes') { // Update source ADR to include supersedes const sourceSupersedes = JSON.parse(sourceResult.data.supersedes || '[]'); if (!sourceSupersedes.includes(input.targetId)) { sourceSupersedes.push(input.targetId); await context.db.run('UPDATE adr_records SET supersedes = ?, updated_at = ? WHERE id = ?', [JSON.stringify(sourceSupersedes), now, input.sourceId]); } // Update target ADR to be superseded by source await context.db.run('UPDATE adr_records SET superseded_by = ?, status = ?, updated_at = ? WHERE id = ?', [input.sourceId, 'superseded', now, input.targetId]); } else { // For other relationships, update related_to field const sourceRelated = JSON.parse(sourceResult.data.related_to || '[]'); if (!sourceRelated.includes(input.targetId)) { sourceRelated.push(input.targetId); await context.db.run('UPDATE adr_records SET related_to = ?, updated_at = ? WHERE id = ?', [JSON.stringify(sourceRelated), now, input.sourceId]); } } return createSuccessResult({ linked: true, relationship: input.relationship, source: sourceResult.data, target: targetResult.data }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to link ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Search ADRs */ const searchADRsTool = createTool({ name: 'search_adrs', description: 'Search ADRs by content, date range, or status', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query for title, context, or decision content', maxLength: 500 }, startDate: { type: 'string', format: 'date', description: 'Start date for search range' }, endDate: { type: 'string', format: 'date', description: 'End date for search range' }, status: { type: 'string', enum: ['proposed', 'accepted', 'rejected', 'deprecated', 'superseded'], description: 'Filter by ADR status' } }, additionalProperties: false }, async execute(input, context) { try { let sql = 'SELECT * FROM adr_records WHERE project_id = ?'; const params = [context.projectId || 'default']; // Add text search if (input.query) { sql += ' AND (title LIKE ? OR context LIKE ? OR decision LIKE ?)'; const searchTerm = `%${input.query}%`; params.push(searchTerm, searchTerm, searchTerm); } // Add date range if (input.startDate) { sql += ' AND created_at >= ?'; params.push(input.startDate); } if (input.endDate) { sql += ' AND created_at <= ?'; params.push(input.endDate + 'T23:59:59Z'); } // Add status filter if (input.status) { sql += ' AND status = ?'; params.push(input.status); } sql += ' ORDER BY created_at DESC'; const result = await context.db.query(sql, params); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to search ADRs', details: { error: result.error }, category: 'system' }); } const adrs = (result.data || []).map((row) => ({ id: row.id, number: row.number, title: row.title, status: row.status, deciders: JSON.parse(row.deciders || '[]'), template: row.template, context: row.context, decision: row.decision, consequences: row.consequences, tags: JSON.parse(row.tags || '[]'), createdAt: new Date(row.created_at).toISOString(), updatedAt: new Date(row.updated_at).toISOString() })); return createSuccessResult({ adrs, count: adrs.length, searchCriteria: { query: input.query || null, startDate: input.startDate || null, endDate: input.endDate || null, status: input.status || null } }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to search ADRs: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Validate ADR references */ const validateADRReferencesTool = createTool({ name: 'validate_adr_references', description: 'Validate all ADR cross-references and relationships', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: {}, required: [], additionalProperties: false }, async execute(input, context) { try { // Get all ADRs const result = await context.db.query('SELECT * FROM adr_records WHERE project_id = ?', [context.projectId || 'default']); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to fetch ADRs for validation', category: 'system' }); } const adrs = result.data || []; const adrIds = new Set(adrs.map((adr) => adr.id)); const brokenReferences = []; const validReferences = []; // Check all references adrs.forEach((adr) => { const supersedes = JSON.parse(adr.supersedes || '[]'); const relatedTo = JSON.parse(adr.related_to || '[]'); // Check supersedes references supersedes.forEach((refId) => { if (!adrIds.has(refId)) { brokenReferences.push(`ADR ${adr.number} (${adr.id}) supersedes non-existent ADR ${refId}`); } else { validReferences.push(`ADR ${adr.number} supersedes reference is valid`); } }); // Check related_to references relatedTo.forEach((refId) => { if (!adrIds.has(refId)) { brokenReferences.push(`ADR ${adr.number} (${adr.id}) relates to non-existent ADR ${refId}`); } else { validReferences.push(`ADR ${adr.number} related_to reference is valid`); } }); // Check superseded_by references if (adr.superseded_by && !adrIds.has(adr.superseded_by)) { brokenReferences.push(`ADR ${adr.number} (${adr.id}) superseded by non-existent ADR ${adr.superseded_by}`); } else if (adr.superseded_by) { validReferences.push(`ADR ${adr.number} superseded_by reference is valid`); } }); const isValid = brokenReferences.length === 0; return createSuccessResult({ valid: isValid, totalReferences: validReferences.length + brokenReferences.length, validReferences: validReferences.length, brokenReferences: brokenReferences.length, issues: brokenReferences, summary: isValid ? 'All ADR references are valid' : `Found ${brokenReferences.length} broken reference(s)` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to validate ADR references: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Generate ADR decision log */ const generateADRLogTool = createTool({ name: 'generate_adr_log', description: 'Generate a comprehensive log of all architectural decisions', category: 'adr', readOnly: true, inputSchema: { type: 'object', properties: { format: { type: 'string', enum: ['markdown', 'json', 'csv'], default: 'markdown', description: 'Output format for the decision log' } }, additionalProperties: false }, async execute(input, context) { try { const result = await context.db.query('SELECT * FROM adr_records WHERE project_id = ? ORDER BY number ASC', [context.projectId || 'default']); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to fetch ADRs for log generation', category: 'system' }); } const adrs = result.data || []; const format = input.format || 'markdown'; if (format === 'json') { return createSuccessResult({ format: 'json', log: adrs.map((adr) => ({ number: adr.number, title: adr.title, status: adr.status, deciders: JSON.parse(adr.deciders || '[]'), template: adr.template, context: adr.context, decision: adr.decision, consequences: adr.consequences, tags: JSON.parse(adr.tags || '[]'), supersedes: JSON.parse(adr.supersedes || '[]'), supersededBy: adr.superseded_by, relatedTo: JSON.parse(adr.related_to || '[]'), createdAt: adr.created_at, updatedAt: adr.updated_at })) }); } if (format === 'csv') { const headers = 'Number,Title,Status,Deciders,Template,Created,Updated'; const rows = adrs.map((adr) => { const deciders = JSON.parse(adr.deciders || '[]').join(';'); return `${adr.number},"${adr.title}",${adr.status},"${deciders}",${adr.template},${adr.created_at},${adr.updated_at}`; }); return createSuccessResult({ format: 'csv', log: [headers, ...rows].join('\n') }); } // Default markdown format const markdownLog = [ '# Architecture Decision Log', '', `This log lists the architectural decisions for the project.`, '', '<!-- adrlog -->', '', ...adrs.map((adr) => { const deciders = JSON.parse(adr.deciders || '[]').join(', '); return `* [ADR-${adr.number}](adr-${adr.number}.md) - ${adr.title} - **${adr.status}** (${deciders})`; }), '', '<!-- adrlogstop -->', '', `_Generated on ${new Date().toISOString()}_` ].join('\n'); return createSuccessResult({ format: 'markdown', log: markdownLog, count: adrs.length }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to generate ADR log: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Setup ADR tools */ export async function setupADRTools() { return { module: 'adr-management', tools: [ createADRTool, createADRInteractiveTool, updateADRTool, linkADRsTool, getADRTool, listADRsTool, searchADRsTool, deleteADRTool, getADRMetricsTool, validateADRReferencesTool, generateADRLogTool ] }; } //# sourceMappingURL=tools.js.map