/** * cli:derive-permission-floor — validate.ts * * Spec validation + prerequisite checks. The floor mirrors the MENU tree — * the only prerequisite is the module folder + its rbac.md (phase 5 wrote * it; the mirror block needs a file to live in). No entité.md dependency: * the floor exists for every node regardless of the data model. */ import { existsSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { DerivePermissionFloorInputSchema, type ValidationResult } from './types.js' export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = DerivePermissionFloorInputSchema.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(`Module folder not found: ${moduleDir}`) } else if (!existsSync(join(moduleDir, 'rbac.md'))) { errors.push( `${spec.app}/${spec.module}/rbac.md not found — run /ba-create-rbac (phase 5) before mirroring the permission floor.`, ) } return { valid: errors.length === 0, errors, warnings: [], spec, resolvedBaRoot } }