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`);