#!/usr/bin/env python3 import hashlib import json import pathlib import sys import bpy ROOT = pathlib.Path(__file__).resolve().parents[3] PARITY_MAP = ROOT / "tests/golden/WBV2-P0-002/parity-map.json" def digest_file(path): return hashlib.sha256(path.read_bytes()).hexdigest() def risk_score(capability): levels = {"L0": 0, "L1": 1, "L2": 2, "L3": 3} return (capability["legacyPending"] * 100 + capability["legacyActive"] * 50 + capability["entryCount"] * 10 + levels[capability["targetParityLevel"]] * 1000) def build_receipt(): parity = json.loads(PARITY_MAP.read_text(encoding="utf-8")) if parity.get("schemaVersion") != 1 or parity.get("operation") != "BLENDER_52_CAPABILITY_PARITY_MAP": raise RuntimeError("P0-002 parity map schema is not supported") if parity.get("summary", {}).get("featureParityClaims") != 0 or parity["summary"].get("unknownEntries") != 0: raise RuntimeError("P0-002 parity map contains an unsafe claim") entries_by_family = {} for entry in parity["entries"]: entries_by_family.setdefault(entry["family"], []).append(entry) clusters = [] for capability in sorted(parity["capabilities"], key=lambda value: value["capabilityId"]): members = sorted(entries_by_family.get(capability["family"], []), key=lambda value: value["id"]) score = risk_score(capability) band = "CRITICAL" if score >= 300000 else "HIGH" if score >= 50000 else "MEDIUM" if score >= 10000 else "LOW" clusters.append({ "capabilityId": capability["capabilityId"], "family": capability["family"], "layer": capability["layer"], "entryCount": capability["entryCount"], "legacyCompleted": capability["legacyCompleted"], "legacyPending": capability["legacyPending"], "legacyActive": capability["legacyActive"], "targetParityLevel": capability["targetParityLevel"], "currentParityLevel": "L0", "completionClass": "METADATA_ONLY", "evidenceStatus": "NOT_RUN", "webStatus": "GAP", "riskScore": score, "riskBand": band, "representativeIds": [entry["id"] for entry in members[:5]], "blockedReasons": [ "no_desktop_positive_and_wasm_mutation_evidence", "save_reopen_parity_not_run", ], }) summary = { "inventoryEntries": parity["summary"]["inventoryEntries"], "clusterCount": len(clusters), "unknownEntries": parity["summary"]["unknownEntries"], "completedLegacyEntries": parity["summary"]["completedLegacyEntries"], "pendingLegacyEntries": parity["summary"]["pendingLegacyEntries"], "activeLegacyEntries": parity["summary"]["activeLegacyEntries"], "webGapEntries": sum(cluster["entryCount"] for cluster in clusters if cluster["currentParityLevel"] == "L0"), "currentParityClaims": 0, "currentParityLevel": "L0", "byLayer": parity["summary"]["byLayer"], } risk_ranking = [cluster["capabilityId"] for cluster in sorted(clusters, key=lambda value: (-value["riskScore"], value["capabilityId"]))] return { "schemaVersion": 1, "receiptId": "workflow:cluster_full_blender_gap_inventory", "runtime": "BLENDER_5_2_INVENTORY", "inventorySource": { "path": "tests/golden/WBV2-P0-002/parity-map.json", "sha256": digest_file(PARITY_MAP), "entryCount": parity["summary"]["inventoryEntries"], }, "classificationPolicy": { "featureParityRequires": ["desktopPositive", "wasmMutation", "saveReopenExact"], "negativeOnlyClasses": ["NEGATIVE_BOUNDARY", "UNSUPPORTED_DECLARED", "BLOCKED"], "unprovenClass": "METADATA_ONLY", "parityLevel": "L0", }, "clusters": clusters, "summary": summary, "riskRanking": risk_ranking, "nextTask": "WBV2-P7-002", } def main(output): bpy.ops.wm.read_factory_settings(use_empty=True) scene = bpy.context.scene scene.name = "WBV2P7001GapInventoryScene" scene.render.engine = "BLENDER_EEVEE" scene.render.resolution_x = 32 scene.render.resolution_y = 32 scene.render.resolution_percentage = 100 bpy.ops.mesh.primitive_cube_add(size=2) cube = bpy.context.object cube.name = "WBV2P7001GapInventoryCube" cube.data.name = "WBV2P7001GapInventoryMesh" receipt = build_receipt() text = bpy.data.texts.new("WBV2-P7-001-ClusterFullBlenderGapInventoryReceipt") text.write(json.dumps(receipt, sort_keys=True, separators=(",", ":"))) scene.frame_set(1) bpy.ops.wm.save_as_mainfile(filepath=str(pathlib.Path(output).resolve()), check_existing=False, compress=True) if __name__ == "__main__": arguments = sys.argv[sys.argv.index("--") + 1 :] if len(arguments) != 1: raise SystemExit("usage: blender -b --factory-startup --python WBV2-P7-001.py -- OUTPUT") main(arguments[0])