/** * validate command * Validate a .specly specification * Generated from SpecVerse specification */ import { Command } from 'commander'; import { readFileSync, existsSync } from 'fs'; import { EngineRegistry } from '@specverse/entities'; import { summarizeSpecForCli } from '@specverse/engines/parser'; import type { ParserEngine } from '@specverse/types'; interface CommandOptions { json?: boolean; strict?: boolean; verify?: boolean; verbose?: boolean; full?: boolean; } /** * Exit codes: // 0: Valid // 1: Errors found */ /** * Register the validate command on the program. */ export function registerValidateCommand(program: Command): void { program .command('validate ') .description('Validate a .specly specification') .option('--json', 'Output results in JSON format', false) .option('--strict', 'Enable strict validation mode', false) .option('--verify', 'Also run L3 Quint invariant verification. Guards whose dependencies are missing from this spec (e.g. inferredControllers before running infer) are skipped, not failed.', false) .option('--verbose', 'Show full details for verification failures and skipped guards', false) .option('--full', 'With --json, also attach the complete parsed AST (the \'complete\' level; --json alone emits the curated spec summary)', false) .action(async (file: string, options: CommandOptions) => { try { if (!existsSync(file)) { console.error('File not found:', file); process.exit(1); } // Discover and initialize parser engine const registry = new EngineRegistry(); await registry.discover(); const parser = registry.getEngineForCapability('parse') as ParserEngine; if (!parser) { console.error('No parser engine found. Install @specverse/engines.'); process.exit(1); } await parser.initialize(); const content = readFileSync(file, 'utf8'); const result = parser.parseContent(content, file); if (result.errors.length > 0) { console.error('Validation failed'); result.errors.forEach((e: string) => console.error(' ', e)); if (result.warnings && result.warnings.length > 0) { console.warn('Warnings:'); result.warnings.forEach((w: string) => console.warn(' ', w)); } process.exit(1); } // Lifecycle + cross-model soundness check (TODO #31). Runs as // part of normal validate — every spec gets these deterministic // gates regardless of flags. Catches lifecycle typos // (transition references unknown state), behavior-requires / // ensures referencing unknown states, and relationship targets // pointing at undeclared models. Violations are validation // errors and abort with exit 1, just like schema errors. try { const { generateQuintFromSpec } = await import('@specverse/engines/inference/quint-gen'); const quintGen = generateQuintFromSpec(result.ast); if (quintGen.violations.length > 0) { if (options.json) { console.log(JSON.stringify({ valid: false, violations: quintGen.violations }, null, 2)); } else { console.error('Validation failed'); for (const v of quintGen.violations) { console.error(' [' + v.kind + '] ' + v.message); } } process.exit(1); } } catch (qe: any) { // Don't let quint-gen module-loading issues block normal // validate (e.g. running against a much older engines version). if (process.env.SPECVERSE_VERBOSE === '2') { console.warn('quint-gen check skipped:', qe?.message ?? String(qe)); } } if (options.json) { // 'useful' level: curated parse facts (name/version/counts + // imports/exports for scope + deployment/manifest names). '--full' // additionally attaches the complete parsed AST. const spec = summarizeSpecForCli(result.ast, { specVersion: result.specVersion, specName: result.specName }); const jsonOut = options.full ? { valid: true, warnings: result.warnings, spec, ast: result.ast } : { valid: true, warnings: result.warnings, spec }; console.log(JSON.stringify(jsonOut, null, 2)); } else { console.log('Validation successful'); if (result.warnings && result.warnings.length > 0) { console.warn('Warnings:'); result.warnings.forEach((w: string) => console.warn(' ', w)); } } // L3 verification — opt-in via --verify flag. Runs the // transpiled Quint guards against the parsed spec. Guards // whose state-var dependencies aren't present in this spec // are reported as skipped (not failed). if (options.verify) { try { const { verifySpec, formatVerificationResult } = await import('@specverse/engines/inference'); const verification = await verifySpec(result.ast); if (options.json) { console.log(JSON.stringify({ valid: true, verification }, null, 2)); } else { console.log(''); console.log(formatVerificationResult(verification, options.verbose)); } if (verification.failed.length > 0) process.exit(1); } catch (ve: any) { console.error('Verification error:', ve.message); process.exit(1); } } } catch (error: any) { console.error('Error:', error.message); process.exit(1); } }); }