/** * cli:audit-ba — validate.ts * * Spec validation + prerequisites: the BA root must exist and contain at * least one application folder — an empty report must mean « corpus vide », * never « chemin faux ». */ import { existsSync, readdirSync, statSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { AuditBaInputSchema, type AuditBaInput } from './types.js' export interface ValidationResult { valid: boolean errors: string[] spec?: AuditBaInput resolvedBaRoot?: string resolvedProjectRoot?: string } export function validateSpec(raw: unknown, workdir?: string): ValidationResult { const parsed = AuditBaInputSchema.safeParse(raw) if (!parsed.success) { return { valid: false, errors: parsed.error.issues.map((i) => `[${i.path.join('.')}] ${i.message}`) } } const spec = parsed.data const base = workdir ?? process.cwd() const resolvedBaRoot = isAbsolute(spec.baRoot) ? spec.baRoot : join(base, spec.baRoot) const errors: string[] = [] if (!existsSync(resolvedBaRoot)) { errors.push(`BA root not found: ${resolvedBaRoot}`) return { valid: false, errors, spec } } try { const hasApp = readdirSync(resolvedBaRoot, { withFileTypes: true }).some( (e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'), ) if (!hasApp) errors.push(`BA root has no application folder: ${resolvedBaRoot} — run /ba-create-menu first.`) } catch { errors.push(`BA root is not readable: ${resolvedBaRoot}`) } let resolvedProjectRoot: string | undefined if (spec.projectRoot !== undefined) { resolvedProjectRoot = isAbsolute(spec.projectRoot) ? spec.projectRoot : join(base, spec.projectRoot) try { if (!existsSync(resolvedProjectRoot) || !statSync(resolvedProjectRoot).isDirectory()) { errors.push(`projectRoot is not a directory: ${resolvedProjectRoot}`) } } catch { errors.push(`projectRoot is not readable: ${resolvedProjectRoot}`) } } return { valid: errors.length === 0, errors, spec, resolvedBaRoot, ...(resolvedProjectRoot !== undefined ? { resolvedProjectRoot } : {}), } }