UNPKG

aios-core

Version:

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

205 lines (171 loc) 6.16 kB
/** * Constitution Generator * * Reads .aios-core/constitution.md and generates .synapse/constitution * in KEY=VALUE format for the SYNAPSE Context Engine L0 layer. * * Usage: node .aios-core/core/synapse/scripts/generate-constitution.js * * @module core/synapse/scripts/generate-constitution * @version 1.0.0 * @created Story SYN-8 - Domain Content Files */ const fs = require('fs'); const path = require('path'); /** * Roman numeral to Arabic number map (I-X) */ const ROMAN_TO_ARABIC = { 'I': 1, 'II': 2, 'III': 3, 'IV': 4, 'V': 5, 'VI': 6, 'VII': 7, 'VIII': 8, 'IX': 9, 'X': 10, }; /** * Clean markdown formatting from text * Removes backticks and trims whitespace * * @param {string} text - Text with possible markdown formatting * @returns {string} Cleaned text */ function cleanText(text) { return text.replace(/`/g, '').trim(); } /** * Parse constitution.md and extract articles with their rules * * @param {string} content - Raw markdown content of constitution.md * @returns {Array<{number: number, roman: string, title: string, severity: string, rules: string[]}>} */ function parseConstitution(content) { if (!content || typeof content !== 'string') { return []; } const articles = []; // Match article headers: ### I. Title (SEVERITY) const articleRegex = /^### ([IVXLC]+)\.\s+(.+?)\s*\(([^)]+)\)\s*$/gm; let match; const articlePositions = []; while ((match = articleRegex.exec(content)) !== null) { articlePositions.push({ roman: match[1], title: match[2].trim(), severity: match[3].trim(), startIndex: match.index, }); } for (let i = 0; i < articlePositions.length; i++) { const start = articlePositions[i].startIndex; const end = i + 1 < articlePositions.length ? articlePositions[i + 1].startIndex : content.indexOf('## Governance', start) !== -1 ? content.indexOf('## Governance', start) : content.length; const articleContent = content.substring(start, end); const rules = extractRules(articleContent); const num = ROMAN_TO_ARABIC[articlePositions[i].roman]; if (!num) { continue; } articles.push({ number: num, roman: articlePositions[i].roman, title: articlePositions[i].title, severity: articlePositions[i].severity, rules, }); } return articles; } /** * Extract rules from article content * Matches bullet points starting with MUST:, MUST NOT:, SHOULD:, SHOULD NOT:, EXCEPTION: * * @param {string} articleContent - Markdown content of a single article * @returns {string[]} Array of rule strings */ function extractRules(articleContent) { const rules = []; const lines = articleContent.split('\n'); for (const line of lines) { const trimmed = line.trim(); const ruleMatch = trimmed.match(/^-\s+(MUST(?:\s+NOT)?|SHOULD(?:\s+NOT)?|EXCEPTION):\s+(.+)$/); if (ruleMatch) { rules.push(`${ruleMatch[1]}: ${cleanText(ruleMatch[2])}`); } } return rules; } /** * Generate KEY=VALUE constitution content from parsed articles * * @param {Array<{number: number, roman: string, title: string, severity: string, rules: string[]}>} articles * @returns {string} KEY=VALUE formatted content */ function generateConstitution(articles) { const lines = [ '# SYNAPSE Constitution Domain (L0)', '# Auto-generated from .aios-core/constitution.md', '# DO NOT EDIT MANUALLY — re-run generate-constitution.js', '', ]; for (const article of articles) { lines.push(`# Article ${article.roman}: ${article.title} (${article.severity})`); // Rule 0: title + severity summary lines.push(`CONSTITUTION_RULE_ART${article.number}_0=${article.title} (${article.severity})`); // Subsequent rules from bullet points for (let i = 0; i < article.rules.length; i++) { lines.push(`CONSTITUTION_RULE_ART${article.number}_${i + 1}=${article.rules[i]}`); } lines.push(''); } return lines.join('\n'); } /** * Main entry point: read constitution.md, generate .synapse/constitution * * @param {object} [options] - Override paths for testing * @param {string} [options.projectRoot] - Project root directory * @param {string} [options.constitutionPath] - Path to constitution.md * @param {string} [options.outputPath] - Path to output file * @returns {{success: boolean, articles?: number, rules?: number, outputPath?: string, error?: string}} */ function main(options = {}) { const projectRoot = options.projectRoot || path.resolve(__dirname, '..', '..', '..', '..'); const constitutionPath = options.constitutionPath || path.join(projectRoot, '.aios-core', 'constitution.md'); const outputPath = options.outputPath || path.join(projectRoot, '.synapse', 'constitution'); // Read source let content; try { content = fs.readFileSync(constitutionPath, 'utf8'); } catch (error) { if (error.code === 'ENOENT') { console.error(`Constitution not found: ${constitutionPath}`); process.exitCode = 1; return { success: false, error: 'Constitution file not found' }; } throw error; } // Parse articles const articles = parseConstitution(content); if (articles.length === 0) { console.error('No articles found in constitution.md'); process.exitCode = 1; return { success: false, error: 'No articles found' }; } // Generate output const output = generateConstitution(articles); // Ensure output directory exists const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // Write output (idempotent — overwrites cleanly) fs.writeFileSync(outputPath, output, 'utf8'); const totalRules = articles.reduce((sum, a) => sum + a.rules.length + 1, 0); console.log(`Constitution generated: ${articles.length} articles, ${totalRules} rules`); return { success: true, articles: articles.length, rules: totalRules, outputPath }; } // Run if called directly if (require.main === module) { main(); } module.exports = { parseConstitution, extractRules, generateConstitution, cleanText, main, ROMAN_TO_ARABIC };