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

155 lines
5.1 KiB
Python

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