#!/usr/bin/env python3 """Assert the DJ-gear named-node contract survives normalization — read from the GLB, not Blender. python3 pipeline/check_contract.py SRC_DIR OUT_DIR MAP.json python3 pipeline/check_contract.py --list FILE.glb [FILE.glb ...] The contract 90sDJsim's GEAR driver reads (dj-gear/README.md) is five bare node names: Platter_SPIN · Arm_YAW · Fader_PITCH · Fader_CROSS · Btn_STARTSTOP Integrator ruling 5 exists because `gltf-transform optimize` silently deleted `Fader_CROSS` from a 696-node mixer. "Silently" is the operative word, so this check parses the **shipped GLB's** node list rather than asking the tool that produced it — a scene-graph scan inside Blender still sees control EMPTIES that were never exported, and would have reported a contract that the file does not actually carry. Verdict per model: PASS = every contract node present in the source is present in the output. FAIL = one or more were lost (the ruling-5 failure mode). N/A = the source carried none (a cartridge has no crossfader). Exit 1 if any FAIL. """ import json, os, struct, sys CONTRACT = ["Platter_SPIN", "Arm_YAW", "Fader_PITCH", "Fader_CROSS", "Btn_STARTSTOP"] def glb_nodes(path): """Node names from a GLB's JSON chunk. Dependency-free — no Blender, no pygltflib.""" d = open(path, "rb").read() if d[:4] != b"glTF": raise ValueError("not a GLB") off = 12 while off + 8 <= len(d): clen, ctype = struct.unpack("