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

122 lines
5.7 KiB
Python

"""Business-scoped acceptance, separate from reusable implementation receipts."""
from __future__ import annotations
import hashlib
import json
from functools import lru_cache
from pathlib import Path
from urllib.parse import urlsplit
ROOT = Path(__file__).resolve().parents[1]
LEDGER = ROOT / "docs/current/business-acceptance.json"
def read(path: Path) -> dict:
try:
value = json.loads(path.read_text(encoding="utf-8"))
return value if isinstance(value, dict) else {}
except (OSError, ValueError):
return {}
def requirements() -> list[dict]:
rows = list(read(ROOT / "docs/current/feature-capability-registry.json").get("records", []))
formats = {}
for row in read(ROOT / "docs/p6/format-coverage-kernel.json").get("records", []):
if row.get("direction") in {"import", "export"}:
record_id = f"business-format:{row['module']}:{row['direction']}:{row['extension']}"
passed = row.get("status") == "passed" or formats.get(record_id, {}).get("status") == "passed"
formats[record_id] = {**row, "id": record_id, "command": f"{row['direction']} {row['extension']}",
"sourcePath": row.get("path"), "sourceLine": row.get("line", 1),
"status": "passed" if passed else row.get("status")}
rows.extend(formats.values())
rows.extend({**row, "module": "Web", "command": row["name"]}
for row in read(ROOT / "docs/current/product-business-backlog.json").get("records", []))
return rows
@lru_cache(maxsize=512)
def digest(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else "missing"
@lru_cache(maxsize=64)
def module_inputs(module: str) -> dict:
slug = "".join(character for character in module.lower() if character.isalnum())
paths = set((ROOT / "web/src").glob(f"{slug}-*.ts*"))
paths.update((ROOT / "web/src").glob(f"{slug}.*.ts"))
modules = read(ROOT / "web/public/modules/manifest.json").get("modules", {})
def owns(path: Path) -> bool:
matches = [name for name in modules if path.stem.lower().startswith(name.lower())]
return max(matches, key=len, default=module) == module and path.stem.lower().startswith(slug)
paths.update(path for path in (ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe").glob("*.*") if owns(path))
artifact = modules.get(module, {})
for worker in [artifact, *artifact.get("workers", {}).values()] if isinstance(artifact, dict) else []:
runtime = worker.get("runtimeUrl")
if isinstance(runtime, str):
stem = Path(urlsplit(runtime).path).stem.removeprefix("FreeCADWeb")
source = ROOT / "freecad-source/src/WebAssembly/FreeCADCoreProbe" / f"{stem}.cpp"
if source.is_file():
paths.add(source)
paths.update(ROOT / f"web/src/{name}" for name in (
"module-router.ts", "native-source-policy.ts", "desktop-command-dispatcher.ts", "facade-envelope.ts"))
if module == "Web":
paths.update((ROOT / "web/src").glob("*-shell.tsx"))
paths.update((ROOT / "web/src").glob("*-desktop-adapter.ts"))
return {
"sources": {str(path.relative_to(ROOT)): digest(path) for path in sorted(paths) if path.is_file()},
"artifact": read(ROOT / "web/public/modules/manifest.json").get("modules", {}).get(module),
}
def fingerprint(row: dict) -> str:
route = row.get("route") or {}
module = (route.get("module") if isinstance(route, dict) else None) or row["module"]
source = str(row.get("sourcePath") or row.get("source") or "")
source_path = ROOT / source
if source and not source_path.is_file():
source_path = ROOT / "freecad-source" / source
value = {"id": row["id"], "route": route, "inputs": module_inputs(module), "source": digest(source_path)}
return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest()
def successful_result(result: dict) -> bool:
cases = result.get("cases", [])
return bool(result.get("status") == "passed" and cases and all(
isinstance(case, dict) and "input" in case and case.get("assertions") and all(
isinstance(item, dict) and item.get("name") and item.get("passed") is True
for item in case["assertions"])
for case in cases))
def accepted_ids(rows: list[dict] | None = None) -> set[str]:
accepted = set()
evidence_cache = {}
entries = read(LEDGER).get("records", {})
for row in rows if rows is not None else requirements():
if "implementationStatus" in row and row["implementationStatus"] != "routed":
continue
entry = entries.get(row["id"], {})
if entry.get("status") != "passed" or entry.get("fingerprint") != fingerprint(row):
continue
path = (ROOT / str(entry.get("evidencePath", ""))).resolve()
if not path.is_relative_to(ROOT) or not path.is_file() or digest(path) != entry.get("evidenceSha256"):
continue
if path not in evidence_cache:
report = read(path)
evidence_cache[path] = {item.get("capabilityId"): item for item in report.get("results", [])} if report.get("schema") == "freecad-web-business-cases/v1" else {}
if successful_result(evidence_cache[path].get(row["id"], {})):
accepted.add(row["id"])
return accepted
def business_complete() -> bool:
from failure_reconciliation import summary_current
audit = read(ROOT / "docs/current/web-business-completion-audit.json")
if audit.get("businessFunctionalComplete") is not True:
return False
digest.cache_clear()
module_inputs.cache_clear()
rows = requirements()
return bool(rows) and len(accepted_ids(rows)) == len(rows) and summary_current()["complete"]