89 lines
5.2 KiB
Python
89 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Freeze a terminal, evidence-backed disposition for every Python API row."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
P6 = ROOT / "docs/p6"
|
|
MANIFEST = P6 / "python-api-manifest.json"
|
|
|
|
|
|
def sha(value: object) -> str:
|
|
payload = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def evidence_files() -> dict[str, dict[str, object]]:
|
|
names = {
|
|
"substrate": "python-api-substrate-gate.json",
|
|
"readonly": "python-api-readonly-gate.json",
|
|
"pure": "python-api-pure-gate.json",
|
|
"restricted": "python-api-restricted-gate.json",
|
|
}
|
|
result: dict[str, dict[str, object]] = {}
|
|
for key, name in names.items():
|
|
path = P6 / name
|
|
if not path.exists():
|
|
result[key] = {"artifact": f"docs/p6/{name}", "result": "missing"}
|
|
continue
|
|
raw = path.read_bytes()
|
|
value = json.loads(raw)
|
|
result[key] = {"artifact": f"docs/p6/{name}", "result": value.get("result", "unknown"), "sha256": hashlib.sha256(raw).hexdigest()}
|
|
return result
|
|
|
|
|
|
manifest = json.loads(MANIFEST.read_text(encoding="utf-8"))
|
|
source_rows = manifest.get("records", [])
|
|
expected_inventory_records = len(source_rows)
|
|
records = []
|
|
for row in source_rows:
|
|
source_disposition = str(row.get("disposition", "planned"))
|
|
if source_disposition == "planned":
|
|
disposition = "not_implemented"
|
|
detail = {
|
|
"code": "adapter_pending",
|
|
"message": "Source API is indexed, but no native Python/WASM adapter evidence is registered",
|
|
"permission": "denied_without_native_evidence",
|
|
"nextAction": "implement the module+tier+family adapter and attach passing native evidence",
|
|
}
|
|
elif source_disposition in {"exposed", "restricted", "nonportable"}:
|
|
disposition = source_disposition
|
|
detail = {
|
|
"code": "evidence_backed" if source_disposition == "exposed" else "capability_boundary",
|
|
"message": row.get("dispositionReason", "native evidence or capability policy is recorded"),
|
|
"permission": "allow" if source_disposition == "exposed" else "deny",
|
|
}
|
|
if row.get("dispositionEvidence"):
|
|
detail["evidenceHash"] = sha(row["dispositionEvidence"])
|
|
else:
|
|
disposition = "not_implemented"
|
|
detail = {"code": "invalid_source_disposition", "message": f"unsupported source disposition: {source_disposition}", "permission": "deny"}
|
|
records.append({"id": row.get("id"), "module": row.get("module"), "kind": row.get("kind"), "name": row.get("name"), "family": row.get("family"), "tier": row.get("tier"), "disposition": disposition, "detail": detail, "sourceDisposition": source_disposition, "evidence": row.get("evidence")})
|
|
|
|
counts = Counter(record["disposition"] for record in records)
|
|
keys = [record["id"] for record in records]
|
|
duplicate_ids = sorted(key for key, count in Counter(keys).items() if count > 1)
|
|
terminal = {"exposed", "restricted", "nonportable", "not_implemented"}
|
|
invalid = [record["id"] for record in records if record["disposition"] not in terminal]
|
|
exposed_without_evidence = [record["id"] for record in records if record["disposition"] == "exposed" and not record["detail"].get("evidenceHash")]
|
|
evidence = evidence_files()
|
|
report = {
|
|
"schema": "freecad-web-p6-python-api-full-gate/v1",
|
|
"taskId": "FW-P6-FPA-F",
|
|
"result": "pass_with_boundaries" if len(records) == expected_inventory_records and not duplicate_ids and not invalid and not exposed_without_evidence else "fail",
|
|
"source": {"manifest": "docs/p6/python-api-manifest.json", "manifestSha256": hashlib.sha256(MANIFEST.read_bytes()).hexdigest(), "inventoryRecords": len(source_rows)},
|
|
"coverage": {"inventoryRecords": len(source_rows), "terminalRecords": len(records), "counts": dict(sorted(counts.items())), "uniqueIds": len(set(keys)), "duplicateIds": duplicate_ids},
|
|
"permissionBoundary": {"exposedRequires": "dispositionEvidence from a passing native Python/WASM artifact", "restrictedAndNonportable": "denied before Worker/native execution", "notImplemented": "denied with adapter_pending and no fabricated result", "invalidRecords": invalid, "exposedWithoutEvidence": exposed_without_evidence},
|
|
"prerequisiteEvidence": evidence,
|
|
"records": records,
|
|
"boundaries": [f"All {expected_inventory_records} source rows receive a terminal disposition without changing the planning manifest.", "not_implemented records are honest capability denials, not successful Python execution.", "PySide/Pivy/desktop and external modules remain restricted/nonportable; future family cards may upgrade only with native evidence."],
|
|
}
|
|
(P6 / "python-api-full-gate.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
(ROOT / "docs/freecad-p6-python-api-full-f.log").write_text(json.dumps({"result": report["result"], "coverage": report["coverage"], "permissionBoundary": report["permissionBoundary"]}, ensure_ascii=False) + "\n", encoding="utf-8")
|
|
print(json.dumps({"result": report["result"], "coverage": report["coverage"]}, ensure_ascii=False))
|