/* RFC-1290: copyReleaseEvidence — release.prepare step that copies every *.json in the workpiece's gitignored .werkstatt/release-evidence/ into the staged release's evidence/ directory, then verifies text-corpus.json's htmlTreeHash against the staged dist tree. Stale evidence is false evidence (DNA-106): a mismatch throws and blocks the release. Do not invoke the diff here — release-commands.ts calls text.corpus.diff by name (DNA-64). Do not skip silently — a missing corpus in evidence is tolerated (pre-1290 sites), a stale one is not. RFC-1290: initial — copyReleaseEvidence + staged-dist htmlTreeHash verification. RFC-1290 RFC-1290: rendered-text corpus + release text diff */ import { join, relative } from "node:path"; import type { WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel"; import { computeHtmlTreeHash, textCorpusSchema, } from "@warpgogol/werkstatt-shared/text-corpus"; const RELEASE_EVIDENCE_REL = join(".werkstatt", "release-evidence"); const STALE_MSG = "[release.prepare] text corpus is stale for the staged dist (RFC-1290) — rerun build.post"; async function collectHtml(io: WorkspaceIO, root: string): Promise { const out: string[] = []; const walk = async (dir: string): Promise => { let entries; try { entries = await io.readdir(dir); } catch { return; } for (const entry of entries) { const p = join(dir, entry.name); if (entry.isDirectory) await walk(p); else if (entry.isFile && entry.name.endsWith(".html")) out.push(p); } }; await walk(root); return out.sort(); } /** * Copy /.werkstatt/release-evidence/[i].json into * /evidence/ and verify the corpus hash against the staged dist. * No-op when the workpiece carries no release-evidence directory. */ export async function copyReleaseEvidence( workpieceDir: string, stagingDir: string, io: WorkspaceIO, ): Promise { const evidenceSrc = join(workpieceDir, RELEASE_EVIDENCE_REL); if (!(await io.exists(evidenceSrc))) return; const evidenceDest = join(stagingDir, "evidence"); await io.mkdir(evidenceDest); for (const entry of await io.readdir(evidenceSrc)) { if (entry.isFile && entry.name.endsWith(".json")) { await io.copyFile(join(evidenceSrc, entry.name), join(evidenceDest, entry.name)); } } const corpusPath = join(evidenceDest, "text-corpus.json"); if (!(await io.exists(corpusPath))) return; const corpus = textCorpusSchema.safeParse( JSON.parse(await io.readFile(corpusPath)), ); if (!corpus.success) { throw new Error( "[release.prepare] text-corpus.json evidence fails schema (RFC-1290) — rerun build.post", ); } const clientRoot = join(stagingDir, "dist", "client"); const distRoot = (await io.exists(clientRoot)) ? clientRoot : join(stagingDir, "dist"); const inputs: Array<{ relPath: string; content: string }> = []; for (const file of await collectHtml(io, distRoot)) { inputs.push({ relPath: relative(distRoot, file).replace(/\\/g, "/"), content: await io.readFile(file), }); } if (computeHtmlTreeHash(inputs) !== corpus.data.htmlTreeHash) { throw new Error(STALE_MSG); } }