88 lines
3.8 KiB
Python
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()
|