Scrum4Me/lib/idea-plan-parser.ts
Scrum4Me Agent d394ced20f ST-cmowjelb1: Parser: bestand-relatieve regel + hint-detectie in YAMLParseError-tak
- Voeg `hint?: string` toe aan PlanParseError type
- Bereken bestand-relatief regelnummer (yamlLine + 1 voor de openings-`---`)
- Detecteer markdown-patronen (numbered/bullet lijst) op de offending regel
- Zet Nederlandstalige hint bij markdown-match
- Render hint als "Tip: …" onder het foutbericht in IdeaMdEditor
2026-05-08 12:38:02 +02:00

82 lines
2.6 KiB
TypeScript

// 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('.') || '<root>'}: ${iss.message}`,
})),
}
}
return { ok: true, plan: validation.data, body: body.trimStart() }
}