/** * cli:derive-referential-codes — validate.ts * * Spec validation + prerequisites. An empty report must mean « no reference * table carries a code », never « the inputs were absent »: the module folder * and its `entité.md` are both existence-gated, and the error names the skill * that produces the missing file. * * The PRD is deliberately NOT a prerequisite: /ba-audit-data-model runs BEFORE * /ba-create-prd in the pipeline, so most runs legitimately have no PRD. Its * absence is reported as PARTIAL COVERAGE (see `SourceCoverage.prdPresent`), * never as a failure. */ import { existsSync, statSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { DeriveReferentialCodesInputSchema, type ValidationResult } from './types.js' /** entité.md exists on disk under either Unicode normalization. */ export function entiteDocPath(moduleDir: string): string | null { for (const fileName of ['entité.md', 'entité.md'.normalize('NFD')]) { const p = join(moduleDir, fileName) try { if (existsSync(p) && statSync(p).isFile()) return p } catch { /* unreadable — treated as absent */ } } return null } export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = DeriveReferentialCodesInputSchema.safeParse(raw) if (!parsed.success) { return { valid: false, errors: parsed.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`), } } const spec = parsed.data const resolvedBaRoot = isAbsolute(spec.baRoot) ? spec.baRoot : join(workdir ?? process.cwd(), spec.baRoot) const errors: string[] = [] const moduleDir = join(resolvedBaRoot, spec.app, spec.module) if (!existsSync(moduleDir)) { errors.push(`Module folder not found: ${moduleDir}`) return { valid: false, errors, spec, resolvedBaRoot } } if (entiteDocPath(moduleDir) === null) { errors.push( `${spec.app}/${spec.module} has no entité.md — run /ba-create-data-model (phase 6) first.`, ) } return { valid: errors.length === 0, errors, spec, resolvedBaRoot } }