/** * cli:build-manifest — validate.ts */ import { BuildManifestInputSchema, type BuildManifestInput, type ValidationResult } from './types.js'; export function validate(raw: unknown): ValidationResult & { data?: BuildManifestInput } { const result = BuildManifestInputSchema.safeParse(raw); if (!result.success) { const errors = result.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`); return { valid: false, errors, warnings: [] }; } const spec = result.data; const errors: string[] = []; const warnings: string[] = []; for (const e of spec.entities) { const allRoles = new Set([ ...e.rolesWithRead, ...e.rolesWithCreate, ...e.rolesWithUpdate, ...e.rolesWithDelete, ...e.rolesWithoutAccess, ]); if (allRoles.size === 0) { warnings.push(`Entity ${e.name} has no roles defined — no tests will be emitted for it.`); } if (e.rolesWithCreate.length > 0 && Object.keys(e.fixture).length === 0) { warnings.push(`Entity ${e.name} has create roles but no fixture — form-submit tests will submit an empty form (likely fail validators).`); } // Sanity: rolesWithoutAccess shouldn't overlap with read roles const readSet = new Set(e.rolesWithRead); for (const role of e.rolesWithoutAccess) { if (readSet.has(role)) { errors.push(`Entity ${e.name}: role "${role}" is in both rolesWithRead and rolesWithoutAccess — pick one.`); } } } return { valid: errors.length === 0, errors, warnings, data: spec }; }