/* RFC-0505: ratgeber extended claim registry migrator — transforms existing ratgeber article claim sidecars (surface/articles/{lang}/*.claims.yaml) into structured claim records (surface/claims/{lang}/{claimId}.md) and deletes the sidecar files after transformation. Idempotent: if no sidecars exist, the migrator is a no-op. Advances the migratorCursor for RFC-0505. Do not auto-generate claimText, limitations, or reviewStatus — those are human-authored editorial fields. The migrator sets safe defaults: claimText is a placeholder, limitations is empty, reviewStatus is "unverified". Do not modify article records — article sources[].claimIds remain unchanged. RFC-0505: initial migrator — transform claim sidecars to claim records, delete sidecars. */ import path from "node:path"; import { parse as yamlParse, stringify as yamlStringify } from "yaml"; import type { Migrator, SternsystemData, MigrationContext } from "./types.ts"; import { MigrationError } from "./types.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export const RFC_0505_MIGRATOR_ID = "rfc-0505"; interface SidecarClaim { provenance?: string; asOf?: string; sourceId?: string; url?: string; title?: string; [key: string]: unknown; } interface ParsedSidecar { [claimId: string]: SidecarClaim; } function buildClaimRecordFrontmatter( claimId: string, articleSlug: string, lang: string, claim: SidecarClaim, ): string { const sourceRefs: Array> = []; if (claim.sourceId || claim.url) { sourceRefs.push({ sourceId: claim.sourceId ?? "unknown", url: claim.url ?? "https://example.com", title: claim.title ?? "Migrated from sidecar", retrievedAt: claim.asOf ?? "2026-01-01", }); } const frontmatter: Record = { claimId, articleId: articleSlug, claimText: "Migrated from claim sidecar — editorial review required.", claimType: "factual", sourceRefs, calculationInputs: [], limitations: [], verifiedAt: claim.asOf ?? "2026-01-01", reviewStatus: "unverified", }; const yamlContent = yamlStringify(frontmatter, { indent: 2 }); return `---\n${yamlContent}---\n`; } export const rfc0505Migrator: Migrator = { id: RFC_0505_MIGRATOR_ID, fromVersion: "4.15.0", toVersion: "4.16.0", description: "Transform ratgeber article claim sidecars (surface/articles/{lang}/*.claims.yaml) into structured claim records (surface/claims/{lang}/{claimId}.md) and delete sidecars. Sets reviewStatus to 'unverified' — editorial review required. Advances migratorCursor for RFC-0505.", transform: async ( data: SternsystemData, ctx: MigrationContext, io: WorkspaceIO = createDefaultIO().io, ) => { const articlesBaseDir = path.join(data.rootPath, "src", "content", "surface", "articles"); const claimsBaseDir = path.join(data.rootPath, "src", "content", "surface", "claims"); let langDirs: string[]; try { langDirs = (await io.readdir(articlesBaseDir)).map((e) => e.name); } catch { ctx.logger.info(`[rfc-0505] no articles directory at ${articlesBaseDir} — skipping`); return data; } for (const lang of langDirs) { const langPath = path.join(articlesBaseDir, lang); const stat = await io.stat(langPath).catch(() => null); if (!stat?.isDirectory) continue; const files = await io.readdir(langPath).catch(() => []); const sidecarFiles = files.filter((f) => f.name.endsWith(".claims.yaml")); if (sidecarFiles.length === 0) { ctx.logger.info(`[rfc-0505] no claim sidecars in ${lang} — skipping`); continue; } const claimsLangDir = path.join(claimsBaseDir, lang); await io.mkdir(claimsLangDir); for (const sidecarFile of sidecarFiles) { const sidecarPath = path.join(langPath, sidecarFile.name); const articleSlug = sidecarFile.name.replace(/\.claims\.yaml$/, ""); let raw: string; try { raw = await io.readFile(sidecarPath); } catch (err) { throw new MigrationError( RFC_0505_MIGRATOR_ID, sidecarPath, "", `Failed to read claim sidecar: ${(err as Error).message}`, ); } let parsed: ParsedSidecar; try { const result = yamlParse(raw) as ParsedSidecar; parsed = result && typeof result === "object" && !Array.isArray(result) ? result : {}; } catch (err) { throw new MigrationError( RFC_0505_MIGRATOR_ID, sidecarPath, "", `Failed to parse claim sidecar YAML: ${(err as Error).message}`, ); } for (const [claimId, claim] of Object.entries(parsed)) { const claimRecordPath = path.join(claimsLangDir, `${claimId}.md`); const exists = await io .stat(claimRecordPath) .then(() => true) .catch(() => false); if (exists) { ctx.logger.info(`[rfc-0505] claim record already exists: ${lang}/${claimId}.md`); continue; } const content = buildClaimRecordFrontmatter(claimId, articleSlug, lang, claim); try { await io.writeFile(claimRecordPath, content); ctx.logger.info(`[rfc-0505] created claim record: ${lang}/${claimId}.md`); } catch (err) { throw new MigrationError( RFC_0505_MIGRATOR_ID, claimRecordPath, "", `Failed to create claim record: ${(err as Error).message}`, ); } } // Delete the sidecar after transformation try { await io.rm(sidecarPath); ctx.logger.info(`[rfc-0505] deleted sidecar: ${lang}/${sidecarFile}`); } catch (err) { throw new MigrationError( RFC_0505_MIGRATOR_ID, sidecarPath, "", `Failed to delete claim sidecar: ${(err as Error).message}`, ); } } } return data; }, };