#!/usr/bin/env python3 import hashlib import json import sys from collections import Counter from pathlib import Path ROOT = Path(__file__).resolve().parents[1] module = sys.argv[1] if len(sys.argv) > 1 else "OpenSCAD" tier = sys.argv[2] if len(sys.argv) > 2 else "tier-2-pure" manifest_path = ROOT / "docs/p6/python-api-manifest.json" manifest = json.loads(manifest_path.read_text()) rows = [row for row in manifest["records"] if row["module"] == module and row["tier"] == tier] counts = Counter(row["disposition"] for row in rows) safe = "".join(ch if ch.isalnum() else "-" for ch in module.lower()) terminal = {"exposed", "restricted", "nonportable", "not_implemented"} records = [ { "id": row["id"], "name": row["name"], "disposition": row["disposition"], "evidence": row.get("dispositionEvidence", []), "reason": row.get("dispositionReason"), } for row in rows ] extra = {} if tier == "tier-3-stateful": runtime_path = ROOT / "docs/p6/python-api-stateful-family-runtime.json" adapter_path = ROOT / "docs/p6/tier3-stateful-adapter-manifest.json" runtime = json.loads(runtime_path.read_text()) adapter = json.loads(adapter_path.read_text()) native_by_id = {record["apiId"]: record for record in runtime.get("result", {}).get("records", [])} summary_by_id = {record["apiId"]: record for record in runtime.get("records", [])} adapter_module = next((item for item in adapter.get("modules", []) if item.get("module") == module), None) adapter_route = adapter_module.get("route", {}) if adapter_module else {} native_route = runtime.get("route", {}) def native_record_valid(row): native = native_by_id.get(row["id"]) summary = summary_by_id.get(row["id"]) if not native or not summary: return False compact = json.dumps(native, ensure_ascii=False, separators=(",", ":")) native_sha256 = hashlib.sha256(compact.encode()).hexdigest() return ( native.get("module") == row["module"] and native.get("name") == row["name"] and native.get("family") == row["family"] and native.get("sourceEntry") == row.get("evidence", {}).get("path") and native.get("status") == row["disposition"] and native.get("status") in terminal and native.get("native") is True and native.get("resourcesReleased") is True and isinstance(native.get("parameterSchema"), str) and bool(native["parameterSchema"]) and isinstance(native.get("returnSchema"), str) and bool(native["returnSchema"]) and isinstance(native.get("documentDiff"), dict) and isinstance(native.get("errorRecovery"), dict) and summary.get("evidenceSha256") == native_sha256 and runtime_path.relative_to(ROOT).as_posix() in row.get("dispositionEvidence", []) ) valid = ( bool(rows) and runtime.get("status") == "pass_with_boundaries" and adapter_module is not None and adapter_module.get("count") == len(rows) and isinstance(adapter_route.get("worker"), str) and bool(adapter_route["worker"]) and isinstance(adapter_route.get("command"), str) and bool(adapter_route["command"]) and native_route.get("command") == "web.statefulFamilyBatch" and "fc_web_dispatch_json" in native_route.get("native", "") and all(row["disposition"] in terminal and native_record_valid(row) for row in rows) ) records = [ { **record, "family": row["family"], "sourceEntry": summary_by_id.get(row["id"], {}).get("sourceEntry"), "sourceRevision": summary_by_id.get(row["id"], {}).get("sourceRevision"), "parameterSchema": summary_by_id.get(row["id"], {}).get("parameterSchema"), "returnSchema": summary_by_id.get(row["id"], {}).get("returnSchema"), "documentDiffSha256": summary_by_id.get(row["id"], {}).get("documentDiffSha256"), "errorRecoverySha256": summary_by_id.get(row["id"], {}).get("errorRecoverySha256"), "evidenceSha256": summary_by_id.get(row["id"], {}).get("evidenceSha256"), } for row, record in zip(rows, records) ] extra = { "nativeRuntime": runtime_path.relative_to(ROOT).as_posix(), "nativeRuntimeSha256": hashlib.sha256(runtime_path.read_bytes()).hexdigest(), "adapterManifest": adapter_path.relative_to(ROOT).as_posix(), "adapterManifestSha256": hashlib.sha256(adapter_path.read_bytes()).hexdigest(), "route": adapter_route or None, "nativeRoute": native_route or None, "resources": runtime.get("resources"), } else: valid = bool(rows) and all(row["disposition"] in terminal | {"planned"} for row in rows) report = { "schema": "freecad-web-p6-python-api-module-batch-gate/v1", "taskId": f"FW-P6-FPA-F{tier.split('-')[1]}-{module.upper()}", "module": module, "tier": tier, "result": ("pass" if tier == "tier-3-stateful" else "pass_with_pending") if valid else "fail", "coverage": {"records": len(rows), "counts": dict(sorted(counts.items()))}, "records": records, "manifestSha256": hashlib.sha256(manifest_path.read_bytes()).hexdigest(), **extra, "boundaries": ["Terminal disposition does not claim that restricted or not_implemented APIs are callable.", "Only exact module-scoped native evidence can promote a symbol."] } out = ROOT / "docs/p6" / f"python-api-{safe}-{tier}-gate.json" out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") (ROOT / "docs" / f"freecad-p6-python-api-{safe}-{tier}-gate.log").write_text(json.dumps({"result": report["result"], "coverage": report["coverage"]}) + "\n") print(json.dumps({"module": module, "tier": tier, "result": report["result"], "coverage": report["coverage"]}, ensure_ascii=False)) raise SystemExit(0 if valid else 1)