UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

453 lines (405 loc) 13.2 kB
/** * Service Registry Builder * * Scans AIOS modules and builds the service registry JSON file. * Extracts metadata from tasks, templates, scripts, checklists, and workflows. * * @module build-registry * @version 1.0.0 * @created Story 2.6 - Service Registry Creation */ const fs = require('fs').promises; const path = require('path'); const { glob } = require('glob'); // Registry version const REGISTRY_VERSION = '1.0.0'; // Source directories to scan const SCAN_SOURCES = [ { pattern: '.aios-core/development/tasks/**/*.md', category: 'task', taskFormat: 'TASK-FORMAT-V1', subcategoryExtractor: (filePath) => { const parts = filePath.split('/'); const filename = parts[parts.length - 1]; // Extract agent prefix (e.g., dev-, qa-, po-) const prefixMatch = filename.match(/^([a-z]+)-/); if (prefixMatch) { return prefixMatch[1]; } // Extract db- prefix for database tasks if (filename.startsWith('db-')) { return 'database'; } // Check for specific keywords if (filename.includes('create')) return 'creation'; if (filename.includes('modify') || filename.includes('edit')) return 'modification'; if (filename.includes('audit') || filename.includes('validate')) return 'validation'; if (filename.includes('analyze')) return 'analysis'; return 'general'; }, }, { pattern: '.aios-core/product/templates/**/*.md', category: 'template', taskFormat: 'TEMPLATE', subcategoryExtractor: (filePath) => { if (filePath.includes('ide-rules')) return 'ide-rules'; if (filePath.includes('personalized')) return 'personalized'; return 'document'; }, }, { pattern: '.aios-core/infrastructure/scripts/**/*.js', category: 'script', taskFormat: 'SCRIPT', subcategoryExtractor: (filePath) => { const filename = path.basename(filePath, '.js'); if (filename.includes('validator') || filename.includes('checker')) return 'validation'; if (filename.includes('analyzer')) return 'analysis'; if (filename.includes('generator')) return 'generation'; if (filename.includes('manager')) return 'management'; if (filename.includes('config') || filename.includes('loader')) return 'configuration'; if (filename.includes('test')) return 'testing'; return 'utility'; }, skipArchived: true, }, { pattern: '.aios-core/product/checklists/**/*.md', category: 'checklist', taskFormat: 'CHECKLIST', subcategoryExtractor: () => 'quality', }, { pattern: '.aios-core/development/workflows/**/*.yaml', category: 'workflow', taskFormat: 'WORKFLOW', subcategoryExtractor: (filePath) => { const filename = path.basename(filePath, '.yaml'); if (filename.includes('brownfield')) return 'brownfield'; if (filename.includes('greenfield')) return 'greenfield'; return 'general'; }, }, { pattern: '.aios-core/core/data/**/*.md', category: 'data', taskFormat: 'TASK-FORMAT-V1', subcategoryExtractor: () => 'knowledge', }, { pattern: '.aios-core/core/data/**/*.yaml', category: 'data', taskFormat: 'TASK-FORMAT-V1', subcategoryExtractor: () => 'configuration', }, ]; /** * Convert filename to kebab-case ID */ function toKebabId(filename) { return filename .replace(/\.(md|js|yaml|yml)$/, '') .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, ''); } /** * Convert kebab-case to title case */ function toTitleCase(kebab) { return kebab .split('-') .map(word => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); } /** * Extract description from markdown file (first paragraph) */ async function extractMarkdownDescription(filePath, baseDir) { try { const fullPath = path.join(baseDir, filePath); const content = await fs.readFile(fullPath, 'utf8'); const lines = content.split('\n'); // Find first non-empty, non-header line for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) { // Clean up markdown formatting return trimmed .replace(/\*\*/g, '') .replace(/\*/g, '') .replace(/`/g, '') .slice(0, 200); } } return 'No description available'; } catch (_error) { return 'No description available'; } } /** * Extract description from JS file (JSDoc or first comment) */ async function extractJSDescription(filePath, baseDir) { try { const fullPath = path.join(baseDir, filePath); const content = await fs.readFile(fullPath, 'utf8'); // Look for JSDoc @description or first line after /** const jsdocMatch = content.match(/\/\*\*[\s\S]*?\*\//); if (jsdocMatch) { const jsdoc = jsdocMatch[0]; // Extract description const descMatch = jsdoc.match(/@description\s+(.+?)(?=\n\s*\*\s*@|\*\/)/s); if (descMatch) { return descMatch[1].replace(/\n\s*\*\s*/g, ' ').trim().slice(0, 200); } // Get first paragraph after /** const firstPara = jsdoc.match(/\/\*\*\s*\n\s*\*\s*(.+?)(?=\n\s*\*\s*\n|\n\s*\*\s*@|\*\/)/s); if (firstPara) { return firstPara[1].replace(/\n\s*\*\s*/g, ' ').trim().slice(0, 200); } } return 'JavaScript utility script'; } catch (_error) { return 'JavaScript utility script'; } } /** * Extract tags from filename and path */ function extractTags(filePath, category, subcategory) { const filename = path.basename(filePath).replace(/\.(md|js|yaml|yml)$/, ''); const tags = new Set(); // Add category and subcategory tags.add(category); if (subcategory) tags.add(subcategory); // Extract words from filename const words = filename.split(/[-_]/).filter(w => w.length > 2); words.forEach(w => tags.add(w.toLowerCase())); // Add specific tags based on patterns if (filename.includes('qa') || filename.includes('test')) tags.add('testing'); if (filename.includes('dev')) tags.add('development'); if (filename.includes('db') || filename.includes('database')) tags.add('database'); if (filename.includes('create') || filename.includes('generate')) tags.add('creation'); if (filename.includes('validate') || filename.includes('check')) tags.add('validation'); if (filename.includes('analyze') || filename.includes('audit')) tags.add('analysis'); if (filename.includes('po') || filename.includes('story')) tags.add('product'); if (filename.includes('github') || filename.includes('git')) tags.add('git'); return Array.from(tags); } /** * Extract agents from filename */ function extractAgents(filePath) { const filename = path.basename(filePath); const agents = []; const agentPrefixes = { 'dev-': 'dev', 'qa-': 'qa', 'po-': 'po', 'pm-': 'pm', 'sm-': 'sm', 'db-': 'db-sage', 'architect-': 'architect', 'analyst-': 'analyst', 'github-devops-': 'github-devops', 'ux-': 'ux-expert', }; for (const [prefix, agent] of Object.entries(agentPrefixes)) { if (filename.startsWith(prefix)) { agents.push(agent); break; } } return agents; } /** * Determine executor types based on category */ function getExecutorTypes(category) { const executorMap = { 'task': ['Agent', 'Worker'], 'template': ['Agent'], 'script': ['CLI', 'Script'], 'checklist': ['Agent'], 'workflow': ['Agent', 'Worker'], 'data': ['Agent'], }; return executorMap[category] || ['Agent']; } /** * Estimate performance based on category */ function estimatePerformance(category) { const perfMap = { 'task': { avgDuration: '1m', cacheable: false, parallelizable: false }, 'template': { avgDuration: '100ms', cacheable: true, parallelizable: true }, 'script': { avgDuration: '500ms', cacheable: true, parallelizable: true }, 'checklist': { avgDuration: '2m', cacheable: false, parallelizable: false }, 'workflow': { avgDuration: '5m', cacheable: false, parallelizable: false }, 'data': { avgDuration: '50ms', cacheable: true, parallelizable: true }, }; return perfMap[category] || { avgDuration: '1s', cacheable: false, parallelizable: false }; } /** * Build worker entry from file */ async function buildWorkerEntry(filePath, source, baseDir) { const filename = path.basename(filePath); const id = toKebabId(filename); const name = toTitleCase(id); const subcategory = source.subcategoryExtractor(filePath); // Extract description based on file type let description; if (filePath.endsWith('.js')) { description = await extractJSDescription(filePath, baseDir); } else { description = await extractMarkdownDescription(filePath, baseDir); } return { id, name, description, category: source.category, subcategory, inputs: [], outputs: [], tags: extractTags(filePath, source.category, subcategory), path: filePath.replace(/\\/g, '/'), taskFormat: source.taskFormat, executorTypes: getExecutorTypes(source.category), performance: estimatePerformance(source.category), agents: extractAgents(filePath), metadata: { source: source.category === 'task' ? 'development' : source.category === 'template' || source.category === 'checklist' ? 'product' : source.category === 'script' ? 'infrastructure' : 'core', addedVersion: REGISTRY_VERSION, }, }; } /** * Build category summary */ function buildCategorySummary(workers) { const categories = {}; for (const worker of workers) { if (!categories[worker.category]) { categories[worker.category] = { count: 0, subcategories: new Set(), description: getCategoryDescription(worker.category), }; } categories[worker.category].count++; if (worker.subcategory) { categories[worker.category].subcategories.add(worker.subcategory); } } // Convert Sets to arrays for (const cat of Object.values(categories)) { cat.subcategories = Array.from(cat.subcategories).sort(); } return categories; } /** * Get category description */ function getCategoryDescription(category) { const descriptions = { 'task': 'Executable task workflows for agents', 'template': 'Document and code templates', 'script': 'JavaScript utility scripts', 'checklist': 'Quality validation checklists', 'workflow': 'Multi-step workflow definitions', 'data': 'Knowledge base and configuration data', }; return descriptions[category] || 'General category'; } /** * Main build function */ async function buildRegistry(baseDir = process.cwd()) { console.log('Building service registry...'); console.log(`Base directory: ${baseDir}`); const workers = []; const seenIds = new Set(); for (const source of SCAN_SOURCES) { console.log(`\nScanning: ${source.pattern}`); try { const files = await glob(source.pattern, { cwd: baseDir, nodir: true, ignore: source.skipArchived ? ['**/_archived/**'] : [], }); console.log(` Found ${files.length} files`); for (const file of files) { const worker = await buildWorkerEntry(file, source, baseDir); // Ensure unique IDs if (seenIds.has(worker.id)) { worker.id = `${worker.id}-${source.category}`; } seenIds.add(worker.id); workers.push(worker); } } catch (error) { console.error(` Error scanning ${source.pattern}:`, error.message); } } // Sort workers by category and name workers.sort((a, b) => { if (a.category !== b.category) { return a.category.localeCompare(b.category); } return a.name.localeCompare(b.name); }); const registry = { version: REGISTRY_VERSION, generated: new Date().toISOString(), totalWorkers: workers.length, categories: buildCategorySummary(workers), workers, }; console.log(`\nTotal workers: ${workers.length}`); console.log('Categories:', Object.keys(registry.categories).join(', ')); return registry; } /** * Save registry to file */ async function saveRegistry(registry, outputPath) { const json = JSON.stringify(registry, null, 2); await fs.writeFile(outputPath, json, 'utf8'); console.log(`\nRegistry saved to: ${outputPath}`); } /** * CLI entry point */ async function main() { const baseDir = process.argv[2] || process.cwd(); const outputPath = process.argv[3] || path.join(baseDir, '.aios-core/core/registry/service-registry.json'); try { const registry = await buildRegistry(baseDir); await saveRegistry(registry, outputPath); console.log('\nBuild complete!'); console.log(`Workers: ${registry.totalWorkers}`); if (registry.totalWorkers < 97) { console.warn(`\nWARNING: Registry has ${registry.totalWorkers} workers, expected 97+`); process.exit(1); } } catch (error) { console.error('Build failed:', error); process.exit(1); } } // Run if called directly if (require.main === module) { main(); } module.exports = { buildRegistry, saveRegistry, REGISTRY_VERSION, };