/* RFC-0492: industry dossier schema migrator — transforms existing industry records by copying deprecated fields to their new dossier equivalents when the new field is absent. Three mechanical, idempotent transforms: 1. proofSignals → trustSignals 2. faqs → industryFaq 3. painPoints → evidenceRequirements Does NOT set notdienst (operator-authored content). Do not delete deprecated fields — the baker falls back to them when new fields are absent. Do not set notdienst — that is operator-authored content. Do not validate industry records — that is surface.industry.validate. RFC-0492: initial migrator — copy deprecated fields to new dossier equivalents. RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt */ import path from "node:path"; import { parse as parseYaml, stringify as stringifyYaml } from "yaml"; import type { Migrator, SternsystemData, MigrationContext } from "./types.ts"; import { MigrationError } from "./types.ts"; import { createDefaultIO, type FileStat, type WorkspaceIO, } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; export const RFC_0492_MIGRATOR_ID = "rfc-0492"; const FIELD_MAPPINGS: Array<{ from: string; to: string }> = [ { from: "proofSignals", to: "trustSignals" }, { from: "faqs", to: "industryFaq" }, { from: "painPoints", to: "evidenceRequirements" }, ]; function transformFrontmatter(data: Record): Record { for (const { from, to } of FIELD_MAPPINGS) { if (data[to] === undefined && data[from] !== undefined) { data[to] = data[from]; } } return data; } async function findIndustryFiles(rootPath: string): Promise { const results: string[] = []; // fs.walk.lint: allow — walks via WorkspaceIO abstraction (io.readdir) — collectFiles has no io contract async function walk(dir: string, io: WorkspaceIO = createDefaultIO().io): Promise { let entries: string[]; try { entries = (await io.readdir(dir)).map((e) => e.name); } catch { return; } for (const entry of entries) { const fullPath = path.join(dir, entry); let stat: FileStat; try { stat = await io.stat(fullPath); } catch { continue; } if (stat.isDirectory) { await walk(fullPath); } else if (entry.endsWith(".md")) { results.push(fullPath); } } } const industriesDir = path.join(rootPath, "src", "content", "surface", "industries"); await walk(industriesDir); return results; } async function transformIndustryFiles( data: SternsystemData, ctx: MigrationContext, io: WorkspaceIO = createDefaultIO().io, ): Promise { const files = await findIndustryFiles(data.rootPath); for (const filePath of files) { let raw: string; try { raw = await io.readFile(filePath); } catch { ctx.logger.info(`[migrator rfc-0492] skip unreadable file: ${filePath}`); continue; } let frontmatter: Record | null = null; let body = ""; const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/); if (fmMatch) { try { frontmatter = parseYaml(fmMatch[1]) as Record; } catch (err) { throw new MigrationError( RFC_0492_MIGRATOR_ID, filePath, "", `failed to parse frontmatter YAML: ${(err as Error).message}`, ); } body = fmMatch[2]; } if (!frontmatter || typeof frontmatter !== "object") { ctx.logger.info(`[migrator rfc-0492] skip non-object frontmatter: ${filePath}`); continue; } const before = JSON.stringify(frontmatter); transformFrontmatter(frontmatter); const after = JSON.stringify(frontmatter); if (before !== after) { const output = "---\n" + stringifyYaml(frontmatter) + "---\n" + body; await io.writeFile(filePath, output); ctx.logger.info(`[migrator rfc-0492] transformed: ${path.relative(data.rootPath, filePath)}`); } } return data; } export const rfc0492Migrator: Migrator = { id: RFC_0492_MIGRATOR_ID, fromVersion: "4.7.0", toVersion: "4.8.0", description: "Copy deprecated industry fields (proofSignals, faqs, painPoints) to new dossier equivalents (trustSignals, industryFaq, evidenceRequirements)", transform: async (data, ctx) => { return transformIndustryFiles(data, ctx); }, };