import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); function arg(name) { const index = process.argv.indexOf(name); return index >= 0 ? process.argv[index + 1] : undefined; } const task = arg("--task"); const status = arg("--status"); const gate = arg("--gate"); const next = arg("--next"); const evidenceRootArg = arg("--evidence-root"); const sourceHashesArg = arg("--source-hashes"); const commandExitCodesArg = arg("--command-exit-codes"); const resolutionArg = arg("--resolution"); const queueMutationArg = arg("--queue-mutation"); const queueMutation = queueMutationArg === undefined ? false : queueMutationArg === "true"; if (!task || !status || !gate || !next || !evidenceRootArg) { throw new Error("usage: node tools/web/write-corrective-handoff.mjs --task C0-xxx --status done --gate G0 --next C0-xxx --evidence-root tests/golden/corrective/C0-xxx [--queue-mutation true]"); } const evidenceRoot = path.resolve(root, evidenceRootArg); if (!evidenceRoot.startsWith(`${root}${path.sep}`)) throw new Error("evidence root must stay inside the repository"); if (!fs.existsSync(evidenceRoot) || !fs.statSync(evidenceRoot).isDirectory()) throw new Error(`missing evidence root: ${evidenceRootArg}`); const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/"); const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); const reports = ["dry-run.json", "write.json"] .map((name) => path.join(evidenceRoot, name)) .filter((file) => fs.existsSync(file)) .map((file) => JSON.parse(fs.readFileSync(file, "utf8"))); const resolution = resolutionArg && fs.existsSync(path.resolve(root, resolutionArg)) ? JSON.parse(fs.readFileSync(path.resolve(root, resolutionArg), "utf8")) : null; const inputDigests = [...new Set([...reports.map((report) => report.inputDigest), resolution?.inputDigest].filter(Boolean))]; if (inputDigests.length > 1 && !resolution) throw new Error("dry-run and write inputDigest differ"); const sourceHashValue = sourceHashesArg && fs.existsSync(path.resolve(root, sourceHashesArg)) ? JSON.parse(fs.readFileSync(path.resolve(root, sourceHashesArg), "utf8")) : resolution?.sourceHashes ?? reports.find((report) => report.sourceHashes)?.sourceHashes ?? null; const sourceHashes = sourceHashValue?.sourceHashes ?? sourceHashValue; const commandExitValue = commandExitCodesArg && fs.existsSync(path.resolve(root, commandExitCodesArg)) ? JSON.parse(fs.readFileSync(path.resolve(root, commandExitCodesArg), "utf8")) : resolution?.commandExitCodes ?? null; const commandExitCodes = commandExitValue?.commandExitCodes ?? commandExitValue; const artifacts = []; const stack = [evidenceRoot]; while (stack.length) { const directory = stack.pop(); for (const entry of fs.readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) { const file = path.join(directory, entry.name); if (entry.isDirectory()) stack.push(file); else if (entry.isFile() && entry.name !== "manifest.json" && entry.name !== "handoff.json") { artifacts.push({ path: relative(file), sha256: sha256(file), bytes: fs.statSync(file).size }); } } } artifacts.sort((a, b) => a.path.localeCompare(b.path)); const handoff = { schemaVersion: 1, task, status, gate, queueMutation, evidenceRoot: relative(evidenceRoot), artifactCount: artifacts.length, artifacts, ...(resolution?.inputDigest ? { inputDigest: resolution.inputDigest } : inputDigests.length === 1 ? { inputDigest: inputDigests[0] } : {}), ...(sourceHashes ? { sourceHashes } : {}), ...(commandExitCodes ? { commandExitCodes } : {}), ...(resolution ? { resolution: { status: resolution.status, writeOutcome: resolution.writeOutcome, authorization: resolution.authorization, candidate: resolution.candidate ?? null } } : {}), nextCorrectiveTask: next, }; fs.writeFileSync(path.join(evidenceRoot, "handoff.json"), `${JSON.stringify(handoff, null, 2)}\n`); fs.writeFileSync(path.join(evidenceRoot, "manifest.json"), `${JSON.stringify(handoff, null, 2)}\n`); process.stdout.write(`corrective-handoff-ok task=${task} status=${status} gate=${gate} artifacts=${artifacts.length} next=${next}\n`);