/** * cli:derive-external-api-spec — validate.ts */ import { existsSync } from 'node:fs' import { join } from 'node:path' import { DeriveExternalApiSpecInputSchema, type ValidationResult } from './types.js' export function validate(raw: unknown): ValidationResult { const parsed = DeriveExternalApiSpecInputSchema.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[] = [] if (!existsSync(spec.baRoot)) { errors.push(`baRoot does not exist: ${spec.baRoot}`) } else if (!existsSync(join(spec.baRoot, spec.applicationCode))) { errors.push(`No application "${spec.applicationCode}" under ${spec.baRoot} — check the BA menu tree.`) } if (spec.mode === 'write' && spec.declarations.length === 0) { errors.push('mode "write" with no declarations[] — nothing to record. Recording an empty decision would silently retire a published API.') } if (spec.mode === 'check' && spec.declarations.length > 0) { warnings.push('declarations[] are ignored in mode "check" — nothing was written.') } for (const d of spec.declarations) { if (d.operations.length > 0 && !d.operations.includes('read')) { errors.push(`${d.entity}: a write-only declaration leaves the third party unable to read back what it wrote, and with no id to address.`) } } return { valid: errors.length === 0, errors, warnings } }