import { createHash, randomUUID } from "node:crypto"; import { promises as fs } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; /** The headings that make up a retained develop-workflow record. */ export const REQUIRED_SECTIONS = Object.freeze([ "Status", "Request and scope", "Git identity", "Acceptance criteria", "Approved implementation plan and gates", "Ownership", "Verification matrix", "Review", "Integration and rollback", "Checkpoints and blockers", ]); const REQUIRED_SECTION_SET = new Set(REQUIRED_SECTIONS); const FILE_MODE = 0o600; const MAX_TEMP_NAME_ATTEMPTS = 16; /** * Error raised for a checkpoint precondition or atomic-update failure. * `code` is stable for callers that need to distinguish a stale record or lock. */ export class CheckpointError extends Error { constructor(code, message, options = {}) { super(message, options); this.name = "CheckpointError"; this.code = code; } } const nodeIo = Object.freeze({ mkdir: (...args) => fs.mkdir(...args), open: (...args) => fs.open(...args), readFile: (...args) => fs.readFile(...args), rename: (...args) => fs.rename(...args), stat: (...args) => fs.stat(...args), unlink: (...args) => fs.unlink(...args), randomUUID: () => randomUUID(), }); /** The default filesystem adapter, useful when composing or testing the helper. */ export const defaultIo = nodeIo; function asBytes(input) { if (typeof input === "string") return Buffer.from(input, "utf8"); if (input instanceof Uint8Array) return Buffer.from(input); if (input instanceof ArrayBuffer) return Buffer.from(new Uint8Array(input)); throw new CheckpointError( "INVALID_INPUT", "checkpoint input must be a UTF-8 string or byte array", ); } function headingText(line, level) { const prefix = "#".repeat(level); const match = line.match(new RegExp(`^${prefix}(?!#)[ \\t]+(.+?)[ \\t]*$`)); if (!match) return null; // Closing ATX hashes are syntax, not part of the heading title. return match[1].replace(/[ \\t]+#+[ \\t]*$/, "").trim(); } /** * Validate a retained record without accessing the destination path. * Markdown headings inside fenced code blocks are not record sections. */ export function validateRecord(input) { const bytes = asBytes(input); let text; try { text = new TextDecoder("utf-8", { fatal: true }).decode(bytes); } catch (error) { throw new CheckpointError("INVALID_RECORD", "checkpoint input is not valid UTF-8", { cause: error, }); } const h1 = []; const h2 = []; let fence = null; for (const line of text.split(/\r?\n/)) { const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/); if (fenceMatch) { const marker = fenceMatch[1][0]; if (fence === null) fence = marker; else if (fence === marker) fence = null; continue; } if (fence !== null) continue; const first = headingText(line, 1); if (first !== null) h1.push(first); const second = headingText(line, 2); if (second !== null) h2.push(second); } if (h1.length !== 1 || h1[0].length === 0) { throw new CheckpointError( "INVALID_RECORD", `checkpoint must contain exactly one non-empty H1 (found ${h1.length})`, ); } const counts = new Map(); for (const section of h2) counts.set(section, (counts.get(section) ?? 0) + 1); const missing = REQUIRED_SECTIONS.filter((section) => !counts.has(section)); const duplicate = REQUIRED_SECTIONS.filter((section) => (counts.get(section) ?? 0) > 1); const unknown = h2.filter((section) => !REQUIRED_SECTION_SET.has(section)); if (missing.length > 0 || duplicate.length > 0 || unknown.length > 0) { const details = []; if (missing.length > 0) details.push(`missing: ${missing.join(", ")}`); if (duplicate.length > 0) details.push(`duplicate: ${duplicate.join(", ")}`); if (unknown.length > 0) details.push(`unknown: ${[...new Set(unknown)].join(", ")}`); throw new CheckpointError("INVALID_RECORD", `invalid checkpoint sections (${details.join("; ")})`); } return { bytes, text, h1: h1[0], sections: [...REQUIRED_SECTIONS] }; } function sha256(bytes) { return createHash("sha256").update(bytes).digest("hex"); } function expectedDigest(value) { if (value === undefined || value === null) return undefined; if (typeof value !== "string" || !/^[0-9a-f]{64}$/i.test(value)) { throw new CheckpointError("INVALID_DIGEST", "expected SHA-256 must be exactly 64 hexadecimal characters"); } return value.toLowerCase(); } function errorCode(error) { return error && typeof error === "object" && "code" in error ? error.code : undefined; } function isMissing(error) { return errorCode(error) === "ENOENT"; } function isAlreadyExists(error) { return errorCode(error) === "EEXIST"; } function messageFor(error) { return error instanceof Error ? error.message : String(error); } function withIo(io) { return { ...nodeIo, ...io }; } async function closeQuietly(handle) { if (handle === null) return undefined; try { await handle.close(); } catch (error) { return error; } return undefined; } async function unlinkQuietly(io, path) { try { await io.unlink(path); return undefined; } catch (error) { return error; } } function sameFile(left, right) { return left.dev === right.dev && left.ino === right.ino; } async function readExisting(io, target) { try { return { exists: true, bytes: await io.readFile(target) }; } catch (error) { if (isMissing(error)) return { exists: false, bytes: undefined }; throw new CheckpointError("READ_FAILED", `could not read checkpoint target: ${messageFor(error)}`, { cause: error, }); } } async function openUniqueTemp(io, target) { for (let attempt = 0; attempt < MAX_TEMP_NAME_ATTEMPTS; attempt += 1) { const id = io.randomUUID(); const path = `${target}.tmp-${process.pid}-${id}`; try { const handle = await io.open(path, "wx", FILE_MODE); return { path, handle }; } catch (error) { // A collision is not stale-file cleanup. Leave the old file alone and try a new name. if (isAlreadyExists(error)) continue; throw new CheckpointError("TEMP_OPEN_FAILED", `could not create checkpoint temporary file: ${messageFor(error)}`, { cause: error, }); } } throw new CheckpointError("TEMP_OPEN_FAILED", "could not create a unique checkpoint temporary file"); } async function writeAll(handle, bytes) { let offset = 0; while (offset < bytes.byteLength) { const result = await handle.write(bytes, offset, bytes.byteLength - offset); const written = typeof result === "number" ? result : result.bytesWritten; if (!Number.isInteger(written) || written <= 0) { throw new CheckpointError("WRITE_FAILED", "checkpoint temporary file write made no progress"); } offset += written; } } async function syncDirectory(io, directory) { let handle = null; let operationError; try { handle = await io.open(directory, "r"); await handle.sync(); } catch (error) { operationError = new CheckpointError("DIRECTORY_SYNC_FAILED", `could not flush checkpoint directory: ${messageFor(error)}`, { cause: error, }); } const closeError = await closeQuietly(handle); if (operationError !== undefined) throw operationError; if (closeError !== undefined) { throw new CheckpointError("DIRECTORY_CLOSE_FAILED", `could not close checkpoint directory: ${messageFor(closeError)}`, { cause: closeError, }); } } /** Atomically create or replace a validated workflow record. */ export async function checkpointRecord(request) { if (request === null || typeof request !== "object") { throw new CheckpointError("INVALID_REQUEST", "checkpoint request must provide a target and input"); } if (typeof request.target !== "string" || request.target.length === 0) { throw new CheckpointError("INVALID_REQUEST", "checkpoint target is required"); } const target = resolve(request.target); // Validate before opening the lock, creating a directory, or reading the destination. const { bytes } = validateRecord(request.input); const expected = expectedDigest(request.expectedSha256); const io = withIo(request.io); const lock = `${target}.lock`; const directory = dirname(target); try { await io.mkdir(directory, { recursive: true, mode: 0o700 }); } catch (error) { throw new CheckpointError( "DIRECTORY_CREATE_FAILED", `could not create checkpoint directory: ${messageFor(error)}`, { cause: error }, ); } let lockHandle = null; let lockIdentity = null; let tempHandle = null; let tempPath = null; let primaryError; let result; try { try { lockHandle = await io.open(lock, "wx", FILE_MODE); lockIdentity = await lockHandle.stat(); } catch (error) { if (isAlreadyExists(error)) { throw new CheckpointError("LOCK_EXISTS", "checkpoint target is locked; refusing to recover or overwrite the lock", { cause: error, }); } throw new CheckpointError("LOCK_FAILED", `could not acquire and identify checkpoint lock: ${messageFor(error)}`, { cause: error, }); } const current = await readExisting(io, target); if (current.exists) { if (expected === undefined) { throw new CheckpointError( "EXPECTED_SHA256_REQUIRED", "replacing an existing checkpoint requires --expected-sha256", ); } const actual = sha256(current.bytes); if (actual !== expected) { throw new CheckpointError( "STALE_DIGEST", `checkpoint target digest is stale (expected ${expected}, found ${actual})`, ); } } else if (expected !== undefined) { throw new CheckpointError( "TARGET_MISSING", "checkpoint target is absent; do not provide an expected SHA-256 when creating it", ); } const temporary = await openUniqueTemp(io, target); tempPath = temporary.path; tempHandle = temporary.handle; let temporaryError; try { await writeAll(tempHandle, bytes); } catch (error) { temporaryError = error instanceof CheckpointError ? error : new CheckpointError("WRITE_FAILED", `could not write checkpoint temporary file: ${messageFor(error)}`, { cause: error }); } if (temporaryError === undefined) { try { await tempHandle.sync(); } catch (error) { temporaryError = new CheckpointError("TEMP_SYNC_FAILED", `could not flush checkpoint temporary file: ${messageFor(error)}`, { cause: error, }); } } const closeError = await closeQuietly(tempHandle); tempHandle = null; if (temporaryError !== undefined) throw temporaryError; if (closeError !== undefined) { throw new CheckpointError("TEMP_CLOSE_FAILED", `could not close checkpoint temporary file: ${messageFor(closeError)}`, { cause: closeError, }); } try { await io.rename(tempPath, target); } catch (error) { throw new CheckpointError("RENAME_FAILED", `could not atomically replace checkpoint target: ${messageFor(error)}`, { cause: error, }); } tempPath = null; await syncDirectory(io, directory); result = { target, created: !current.exists, sha256: sha256(bytes), }; } catch (error) { primaryError = error; } // Cleanup is best effort. In particular, never remove a pre-existing stale temp // or lock and never replace an operation error with a cleanup error. const tempCloseError = await closeQuietly(tempHandle); if (tempCloseError !== undefined && primaryError === undefined) { primaryError = new CheckpointError("TEMP_CLOSE_FAILED", `could not close checkpoint temporary file: ${messageFor(tempCloseError)}`, { cause: tempCloseError, }); } if (tempPath !== null) { const tempUnlinkError = await unlinkQuietly(io, tempPath); if (tempUnlinkError !== undefined && !isMissing(tempUnlinkError) && primaryError === undefined) { primaryError = new CheckpointError("TEMP_CLEANUP_FAILED", `could not remove checkpoint temporary file: ${messageFor(tempUnlinkError)}`, { cause: tempUnlinkError, }); } } const lockCloseError = await closeQuietly(lockHandle); if (lockCloseError !== undefined && primaryError === undefined) { primaryError = new CheckpointError("LOCK_CLOSE_FAILED", `could not close checkpoint lock: ${messageFor(lockCloseError)}`, { cause: lockCloseError, }); } if (lockIdentity !== null) { let currentLockIdentity; try { currentLockIdentity = await io.stat(lock); } catch (error) { if (!isMissing(error) && primaryError === undefined) { primaryError = new CheckpointError("LOCK_CLEANUP_FAILED", `could not verify checkpoint lock ownership: ${messageFor(error)}`, { cause: error, }); } } if (currentLockIdentity !== undefined && sameFile(lockIdentity, currentLockIdentity)) { const lockUnlinkError = await unlinkQuietly(io, lock); if (lockUnlinkError !== undefined && !isMissing(lockUnlinkError) && primaryError === undefined) { primaryError = new CheckpointError("LOCK_CLEANUP_FAILED", `could not remove checkpoint lock: ${messageFor(lockUnlinkError)}`, { cause: lockUnlinkError, }); } } } if (primaryError !== undefined) throw primaryError; return result; } function usage() { return "Usage: node checkpoint-record.mjs [--input ] [--expected-sha256 ]"; } function parseCli(argv) { let target; let inputPath; let expectedSha256; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--input") { if (inputPath !== undefined || index + 1 >= argv.length || argv[index + 1].startsWith("--")) { throw new CheckpointError("CLI_USAGE", usage()); } inputPath = argv[++index]; } else if (argument === "--expected-sha256") { if (expectedSha256 !== undefined || index + 1 >= argv.length || argv[index + 1].startsWith("--")) { throw new CheckpointError("CLI_USAGE", usage()); } expectedSha256 = argv[++index]; } else if (argument.startsWith("--")) { throw new CheckpointError("CLI_USAGE", usage()); } else if (target === undefined) { target = argument; } else { throw new CheckpointError("CLI_USAGE", usage()); } } if (target === undefined) throw new CheckpointError("CLI_USAGE", usage()); return { target, inputPath, expectedSha256 }; } async function readStdin() { const chunks = []; for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); return Buffer.concat(chunks); } export async function main(argv = process.argv.slice(2)) { try { const { target, inputPath, expectedSha256 } = parseCli(argv); const input = inputPath === undefined ? await readStdin() : await fs.readFile(inputPath); await checkpointRecord({ target, input, expectedSha256 }); return 0; } catch (error) { process.stderr.write(`checkpoint-record: ${messageFor(error)}\n`); return 2; } } const invokedPath = process.argv[1] === undefined ? null : resolve(process.argv[1]); if (invokedPath !== null && invokedPath === fileURLToPath(import.meta.url)) { process.exitCode = await main(); }