/** * cli:derive-seed-delta — validate.ts * * Two-stage validation: * 1. Structural (Zod) — schema parse, dotted-numeric version. * 2. Business — resolvedRenames coherence (navigation entries need a level, * from ≠ to, no double-claim of the same source key). */ import { DeriveSeedDeltaSpecSchema, type DeriveSeedDeltaSpec, type ValidationResult } from './types.js'; export function validateStructure(raw: unknown): ValidationResult { const result = DeriveSeedDeltaSpecSchema.safeParse(raw); if (result.success) return { valid: true, errors: [], warnings: [] }; const errors = result.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`); return { valid: false, errors, warnings: [] }; } export function validateBusinessRules(spec: DeriveSeedDeltaSpec): ValidationResult { const errors: string[] = []; const warnings: string[] = []; const claimed = new Set(); for (const r of spec.resolvedRenames) { const ctx = `resolvedRenames[${r.kind}:${r.from}->${r.to}]`; if (r.kind === 'navigation' && !r.level) { errors.push(`${ctx}: navigation renames require 'level' (application|module|section|resource).`); } if (r.from === r.to) { errors.push(`${ctx}: 'from' and 'to' are identical — not a rename.`); } const key = `${r.app}|${r.kind}|${r.level ?? ''}|${r.parentCode ?? ''}|${r.from}`; if (claimed.has(key)) { errors.push(`${ctx}: source '${r.from}' is claimed by more than one resolution in the same scope.`); } claimed.add(key); } return { valid: errors.length === 0, errors, warnings }; } export function validate(raw: unknown): ValidationResult { const structural = validateStructure(raw); if (!structural.valid) return structural; const spec = DeriveSeedDeltaSpecSchema.parse(raw); return validateBusinessRules(spec); }