/*
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);
}
}