#!/usr/bin/env node import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawn, spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; import { brotliCompressSync, gzipSync } from 'node:zlib'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const repoRoot = path.resolve(__dirname, '..'); const REQUIRED_GATE_IDS = [ 'environment', 'format', 'typecheck', 'lint', 'unit', 'build', 'bundle-budget', 'e2e', ]; const OPTIONAL_GATE_IDS = ['install', 'playwright-install']; const HELP = ` Mechanica final validation runner Usage: node scripts/run-final-validation.mjs [options] Options: --out-dir Directory for validation artifacts. Default: .validation/latest --skip-install Do not run dependency installation. The resulting summary will only pass if all required gates still pass. --skip-playwright-install Do not run "playwright install" before E2E. --skip-e2e Skip E2E tests. This is useful for local smoke checks but cannot produce a passing final validation summary. --quiet Write command output to logs only. --help Show this help text. Environment: MECHANICA_SKIP_INSTALL=1 MECHANICA_SKIP_PLAYWRIGHT_INSTALL=1 MECHANICA_SKIP_E2E=1 MECHANICA_VALIDATION_OUT_DIR=.validation/latest MECHANICA_VALIDATION_GATE_TIMEOUT_SECONDS=900 MECHANICA_VALIDATION_E2E_TIMEOUT_SECONDS=1800 MECHANICA_DIST_DIR=dist Bundle budget overrides: MECHANICA_BUDGET_TOTAL_JS_GZIP_KB=1600 MECHANICA_BUDGET_TOTAL_CSS_GZIP_KB=180 MECHANICA_BUDGET_TOTAL_SERVED_GZIP_KB=2400 MECHANICA_BUDGET_LARGEST_JS_GZIP_KB=1050 MECHANICA_BUDGET_LARGEST_ASSET_RAW_KB=8192 `; function isTruthy(value) { return ['1', 'true', 'yes', 'y', 'on'].includes(String(value ?? '').toLowerCase()); } function isFalsy(value) { return ['0', 'false', 'no', 'n', 'off'].includes(String(value ?? '').toLowerCase()); } function parseNumberEnv(name, fallback) { const raw = process.env[name]; if (raw === undefined || raw === '') { return fallback; } const parsed = Number(raw); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } function parseArgs(argv) { const options = { outDir: process.env.MECHANICA_VALIDATION_OUT_DIR || '.validation/latest', skipInstall: isTruthy(process.env.MECHANICA_SKIP_INSTALL) || isFalsy(process.env.MECHANICA_VALIDATION_INSTALL), skipPlaywrightInstall: isTruthy(process.env.MECHANICA_SKIP_PLAYWRIGHT_INSTALL), skipE2E: isTruthy(process.env.MECHANICA_SKIP_E2E), quiet: isTruthy(process.env.MECHANICA_VALIDATION_QUIET), defaultTimeoutMs: parseNumberEnv('MECHANICA_VALIDATION_GATE_TIMEOUT_SECONDS', 900) * 1000, e2eTimeoutMs: parseNumberEnv('MECHANICA_VALIDATION_E2E_TIMEOUT_SECONDS', 1800) * 1000, }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === '--help' || arg === '-h') { console.log(HELP.trim()); process.exit(0); } if (arg === '--skip-install') { options.skipInstall = true; continue; } if (arg === '--skip-playwright-install') { options.skipPlaywrightInstall = true; continue; } if (arg === '--skip-e2e') { options.skipE2E = true; continue; } if (arg === '--quiet') { options.quiet = true; continue; } if (arg === '--out-dir') { const value = argv[index + 1]; if (!value) { throw new Error('--out-dir requires a path value'); } options.outDir = value; index += 1; continue; } if (arg.startsWith('--out-dir=')) { options.outDir = arg.slice('--out-dir='.length); continue; } throw new Error(`Unknown option: ${arg}`); } return options; } function npmCommand() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; } function commandOutput(command, args, cwd = repoRoot) { const result = spawnSync(command, args, { cwd, encoding: 'utf8', windowsHide: true, }); if (result.error || result.status !== 0) { return null; } return String(result.stdout || '').trim(); } function gitMetadata() { const git = process.platform === 'win32' ? 'git.exe' : 'git'; return { commit: commandOutput(git, ['rev-parse', 'HEAD']) || 'unknown', branch: commandOutput(git, ['rev-parse', '--abbrev-ref', 'HEAD']) || 'unknown', dirtyFiles: (commandOutput(git, ['status', '--short']) || '') .split('\n') .map((line) => line.trim()) .filter(Boolean), }; } function ensureDir(dirPath) { fs.mkdirSync(dirPath, { recursive: true }); } function readJsonFile(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } function writeJsonFile(filePath, value) { ensureDir(path.dirname(filePath)); fs.writeFileSync(`${filePath}.tmp`, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); fs.renameSync(`${filePath}.tmp`, filePath); } function writeTextFile(filePath, value) { ensureDir(path.dirname(filePath)); fs.writeFileSync(`${filePath}.tmp`, value, 'utf8'); fs.renameSync(`${filePath}.tmp`, filePath); } function posixPath(value) { return value.split(path.sep).join('/'); } function relativeToRepo(filePath) { return posixPath(path.relative(repoRoot, filePath)); } function shellQuote(value) { const stringValue = String(value); if (/^[\w@%+=:,./\\-]+$/.test(stringValue)) { return stringValue; } return JSON.stringify(stringValue); } function formatCommand(command, args) { return [command, ...args].map(shellQuote).join(' '); } function formatDuration(ms) { if (ms < 1000) { return `${ms}ms`; } const seconds = ms / 1000; if (seconds < 60) { return `${seconds.toFixed(1)}s`; } const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.round(seconds % 60); return `${minutes}m ${remainingSeconds}s`; } function formatBytes(bytes) { if (bytes < 1024) { return `${bytes} B`; } const kib = bytes / 1024; if (kib < 1024) { return `${kib.toFixed(1)} KiB`; } return `${(kib / 1024).toFixed(2)} MiB`; } function createContext(options) { const runDir = path.resolve(repoRoot, options.outDir); const logsDir = path.join(runDir, 'logs'); const playwrightResultsDir = path.join(runDir, 'playwright-results'); const playwrightReportDir = path.join(runDir, 'playwright-report'); if (runDir === repoRoot || runDir === path.dirname(repoRoot)) { throw new Error(`Refusing to use unsafe validation output directory: ${runDir}`); } fs.rmSync(runDir, { recursive: true, force: true }); ensureDir(logsDir); return { repoRoot, runDir, logsDir, playwrightResultsDir, playwrightReportDir, startedAt: new Date(), options, npm: npmCommand(), baseEnv: { ...process.env, CI: process.env.CI || '1', FORCE_COLOR: '0', NO_COLOR: '1', }, }; } function readPackageJson() { const packagePath = path.join(repoRoot, 'package.json'); if (!fs.existsSync(packagePath)) { throw new Error('package.json was not found at repository root'); } return readJsonFile(packagePath); } function selectPackageScript(packageJson, candidates) { const scripts = packageJson.scripts || {}; return candidates.find((candidate) => Boolean(scripts[candidate])) || null; } function npmScriptArgs(scriptName, extraArgs = []) { return ['run', scriptName, ...(extraArgs.length > 0 ? ['--', ...extraArgs] : [])]; } function npmExecArgs(binary, args = []) { return ['exec', '--', binary, ...args]; } async function runCommand(ctx, gate) { const startedAt = new Date(); const startMs = Date.now(); const logPath = path.join(ctx.logsDir, `${gate.id}.log`); const commandLine = formatCommand(gate.command, gate.args || []); const timeoutMs = gate.timeoutMs ?? ctx.options.defaultTimeoutMs; const env = { ...ctx.baseEnv, ...(gate.env || {}), }; ensureDir(path.dirname(logPath)); const header = [ `# ${gate.label}`, `Gate ID: ${gate.id}`, `Started: ${startedAt.toISOString()}`, `Working directory: ${ctx.repoRoot}`, `Command: ${commandLine}`, `Timeout: ${formatDuration(timeoutMs)}`, '', '--- output ---', '', ].join('\n'); const stream = fs.createWriteStream(logPath, { flags: 'w' }); stream.write(header); if (!ctx.options.quiet) { console.log(`\n▶ ${gate.label}`); console.log(` ${commandLine}`); } return new Promise((resolve) => { let resolved = false; let timedOut = false; let child; const finish = (status, exitCode, signal, errorMessage) => { if (resolved) { return; } resolved = true; const completedAt = new Date(); const durationMs = Date.now() - startMs; const footer = [ '', '--- result ---', `Completed: ${completedAt.toISOString()}`, `Status: ${status}`, `Exit code: ${exitCode === null || exitCode === undefined ? 'n/a' : exitCode}`, `Signal: ${signal || 'n/a'}`, `Duration: ${formatDuration(durationMs)}`, timedOut ? `Timed out after: ${formatDuration(timeoutMs)}` : null, errorMessage ? `Error: ${errorMessage}` : null, '', ] .filter(Boolean) .join('\n'); stream.write(footer); stream.end(() => { if (!ctx.options.quiet) { const marker = status === 'passed' ? '✓' : '✗'; console.log(`${marker} ${gate.label} (${formatDuration(durationMs)})`); } resolve({ id: gate.id, label: gate.label, status, command: commandLine, startedAt: startedAt.toISOString(), completedAt: completedAt.toISOString(), durationMs, exitCode: exitCode ?? null, signal: signal || null, timedOut, log: relativeToRepo(logPath), error: errorMessage || null, }); }); }; const timeout = setTimeout(() => { timedOut = true; if (child && !child.killed) { child.kill('SIGTERM'); setTimeout(() => { if (child && !child.killed) { child.kill('SIGKILL'); } }, 5000).unref(); } }, timeoutMs); timeout.unref(); try { child = spawn(gate.command, gate.args || [], { cwd: ctx.repoRoot, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, }); } catch (error) { clearTimeout(timeout); finish('failed', null, null, error instanceof Error ? error.message : String(error)); return; } child.stdout.on('data', (chunk) => { stream.write(chunk); if (!ctx.options.quiet) { process.stdout.write(chunk); } }); child.stderr.on('data', (chunk) => { stream.write(chunk); if (!ctx.options.quiet) { process.stderr.write(chunk); } }); child.on('error', (error) => { clearTimeout(timeout); finish('failed', null, null, error.message); }); child.on('close', (code, signal) => { clearTimeout(timeout); const status = code === 0 && !timedOut ? 'passed' : 'failed'; finish(status, code, signal, null); }); }); } async function runSkippedGate(ctx, id, label, reason) { const startedAt = new Date(); const logPath = path.join(ctx.logsDir, `${id}.log`); const content = [ `# ${label}`, `Gate ID: ${id}`, `Started: ${startedAt.toISOString()}`, 'Status: skipped', `Reason: ${reason}`, '', ].join('\n'); writeTextFile(logPath, content); if (!ctx.options.quiet) { console.log(`↷ ${label}: skipped (${reason})`); } return { id, label, status: 'skipped', command: null, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), durationMs: 0, exitCode: null, signal: null, timedOut: false, log: relativeToRepo(logPath), error: null, skipReason: reason, }; } async function runEnvironmentGate(ctx, packageJson) { const id = 'environment'; const label = 'Environment snapshot'; const startedAt = new Date(); const startMs = Date.now(); const logPath = path.join(ctx.logsDir, `${id}.log`); const envPath = path.join(ctx.runDir, 'environment.md'); const envJsonPath = path.join(ctx.runDir, 'environment.json'); try { const git = gitMetadata(); const nodeVersion = process.version; const npmVersion = commandOutput(ctx.npm, ['--version']) || 'unknown'; const packageManager = detectPackageManager(); const scripts = packageJson.scripts || {}; const selectedEnvironment = { CI: process.env.CI || '', NODE_ENV: process.env.NODE_ENV || '', VERCEL: process.env.VERCEL || '', MECHANICA_DIST_DIR: process.env.MECHANICA_DIST_DIR || '', MECHANICA_SKIP_INSTALL: process.env.MECHANICA_SKIP_INSTALL || '', MECHANICA_SKIP_PLAYWRIGHT_INSTALL: process.env.MECHANICA_SKIP_PLAYWRIGHT_INSTALL || '', MECHANICA_SKIP_E2E: process.env.MECHANICA_SKIP_E2E || '', }; const environment = { schemaVersion: 1, project: packageJson.name || 'mechanica', generatedAt: startedAt.toISOString(), platform: process.platform, arch: process.arch, cpus: os.cpus().length, totalMemoryBytes: os.totalmem(), nodeVersion, npmVersion, packageManager, git, selectedEnvironment, packageScripts: scripts, validationOptions: ctx.options, }; const markdown = [ '# Mechanica Final Validation Environment', '', `Generated: ${startedAt.toISOString()}`, '', '## Runtime', '', `- Node: \`${nodeVersion}\``, `- npm: \`${npmVersion}\``, `- Package manager: \`${packageManager}\``, `- Platform: \`${process.platform} ${process.arch}\``, `- CPUs: \`${os.cpus().length}\``, `- Total memory: \`${formatBytes(os.totalmem())}\``, '', '## Git', '', `- Branch: \`${git.branch}\``, `- Commit: \`${git.commit}\``, `- Dirty files: ${git.dirtyFiles.length === 0 ? '`none`' : ''}`, ...git.dirtyFiles.map((file) => ` - \`${file}\``), '', '## Selected environment variables', '', '| Variable | Value |', '| --- | --- |', ...Object.entries(selectedEnvironment).map(([key, value]) => `| \`${key}\` | \`${value}\` |`), '', '## Package scripts', '', '| Script | Command |', '| --- | --- |', ...Object.entries(scripts).map( ([key, value]) => `| \`${key}\` | \`${String(value).replaceAll('|', '\\|')}\` |`, ), '', ].join('\n'); writeTextFile(envPath, markdown); writeJsonFile(envJsonPath, environment); writeTextFile( logPath, [ `# ${label}`, `Gate ID: ${id}`, `Started: ${startedAt.toISOString()}`, `Status: passed`, `Environment markdown: ${relativeToRepo(envPath)}`, `Environment JSON: ${relativeToRepo(envJsonPath)}`, '', ].join('\n'), ); return { id, label, status: 'passed', command: null, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), durationMs: Date.now() - startMs, exitCode: 0, signal: null, timedOut: false, log: relativeToRepo(logPath), error: null, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); writeTextFile( logPath, [ `# ${label}`, `Gate ID: ${id}`, `Started: ${startedAt.toISOString()}`, `Status: failed`, `Error: ${message}`, '', ].join('\n'), ); return { id, label, status: 'failed', command: null, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), durationMs: Date.now() - startMs, exitCode: 1, signal: null, timedOut: false, log: relativeToRepo(logPath), error: message, }; } } function detectPackageManager() { const userAgent = process.env.npm_config_user_agent; if (userAgent) { return userAgent.split(' ')[0] || userAgent; } if (fs.existsSync(path.join(repoRoot, 'pnpm-lock.yaml'))) { return 'pnpm'; } if (fs.existsSync(path.join(repoRoot, 'yarn.lock'))) { return 'yarn'; } if (fs.existsSync(path.join(repoRoot, 'package-lock.json'))) { return 'npm+package-lock'; } return 'npm'; } async function runPackageOrFallback(ctx, packageJson, config) { const scriptName = selectPackageScript(packageJson, config.scriptCandidates || []); if (scriptName) { return runCommand(ctx, { id: config.id, label: `${config.label} (npm run ${scriptName})`, command: ctx.npm, args: npmScriptArgs(scriptName, config.scriptExtraArgs || []), timeoutMs: config.timeoutMs, env: config.env, }); } return runCommand(ctx, { id: config.id, label: `${config.label} (${config.fallbackBinary})`, command: ctx.npm, args: npmExecArgs(config.fallbackBinary, config.fallbackArgs || []), timeoutMs: config.timeoutMs, env: config.env, }); } function packageHasLockfile() { return fs.existsSync(path.join(repoRoot, 'package-lock.json')); } function installArgsForCheckout() { if (packageHasLockfile()) { return ['ci', '--no-audit', '--fund=false']; } return ['install', '--no-audit', '--fund=false', '--package-lock=false']; } function unitScriptExtraArgs(packageJson) { const scriptName = selectPackageScript(packageJson, ['test:unit', 'test:run', 'test']); if (!scriptName) { return []; } const script = String((packageJson.scripts || {})[scriptName] || ''); const usesVitest = /\bvitest\b/.test(script); const alreadyRunsOnce = /(^|\s)(run|--run)(\s|$)/.test(script); return usesVitest && !alreadyRunsOnce ? ['--run'] : []; } function playwrightInstallArgs() { return process.platform === 'linux' ? npmExecArgs('playwright', ['install', '--with-deps']) : npmExecArgs('playwright', ['install']); } async function runBundleBudgetGate(ctx) { const id = 'bundle-budget'; const label = 'Bundle budget analysis'; const startedAt = new Date(); const startMs = Date.now(); const logPath = path.join(ctx.logsDir, `${id}.log`); const jsonPath = path.join(ctx.runDir, 'bundle-budget.json'); const markdownPath = path.join(ctx.runDir, 'bundle-budget.md'); try { const report = analyzeBundleBudget(ctx); writeJsonFile(jsonPath, report); writeTextFile(markdownPath, bundleBudgetMarkdown(report)); const status = report.status === 'passed' ? 'passed' : 'failed'; const log = [ `# ${label}`, `Gate ID: ${id}`, `Started: ${startedAt.toISOString()}`, `Status: ${status}`, `Bundle report: ${relativeToRepo(markdownPath)}`, '', report.checks .map( (check) => `${check.passed ? 'PASS' : 'FAIL'} ${check.label}: ${formatBytes( check.actualBytes, )} / ${formatBytes(check.budgetBytes)}`, ) .join('\n'), '', ].join('\n'); writeTextFile(logPath, log); if (!ctx.options.quiet) { const marker = status === 'passed' ? '✓' : '✗'; console.log(`${marker} ${label}`); } return { id, label, status, command: null, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), durationMs: Date.now() - startMs, exitCode: status === 'passed' ? 0 : 1, signal: null, timedOut: false, log: relativeToRepo(logPath), error: status === 'passed' ? null : `Bundle budget failed: ${report.checks .filter((check) => !check.passed) .map((check) => check.label) .join(', ')}`, }; } catch (error) { const message = error instanceof Error ? error.message : String(error); const failedReport = { schemaVersion: 1, project: 'Mechanica', generatedAt: new Date().toISOString(), status: 'failed', error: message, distDir: process.env.MECHANICA_DIST_DIR || 'dist', budgets: budgetConfig(), checks: [], files: [], totals: {}, }; writeJsonFile(jsonPath, failedReport); writeTextFile(markdownPath, bundleBudgetMarkdown(failedReport)); writeTextFile( logPath, [`# ${label}`, `Gate ID: ${id}`, `Started: ${startedAt.toISOString()}`, 'Status: failed', `Error: ${message}`, ''].join( '\n', ), ); return { id, label, status: 'failed', command: null, startedAt: startedAt.toISOString(), completedAt: new Date().toISOString(), durationMs: Date.now() - startMs, exitCode: 1, signal: null, timedOut: false, log: relativeToRepo(logPath), error: message, }; } } function budgetConfig() { return { totalJsGzipBytes: Math.round( parseNumberEnv('MECHANICA_BUDGET_TOTAL_JS_GZIP_KB', 1600) * 1024, ), totalCssGzipBytes: Math.round( parseNumberEnv('MECHANICA_BUDGET_TOTAL_CSS_GZIP_KB', 180) * 1024, ), totalServedGzipBytes: Math.round( parseNumberEnv('MECHANICA_BUDGET_TOTAL_SERVED_GZIP_KB', 2400) * 1024, ), largestJsGzipBytes: Math.round( parseNumberEnv('MECHANICA_BUDGET_LARGEST_JS_GZIP_KB', 1050) * 1024, ), largestAssetRawBytes: Math.round( parseNumberEnv('MECHANICA_BUDGET_LARGEST_ASSET_RAW_KB', 8192) * 1024, ), }; } function analyzeBundleBudget(ctx) { const distDir = path.resolve(repoRoot, process.env.MECHANICA_DIST_DIR || 'dist'); if (!fs.existsSync(distDir) || !fs.statSync(distDir).isDirectory()) { throw new Error(`Build output directory was not found: ${distDir}`); } const files = listFilesRecursive(distDir) .map((absolutePath) => bundleFileInfo(distDir, absolutePath)) .sort((a, b) => a.path.localeCompare(b.path)); if (files.length === 0) { throw new Error(`Build output directory is empty: ${distDir}`); } const servedFiles = files.filter((file) => file.type !== 'sourcemap'); const jsFiles = servedFiles.filter((file) => file.type === 'javascript'); const cssFiles = servedFiles.filter((file) => file.type === 'css'); const budgets = budgetConfig(); const totals = { rawBytes: sum(files, 'rawBytes'), gzipBytes: sum(files, 'gzipBytes'), brotliBytes: sum(files, 'brotliBytes'), servedRawBytes: sum(servedFiles, 'rawBytes'), servedGzipBytes: sum(servedFiles, 'gzipBytes'), servedBrotliBytes: sum(servedFiles, 'brotliBytes'), jsRawBytes: sum(jsFiles, 'rawBytes'), jsGzipBytes: sum(jsFiles, 'gzipBytes'), jsBrotliBytes: sum(jsFiles, 'brotliBytes'), cssRawBytes: sum(cssFiles, 'rawBytes'), cssGzipBytes: sum(cssFiles, 'gzipBytes'), cssBrotliBytes: sum(cssFiles, 'brotliBytes'), }; const largestJs = jsFiles.reduce((largest, file) => { return !largest || file.gzipBytes > largest.gzipBytes ? file : largest; }, null); const largestServedAsset = servedFiles.reduce((largest, file) => { return !largest || file.rawBytes > largest.rawBytes ? file : largest; }, null); const checks = [ { id: 'total-js-gzip', label: 'Total JavaScript gzip', actualBytes: totals.jsGzipBytes, budgetBytes: budgets.totalJsGzipBytes, passed: totals.jsGzipBytes <= budgets.totalJsGzipBytes, }, { id: 'total-css-gzip', label: 'Total CSS gzip', actualBytes: totals.cssGzipBytes, budgetBytes: budgets.totalCssGzipBytes, passed: totals.cssGzipBytes <= budgets.totalCssGzipBytes, }, { id: 'total-served-gzip', label: 'Total served assets gzip', actualBytes: totals.servedGzipBytes, budgetBytes: budgets.totalServedGzipBytes, passed: totals.servedGzipBytes <= budgets.totalServedGzipBytes, }, { id: 'largest-js-gzip', label: `Largest JavaScript chunk gzip${largestJs ? ` (${largestJs.path})` : ''}`, actualBytes: largestJs?.gzipBytes || 0, budgetBytes: budgets.largestJsGzipBytes, passed: (largestJs?.gzipBytes || 0) <= budgets.largestJsGzipBytes, }, { id: 'largest-asset-raw', label: `Largest served asset raw${largestServedAsset ? ` (${largestServedAsset.path})` : ''}`, actualBytes: largestServedAsset?.rawBytes || 0, budgetBytes: budgets.largestAssetRawBytes, passed: (largestServedAsset?.rawBytes || 0) <= budgets.largestAssetRawBytes, }, ]; return { schemaVersion: 1, project: 'Mechanica', generatedAt: new Date().toISOString(), status: checks.every((check) => check.passed) ? 'passed' : 'failed', distDir: relativeToRepo(distDir), budgets, checks, totals, files, topFilesByGzip: [...servedFiles].sort((a, b) => b.gzipBytes - a.gzipBytes).slice(0, 25), topFilesByRaw: [...servedFiles].sort((a, b) => b.rawBytes - a.rawBytes).slice(0, 25), }; } function listFilesRecursive(dir) { const entries = fs.readdirSync(dir, { withFileTypes: true }); const files = []; for (const entry of entries) { const absolutePath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...listFilesRecursive(absolutePath)); continue; } if (entry.isFile()) { files.push(absolutePath); } } return files; } function bundleFileInfo(distDir, absolutePath) { const raw = fs.readFileSync(absolutePath); const relativePath = posixPath(path.relative(distDir, absolutePath)); return { path: relativePath, type: classifyBundleFile(relativePath), rawBytes: raw.length, gzipBytes: gzipSync(raw, { level: 9 }).length, brotliBytes: brotliCompressSync(raw).length, }; } function classifyBundleFile(relativePath) { const ext = path.extname(relativePath).toLowerCase(); if (ext === '.map') { return 'sourcemap'; } if (['.js', '.mjs', '.cjs'].includes(ext)) { return 'javascript'; } if (ext === '.css') { return 'css'; } if (['.html', '.htm'].includes(ext)) { return 'html'; } if (['.wasm'].includes(ext)) { return 'wasm'; } if (['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif', '.svg', '.ico'].includes(ext)) { return 'image'; } if (['.woff', '.woff2', '.ttf', '.otf', '.eot'].includes(ext)) { return 'font'; } if (['.glb', '.gltf', '.bin', '.ktx2', '.hdr', '.exr'].includes(ext)) { return '3d-asset'; } return 'other'; } function sum(files, property) { return files.reduce((total, file) => total + Number(file[property] || 0), 0); } function bundleBudgetMarkdown(report) { if (report.error) { return [ '# Mechanica Bundle Budget', '', `Generated: ${report.generatedAt}`, '', `Status: **FAILED**`, '', `Error: ${report.error}`, '', ].join('\n'); } return [ '# Mechanica Bundle Budget', '', `Generated: ${report.generatedAt}`, `Build output: \`${report.distDir}\``, `Status: **${report.status.toUpperCase()}**`, '', '## Budget checks', '', '| Status | Check | Actual | Budget |', '| --- | --- | ---: | ---: |', ...report.checks.map( (check) => `| ${check.passed ? 'PASS' : 'FAIL'} | ${check.label.replaceAll('|', '\\|')} | ${formatBytes( check.actualBytes, )} | ${formatBytes(check.budgetBytes)} |`, ), '', '## Totals', '', '| Metric | Size |', '| --- | ---: |', `| Raw bytes, all files | ${formatBytes(report.totals.rawBytes)} |`, `| Gzip bytes, all files | ${formatBytes(report.totals.gzipBytes)} |`, `| Brotli bytes, all files | ${formatBytes(report.totals.brotliBytes)} |`, `| Raw bytes, served files | ${formatBytes(report.totals.servedRawBytes)} |`, `| Gzip bytes, served files | ${formatBytes(report.totals.servedGzipBytes)} |`, `| Brotli bytes, served files | ${formatBytes(report.totals.servedBrotliBytes)} |`, `| JavaScript gzip | ${formatBytes(report.totals.jsGzipBytes)} |`, `| CSS gzip | ${formatBytes(report.totals.cssGzipBytes)} |`, '', '## Largest served files by gzip size', '', '| File | Type | Raw | Gzip | Brotli |', '| --- | --- | ---: | ---: | ---: |', ...report.topFilesByGzip.map( (file) => `| \`${file.path.replaceAll('|', '\\|')}\` | ${file.type} | ${formatBytes( file.rawBytes, )} | ${formatBytes(file.gzipBytes)} | ${formatBytes(file.brotliBytes)} |`, ), '', ].join('\n'); } function copyRuntimeArtifactsIntoRun(ctx) { const candidates = [ 'playwright-report', 'playwright-results', 'test-results', 'blob-report', 'coverage', ]; for (const name of candidates) { const source = path.join(repoRoot, name); const destination = path.join(ctx.runDir, name); if (!fs.existsSync(source)) { continue; } const resolvedSource = path.resolve(source); const resolvedDestination = path.resolve(destination); if ( resolvedSource === resolvedDestination || resolvedSource.startsWith(`${resolvedDestination}${path.sep}`) || resolvedSource.startsWith(`${path.resolve(ctx.runDir)}${path.sep}`) ) { continue; } if (fs.existsSync(destination)) { continue; } fs.cpSync(source, destination, { recursive: true, force: true }); } } function finalStatusFor(results) { const requiredFailures = results.filter( (result) => REQUIRED_GATE_IDS.includes(result.id) && result.status !== 'passed', ); const explicitFailures = results.filter((result) => result.status === 'failed'); return requiredFailures.length === 0 && explicitFailures.length === 0 ? 'passed' : 'failed'; } function summaryMarkdown(summary) { const gateRows = summary.gates.map((gate) => { const required = summary.requiredGateIds.includes(gate.id) ? 'yes' : 'no'; const duration = formatDuration(gate.durationMs || 0); const log = gate.log ? `[\`${gate.log}\`](${gate.log})` : ''; return `| ${gate.status.toUpperCase()} | \`${gate.id}\` | ${required} | ${duration} | ${log} |`; }); const failedRequired = summary.gates.filter( (gate) => summary.requiredGateIds.includes(gate.id) && gate.status !== 'passed', ); const failedGates = summary.gates.filter((gate) => gate.status === 'failed'); return [ '# Mechanica Final Validation Summary', '', `Generated: ${summary.generatedAt}`, `Started: ${summary.startedAt}`, `Completed: ${summary.completedAt}`, `Status: **${summary.status.toUpperCase()}**`, `Run directory: \`${summary.runDirectory}\``, '', '## Gate results', '', '| Status | Gate | Required | Duration | Log |', '| --- | --- | --- | ---: | --- |', ...gateRows, '', '## Required follow-up', '', summary.status === 'passed' ? 'All required validation gates passed. Collect evidence with `node scripts/collect-validation-evidence.mjs` and verify it with `node scripts/verify-validation-evidence.mjs `.' : [ 'The validation run is not passing yet. Inspect the failed gate logs, fix the underlying source/test/configuration issue, and rerun this script on a clean checkout or CI runner.', '', ...failedRequired.map( (gate) => `- Required gate \`${gate.id}\` ended with status \`${gate.status}\`.`, ), ...failedGates .filter((gate) => !failedRequired.some((requiredGate) => requiredGate.id === gate.id)) .map((gate) => `- Gate \`${gate.id}\` failed.`), ].join('\n'), '', '## Artifact index', '', `- Environment: [\`environment.md\`](environment.md)`, `- Bundle budget: [\`bundle-budget.md\`](bundle-budget.md)`, `- Logs: [\`logs/\`](logs/)`, `- Playwright report: [\`playwright-report/\`](playwright-report/) if generated`, `- Playwright traces/results: [\`playwright-results/\`](playwright-results/) if generated`, '', ].join('\n'); } function writeSummary(ctx, results) { const completedAt = new Date(); const status = finalStatusFor(results); const summary = { schemaVersion: 1, project: 'Mechanica', generatedAt: completedAt.toISOString(), startedAt: ctx.startedAt.toISOString(), completedAt: completedAt.toISOString(), status, runDirectory: relativeToRepo(ctx.runDir), requiredGateIds: REQUIRED_GATE_IDS, optionalGateIds: OPTIONAL_GATE_IDS, gates: results, artifacts: { validationSummaryMarkdown: relativeToRepo(path.join(ctx.runDir, 'validation-summary.md')), validationSummaryJson: relativeToRepo(path.join(ctx.runDir, 'validation-summary.json')), bundleBudgetMarkdown: relativeToRepo(path.join(ctx.runDir, 'bundle-budget.md')), bundleBudgetJson: relativeToRepo(path.join(ctx.runDir, 'bundle-budget.json')), environmentMarkdown: relativeToRepo(path.join(ctx.runDir, 'environment.md')), logsDirectory: relativeToRepo(path.join(ctx.runDir, 'logs')), playwrightReportDirectory: relativeToRepo(path.join(ctx.runDir, 'playwright-report')), playwrightResultsDirectory: relativeToRepo(path.join(ctx.runDir, 'playwright-results')), }, git: gitMetadata(), }; writeJsonFile(path.join(ctx.runDir, 'validation-summary.json'), summary); writeTextFile(path.join(ctx.runDir, 'validation-summary.md'), summaryMarkdown(summary)); return summary; } async function main() { const options = parseArgs(process.argv.slice(2)); const ctx = createContext(options); const packageJson = readPackageJson(); const results = []; console.log('Mechanica final validation started.'); console.log(`Artifacts: ${relativeToRepo(ctx.runDir)}`); results.push(await runEnvironmentGate(ctx, packageJson)); if (ctx.options.skipInstall) { results.push( await runSkippedGate( ctx, 'install', 'Dependency installation', 'Skipped by --skip-install or MECHANICA_SKIP_INSTALL', ), ); } else { results.push( await runCommand(ctx, { id: 'install', label: packageHasLockfile() ? 'Dependency installation (npm ci)' : 'Dependency installation (npm install without lockfile mutation)', command: ctx.npm, args: installArgsForCheckout(), timeoutMs: Math.max(ctx.options.defaultTimeoutMs, 1200 * 1000), }), ); } results.push( await runPackageOrFallback(ctx, packageJson, { id: 'format', label: 'Prettier format check', scriptCandidates: ['format:check', 'prettier:check'], fallbackBinary: 'prettier', fallbackArgs: ['.', '--check'], }), ); results.push( await runPackageOrFallback(ctx, packageJson, { id: 'typecheck', label: 'TypeScript type check', scriptCandidates: ['typecheck', 'type-check', 'check:types'], fallbackBinary: 'tsc', fallbackArgs: ['-p', 'tsconfig.json', '--noEmit'], }), ); results.push( await runPackageOrFallback(ctx, packageJson, { id: 'lint', label: 'ESLint', scriptCandidates: ['lint'], fallbackBinary: 'eslint', fallbackArgs: ['.', '--max-warnings=0'], }), ); results.push( await runPackageOrFallback(ctx, packageJson, { id: 'unit', label: 'Unit and component tests', scriptCandidates: ['test:unit', 'test:run', 'test'], scriptExtraArgs: unitScriptExtraArgs(packageJson), fallbackBinary: 'vitest', fallbackArgs: ['run'], }), ); results.push( await runPackageOrFallback(ctx, packageJson, { id: 'build', label: 'Production build', scriptCandidates: ['build'], fallbackBinary: 'vite', fallbackArgs: ['build'], timeoutMs: Math.max(ctx.options.defaultTimeoutMs, 1200 * 1000), }), ); results.push(await runBundleBudgetGate(ctx)); if (ctx.options.skipE2E) { results.push( await runSkippedGate(ctx, 'playwright-install', 'Playwright browser installation', 'Skipped because E2E was skipped'), ); results.push( await runSkippedGate(ctx, 'e2e', 'Playwright end-to-end tests', 'Skipped by --skip-e2e or MECHANICA_SKIP_E2E'), ); } else { if (ctx.options.skipPlaywrightInstall) { results.push( await runSkippedGate( ctx, 'playwright-install', 'Playwright browser installation', 'Skipped by --skip-playwright-install or MECHANICA_SKIP_PLAYWRIGHT_INSTALL', ), ); } else { results.push( await runCommand(ctx, { id: 'playwright-install', label: 'Playwright browser installation', command: ctx.npm, args: playwrightInstallArgs(), timeoutMs: Math.max(ctx.options.defaultTimeoutMs, 1200 * 1000), }), ); } const playwrightArgs = [ '--output', ctx.playwrightResultsDir, '--reporter', 'list,html', ]; results.push( await runPackageOrFallback(ctx, packageJson, { id: 'e2e', label: 'Playwright end-to-end tests', scriptCandidates: ['test:e2e', 'e2e'], scriptExtraArgs: playwrightArgs, fallbackBinary: 'playwright', fallbackArgs: ['test', ...playwrightArgs], timeoutMs: ctx.options.e2eTimeoutMs, env: { PLAYWRIGHT_HTML_REPORT: ctx.playwrightReportDir, }, }), ); } copyRuntimeArtifactsIntoRun(ctx); const summary = writeSummary(ctx, results); if (summary.status === 'passed') { console.log('\n✓ Mechanica final validation passed.'); console.log(`Summary: ${relativeToRepo(path.join(ctx.runDir, 'validation-summary.md'))}`); console.log('Next: node scripts/collect-validation-evidence.mjs'); process.exitCode = 0; } else { console.error('\n✗ Mechanica final validation failed.'); console.error(`Summary: ${relativeToRepo(path.join(ctx.runDir, 'validation-summary.md'))}`); console.error('Inspect the failed gate logs under the validation artifact directory.'); process.exitCode = 1; } } main().catch((error) => { console.error(error instanceof Error ? error.stack || error.message : String(error)); process.exitCode = 1; });