347 lines
17 KiB
Python
347 lines
17 KiB
Python
#!/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())
|