Files
FreeCADWasmDev/tools/generate_object_contract_matrix.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

88 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""Generate the source-backed 403-object contract matrix."""
from __future__ import annotations
import hashlib
import json
import re
from collections import Counter
from datetime import date
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
INPUT = DOCS / "object-inventory.json"
OUTPUT = DOCS / "p6" / "object-contract-matrix.json"
def namespace(name: str) -> str:
return name.split("::", 1)[0] if "::" in name else "App"
def family(name: str, module: str) -> str:
low = name.lower()
if any(token in low for token in ("link", "group", "assembly", "joint", "dependency", "container")):
return "document-graph"
if any(token in low for token in ("view", "visual", "draw", "cosmetic", "hatch", "section")) or module == "TechDraw":
return "view-provider"
if any(token in low for token in ("mesh", "point", "surface", "shape", "feature", "solid", "face", "edge", "wire", "sketch", "box", "cylinder", "sphere", "boolean", "pad", "pocket")):
return "geometry-recompute"
if module in {"Spreadsheet", "Material", "Inspection", "Measure", "JtReader"}:
return "data-property"
return "generic-document-object"
def capabilities(row: dict[str, object], adapter: str) -> list[str]:
evidence = {str(item.get("kind")) for item in row.get("evidence", [])}
result = ["native-registration", "inspect"]
if "add_object" in evidence or adapter in {"geometry-recompute", "document-graph", "data-property"}:
result += ["create", "recompute"]
if adapter == "document-graph":
result += ["property", "link"]
if adapter == "view-provider":
result += ["view-projection"]
result.append("fcstd-persistence")
return sorted(set(result))
def main() -> None:
source = INPUT.read_bytes()
rows = json.loads(source)["objects"]
records = []
families = Counter()
modules = Counter()
for row in sorted(rows, key=lambda item: (str(item["module"]), str(item["name"]))):
name = str(row["name"])
module = str(row["module"])
adapter = family(name, module)
record = {
"id": f"obj:{module}:{re.sub(r'[^A-Za-z0-9_.:-]+', '_', name)}",
"name": name,
"module": module,
"namespace": namespace(name),
"adapterFamily": adapter,
"capabilities": capabilities(row, adapter),
"status": "planned",
"evidence": [{"kind": item["kind"], "path": item["path"], "line": item["line"]} for item in row["evidence"]],
}
records.append(record)
families[adapter] += 1
modules[module] += 1
report = {
"schema": "freecad-web-object-contract-matrix/v1",
"generatedAt": date.today().isoformat(),
"source": {"inventory": "docs/object-inventory.json", "inventorySha256": hashlib.sha256(source).hexdigest()},
"counts": {"inputRecords": len(rows), "canonicalRecords": len(records), "exactDuplicateRecords": len(rows) - len({(r["module"], r["name"]) for r in records}), "byAdapterFamily": dict(sorted(families.items())), "byModule": dict(sorted(modules.items()))},
"identity": "module + fully-qualified object name; evidence paths are not identity",
"policy": {"initialStatus": "planned", "guiOnly": "explicit-boundary", "persistence": "semantic FCStd comparison; never ZIP byte equality"},
"records": records,
}
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(json.dumps({"output": str(OUTPUT.relative_to(ROOT)), "records": len(records), "families": dict(sorted(families.items())), "sha256": hashlib.sha256(OUTPUT.read_bytes()).hexdigest()}, ensure_ascii=False))
if __name__ == "__main__":
main()