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`);