aios-core
Version:
Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework
309 lines (269 loc) • 9.3 kB
JavaScript
/**
* Template Engine v2.0
* Main entry point for the AIOS Template Engine
*
* @module TemplateEngine
* @version 2.0.0
*/
;
const path = require('path');
const fs = require('fs').promises;
const { TemplateLoader } = require('./loader');
const { VariableElicitation } = require('./elicitation');
const { TemplateRenderer } = require('./renderer');
const { TemplateValidator } = require('./validator');
/**
* Supported template types
* @constant {Array<string>}
*/
const SUPPORTED_TYPES = ['prd', 'prd-v2', 'adr', 'pmdr', 'dbdr', 'story', 'epic', 'task'];
/**
* TemplateEngine class - Main orchestrator for template generation
*/
class TemplateEngine {
/**
* Create a TemplateEngine instance
* @param {Object} options - Configuration options
* @param {string} options.templatesDir - Path to templates directory
* @param {string} options.schemasDir - Path to schemas directory
* @param {boolean} options.interactive - Enable interactive mode (default: true)
* @param {Object} options.helpers - Custom Handlebars helpers
*/
constructor(options = {}) {
// Store baseDir as instance property for predictable path resolution
this.baseDir = options.baseDir || process.cwd();
this.templatesDir = options.templatesDir ||
path.join(this.baseDir, '.aios-core', 'product', 'templates');
this.schemasDir = options.schemasDir ||
path.join(this.templatesDir, 'engine', 'schemas');
this.outputDir = options.outputDir ||
path.join(this.baseDir, 'docs');
this.interactive = options.interactive !== false;
// Initialize components
this.loader = new TemplateLoader({ templatesDir: this.templatesDir });
this.elicitation = new VariableElicitation({ interactive: this.interactive });
this.renderer = new TemplateRenderer({ helpers: options.helpers });
this.validator = new TemplateValidator({ schemasDir: this.schemasDir });
}
/**
* Get list of supported template types
* @returns {Array<string>} Supported template types
*/
get supportedTypes() {
return [...SUPPORTED_TYPES];
}
/**
* Generate a document from a template
* @param {string} templateType - Template type (prd, adr, etc.)
* @param {Object} context - Pre-provided context values
* @param {Object} options - Generation options
* @param {boolean} options.validate - Validate output (default: true)
* @param {boolean} options.save - Save to file (default: false)
* @param {string} options.outputPath - Custom output path
* @returns {Promise<Object>} Generation result with content and metadata
*/
async generate(templateType, context = {}, options = {}) {
const { validate = true, save = false, outputPath } = options;
// Validate template type
if (!SUPPORTED_TYPES.includes(templateType)) {
throw new Error(
`Unsupported template type: ${templateType}. ` +
`Supported types: ${SUPPORTED_TYPES.join(', ')}`,
);
}
// Load template
const template = await this.loader.load(templateType);
// Elicit variables
const variables = await this.elicitation.elicit(template.variables, context);
// Validate required variables
const elicitValidation = this.elicitation.validate(template.variables, variables);
if (!elicitValidation.isValid) {
throw new Error(`Missing required variables: ${elicitValidation.errors.join(', ')}`);
}
// Render template
const content = this.renderer.render(template, variables);
// Validate output if schema exists
let validation = { isValid: true, errors: [] };
if (validate) {
try {
validation = await this.validator.validate(variables, templateType);
if (!validation.isValid) {
console.warn('Template validation warnings:', validation.errors);
}
} catch (error) {
// Schema may not exist yet for all template types
if (!error.message.includes('Schema not found')) {
throw error;
}
}
// Also validate structure
const structureValidation = this.validator.validateStructure(content, template);
if (!structureValidation.isValid) {
validation.errors.push(...structureValidation.errors);
validation.isValid = validation.isValid && structureValidation.isValid;
}
}
const result = {
templateType,
content,
variables,
metadata: template.metadata,
validation,
generatedAt: new Date().toISOString(),
};
// Save to file if requested
if (save) {
const filePath = outputPath || this.resolveOutputPath(templateType, variables);
await this.saveOutput(content, filePath);
result.savedTo = filePath;
}
return result;
}
/**
* Resolve output path for generated document
* @param {string} templateType - Template type
* @param {Object} variables - Template variables
* @returns {string} Output file path
*/
resolveOutputPath(templateType, variables) {
const outputDirs = {
prd: 'docs/prd',
'prd-v2': 'docs/prd',
adr: 'docs/architecture/decisions',
pmdr: 'docs/decisions',
dbdr: 'docs/decisions',
story: 'docs/stories',
epic: 'docs/epics',
task: 'docs/tasks',
};
const dir = outputDirs[templateType] || 'docs';
const number = variables.number ? `-${String(variables.number).padStart(3, '0')}` : '';
const slug = variables.title
? `-${variables.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').slice(0, 50)}`
: '';
// Use baseDir for predictable path resolution instead of fragile '..' navigation
return path.join(this.baseDir, dir, `${templateType}${number}${slug}.md`);
}
/**
* Save generated content to file
* @param {string} content - Content to save
* @param {string} filePath - Output file path
*/
async saveOutput(content, filePath) {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf-8');
}
/**
* Preview template rendering without saving
* @param {string} templateType - Template type
* @param {Object} context - Context values
* @returns {Promise<string>} Rendered content preview
*/
async preview(templateType, context = {}) {
const result = await this.generate(templateType, context, {
validate: false,
save: false,
});
return result.content;
}
/**
* Load and display template information
* @param {string} templateType - Template type
* @returns {Promise<Object>} Template information
*/
async getTemplateInfo(templateType) {
const template = await this.loader.load(templateType);
return {
type: templateType,
name: template.metadata.template_name,
version: template.metadata.version,
variables: template.variables.map(v => ({
name: v.name,
type: v.type,
required: v.required,
description: v.prompt,
})),
};
}
/**
* List all available templates
* @returns {Promise<Array>} List of template information
*/
async listTemplates() {
const templates = [];
for (const type of SUPPORTED_TYPES) {
try {
const info = await this.getTemplateInfo(type);
templates.push(info);
} catch {
// Template may not exist yet
templates.push({
type,
name: type.toUpperCase(),
version: 'N/A',
variables: [],
status: 'missing',
});
}
}
return templates;
}
/**
* Register a custom Handlebars helper
* @param {string} name - Helper name
* @param {Function} fn - Helper function
*/
registerHelper(name, fn) {
this.renderer.registerHelper(name, fn);
}
/**
* Register a custom auto-resolver for elicitation
* @param {string} name - Resolver name
* @param {Function} resolver - Async resolver function
*/
registerAutoResolver(name, resolver) {
this.elicitation.registerAutoResolver(name, resolver);
}
/**
* Validate an existing document against its schema
* @param {string} content - Document content
* @param {string} templateType - Template type
* @returns {Promise<Object>} Validation result
*/
async validateDocument(content, templateType) {
const template = await this.loader.load(templateType);
const structureResult = this.validator.validateStructure(content, template);
// Extract data for schema validation
const data = this.validator.extractDataFromMarkdown(content, template);
let schemaResult = { isValid: true, errors: [] };
try {
schemaResult = await this.validator.validate(data, templateType);
} catch (error) {
if (!error.message.includes('Schema not found')) {
throw error;
}
}
return {
isValid: structureResult.isValid && schemaResult.isValid,
structureErrors: structureResult.errors,
schemaErrors: schemaResult.errors,
};
}
/**
* Clear all caches
*/
clearCache() {
this.loader.clearCache();
this.validator.clearCache();
}
}
// Export main class and components
module.exports = {
TemplateEngine,
TemplateLoader,
VariableElicitation,
TemplateRenderer,
TemplateValidator,
SUPPORTED_TYPES,
};