/** * cli:scaffold-data-scope — validate.ts * * Zod parse + cross-entity coherence. Fail-closed on anything that would emit * a policy that cannot compile or that silently filters on the wrong column. */ import { DataScopeSpecSchema, type ValidationResult } from './types.js'; export function validate(raw: unknown): ValidationResult { const parsed = DataScopeSpecSchema.safeParse(raw); if (!parsed.success) { return { valid: false, errors: parsed.error.issues.map(i => `[${i.path.join('.')}] ${i.message}`), warnings: [], }; } const spec = parsed.data; const errors: string[] = []; const warnings: string[] = []; const seen = new Set(); for (const e of spec.entities) { if (seen.has(e.entityName)) { errors.push(`duplicate entity "${e.entityName}" — one DataScopePolicy per entity type (the platform registry rejects duplicates at boot)`); } seen.add(e.entityName); if (e.mode === 'own-assigned' && e.ownerProperty === e.assignedProperty) { errors.push(`entity "${e.entityName}": ownerProperty and assignedProperty are both "${e.ownerProperty}" — own-assigned needs two distinct columns`); } // The permission path should be app-qualified (app.module.section.read = 4 segments). if (e.readPermission.split('.').length < 4) { warnings.push( `entity "${e.entityName}": readPermission "${e.readPermission}" has fewer than 4 segments — ` + `expected the app-qualified "{app}.{module}.{section}.read" so the derived ".all" row matches the seeded permission paths.`, ); } } return { valid: errors.length === 0, errors, warnings, data: spec }; }