UNPKG

aios-core

Version:

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

453 lines (376 loc) 12.3 kB
/** * Migration Validation Module * * Validates migrated structure and runs post-migration checks. * * @module cli/commands/migrate/validate * @version 1.0.0 * @story 2.14 - Migration Script v2.0 → v4.0.4 */ const fs = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const { MODULE_MAPPING } = require('./analyze'); const { verifyImports } = require('./update-imports'); // formatSize available but currently unused // const { formatSize } = require('./analyze'); /** * Validate the v4.0.4 module structure exists * @param {string} aiosCoreDir - Path to .aios-core * @returns {Promise<Object>} Validation result */ async function validateStructure(aiosCoreDir) { const result = { valid: true, modules: {}, errors: [], warnings: [], }; const expectedModules = ['core', 'development', 'product', 'infrastructure']; for (const moduleName of expectedModules) { const moduleDir = path.join(aiosCoreDir, moduleName); const moduleResult = { exists: false, fileCount: 0, totalSize: 0, hasExpectedDirs: true, missingDirs: [], }; if (fs.existsSync(moduleDir)) { moduleResult.exists = true; // Count files const files = await countFiles(moduleDir); moduleResult.fileCount = files.count; moduleResult.totalSize = files.size; // Check expected subdirectories const expectedDirs = MODULE_MAPPING[moduleName]?.directories || []; for (const dir of expectedDirs) { const dirPath = path.join(moduleDir, dir); if (!fs.existsSync(dirPath)) { moduleResult.missingDirs.push(dir); moduleResult.hasExpectedDirs = false; } } if (moduleResult.missingDirs.length > 0) { result.warnings.push({ module: moduleName, message: `Missing expected directories: ${moduleResult.missingDirs.join(', ')}`, }); } } else { result.warnings.push({ module: moduleName, message: `Module directory not found: ${moduleName}/`, }); } result.modules[moduleName] = moduleResult; } // Check for leftover v2.0 directories at root level const rootEntries = await fs.promises.readdir(aiosCoreDir, { withFileTypes: true }); for (const entry of rootEntries) { if (entry.isDirectory() && !expectedModules.includes(entry.name)) { // Check if this is a v2.0 directory that should have been migrated for (const config of Object.values(MODULE_MAPPING)) { if (config.directories.includes(entry.name)) { result.warnings.push({ type: 'leftover', message: `Leftover v2.0 directory found: ${entry.name}/ (should be in module)`, }); } } } } return result; } /** * Count files and total size in a directory * @param {string} dir - Directory path * @returns {Promise<Object>} File count and total size */ async function countFiles(dir) { let count = 0; let size = 0; async function walk(d) { const entries = await fs.promises.readdir(d, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(d, entry.name); if (entry.isDirectory()) { await walk(fullPath); } else { count++; const stats = await fs.promises.stat(fullPath); size += stats.size; } } } await walk(dir); return { count, size }; } /** * Run lint check on migrated files * @param {string} projectRoot - Project root * @param {Object} options - Options * @returns {Promise<Object>} Lint result */ async function runLintCheck(projectRoot, options = {}) { const { timeout = 60000 } = options; return new Promise((resolve) => { const result = { ran: false, passed: false, errors: 0, warnings: 0, output: '', error: null, }; // Check if package.json has lint script const packageJsonPath = path.join(projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { result.error = 'No package.json found'; resolve(result); return; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (!packageJson.scripts?.lint) { result.error = 'No lint script found in package.json'; resolve(result); return; } result.ran = true; const isWindows = process.platform === 'win32'; const shell = isWindows ? 'cmd' : '/bin/sh'; const shellArgs = isWindows ? ['/c', 'npm run lint'] : ['-c', 'npm run lint']; const child = spawn(shell, shellArgs, { cwd: projectRoot, timeout, }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { result.output = stdout + stderr; result.passed = code === 0; // Try to parse error/warning counts from output const errorMatch = result.output.match(/(\d+)\s*error/i); const warningMatch = result.output.match(/(\d+)\s*warning/i); if (errorMatch) result.errors = parseInt(errorMatch[1], 10); if (warningMatch) result.warnings = parseInt(warningMatch[1], 10); resolve(result); }); child.on('error', (err) => { result.error = err.message; resolve(result); }); }); } /** * Run test suite * @param {string} projectRoot - Project root * @param {Object} options - Options * @returns {Promise<Object>} Test result */ async function runTests(projectRoot, options = {}) { const { timeout = 300000 } = options; // 5 min default for tests return new Promise((resolve) => { const result = { ran: false, success: false, total: 0, passed: 0, failed: 0, output: '', error: null, }; // Check if package.json has test script const packageJsonPath = path.join(projectRoot, 'package.json'); if (!fs.existsSync(packageJsonPath)) { result.error = 'No package.json found'; resolve(result); return; } const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); if (!packageJson.scripts?.test) { result.error = 'No test script found in package.json'; resolve(result); return; } result.ran = true; const isWindows = process.platform === 'win32'; const shell = isWindows ? 'cmd' : '/bin/sh'; const shellArgs = isWindows ? ['/c', 'npm test'] : ['-c', 'npm test']; const child = spawn(shell, shellArgs, { cwd: projectRoot, timeout, env: { ...process.env, CI: 'true' }, // Disable watch mode }); let stdout = ''; let stderr = ''; child.stdout?.on('data', (data) => { stdout += data.toString(); }); child.stderr?.on('data', (data) => { stderr += data.toString(); }); child.on('close', (code) => { result.output = stdout + stderr; result.passed = code === 0; // Try to parse test counts from output (Jest format) const passedMatch = result.output.match(/(\d+)\s*passed/i); const failedMatch = result.output.match(/(\d+)\s*failed/i); const totalMatch = result.output.match(/Tests:\s*(\d+)/i); if (passedMatch) result.passedCount = parseInt(passedMatch[1], 10); if (failedMatch) result.failed = parseInt(failedMatch[1], 10); if (totalMatch) result.total = parseInt(totalMatch[1], 10); resolve(result); }); child.on('error', (err) => { result.error = err.message; resolve(result); }); }); } /** * Run full validation suite after migration * @param {string} projectRoot - Project root * @param {Object} options - Options * @returns {Promise<Object>} Full validation result */ async function runFullValidation(projectRoot, options = {}) { const { onProgress = () => {}, skipTests = false, skipLint = false } = options; const aiosCoreDir = path.join(projectRoot, '.aios-core'); const result = { valid: true, structure: null, imports: null, lint: null, tests: null, summary: { passed: [], failed: [], skipped: [], }, }; // Validate structure onProgress({ phase: 'structure', message: '✓ Validating structure...' }); result.structure = await validateStructure(aiosCoreDir); if (result.structure.errors.length > 0) { result.valid = false; result.summary.failed.push('Structure validation'); } else { result.summary.passed.push('Structure validation'); } // Verify imports onProgress({ phase: 'imports', message: '✓ Verifying imports...' }); result.imports = await verifyImports(aiosCoreDir); if (!result.imports.valid) { result.valid = false; result.summary.failed.push('Import verification'); } else { result.summary.passed.push('Import verification'); } // Run lint if (!skipLint) { onProgress({ phase: 'lint', message: '✓ Running lint...' }); result.lint = await runLintCheck(projectRoot); if (result.lint.ran) { if (result.lint.passed) { result.summary.passed.push(`Lint (${result.lint.errors} errors)`); } else { result.valid = false; result.summary.failed.push(`Lint (${result.lint.errors} errors)`); } } else { result.summary.skipped.push('Lint (no script)'); } } else { result.summary.skipped.push('Lint (skipped)'); } // Run tests if (!skipTests) { onProgress({ phase: 'tests', message: '✓ Running tests...' }); result.tests = await runTests(projectRoot); if (result.tests.ran) { if (result.tests.passed) { result.summary.passed.push(`Tests (${result.tests.passedCount || 'all'} passed)`); } else { result.valid = false; result.summary.failed.push(`Tests (${result.tests.failed} failed)`); } } else { result.summary.skipped.push('Tests (no script)'); } } else { result.summary.skipped.push('Tests (skipped)'); } return result; } /** * Generate migration summary report * @param {Object} migrationResult - Result from executeMigration * @param {Object} validationResult - Result from runFullValidation * @param {Object} options - Options * @returns {string} Formatted summary */ function generateSummary(migrationResult, validationResult, options = {}) { const { backupLocation = null, duration = null } = options; const lines = []; lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); if (validationResult?.valid !== false) { lines.push('✅ Migration complete!' + (duration ? ` (${duration})` : '')); } else { lines.push('⚠️ Migration completed with issues'); } lines.push(''); lines.push('Summary:'); lines.push(` • Files migrated: ${migrationResult.totalFiles}`); if (migrationResult.modules) { for (const [name, data] of Object.entries(migrationResult.modules)) { if (data.files > 0) { lines.push(` - ${name}: ${data.files} files`); } } } if (validationResult?.imports) { lines.push(` • Imports verified: ${validationResult.imports.totalImports}`); } if (backupLocation) { lines.push(` • Backup location: ${backupLocation}`); } if (validationResult?.summary) { if (validationResult.summary.passed.length > 0) { lines.push(''); lines.push('Passed:'); for (const item of validationResult.summary.passed) { lines.push(` ✓ ${item}`); } } if (validationResult.summary.failed.length > 0) { lines.push(''); lines.push('Failed:'); for (const item of validationResult.summary.failed) { lines.push(` ✗ ${item}`); } } } lines.push(''); lines.push('Next steps:'); lines.push(' 1. Test your project: npm test'); lines.push(' 2. Review changes: git diff'); lines.push(' 3. Report issues: github.com/aios/issues'); lines.push(''); lines.push('To rollback: aios migrate --rollback'); return lines.join('\n'); } module.exports = { validateStructure, countFiles, runLintCheck, runTests, runFullValidation, generateSummary, };