/** * cli:derive-extension-request — validate.ts * * Spec validation + prerequisites. An empty reconciliation must mean « no * module of this application asks anything of another », never « the inputs * were absent »: the application folder and at least one `entité.md` are * existence-gated, and a `projectPath` that does not exist is refused rather * than silently dropping the code leg. */ import { existsSync, readdirSync, statSync } from 'node:fs' import { isAbsolute, join } from 'node:path' import { DeriveExtensionRequestInputSchema, type ValidationResult } from './types.js' /** Module folders of an application — `_`/`.` folders and pagespecs excluded, sorted. */ export function listModuleFolders(appDir: string): string[] { try { return readdirSync(appDir, { withFileTypes: true }) .filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.') && e.name !== 'pagespecs' && e.name !== 'node_modules') .map((e) => e.name) .sort() } catch { return [] } } 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 = DeriveExtensionRequestInputSchema.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[] = [] const appDir = join(resolvedBaRoot, spec.app) if (!existsSync(appDir)) { errors.push(`Application folder not found: ${appDir}`) return { valid: false, errors, spec, resolvedBaRoot } } const modules = listModuleFolders(appDir).filter((m) => entiteDocPath(join(appDir, m)) !== null) if (modules.length === 0) { errors.push(`${spec.app} has no module carrying an entité.md — run /ba-create-data-model (phase 6) first.`) } if (spec.module !== undefined && !listModuleFolders(appDir).some((m) => m.toLowerCase() === spec.module!.toLowerCase())) { errors.push(`Module folder not found: ${join(appDir, spec.module)}`) } let resolvedProjectPath: string | undefined if (spec.projectPath !== undefined) { resolvedProjectPath = isAbsolute(spec.projectPath) ? spec.projectPath : join(base, spec.projectPath) if (!existsSync(resolvedProjectPath) || !statSync(resolvedProjectPath).isDirectory()) { errors.push(`projectPath does not exist: ${resolvedProjectPath} — omit it for the BA leg alone.`) } } return { valid: errors.length === 0, errors, spec, resolvedBaRoot, ...(resolvedProjectPath !== undefined ? { resolvedProjectPath } : {}), } }