#!/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()