120 lines
5.9 KiB
Python
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)
|