191 lines
8.3 KiB
Python
191 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate a deterministic, source-backed Python API migration manifest.
|
|
|
|
The inventory remains the source of truth for individual symbols. This
|
|
manifest adds stable IDs, reusable adapter families, migration tiers, and
|
|
evidence pointers without pretending that a source record is an implemented
|
|
Web binding.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import re
|
|
from collections import Counter, defaultdict
|
|
from datetime import date
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
DOCS = ROOT / "docs"
|
|
INPUT = DOCS / "format-api-inventory.json"
|
|
OUTPUT = DOCS / "p6" / "python-api-manifest.json"
|
|
RUNTIME_EVIDENCE = DOCS / "p6" / "python-api-runtime-evidence.json"
|
|
|
|
GUI_EXTERNAL_MODULES = {"Help", "Start", "Tux"}
|
|
|
|
|
|
def token_name(name: str) -> str:
|
|
return name.rsplit(".", 1)[-1].lower()
|
|
|
|
|
|
def family_for(row: dict[str, object]) -> str:
|
|
"""Map symbols to a small adapter family, using only stable source text."""
|
|
name = token_name(str(row["name"]))
|
|
full_name = str(row["name"]).lower()
|
|
if str(row["module"]) == "Measure" and (full_name.startswith("measurecom") or full_name.startswith("measurebasepython") or full_name == "makemeasurecom"):
|
|
return "gui-external"
|
|
if str(row["module"]) == "Measure" and (full_name.startswith("measurement.") or full_name.startswith("measurecom.")):
|
|
return "measurement-state"
|
|
if any(token in full_name for token in ("pyside", "pyqt", "qt.", "pivy", "coin.", "coin3d")):
|
|
return "gui-external"
|
|
if any(token in name for token in ("dump", "load", "serialize", "deserialize", "restore", "save", "read", "write")):
|
|
return "serialization"
|
|
if any(token in name for token in ("icon", "resource", "tooltip", "menu", "toolbar", "display", "render", "view", "visual")):
|
|
return "view-resource"
|
|
if any(token in name for token in ("property", "properties", "param", "parameter", "field", "label", "name", "value", "type")):
|
|
return "property"
|
|
if any(token in name for token in ("document", "object", "claimchildren", "child", "parent", "group", "link", "selection")):
|
|
return "document-graph"
|
|
if any(token in name for token in ("activate", "deactivate", "initialize", "init", "setup", "teardown", "finish", "cancel", "accept", "reject", "execute", "action", "command", "task")):
|
|
return "lifecycle-command"
|
|
if any(token in name for token in ("shape", "solid", "face", "edge", "wire", "mesh", "vector", "matrix", "geometry", "point", "curve", "surface")):
|
|
return "geometry-value"
|
|
if str(row["kind"]) == "function":
|
|
return "pure-function"
|
|
return "generic-object"
|
|
|
|
|
|
def tier_for(row: dict[str, object], family: str) -> str:
|
|
module = str(row["module"])
|
|
name = token_name(str(row["name"]))
|
|
if family == "gui-external":
|
|
return "tier-4-restricted"
|
|
if family == "measurement-state":
|
|
return "tier-3-stateful"
|
|
if module in GUI_EXTERNAL_MODULES or family == "gui-external":
|
|
return "tier-4-restricted"
|
|
if family in {"property", "document-graph", "lifecycle-command", "serialization"}:
|
|
return "tier-3-stateful"
|
|
if family in {"geometry-value", "pure-function"} and str(row["kind"]) == "function":
|
|
return "tier-2-pure"
|
|
if name.startswith(("get", "is", "has", "can", "supports")) or family == "view-resource":
|
|
return "tier-1-readonly"
|
|
if str(row["kind"]) == "class":
|
|
return "tier-1-readonly"
|
|
return "tier-2-pure"
|
|
|
|
|
|
def stable_id(row: dict[str, object]) -> str:
|
|
module = str(row["module"]).strip()
|
|
kind = str(row["kind"]).strip().lower()
|
|
name = re.sub(r"\s+", "", str(row["name"]).strip())
|
|
return f"py:{module}:{kind}:{name}"
|
|
|
|
|
|
def main() -> None:
|
|
source = INPUT.read_bytes()
|
|
inventory = json.loads(source)
|
|
rows = inventory["python_api"]
|
|
runtime_names = set()
|
|
runtime_artifacts: dict[tuple[str, str], list[str]] = {}
|
|
if RUNTIME_EVIDENCE.exists():
|
|
runtime = json.loads(RUNTIME_EVIDENCE.read_text(encoding="utf-8"))
|
|
for item in runtime.get("names", []):
|
|
module = str(item.get("module", "")).strip().lower()
|
|
name = str(item.get("name", "")).strip().lower()
|
|
if module and name:
|
|
runtime_names.add((module, name))
|
|
runtime_artifacts[(module, name)] = [str(path) for path in item.get("artifacts", [])]
|
|
exact_keys = [(str(row["module"]), str(row["kind"]), str(row["name"])) for row in rows]
|
|
duplicate_counts = Counter(exact_keys)
|
|
duplicates = sorted(
|
|
{"key": list(key), "count": count} for key, count in duplicate_counts.items() if count > 1
|
|
)
|
|
|
|
records = []
|
|
family_counts = Counter()
|
|
tier_counts = Counter()
|
|
module_stats: dict[str, Counter[str]] = defaultdict(Counter)
|
|
for row in sorted(rows, key=lambda item: (str(item["module"]), str(item["kind"]), str(item["name"]))):
|
|
family = family_for(row)
|
|
tier = tier_for(row, family)
|
|
full_name = str(row["name"])
|
|
runtime_module = str(row["module"]).lower()
|
|
runtime_key = full_name.lower()
|
|
runtime_leaf = full_name.rsplit(".", 1)[-1].lower()
|
|
runtime_match = (runtime_module, runtime_key) in runtime_names or (runtime_module, runtime_leaf) in runtime_names
|
|
disposition = "restricted" if tier == "tier-4-restricted" else "exposed" if runtime_match else "planned"
|
|
record = {
|
|
"id": stable_id(row),
|
|
"module": str(row["module"]),
|
|
"kind": str(row["kind"]),
|
|
"name": str(row["name"]),
|
|
"family": family,
|
|
"tier": tier,
|
|
"disposition": disposition,
|
|
"evidence": {"path": str(row["path"]), "line": int(row["line"])},
|
|
}
|
|
if tier == "tier-4-restricted":
|
|
record["dispositionReason"] = "requires GUI/PySide/Pivy/desktop migration or external UI capability outside GUI-free Web runtime"
|
|
elif runtime_match:
|
|
record["dispositionEvidence"] = runtime_artifacts.get((runtime_module, runtime_key), runtime_artifacts.get((runtime_module, runtime_leaf), []))
|
|
record["dispositionReason"] = "passing native python-api runtime evidence matched by fully-qualified or leaf API name"
|
|
records.append(record)
|
|
family_counts[family] += 1
|
|
tier_counts[tier] += 1
|
|
module_stats[record["module"]][record["kind"]] += 1
|
|
|
|
modules = []
|
|
for module in sorted(module_stats):
|
|
counts = module_stats[module]
|
|
modules.append({
|
|
"module": module,
|
|
"count": sum(counts.values()),
|
|
"classes": counts["class"],
|
|
"functions": counts["function"],
|
|
"methods": counts["method"],
|
|
})
|
|
|
|
manifest = {
|
|
"schema": "freecad-web-python-api-manifest/v1",
|
|
"generatedAt": date.today().isoformat(),
|
|
"source": {
|
|
"inventory": "docs/format-api-inventory.json",
|
|
"inventorySha256": hashlib.sha256(source).hexdigest(),
|
|
"sourceRevision": inventory.get("source_revision", "unknown"),
|
|
},
|
|
"counts": {
|
|
"inputRecords": len(rows),
|
|
"canonicalRecords": len(records),
|
|
"exactDuplicateRecords": len(rows) - len(duplicate_counts),
|
|
"exactDuplicateKeys": len(duplicates),
|
|
"byKind": dict(sorted(Counter(str(row["kind"]) for row in rows).items())),
|
|
"byFamily": dict(sorted(family_counts.items())),
|
|
"byTier": dict(sorted(tier_counts.items())),
|
|
},
|
|
"identity": {
|
|
"key": "module + kind + fully-qualified name",
|
|
"sourcePathAndLine": "evidence only; never part of identity",
|
|
"overloadPolicy": "group by family adapter and preserve source signature evidence when extracted",
|
|
},
|
|
"modules": modules,
|
|
"duplicates": duplicates,
|
|
"records": records,
|
|
}
|
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUTPUT.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps({
|
|
"output": str(OUTPUT.relative_to(ROOT)),
|
|
"inputRecords": len(rows),
|
|
"canonicalRecords": len(records),
|
|
"exactDuplicateRecords": manifest["counts"]["exactDuplicateRecords"],
|
|
"families": len(family_counts),
|
|
"tiers": dict(sorted(tier_counts.items())),
|
|
"sha256": hashlib.sha256(OUTPUT.read_bytes()).hexdigest(),
|
|
}, ensure_ascii=False))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|