Files
FreeCADWasmDev/tools/run_p6_python_api_module_batch_gate.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

120 lines
5.9 KiB
Python

#!/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)