#!/usr/bin/env python3 """Inventory source-backed FreeCAD object/type registration evidence.""" from __future__ import annotations import argparse import hashlib import json import re import subprocess from collections import defaultdict from pathlib import Path SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"} TYPE_PATTERNS = { "typesystem": re.compile(r"TYPESYSTEM_SOURCE(?:_WITH_OVERRIDE)?\s*\(\s*([A-Za-z_][\w:]*)"), "property_header": re.compile(r"PROPERTY_HEADER(?:_WITH_OVERRIDE)?\s*\(\s*([A-Za-z_][\w:]*)"), "add_object": re.compile(r"\b(?:addObject|addObjectWithName)\s*\(\s*\"([A-Za-z_][\w:]*)\""), } def revision(source_root: Path) -> str: try: return subprocess.run( ["git", "-C", str(source_root), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, ).stdout.strip() except (OSError, subprocess.CalledProcessError): return "unknown" def sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def collect(source_root: Path, module: str) -> list[dict[str, object]]: app_root = source_root / "src/Mod" / module / "App" evidence: dict[str, list[dict[str, object]]] = defaultdict(list) if not app_root.is_dir(): return [] for path in sorted(p for p in app_root.rglob("*") if p.is_file() and p.suffix in SOURCE_SUFFIXES): text = path.read_text(encoding="utf-8", errors="replace") lines = text.splitlines() for kind, pattern in TYPE_PATTERNS.items(): for line_number, line in enumerate(lines, start=1): for match in pattern.finditer(line): name = match.group(1) evidence[name].append( { "kind": kind, "path": str(path.relative_to(source_root)), "line": line_number, "text": line.strip(), } ) rows = [] for name, matches in sorted(evidence.items()): rows.append({"name": name, "module": module, "evidence": matches}) return rows def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--source-root", type=Path, required=True) parser.add_argument("--workbench-inventory", 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() workbench = json.loads(args.workbench_inventory.read_text(encoding="utf-8")) rows: list[dict[str, object]] = [] for module in workbench["modules"]: rows.extend(collect(source_root, str(module["id"]))) names = {(row["module"], row["name"]) for row in rows} report = { "schema": "freecad-web-object-inventory/v1", "source_root": str(source_root), "source_revision": revision(source_root), "workbench_inventory": str(args.workbench_inventory), "object_count": len(rows), "unique_name_count": len({row["name"] for row in rows}), "duplicate_module_name_count": len(rows) - len(names), "objects": rows, } 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") lines = [ "# FreeCAD Web Object Inventory", "", f"- Source revision: `{report['source_revision']}`", f"- Object evidence rows: `{report['object_count']}`", f"- Unique names: `{report['unique_name_count']}`", "", "| Module | Object/type name | Evidence count | First evidence |", "|---|---|---:|---|", ] for row in rows: first = row["evidence"][0] lines.append( f"| `{row['module']}` | `{row['name']}` | {len(row['evidence'])} | " f"`{first['path']}:{first['line']}` ({first['kind']}) |" ) args.markdown_output.write_text("\n".join(lines) + "\n", encoding="utf-8") print(json.dumps({"schema": report["schema"], "object_count": report["object_count"], "unique_name_count": report["unique_name_count"]})) return 0 if __name__ == "__main__": raise SystemExit(main())