91 lines
4.1 KiB
JavaScript
91 lines
4.1 KiB
JavaScript
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)), "../..");
|
||
const read = (relative) => fs.readFileSync(path.join(root, relative), "utf8");
|
||
const sha256 = (relative) => crypto.createHash("sha256").update(read(relative)).digest("hex");
|
||
const parseJson = (relative) => JSON.parse(read(relative));
|
||
const relative = (absolute) => path.relative(root, absolute).replaceAll(path.sep, "/");
|
||
|
||
function argument(name) {
|
||
const index = process.argv.indexOf(name);
|
||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||
}
|
||
|
||
function queueCurrentTask() {
|
||
const source = read("docs/EXECUTION_QUEUE.md");
|
||
return source.match(/\|\s*当前任务\s*\|\s*`([^`]+)`/u)?.[1]
|
||
?? source.match(/当前任务\s*[::]\s*`([^`]+)`/u)?.[1];
|
||
}
|
||
|
||
function countLegacyEvidence() {
|
||
const rootPath = path.join(root, "tests/golden");
|
||
return fs.readdirSync(rootPath, { withFileTypes: true })
|
||
.filter((entry) => entry.isDirectory() && /^M16-GAP-\d{5}$/u.test(entry.name))
|
||
.length;
|
||
}
|
||
|
||
const task = argument("--task") ?? queueCurrentTask();
|
||
const output = argument("--output");
|
||
if (!task || !output) {
|
||
throw new Error("usage: node tools/web/capture-corrective-baseline.mjs --task M16-GAP-xxxxx --output path");
|
||
}
|
||
|
||
const queue = read("docs/EXECUTION_QUEUE.md");
|
||
const parentManifestPath = queue.match(/\|\s*parent manifest\s*\|\s*`([^`]+)`/iu)?.[1];
|
||
if (!parentManifestPath) throw new Error("EXECUTION_QUEUE.md does not declare parent manifest");
|
||
const parentManifest = parseJson(parentManifestPath);
|
||
if (queueCurrentTask() !== task) throw new Error(`queue current task mismatch: ${queueCurrentTask()} != ${task}`);
|
||
if (parentManifest.nextTask !== task) throw new Error(`parent nextTask mismatch: ${parentManifest.nextTask} != ${task}`);
|
||
const parentStatusPath = `docs/status/${parentManifest.task}.md`;
|
||
if (!fs.existsSync(path.join(root, parentStatusPath))) throw new Error(`missing parent status: ${parentStatusPath}`);
|
||
|
||
const plan = parseJson("tests/golden/M15-03A/next-task-plan.json");
|
||
const catalog = read("tests/golden/M15-03A/task-catalog.jsonl");
|
||
const activeIds = catalog.split("\n").filter(Boolean).map((line) => JSON.parse(line))
|
||
.filter((entry) => entry.state === "active").map((entry) => entry.id);
|
||
const sourcePaths = [
|
||
"docs/EXECUTION_QUEUE.md",
|
||
parentManifestPath,
|
||
parentStatusPath,
|
||
"tests/golden/M15-03A/completed-gap-tasks.json",
|
||
"tests/golden/M15-03A/task-catalog.jsonl",
|
||
"tests/golden/M15-03A/task-index.json",
|
||
"tests/golden/M15-03A/next-task-plan.json",
|
||
];
|
||
const baseline = {
|
||
schemaVersion: 1,
|
||
task,
|
||
queue: {
|
||
currentTask: queueCurrentTask(),
|
||
parentManifest: parentManifestPath,
|
||
parentNextTask: parentManifest.nextTask,
|
||
},
|
||
generatedState: {
|
||
catalogActiveTask: activeIds.length === 1 ? activeIds[0] : null,
|
||
catalogActiveIds: activeIds,
|
||
planFirstTask: plan.firstTask,
|
||
planNextTask: plan.nextTask,
|
||
completionCount: parseJson("tests/golden/M15-03A/completed-gap-tasks.json").length,
|
||
legacyEvidenceDirectoryCount: countLegacyEvidence(),
|
||
},
|
||
sourceHashes: Object.fromEntries(sourcePaths.map((sourcePath) => [sourcePath, sha256(sourcePath)])),
|
||
invariants: {
|
||
queueMutationAllowed: false,
|
||
parentPointerMatchesCurrent: parentManifest.nextTask === task,
|
||
exactlyOneCatalogActive: activeIds.length === 1,
|
||
catalogActiveMatchesPlan: activeIds[0] === plan.firstTask,
|
||
},
|
||
};
|
||
if (!baseline.invariants.parentPointerMatchesCurrent || !baseline.invariants.exactlyOneCatalogActive || !baseline.invariants.catalogActiveMatchesPlan) {
|
||
throw new Error(`generated state is inconsistent: ${JSON.stringify(baseline.invariants)}`);
|
||
}
|
||
|
||
const outputPath = path.resolve(root, output);
|
||
if (!outputPath.startsWith(`${root}${path.sep}`)) throw new Error("baseline output must stay inside the repository");
|
||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||
fs.writeFileSync(outputPath, `${JSON.stringify(baseline, null, 2)}\n`);
|
||
process.stdout.write(`corrective-baseline-ok task=${task} active=${activeIds[0]} parentNext=${parentManifest.nextTask} output=${relative(outputPath)}\n`);
|