/** * cli:derive-lookup-grants — validate.ts * * Spec validation + prerequisite checks. The derivation is only meaningful * once BOTH upstream phases exist for the consumer module: the RBAC matrix * (phase 5 — grantees come from its create/update rows) and the data model * (phase 6 — FKs come from entité.md). A missing/placeholder prerequisite is * a hard error, not an empty block: an empty machine block must mean "the * derivation ran and found nothing", never "the inputs were absent". */ import { existsSync, readFileSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { isPlaceholderEntityDoc } from '../../../../lib/ba-placeholder.js' import { DeriveLookupGrantsInputSchema, type ValidationResult } from './types.js' export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = DeriveLookupGrantsInputSchema.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 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(`Consumer module folder not found: ${moduleDir}`) } else { const rbacPath = join(moduleDir, 'rbac.md') if (!existsSync(rbacPath)) { errors.push( `${spec.app}/${spec.module}/rbac.md not found — run /ba-create-rbac (phase 5) before deriving lookup grants.`, ) } const entitePath = join(moduleDir, 'entité.md') if (!existsSync(entitePath)) { errors.push( `${spec.app}/${spec.module}/entité.md not found — run /ba-create-data-model (phase 6) before deriving lookup grants.`, ) } else { try { // No entity block at all — never a marker test on the whole file, which // refused an authored model over one leftover line (lib/ba-placeholder). if (isPlaceholderEntityDoc(readFileSync(entitePath, 'utf8'))) { errors.push( `${spec.app}/${spec.module}/entité.md is still a placeholder — author the data model (phase 6) first.`, ) } } catch { errors.push(`Cannot read ${entitePath}`) } } } return { valid: errors.length === 0, errors, warnings: [], spec, resolvedBaRoot } }