Scrum4Me/lib/product-doc-parser.ts
Madhura68 afafbca855 feat(PBI-96/T-1060): add frontmatter parser + serializer (P2-fix)
- lib/product-doc-parser.ts: parseProductDocMd(md) → {ok, frontmatter, body}
  | {ok:false, errors[]} met line-info bij YAML-fouten. Pattern gespiegeld
  uit lib/idea-plan-parser.ts.
- lib/product-doc-frontmatter.ts: setProductDocFrontmatterFields(md, patch)
  laat de server `last_updated` server-side normaliseren (P2-review-fix
  uit docs/recommendations/product-docs-storage-system-review-2026-05-16).
  Gebruikt yaml.parseDocument om field-ordering best-effort te behouden.
- todayIsoDate() helper voor 'yyyy-mm-dd' string.
- __tests__: 19 nieuwe tests groen — parse-success/fail-paden, en
  expliciete P2-coverage (vervangen + toevoegen last_updated, behoud
  overige velden + body).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 11:42:03 +02:00

74 lines
2.1 KiB
TypeScript

// Parser voor de product_doc markdown. Format: yaml-frontmatter (gevalideerd
// via productDocFrontmatterSchema) + markdown-body. Synchroon — geen LLM.
//
// Wordt door alle Product Doc server-actions (create/update) gebruikt om
// frontmatter te valideren vóór save. Bij parse-fout: 422 met line-info.
// Pattern gespiegeld uit lib/idea-plan-parser.ts.
import { parse as parseYaml, YAMLParseError } from 'yaml'
import {
productDocFrontmatterSchema,
type ProductDocFrontmatter,
} from '@/lib/schemas/product-doc'
export type ProductDocParseError = {
line?: number
message: string
hint?: string
}
export type ProductDocParseResult =
| { ok: true; frontmatter: ProductDocFrontmatter; body: string }
| { ok: false; errors: ProductDocParseError[] }
const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/
export function parseProductDocMd(md: string): ProductDocParseResult {
const match = md.match(FRONTMATTER_RE)
if (!match) {
return {
ok: false,
errors: [
{
line: 1,
message:
'Doc mist yaml-frontmatter. Eerste regel moet `---` zijn, gevolgd door de frontmatter en een afsluitende `---`.',
},
],
}
}
const [, frontmatterRaw, body] = match
let parsed: unknown
try {
parsed = parseYaml(frontmatterRaw)
} catch (err) {
if (err instanceof YAMLParseError) {
const yamlLine = err.linePos?.[0]?.line
// +1 voor de openende `---`-regel (frontmatterRaw start op regel 2)
const fileLine = yamlLine != null ? yamlLine + 1 : undefined
return {
ok: false,
errors: [{ line: fileLine, message: err.message }],
}
}
return {
ok: false,
errors: [{ message: err instanceof Error ? err.message : String(err) }],
}
}
const validation = productDocFrontmatterSchema.safeParse(parsed)
if (!validation.success) {
return {
ok: false,
errors: validation.error.issues.map((iss) => ({
message: `${iss.path.join('.') || '<root>'}: ${iss.message}`,
})),
}
}
return { ok: true, frontmatter: validation.data, body: body.trimStart() }
}