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

347 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())