/** * cli:derive-rule-links — validate.ts * * Spec validation + prerequisites: the module folder must exist and carry at * least one `règles-métier.md` (module or section level) AND a `pagespecs/` * dir — an empty report must mean "nothing to link", never "inputs absent". */ import { existsSync, readdirSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { DeriveRuleLinksInputSchema, type ValidationResult } from './types.js' export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = DeriveRuleLinksInputSchema.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 } } let anyRules = existsSync(join(moduleDir, 'règles-métier.md')) if (!anyRules) { try { anyRules = readdirSync(moduleDir, { withFileTypes: true }).some( (e) => e.isDirectory() && /^[a-z]/.test(e.name) && existsSync(join(moduleDir, e.name, 'règles-métier.md')), ) } catch { /* fallthrough to the error below */ } } if (!anyRules) { errors.push( `${spec.app}/${spec.module} has no règles-métier.md (module or section) — run /ba-create-business-rules (phase 4) first.`, ) } if (!existsSync(join(moduleDir, 'pagespecs'))) { errors.push(`${moduleDir}/pagespecs not found — run /ba-create-prd before deriving the rule links.`) } return { valid: errors.length === 0, errors, spec, resolvedBaRoot } }