85 lines
4.2 KiB
JavaScript
85 lines
4.2 KiB
JavaScript
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const abs = (file) => path.join(root, file);
|
|
const cachePath = abs("tests/golden/WBV2/context-cache.json");
|
|
const read = (file) => fs.readFileSync(abs(file), "utf8");
|
|
const digest = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
const queue = read("docs/EXECUTION_QUEUE.md");
|
|
const task = process.argv.includes("--task")
|
|
? process.argv[process.argv.indexOf("--task") + 1]
|
|
: queue.match(/\| 当前任务 \| `([^`]+)` \|/u)?.[1];
|
|
if (!task) throw new Error("unable to resolve current WBV2 task");
|
|
const parentManifest = queue.match(/\| parent manifest \| `([^`]+)` \|/u)?.[1];
|
|
if (!parentManifest) throw new Error("queue parent manifest is missing");
|
|
const baseInputPaths = [
|
|
"docs/EXECUTION_QUEUE.md",
|
|
`docs/tasks/${task}.md`,
|
|
parentManifest,
|
|
`tests/golden/${task}/task-context.json`,
|
|
];
|
|
const statInput = (file) => {
|
|
try {
|
|
const stat = fs.statSync(abs(file), { bigint: true });
|
|
return { bytes: Number(stat.size), mtime_ns: stat.mtimeNs.toString(), exists: true };
|
|
}
|
|
catch (error) {
|
|
return { bytes: null, mtime_ns: null, exists: false, error: error?.code ?? "STAT_FAILED" };
|
|
}
|
|
};
|
|
const metadataComparable = (value) => value && value.exists === true
|
|
&& Number.isSafeInteger(value.bytes) && typeof value.mtime_ns === "string";
|
|
let cached = null;
|
|
try { cached = JSON.parse(fs.readFileSync(cachePath, "utf8")); }
|
|
catch { /* First use or an interrupted write. */ }
|
|
const inputPaths = [...new Set([
|
|
...baseInputPaths,
|
|
...Object.keys(cached?.inputs ?? {}),
|
|
])];
|
|
const inputMetadata = Object.fromEntries(inputPaths.map((file) => [file, statInput(file)]));
|
|
const metadataUnchanged = (cachedInputs) => inputPaths.every((file) => {
|
|
const current = inputMetadata[file];
|
|
const cached = cachedInputs?.[file];
|
|
return metadataComparable(current) && metadataComparable(cached)
|
|
&& current.bytes === cached.bytes && current.mtime_ns === cached.mtime_ns;
|
|
});
|
|
const digestPath = (file) => {
|
|
try { return digest(read(file)); }
|
|
catch { return "MISSING"; }
|
|
};
|
|
const inputHashFor = (paths = inputPaths) => digest(paths.map((file) => `${file}\0${digestPath(file)}\n`).join(""));
|
|
if (!process.argv.includes("--force") && cached?.status === "PASS" && cached.task === task && metadataUnchanged(cached.inputs)) {
|
|
process.stdout.write(`CONTEXT_PACKET_REUSED task=${task} inputHash=${cached.inputHash ?? "UNKNOWN"}\n`);
|
|
process.exit(0);
|
|
}
|
|
const result = spawnSync(process.execPath, [abs("tools/web/print-task-context.mjs"), "--task", task], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
|
|
const check = spawnSync(process.execPath, [abs("tools/web/check-task-context.mjs"), "--task", task, "--write"], { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
|
|
if ((result.status ?? 1) !== 0 || (check.status ?? 1) !== 0) {
|
|
process.stderr.write(`${result.stdout ?? ""}${result.stderr ?? ""}${check.stdout ?? ""}${check.stderr ?? ""}`);
|
|
process.exit(Math.max(result.status ?? 1, check.status ?? 1));
|
|
}
|
|
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
|
|
let contextMetadata = {};
|
|
try {
|
|
const context = JSON.parse(read(`tests/golden/${task}/task-context.json`));
|
|
contextMetadata = context.inputMetadata && typeof context.inputMetadata === "object" ? context.inputMetadata : {};
|
|
} catch { /* The task-context checker already reported the precise failure. */ }
|
|
const refreshedPaths = [...new Set([...inputPaths, ...Object.keys(contextMetadata)])];
|
|
const refreshedInputHash = inputHashFor(refreshedPaths);
|
|
const payload = {
|
|
schemaVersion: 2,
|
|
status: "PASS",
|
|
task,
|
|
inputHash: refreshedInputHash,
|
|
inputs: Object.fromEntries(refreshedPaths.map((file) => [file, { ...statInput(file), sha256: contextMetadata[file]?.sha256 ?? digestPath(file) }])),
|
|
refreshedAt: new Date().toISOString(),
|
|
};
|
|
const temporary = `${cachePath}.tmp-${process.pid}`;
|
|
fs.writeFileSync(temporary, `${JSON.stringify(payload, null, 2)}\n`);
|
|
fs.renameSync(temporary, cachePath);
|
|
process.stdout.write(`wbv2-context-cache-refresh task=${task} inputHash=${refreshedInputHash}\n`);
|