Improve execution preflight diagnostics
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled

This commit is contained in:
mes123456
2026-08-24 17:04:16 -04:00
parent 5c8cfd1a8c
commit 603a5a92f2
6 changed files with 240 additions and 3 deletions

View File

@@ -0,0 +1,80 @@
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;

View File

@@ -4,13 +4,21 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
import crypto from "node:crypto";
import { spawnSync } from "node:child_process";
import factory from "../../web/app/src/vendor/blender/web_engine.js";
import { readIndexedTask, verifyTaskIndex } from "./task-context-lib.mjs";
import { collectGeneratedGapPreflight, formatGeneratedGapPreflight } from "./generated-gap-preflight.mjs";
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
const task = process.argv[process.argv.indexOf("--task") + 1];
const taskArgument = process.argv.indexOf("--task");
const task = taskArgument >= 0 ? process.argv[taskArgument + 1] : undefined;
verifyTaskIndex();
const entry = readIndexedTask(task); assert.ok(entry); assert.ok(["active", "completed"].includes(entry.state));
const entry = readIndexedTask(task);
const preflightIssues = collectGeneratedGapPreflight({ root, task, entry });
if (preflightIssues.length !== 0) {
process.stderr.write(`${formatGeneratedGapPreflight(task, preflightIssues)}\n`);
process.exitCode = 2;
process.exit();
}
const wasmBinary = fs.readFileSync(path.join(root, "web/app/src/vendor/blender/web_engine.wasm"));
const factory = async (options) => (await import("../../web/app/src/vendor/blender/web_engine.js")).default(options);
const open = (engine, handle, input) => { const pointer = engine._malloc(input.byteLength); engine.HEAPU8.set(input, pointer); try { assert.equal(engine._web_engine_open_blend(handle, pointer, input.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); } finally { engine._free(pointer); } };
const output = (engine, handle, fn, owned = false) => { const data = engine._malloc(4); const length = engine._malloc(4); try { assert.equal(fn(handle, data, length), 0, engine.UTF8ToString(engine._web_engine_last_error_message())); const pointer = engine.HEAPU32[data >>> 2]; const size = engine.HEAPU32[length >>> 2]; const result = engine.HEAPU8.slice(pointer, pointer + size); if (owned) engine._web_engine_free_buffer(pointer); return result; } finally { engine._free(data); engine._free(length); } };
const snapshot = (engine, handle) => JSON.parse(new TextDecoder().decode(output(engine, handle, engine._web_engine_get_scene_snapshot)));

View File

@@ -0,0 +1,45 @@
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(",")}`;
}