Files
harness-tools/skills/develop/develop-workflow/scripts/checkpoint-record.mjs
T
2026-09-04 10:47:04 -04:00

461 lines
15 KiB
JavaScript

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),
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;
}
}
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) {
const directory = dirname(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, directory };
} 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,
});
}
}
function requestFromArgs(targetOrRequest, input, options) {
if (typeof targetOrRequest === "string" || targetOrRequest instanceof URL) {
return { ...(options ?? {}), target: targetOrRequest, input };
}
if (targetOrRequest === null || typeof targetOrRequest !== "object") {
throw new CheckpointError("INVALID_REQUEST", "checkpoint request must provide a target and input");
}
return targetOrRequest;
}
/**
* Atomically create or replace a validated workflow record.
*
* Object form: `checkpointRecord({ target, input, expectedSha256, io })`.
* Positional form is also supported by `recordCheckpoint(target, input, options)`.
*/
export async function checkpointRecord(targetOrRequest, input, options) {
const request = requestFromArgs(targetOrRequest, input, options);
const targetValue = request.target;
if (typeof targetValue !== "string" && !(targetValue instanceof URL)) {
throw new CheckpointError("INVALID_REQUEST", "checkpoint target is required");
}
const target = resolve(targetValue instanceof URL ? fileURLToPath(targetValue) : targetValue);
const source = request.input ?? request.content;
// This deliberately happens before opening the lock or reading the destination.
const { bytes } = validateRecord(source);
const expected = expectedDigest(request.expectedSha256 ?? request.expectedDigest);
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 tempHandle = null;
let tempPath = null;
let primaryError;
let result;
try {
try {
lockHandle = await io.open(lock, "wx", FILE_MODE);
} 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 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 (lockHandle !== null) {
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;
}
export async function recordCheckpoint(target, input, options = {}) {
return checkpointRecord(target, input, options);
}
export const writeCheckpoint = recordCheckpoint;
function usage() {
return "Usage: node checkpoint-record.mjs <target> [--input <path>] [--expected-sha256 <digest>]";
}
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();
}