#!/usr/bin/env python3 """Generate a source-backed FreeCAD 1.2 Document API contract inventory.""" from __future__ import annotations import argparse import hashlib import json import re import subprocess from pathlib import Path CONTRACTS = { "Application": { "path": "src/App/Application.h", "patterns": [ r"\bnewDocument\s*\(", r"\bcloseDocument\s*\(", r"\bgetDocument\s*\(", r"\bcommitTransaction\s*\(", r"\babortTransaction\s*\(", ], }, "Document": { "path": "src/App/Document.h", "patterns": [ r"\baddObject\s*\(", r"\bgetObject\s*\(", r"\bopenTransaction\s*\(", r"\bcommitTransaction\s*\(", r"\babortTransaction\s*\(", r"\brecompute\s*\(", r"\bgetObjectList\s*\(", r"\bgetDependencyList\s*\(", ], }, "DocumentObject": { "path": "src/App/DocumentObject.h", "patterns": [ r"\bgetNameInDocument\s*\(", r"\bgetFullName\s*\(", r"\bgetID\s*\(", r"\bisTouched\s*\(", r"\bisError\s*\(", r"\bgetInList\s*\(", r"\bgetOutList\s*\(", ], }, "PropertyContainer": { "path": "src/App/PropertyContainer.h", "patterns": [ r"\baddProperty\s*\(", r"\bgetPropertyByName\s*\(", r"\bgetPropertyList\s*\(", ], }, "PartFeature": { "path": "src/Mod/Part/App/PartFeature.h", "patterns": [ r"\bPropertyPartShape\s+Shape\b", r"\bgetShape\s*\(", r"\bgetTopoShape\s*\(", r"\bbuildHistory\s*\(", ], }, } def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def collect_contract(source_root: Path, name: str, spec: dict[str, object]) -> dict[str, object]: relative = str(spec["path"]) path = source_root / relative text = path.read_text(encoding="utf-8", errors="replace") lines = text.splitlines() matches: list[dict[str, object]] = [] for pattern in spec["patterns"]: # type: ignore[union-attr] regex = re.compile(str(pattern)) for number, line in enumerate(lines, start=1): if regex.search(line): matches.append({"pattern": str(pattern), "line": number, "text": line.strip()}) return { "name": name, "path": relative, "sha256": sha256(path), "patterns": list(spec["patterns"]), # type: ignore[arg-type] "matches": matches, "match_count": len(matches), } def render_markdown(report: dict[str, object]) -> str: lines = [ "# FreeCAD 1.2 Document Contract", "", "> This is a source inventory, not a claim that the full Document runtime is available in WebAssembly.", "", f"- Schema: `{report['schema']}`", f"- Source root: `{report['source_root']}`", f"- Source revision: `{report['source_revision']}`", "", "| Contract | Source | Matches | SHA-256 |", "| --- | --- | ---: | --- |", ] for contract in report["contracts"]: # type: ignore[union-attr] lines.append( f"| `{contract['name']}` | `{contract['path']}` | {contract['match_count']} | `{contract['sha256']}` |" ) lines.extend(["", "## Matched declarations", ""]) for contract in report["contracts"]: # type: ignore[union-attr] lines.extend([f"### {contract['name']}", ""]) for match in contract["matches"]: # type: ignore[union-attr] lines.append(f"- L{match['line']}: `{match['text']}`") lines.append("") return "\n".join(lines) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", type=Path, required=True) parser.add_argument("--json-output", type=Path, required=True) parser.add_argument("--markdown-output", type=Path, required=True) args = parser.parse_args() source_root = args.source_root.resolve() revision = "unknown" try: revision = subprocess.run( ["git", "-C", str(source_root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, ).stdout.strip() except (OSError, subprocess.CalledProcessError): pass contracts = [collect_contract(source_root, name, spec) for name, spec in CONTRACTS.items()] report = { "schema": "freecad-web-document-contract/v1", "source_root": str(source_root), "source_revision": revision, "contracts": contracts, } args.json_output.parent.mkdir(parents=True, exist_ok=True) args.json_output.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") args.markdown_output.write_text(render_markdown(report), encoding="utf-8") print(json.dumps({"schema": report["schema"], "contracts": len(contracts), "json": str(args.json_output), "markdown": str(args.markdown_output)})) return 0 if __name__ == "__main__": raise SystemExit(main())