#!/usr/bin/env python3 """Generate a compact, source-backed Web parity matrix by module.""" from __future__ import annotations import argparse import json import subprocess from collections import defaultdict from pathlib import Path 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 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("--object-inventory", type=Path, required=True) parser.add_argument("--format-api-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() state_path = args.json_output.parent / "current" / "task-state.json" state_modules = {} if state_path.exists(): state_modules = json.loads(state_path.read_text(encoding="utf-8")).get("module_status", {}) previous_modules = {} if args.json_output.exists(): previous = json.loads(args.json_output.read_text(encoding="utf-8")) previous_modules = { row.get("module"): row.get("web_status", "pending") for row in previous.get("rows", []) } workbenches = json.loads(args.workbench_inventory.read_text(encoding="utf-8"))["modules"] objects = json.loads(args.object_inventory.read_text(encoding="utf-8"))["objects"] format_api = json.loads(args.format_api_inventory.read_text(encoding="utf-8")) objects_by_module: dict[str, list[str]] = defaultdict(list) for item in objects: objects_by_module[str(item["module"])].append(str(item["name"])) api_by_module: dict[str, int] = defaultdict(int) for item in format_api["python_api"]: api_by_module[str(item["module"])] += 1 extensions = sorted({str(item["extension"]) for item in format_api["formats"] if "extension" in item}) formats_by_module: dict[str, set[str]] = defaultdict(set) for item in format_api["formats"]: if "extension" in item: formats_by_module[str(item["module"])].add(str(item["extension"])) rows = [] for module in workbenches: module_id = str(module["id"]) module_objects = sorted(set(objects_by_module.get(module_id, []))) actions = ["inspect/source-inventory"] if module.get("has_app"): actions.extend(["create/recompute", "save/reopen"]) if module_id in {"Import", "Mesh", "Part", "TechDraw", "BIM", "CAM"}: actions.append("import/export") if api_by_module.get(module_id): actions.append("python-api") rows.append( { "module": module_id, "user_actions": sorted(set(actions)), "object_type_count": len(module_objects), "object_types": module_objects, "format_extensions": sorted(formats_by_module.get(module_id, set())), "python_api_count": api_by_module.get(module_id, 0), "gui_dependency": bool(module.get("has_gui")), "web_status": state_modules.get(module_id, previous_modules.get(module_id, "pending")), "source_evidence": { "workbench": str(args.workbench_inventory), "objects": str(args.object_inventory), "format_api": str(args.format_api_inventory), }, "regression_entry": "FreeCAD/docs/tasks/FW-P3-001B.md", } ) report = { "schema": "freecad-web-parity-matrix/v1", "source_revision": revision(source_root), "status_values": ["pending", "in_progress", "blocked", "completed"], "module_count": len(rows), "rows": 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 Parity Matrix", "", f"- Source revision: `{report['source_revision']}`", f"- Module rows: `{len(rows)}`", "- Web status is projected from the task-state ledger; `pending` means no complete module gate exists.", "", "| Module | User actions | Objects | Formats | Python API | GUI | Web status | Regression entry |", "|---|---|---:|---|---:|---:|---|---|", ] for row in rows: lines.append( f"| `{row['module']}` | {', '.join(row['user_actions'])} | {row['object_type_count']} | " f"{', '.join(f'`{x}`' for x in row['format_extensions']) or '-'} | {row['python_api_count']} | " f"{'yes' if row['gui_dependency'] else 'no'} | `{row['web_status']}` | `{row['regression_entry']}` |" ) args.markdown_output.write_text("\n".join(lines) + "\n", encoding="utf-8") print(json.dumps({ "schema": report["schema"], "module_count": report["module_count"], "pending": sum(row["web_status"] == "pending" for row in rows), })) return 0 if __name__ == "__main__": raise SystemExit(main())