#!/usr/bin/env python3 """Generate a truthful business-level completion audit for FreeCAD Web.""" from __future__ import annotations import hashlib import json from collections import Counter from datetime import datetime, timezone from pathlib import Path from business_acceptance import accepted_ids, requirements from failure_reconciliation import summary_current as failure_summary ROOT = Path(__file__).resolve().parents[1] OUTPUT = ROOT / "docs/current/web-business-completion-audit.json" MARKDOWN = ROOT / "docs/current/WEB_BUSINESS_COMPLETION_AUDIT.md" SOURCES = { "tasks": ROOT / "docs/current/task-state.json", "features": ROOT / "docs/current/feature-capability-registry.json", "receipts": ROOT / "docs/current/implementation-first-registry.json", "pythonApi": ROOT / "docs/p6/python-api-manifest.json", "objects": ROOT / "docs/p6/object-contract-matrix.json", "workbench": ROOT / "docs/p6/workbench-command-matrix.json", "formats": ROOT / "docs/p6/format-coverage-kernel.json", "fullParity": ROOT / "docs/p6/full-parity-gate.json", "modules": ROOT / "web/public/modules/manifest.json", "product": ROOT / "docs/current/product-business-backlog.json", "businessAcceptance": ROOT / "docs/current/business-acceptance.json", "failureReconciliation": ROOT / "docs/current/failure-reconciliation.json", } def read(path: Path) -> dict: try: value = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return {} return value if isinstance(value, dict) else {} def count(rows: list[object], key: str, default: str) -> dict[str, int]: values = Counter(str(row.get(key, default)) for row in rows if isinstance(row, dict)) return dict(sorted(values.items())) def source_entry(path: Path) -> dict[str, object]: if not path.is_file(): return {"path": str(path.relative_to(ROOT)), "present": False} return { "path": str(path.relative_to(ROOT)), "present": True, "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), } def main() -> int: data = {name: read(path) for name, path in SOURCES.items()} feature_rows = [row for row in data["features"].get("records", []) if isinstance(row, dict)] routed_ids = {str(row.get("id")) for row in feature_rows if row.get("implementationStatus") == "routed"} receipt_rows = data["receipts"].get("records", {}) if isinstance(data["receipts"].get("records"), dict) else {} receipt_states = {"implementation_ready", "implemented_pending_use"} all_receipted_ids = { str(record_id) for record_id, row in receipt_rows.items() if isinstance(row, dict) and str(row.get("status")) in receipt_states } receipted_ids = { str(record_id) for record_id, row in receipt_rows.items() if isinstance(row, dict) and str(row.get("status")) in receipt_states and str(record_id) in routed_ids } python_rows = [row for row in data["pythonApi"].get("records", []) if isinstance(row, dict)] python_counts = count(python_rows, "disposition", "planned") python_implementation_ids = { str(row.get("id")) for row in python_rows if row.get("disposition") in {"planned", "not_implemented"} } - all_receipted_ids python_boundary_ids = { str(row.get("id")) for row in python_rows if row.get("disposition") in {"restricted", "nonportable"} } - all_receipted_ids python_backlog_by_module = Counter( str(row.get("module") or "unknown") for row in python_rows if str(row.get("id")) in python_implementation_ids ) object_rows = [row for row in data["objects"].get("records", []) if isinstance(row, dict)] object_counts = count(object_rows, "status", "planned") workbench_rows = [row for row in data["workbench"].get("records", []) if isinstance(row, dict)] workbench_counts = count(workbench_rows, "disposition", "restricted") format_rows = [row for row in data["formats"].get("records", []) if isinstance(row, dict)] format_coverage = data["formats"].get("coverage", {}) if isinstance(data["formats"].get("coverage"), dict) else {} format_counts = format_coverage.get("counts", {}) if isinstance(format_coverage.get("counts"), dict) else {} parity_areas = [row for row in data["fullParity"].get("areas", []) if isinstance(row, dict)] task_counts = data["tasks"].get("counts", {}) if isinstance(data["tasks"].get("counts"), dict) else {} module_entries = data["modules"].get("modules", {}) module_count = len(module_entries) if isinstance(module_entries, (dict, list)) else 0 python_implementation_backlog = len(python_implementation_ids) python_boundary_backlog = len(python_boundary_ids) object_implementation_ids = { str(row.get("id")) for row in object_rows if row.get("status") in {"planned", "not_implemented", "blocked"} } - all_receipted_ids object_boundary_ids = { str(row.get("id")) for row in object_rows if row.get("status") in {"restricted", "passed_boundary"} } - all_receipted_ids workbench_ids = { str(row.get("id")) for row in workbench_rows if row.get("disposition") in {"restricted", "not_implemented"} } workbench_backlog = len(workbench_ids - all_receipted_ids) format_groups: dict[tuple[str, str, str], list[dict]] = {} for row in format_rows: if row.get("direction") not in {"import", "export"}: continue key = (str(row.get("module")), str(row.get("direction")), str(row.get("extension"))) format_groups.setdefault(key, []).append(row) format_pending = { f"business-format:{module}:{direction}:{extension}" for (module, direction, extension), rows in format_groups.items() if not any(row.get("status") == "passed" for row in rows) } - all_receipted_ids format_backlog = len(format_pending) product_rows = [row for row in data["product"].get("records", []) if isinstance(row, dict)] product_pending = {str(row.get("id")) for row in product_rows if row.get("id")} - all_receipted_ids object_implementation_backlog = len(object_implementation_ids) object_boundary_backlog = len(object_boundary_ids) parity_pending = sum(row.get("status") != "complete" for row in parity_areas) feature_receipt_backlog = len(routed_ids - receipted_ids) feature_route_backlog = len(feature_rows) - len(routed_ids) business_rows = requirements() accepted = accepted_ids(business_rows) acceptance_backlog = len(business_rows) - len(accepted) feature_by_module = Counter( str(row.get("module") or "unknown") for row in feature_rows if str(row.get("id")) not in receipted_ids ) python_boundary_by_module = Counter( str(row.get("module") or "unknown") for row in python_rows if str(row.get("id")) in python_boundary_ids ) object_by_module = Counter( str(row.get("module") or "unknown") for row in object_rows if str(row.get("id")) in object_implementation_ids | object_boundary_ids ) workbench_by_module = Counter( str(row.get("module") or "unknown") for row in workbench_rows if str(row.get("id")) in workbench_ids - all_receipted_ids ) format_by_module = Counter( module for (module, direction, extension), rows in format_groups.items() if f"business-format:{module}:{direction}:{extension}" in format_pending ) module_names = { *(str(row.get("module")) for row in feature_rows if row.get("module")), *(str(row.get("module")) for row in python_rows if row.get("module")), *(str(row.get("module")) for row in object_rows if row.get("module")), *(str(row.get("module")) for row in workbench_rows if row.get("module")), *(str(row.get("module")) for row in format_rows if row.get("module")), } priority = ["Part", "PartDesign", "Sketcher", "TechDraw", "Assembly", "BIM", "Draft", "Material", "Mesh", "Import", "Fem", "CAM"] module_rows = [] for module in sorted(module_names, key=lambda value: (priority.index(value) if value in priority else len(priority), value)): values = { "featureReceipts": feature_by_module[module], "workbench": workbench_by_module[module], "formats": format_by_module[module], "product": len(product_pending) if module == "Web" else 0, } # Workbench rows are the desktop-command subset of featureReceipts; # report them for diagnosis without double-counting module work. remaining = values["featureReceipts"] + values["formats"] + values["product"] module_rows.append({ "module": module, "status": "implementation_ready" if remaining == 0 else "pending", "remaining": remaining, **values, }) reconciliation = failure_summary() complete = all(( feature_route_backlog == 0, feature_receipt_backlog == 0, workbench_backlog == 0, format_backlog == 0, len(product_pending) == 0, module_count == 32, acceptance_backlog == 0, reconciliation["complete"], )) phases = [ { "id": "failure-reconciliation-alongside-business", "status": "complete" if reconciliation["complete"] else "in_progress", "remaining": reconciliation["pending"], "exit": "all current failure occurrences have attributable resolution or reviewed replacement evidence", }, { "id": "P1-feature-build-receipts", "status": "complete" if feature_receipt_backlog + feature_route_backlog == 0 else "in_progress", "remaining": feature_receipt_backlog + feature_route_backlog, "exit": "every routed user command has a target build or React-only implementation receipt", }, { "id": "P2-module-import-export-workflows", "status": "pending" if format_backlog else "complete", "remaining": format_backlog, "exit": "each source-declared user import/export extension is handled by its existing module dispatcher", }, { "id": "P3-react-product-integration", "status": "pending" if product_pending else "complete", "remaining": len(product_pending), "exit": "one primary React page integrates documents, workbenches, task/property panels, viewport, formats, Python console and recovery", }, { "id": "P4-workbench-route-reconciliation", "status": "tracked_with_P1" if workbench_backlog else "complete", "remaining": workbench_backlog, "exit": "desktop restricted commands are reconciled to built native/React routes; no source-only command claim remains", }, { "id": "P5-business-acceptance", "status": "pending" if acceptance_backlog else "complete", "remaining": acceptance_backlog, "exit": "business-specific input and result assertions pass against the current module source and artifact; reuse matching evidence", }, { "id": "validation-nonblocking", "status": "disabled", "remaining": 0, "exit": "historical API and full-parity gates are outside the business implementation queue", }, ] payload = { "schema": "freecad-web-business-completion-audit/v1", "generatedAt": datetime.now(timezone.utc).isoformat(), "verdict": "complete" if complete else "incomplete", "businessFunctionalComplete": complete, "failureReconciliation": reconciliation, "usableFoundation": { "dynamicWasmModules": module_count, "featureRoutes": {"routed": len(routed_ids), "total": len(feature_rows)}, "featureBuildReceipts": {"ready": len(receipted_ids), "total": len(routed_ids), "remaining": feature_receipt_backlog}, }, "businessProgress": { "completed": len(accepted), "total": len(business_rows), "remaining": acceptance_backlog, }, "implementationProgress": { "completed": len(receipted_ids) + (len(format_groups) - format_backlog) + (len(product_rows) - len(product_pending)), "total": len(feature_rows) + len(format_groups) + len(product_rows), "remaining": feature_route_backlog + feature_receipt_backlog + format_backlog + len(product_pending), }, "remaining": { "failureReconciliation": reconciliation["pending"], "featureRoutes": feature_route_backlog, "businessAcceptance": acceptance_backlog, "workbenchCommands": workbench_backlog, "formatWorkflows": format_backlog, "productWorkflows": len(product_pending), }, "traceabilityOnly": { "pythonApi": {"implementation": python_implementation_backlog, "restrictedOrNonportable": python_boundary_backlog}, "objectRows": {"implementation": object_implementation_backlog, "boundary": object_boundary_backlog}, "fullParityAreas": parity_pending, "historicalTaskCards": {key: int(task_counts.get(key, 0) or 0) for key in ("completed", "in_progress", "planned", "blocked")}, "blockingBusinessCompletion": False, }, "inventories": { "pythonApi": {"total": len(python_rows), "counts": python_counts, "implementationBacklogByModule": dict(python_backlog_by_module.most_common())}, "objects": {"total": len(object_rows), "counts": object_counts}, "workbench": {"total": len(workbench_rows), "counts": workbench_counts, "coveredByImplementationReceipts": len(workbench_ids & all_receipted_ids)}, "formats": {"total": int(format_coverage.get("inventoryRecords", 0) or 0), "counts": format_counts}, }, "modules": module_rows, "phases": phases, "rules": [ "route coverage, source classification, build receipts and runtime validation are separate states", "functional modules call FreeCAD internals directly; Python API inventory is not an implementation queue", "per-type and API inventory remain nonblocking source traceability", "business completion requires current input/result evidence; legacy API/full-parity gates do not drive implementation", "all Worker/native calls retain the single cmd+args JSON dispatcher contract", ], "sources": {name: source_entry(path) for name, path in SOURCES.items()}, } OUTPUT.parent.mkdir(parents=True, exist_ok=True) temporary = OUTPUT.with_suffix(".tmp") temporary.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") temporary.replace(OUTPUT) phase_lines = "\n".join( f"| {row['id']} | {row['status']} | {row['remaining']} | {row['exit']} |" for row in phases ) module_lines = "\n".join( f"| {row['module']} | {row['status']} | {row['featureReceipts']} | {row['workbench']} | {row['formats']} | {row['product']} | {row['remaining']} |" for row in module_rows ) markdown = f"""# FreeCAD Web 业务完成审计 生成时间:`{payload['generatedAt']}` 结论:**{'已完成' if complete else '未完成'}功能模块移植**。当前已有 {module_count} 个动态 WASM 模块,功能路由 {len(routed_ids)}/{len(feature_rows)}。以功能模块业务、功能路由带动全部移植;构建回执保留复用,业务完成需要与当前实现对应的输入和结果证据。API 数量和历史全量 gate 不进入实施队列。 ## 当前真实缺口 - 功能路由待构建回执:{feature_receipt_backlog} - 未接通或被入口阻断的功能路由:{feature_route_backlog} - 失败记录待业务闭环:{reconciliation['pending']};未分类或已变化记录:{reconciliation['unclassified']} - 业务结果待确认:{acceptance_backlog}(已有匹配证据 {len(accepted)};缺证据不等于已有实现被删除) - 工作台命令待 React/native 对账:{workbench_backlog}(属于 P1 子集,不重复计数) - 模块导入/导出工作流:{format_backlog} - React 主产品工作流:{len(product_pending)} 13,386 API 和逐对象矩阵只保留为源码导航资料;功能模块在 WASM 内直接调用 FreeCAD C++/Python 能力,不逐 API 公开或移植。验证与历史 Y gate 也不阻断本轮实现。 ## 连续实施顺序 | 阶段 | 状态 | 剩余 | 完成条件 | |---|---:|---:|---| {phase_lines} ## 模块业务缺口 模块直接复用 FreeCAD C++/Python 内部能力,不公开或逐条移植 API。 | 模块 | 状态 | 功能回执 | 工作台 | 格式 | 产品集成 | 合计 | |---|---:|---:|---:|---:|---:|---:| {module_lines} 后续按模块完成用户业务、格式工作流和 React 产品集成,并复用或补齐对应业务结果证据;不安排逐 API 移植。结构化事实见 `docs/current/web-business-completion-audit.json`。 """ markdown_temporary = MARKDOWN.with_suffix(".tmp") markdown_temporary.write_text(markdown, encoding="utf-8") markdown_temporary.replace(MARKDOWN) print(json.dumps({"status": "generated", "verdict": payload["verdict"], "featureReceiptBacklog": feature_receipt_backlog, "workbenchCommands": workbench_backlog, "formatWorkflows": format_backlog, "productWorkflows": len(product_pending)}, ensure_ascii=False)) return 0 if __name__ == "__main__": raise SystemExit(main())