Files
workinf_Blender_Wasm/tools/web/generate-wbv2-inventory.mjs
mes123456 c2c65d3a09
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
Rebuild local Blender WASM environment and recovery evidence
2026-08-27 20:22:15 +08:00

210 lines
11 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 defaultOutput = path.join(root, "tests/golden/WBV2-P0-002");
const outputDir = path.resolve(process.argv[2] ?? defaultOutput);
const relative = (file) => path.relative(root, file).replaceAll(path.sep, "/");
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
const readJson = (relativePath) => {
const file = path.join(root, relativePath);
return { path: relativePath, file, bytes: fs.readFileSync(file), value: JSON.parse(fs.readFileSync(file, "utf8")) };
};
const sources = [
readJson("tests/golden/M15-02A/blender-parity-map.json"),
readJson("tests/golden/M15-03A/next-task-plan.json"),
readJson("tests/golden/M15-03A/completed-gap-tasks.json"),
readJson("docs/BLENDER_WASM_CAPABILITY_MATRIX_TEMPLATE.json"),
readJson("docs/BLENDER_WEB_PARITY_TASK_PLAN_V2.json"),
readJson("tests/golden/WBV2-P0-001/manifest.json"),
];
const sourceHashes = Object.fromEntries(sources.map((source) => [source.path, sha256(source.bytes)]));
const sourceRefs = Object.fromEntries(sources.map((source) => [source.path, { path: source.path, sha256: sourceHashes[source.path] }]));
const parityMap = sources[0].value;
const legacyPlan = sources[1].value;
const capabilityMatrix = sources[3].value;
const taskByGapId = new Map(legacyPlan.tasks.map((task) => [task.gapId, task]));
const matrixByFamily = new Map((capabilityMatrix.entries ?? []).map((entry) => [entry.family, entry]));
const evidenceByTask = new Map((parityMap.sourceInventories ? Object.keys(parityMap.sourceInventories) : []).map((task) => [
task,
`tests/golden/${task}/manifest.json`,
]));
const coreSceneNames = new Set(["Scene", "Collection", "Object", "ViewLayer", "LayerCollection", "Window", "WindowManager", "Screen", "RenderSettings", "World", "Camera", "Light", "Material", "NodeTree"]);
const coreMeshNames = new Set(["Mesh", "Attribute", "AttributeGroupMesh", "BoolAttribute", "ByteColorAttribute", "FloatAttribute", "IntAttribute"]);
const familySpec = {
CORE_DNA_RNA: { layer: "CORE", targetParityLevel: "L2" },
CORE_MESH: { layer: "CORE", targetParityLevel: "L3" },
CORE_OBJECT: { layer: "CORE", targetParityLevel: "L3" },
CORE_SCENE_GRAPH: { layer: "CORE", targetParityLevel: "L3" },
MESH_EDITING: { layer: "FEATURE", targetParityLevel: "L3" },
OBJECT_OPERATIONS: { layer: "FEATURE", targetParityLevel: "L3" },
MATERIAL_RENDER: { layer: "FEATURE", targetParityLevel: "L3" },
MODIFIER_SYSTEM: { layer: "FEATURE", targetParityLevel: "L3" },
NODES_GRAPH: { layer: "FEATURE", targetParityLevel: "L3" },
RIG_ANIMATION: { layer: "FEATURE", targetParityLevel: "L3" },
SIMULATION: { layer: "FEATURE", targetParityLevel: "L2" },
SCULPT_PAINT: { layer: "FEATURE", targetParityLevel: "L2" },
MEDIA_IO: { layer: "FEATURE", targetParityLevel: "L3" },
UI_EDITOR: { layer: "UI", targetParityLevel: "L2" },
OTHER_OPERATOR: { layer: "FEATURE", targetParityLevel: "L1" },
UNKNOWN: { layer: "UNCLASSIFIED", targetParityLevel: "L0" },
};
function classify(entry) {
const id = entry.id;
const lower = id.toLowerCase();
const suffix = id.includes(":") ? id.slice(id.indexOf(":") + 1) : id;
const name = suffix.split(":").at(-1) ?? suffix;
if (id.startsWith("datablock:")) {
if (name === "Mesh") return "CORE_MESH";
if (name === "Object") return "CORE_OBJECT";
if (coreSceneNames.has(name)) return "CORE_SCENE_GRAPH";
return "CORE_DNA_RNA";
}
if (id.startsWith("rna:")) {
if (entry.ownerFamily === "Mesh" || coreMeshNames.has(name)) return "CORE_MESH";
if (name === "Object") return "CORE_OBJECT";
if (entry.ownerFamily === "Scene" || entry.ownerFamily === "Main" || entry.ownerFamily === "Depsgraph" || coreSceneNames.has(name)) return "CORE_SCENE_GRAPH";
return "CORE_DNA_RNA";
}
if (id.startsWith("modifier:")) return "MODIFIER_SYSTEM";
if (id.startsWith("constraint:")) return "RIG_ANIMATION";
if (id.startsWith("node:")) return lower.includes("shader") ? "MATERIAL_RENDER" : "NODES_GRAPH";
if (id.startsWith("physics:")) return "SIMULATION";
if (id.startsWith("strip:")) return "MEDIA_IO";
if (id.startsWith("io:")) return "MEDIA_IO";
if (/^(area|region|space|space-class|workspace|keymap|keymap-item):/u.test(id)) return "UI_EDITOR";
if (id.startsWith("operator:")) {
const module = id.slice("operator:".length).split(".")[0];
if (["node", "geometry"].includes(module)) return "NODES_GRAPH";
if (["grease_pencil", "gpencil", "curve", "curves", "font", "lattice", "mball", "pointcloud", "surface", "geometry"].includes(module)) return "MESH_EDITING";
if (["sequencer", "sound", "image", "clip", "mask", "text", "file", "wm", "import_scene", "export_scene", "import_anim", "export_anim", "import_curve", "asset", "collection", "catalog"].includes(module)) return "MEDIA_IO";
if (["graph", "anim", "nla", "poselib", "marker", "action", "armature", "pose"].includes(module)) return "RIG_ANIMATION";
if (["view3d", "view2d", "outliner", "screen", "ui", "preferences", "console", "info", "buttons", "spreadsheet", "uilist", "workspace", "ed", "text_editor"].includes(module)) return "UI_EDITOR";
if (["paint", "sculpt", "brush", "paintcurve", "sculpt_curves"].includes(module)) return "SCULPT_PAINT";
if (["rigidbody", "boid", "ptcache", "cloth", "dpaint", "cachefile"].includes(module)) return "SIMULATION";
if (["cycles", "render", "camera", "light"].includes(module)) return "MATERIAL_RENDER";
if (["scene", "transform", "lattice"].includes(module)) return "OBJECT_OPERATIONS";
if (lower.includes("asset.catalog")) return "MEDIA_IO";
if (lower.includes("mesh.")) return "MESH_EDITING";
if (lower.includes("object.")) return "OBJECT_OPERATIONS";
if (/(material|shader|render|light|world)\./u.test(lower)) return "MATERIAL_RENDER";
if (/(armature|pose|action|keyframe|animation|constraint)\./u.test(lower)) return "RIG_ANIMATION";
if (/(physics|fluid|particle|simulation|bake)\./u.test(lower)) return "SIMULATION";
if (/(import|export|save|open|library|catalog)\./u.test(lower)) return "MEDIA_IO";
return "OTHER_OPERATOR";
}
return "UNKNOWN";
}
function completionFor(state) {
// Inventory state is deliberately separate from parity. A completed legacy
// receipt is useful coverage evidence, but it does not prove browser parity.
return { completionClass: "METADATA_ONLY", parityLevel: "L0", evidenceStatus: "NOT_RUN", state };
}
const entries = parityMap.entries.map((entry) => {
const task = taskByGapId.get(entry.id);
if (!task) throw new Error(`legacy plan is missing inventory entry ${entry.id}`);
const family = classify(entry);
const spec = familySpec[family];
const legacyState = task.state === "completed" ? "completed" : task.state === "active" ? "active" : "pending";
return {
id: entry.id,
sourceTask: entry.task,
kind: entry.id.split(":", 1)[0],
ownerFamily: entry.ownerFamily,
capabilityId: `CAP-${family.replaceAll("_", "-")}`,
family,
layer: spec.layer,
classificationStatus: family === "UNKNOWN" ? "UNKNOWN_EXPLICIT" : "MAPPED",
legacyState,
targetImplementationClass: task.targetImplementationClass,
...completionFor(legacyState),
targetParityLevel: spec.targetParityLevel,
parityClaim: "NONE",
evidence: {
sourceManifest: evidenceByTask.get(entry.task) ?? null,
legacyTask: task.id,
representativeOnly: true,
},
};
}).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
const ids = entries.map((entry) => entry.id);
if (new Set(ids).size !== ids.length) throw new Error("duplicate inventory IDs");
const allFamilies = [...new Set(entries.map((entry) => entry.family))].sort();
const capabilities = allFamilies.map((family) => {
const members = entries.filter((entry) => entry.family === family);
const spec = familySpec[family];
const completed = members.filter((entry) => entry.legacyState === "completed").length;
const pending = members.filter((entry) => entry.legacyState === "pending").length;
const active = members.filter((entry) => entry.legacyState === "active").length;
const matrixEntry = [...matrixByFamily.values()].find((entry) => entry.family === family);
return {
capabilityId: `CAP-${family.replaceAll("_", "-")}`,
family,
layer: spec.layer,
entryCount: members.length,
legacyCompleted: completed,
legacyPending: pending,
legacyActive: active,
completionClass: "METADATA_ONLY",
parityLevel: "L0",
targetParityLevel: spec.targetParityLevel,
lifecycleStatus: "planned",
evidenceStatus: "NOT_RUN",
sourceCapability: matrixEntry?.capabilityId ?? null,
claim: "inventory_only_no_feature_parity",
};
});
const summary = {
inventoryEntries: entries.length,
capabilityCount: capabilities.length,
unknownEntries: entries.filter((entry) => entry.family === "UNKNOWN").length,
completedLegacyEntries: entries.filter((entry) => entry.legacyState === "completed").length,
pendingLegacyEntries: entries.filter((entry) => entry.legacyState === "pending").length,
activeLegacyEntries: entries.filter((entry) => entry.legacyState === "active").length,
featureParityClaims: entries.filter((entry) => entry.parityClaim !== "NONE" || entry.parityLevel !== "L0").length,
byKind: Object.fromEntries([...new Set(entries.map((entry) => entry.kind))].sort().map((kind) => [kind, entries.filter((entry) => entry.kind === kind).length])),
byLayer: Object.fromEntries([...new Set(entries.map((entry) => entry.layer))].sort().map((layer) => [layer, entries.filter((entry) => entry.layer === layer).length])),
};
const inventory = {
schemaVersion: 1,
task: "WBV2-P0-002",
operation: "BLENDER_52_INVENTORY",
status: "INVENTORY_ONLY",
runtime: "Blender 5.2 baseline inventories",
sourceHashes,
summary,
entries: entries.map(({ id, sourceTask, kind, ownerFamily }) => ({ id, sourceTask, kind, ownerFamily })),
nextTask: "WBV2-P0-003",
};
const parity = {
schemaVersion: 1,
task: "WBV2-P0-002",
operation: "BLENDER_52_CAPABILITY_PARITY_MAP",
statusAxis: "INVENTORY_ONLY",
implementationClasses: ["METADATA_ONLY"],
sourceInventories: sourceRefs,
summary,
capabilities,
entries,
nextTask: "WBV2-P0-003",
};
fs.mkdirSync(outputDir, { recursive: true });
fs.writeFileSync(path.join(outputDir, "source-hashes.json"), `${JSON.stringify({ schemaVersion: 1, task: "WBV2-P0-002", sources: sourceHashes }, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "inventory.json"), `${JSON.stringify(inventory, null, 2)}\n`);
fs.writeFileSync(path.join(outputDir, "parity-map.json"), `${JSON.stringify(parity, null, 2)}\n`);
process.stdout.write(`wbv2-inventory-generated entries=${entries.length} capabilities=${capabilities.length} unknown=${summary.unknownEntries} output=${relative(outputDir)}\n`);