import fs from "node:fs"; import path from "node:path"; const TASK_ID = /^M\d+-GAP-\d{5}$/u; function add(issues, code, detail) { issues.push({ code, detail }); } /** * Validate the cheap, deterministic inputs before a generated-gap checker * loads the WASM module or starts Blender. This is intentionally independent * of the task-specific assertions in check-generated-gap.mjs. */ export function collectGeneratedGapPreflight({ root, task, entry }) { const issues = []; if (typeof task !== "string" || !TASK_ID.test(task)) { add(issues, "TASK_ARGUMENT_INVALID", "use --task Mxx-GAP-nnnnn"); return issues; } if (!entry) { add(issues, "TASK_NOT_INDEXED", task); return issues; } if (!["active", "completed"].includes(entry.state)) { add(issues, "TASK_NOT_RUNNABLE", `${task} state=${entry.state}; expected active or completed`); } const required = []; if (entry.fixture?.path) required.push(["fixture", entry.fixture.path]); const generator = typeof entry.desktopCommand === "string" ? entry.desktopCommand.match(/\s--python\s+(\S+)/u)?.[1] : undefined; if (generator) required.push(["generator", generator]); for (const [role, relativePath] of required) { const resolved = path.resolve(root, relativePath); if (!fs.existsSync(resolved)) add(issues, "INPUT_MISSING", `${role}=${relativePath}`); else if (!fs.statSync(resolved).isFile()) add(issues, "INPUT_NOT_FILE", `${role}=${relativePath}`); } return issues; } export function formatGeneratedGapPreflight(task, issues) { return `generated-gap-preflight-failed task=${task ?? "MISSING"} issues=${issues.map((issue) => `${issue.code}:${issue.detail}`).join(",")}`; }