#!/usr/bin/env node /** * cli:readiness-report * Aggregates the results of the 3 validators (roslyn, eslint, cross-validate), * computes a /100 score, decides GO / NO-GO / STOP. * * Usage: * npx --prefer-offline tsx skills/validation/readiness-report/cli/index.ts \ * --specs --src [--json] * * Exit codes: * 0 GO (score >= 90) * 1 NO-GO (score 70-89) * 2 STOP (score < 70) or internal error */ import { parseArgs } from 'node:util' import { resolve } from 'node:path' import { validate } from './validate' import { execute } from './execute' import type { ReadinessReport } from './types' // ─── Parse args ─── const { values } = parseArgs({ options: { specs: { type: 'string' }, src: { type: 'string' }, json: { type: 'boolean', default: false }, help: { type: 'boolean', short: 'h' }, }, strict: true, }) if (values.help) { const paren = String.fromCharCode(41) console.log(`cli:readiness-report — SmartStack Studio Aggregates the results of the 3 validators and computes a /100 score. Sources: roslyn-validator 40 pts (4 pts x 10 rules SS001-SS010${paren} eslint-smartstack 25 pts (5 pts x 5 ss/ rules${paren} cross-validate-stack 35 pts (35 pts over active checks (retired ones excluded)${paren} Thresholds: >= 90 GO — Phase 6 development allowed 70-89 NO-GO — Fixes required < 70 STOP — Back to Phase 3 or 4 Usage: npx --prefer-offline tsx skills/validation/readiness-report/cli/index.ts \\ --specs --src [--json] Options: --specs Directory of the JSON specs (required${paren} --src Source code directory (required${paren} --json JSON output only (no fancy console${paren} -h, --help Show this help Exit codes: 0 GO (score >= 90${paren} 1 NO-GO (score 70-89${paren} 2 STOP (score < 70${paren} or internal error`) process.exit(0) } if (!values.specs || !values.src) { const missing: string[] = [] if (!values.specs) missing.push('--specs') if (!values.src) missing.push('--src') console.error(JSON.stringify({ timestamp: new Date().toISOString(), scores: {}, total: 0, max: 100, recommendation: 'STOP', blockers: missing.map(m => `${m} is required`), }, null, 2)) process.exit(2) } // ─── Resolve paths ─── const specsPath = resolve(values.specs) const srcPath = resolve(values.src) // ─── Validate input ─── const validation = validate({ specs: specsPath, src: srcPath }) if (!validation.valid) { const errorReport = { timestamp: new Date().toISOString(), scores: {}, total: 0, max: 100, recommendation: 'STOP', blockers: validation.blockers, } if (values.json) { console.log(JSON.stringify(errorReport, null, 2)) } else { console.error(`[STOP] Validation failed:`) for (const b of validation.blockers) { console.error(` - ${b}`) } } process.exit(2) } // ─── Execute ─── let report: ReadinessReport try { report = execute(validation.data!) } catch (err) { const message = err instanceof Error ? err.message : String(err) const errorReport = { timestamp: new Date().toISOString(), scores: {}, total: 0, max: 100, recommendation: 'STOP', blockers: [`Internal error: ${message}`], } if (values.json) { console.log(JSON.stringify(errorReport, null, 2)) } else { console.error(`[STOP] Internal error: ${message}`) } process.exit(2) } // ─── Output ─── if (values.json) { console.log(JSON.stringify(report, null, 2)) } else { printFancyReport(report) } // ─── Exit code ─── if (report.recommendation === 'GO') { process.exit(0) } else if (report.recommendation === 'NO-GO') { process.exit(1) } else { process.exit(2) } // ─── Fancy console output ─── function printFancyReport(r: ReadinessReport): void { const W = 42 // inner width const checkMark = String.fromCharCode(10003) // ✓ const warningMark = String.fromCharCode(9888) // ⚠ const crossMark = String.fromCharCode(10007) // ✗ function statusIcon(status: string): string { if (status === 'ok') return checkMark if (status === 'warning') return warningMark if (status === 'error') return crossMark return '?' } function recIcon(rec: string): string { if (rec === 'GO') return checkMark if (rec === 'NO-GO') return warningMark return crossMark } function pad(s: string, len: number): string { if (s.length >= len) return s.substring(0, len) return s + ' '.repeat(len - s.length) } function padLeft(s: string, len: number): string { if (s.length >= len) return s.substring(0, len) return ' '.repeat(len - s.length) + s } const topLine = '\u2554' + '\u2550'.repeat(W) + '\u2557' const midLine = '\u2560' + '\u2550'.repeat(W) + '\u2563' const botLine = '\u255A' + '\u2550'.repeat(W) + '\u255D' const pipe = '\u2551' console.log(topLine) console.log(`${pipe} SmartStack Readiness Report${' '.repeat(W - 31)}${pipe}`) console.log(midLine) // Score lines const sources: Array<{ label: string; key: 'roslyn' | 'eslint' | 'cross-validate' }> = [ { label: 'roslyn-validator', key: 'roslyn' }, { label: 'eslint-smartstack', key: 'eslint' }, { label: 'cross-validate-stack', key: 'cross-validate' }, ] for (const s of sources) { const sc = r.scores[s.key] const scoreStr = `${sc.score}/${sc.max}` const icon = statusIcon(sc.status) // Format: " label score icon " const labelPart = pad(` ${s.label}`, 24) const scorePart = padLeft(scoreStr, 7) const iconPart = ` ${icon}` const lineContent = labelPart + scorePart + iconPart const remaining = W - lineContent.length const line = lineContent + ' '.repeat(Math.max(0, remaining)) console.log(`${pipe}${line}${pipe}`) } console.log(midLine) // Total const totalStr = `${r.total}/${r.max}` const totalLabel = pad(' TOTAL SCORE', 24) const totalScore = padLeft(totalStr, 7) const totalRemaining = W - totalLabel.length - totalScore.length const totalLine = totalLabel + totalScore + ' '.repeat(Math.max(0, totalRemaining)) console.log(`${pipe}${totalLine}${pipe}`) // Recommendation const recStr = `${recIcon(r.recommendation)} ${r.recommendation}` const recLabel = pad(' RECOMMENDATION', 24) const recRemaining = W - recLabel.length - recStr.length const recLine = recLabel + recStr + ' '.repeat(Math.max(0, recRemaining)) console.log(`${pipe}${recLine}${pipe}`) console.log(botLine) // Blockers summary if (r.blockers.length > 0) { console.log('') console.log(`Blockers (${r.blockers.length}):`) for (const b of r.blockers) { console.log(` - ${b}`) } } }