#!/usr/bin/env python3 """Generate the bounded source-backed Full Parity audit baseline.""" from __future__ import annotations import json import subprocess from collections import Counter from datetime import date from pathlib import Path ROOT = Path(__file__).resolve().parents[1] DOCS = ROOT / "docs" def revision() -> str: try: return subprocess.run( ["git", "-C", str(ROOT / "freecad-source"), "rev-parse", "HEAD"], check=True, capture_output=True, text=True, ).stdout.strip() except (OSError, subprocess.CalledProcessError): return "unknown" workbenches = json.loads((DOCS / "workbench-inventory.json").read_text()) objects = json.loads((DOCS / "object-inventory.json").read_text()) format_api = json.loads((DOCS / "format-api-inventory.json").read_text()) matrix = json.loads((DOCS / "web-parity-matrix.json").read_text()) modules = workbenches["modules"] object_rows = objects["objects"] format_rows = format_api["formats"] api_rows = format_api["python_api"] extensions = sorted({str(item["extension"]) for item in format_rows if item.get("extension")}) module_status = {row["module"]: row.get("web_status", "pending") for row in matrix["rows"]} web_counts = { "htmlEntrypoints": len(list(ROOT.joinpath("web").glob("*.html"))), "workers": len(list((ROOT / "web" / "src").glob("*.worker.ts"))), "facades": len(list((ROOT / "web" / "src").glob("*facade.ts"))), "nativeProbeSources": len(list((ROOT / "freecad-source" / "src" / "WebAssembly" / "FreeCADCoreProbe").glob("*Probe.cpp"))), } baseline = { "sourceModules": len(modules), "appModules": sum(bool(item.get("has_app")) for item in modules), "guiModules": sum(bool(item.get("has_gui")) for item in modules), "guiOnlyModules": sum(bool(item.get("has_gui")) and not bool(item.get("has_app")) for item in modules), "noAppNoGuiModules": sum(not bool(item.get("has_gui")) and not bool(item.get("has_app")) for item in modules), "objectTypes": len(object_rows), "objectModules": len({str(item["module"]) for item in object_rows}), "formatRecords": len(format_rows), "uniqueFormatExtensions": len(extensions), "pythonApiRecords": len(api_rows), "pythonApiModules": len({str(item["module"]) for item in api_rows}), "web": web_counts, "p6ModuleGatesCompleted": sum(status == "completed" for status in module_status.values()), } gaps = [ { "id": "FPA-CORE", "area": "core-shell", "priority": "P0", "status": "planned", "evidence": ["web/src/main.ts", "docs/current/FREECAD_WEB_MIGRATION_ANALYSIS.md"], "scope": "统一 React 工作区、Document actor、命令/错误/取消/进度、树/属性/选择/视图状态。", "exit": "FW-P6-FPA-B + FW-P6-FPA-C", }, { "id": "FPA-DOC", "area": "document-persistence", "priority": "P0", "status": "planned", "evidence": ["docs/p6/sketcher-action-04-kernel.json", "docs/p6/techdraw-action-05-persistence-native.json"], "scope": "多对象图、跨模块链接、属性、重算、FCStd、Undo/Redo、事务和恢复。", "exit": "FW-P6-FPA-C", }, { "id": "FPA-OBJ", "area": "object-coverage", "priority": "P0", "status": "planned", "count": len(object_rows), "evidence": ["docs/object-inventory.json", "docs/web-parity-matrix.json"], "scope": "逐对象 native create/recompute/inspect/property/view/persistence 适用性。", "exit": "FW-P6-FPA-D", }, { "id": "FPA-FMT", "area": "format-coverage", "priority": "P0", "status": "planned", "count": len(format_rows), "evidence": ["docs/format-api-inventory.json"], "scope": "逐记录 handler、参数、错误、恢复、互操作和浏览器回放。", "exit": "FW-P6-FPA-E", }, { "id": "FPA-PY", "area": "python-api", "priority": "P0", "status": "planned", "count": len(api_rows), "evidence": ["docs/format-api-inventory.json", "docs/current/FREECAD_WEB_MIGRATION_ANALYSIS.md"], "scope": "完整 API 分类、binding、权限、参数/返回值、错误和受限执行。", "exit": "FW-P6-FPA-F", }, { "id": "FPA-GUI", "area": "workbench-gui", "priority": "P0", "status": "planned", "count": sum(bool(item.get("has_gui")) for item in modules), "evidence": ["docs/workbench-inventory.json", "freecad-source/src/Gui"], "scope": "32 模块、22 GUI 模块的命令、任务面板、资源、选择和视图行为。", "exit": "FW-P6-FPA-G", }, { "id": "FPA-UX", "area": "rendering-interaction", "priority": "P1", "status": "planned", "evidence": ["web/src/main.ts", "web/package.json"], "scope": "Three.js 渲染、选择/预选、相机、标准视图、属性编辑、快捷键、响应式和可访问性。", "exit": "FW-P6-FPA-H", }, { "id": "FPA-QUAL", "area": "quality-release", "priority": "P1", "status": "planned", "evidence": ["docs/current/FREECAD_WEB_MIGRATION_ANALYSIS.md", "tools/run_p6_chrome_harness.mjs"], "scope": "性能、内存、CSP/权限、无障碍、兼容性和发布 gate。", "exit": "FW-P6-FPA-I", }, ] report = { "schema": "freecad-web-full-parity-audit/v1", "generatedAt": date.today().isoformat(), "sourceRevision": revision(), "truth": "P6 module completed means bounded slice gate; Full Parity remains planned until every audit record has a conclusion.", "baseline": baseline, "moduleStatus": module_status, "formatExtensions": extensions, "gaps": gaps, "roadmap": [ {"task": "FW-P6-FPA-A", "area": "audit", "status": "in_progress"}, {"task": "FW-P6-FPA-B", "area": "core-shell", "status": "planned"}, {"task": "FW-P6-FPA-C", "area": "document-persistence", "status": "planned"}, {"task": "FW-P6-FPA-D", "area": "object-coverage", "status": "planned"}, {"task": "FW-P6-FPA-E", "area": "format-coverage", "status": "planned"}, {"task": "FW-P6-FPA-F", "area": "python-api", "status": "planned"}, {"task": "FW-P6-FPA-G", "area": "workbench-gui", "status": "planned"}, {"task": "FW-P6-FPA-H", "area": "rendering-interaction", "status": "planned"}, {"task": "FW-P6-FPA-I", "area": "quality-release", "status": "planned"}, {"task": "FW-P6-FPA-Y", "area": "full-parity-gate", "status": "planned"}, ], } (DOCS / "p6" / "full-parity-audit.json").write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n") lines = [ "# FreeCAD Web 完整对标审计", "", f"- 源码 revision:`{report['sourceRevision']}`", "- 当前结论:`planned`;P6 模块 gate 不是 Full Parity 结论。", f"- 模块:{baseline['sourceModules']}(App {baseline['appModules']},GUI {baseline['guiModules']})。", f"- 对象:{baseline['objectTypes']} 个,覆盖 {baseline['objectModules']} 个模块。", f"- 格式:{baseline['formatRecords']} 条记录,{baseline['uniqueFormatExtensions']} 个扩展名。", f"- Python:{baseline['pythonApiRecords']} 条 API 记录,覆盖 {baseline['pythonApiModules']} 个模块。", f"- Web 资产:{web_counts['htmlEntrypoints']} 个入口页、{web_counts['workers']} 个 Worker、{web_counts['facades']} 个 Facade、{web_counts['nativeProbeSources']} 个 native probe 源文件。", "", "## 关键差距", "", ] for gap in gaps: lines.append(f"- `{gap['id']}` `{gap['priority']}` `{gap['area']}`:{gap['scope']} 出口 `{gap['exit']}`。") lines.extend([ "", "## 连续队列", "", "`FW-P6-FPA-A → B → C → (D/E/F) → G → H → I → Y`。每张卡独立上下文、独立证据和独立恢复卡;任意未实现记录都会阻止 Y gate。", ]) (DOCS / "FREECAD_WEB_FULL_PARITY_AUDIT.md").write_text("\n".join(lines) + "\n") print(json.dumps({"schema": report["schema"], "sourceRevision": report["sourceRevision"], "gaps": len(gaps), "objectTypes": baseline["objectTypes"], "formatRecords": baseline["formatRecords"], "pythonApiRecords": baseline["pythonApiRecords"]}, ensure_ascii=False))