71 lines
4.5 KiB
JavaScript
71 lines
4.5 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 output = path.resolve(process.argv[2] ?? path.join(root, "tests/golden/M15-03A/next-task-plan.json"));
|
|
const mapPath = path.join(root, "tests/golden/M15-02A/blender-parity-map.json");
|
|
const gapPath = path.join(root, "tests/golden/M15-02B/blender-gap-audit.json");
|
|
const map = JSON.parse(fs.readFileSync(mapPath, "utf8"));
|
|
const gaps = JSON.parse(fs.readFileSync(gapPath, "utf8"));
|
|
const completionPath = path.join(root, "tests/golden/M15-03A/completed-gap-tasks.json");
|
|
const completedIds = fs.existsSync(completionPath) ? new Set(JSON.parse(fs.readFileSync(completionPath, "utf8"))) : new Set();
|
|
const byId = new Map(map.entries.map((entry) => [entry.id, entry]));
|
|
const waveFor = (owner) => {
|
|
if (["Main", "Mesh", "MODIFIER", "RNA_DATABLOCK", "OPERATOR"].includes(owner)) return "M16";
|
|
if (owner === "CONSTRAINT") return "M17";
|
|
if (["SHADER_NODE", "GEOMETRY_NODE", "COMPOSITOR_NODE"].includes(owner)) return "M18";
|
|
if (owner === "Core") return "M19";
|
|
if (["PHYSICS", "SEQUENCER"].includes(owner)) return "M20";
|
|
if (["IMPORT_EXPORT", "EDITOR", "REGION", "SPACE", "WORKSPACE", "KEYMAP"].includes(owner)) return "M21";
|
|
return "M22";
|
|
};
|
|
const safeName = (value) => value.replace(/[^A-Za-z0-9_.-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 96) || "gap";
|
|
const sourceGaps = [...gaps.gaps.unmapped, ...gaps.gaps.summaryOnly, ...gaps.gaps.proxyOnly, ...gaps.gaps.routeOnly];
|
|
const uniqueGaps = [...new Set(sourceGaps)];
|
|
const work = uniqueGaps.map((gapId) => {
|
|
const mapping = byId.get(gapId);
|
|
if (!mapping) throw new Error(`gap has no parity map entry: ${gapId}`);
|
|
return { gapId, mapping, wave: waveFor(mapping.ownerFamily) };
|
|
});
|
|
work.sort((a, b) => a.wave.localeCompare(b.wave) || (a.gapId < b.gapId ? -1 : a.gapId > b.gapId ? 1 : 0));
|
|
const waveCounts = new Map();
|
|
const activeIndex = work.findIndex((value) => !completedIds.has(value.gapId));
|
|
const tasks = work.map((value, index) => {
|
|
const serial = (waveCounts.get(value.wave) ?? 0) + 1;
|
|
waveCounts.set(value.wave, serial);
|
|
const id = `${value.wave}-GAP-${String(serial).padStart(5, "0")}`;
|
|
const fixturePath = `tests/files/web/generated/${id}-${safeName(value.gapId)}.blend`;
|
|
return {
|
|
id,
|
|
gapId: value.gapId,
|
|
ownerFamily: value.mapping.ownerFamily,
|
|
sourceTask: value.mapping.task,
|
|
state: completedIds.has(value.gapId) ? "completed" : index === activeIndex ? "active" : "pending",
|
|
dependencies: [],
|
|
targetImplementationClass: "LOCAL_EXACT",
|
|
fixture: { path: fixturePath, state: "REQUIRED" },
|
|
desktopCommand: `build_blender_5.2.0/bin/blender -b --factory-startup --python tools/web/generated/${id}.py -- ${fixturePath}`,
|
|
webCommand: `npm --prefix web run test:generated-gap -- --task ${id}`,
|
|
comparator: `node tools/web/check-generated-gap.mjs --task ${id}`,
|
|
exitCriteria: ["desktop fixture evidence exists", "WASM uses the same fixture", "comparator passes", "save/reopen preserves Main", "manifest hashes all artifacts"],
|
|
};
|
|
});
|
|
const sha256File = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
|
const completionBytes = fs.readFileSync(completionPath);
|
|
const plan = {
|
|
schemaVersion: 1,
|
|
task: "M15-03A",
|
|
operation: "BLENDER_NEXT_TASK_PLAN",
|
|
sources: { parityMap: { path: "tests/golden/M15-02A/blender-parity-map.json", sha256: sha256File(mapPath) }, gapAudit: { path: "tests/golden/M15-02B/blender-gap-audit.json", sha256: sha256File(gapPath) }, completions: { path: "tests/golden/M15-03A/completed-gap-tasks.json", sha256: crypto.createHash("sha256").update(completionBytes).digest("hex") } },
|
|
summary: { taskCount: tasks.length, active: tasks.filter((task) => task.state === "active").length, pending: tasks.filter((task) => task.state === "pending").length, completed: tasks.filter((task) => task.state === "completed").length, blocked: tasks.filter((task) => task.state === "blocked").length, byWave: Object.fromEntries([...waveCounts.entries()]) },
|
|
tasks,
|
|
firstTask: tasks.find((task) => task.state === "active")?.id ?? null,
|
|
closureGate: "M15-03E",
|
|
nextTask: "M15-03B",
|
|
};
|
|
fs.mkdirSync(path.dirname(output), { recursive: true });
|
|
fs.writeFileSync(output, `${JSON.stringify(plan, null, 2)}\n`);
|
|
process.stdout.write(`blender-next-task-plan-generated tasks=${tasks.length} active=${plan.summary.active} first=${plan.firstTask} output=${output}\n`);
|