/** * cli:derive-related-tabs — validate.ts * * Stage 1: Zod structural validation (strict — unknown keys rejected). * Stage 2: existence checks (baRoot dir, app dir), with `baRoot` resolved * against `--workdir` (or cwd). A missing MODULE dir is only a * warning — the run then legitimately reports zero screens/tabs. */ import { existsSync, statSync } from 'node:fs' import { join, resolve } from 'node:path' import { DeriveRelatedTabsInputSchema } from './types.js' import type { ValidationResult } from './types.js' export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = DeriveRelatedTabsInputSchema.safeParse(raw) if (!parsed.success) { return { valid: false, errors: parsed.error.issues.map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`), warnings: [], } } const spec = parsed.data const warnings: string[] = [] const resolvedBaRoot = resolve(workdir ?? process.cwd(), spec.baRoot) if (!isDirectory(resolvedBaRoot)) { return { valid: false, errors: [`BA root directory not found: ${resolvedBaRoot}`], warnings, } } const appDir = join(resolvedBaRoot, spec.app) if (!isDirectory(appDir)) { return { valid: false, errors: [`Application directory not found: ${appDir} (app code: ${spec.app})`], warnings, } } if (!isDirectory(join(appDir, spec.module))) { warnings.push( `Module directory not found: ${spec.app}/${spec.module} — the run will report zero screens`, ) } if (spec.pagespecs && spec.mode !== 'validate') { warnings.push('pagespecs: true is only honored in mode "validate" — ignored in derive mode') } return { valid: true, errors: [], warnings, spec, resolvedBaRoot } } function isDirectory(p: string): boolean { try { return existsSync(p) && statSync(p).isDirectory() } catch { return false } }