master
Drew Owens Publish 2.6.0 3148417 2mo ago
#!/usr/bin/env python3
"""render_parity.py - catch a degraded render/board.glb at publish (adom/wiki#91).

`--render-board` (nucleus/service-kicad) sometimes writes a DEGRADED
`render/board.glb` - fewer meshes than the board really has - and because board
pages declare `model_3d_path = render/1.0.0/board.glb`, that degraded file is
what the 3D tab serves. The wiki-side alphabetical fallback was fixed (Colby),
but a declared path still selects the degraded copy, so the pipeline must
refuse to publish one.

Signal is MESH COUNT, never file size (Colby: 28-byte deltas are mesh-identical,
and some molecules have a legitimately larger render/ copy - size gives false
positives both ways). When the render is faithful, `render/board.glb` and the
page-level `<slug>.glb` have the SAME mesh count (verified on the workcell
boards: zu 190=190, pinch 155=155; the degraded ones diverged 35 vs 336, 11 vs
20). So the gate is: render/board.glb mesh count >= the page model's, within a
small tolerance.

  check <render/board.glb> <page-model.glb> [--tolerance 0.05]
      exit 0 = parity OK; exit 1 = render is degraded (do not publish)
"""
import argparse
import json
import struct
import sys


def mesh_count(path: str) -> int:
    with open(path, "rb") as f:
        b = f.read()
    if b[:4] != b"glTF":
        raise ValueError(f"{path} is not a GLB")
    json_len = struct.unpack_from("<I", b, 12)[0]
    j = json.loads(b[20:20 + json_len])
    return len(j.get("meshes", []))


def main():
    ap = argparse.ArgumentParser(description=__doc__,
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
    ap.add_argument("render_glb", help="render/board.glb emitted by --render-board")
    ap.add_argument("page_glb", help="the page-level <slug>.glb (the faithful model)")
    ap.add_argument("--tolerance", type=float, default=0.05,
                    help="allowed shortfall fraction (default 0.05 = render may be "
                         "up to 5%% below the page model before it's called degraded)")
    a = ap.parse_args()

    try:
        rn = mesh_count(a.render_glb)
        pg = mesh_count(a.page_glb)
    except Exception as e:
        print(f"FAIL: {e}")
        return 2

    floor = pg * (1 - a.tolerance)
    if rn < floor:
        pct = 100 * rn / pg if pg else 0
        print(f"FAIL: render/board.glb is DEGRADED - {rn} meshes vs {pg} in the page "
              f"model ({pct:.0f}%). The 3D tab would serve a stripped model "
              f"(adom/wiki#91). Re-run --render-board; if it recurs, the render emitter "
              f"is dropping geometry (report to the --render-board / service-kicad owner "
              f"with this board).")
        return 1
    extra = f" (render has {rn - pg} more - a legitimately richer render/ copy)" if rn > pg else ""
    print(f"OK: render/board.glb parity - {rn} meshes vs {pg} in the page model{extra}.")
    return 0


if __name__ == "__main__":
    sys.exit(main())