@boundless-oss/atlas
Version:
Atlas - MCP Server for comprehensive startup project management
261 lines (240 loc) • 6.98 kB
text/typescript
import type { ADR, ADRTemplate, ADRTemplateDefinition } from './types.js';
export function getNygardTemplate(): ADRTemplateDefinition {
return {
name: 'nygard',
description: 'Michael Nygard\'s template - simple and effective for most decisions',
fields: [
'title',
'status',
'date',
'deciders',
'context',
'decision',
'consequences',
'tags',
'relatedTo',
'supersedes',
'supersededBy'
],
requiredFields: [
'title',
'deciders',
'context',
'decision',
'consequences'
],
optionalFields: [
'tags',
'relatedTo',
'supersedes',
'supersededBy'
]
};
}
export function getMADRTemplate(): ADRTemplateDefinition {
return {
name: 'madr',
description: 'Markdown Architectural Decision Records - comprehensive template with structured options',
fields: [
'title',
'status',
'date',
'deciders',
'context',
'decisionDrivers',
'consideredOptions',
'decision',
'consequences',
'prosAndCons',
'links',
'tags',
'relatedTo',
'supersedes',
'supersededBy'
],
requiredFields: [
'title',
'deciders',
'context',
'decisionDrivers',
'decision',
'consequences'
],
optionalFields: [
'consideredOptions',
'prosAndCons',
'links',
'tags',
'relatedTo',
'supersedes',
'supersededBy'
]
};
}
export function getYStatementTemplate(): ADRTemplateDefinition {
return {
name: 'y-statement',
description: 'Y-statement - simple format focusing on context, decision, and consequences',
fields: [
'title',
'status',
'date',
'deciders',
'context',
'decision',
'consequences',
'tags'
],
requiredFields: [
'title',
'deciders',
'context',
'decision',
'consequences'
],
optionalFields: [
'tags'
]
};
}
export function renderADRTemplate(adr: ADR): string {
let markdown = `# ${adr.id}: ${adr.title}\n\n`;
// Status
if (adr.template === 'y-statement') {
markdown += `## Status: ${adr.status.charAt(0).toUpperCase() + adr.status.slice(1)}\n\n`;
} else {
markdown += `## Status\n\n`;
markdown += `${adr.status.charAt(0).toUpperCase() + adr.status.slice(1)}\n\n`;
}
// Metadata
markdown += `**Date**: ${adr.date.toISOString().split('T')[0]}\n`;
markdown += `**Deciders**: ${adr.deciders.join(', ')}\n`;
if (adr.tags && adr.tags.length > 0) {
markdown += `**Tags**: ${adr.tags.join(', ')}\n`;
}
markdown += '\n';
// Context
markdown += `## Context\n\n${adr.context}\n\n`;
// Decision Drivers (MADR only)
if (adr.template === 'madr' && adr.decisionDrivers && adr.decisionDrivers.length > 0) {
markdown += `## Decision Drivers\n\n`;
adr.decisionDrivers.forEach(driver => {
markdown += `* ${driver}\n`;
});
markdown += '\n';
}
// Considered Options (MADR only)
if (adr.template === 'madr' && adr.consideredOptions && adr.consideredOptions.length > 0) {
markdown += `## Considered Options\n\n`;
adr.consideredOptions.forEach(option => {
markdown += `* ${option.title} - ${option.description}\n`;
});
markdown += '\n';
}
// Decision
markdown += `## Decision\n\n${adr.decision}\n\n`;
// Consequences
markdown += `## Consequences\n\n${adr.consequences}\n\n`;
// Pros and Cons per option (MADR)
if (adr.template === 'madr' && adr.consideredOptions && adr.consideredOptions.length > 0) {
adr.consideredOptions.forEach(option => {
markdown += `### ${option.title}\n\n`;
if (option.description) {
markdown += `${option.description}\n\n`;
}
option.pros.forEach(pro => {
markdown += `* Good, because ${pro}\n`;
});
option.cons.forEach(con => {
markdown += `* Bad, because ${con}\n`;
});
markdown += '\n';
});
}
// Links/Relationships
const hasLinks = adr.supersedes?.length || adr.supersededBy || adr.relatedTo?.length;
if (hasLinks) {
markdown += `## Links\n\n`;
if (adr.supersedes && adr.supersedes.length > 0) {
markdown += `* Supersedes: ${adr.supersedes.join(', ')}\n`;
}
if (adr.supersededBy) {
markdown += `* Superseded by: ${adr.supersededBy}\n`;
}
if (adr.relatedTo && adr.relatedTo.length > 0) {
markdown += `* Related to: ${adr.relatedTo.join(', ')}\n`;
}
markdown += '\n';
}
// Status History
if (adr.statusHistory && adr.statusHistory.length > 0) {
markdown += `## Status History\n\n`;
adr.statusHistory.forEach(change => {
markdown += `* ${change.from} → ${change.to} on ${change.date.toISOString().split('T')[0]}`;
markdown += ` by ${change.changedBy}`;
if (change.reason) {
markdown += `: ${change.reason}`;
}
markdown += '\n';
});
}
return markdown;
}
export function getTemplateFields(template: ADRTemplate): {
required: string[];
optional: string[];
} {
switch (template) {
case 'nygard':
return {
required: getNygardTemplate().requiredFields,
optional: getNygardTemplate().optionalFields
};
case 'madr':
return {
required: getMADRTemplate().requiredFields,
optional: getMADRTemplate().optionalFields
};
case 'y-statement':
return {
required: getYStatementTemplate().requiredFields,
optional: getYStatementTemplate().optionalFields
};
default:
throw new Error(`Unknown template: ${template}`);
}
}
export function validateTemplateData(
template: ADRTemplate,
data: Record<string, any>
): { valid: boolean; errors: string[] } {
const errors: string[] = [];
const fields = getTemplateFields(template);
// Check required fields
for (const field of fields.required) {
if (!(field in data) || data[field] === undefined || data[field] === null) {
errors.push(`Missing required field: ${field}`);
} else if (Array.isArray(data[field]) && data[field].length === 0 && field === 'deciders') {
errors.push(`Field "deciders" cannot be empty`);
}
}
// Validate array fields
const arrayFields = ['deciders', 'tags', 'decisionDrivers', 'consideredOptions', 'prosAndCons'];
for (const field of arrayFields) {
if (field in data && data[field] !== undefined && !Array.isArray(data[field])) {
errors.push(`Field "${field}" must be an array`);
}
}
// Validate consideredOptions structure
if (data.consideredOptions && Array.isArray(data.consideredOptions)) {
data.consideredOptions.forEach((option: any, index: number) => {
if (!option.title || !option.description || !option.pros || !option.cons) {
errors.push(`Option must have title, description, pros, and cons`);
}
});
}
return {
valid: errors.length === 0,
errors
};
}