Files
FreeCADWasmDev/tools/generate_web_business_completion_audit.py

365 lines
19 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
from reconcile_business_acceptance import reconcile, write_report
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_report = reconcile(business_rows)
write_report(acceptance_report)
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,
"businessTotal": sum(row["module"] == module for row in business_rows),
"businessAccepted": sum(row["module"] == module and row["id"] in accepted for row in business_rows),
"businessPending": sum(row["module"] == module and row["id"] not in accepted for row in business_rows),
**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 has an implementation receipt; business acceptance remains P5",
},
{
"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,
"counts": acceptance_report["counts"],
"executable": acceptance_report["executable"],
"missingCases": acceptance_report["missingCases"],
},
"manualCoverage": acceptance_report["manualCoverage"],
"acceptanceReconciliation": {
"complete": acceptance_report["reconciliationComplete"],
"integrity": acceptance_report["integrity"],
"report": "docs/current/business-acceptance-reconciliation.json",
"note": "Accounting for every item does not imply business acceptance passed.",
},
"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['businessAccepted']}/{row['businessTotal']} | {row['businessPending']} |"
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)};缺证据不等于已有实现被删除)
- 业务证据分类:{json.dumps(acceptance_report['counts'], ensure_ascii=False)};缺少可执行业务用例:{acceptance_report['missingCases']}
- 手册样例累计结果:{json.dumps(acceptance_report['manualCoverage']['counts'], ensure_ascii=False)}。手册与业务项通过显式映射对账,不能从入口检查推导命令通过;逐项原因见 `business-acceptance-reconciliation.json`。
- 工作台命令待 React/native 对账:{workbench_backlog}(属于 P1 子集,不重复计数)
- 模块导入/导出实现回执缺口:{format_backlog}(不代表格式业务验收通过)
- React 主产品实现回执缺口:{len(product_pending)}(不代表产品业务验收通过)
13,386 API 和逐对象矩阵只保留为源码导航资料;功能模块在 WASM 内直接调用 FreeCAD C++/Python 能力,不逐 API 公开或移植。本轮自动执行业务验收;历史逐 API/Full Parity 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())