import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildTaskContext, parseQueue, readIndexedTask, verifyTaskIndex, } from "./task-context-lib.mjs"; import { collectGeneratedGapPreflight } from "./generated-gap-preflight.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const requested = process.argv.indexOf("--task"); const requestedTask = requested >= 0 ? process.argv[requested + 1] : undefined; const issues = []; const add = (code, detail) => issues.push({ code, detail }); const relative = (file) => path.relative(repoRoot, file).replaceAll(path.sep, "/"); let queue; let index; let entry; try { queue = parseQueue(); index = verifyTaskIndex(); const task = requestedTask ?? queue.currentTask; const checkingCurrent = requestedTask === undefined; entry = readIndexedTask(task); if (!entry) add("TASK_NOT_INDEXED", task); const parentPath = path.resolve(repoRoot, queue.parentManifest); if (checkingCurrent && !fs.existsSync(parentPath)) add("PARENT_MANIFEST_MISSING", queue.parentManifest); else if (checkingCurrent) { const parent = JSON.parse(fs.readFileSync(parentPath, "utf8")); if (parent.nextTask !== queue.currentTask) add("PARENT_NEXT_TASK_MISMATCH", `${parent.nextTask ?? "NONE"}!=${queue.currentTask}`); } if (index) { if (checkingCurrent && index.activeTask !== queue.currentTask) add("INDEX_ACTIVE_TASK_MISMATCH", `${index.activeTask ?? "NONE"}!=${queue.currentTask}`); const catalogPath = path.resolve(repoRoot, index.catalog.path); const active = fs.readFileSync(catalogPath, "utf8").trimEnd().split("\n").filter(Boolean) .map((line) => JSON.parse(line)).filter((record) => record.state === "active"); if (checkingCurrent && active.length !== 1) add("CATALOG_ACTIVE_TASK_COUNT", `count=${active.length}`); else if (checkingCurrent && active[0].id !== queue.currentTask) add("CATALOG_ACTIVE_TASK_MISMATCH", `${active[0].id}!=${queue.currentTask}`); } const cardPath = path.join(repoRoot, "docs/tasks", `${task}.md`); if (!fs.existsSync(cardPath) || fs.statSync(cardPath).size === 0) add("TASK_CARD_MISSING", relative(cardPath)); else if (checkingCurrent) { const context = buildTaskContext(task); if (context.context.task !== task) add("TASK_CONTEXT_MISMATCH", `${context.context.task}!=${task}`); } for (const issue of collectGeneratedGapPreflight({ root: repoRoot, task: requestedTask ?? queue.currentTask, entry })) { add(issue.code, issue.detail); } } catch (error) { add("HEALTH_CHECK_ERROR", error instanceof Error ? error.message : String(error)); } const report = { schemaVersion: 1, operation: "WEB_EXECUTION_HEALTH", status: issues.length === 0 ? "PASS" : "BLOCKED", queueTask: queue?.currentTask ?? null, checkedTask: requestedTask ?? queue?.currentTask ?? null, indexActiveTask: index?.activeTask ?? null, issueCount: issues.length, issues, remediation: [ ...(issues.some(({ code }) => ["INDEX_ACTIVE_TASK_MISMATCH", "CATALOG_ACTIVE_TASK_MISMATCH", "CATALOG_ACTIVE_TASK_COUNT", "PARENT_NEXT_TASK_MISMATCH"].includes(code)) ? ["repair the queue/index/catalog pointer through the repository generator"] : []), ...(issues.some(({ code }) => code === "TASK_CARD_MISSING") ? ["generate the compact task card before running a gap checker"] : []), ...(issues.some(({ code }) => ["TASK_NOT_RUNNABLE", "INPUT_MISSING", "INPUT_NOT_FILE"].includes(code)) ? ["do not start Blender or load WASM until the task state and inputs are ready"] : []), ], }; process.stdout.write(`${JSON.stringify(report)}\n`); if (issues.length !== 0) process.exitCode = 2;