// Parser voor de plan_md die make-plan-job produceert. // Format: yaml-frontmatter (structuur, parseerbaar) + markdown-body (vrije // reasoning). Frontmatter wordt gevalideerd via ideaPlanMdFrontmatterSchema. // // Wordt zowel door de server-action materializeIdeaPlanAction als door de // MCP-tool update_idea_plan_md gebruikt. Synchroon — geen LLM-call. // // Zie docs/plans/M12-ideas.md "Plan-md formaat A" voor het format-voorbeeld. import { parse as parseYaml, YAMLParseError } from 'yaml' import { ideaPlanMdFrontmatterSchema, type IdeaPlanFrontmatter, } from '@/lib/schemas/idea' export type PlanParseError = { line?: number; message: string; hint?: string } export type PlanParseResult = | { ok: true; plan: IdeaPlanFrontmatter; body: string } | { ok: false; errors: PlanParseError[] } const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/ export function parsePlanMd(md: string): PlanParseResult { const match = md.match(FRONTMATTER_RE) if (!match) { return { ok: false, errors: [ { line: 1, message: 'Plan ontbreekt yaml-frontmatter. Verwacht eerste regel: ---', }, ], } } const [, frontmatterRaw, body] = match let parsed: unknown try { parsed = parseYaml(frontmatterRaw) } catch (err) { if (err instanceof YAMLParseError) { const yamlLine = err.linePos?.[0]?.line const fileLine = yamlLine != null ? yamlLine + 1 : undefined const offendingLine = yamlLine != null ? frontmatterRaw.split(/\r?\n/)[(yamlLine ?? 1) - 1] : undefined const isMarkdown = offendingLine != null && (/^\s*\d+\.\s+\*\*/.test(offendingLine) || /^\s*[-*]\s+\*\*/.test(offendingLine) || /^\s*\d+\..*:/.test(offendingLine)) const hint = isMarkdown ? 'Lijkt op markdown-content (genummerde of opsommingslijst) binnen YAML-frontmatter. Verplaats deze regels naar na de afsluitende `---`, of zet ze in een `description: |` blok.' : undefined return { ok: false, errors: [{ line: fileLine, message: err.message, hint }], } } return { ok: false, errors: [{ message: err instanceof Error ? err.message : String(err) }], } } const validation = ideaPlanMdFrontmatterSchema.safeParse(parsed) if (!validation.success) { return { ok: false, errors: validation.error.issues.map((iss) => ({ message: `${iss.path.join('.') || ''}: ${iss.message}`, })), } } return { ok: true, plan: validation.data, body: body.trimStart() } }